UNPKG

vsix-extension-manager

Version:

VSIX Extension Manager: A comprehensive CLI tool to download, export, import, and manage VS Code/Cursor extensions as VSIX files

670 lines 28 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.installExtensions = installExtensions; exports.runInstallVsixUI = runInstallVsixUI; exports.runInstallVsixDirUI = runInstallVsixDirUI; exports.runInstallFromListUI = runInstallFromListUI; const p = __importStar(require("@clack/prompts")); const path_1 = __importDefault(require("path")); const fs_extra_1 = __importDefault(require("fs-extra")); const install_1 = require("../features/install"); const constants_1 = require("../config/constants"); async function installExtensions(options) { console.clear(); p.intro("⚙️ VSIX Extension Manager - Install"); try { // Determine install mode (interactive if no clear mode provided) const installMode = determineInstallMode(options); switch (installMode) { case "single-vsix": await installSingleVsix(options); break; case "vsix-directory": await installFromVsixDirectory(options); break; case "from-list": await installFromList(options); break; default: await interactiveInstallMode(options); } } catch (error) { p.log.error("❌ Error: " + (error instanceof Error ? error.message : String(error))); process.exit(1); } } function determineInstallMode(options) { if (options.vsix) return "single-vsix"; if (options.vsixDir) return "vsix-directory"; if (options.file) return "from-list"; return "interactive"; } function normalizeVsixDirs(vsixDir) { if (!vsixDir) return []; return Array.isArray(vsixDir) ? vsixDir : [vsixDir]; } function detectIdentityFromPath(pth) { const lower = pth.toLowerCase(); if (lower.includes("cursor.app")) return "cursor"; if (lower.includes("visual studio code.app")) return "vscode"; if (/(^|[\\/])cursor([\\/]|$)/.test(lower)) return "cursor"; if (/(^|[\\/])code([\\/]|$)/.test(lower)) return "vscode"; return "unknown"; } function getIdentityBadge(expected, binaryPath) { const identity = detectIdentityFromPath(binaryPath); if (identity === "unknown") return ""; return identity === expected ? " — OK" : " — MISMATCH"; } async function interactiveInstallMode(options) { const mode = await p.select({ message: "Choose install mode:", options: [ { value: "single-vsix", label: "Install single VSIX file", hint: "Install one .vsix file into VS Code or Cursor", }, { value: "vsix-directory", label: "Install all VSIX files from directory", hint: "Scan and install multiple .vsix files", }, { value: "from-list", label: "Install from extension list", hint: "Install from .txt file or extensions.json", }, ], }); if (p.isCancel(mode)) { p.cancel("Operation cancelled."); process.exit(0); } switch (mode) { case "single-vsix": await installSingleVsix(options); break; case "vsix-directory": await installFromVsixDirectory(options); break; case "from-list": await installFromList(options); break; } } async function installSingleVsix(options) { let vsixPath = options.vsix; if (!vsixPath) { const result = await p.text({ message: "Enter path to VSIX file or directory:", validate: (input) => { const trimmed = input.trim(); if (!trimmed) return "Please enter a path"; if (!fs_extra_1.default.existsSync(trimmed)) return "File or directory does not exist"; try { const stat = fs_extra_1.default.statSync(trimmed); if (stat.isFile() && !trimmed.toLowerCase().endsWith(".vsix")) { return "Must be a .vsix file or a directory"; } } catch { return "Unable to access the given path"; } return undefined; }, }); if (p.isCancel(result)) { p.cancel("Operation cancelled."); process.exit(0); } vsixPath = result; } // Resolve VSIX path: allow directory input and pick a VSIX inside if (!fs_extra_1.default.existsSync(vsixPath)) { p.log.error(`❌ VSIX file not found: ${vsixPath}`); process.exit(1); } const vsixStat = fs_extra_1.default.statSync(vsixPath); if (vsixStat.isDirectory()) { const vsixScanner = (0, install_1.getVsixScanner)(); const scanResult = await vsixScanner.scanDirectory(vsixPath, { recursive: false }); const candidates = scanResult.validVsixFiles; if (candidates.length === 0) { p.log.error(`❌ No VSIX files found in directory: ${vsixPath}`); process.exit(1); } if (!options.quiet && !options.json && candidates.length > 1) { const choice = await p.select({ message: "Select VSIX to install:", options: candidates.map((f) => ({ value: f.path, label: f.filename })), }); if (p.isCancel(choice)) { p.cancel("Operation cancelled."); process.exit(0); } vsixPath = choice; } else { const chosen = candidates.sort((a, b) => b.modified.getTime() - a.modified.getTime())[0]; if (!options.quiet) { p.log.info(`🔍 Using ${path_1.default.basename(chosen.path)} from directory`); } vsixPath = chosen.path; } } else { if (!vsixPath.toLowerCase().endsWith(".vsix")) { p.log.error("❌ File must be a .vsix file"); process.exit(1); } } const editor = await resolveEditor(options); const binPath = await resolveEditorBinary(editor, options); // Preflight const preflight = await (0, install_1.getInstallService)().validatePrerequisites(binPath); if (!preflight.valid) { p.log.error("❌ Preflight checks failed:"); preflight.errors.forEach((e) => p.log.error(` • ${e}`)); process.exit(1); } p.note(`VSIX: ${vsixPath}\nEditor: ${editor}\nBinary: ${binPath}`, "Install Details"); if (!options.quiet && !options.json) { const confirmRes = await p.confirm({ message: "Proceed with installation?", initialValue: true, }); if (p.isCancel(confirmRes) || !confirmRes) { p.cancel("Installation cancelled."); return; } } // Perform installation const installService = (0, install_1.getInstallService)(); const spinner = p.spinner(); spinner.start("Installing VSIX file..."); try { const result = await installService.installSingleVsix(binPath, vsixPath, { dryRun: options.dryRun, forceReinstall: options.forceReinstall, timeout: 30000, }); if (result.success) { spinner.stop("✅ Installation successful!"); p.note(`File: ${vsixPath}\nExit Code: ${result.exitCode}`, "Install Result"); } else { spinner.stop("❌ Installation failed", 1); p.note(`File: ${vsixPath}\nExit Code: ${result.exitCode}\nError: ${result.error || "Unknown error"}`, "Install Result"); } } catch (error) { spinner.stop("❌ Installation failed", 1); throw error; } // JSON/stdout and summary support const summaryData = { timestamp: new Date().toISOString(), file: vsixPath, editor, binary: binPath, dryRun: Boolean(options.dryRun), }; if (options.summary) { try { await fs_extra_1.default.writeJson(options.summary, summaryData, { spaces: 2 }); if (!options.quiet && !options.json) { p.log.success(`📄 Summary written to: ${options.summary}`); } } catch (error) { if (!options.quiet) p.log.warn(`⚠️ Failed to write summary: ${error instanceof Error ? error.message : String(error)}`); } } if (options.json) { console.log(JSON.stringify(summaryData, null, 2)); } else if (!options.quiet) { p.outro("✅ Installation completed!"); } } async function installFromVsixDirectory(options) { let scanDirs = normalizeVsixDirs(options.vsixDir); if (scanDirs.length === 0) { const result = await p.text({ message: "Enter directory to scan for VSIX files:", placeholder: "./downloads", initialValue: constants_1.DEFAULT_OUTPUT_DIR, validate: (input) => { if (!input.trim()) return "Please enter a directory path"; if (!fs_extra_1.default.existsSync(input.trim())) return "Directory does not exist"; return undefined; }, }); if (p.isCancel(result)) { p.cancel("Operation cancelled."); process.exit(0); } scanDirs = [result]; } // Validate all directories exist for (const scanDir of scanDirs) { if (!fs_extra_1.default.existsSync(scanDir)) { p.log.error(`❌ Directory not found: ${scanDir}`); process.exit(1); } } const editor = await resolveEditor(options); const binPath = await resolveEditorBinary(editor, options); // Preflight const preflight = await (0, install_1.getInstallService)().validatePrerequisites(binPath); if (!preflight.valid) { p.log.error("❌ Preflight checks failed:"); preflight.errors.forEach((e) => p.log.error(` • ${e}`)); process.exit(1); } // Scan for VSIX files in all directories const vsixScanner = (0, install_1.getVsixScanner)(); const installService = (0, install_1.getInstallService)(); const spinner = p.spinner(); spinner.start("Scanning for VSIX files..."); try { const allValidVsix = []; const allInvalidFiles = []; let totalFiles = 0; for (const scanDir of scanDirs) { const scanResult = await vsixScanner.scanDirectory(scanDir); allValidVsix.push(...scanResult.validVsixFiles); allInvalidFiles.push(...scanResult.invalidFiles); totalFiles += scanResult.totalFiles; } const combinedScanResult = { totalFiles, validVsixFiles: allValidVsix, invalidFiles: allInvalidFiles, errors: [], }; spinner.stop(`Found ${combinedScanResult.validVsixFiles.length} VSIX file(s)`); if (combinedScanResult.validVsixFiles.length === 0) { p.log.warn("⚠️ No valid VSIX files found in the directories"); return; } // Show scan summary const summary = vsixScanner.getScanSummary(combinedScanResult); p.note(`Total: ${summary.total}\nValid: ${summary.valid}\nInvalid: ${summary.invalid}\nUnique Extensions: ${summary.uniqueExtensions}`, "Scan Summary"); if (combinedScanResult.invalidFiles.length > 0) { p.log.warn(`⚠️ ${combinedScanResult.invalidFiles.length} invalid file(s) found:`); combinedScanResult.invalidFiles.forEach((file) => { p.log.warn(` • ${file.filename}: ${file.error}`); }); } // Confirm installation (skip in quiet/json mode) if (!options.quiet && !options.json) { const shouldProceed = await p.confirm({ message: `Install ${combinedScanResult.validVsixFiles.length} VSIX file(s)?`, initialValue: true, }); if (p.isCancel(shouldProceed) || !shouldProceed) { p.cancel("Installation cancelled."); return; } } // Create install tasks const installTasks = combinedScanResult.validVsixFiles.map((vsixFile) => ({ vsixFile, extensionId: vsixFile.extensionId, targetVersion: vsixFile.version, })); // Perform bulk installation spinner.start("Installing VSIX files..."); const installResult = await installService.installBulkVsix(binPath, installTasks, { dryRun: options.dryRun, forceReinstall: options.forceReinstall, skipInstalled: options.skipInstalled, parallel: Number(options.parallel) || options.installParallel || 1, retry: Number(options.retry) || options.installRetry || 2, retryDelay: Number(options.retryDelay) || options.installRetryDelay || 1000, timeout: 30000, quiet: options.quiet, }, (result) => { if (!options.quiet) { const status = result.success ? "✅" : result.skipped ? "⏭️" : "❌"; const filename = path_1.default.basename(result.task.vsixFile.path); spinner.message(`${status} ${filename}`); } }); spinner.stop(`Installation completed!`); // Show results p.note(`Total: ${installResult.total}\nSuccessful: ${installResult.successful}\nSkipped: ${installResult.skipped}\nFailed: ${installResult.failed}\nDuration: ${Math.round(installResult.elapsedMs / 1000)}s`, "Install Summary"); if (installResult.failed > 0 && !options.quiet) { p.log.error("❌ Failed installations:"); installResult.results .filter((r) => !r.success && !r.skipped) .forEach((result) => { p.log.error(` • ${path_1.default.basename(result.task.vsixFile.path)}: ${result.error}`); }); } // Write summary JSON if requested if (options.summary) { try { const summaryData = { timestamp: new Date().toISOString(), scanResult: { totalFiles: combinedScanResult.totalFiles, validVsixFiles: combinedScanResult.validVsixFiles.length, invalidFiles: combinedScanResult.invalidFiles.length, }, installResult: { total: installResult.total, successful: installResult.successful, skipped: installResult.skipped, failed: installResult.failed, elapsedMs: installResult.elapsedMs, results: installResult.results.map((r) => ({ filename: path_1.default.basename(r.task.vsixFile.path), extensionId: r.task.extensionId, version: r.task.targetVersion, success: r.success, skipped: r.skipped, error: r.error, elapsedMs: r.elapsedMs, })), }, }; await fs_extra_1.default.writeJson(options.summary, summaryData, { spaces: 2 }); if (!options.quiet) { p.log.success(`📄 Summary written to: ${options.summary}`); } } catch (error) { p.log.warn(`⚠️ Failed to write summary: ${error instanceof Error ? error.message : String(error)}`); } } } catch (error) { spinner.stop("❌ Installation failed", 1); throw error; } p.outro("✅ Bulk installation completed!"); } async function installFromList(options) { let listPath = options.file; if (!listPath) { const result = await p.text({ message: "Enter path to extension list file:", validate: (input) => { if (!input.trim()) return "Please enter a file path"; if (!fs_extra_1.default.existsSync(input.trim())) return "File does not exist"; return undefined; }, }); if (p.isCancel(result)) { p.cancel("Operation cancelled."); process.exit(0); } listPath = result; } // Validate file exists if (!fs_extra_1.default.existsSync(listPath)) { p.log.error(`❌ File not found: ${listPath}`); process.exit(1); } const editor = await resolveEditor(options); const binPath = await resolveEditorBinary(editor, options); // Preflight const preflight = await (0, install_1.getInstallService)().validatePrerequisites(binPath); if (!preflight.valid) { p.log.error("❌ Preflight checks failed:"); preflight.errors.forEach((e) => p.log.error(` • ${e}`)); process.exit(1); } // Determine VSIX search directories let vsixSearchDirs = normalizeVsixDirs(options.vsixDir); if (vsixSearchDirs.length === 0) { vsixSearchDirs = [options.outputDir || options.output || constants_1.DEFAULT_OUTPUT_DIR]; } // Always include cache directory if specified if (options.cacheDir && !vsixSearchDirs.includes(options.cacheDir)) { vsixSearchDirs.unshift(options.cacheDir); } // Include output directory if not already present const outputDir = options.outputDir || options.output || constants_1.DEFAULT_OUTPUT_DIR; if (!vsixSearchDirs.includes(outputDir)) { vsixSearchDirs.push(outputDir); } // Install from list const installFromListService = (0, install_1.getInstallFromListService)(); const spinner = p.spinner(); spinner.start("Processing extension list..."); try { const result = await installFromListService.installFromList(binPath, listPath, vsixSearchDirs, { downloadMissing: options.downloadMissing, downloadOptions: { outputDir: options.outputDir || options.output, cacheDir: options.cacheDir, source: options.source === "auto" ? undefined : options.source, preRelease: options.preRelease, quiet: options.quiet, parallel: options.parallel, retry: options.retry, retryDelay: options.retryDelay, }, installOptions: { dryRun: options.dryRun, forceReinstall: options.forceReinstall, skipInstalled: options.skipInstalled, parallel: Number(options.parallel) || options.installParallel || 1, retry: Number(options.retry) || options.installRetry || 2, retryDelay: Number(options.retryDelay) || options.installRetryDelay || 1000, timeout: 30000, quiet: options.quiet, }, }, (message) => { if (!options.quiet) { spinner.message(message); } }); spinner.stop("Installation completed!"); // Show results p.note(`Total Extensions: ${result.totalExtensions}\nVSIX Files Found: ${result.foundVsixFiles}\nDownloaded: ${result.downloadedExtensions}\nInstalled: ${result.installedExtensions}\nSkipped: ${result.skippedExtensions}\nFailed: ${result.failedExtensions}`, "Install Summary"); if (result.downloadResult && !options.quiet) { p.note(`Downloaded: ${result.downloadResult.successful}\nDownload Failed: ${result.downloadResult.failed}`, "Download Summary"); } if (result.errors.length > 0) { p.log.error("❌ Errors encountered:"); result.errors.forEach((error) => { p.log.error(` • ${error}`); }); } if (result.installResult.failed > 0 && !options.quiet) { p.log.error("❌ Failed installations:"); result.installResult.results .filter((r) => !r.success && !r.skipped) .forEach((result) => { const filename = result.task.vsixFile ? path_1.default.basename(result.task.vsixFile.path) : result.task.extensionId || "unknown"; p.log.error(` • ${filename}: ${result.error}`); }); } // Write summary JSON if requested if (options.summary) { try { const summaryData = { timestamp: new Date().toISOString(), totalExtensions: result.totalExtensions, foundVsixFiles: result.foundVsixFiles, downloadedExtensions: result.downloadedExtensions, installedExtensions: result.installedExtensions, skippedExtensions: result.skippedExtensions, failedExtensions: result.failedExtensions, downloadResult: result.downloadResult, installResult: { total: result.installResult.total, successful: result.installResult.successful, skipped: result.installResult.skipped, failed: result.installResult.failed, elapsedMs: result.installResult.elapsedMs, results: result.installResult.results.map((r) => ({ extensionId: r.task.extensionId, vsixPath: r.task.vsixFile?.path, version: r.task.targetVersion, success: r.success, skipped: r.skipped, error: r.error, elapsedMs: r.elapsedMs, })), }, errors: result.errors, }; await fs_extra_1.default.writeJson(options.summary, summaryData, { spaces: 2 }); if (!options.quiet) { p.log.success(`📄 Summary written to: ${options.summary}`); } } catch (error) { p.log.warn(`⚠️ Failed to write summary: ${error instanceof Error ? error.message : String(error)}`); } } } catch (error) { spinner.stop("❌ Installation failed", 1); throw error; } p.outro("✅ List installation completed!"); } async function resolveEditor(options) { let editor = options.editor; if (!editor || editor === "auto") { // Show spinner during editor detection const spinner = p.spinner(); if (!options.quiet && !options.json) { spinner.start("Detecting installed editors..."); } const editorService = (0, install_1.getEditorService)(); const availableEditors = await editorService.getAvailableEditors(); if (!options.quiet && !options.json) { spinner.stop("Editor detection complete"); } if (availableEditors.length === 0) { p.log.error("❌ No editors found. Please install VS Code or Cursor."); p.log.info("💡 Install VS Code: https://code.visualstudio.com/"); p.log.info("💡 Install Cursor: https://cursor.sh/"); process.exit(1); } if (availableEditors.length === 1) { const detected = availableEditors[0]; p.log.info(`🔍 Auto-detected ${detected.displayName} at ${detected.binaryPath}`); return detected.name; } // Multiple editors available const choices = availableEditors.map((editor) => ({ value: editor.name, label: `${editor.displayName} (${editor.binaryPath})${getIdentityBadge(editor.name, editor.binaryPath)}`, })); if (options.quiet || options.json) { // In quiet/json mode with multiple editors, require explicit selection throw new Error(`Multiple editors found (${availableEditors.map((e) => e.displayName).join(", ")}). ` + `Please specify which editor to use with --editor vscode or --editor cursor`); } const result = await p.select({ message: "Multiple editors found. Select target editor:", options: choices, }); if (p.isCancel(result)) { p.cancel("Operation cancelled."); process.exit(0); } editor = result; } return editor; } async function resolveEditorBinary(editor, options) { const editorService = (0, install_1.getEditorService)(); // Use explicit binary path if provided const explicitPath = editor === "vscode" ? options.codeBin : options.cursorBin; try { return await editorService.resolveEditorBinary(editor, explicitPath, Boolean(options.allowMismatchedBinary)); } catch (error) { p.log.error(`❌ ${error instanceof Error ? error.message : String(error)}`); // Show available editors for troubleshooting const availableEditors = await editorService.getAvailableEditors(); if (availableEditors.length > 0) { p.log.info("📋 Available editors:"); availableEditors.forEach((editor) => { p.log.info(` • ${editor.displayName}: ${editor.binaryPath}`); }); } process.exit(1); } } // Lightweight UI entrypoints for interactive launcher async function runInstallVsixUI(options) { options.vsix = options.vsix || ""; // Trigger interactive single VSIX mode await installSingleVsix(options); } async function runInstallVsixDirUI(options) { // Leave vsixDir undefined to prompt for directory interactively await installFromVsixDirectory(options); } async function runInstallFromListUI(options) { options.file = options.file || ""; // Trigger interactive list mode await installFromList(options); } //# sourceMappingURL=install.js.map