@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.
- package/README.md +360 -0
- package/package.json +57 -0
- package/src/certificates.js +320 -0
- package/src/cli.js +383 -0
- package/src/constants.js +140 -0
- package/src/index.js +414 -0
- package/src/manifest.js +389 -0
- package/src/package.js +909 -0
- package/src/sea-handler-new.js +301 -0
- package/src/sea-handler.js +1124 -0
- package/src/utils.js +292 -0
- package/src/validation.js +228 -0
|
@@ -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();
|