@involvex/msix-packager-cli 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,320 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+ const { executeCommand } = require("./utils");
4
+ const { CONSTANTS, CertificateError, MSIXError } = require("./constants");
5
+
6
+ /**
7
+ * Finds code signing certificates in the certificate store
8
+ * @returns {Array} Array of certificate objects
9
+ */
10
+ async function findCodeSigningCertificates() {
11
+ try {
12
+ console.log("Searching for code signing certificates...");
13
+
14
+ const psScript = `
15
+ $results = @()
16
+
17
+ function Has-CodeSigning($cert) {
18
+ foreach($ext in $cert.Extensions) {
19
+ if($ext.Oid.Value -eq '2.5.29.37') {
20
+ $ekuText = $ext.Format($true)
21
+ if($ekuText -match 'Code Signing') {
22
+ return $true
23
+ }
24
+ }
25
+ }
26
+ return $false
27
+ }
28
+
29
+ function Check-Store($storeLocation, $storeName) {
30
+ try {
31
+ $store = New-Object System.Security.Cryptography.X509Certificates.X509Store([System.Security.Cryptography.X509Certificates.StoreName]::$storeName, [System.Security.Cryptography.X509Certificates.StoreLocation]::$storeLocation)
32
+ $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly)
33
+
34
+ foreach($cert in $store.Certificates) {
35
+ if($cert.HasPrivateKey -and (Has-CodeSigning $cert)) {
36
+ $script:results += @{
37
+ Subject = $cert.Subject
38
+ Thumbprint = $cert.Thumbprint
39
+ NotBefore = $cert.NotBefore.ToString('yyyy-MM-ddTHH:mm:ssZ')
40
+ NotAfter = $cert.NotAfter.ToString('yyyy-MM-ddTHH:mm:ssZ')
41
+ Store = $storeLocation
42
+ }
43
+ }
44
+ }
45
+ $store.Close()
46
+ } catch {
47
+ # Continue to next store
48
+ }
49
+ }
50
+
51
+ Check-Store 'CurrentUser' 'My'
52
+ Check-Store 'LocalMachine' 'My'
53
+
54
+ if($results.Count -eq 0) {
55
+ Write-Output '[]'
56
+ } else {
57
+ $results | ConvertTo-Json -Depth 2
58
+ }
59
+ `;
60
+
61
+ let result = "";
62
+ try {
63
+ const tempScript = path.join(
64
+ process.env.TEMP || "/tmp",
65
+ "find-certs.ps1",
66
+ );
67
+ await fs.writeFile(tempScript, psScript);
68
+
69
+ result = executeCommand(
70
+ `powershell -ExecutionPolicy Bypass -File "${tempScript}"`,
71
+ { silent: true },
72
+ );
73
+
74
+ await fs.unlink(tempScript).catch(() => {}); // Ignore cleanup errors
75
+ } catch (error) {
76
+ console.warn(`Certificate search failed: ${error.message}`);
77
+ return [];
78
+ }
79
+
80
+ const certificates = [];
81
+
82
+ if (result && result.trim() && result.trim() !== "[]") {
83
+ try {
84
+ const parsed = JSON.parse(result);
85
+ const certsArray = Array.isArray(parsed) ? parsed : [parsed];
86
+
87
+ for (const cert of certsArray) {
88
+ if (cert?.Subject && cert?.Thumbprint) {
89
+ certificates.push({
90
+ subject: cert.Subject,
91
+ thumbprint: cert.Thumbprint.replace(/\s/g, ""),
92
+ store: cert.Store || "CurrentUser",
93
+ notBefore: new Date(cert.NotBefore),
94
+ notAfter: new Date(cert.NotAfter),
95
+ isExpired: new Date(cert.NotAfter) < new Date(),
96
+ isValid:
97
+ new Date(cert.NotBefore) <= new Date() &&
98
+ new Date(cert.NotAfter) >= new Date(),
99
+ });
100
+ }
101
+ }
102
+ } catch (parseError) {
103
+ console.warn(
104
+ `Could not parse certificate results: ${parseError.message}`,
105
+ );
106
+ }
107
+ }
108
+
109
+ console.log(`Found ${certificates.length} code signing certificate(s)`);
110
+
111
+ return certificates;
112
+ } catch (error) {
113
+ console.warn(`Certificate search error: ${error.message}`);
114
+ return []; // Return empty array instead of throwing
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Selects the best certificate from available certificates
120
+ * @param {Array} certificates - Array of available certificates
121
+ * @param {Object} preferences - Certificate selection preferences
122
+ * @returns {Object} Selected certificate
123
+ */
124
+ function selectBestCertificate(certificates, preferences = {}) {
125
+ if (certificates.length === 0) {
126
+ throw new CertificateError("No code signing certificates found");
127
+ }
128
+
129
+ const validCertificates = certificates.filter((cert) => cert.isValid);
130
+
131
+ if (validCertificates.length === 0) {
132
+ throw new CertificateError(
133
+ "No valid (non-expired) code signing certificates found",
134
+ );
135
+ }
136
+
137
+ // If thumbprint is specified, try to find matching certificate
138
+ if (preferences.thumbprint) {
139
+ const matchingCert = validCertificates.find(
140
+ (cert) =>
141
+ cert.thumbprint.replace(/\s/g, "").toLowerCase() ===
142
+ preferences.thumbprint.replace(/\s/g, "").toLowerCase(),
143
+ );
144
+
145
+ if (!matchingCert) {
146
+ throw new CertificateError(
147
+ `Certificate with thumbprint ${preferences.thumbprint} not found`,
148
+ );
149
+ }
150
+ return matchingCert;
151
+ }
152
+
153
+ // If subject is specified, try to find matching certificate
154
+ if (preferences.subject) {
155
+ const matchingCert = validCertificates.find((cert) =>
156
+ cert.subject.toLowerCase().includes(preferences.subject.toLowerCase()),
157
+ );
158
+
159
+ if (!matchingCert) {
160
+ throw new CertificateError(
161
+ `Certificate with subject containing "${preferences.subject}" not found`,
162
+ );
163
+ }
164
+ return matchingCert;
165
+ }
166
+
167
+ // Select the certificate that expires latest
168
+ return validCertificates.reduce((best, current) =>
169
+ current.notAfter > best.notAfter ? current : best,
170
+ );
171
+ }
172
+
173
+ /**
174
+ * Gets certificate information from PFX file and validates it
175
+ * @param {string} pfxPath - Path to PFX file
176
+ * @param {string} password - PFX password
177
+ * @param {boolean} validateOnly - If true, only validates without returning full info
178
+ * @returns {Object|boolean} Certificate information or validation result
179
+ */
180
+ async function getPfxCertificateInfo(
181
+ pfxPath,
182
+ password = "",
183
+ validateOnly = false,
184
+ ) {
185
+ try {
186
+ if (!(await fs.pathExists(pfxPath))) {
187
+ throw new CertificateError(`PFX file not found: ${pfxPath}`);
188
+ }
189
+
190
+ const psCommand = `
191
+ try {
192
+ $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("${pfxPath}", "${password}")
193
+ if (-not ($cert.HasPrivateKey -and $cert.Subject)) {
194
+ throw "Invalid certificate"
195
+ }
196
+ ${
197
+ validateOnly
198
+ ? 'Write-Output "Valid"'
199
+ : `$info = @{
200
+ Subject = $cert.Subject
201
+ Thumbprint = $cert.Thumbprint
202
+ NotBefore = $cert.NotBefore.ToString('yyyy-MM-ddTHH:mm:ss')
203
+ NotAfter = $cert.NotAfter.ToString('yyyy-MM-ddTHH:mm:ss')
204
+ }
205
+ $info | ConvertTo-Json -Depth 2`
206
+ }
207
+ } catch {
208
+ ${validateOnly ? 'Write-Output "Invalid"' : 'Write-Error "Failed to read certificate: $_"; exit 1'}
209
+ }
210
+ `;
211
+
212
+ const output = executeCommand(
213
+ `powershell -ExecutionPolicy Bypass -Command "${psCommand}"`,
214
+ { silent: true },
215
+ );
216
+
217
+ if (validateOnly) {
218
+ if (output.trim() !== "Valid") {
219
+ throw new CertificateError(`Invalid PFX file or password`);
220
+ }
221
+ return true;
222
+ }
223
+
224
+ const certInfo = JSON.parse(output);
225
+
226
+ return {
227
+ subject: certInfo.Subject || "Unknown",
228
+ thumbprint: (certInfo.Thumbprint || "").replace(/\s/g, ""),
229
+ notBefore: certInfo.NotBefore ? new Date(certInfo.NotBefore) : null,
230
+ notAfter: certInfo.NotAfter ? new Date(certInfo.NotAfter) : null,
231
+ source: "PFX File",
232
+ path: pfxPath,
233
+ isExpired: certInfo.NotAfter
234
+ ? new Date(certInfo.NotAfter) < new Date()
235
+ : true,
236
+ isValid:
237
+ certInfo.NotBefore && certInfo.NotAfter
238
+ ? new Date(certInfo.NotBefore) <= new Date() &&
239
+ new Date(certInfo.NotAfter) >= new Date()
240
+ : false,
241
+ };
242
+ } catch (error) {
243
+ if (error instanceof CertificateError) {
244
+ throw error;
245
+ }
246
+ throw new CertificateError(
247
+ `Failed to ${validateOnly ? "validate" : "get info from"} PFX file: ${error.message}`,
248
+ );
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Validates that a PFX file exists and can be used for signing
254
+ * @param {string} pfxPath - Path to PFX file
255
+ * @param {string} password - PFX password
256
+ * @returns {boolean} True if PFX is valid
257
+ */
258
+ async function validatePfxFile(pfxPath, password = "") {
259
+ return await getPfxCertificateInfo(pfxPath, password, true);
260
+ }
261
+
262
+ /**
263
+ * Determines the best signing method based on available certificates and configuration
264
+ * @param {Object} signingConfig - Signing configuration
265
+ * @returns {Object} Signing method and parameters
266
+ */
267
+ async function determineSigningMethod(signingConfig) {
268
+ if (!signingConfig.sign) {
269
+ return null;
270
+ }
271
+
272
+ // Priority 1: Use PFX file if specified
273
+ if (signingConfig.certificatePath) {
274
+ if (!(await fs.pathExists(signingConfig.certificatePath))) {
275
+ throw new CertificateError(
276
+ `Certificate file not found: ${signingConfig.certificatePath}`,
277
+ );
278
+ }
279
+
280
+ await validatePfxFile(
281
+ signingConfig.certificatePath,
282
+ signingConfig.certificatePassword || "",
283
+ );
284
+
285
+ return {
286
+ method: "pfx",
287
+ parameters: {
288
+ pfxPath: signingConfig.certificatePath,
289
+ password: signingConfig.certificatePassword || "",
290
+ timestampUrl:
291
+ signingConfig.timestampUrl || CONSTANTS.DEFAULT_TIMESTAMP_URL,
292
+ },
293
+ };
294
+ }
295
+
296
+ // Priority 2: Use certificate store
297
+ const certificates = await findCodeSigningCertificates();
298
+ const selectedCert = selectBestCertificate(certificates, {
299
+ thumbprint: signingConfig.certificateThumbprint,
300
+ subject: signingConfig.certificateSubject,
301
+ });
302
+
303
+ return {
304
+ method: "store",
305
+ parameters: {
306
+ thumbprint: selectedCert.thumbprint,
307
+ store: selectedCert.store,
308
+ timestampUrl:
309
+ signingConfig.timestampUrl || CONSTANTS.DEFAULT_TIMESTAMP_URL,
310
+ },
311
+ };
312
+ }
313
+
314
+ module.exports = {
315
+ findCodeSigningCertificates,
316
+ selectBestCertificate,
317
+ validatePfxFile,
318
+ getPfxCertificateInfo,
319
+ determineSigningMethod,
320
+ };
package/src/cli.js ADDED
@@ -0,0 +1,383 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { Command } = require("commander");
4
+ const fs = require("fs-extra");
5
+ const path = require("path");
6
+ const chalk = require("chalk");
7
+
8
+ // Get package information first
9
+ const packageJson = require("../package.json");
10
+
11
+ const program = new Command();
12
+
13
+ program
14
+ .name("node-msix")
15
+ .description("Create MSIX packages from Node.js applications")
16
+ .version(packageJson.version)
17
+ .addHelpText(
18
+ "after",
19
+ `
20
+ Examples:
21
+ $ node-msix package --input ./my-app --name "My App" --publisher "CN=MyCompany"
22
+ $ node-msix package --config ./msix-config.json
23
+ $ node-msix init
24
+ $ node-msix list-certificates
25
+ $ node-msix sign ./dist/MyApp.msix --cert-path ./cert.pfx --cert-password mypassword
26
+
27
+ For more information, visit: https://github.com/your-username/node-msix
28
+ `,
29
+ );
30
+
31
+ /**
32
+ * Package command - creates an MSIX package
33
+ */
34
+ program
35
+ .command("package")
36
+ .description("Create an MSIX package from a Node.js application")
37
+ .option(
38
+ "-i, --input <path>",
39
+ "Input directory containing the Node.js application",
40
+ )
41
+ .option(
42
+ "-o, --output <path>",
43
+ "Output directory for the MSIX package",
44
+ "./dist",
45
+ )
46
+ .option("-n, --name <name>", "Application name")
47
+ .option(
48
+ "-p, --publisher <publisher>",
49
+ 'Publisher name (e.g., "CN=My Company")',
50
+ )
51
+ .option("-v, --version <version>", 'Application version (e.g., "1.0.0.0")')
52
+ .option("-d, --description <description>", "Application description")
53
+ .option("-e, --executable <executable>", "Main executable file")
54
+ .option(
55
+ "-a, --architecture <arch>",
56
+ "Target architecture (x86, x64, arm, arm64)",
57
+ "x64",
58
+ )
59
+ .option("--icon <path>", "Path to application icon file")
60
+ .option("--display-name <name>", "Display name for the application")
61
+ .option("--package-name <name>", "Package identity name")
62
+ .option(
63
+ "--capabilities <capabilities>",
64
+ "Comma-separated list of capabilities",
65
+ "internetClient",
66
+ )
67
+ .option(
68
+ "--background-color <color>",
69
+ "Background color for tiles",
70
+ "transparent",
71
+ )
72
+ .option("--skip-build", "Skip running bun run build script")
73
+ .option(
74
+ "--install-dev-deps",
75
+ "Install dev dependencies for build (default: true)",
76
+ )
77
+ .option("--no-install-dev-deps", "Skip installing dev dependencies")
78
+ .option("--no-sign", "Skip signing the package")
79
+ .option(
80
+ "--cert-thumbprint <thumbprint>",
81
+ "Certificate thumbprint for signing",
82
+ )
83
+ .option("--cert-subject <subject>", "Certificate subject name for signing")
84
+ .option("--cert-path <path>", "Path to PFX certificate file")
85
+ .option("--cert-password <password>", "Password for PFX certificate")
86
+ .option("--timestamp-url <url>", "Timestamp server URL")
87
+ .option(
88
+ "-c, --config <path>",
89
+ "Path to configuration file",
90
+ "msix-config.json",
91
+ )
92
+ .action(async (options) => {
93
+ try {
94
+ // Import main functions only when needed
95
+ const { createMsixPackage, CONSTANTS } = require("./index");
96
+
97
+ console.log(chalk.blue(`📦 Node-MSIX v${packageJson.version}`));
98
+ console.log(chalk.blue("Creating MSIX package..."));
99
+ console.log();
100
+
101
+ // Load configuration
102
+ let config = {};
103
+
104
+ // Load from config file if it exists
105
+ if (await fs.pathExists(options.config)) {
106
+ console.log(
107
+ chalk.blue(`📋 Loading configuration from ${options.config}`),
108
+ );
109
+ const configContent = await fs.readFile(options.config, "utf8");
110
+ config = JSON.parse(configContent);
111
+ }
112
+
113
+ // Override with command line options
114
+ if (options.input) config.inputPath = options.input;
115
+ if (options.output) config.outputPath = options.output;
116
+ if (options.name) config.appName = options.name;
117
+ if (options.publisher) config.publisher = options.publisher;
118
+ if (options.version) config.version = options.version;
119
+ if (options.description) config.description = options.description;
120
+ if (options.executable) config.executable = options.executable;
121
+ if (options.architecture) config.architecture = options.architecture;
122
+ if (options.icon) config.icon = options.icon;
123
+ if (options.displayName) config.displayName = options.displayName;
124
+ if (options.packageName) config.packageName = options.packageName;
125
+ if (options.capabilities)
126
+ config.capabilities = options.capabilities.split(",");
127
+ if (options.backgroundColor)
128
+ config.backgroundColor = options.backgroundColor;
129
+
130
+ // Build options
131
+ if (options.skipBuild) config.skipBuild = true;
132
+ if (options.installDevDeps === false) config.installDevDeps = false;
133
+
134
+ // Signing options
135
+ config.sign = options.sign !== false; // Default to true unless --no-sign is used
136
+ if (options.certThumbprint)
137
+ config.certificateThumbprint = options.certThumbprint;
138
+ if (options.certSubject) config.certificateSubject = options.certSubject;
139
+ if (options.certPath) config.certificatePath = options.certPath;
140
+ if (options.certPassword)
141
+ config.certificatePassword = options.certPassword;
142
+ if (options.timestampUrl) config.timestampUrl = options.timestampUrl;
143
+
144
+ // Validate required fields
145
+ if (!config.inputPath) {
146
+ console.error(chalk.red("❌ Error: Input path is required"));
147
+ console.log(
148
+ chalk.blue("Use --input <path> or specify inputPath in config file"),
149
+ );
150
+ process.exit(1);
151
+ }
152
+
153
+ if (!config.appName) {
154
+ console.error(chalk.red("❌ Error: Application name is required"));
155
+ console.log(
156
+ chalk.blue("Use --name <name> or specify appName in config file"),
157
+ );
158
+ process.exit(1);
159
+ }
160
+
161
+ if (!config.publisher) {
162
+ console.error(chalk.red("❌ Error: Publisher name is required"));
163
+ console.log(
164
+ chalk.blue(
165
+ 'Use --publisher "CN=My Company" or specify publisher in config file',
166
+ ),
167
+ );
168
+ process.exit(1);
169
+ }
170
+
171
+ // Create the MSIX package
172
+ const result = await createMsixPackage(config);
173
+
174
+ console.log();
175
+ console.log(chalk.green("🎉 Success!"));
176
+ console.log(chalk.green(`📁 Package: ${result.packagePath}`));
177
+ console.log(chalk.green(`📏 Size: ${result.packageSize}`));
178
+ console.log(chalk.green(`🔐 Signed: ${result.signed ? "Yes" : "No"}`));
179
+ console.log();
180
+ } catch (error) {
181
+ console.log();
182
+ console.error(chalk.red(`❌ Error: ${error.message}`));
183
+ console.log();
184
+
185
+ if (error.name === "ValidationError") {
186
+ console.log(chalk.yellow("💡 Configuration tips:"));
187
+ console.log(
188
+ chalk.yellow(" • Use --help to see all available options"),
189
+ );
190
+ console.log(
191
+ chalk.yellow(" • Create a config file with: node-msix init"),
192
+ );
193
+ console.log(
194
+ chalk.yellow(
195
+ " • Ensure input directory contains a valid Node.js application",
196
+ ),
197
+ );
198
+ }
199
+
200
+ process.exit(1);
201
+ }
202
+ });
203
+
204
+ /**
205
+ * Sign command - signs an existing MSIX package
206
+ */
207
+ program
208
+ .command("sign <packagePath>")
209
+ .description("Sign an existing MSIX package")
210
+ .option(
211
+ "--cert-thumbprint <thumbprint>",
212
+ "Certificate thumbprint for signing",
213
+ )
214
+ .option("--cert-subject <subject>", "Certificate subject name for signing")
215
+ .option("--cert-path <path>", "Path to PFX certificate file")
216
+ .option("--cert-password <password>", "Password for PFX certificate")
217
+ .option("--timestamp-url <url>", "Timestamp server URL")
218
+ .action(async (packagePath, options) => {
219
+ try {
220
+ // Import signing function only when needed
221
+ const { signMsixPackage, CONSTANTS } = require("./index");
222
+
223
+ console.log(chalk.blue(`📦 Node-MSIX v${packageJson.version}`));
224
+ console.log(chalk.blue(`Signing package: ${packagePath}`));
225
+ console.log();
226
+
227
+ const signingConfig = {
228
+ certificateThumbprint: options.certThumbprint,
229
+ certificateSubject: options.certSubject,
230
+ certificatePath: options.certPath,
231
+ certificatePassword: options.certPassword,
232
+ timestampUrl: options.timestampUrl,
233
+ };
234
+
235
+ const result = await signMsixPackage(packagePath, signingConfig);
236
+
237
+ if (result) {
238
+ console.log();
239
+ console.log(chalk.green("🎉 Package signed successfully!"));
240
+ } else {
241
+ console.log();
242
+ console.log(chalk.yellow("⚠️ Package signing failed"));
243
+ process.exit(1);
244
+ }
245
+ } catch (error) {
246
+ console.log();
247
+ console.error(chalk.red(`❌ Error: ${error.message}`));
248
+ process.exit(1);
249
+ }
250
+ });
251
+
252
+ /**
253
+ * List certificates command
254
+ */
255
+ program
256
+ .command("list-certificates")
257
+ .alias("certs")
258
+ .description("List available code signing certificates on this system")
259
+ .action(async () => {
260
+ try {
261
+ // Import certificate functions only when needed
262
+ const { listCertificates } = require("./index");
263
+
264
+ console.log(chalk.blue(`📦 Node-MSIX v${packageJson.version}`));
265
+ console.log();
266
+
267
+ const certificates = await listCertificates();
268
+
269
+ if (certificates.length === 0) {
270
+ console.log(chalk.yellow("⚠️ No code signing certificates found"));
271
+ console.log();
272
+ console.log(chalk.blue("💡 To use certificate signing:"));
273
+ console.log(
274
+ chalk.blue(
275
+ " 1. Install a code signing certificate in your certificate store",
276
+ ),
277
+ );
278
+ console.log(
279
+ chalk.blue(" 2. Ensure the certificate has a private key"),
280
+ );
281
+ console.log(
282
+ chalk.blue(
283
+ ' 3. The certificate should have "Code Signing" capability',
284
+ ),
285
+ );
286
+ console.log();
287
+ return;
288
+ }
289
+
290
+ console.log(
291
+ chalk.green(
292
+ `✅ Found ${certificates.length} code signing certificate(s):`,
293
+ ),
294
+ );
295
+ console.log();
296
+
297
+ certificates.forEach((cert, index) => {
298
+ console.log(chalk.white(`${index + 1}. ${cert.subject}`));
299
+ console.log(chalk.gray(` Thumbprint: ${cert.thumbprint}`));
300
+ console.log(chalk.gray(` Store: ${cert.store}`));
301
+ console.log(
302
+ chalk.gray(` Valid: ${cert.isValid ? "Yes" : "No (Expired)"}`),
303
+ );
304
+ console.log(
305
+ chalk.gray(` Expires: ${cert.notAfter.toLocaleDateString()}`),
306
+ );
307
+ if (index < certificates.length - 1) console.log();
308
+ });
309
+
310
+ console.log();
311
+ console.log(chalk.blue("💡 To use a certificate for signing:"));
312
+ console.log(chalk.blue(" --cert-thumbprint <thumbprint>"));
313
+ console.log(chalk.blue(" or"));
314
+ console.log(chalk.blue(' --cert-subject "part-of-subject-name"'));
315
+ console.log();
316
+ } catch (error) {
317
+ console.log();
318
+ console.error(chalk.red(`❌ Error: ${error.message}`));
319
+ process.exit(1);
320
+ }
321
+ });
322
+
323
+ /**
324
+ * Init command - creates a configuration file
325
+ */
326
+ program
327
+ .command("init [directory]")
328
+ .description("Initialize a new MSIX configuration file")
329
+ .option("-f, --force", "Overwrite existing configuration file")
330
+ .option("--name <name>", "Application name")
331
+ .option("--publisher <publisher>", 'Publisher name (e.g., "CN=My Company")')
332
+ .option("--version <version>", "Application version")
333
+ .option("--description <description>", "Application description")
334
+ .action(async (directory = process.cwd(), options) => {
335
+ try {
336
+ // Import init functions only when needed
337
+ const { initConfig, CONSTANTS } = require("./index");
338
+
339
+ console.log(chalk.blue(`📦 Node-MSIX v${packageJson.version}`));
340
+ console.log(chalk.blue(`Initializing configuration in: ${directory}`));
341
+ console.log();
342
+
343
+ // Check if config already exists and force flag is not set
344
+ const configPath = path.join(directory, CONSTANTS.CONFIG_FILENAME);
345
+ if ((await fs.pathExists(configPath)) && !options.force) {
346
+ console.log(chalk.yellow("⚠️ Configuration file already exists"));
347
+ console.log(chalk.blue("Use --force to overwrite"));
348
+ process.exit(1);
349
+ }
350
+
351
+ const configFile = await initConfig(directory, {
352
+ appName: options.name,
353
+ publisher: options.publisher,
354
+ version: options.version,
355
+ description: options.description,
356
+ });
357
+
358
+ console.log();
359
+ console.log(chalk.green("🎉 Configuration initialized!"));
360
+ console.log(chalk.green(`📁 Config file: ${configFile}`));
361
+ console.log();
362
+ console.log(chalk.blue("💡 Next steps:"));
363
+ console.log(
364
+ chalk.blue(
365
+ " 1. Edit the configuration file to customize your package",
366
+ ),
367
+ );
368
+ console.log(chalk.blue(" 2. Run: node-msix package"));
369
+ console.log();
370
+ } catch (error) {
371
+ console.log();
372
+ console.error(chalk.red(`❌ Error: ${error.message}`));
373
+ process.exit(1);
374
+ }
375
+ });
376
+
377
+ // Show help if no command provided
378
+ if (process.argv.length === 2) {
379
+ program.help();
380
+ }
381
+
382
+ // Parse command line arguments
383
+ program.parse();