@nanocollective/nanocoder
Version:
A local-first CLI coding agent that brings the power of agentic coding tools like Claude Code and Gemini CLI to local models or controlled APIs like OpenRouter
204 lines • 6.93 kB
JavaScript
/**
* VS Code extension installation utilities
*/
import { exec as execAsync, spawn } from 'child_process';
import { existsSync } from 'fs';
import { dirname, join } from 'path';
import { platform } from 'process';
import { fileURLToPath } from 'url';
import { promisify } from 'util';
const exec = promisify(execAsync);
const __dirname = dirname(fileURLToPath(import.meta.url));
const isWindows = platform === 'win32';
/**
* List of supported VS Code CLI executables (including forks).
* These are known to support the --install-extension and --list-extensions CLI flags.
*/
export const SUPPORTED_CLIS = [
'code',
'code-insiders',
'cursor',
'codium',
'vscodium',
'windsurf',
'trae',
'positron',
];
// Cache for available CLIs to avoid repeated slow PATH lookups
let cachedAvailableClis = null;
/**
* Get the path to the bundled VSIX file
*/
export function getVsixPath() {
// In development: assets folder is at project root
// In production (npm install): assets folder is in package root
const possiblePaths = [
join(__dirname, '../../assets/nanocoder-vscode.vsix'), // development
join(__dirname, '../../../assets/nanocoder-vscode.vsix'), // npm installed
];
for (const path of possiblePaths) {
if (existsSync(path)) {
return path;
}
}
throw new Error('VS Code extension VSIX not found in package');
}
/**
* Get all available VS Code (or fork) CLIs in the PATH.
* Uses parallel async checks with timeouts for better performance.
*/
export async function getAvailableClis(forceRefresh = false) {
if (cachedAvailableClis && !forceRefresh) {
return cachedAvailableClis;
}
const results = await Promise.all(SUPPORTED_CLIS.map(async (cli) => {
try {
// Use a short timeout (2s) to avoid hanging on unresponsive executables
await exec(`${cli} --version`, {
timeout: 2000,
...(isWindows && { shell: 'cmd.exe' }),
});
return cli;
}
catch {
return null;
}
}));
cachedAvailableClis = results.filter((cli) => cli !== null);
return cachedAvailableClis;
}
/**
* Check if any VS Code CLI is available
*/
export async function isVSCodeCliAvailable() {
const available = await getAvailableClis();
return available.length > 0;
}
/**
* Get detailed status for all supported VS Code flavors
*/
export async function getExtensionStatus() {
const availableClis = await getAvailableClis();
return Promise.all(availableClis.map(async (cli) => {
try {
const { stdout } = await exec(`${cli} --list-extensions`, {
timeout: 5000,
encoding: 'utf-8',
...(isWindows && { shell: 'cmd.exe' }),
});
const extensionInstalled = stdout
.toLowerCase()
.includes('nanocollective.nanocoder-vscode');
return {
cli,
available: true,
extensionInstalled,
};
}
catch {
return {
cli,
available: true,
extensionInstalled: false,
};
}
}));
}
/**
* Check if the nanocoder VS Code extension is installed in any available VS Code flavor
* @deprecated Use getExtensionStatus() for richer information
*/
export async function isExtensionInstalled() {
const status = await getExtensionStatus();
return status.some(s => s.extensionInstalled);
}
/**
* Install the VS Code extension to a specific CLI
*/
async function installToCli(cli, vsixPath) {
// Validate CLI is in the allowed list to prevent command injection
// This check satisfies semgrep's detect-child-process rule
if (!SUPPORTED_CLIS.includes(cli)) {
return false;
}
return new Promise(resolve => {
// nosemgrep
const child = spawn(cli, ['--install-extension', vsixPath], {
stdio: ['ignore', 'pipe', 'pipe'],
...(isWindows && { shell: 'cmd.exe' }),
});
// Add a timeout for installation (30s)
const timeout = setTimeout(() => {
child.kill();
resolve(false);
}, 30000);
child.on('close', code => {
clearTimeout(timeout);
resolve(code === 0);
});
child.on('error', () => {
clearTimeout(timeout);
resolve(false);
});
});
}
/**
* Install the VS Code extension from the bundled VSIX to all or specific available VS Code flavors.
* Returns a promise that resolves when installation is complete.
*/
export async function installExtension(targetClis) {
const availableClis = await getAvailableClis();
const clisToInstall = targetClis
? targetClis.filter(cli => availableClis.includes(cli))
: availableClis;
if (clisToInstall.length === 0) {
const checkedList = SUPPORTED_CLIS.join(', ');
return {
success: false,
message: `No supported VS Code flavor found. Checked: ${checkedList}\n\n` +
"Please ensure your editor's CLI is in your PATH. To enable it:\n" +
' 1. Open VS Code or your preferred editor\n' +
' 2. Press Cmd+Shift+P (Mac) or Ctrl+Shift+P (Windows/Linux)\n' +
' 3. Search for "Shell Command: Install \'code\' command in PATH"',
results: [],
};
}
try {
const vsixPath = getVsixPath();
const results = await Promise.all(clisToInstall.map(async (cli) => ({
cli,
success: await installToCli(cli, vsixPath),
})));
const successful = results.filter(r => r.success);
if (successful.length === 0) {
return {
success: false,
message: `Failed to install extension to: ${clisToInstall.join(', ')}.`,
results,
};
}
const successMessage = successful.length === clisToInstall.length
? `VS Code extension installed successfully for: ${successful
.map(r => r.cli)
.join(', ')}!`
: `VS Code extension installed for: ${successful
.map(r => r.cli)
.join(', ')}. (Failed for: ${results
.filter(r => !r.success)
.map(r => r.cli)
.join(', ')})`;
return {
success: true,
message: `${successMessage} Please reload your editor to activate it.`,
results,
};
}
catch (error) {
return {
success: false,
message: `Failed to install extension: ${error instanceof Error ? error.message : String(error)}`,
results: [],
};
}
}
//# sourceMappingURL=extension-installer.js.map