Skip to content

Commit 531b70a

Browse files
Fixes formatting and updates to vscode link style installation
1 parent 8a539d3 commit 531b70a

File tree

1 file changed

+117
-40
lines changed

1 file changed

+117
-40
lines changed

src/env/node/gk/cli/integration.ts

Lines changed: 117 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { homedir } from 'os';
22
import { arch } from 'process';
33
import type { ConfigurationChangeEvent } from 'vscode';
4-
import { Disposable, env, ProgressLocation, Uri, window, workspace } from 'vscode';
4+
import { version as codeVersion, Disposable, env, ProgressLocation, Uri, window, workspace } from 'vscode';
55
import type { Container } from '../../../../container';
66
import type { SubscriptionChangeEvent } from '../../../../plus/gk/subscriptionService';
77
import { registerCommand } from '../../../../system/-webview/command';
88
import { configuration } from '../../../../system/-webview/configuration';
99
import { getContext } from '../../../../system/-webview/context';
10+
import { openUrl } from '../../../../system/-webview/vscode/uris';
1011
import { Logger } from '../../../../system/logger';
12+
import { compare } from '../../../../system/version';
1113
import { run } from '../../git/shell';
1214
import { getPlatform, isWeb } from '../../platform';
1315
import { CliCommandHandlers } from './commands';
@@ -33,9 +35,12 @@ export class GkCliIntegrationProvider implements Disposable {
3335
);
3436

3537
this.onConfigurationChanged();
36-
setTimeout(() => {
37-
void this.installMCPIfNeeded(true);
38-
}, 10000 + Math.floor(Math.random() * 20000));
38+
setTimeout(
39+
() => {
40+
void this.installMCPIfNeeded(true);
41+
},
42+
10000 + Math.floor(Math.random() * 20000),
43+
);
3944
}
4045

