vsix-extension-manager
Version:
VSIX Extension Manager: A comprehensive CLI tool to download, export, import, and manage VS Code/Cursor extensions as VSIX files
467 lines • 20.8 kB
JavaScript
;
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.downloadVsix = downloadVsix;
exports.runSingleDownloadUI = runSingleDownloadUI;
exports.runBulkJsonDownloadUI = runBulkJsonDownloadUI;
const p = __importStar(require("@clack/prompts"));
const path_1 = __importDefault(require("path"));
const registry_1 = require("../core/registry");
const filesystem_1 = require("../core/filesystem");
const download_1 = require("../features/download");
const filesystem_2 = require("../core/filesystem");
const progress_1 = require("../core/ui/progress");
const helpers_1 = require("../core/helpers");
const constants_1 = require("../config/constants");
async function downloadVsix(options) {
console.clear();
p.intro("🔽 VSIX Extension Manager");
try {
// If a bulk file is provided, run bulk mode non-interactively
if (options.file) {
await downloadBulkFromJson(options);
return;
}
// If command line options for single are provided, skip mode selection and go straight to single download
if (options.url || options.version) {
await downloadSingleExtension(options);
return;
}
// Ask user to choose download mode
const downloadMode = await p.select({
message: "Choose download mode:",
options: [
{
value: "single",
label: "Download single extension from marketplace URL",
hint: "Provide extension URL and version (or latest)",
},
{
value: "bulk",
label: "Download multiple extensions from JSON collection (URLs + versions)",
hint: "Provide a JSON file of { url, version, source? } entries",
},
],
});
if (p.isCancel(downloadMode)) {
p.cancel("Operation cancelled.");
process.exit(0);
}
if (downloadMode === "single") {
await downloadSingleExtension(options);
}
else {
await downloadBulkFromJson(options);
}
}
catch (error) {
p.log.error("❌ Error: " + (error instanceof Error ? error.message : String(error)));
process.exit(1);
}
}
async function downloadSingleExtension(options) {
// Get marketplace URL
let marketplaceUrl = options.url;
if (!marketplaceUrl) {
const urlResult = await p.text({
message: "Enter the extension URL (Marketplace or OpenVSX):",
validate: (input) => {
if (!input.trim()) {
return "Please enter a valid URL";
}
return undefined;
},
});
if (p.isCancel(urlResult)) {
p.cancel("Operation cancelled.");
process.exit(0);
}
marketplaceUrl = urlResult;
}
// Parse URL to extract extension info (for display only)
const parseSpinner = p.spinner();
parseSpinner.start("Parsing extension URL...");
let extensionInfo;
try {
extensionInfo = (0, registry_1.parseExtensionUrl)(marketplaceUrl);
parseSpinner.stop("Extension info extracted");
}
catch (error) {
parseSpinner.stop("Failed to parse extension URL", 1);
throw error;
}
const displayName = (0, registry_1.getDisplayNameFromUrl)(marketplaceUrl);
p.note(`${displayName}`, "Extension");
// Get version
let version = options.version;
if (!version) {
const versionResult = await p.text({
message: "Enter the extension version (or use version number):",
placeholder: "e.g., 1.2.3 or latest",
initialValue: "latest",
validate: (input) => {
if (!input.trim()) {
return "Please enter a version number";
}
const v = input.trim().toLowerCase();
if (v === "latest")
return undefined;
// Basic semver validation (allow optional prerelease)
if (!/^\d+\.\d+\.\d+(?:-.+)?$/.test(v)) {
return "Enter a valid version (e.g., 1.2.3) or 'latest'";
}
return undefined;
},
});
if (p.isCancel(versionResult)) {
p.cancel("Operation cancelled.");
process.exit(0);
}
version = versionResult.trim();
}
// Source selection (interactive with default inference)
let effectiveSource = (options.source || (0, registry_1.inferSourceFromUrl)(marketplaceUrl)).toLowerCase();
if (!options.source) {
const pick = await p.select({
message: "Select source registry:",
options: [
{ value: "marketplace", label: "Visual Studio Marketplace" },
{ value: "open-vsx", label: "OpenVSX" },
],
initialValue: effectiveSource,
});
if (p.isCancel(pick)) {
p.cancel("Operation cancelled.");
process.exit(0);
}
effectiveSource = pick.toLowerCase();
}
// Determine file exists behavior
let fileExistsAction;
if (options.skipExisting)
fileExistsAction = filesystem_1.FileExistsAction.SKIP;
else if (options.overwrite)
fileExistsAction = filesystem_1.FileExistsAction.OVERWRITE;
else
fileExistsAction = filesystem_1.FileExistsAction.PROMPT;
// Prepare params for core single download
const filenameTemplate = options.filenameTemplate || filesystem_1.DEFAULT_FILENAME_TEMPLATE;
const outputDirInput = options.cacheDir
? options.cacheDir
: options.output
? options.output
: (await p.text({
message: "Enter output directory:",
placeholder: constants_1.DEFAULT_OUTPUT_DIR,
initialValue: constants_1.DEFAULT_OUTPUT_DIR,
}));
if (p.isCancel(outputDirInput)) {
p.cancel("Operation cancelled.");
process.exit(0);
}
const resolvedOutput = outputDirInput.trim() || constants_1.DEFAULT_OUTPUT_DIR;
const progressWrapper = (progress) => {
const now = Date.now();
if ((0, helpers_1.shouldUpdateProgress)(lastProgressUpdate, now)) {
const progressBar = (0, progress_1.createProgressBar)(progress.percentage, 30);
const downloaded = (0, progress_1.formatBytes)(progress.downloaded);
const total = (0, progress_1.formatBytes)(progress.total);
downloadSpinner.message(`${progressBar} ${downloaded}/${total}`);
lastProgressUpdate = now;
}
};
const confirmOverwrite = async () => {
const result = await p.confirm({
message: `File already exists. Overwrite?`,
initialValue: false,
});
if (p.isCancel(result)) {
p.cancel("Operation cancelled.");
process.exit(0);
}
return Boolean(result);
};
// Start spinner and perform download via core service
const downloadSpinner = p.spinner();
let lastProgressUpdate = Date.now();
downloadSpinner.start(`Downloading ${(0, helpers_1.truncateText)("pending...")}`);
// First resolve final file details for pretty note
// We repeat minimal parse here to build display URL post-download
const resolvedVersion = await (0, registry_1.resolveVersion)(extensionInfo.itemName, version, Boolean(options.preRelease), effectiveSource);
const downloadUrl = effectiveSource === "open-vsx"
? (0, registry_1.constructOpenVsxDownloadUrl)(extensionInfo, resolvedVersion)
: (0, registry_1.constructDownloadUrl)(extensionInfo, resolvedVersion);
const filename = (0, filesystem_1.generateFilename)(filenameTemplate, {
name: extensionInfo.itemName,
version: resolvedVersion,
source: effectiveSource,
publisher: extensionInfo.itemName.split(".")[0],
});
const displayUrl = downloadUrl.length > 50
? `${downloadUrl.slice(0, 30)}...${downloadUrl.slice(-10)}`
: downloadUrl;
p.note(`Filename: ${filename}\nOutput: ${resolvedOutput}\nResolved Version: ${resolvedVersion}\nTemplate: ${filenameTemplate}\nURL: ${displayUrl}`, "Download Details");
// Confirm download (only if not already confirmed via overwrite prompt)
if (fileExistsAction !== filesystem_1.FileExistsAction.PROMPT) {
const shouldProceed = await p.confirm({
message: `Download ${filename}?`,
initialValue: true,
});
if (p.isCancel(shouldProceed) || !shouldProceed) {
p.cancel("Download cancelled.");
return;
}
}
// Validate checksum format if provided
if (options.verifyChecksum && !(0, filesystem_2.isValidSHA256)(options.verifyChecksum)) {
throw new Error("Invalid SHA256 hash format. Expected 64 hexadecimal characters.");
}
// Update spinner to actual filename now that details are known
downloadSpinner.message(`Downloading ${(0, helpers_1.truncateText)(filename)}...`);
try {
const downloadRequest = {
url: marketplaceUrl,
requestedVersion: version,
preferPreRelease: Boolean(options.preRelease),
source: effectiveSource,
filenameTemplate,
cacheDir: options.cacheDir,
outputDir: resolvedOutput,
fileExistsAction,
promptOverwrite: fileExistsAction === filesystem_1.FileExistsAction.PROMPT ? confirmOverwrite : undefined,
quiet: options.quiet,
progressCallback: options.quiet ? undefined : progressWrapper,
};
const result = await (0, download_1.downloadSingleExtension)(downloadRequest);
const downloadedFilePath = result.filePath || path_1.default.join(resolvedOutput, filename);
downloadSpinner.stop(`Downloaded successfully!`);
// Get file size for display
const fs = await Promise.resolve().then(() => __importStar(require("fs-extra")));
const stats = await fs.stat(downloadedFilePath);
const formattedSize = (0, progress_1.formatBytes)(stats.size);
let checksumInfo = "";
let verificationInfo = "";
// Generate checksum if requested
if (options.checksum) {
const checksumSpinner = p.spinner();
checksumSpinner.start("Generating SHA256 checksum...");
try {
const hash = await (0, filesystem_2.generateSHA256)(downloadedFilePath);
checksumSpinner.stop("Checksum generated");
checksumInfo = `\nSHA256: ${hash}`;
}
catch (error) {
checksumSpinner.stop("Checksum generation failed", 1);
p.log.warn(`⚠️ Failed to generate checksum: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
// Verify checksum if provided
if (options.verifyChecksum) {
const verifySpinner = p.spinner();
verifySpinner.start("Verifying checksum...");
try {
const isValid = await (0, filesystem_2.verifySHA256)(downloadedFilePath, options.verifyChecksum);
if (isValid) {
verifySpinner.stop("✅ Checksum verification passed");
verificationInfo = `\nVerification: ✅ PASSED (${(0, filesystem_2.formatHashForDisplay)(options.verifyChecksum)})`;
}
else {
verifySpinner.stop("❌ Checksum verification failed", 1);
const actualHash = await (0, filesystem_2.generateSHA256)(downloadedFilePath);
verificationInfo = `\nVerification: ❌ FAILED\nExpected: ${options.verifyChecksum}\nActual: ${actualHash}`;
p.log.error("❌ File integrity check failed! The downloaded file may be corrupted.");
}
}
catch (error) {
verifySpinner.stop("Checksum verification failed", 1);
p.log.warn(`⚠️ Failed to verify checksum: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
p.note(`File: ${filename}\nLocation: ${downloadedFilePath}\nSize: ${formattedSize}${checksumInfo}${verificationInfo}`, "Download Complete");
// Install after download if requested
if (options.installAfter) {
if (!options.quiet) {
p.log.info("🔧 Installing downloaded extension...");
}
try {
const { getInstallService, getEditorService } = await Promise.resolve().then(() => __importStar(require("../features/install")));
const installService = getInstallService();
const editorService = getEditorService();
// Auto-detect editor
const availableEditors = await editorService.getAvailableEditors();
if (availableEditors.length === 0) {
p.log.warn("⚠️ No editors found for installation. Extension downloaded but not installed.");
}
else {
// Prefer Cursor, fallback to VS Code
const editor = availableEditors.find((e) => e.name === "cursor") || availableEditors[0];
const binPath = editor.binaryPath;
const installResult = await installService.installSingleVsix(binPath, downloadedFilePath, {
dryRun: false,
forceReinstall: false,
timeout: 30000,
});
if (installResult.success) {
p.log.success(`✅ Extension installed successfully into ${editor.displayName}!`);
}
else {
p.log.warn(`⚠️ Installation failed: ${installResult.error || "Unknown error"}`);
}
}
}
catch (error) {
p.log.warn(`⚠️ Installation failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
p.outro(options.installAfter
? `🎉 Download and install completed!`
: `🎉 Successfully downloaded VSIX extension!`);
}
catch (error) {
downloadSpinner.stop("Download failed", 1);
throw error;
}
}
async function downloadBulkFromJson(options) {
// Get JSON file path
let jsonPathStr = options.file;
if (!jsonPathStr) {
const jsonPath = await p.text({
message: "Enter the path to your JSON file:",
placeholder: "e.g., ./list.json or /path/to/extensions.json",
validate: (input) => {
if (!input.trim()) {
return "Please enter a valid file path";
}
if (!input.endsWith(".json")) {
return "File must have .json extension";
}
return undefined;
},
});
if (p.isCancel(jsonPath)) {
p.cancel("Operation cancelled.");
process.exit(0);
}
jsonPathStr = jsonPath;
}
// Determine output directory (cache-dir takes precedence, then output, then prompt)
let outputDir;
if (options.cacheDir) {
outputDir = options.cacheDir;
}
else if (options.output) {
outputDir = options.output;
}
else {
const outputInput = await p.text({
message: "Enter output directory:",
placeholder: "./downloads",
initialValue: constants_1.DEFAULT_OUTPUT_DIR,
});
if (p.isCancel(outputInput)) {
p.cancel("Operation cancelled.");
process.exit(0);
}
outputDir = outputInput.trim() || constants_1.DEFAULT_OUTPUT_DIR;
}
// Start bulk download process
if (!options.quiet) {
p.log.info("🔍 Reading and validating JSON file...");
}
// Interactive bulk (no --file provided initially) should run sequentially
const isInteractive = !options.file;
const bulkOptions = (0, helpers_1.buildBulkOptionsFromCli)(options, isInteractive ? { parallel: 1, retry: 2, retryDelay: 1000 } : undefined);
await (0, download_1.downloadBulkExtensions)(jsonPathStr, outputDir, bulkOptions);
// Install after bulk download if requested
if (options.installAfter) {
if (!options.quiet) {
p.log.info("🔧 Installing downloaded extensions...");
}
try {
const { getInstallService, getEditorService, getVsixScanner } = await Promise.resolve().then(() => __importStar(require("../features/install")));
const installService = getInstallService();
const editorService = getEditorService();
// Auto-detect editor
const availableEditors = await editorService.getAvailableEditors();
if (availableEditors.length === 0) {
p.log.warn("⚠️ No editors found for installation. Extensions downloaded but not installed.");
}
else {
// Prefer Cursor, fallback to VS Code
const editor = availableEditors.find((e) => e.name === "cursor") || availableEditors[0];
const binPath = editor.binaryPath;
// Install all VSIX files from the output directory
const vsixScanner = getVsixScanner();
const scanResult = await vsixScanner.scanDirectory(outputDir);
if (scanResult.validVsixFiles.length > 0) {
const installTasks = scanResult.validVsixFiles.map((vsixFile) => ({
vsixFile,
extensionId: vsixFile.extensionId,
targetVersion: vsixFile.version,
}));
const installResult = await installService.installBulkVsix(binPath, installTasks, {
dryRun: false,
skipInstalled: true,
parallel: 1,
retry: Number(options.retry) || 2,
retryDelay: Number(options.retryDelay) || 1000,
quiet: options.quiet,
});
if (!options.quiet) {
p.note(`Downloaded: ${scanResult.validVsixFiles.length}\nInstalled: ${installResult.successful}\nSkipped: ${installResult.skipped}\nFailed: ${installResult.failed}`, "Download & Install Summary");
}
}
else {
p.log.warn("⚠️ No valid VSIX files found for installation.");
}
}
}
catch (error) {
p.log.warn(`⚠️ Installation failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
// Lightweight UI entrypoints for interactive launcher
async function runSingleDownloadUI(options) {
await downloadSingleExtension(options);
}
async function runBulkJsonDownloadUI(options) {
await downloadBulkFromJson(options);
}
//# sourceMappingURL=download.js.map