Skip to content

Commit d2a151f

Browse files
committed
darwin: cli install/uninstall
1 parent 771eedb commit d2a151f

3 files changed

Lines changed: 215 additions & 2 deletions

File tree

extensions/shellscript/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
"languages": [{
88
"id": "shellscript",
99
"aliases": ["Shell Script (Bash)", "shellscript"],
10-
"extensions": [".sh", ".bash", ".zsh", ".bashrc", ".bash_profile", ".bash_login", ".profile", ".bash_logout"],
10+
"extensions": [".sh", ".bash", ".bashrc", ".bash_profile", ".bash_login", ".profile", ".bash_logout", ".zsh", ".zshrc"],
1111
"firstLine": "^#!.*\\b(bash|zsh|sh|tcsh)|^#\\s*-\\*-[^*]*mode:\\s*shell-script[^*]*-\\*-",
12-
"configuration": "./shellscript.configuration.json"
12+
"configuration": "./shellscript.configuration.json",
13+
"mimetypes": ["text/x-shellscript"]
1314
}],
1415
"grammars": [{
1516
"language": "shellscript",
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as nls from 'vs/nls';
7+
import * as path from 'path';
8+
import * as fs from 'fs';
9+
import * as os from 'os';
10+
import * as cp from 'child_process';
11+
import * as pfs from 'vs/base/node/pfs';
12+
import { nfcall } from 'vs/base/common/async';
13+
import { TPromise } from 'vs/base/common/winjs.base';
14+
import URI from 'vs/base/common/uri';
15+
import { Action } from 'vs/base/common/actions';
16+
import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actionRegistry';
17+
import { IWorkbenchContributionsRegistry, IWorkbenchContribution, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
18+
import { Registry } from 'vs/platform/platform';
19+
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
20+
import { IWorkspaceContextService } from 'vs/workbench/services/workspace/common/contextService';
21+
import { IMessageService, Severity } from 'vs/platform/message/common/message';
22+
import { IEditorService } from 'vs/platform/editor/common/editor';
23+
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
24+
25+
function ignore<T>(code: string, value: T = null): (err: any) => TPromise<T> {
26+
return err => err.code === code ? TPromise.as<T>(value) : TPromise.wrapError<T>(err);
27+
}
28+
29+
const root = URI.parse(require.toUrl('')).fsPath;
30+
const source = path.resolve(root, '..', 'bin', 'code');
31+
const isAvailable = fs.existsSync(source);
32+
33+
class InstallAction extends Action {
34+
35+
static ID = 'workbench.action.installCommandLine';
36+
static LABEL = nls.localize('install', 'Install in PATH');
37+
38+
constructor(
39+
id: string,
40+
label: string,
41+
@IWorkspaceContextService private contextService: IWorkspaceContextService,
42+
@IMessageService private messageService: IMessageService,
43+
@IEditorService private editorService: IEditorService
44+
) {
45+
super(id, label);
46+
}
47+
48+
private get applicationName(): string {
49+
return this.contextService.getConfiguration().env.applicationName;
50+
}
51+
52+
private get target(): string {
53+
return `/usr/local/bin/${ this.applicationName }`;
54+
}
55+
56+
run(): TPromise<void> {
57+
return this.checkLegacy()
58+
.then(files => {
59+
if (files.length > 0) {
60+
const file = files[0];
61+
const resource = URI.create('file', null, file);
62+
const message = nls.localize('exists', "Please remove the 'code' alias in '{0}' and retry this action.", file);
63+
const input = { resource, mime: 'text/x-shellscript' };
64+
const actions = [
65+
new Action('inlineEdit', nls.localize('editFile', "Edit '{0}'", file), '', true, () => {
66+
return this.editorService.openEditor(input).then(() => {
67+
const message = nls.localize('again', "Once you remove the 'code' alias, you can retry the PATH installation.");
68+
const actions = [
69+
new Action('cancel', nls.localize('cancel', "Cancel")),
70+
new Action('yes', nls.localize('retry', "Retry"), '', true, () => this.run())
71+
];
72+
73+
this.messageService.show(Severity.Info, { message, actions });
74+
});
75+
})
76+
];
77+
78+
this.messageService.show(Severity.Warning, { message, actions });
79+
return TPromise.as(null);
80+
}
81+
82+
return this.isInstalled()
83+
.then(isInstalled => {
84+
if (!isAvailable || isInstalled) {
85+
return TPromise.as(null);
86+
} else {
87+
const createSymlink = () => {
88+
return pfs.unlink(this.target)
89+
.then(null, ignore('ENOENT'))
90+
.then(() => pfs.symlink(source, this.target));
91+
};
92+
93+
return createSymlink().then(null, err => {
94+
if (err.code === 'EACCES' || err.code === 'ENOENT') {
95+
return this.createBinFolder()
96+
.then(() => createSymlink());
97+
}
98+
99+
return TPromise.wrapError(err);
100+
});
101+
}
102+
})
103+
.then(() => this.messageService.show(Severity.Info, nls.localize('success', 'Shortcut \'{0}\' successfully installed in PATH.', this.applicationName)));
104+
});
105+
}
106+
107+
private isInstalled(): TPromise<boolean> {
108+
return pfs.lstat(this.target)
109+
.then(stat => stat.isSymbolicLink())
110+
.then(() => pfs.readlink(this.target))
111+
.then(link => link === source)
112+
.then(null, ignore('ENOENT', false));
113+
}
114+
115+
private createBinFolder(): TPromise<void> {
116+
const command = 'osascript -e "do shell script \\"mkdir -p /usr/local/bin && chown \\" & (do shell script (\\"whoami\\")) & \\" /usr/local/bin\\" with administrator privileges"';
117+
118+
return nfcall(cp.exec, command, {})
119+
.then(null, _ => TPromise.wrapError(new Error(nls.localize('cantCreateBinFolder', "Unable to create '/usr/local/bin'."))));
120+
}
121+
122+
public checkLegacy(): TPromise<string[]> {
123+
const readOrEmpty = name => pfs.readFile(name, 'utf8')
124+
.then(null, ignore('ENOENT', ''));
125+
126+
const files = [
127+
path.join(os.homedir(), '.bash_profile'),
128+
path.join(os.homedir(), '.bashrc'),
129+
path.join(os.homedir(), '.zshrc')
130+
];
131+
132+
return TPromise.join(files.map(f => readOrEmpty(f))).then(result => {
133+
return result.reduce((result, contents, index) => {
134+
const env = this.contextService.getConfiguration().env;
135+
136+
if (contents.indexOf(env.darwinBundleIdentifier) > -1) {
137+
result.push(files[index]);
138+
}
139+
140+
return result;
141+
}, []);
142+
});
143+
}
144+
}
145+
146+
class UninstallAction extends Action {
147+
148+
static ID = 'workbench.action.uninstallCommandLine';
149+
static LABEL = nls.localize('uninstall', 'Uninstall from PATH');
150+
151+
constructor(
152+
id: string,
153+
label: string,
154+
@IWorkspaceContextService private contextService: IWorkspaceContextService,
155+
@IMessageService private messageService: IMessageService
156+
) {
157+
super(id, label);
158+
}
159+
160+
private get applicationName(): string {
161+
return this.contextService.getConfiguration().env.applicationName;
162+
}
163+
164+
private get target(): string {
165+
return `/usr/local/bin/${ this.applicationName }`;
166+
}
167+
168+
run(): TPromise<void> {
169+
return pfs.unlink(this.target)
170+
.then(null, ignore('ENOENT'))
171+
.then(() => this.messageService.show(Severity.Info, nls.localize('success', 'Shortcut \'{0}\' successfully uninstalled from PATH.', this.applicationName)))
172+
}
173+
}
174+
175+
class DarwinCLIHelper implements IWorkbenchContribution {
176+
177+
constructor(
178+
@IInstantiationService instantiationService: IInstantiationService,
179+
@IMessageService messageService: IMessageService
180+
) {
181+
const installAction = instantiationService.createInstance(InstallAction, InstallAction.ID, InstallAction.LABEL);
182+
183+
installAction.checkLegacy().done(files => {
184+
if (files.length > 0) {
185+
const message = nls.localize('update', "Code needs to update the command line launcher. Would you like to do this now?");
186+
const actions = [
187+
new Action('later', nls.localize('now', "Later")),
188+
new Action('now', nls.localize('now', "Update Now"), '', true, () => installAction.run())
189+
];
190+
191+
messageService.show(Severity.Info, { message, actions });
192+
}
193+
});
194+
}
195+
196+
getId(): string {
197+
return 'darwin.cli';
198+
}
199+
}
200+
201+
if (isAvailable && process.platform === 'darwin') {
202+
const category = nls.localize('commandLine', "Command Line");
203+
204+
const workbenchActionsRegistry = <IWorkbenchActionRegistry>Registry.as(ActionExtensions.WorkbenchActions);
205+
workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(InstallAction, InstallAction.ID, InstallAction.LABEL), category);
206+
workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(UninstallAction, UninstallAction.ID, UninstallAction.LABEL), category);
207+
208+
const workbenchRegistry = <IWorkbenchContributionsRegistry>Registry.as(WorkbenchExtensions.Workbench);
209+
workbenchRegistry.registerWorkbenchContribution(DarwinCLIHelper);
210+
}

src/vs/workbench/workbench.main.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ define([
8181

8282
'vs/workbench/parts/gettingStarted/electron-browser/electronGettingStarted.contribution',
8383

84+
'vs/workbench/electron-browser/darwin/cli.contribution',
85+
8486
'vs/workbench/electron-browser/main.contribution',
8587
'vs/workbench/electron-browser/main'
8688

0 commit comments

Comments
 (0)