4146
dispose(): void {
@@ -74,6 +79,16 @@ export class GkCliIntegrationProvider implements Disposable {
7479

7580
private async installMCPIfNeeded(silent?: boolean): Promise<void> {
7681
try {
82+
if (
83+
(env.appName === 'Visual Studio Code' || env.appName === 'Visual Studio Code - Insiders') &&
84+
compare(codeVersion, '1.102') < 0
85+
) {
86+
if (!silent) {
87+
void window.showInformationMessage('Use of this command requires VS Code 1.102 or later.');
88+
}
89+
return;
90+
}
91+
7792
if (silent && this.container.storage.get('ai:mcp:attemptInstall')) {
7893
return;
7994
}
@@ -89,7 +104,7 @@ export class GkCliIntegrationProvider implements Disposable {
89104
return;
90105
}
91106

92-
if ( getContext('gitlens:gk:organization:ai:enabled', true) !== true) {
107+
if (getContext('gitlens:gk:organization:ai:enabled', true) !== true) {
93108
const message = 'Cannot install MCP: AI is disabled by your organization';
94109
Logger.log(message);
95110
if (silent !== true) {
@@ -140,7 +155,9 @@ export class GkCliIntegrationProvider implements Disposable {
140155
const message = `Skipping MCP installation: unsupported platform ${platform}`;
141156
Logger.log(message);
142157
if (silent !== true) {
143-
void window.showErrorMessage(`Cannot install MCP integration: unsupported platform ${platform}`);
158+
void window.showErrorMessage(
159+
`Cannot install MCP integration: unsupported platform ${platform}`,
160+
);
144161
}
145162
return;
146163
}
@@ -156,7 +173,14 @@ export class GkCliIntegrationProvider implements Disposable {
156173

157174
try {
158175
// Download the MCP proxy installer
159-
const proxyUrl = this.container.urls.getGkApiUrl('releases', 'gkcli-proxy', 'production', platformName, architecture, 'active');
176+
const proxyUrl = this.container.urls.getGkApiUrl(
177+
'releases',
178+
'gkcli-proxy',
179+
'production',
180+
platformName,
181+
architecture,
182+
'active',
183+
);
160184

161185
let response = await fetch(proxyUrl);
162186
if (!response.ok) {
@@ -167,7 +191,8 @@ export class GkCliIntegrationProvider implements Disposable {
167191

168192
let downloadUrl: string | undefined;
169193
try {
170-
const mcpInstallerInfo: { version?: string; packages?: { zip?: string } } | undefined = await response.json() as any;
194+
const mcpInstallerInfo: { version?: string; packages?: { zip?: string } } | undefined =
195+
(await response.json()) as any;
171196
downloadUrl = mcpInstallerInfo?.packages?.zip;
172197
} catch (ex) {
173198
const errorMsg = `Failed to parse MCP installer info: ${ex}`;
@@ -211,7 +236,10 @@ export class GkCliIntegrationProvider implements Disposable {
211236
// On Windows, use PowerShell to extract the zip file
212237
await run(
213238
'powershell.exe',
214-
['-Command', `Expand-Archive -Path "${mcpInstallerPath.fsPath}" -DestinationPath "${this.container.context.globalStorageUri.fsPath}"`],
239+
[
240+
'-Command',
241+
`Expand-Archive -Path "${mcpInstallerPath.fsPath}" -DestinationPath "${this.container.context.globalStorageUri.fsPath}"`,
242+
],
215243
'utf8',
216244
);
217245
} else {
@@ -224,7 +252,10 @@ export class GkCliIntegrationProvider implements Disposable {
224252
}
225253
// The gk file should be in a subfolder named after the installer file name
226254
const extractedFolderName = installerFileName.replace(/\.zip$/, '');
227-
mcpExtractedFolderPath = Uri.joinPath(this.container.context.globalStorageUri, extractedFolderName);
255+
mcpExtractedFolderPath = Uri.joinPath(
256+
this.container.context.globalStorageUri,
257+
extractedFolderName,
258+
);
228259
mcpExtractedPath = Uri.joinPath(mcpExtractedFolderPath, mcpFileName);
229260

230261
// Check using stat to make sure the newly extracted file exists.
@@ -263,12 +294,19 @@ export class GkCliIntegrationProvider implements Disposable {
263294

264295
// Configure the MCP server in settings.json
265296
try {
266-
const installOutput = await run(platform === 'windows' ? mcpFileName : `./${mcpFileName}`, ['install'], 'utf8', { cwd: mcpExtractedFolderPath.fsPath });
297+
const installOutput = await run(
298+
platform === 'windows' ? mcpFileName : `./${mcpFileName}`,
299+
['install'],
300+
'utf8',
301+
{ cwd: mcpExtractedFolderPath.fsPath },
302+
);
267303
const directory = installOutput.match(/Directory: (.*)/);
304+
let directoryPath;
268305
if (directory != null) {
269306
try {
270-
const directoryPath = directory[1];
307+
directoryPath = directory[1];
271308
await this.container.storage.store('gk:cli:installedPath', directoryPath);
309+
// Add to PATH
272310
if (platform === 'windows') {
273311
await run(
274312
'powershell.exe',
@@ -279,32 +317,35 @@ export class GkCliIntegrationProvider implements Disposable {
279317
'utf8',
280318
);
281319
} else {
282-
// For Unix-like systems, detect and modify the appropriate shell profile
283-
const homeDir = homedir();
284-
// Try to detect which shell profile exists and is in use
285-
const possibleProfiles = [
286-
{ path: `${homeDir}/.zshrc`, shell: 'zsh' },
320+
// For Unix-like systems, detect and modify the appropriate shell profile
321+
const homeDir = homedir();
322+
// Try to detect which shell profile exists and is in use
323+
const possibleProfiles = [
324+
{ path: `${homeDir}/.zshrc`, shell: 'zsh' },
287325
{ path: `${homeDir}/.zprofile`, shell: 'zsh' },
288-
{ path: `${homeDir}/.bashrc`, shell: 'bash' },
289-
{ path: `${homeDir}/.profile`, shell: 'sh' }
290-
];
291-
292-
// Find the first profile that exists
293-
let shellProfile;
294-
for (const profile of possibleProfiles) {
295-
try {
296-
await workspace.fs.stat(Uri.file(profile.path));
297-
shellProfile = profile.path;
298-
break;
299-
} catch {
300-
// Profile doesn't exist, try next one
301-
}
302-
}
326+
{ path: `${homeDir}/.bashrc`, shell: 'bash' },
327+
{ path: `${homeDir}/.profile`, shell: 'sh' },
328+
];
329+
330+
// Find the first profile that exists
331+
let shellProfile;
332+
for (const profile of possibleProfiles) {
333+
try {
334+
await workspace.fs.stat(Uri.file(profile.path));
335+
shellProfile = profile.path;
336+
break;
337+
} catch {
338+
// Profile doesn't exist, try next one
339+
}
340+
}
303341

304342
if (shellProfile != null) {
305343
await run(
306344
'sh',
307-
['-c', `echo '# Added by GitLens for MCP support' >> ${shellProfile} && echo 'export PATH="$PATH:${directoryPath}"' >> ${shellProfile}`],
345+
[
346+
'-c',
347+
`echo '# Added by GitLens for MCP support' >> ${shellProfile} && echo 'export PATH="$PATH:${directoryPath}"' >> ${shellProfile}`,
348+
],
308349
'utf8',
309350
);
310351
} else {
@@ -316,12 +357,37 @@ export class GkCliIntegrationProvider implements Disposable {
316357
}
317358
} else {
318359
Logger.warn('MCP Install: Failed to find directory in install output');
360+
if (appName === 'vscode') {
361+
throw new Error('MCP command path not availavle');
362+
}
363+
}
364+
365+
if (appName === 'vscode') {
366+
const config = {
367+
name: 'GitKraken',
368+
command: Uri.file(`${directoryPath}\\${mcpFileName}`).fsPath,
369+
args: ['mcp'],
370+
type: 'stdio',
371+
};
372+
const installDeepLinkUrl = `${isInsiders ? 'vscode-insiders' : 'vscode'}:mcp/install?${encodeURIComponent(JSON.stringify(config))}`;
373+
await openUrl(installDeepLinkUrl);
374+
} else {
375+
await run(
376+
platform === 'windows' ? mcpFileName : `./${mcpFileName}`,
377+
['mcp', 'install', appName, ...(isInsiders ? ['--file-path', settingsPath] : [])],
378+
'utf8',
379+
{ cwd: mcpExtractedFolderPath.fsPath },
380+
);
319381
}
320382

321-
await run(platform === 'windows' ? mcpFileName : `./${mcpFileName}`, ['mcp', 'install', appName, ...isInsiders ? ['--file-path', settingsPath] : []], 'utf8', { cwd: mcpExtractedFolderPath.fsPath });
322383
const gkAuth = (await this.container.subscription.getAuthenticationSession())?.accessToken;
323384
if (gkAuth != null) {
324-
await run(platform === 'windows' ? mcpFileName : `./${mcpFileName}`, ['auth', 'login', '-t', gkAuth], 'utf8', { cwd: mcpExtractedFolderPath.fsPath });
385+
await run(
386+
platform === 'windows' ? mcpFileName : `./${mcpFileName}`,
387+
['auth', 'login', '-t', gkAuth],
388+
'utf8',
389+
{ cwd: mcpExtractedFolderPath.fsPath },
390+
);
325391
}
326392

327393
Logger.log('MCP configuration completed');
@@ -369,7 +435,7 @@ export class GkCliIntegrationProvider implements Disposable {
369435
},
370436
async () => {
371437
await installationTask();
372-
}
438+
},
373439
);
374440
} else {
375441
await installationTask();
@@ -383,7 +449,9 @@ export class GkCliIntegrationProvider implements Disposable {
383449

384450
// Show error notification if not silent
385451
if (silent !== true) {
386-
void window.showErrorMessage(`Failed to install MCP integration: ${error instanceof Error ? error.message : String(error)}`);
452+
void window.showErrorMessage(
453+
`Failed to install MCP integration: ${error instanceof Error ? error.message : String(error)}`,
454+
);
387455
}
388456
}
389457
}
@@ -392,17 +460,26 @@ export class GkCliIntegrationProvider implements Disposable {
392460
const mcpInstallStatus = this.container.storage.get('ai:mcp:attemptInstall');
393461
const mcpDirectoryPath = this.container.storage.get('gk:cli:installedPath');
394462
const platform = getPlatform();
395-
if (e.current?.account?.id != null && e.current.account.id !== e.previous?.account?.id && mcpInstallStatus === 'completed' && mcpDirectoryPath != null) {
463+
if (
464+
e.current?.account?.id != null &&
465+
e.current.account.id !== e.previous?.account?.id &&
466+
mcpInstallStatus === 'completed' &&
467+
mcpDirectoryPath != null
468+
) {
396469
const currentSessionToken = (await this.container.subscription.getAuthenticationSession())?.accessToken;
397470
if (currentSessionToken != null) {
398471
try {
399-
await run(platform === 'windows' ? 'gk.exe' : './gk', ['auth', 'login', '-t', currentSessionToken], 'utf8', { cwd: mcpDirectoryPath });
472+
await run(
473+
platform === 'windows' ? 'gk.exe' : './gk',
474+
['auth', 'login', '-t', currentSessionToken],
475+
'utf8',
476+
{ cwd: mcpDirectoryPath },
477+
);
400478
} catch {}
401479
}
402480
}
403481
}
404482

405-
406483
private registerCommands(): Disposable[] {
407484
return [registerCommand('gitlens.ai.mcp.install', () => this.installMCPIfNeeded())];
408485
}

0 commit comments

Comments
 (0)