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
JavaScript
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();