UNPKG

node-msix-packager

Version:

A utility to create MSIX packages from Node.js applications with MCP server support, Node.js Single Executable Application (SEA) bundling using @vercel/ncc and postject, and enhanced build options

306 lines (263 loc) 12.8 kB
#!/usr/bin/env node const { Command } = require('commander'); const fs = require('fs-extra'); const path = require('path'); const chalk = require('chalk'); // Get package information first const packageJson = require('../package.json'); const program = new Command(); program .name('node-msix') .description('Create MSIX packages from Node.js applications') .version(packageJson.version) .addHelpText('after', ` Examples: $ node-msix package --input ./my-app --name "My App" --publisher "CN=MyCompany" $ node-msix package --config ./msix-config.json $ node-msix init $ node-msix list-certificates $ node-msix sign ./dist/MyApp.msix --cert-path ./cert.pfx --cert-password mypassword For more information, visit: https://github.com/your-username/node-msix `); /** * Package command - creates an MSIX package */ program .command('package') .description('Create an MSIX package from a Node.js application') .option('-i, --input <path>', 'Input directory containing the Node.js application') .option('-o, --output <path>', 'Output directory for the MSIX package', './dist') .option('-n, --name <name>', 'Application name') .option('-p, --publisher <publisher>', 'Publisher name (e.g., "CN=My Company")') .option('-v, --version <version>', 'Application version (e.g., "1.0.0.0")') .option('-d, --description <description>', 'Application description') .option('-e, --executable <executable>', 'Main executable file') .option('-a, --architecture <arch>', 'Target architecture (x86, x64, arm, arm64)', 'x64') .option('--icon <path>', 'Path to application icon file') .option('--display-name <name>', 'Display name for the application') .option('--package-name <name>', 'Package identity name') .option('--capabilities <capabilities>', 'Comma-separated list of capabilities', 'internetClient') .option('--background-color <color>', 'Background color for tiles', 'transparent') .option('--skip-build', 'Skip running npm build script') .option('--install-dev-deps', 'Install dev dependencies for build (default: true)') .option('--no-install-dev-deps', 'Skip installing dev dependencies') .option('--no-sign', 'Skip signing the package') .option('--cert-thumbprint <thumbprint>', 'Certificate thumbprint for signing') .option('--cert-subject <subject>', 'Certificate subject name for signing') .option('--cert-path <path>', 'Path to PFX certificate file') .option('--cert-password <password>', 'Password for PFX certificate') .option('--timestamp-url <url>', 'Timestamp server URL') .option('-c, --config <path>', 'Path to configuration file', 'msix-config.json') .action(async (options) => { try { // Import main functions only when needed const { createMsixPackage, CONSTANTS } = require('./index'); console.log(chalk.blue(`📦 Node-MSIX v${packageJson.version}`)); console.log(chalk.blue('Creating MSIX package...')); console.log(); // Load configuration let config = {}; // Load from config file if it exists if (await fs.pathExists(options.config)) { console.log(chalk.blue(`📋 Loading configuration from ${options.config}`)); const configContent = await fs.readFile(options.config, 'utf8'); config = JSON.parse(configContent); } // Override with command line options if (options.input) config.inputPath = options.input; if (options.output) config.outputPath = options.output; if (options.name) config.appName = options.name; if (options.publisher) config.publisher = options.publisher; if (options.version) config.version = options.version; if (options.description) config.description = options.description; if (options.executable) config.executable = options.executable; if (options.architecture) config.architecture = options.architecture; if (options.icon) config.icon = options.icon; if (options.displayName) config.displayName = options.displayName; if (options.packageName) config.packageName = options.packageName; if (options.capabilities) config.capabilities = options.capabilities.split(','); if (options.backgroundColor) config.backgroundColor = options.backgroundColor; // Build options if (options.skipBuild) config.skipBuild = true; if (options.installDevDeps === false) config.installDevDeps = false; // Signing options config.sign = options.sign !== false; // Default to true unless --no-sign is used if (options.certThumbprint) config.certificateThumbprint = options.certThumbprint; if (options.certSubject) config.certificateSubject = options.certSubject; if (options.certPath) config.certificatePath = options.certPath; if (options.certPassword) config.certificatePassword = options.certPassword; if (options.timestampUrl) config.timestampUrl = options.timestampUrl; // Validate required fields if (!config.inputPath) { console.error(chalk.red('❌ Error: Input path is required')); console.log(chalk.blue('Use --input <path> or specify inputPath in config file')); process.exit(1); } if (!config.appName) { console.error(chalk.red('❌ Error: Application name is required')); console.log(chalk.blue('Use --name <name> or specify appName in config file')); process.exit(1); } if (!config.publisher) { console.error(chalk.red('❌ Error: Publisher name is required')); console.log(chalk.blue('Use --publisher "CN=My Company" or specify publisher in config file')); process.exit(1); } // Create the MSIX package const result = await createMsixPackage(config); console.log(); console.log(chalk.green('🎉 Success!')); console.log(chalk.green(`📁 Package: ${result.packagePath}`)); console.log(chalk.green(`📏 Size: ${result.packageSize}`)); console.log(chalk.green(`🔐 Signed: ${result.signed ? 'Yes' : 'No'}`)); console.log(); } catch (error) { console.log(); console.error(chalk.red(`❌ Error: ${error.message}`)); console.log(); if (error.name === 'ValidationError') { console.log(chalk.yellow('💡 Configuration tips:')); console.log(chalk.yellow(' • Use --help to see all available options')); console.log(chalk.yellow(' • Create a config file with: node-msix init')); console.log(chalk.yellow(' • Ensure input directory contains a valid Node.js application')); } process.exit(1); } }); /** * Sign command - signs an existing MSIX package */ program .command('sign <packagePath>') .description('Sign an existing MSIX package') .option('--cert-thumbprint <thumbprint>', 'Certificate thumbprint for signing') .option('--cert-subject <subject>', 'Certificate subject name for signing') .option('--cert-path <path>', 'Path to PFX certificate file') .option('--cert-password <password>', 'Password for PFX certificate') .option('--timestamp-url <url>', 'Timestamp server URL') .action(async (packagePath, options) => { try { // Import signing function only when needed const { signMsixPackage, CONSTANTS } = require('./index'); console.log(chalk.blue(`📦 Node-MSIX v${packageJson.version}`)); console.log(chalk.blue(`Signing package: ${packagePath}`)); console.log(); const signingConfig = { certificateThumbprint: options.certThumbprint, certificateSubject: options.certSubject, certificatePath: options.certPath, certificatePassword: options.certPassword, timestampUrl: options.timestampUrl }; const result = await signMsixPackage(packagePath, signingConfig); if (result) { console.log(); console.log(chalk.green('🎉 Package signed successfully!')); } else { console.log(); console.log(chalk.yellow('⚠️ Package signing failed')); process.exit(1); } } catch (error) { console.log(); console.error(chalk.red(`❌ Error: ${error.message}`)); process.exit(1); } }); /** * List certificates command */ program .command('list-certificates') .alias('certs') .description('List available code signing certificates on this system') .action(async () => { try { // Import certificate functions only when needed const { listCertificates } = require('./index'); console.log(chalk.blue(`📦 Node-MSIX v${packageJson.version}`)); console.log(); const certificates = await listCertificates(); if (certificates.length === 0) { console.log(chalk.yellow('⚠️ No code signing certificates found')); console.log(); console.log(chalk.blue('💡 To use certificate signing:')); console.log(chalk.blue(' 1. Install a code signing certificate in your certificate store')); console.log(chalk.blue(' 2. Ensure the certificate has a private key')); console.log(chalk.blue(' 3. The certificate should have "Code Signing" capability')); console.log(); return; } console.log(chalk.green(`✅ Found ${certificates.length} code signing certificate(s):`)); console.log(); certificates.forEach((cert, index) => { console.log(chalk.white(`${index + 1}. ${cert.subject}`)); console.log(chalk.gray(` Thumbprint: ${cert.thumbprint}`)); console.log(chalk.gray(` Store: ${cert.store}`)); console.log(chalk.gray(` Valid: ${cert.isValid ? 'Yes' : 'No (Expired)'}`)); console.log(chalk.gray(` Expires: ${cert.notAfter.toLocaleDateString()}`)); if (index < certificates.length - 1) console.log(); }); console.log(); console.log(chalk.blue('💡 To use a certificate for signing:')); console.log(chalk.blue(' --cert-thumbprint <thumbprint>')); console.log(chalk.blue(' or')); console.log(chalk.blue(' --cert-subject "part-of-subject-name"')); console.log(); } catch (error) { console.log(); console.error(chalk.red(`❌ Error: ${error.message}`)); process.exit(1); } }); /** * Init command - creates a configuration file */ program .command('init [directory]') .description('Initialize a new MSIX configuration file') .option('-f, --force', 'Overwrite existing configuration file') .option('--name <name>', 'Application name') .option('--publisher <publisher>', 'Publisher name (e.g., "CN=My Company")') .option('--version <version>', 'Application version') .option('--description <description>', 'Application description') .action(async (directory = process.cwd(), options) => { try { // Import init functions only when needed const { initConfig, CONSTANTS } = require('./index'); console.log(chalk.blue(`📦 Node-MSIX v${packageJson.version}`)); console.log(chalk.blue(`Initializing configuration in: ${directory}`)); console.log(); // Check if config already exists and force flag is not set const configPath = path.join(directory, CONSTANTS.CONFIG_FILENAME); if (await fs.pathExists(configPath) && !options.force) { console.log(chalk.yellow('⚠️ Configuration file already exists')); console.log(chalk.blue('Use --force to overwrite')); process.exit(1); } const configFile = await initConfig(directory, { appName: options.name, publisher: options.publisher, version: options.version, description: options.description }); console.log(); console.log(chalk.green('🎉 Configuration initialized!')); console.log(chalk.green(`📁 Config file: ${configFile}`)); console.log(); console.log(chalk.blue('💡 Next steps:')); console.log(chalk.blue(' 1. Edit the configuration file to customize your package')); console.log(chalk.blue(' 2. Run: node-msix package')); console.log(); } catch (error) { console.log(); console.error(chalk.red(`❌ Error: ${error.message}`)); process.exit(1); } }); // Show help if no command provided if (process.argv.length === 2) { program.help(); } // Parse command line arguments program.parse();