@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
package/src/package.js
ADDED
|
@@ -0,0 +1,909 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const fs = require("fs-extra");
|
|
3
|
+
const {
|
|
4
|
+
executeCommand,
|
|
5
|
+
findWindowsSDKTools,
|
|
6
|
+
formatFileSize,
|
|
7
|
+
getLatestNodeVersion,
|
|
8
|
+
} = require("./utils");
|
|
9
|
+
const { determineSigningMethod } = require("./certificates");
|
|
10
|
+
const {
|
|
11
|
+
generateManifest,
|
|
12
|
+
createDefaultAssets,
|
|
13
|
+
generateResourceConfig,
|
|
14
|
+
} = require("./manifest");
|
|
15
|
+
const { CONSTANTS, MSIXError, SigningError } = require("./constants");
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates an MSIX package
|
|
19
|
+
* @param {string} packageDir - Directory containing package contents
|
|
20
|
+
* @param {string} outputPath - Output MSIX file path
|
|
21
|
+
* @returns {string} Path to created MSIX file
|
|
22
|
+
*/
|
|
23
|
+
async function createMsixPackage(packageDir, outputPath) {
|
|
24
|
+
try {
|
|
25
|
+
console.log("Creating MSIX package...");
|
|
26
|
+
|
|
27
|
+
const tools = findWindowsSDKTools();
|
|
28
|
+
|
|
29
|
+
// Ensure output directory exists
|
|
30
|
+
const outputDir = path.dirname(outputPath);
|
|
31
|
+
await fs.ensureDir(outputDir);
|
|
32
|
+
|
|
33
|
+
// Create the MSIX package using makeappx
|
|
34
|
+
const command = `"${tools.makeappx}" pack /d "${packageDir}" /p "${outputPath}" /overwrite`;
|
|
35
|
+
|
|
36
|
+
console.log("Running makeappx...");
|
|
37
|
+
executeCommand(command);
|
|
38
|
+
|
|
39
|
+
// Verify the package was created
|
|
40
|
+
if (!(await fs.pathExists(outputPath))) {
|
|
41
|
+
throw new MSIXError("MSIX package was not created successfully");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const stats = await fs.stat(outputPath);
|
|
45
|
+
console.log(
|
|
46
|
+
`MSIX package created: ${outputPath} (${formatFileSize(stats.size)})`,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
return outputPath;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
throw new MSIXError(`Failed to create MSIX package: ${error.message}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Signs an MSIX package
|
|
57
|
+
* @param {string} packagePath - Path to MSIX package to sign
|
|
58
|
+
* @param {Object} signingConfig - Signing configuration
|
|
59
|
+
* @returns {boolean} True if signing was successful
|
|
60
|
+
*/
|
|
61
|
+
async function signMsixPackage(packagePath, signingConfig) {
|
|
62
|
+
try {
|
|
63
|
+
const signingMethod = await determineSigningMethod(signingConfig);
|
|
64
|
+
|
|
65
|
+
if (!signingMethod) {
|
|
66
|
+
console.log("Signing disabled, skipping...");
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(`Signing package using ${signingMethod.method} method...`);
|
|
71
|
+
|
|
72
|
+
const tools = findWindowsSDKTools();
|
|
73
|
+
let signCommand;
|
|
74
|
+
|
|
75
|
+
if (signingMethod.method === "pfx") {
|
|
76
|
+
// Sign with PFX file
|
|
77
|
+
const { pfxPath, password, timestampUrl } = signingMethod.parameters;
|
|
78
|
+
signCommand = `"${tools.signtool}" sign /f "${pfxPath}" /p "${password}" /tr "${timestampUrl}" /td SHA256 /fd SHA256 "${packagePath}"`;
|
|
79
|
+
} else if (signingMethod.method === "store") {
|
|
80
|
+
// Sign with certificate from store
|
|
81
|
+
const { thumbprint, store, timestampUrl } = signingMethod.parameters;
|
|
82
|
+
const storeLocation = store.toLowerCase() === "localmachine" ? "/sm" : "";
|
|
83
|
+
console.log(
|
|
84
|
+
`Using certificate from ${store} store with thumbprint: ${thumbprint}`,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// For test certificates, try the most basic signing approach first
|
|
88
|
+
if (timestampUrl && timestampUrl.length > 0) {
|
|
89
|
+
try {
|
|
90
|
+
signCommand = `"${tools.signtool}" sign /sha1 "${thumbprint}" /s "My" ${storeLocation} /fd SHA256 "${packagePath}"`;
|
|
91
|
+
console.log("Attempting basic signing without timestamp first...");
|
|
92
|
+
executeCommand(signCommand);
|
|
93
|
+
console.log(
|
|
94
|
+
"Basic signing successful, package signed without timestamp",
|
|
95
|
+
);
|
|
96
|
+
return true;
|
|
97
|
+
} catch (basicError) {
|
|
98
|
+
console.log("Basic signing failed, trying with timestamp...");
|
|
99
|
+
signCommand = `"${tools.signtool}" sign /sha1 "${thumbprint}" /s "My" ${storeLocation} /tr "${timestampUrl}" /td SHA256 /fd SHA256 "${packagePath}"`;
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
console.log("Signing without timestamp server...");
|
|
103
|
+
signCommand = `"${tools.signtool}" sign /sha1 "${thumbprint}" /s "My" ${storeLocation} /fd SHA256 "${packagePath}"`;
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
throw new SigningError(`Unknown signing method: ${signingMethod.method}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log("Running signtool...");
|
|
110
|
+
executeCommand(signCommand);
|
|
111
|
+
|
|
112
|
+
console.log("Package signed successfully");
|
|
113
|
+
return true;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
throw new SigningError(`Failed to sign MSIX package: ${error.message}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Prepares the package directory structure with enhanced workflow
|
|
121
|
+
* This implements the proper Node.js app packaging workflow:
|
|
122
|
+
* 1. Copy source code to temp directory
|
|
123
|
+
* 2. Build the application (install deps, run build scripts)
|
|
124
|
+
* 3. Create executable structure
|
|
125
|
+
* 4. Create proper MSIX directory structure
|
|
126
|
+
* @param {Object} config - Configuration object
|
|
127
|
+
* @param {string} tempDir - Temporary directory for package preparation
|
|
128
|
+
* @param {Object} packageJson - package.json content
|
|
129
|
+
* @returns {string} Path to prepared package directory
|
|
130
|
+
*/
|
|
131
|
+
async function preparePackageDirectory(config, tempDir, packageJson) {
|
|
132
|
+
try {
|
|
133
|
+
console.log("🏗️ Preparing package directory structure...");
|
|
134
|
+
|
|
135
|
+
// Step 1: Create temporary directories for the enhanced workflow
|
|
136
|
+
const sourceDir = path.join(tempDir, "source");
|
|
137
|
+
const buildDir = path.join(tempDir, "build");
|
|
138
|
+
const packageDir = path.join(tempDir, "package");
|
|
139
|
+
const assetsDir = path.join(packageDir, "Assets");
|
|
140
|
+
|
|
141
|
+
await fs.ensureDir(sourceDir);
|
|
142
|
+
await fs.ensureDir(buildDir);
|
|
143
|
+
await fs.ensureDir(packageDir);
|
|
144
|
+
await fs.ensureDir(assetsDir);
|
|
145
|
+
|
|
146
|
+
// Step 2: Copy source code to temporary directory
|
|
147
|
+
console.log("📁 Copying source code to temporary directory...");
|
|
148
|
+
const { copyFiles } = require("./utils");
|
|
149
|
+
await copyFiles(config.inputPath, sourceDir, true);
|
|
150
|
+
|
|
151
|
+
// Step 3: Build the application (install dependencies and run build scripts)
|
|
152
|
+
console.log("🔧 Building the application...");
|
|
153
|
+
await buildApplication(sourceDir, buildDir, packageJson, config);
|
|
154
|
+
|
|
155
|
+
// Step 4: Create executable structure
|
|
156
|
+
console.log("⚙️ Creating executable structure...");
|
|
157
|
+
await createExecutableStructure(buildDir, packageDir, config, packageJson);
|
|
158
|
+
|
|
159
|
+
// Step 5: Generate MSIX directory structure
|
|
160
|
+
console.log("📦 Setting up MSIX directory structure...");
|
|
161
|
+
await setupMsixStructure(packageDir, assetsDir, config, packageJson);
|
|
162
|
+
|
|
163
|
+
console.log("✅ Package directory prepared successfully");
|
|
164
|
+
return packageDir;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
throw new MSIXError(
|
|
167
|
+
`Failed to prepare package directory: ${error.message}`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Builds the Node.js application in the build directory
|
|
174
|
+
* @param {string} sourceDir - Source directory path
|
|
175
|
+
* @param {string} buildDir - Build directory path
|
|
176
|
+
* @param {Object} packageJson - package.json content
|
|
177
|
+
* @param {Object} config - Configuration object
|
|
178
|
+
*/
|
|
179
|
+
async function buildApplication(sourceDir, buildDir, packageJson, config) {
|
|
180
|
+
try {
|
|
181
|
+
// Copy source files to build directory with filtering
|
|
182
|
+
console.log("📋 Copying source files for build...");
|
|
183
|
+
await fs.copy(sourceDir, buildDir, {
|
|
184
|
+
overwrite: true,
|
|
185
|
+
errorOnExist: false,
|
|
186
|
+
filter: (src) => {
|
|
187
|
+
// Skip node_modules, build artifacts, and temporary files
|
|
188
|
+
const relativePath = path.relative(sourceDir, src);
|
|
189
|
+
const excludePatterns = [
|
|
190
|
+
"node_modules",
|
|
191
|
+
".git",
|
|
192
|
+
"dist",
|
|
193
|
+
"build",
|
|
194
|
+
".nyc_output",
|
|
195
|
+
"coverage",
|
|
196
|
+
".vscode",
|
|
197
|
+
".idea",
|
|
198
|
+
"*.log",
|
|
199
|
+
".env",
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
return !excludePatterns.some(
|
|
203
|
+
(pattern) =>
|
|
204
|
+
relativePath.includes(pattern) ||
|
|
205
|
+
relativePath.endsWith(pattern.replace("*", "")),
|
|
206
|
+
);
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Install dependencies (include dev dependencies if build script exists or configured)
|
|
211
|
+
const packageJsonPath = path.join(buildDir, "package.json");
|
|
212
|
+
if (await fs.pathExists(packageJsonPath)) {
|
|
213
|
+
const needsDevDeps =
|
|
214
|
+
config.installDevDeps &&
|
|
215
|
+
((packageJson.scripts && packageJson.scripts.build) ||
|
|
216
|
+
config.installDevDeps === true);
|
|
217
|
+
if (needsDevDeps) {
|
|
218
|
+
console.log(
|
|
219
|
+
"📦 Installing all dependencies (including dev dependencies for build)...",
|
|
220
|
+
);
|
|
221
|
+
await installNodeDependencies(buildDir, false); // false = don't production-only
|
|
222
|
+
} else {
|
|
223
|
+
console.log("📦 Installing production dependencies...");
|
|
224
|
+
await installNodeDependencies(buildDir, true); // true = production-only
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Run build script if defined in package.json and not skipped
|
|
229
|
+
if (!config.skipBuild && packageJson.scripts && packageJson.scripts.build) {
|
|
230
|
+
console.log("🔨 Running build script...");
|
|
231
|
+
const originalCwd = process.cwd();
|
|
232
|
+
try {
|
|
233
|
+
process.chdir(buildDir);
|
|
234
|
+
executeCommand("bun run build");
|
|
235
|
+
} catch (buildError) {
|
|
236
|
+
console.warn(`⚠️ Build script failed: ${buildError.message}`);
|
|
237
|
+
|
|
238
|
+
// Check if this is a TypeScript build issue
|
|
239
|
+
if (
|
|
240
|
+
buildError.message.includes("tsc") ||
|
|
241
|
+
buildError.message.includes("TypeScript")
|
|
242
|
+
) {
|
|
243
|
+
console.log(
|
|
244
|
+
"🔧 Detected TypeScript build failure. Attempting to install TypeScript...",
|
|
245
|
+
);
|
|
246
|
+
try {
|
|
247
|
+
// Try to install TypeScript locally
|
|
248
|
+
executeCommand("bun install typescript --save-dev --silent");
|
|
249
|
+
console.log("📦 TypeScript installed. Retrying build...");
|
|
250
|
+
executeCommand("bun run build");
|
|
251
|
+
console.log("✅ Build succeeded after installing TypeScript");
|
|
252
|
+
} catch (retryError) {
|
|
253
|
+
console.warn(
|
|
254
|
+
`⚠️ Build still failed after installing TypeScript: ${retryError.message}`,
|
|
255
|
+
);
|
|
256
|
+
console.log(
|
|
257
|
+
"📝 Continuing without build step. You may need to manually build your application before packaging.",
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
console.warn(
|
|
262
|
+
"📝 Continuing without build step. Ensure your application is pre-built if needed.",
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
} finally {
|
|
266
|
+
process.chdir(originalCwd);
|
|
267
|
+
}
|
|
268
|
+
} else if (config.skipBuild) {
|
|
269
|
+
console.log("⏭️ Skipping build step as requested in configuration");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
console.log("✅ Application built successfully");
|
|
273
|
+
} catch (error) {
|
|
274
|
+
throw new MSIXError(`Failed to build application: ${error.message}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Creates the executable structure for the MSIX package
|
|
280
|
+
* @param {string} buildDir - Build directory path
|
|
281
|
+
* @param {string} packageDir - Package directory path
|
|
282
|
+
* @param {Object} config - Configuration object
|
|
283
|
+
* @param {Object} packageJson - package.json content
|
|
284
|
+
*/
|
|
285
|
+
async function createExecutableStructure(
|
|
286
|
+
buildDir,
|
|
287
|
+
packageDir,
|
|
288
|
+
config,
|
|
289
|
+
packageJson,
|
|
290
|
+
) {
|
|
291
|
+
try {
|
|
292
|
+
// Check for pre-built executable (e.g., bun-compiled binary)
|
|
293
|
+
if (config.prebuiltExecutablePath) {
|
|
294
|
+
const prebuiltPath = path.resolve(
|
|
295
|
+
config.inputPath,
|
|
296
|
+
config.prebuiltExecutablePath,
|
|
297
|
+
);
|
|
298
|
+
if (await fs.pathExists(prebuiltPath)) {
|
|
299
|
+
const exeName = path.basename(prebuiltPath);
|
|
300
|
+
console.log(`✅ Using pre-built executable: ${exeName}`);
|
|
301
|
+
await fs.copyFile(prebuiltPath, path.join(packageDir, exeName));
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
throw new Error(
|
|
305
|
+
`Pre-built executable not found: ${prebuiltPath}. Run the compile step first.`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Copy built application files to package directory
|
|
310
|
+
console.log("📁 Copying built application files...");
|
|
311
|
+
await fs.copy(buildDir, packageDir, {
|
|
312
|
+
overwrite: true,
|
|
313
|
+
errorOnExist: false,
|
|
314
|
+
filter: (src) => {
|
|
315
|
+
// Skip development files
|
|
316
|
+
const basename = path.basename(src);
|
|
317
|
+
const excludeFiles = [
|
|
318
|
+
".gitignore",
|
|
319
|
+
".env.example",
|
|
320
|
+
"README.md",
|
|
321
|
+
"CHANGELOG.md",
|
|
322
|
+
"LICENSE",
|
|
323
|
+
"tsconfig.json",
|
|
324
|
+
"jest.config.js",
|
|
325
|
+
"webpack.config.js",
|
|
326
|
+
];
|
|
327
|
+
return !excludeFiles.includes(basename);
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Ensure Node.js runtime is available
|
|
332
|
+
console.log("🟢 Ensuring Node.js runtime...");
|
|
333
|
+
await ensureNodeRuntime(packageDir, config);
|
|
334
|
+
|
|
335
|
+
// Create startup script if the main executable is node.exe
|
|
336
|
+
if (
|
|
337
|
+
config.executable === "node.exe" ||
|
|
338
|
+
config.executable.endsWith("node.exe")
|
|
339
|
+
) {
|
|
340
|
+
await createNodeStartupScript(packageDir, packageJson, config);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Try to create SEA (Single Executable Application) first
|
|
344
|
+
const {
|
|
345
|
+
createSingleExecutableApp,
|
|
346
|
+
createFallbackLauncher,
|
|
347
|
+
} = require("./sea-handler");
|
|
348
|
+
const seaSuccess = await createSingleExecutableApp(
|
|
349
|
+
packageDir,
|
|
350
|
+
config,
|
|
351
|
+
packageJson,
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
// Create fallback launcher if SEA creation failed
|
|
355
|
+
if (!seaSuccess) {
|
|
356
|
+
console.log("📄 Creating fallback Node.js launcher...");
|
|
357
|
+
await createFallbackLauncher(packageDir, config, packageJson);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
console.log("✅ Executable structure created successfully");
|
|
361
|
+
} catch (error) {
|
|
362
|
+
throw new MSIXError(
|
|
363
|
+
`Failed to create executable structure: ${error.message}`,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Sets up the MSIX directory structure with manifests and assets
|
|
370
|
+
* @param {string} packageDir - Package directory path
|
|
371
|
+
* @param {string} assetsDir - Assets directory path
|
|
372
|
+
* @param {Object} config - Configuration object
|
|
373
|
+
* @param {Object} packageJson - package.json content
|
|
374
|
+
*/
|
|
375
|
+
async function setupMsixStructure(packageDir, assetsDir, config, packageJson) {
|
|
376
|
+
try {
|
|
377
|
+
// Generate AppxManifest.xml
|
|
378
|
+
console.log("📄 Generating AppxManifest.xml...");
|
|
379
|
+
const manifestContent = generateManifest(config, packageJson);
|
|
380
|
+
await fs.writeFile(
|
|
381
|
+
path.join(packageDir, "AppxManifest.xml"),
|
|
382
|
+
manifestContent,
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
// Create package assets
|
|
386
|
+
console.log("🎨 Creating package assets...");
|
|
387
|
+
await createDefaultAssets(assetsDir, config.icon);
|
|
388
|
+
|
|
389
|
+
// Create MCP Server configuration
|
|
390
|
+
console.log("🔧 Creating MCP Server configuration...");
|
|
391
|
+
await createMcpServerConfig(assetsDir, config);
|
|
392
|
+
|
|
393
|
+
// Generate resource configuration
|
|
394
|
+
console.log("⚙️ Generating resource configuration...");
|
|
395
|
+
const resourceConfig = generateResourceConfig(config);
|
|
396
|
+
await fs.writeFile(
|
|
397
|
+
path.join(packageDir, "msix-resource-config.json"),
|
|
398
|
+
resourceConfig,
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
// Create registry entries if needed
|
|
402
|
+
await createRegistryEntries(packageDir, config, packageJson);
|
|
403
|
+
|
|
404
|
+
console.log("✅ MSIX structure setup completed");
|
|
405
|
+
} catch (error) {
|
|
406
|
+
throw new MSIXError(`Failed to setup MSIX structure: ${error.message}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Creates a Node.js startup script
|
|
412
|
+
* @param {string} packageDir - Package directory path
|
|
413
|
+
* @param {Object} packageJson - package.json content
|
|
414
|
+
* @param {Object} config - Configuration object
|
|
415
|
+
*/
|
|
416
|
+
async function createNodeStartupScript(packageDir, packageJson, config) {
|
|
417
|
+
try {
|
|
418
|
+
const mainScript = packageJson.main || "index.js";
|
|
419
|
+
const startScript = packageJson.scripts && packageJson.scripts.start;
|
|
420
|
+
|
|
421
|
+
// Create batch file to start the Node.js application
|
|
422
|
+
let batchContent;
|
|
423
|
+
|
|
424
|
+
if (startScript && !startScript.includes("node ")) {
|
|
425
|
+
// Use the start script if it doesn't already include 'node'
|
|
426
|
+
batchContent = `@echo off
|
|
427
|
+
title ${config.displayName || config.appName}
|
|
428
|
+
cd /d "%~dp0"
|
|
429
|
+
echo Starting ${config.displayName || config.appName}...
|
|
430
|
+
bun run start
|
|
431
|
+
if errorlevel 1 (
|
|
432
|
+
echo.
|
|
433
|
+
echo Failed to start the application.
|
|
434
|
+
echo Make sure Node.js is installed on this system.
|
|
435
|
+
echo.
|
|
436
|
+
pause
|
|
437
|
+
exit /b 1
|
|
438
|
+
)
|
|
439
|
+
`;
|
|
440
|
+
} else {
|
|
441
|
+
// Create direct node execution with configurable args
|
|
442
|
+
const nodeArgs = config.executableArgs || mainScript;
|
|
443
|
+
batchContent = `@echo off
|
|
444
|
+
title ${config.displayName || config.appName}
|
|
445
|
+
cd /d "%~dp0"
|
|
446
|
+
echo Starting ${config.displayName || config.appName}...
|
|
447
|
+
node ${nodeArgs} %*
|
|
448
|
+
if errorlevel 1 (
|
|
449
|
+
echo.
|
|
450
|
+
echo Failed to start the application.
|
|
451
|
+
echo Make sure Node.js is installed on this system.
|
|
452
|
+
echo.
|
|
453
|
+
pause
|
|
454
|
+
exit /b 1
|
|
455
|
+
)
|
|
456
|
+
`;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const batchPath = path.join(packageDir, "start.bat");
|
|
460
|
+
await fs.writeFile(batchPath, batchContent);
|
|
461
|
+
|
|
462
|
+
// Also create a silent launcher for background services
|
|
463
|
+
const silentBatchContent = `@echo off
|
|
464
|
+
cd /d "%~dp0"
|
|
465
|
+
start "" /min cmd /c "node "${mainScript}" %*"
|
|
466
|
+
`;
|
|
467
|
+
|
|
468
|
+
const silentBatchPath = path.join(packageDir, "start-silent.bat");
|
|
469
|
+
await fs.writeFile(silentBatchPath, silentBatchContent);
|
|
470
|
+
|
|
471
|
+
console.log("📄 Created Node.js startup scripts (interactive and silent)");
|
|
472
|
+
} catch (error) {
|
|
473
|
+
console.warn(`Warning: Could not create startup script: ${error.message}`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Creates registry entries for the application if needed
|
|
479
|
+
* @param {string} packageDir - Package directory path
|
|
480
|
+
* @param {Object} config - Configuration object
|
|
481
|
+
* @param {Object} packageJson - package.json content
|
|
482
|
+
*/
|
|
483
|
+
async function createRegistryEntries(packageDir, config, packageJson) {
|
|
484
|
+
try {
|
|
485
|
+
// Create a registry file for application registration
|
|
486
|
+
const regContent = `Windows Registry Editor Version 5.00
|
|
487
|
+
|
|
488
|
+
[HKEY_CURRENT_USER\\Software\\Classes\\Applications\\${config.executable}]
|
|
489
|
+
"FriendlyAppName"="${config.displayName || config.appName}"
|
|
490
|
+
|
|
491
|
+
[HKEY_CURRENT_USER\\Software\\Classes\\Applications\\${config.executable}\\shell]
|
|
492
|
+
|
|
493
|
+
[HKEY_CURRENT_USER\\Software\\Classes\\Applications\\${config.executable}\\shell\\open]
|
|
494
|
+
|
|
495
|
+
[HKEY_CURRENT_USER\\Software\\Classes\\Applications\\${config.executable}\\shell\\open\\command]
|
|
496
|
+
@="\\"${config.executable}\\" \\"%1\\""
|
|
497
|
+
`;
|
|
498
|
+
|
|
499
|
+
const regPath = path.join(packageDir, "app-registration.reg");
|
|
500
|
+
await fs.writeFile(regPath, regContent);
|
|
501
|
+
|
|
502
|
+
console.log("📋 Created registry entries");
|
|
503
|
+
} catch (error) {
|
|
504
|
+
console.warn(
|
|
505
|
+
`Warning: Could not create registry entries: ${error.message}`,
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Creates MCP Server configuration file in the Assets directory
|
|
512
|
+
* @param {string} assetsDir - Assets directory path
|
|
513
|
+
* @param {Object} config - Configuration object
|
|
514
|
+
*/
|
|
515
|
+
async function createMcpServerConfig(assetsDir, config) {
|
|
516
|
+
try {
|
|
517
|
+
// Generate the executable name from config
|
|
518
|
+
let executableName;
|
|
519
|
+
if (config.executable && config.executable.endsWith(".exe")) {
|
|
520
|
+
// Use the configured executable name
|
|
521
|
+
executableName = config.executable;
|
|
522
|
+
} else {
|
|
523
|
+
// Generate executable name from app name
|
|
524
|
+
const appName = config.appName || config.displayName || "app";
|
|
525
|
+
executableName = `${appName.replace(/[^a-zA-Z0-9.-]/g, "_")}.exe`;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Create MCP server configuration
|
|
529
|
+
const mcpConfig = {
|
|
530
|
+
version: 1,
|
|
531
|
+
mcpServers: [
|
|
532
|
+
{
|
|
533
|
+
name: `${config.packageName || config.appName}MCPServer`,
|
|
534
|
+
type: "stdio",
|
|
535
|
+
command: executableName,
|
|
536
|
+
},
|
|
537
|
+
],
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
const configPath = path.join(assetsDir, "mcpServerConfig.json");
|
|
541
|
+
await fs.writeFile(configPath, JSON.stringify(mcpConfig, null, 2));
|
|
542
|
+
|
|
543
|
+
console.log("📄 Created MCP server configuration: mcpServerConfig.json");
|
|
544
|
+
} catch (error) {
|
|
545
|
+
console.warn(
|
|
546
|
+
`Warning: Could not create MCP server configuration: ${error.message}`,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Installs Node.js dependencies in the package directory
|
|
553
|
+
* @param {string} packageDir - Package directory path
|
|
554
|
+
* @param {boolean} productionOnly - Whether to install only production dependencies
|
|
555
|
+
*/
|
|
556
|
+
async function installNodeDependencies(packageDir, productionOnly = true) {
|
|
557
|
+
try {
|
|
558
|
+
const originalCwd = process.cwd();
|
|
559
|
+
process.chdir(packageDir);
|
|
560
|
+
|
|
561
|
+
try {
|
|
562
|
+
// Use bun install for faster, reliable, reproducible builds
|
|
563
|
+
const productionFlag = productionOnly ? "--production" : "";
|
|
564
|
+
executeCommand(`bun install ${productionFlag} --silent`);
|
|
565
|
+
} catch (error) {
|
|
566
|
+
// Fallback to bun install
|
|
567
|
+
console.log("bun install failed, falling back to bun install...");
|
|
568
|
+
const productionFlag = productionOnly ? "--production" : "";
|
|
569
|
+
executeCommand(`bun install ${productionFlag} --silent`);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
process.chdir(originalCwd);
|
|
573
|
+
console.log("Dependencies installed successfully");
|
|
574
|
+
} catch (error) {
|
|
575
|
+
throw new MSIXError(
|
|
576
|
+
`Failed to install Node.js dependencies: ${error.message}`,
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Ensures Node.js runtime is available in the package
|
|
583
|
+
* @param {string} packageDir - Package directory path
|
|
584
|
+
* @param {Object} config - Configuration object
|
|
585
|
+
*/
|
|
586
|
+
async function ensureNodeRuntime(packageDir, config) {
|
|
587
|
+
try {
|
|
588
|
+
const nodeExecutablePath = path.join(packageDir, "node.exe");
|
|
589
|
+
|
|
590
|
+
// Always remove existing node.exe to ensure fresh copy
|
|
591
|
+
if (await fs.pathExists(nodeExecutablePath)) {
|
|
592
|
+
console.log(
|
|
593
|
+
"🗑️ Removing existing Node.js runtime to ensure fresh copy...",
|
|
594
|
+
);
|
|
595
|
+
await fs.remove(nodeExecutablePath);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Check if we have a bundled Node.js runtime
|
|
599
|
+
const bundledNodePath = path.join(
|
|
600
|
+
__dirname,
|
|
601
|
+
"..",
|
|
602
|
+
"runtime",
|
|
603
|
+
"nodejs",
|
|
604
|
+
"node.exe",
|
|
605
|
+
);
|
|
606
|
+
if (await fs.pathExists(bundledNodePath)) {
|
|
607
|
+
console.log("Copying bundled Node.js runtime...");
|
|
608
|
+
await fs.copy(bundledNodePath, nodeExecutablePath);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Try to copy system Node.js executable using fresh binary detection
|
|
613
|
+
try {
|
|
614
|
+
console.log("🔍 Attempting to get fresh Node.js binary...");
|
|
615
|
+
const { getFreshNodeBinary } = require("./sea-handler");
|
|
616
|
+
|
|
617
|
+
// Use the enhanced fresh binary detection that avoids SEA markers
|
|
618
|
+
const freshNodePath = await getFreshNodeBinary();
|
|
619
|
+
console.log(`🔍 Fresh binary search result: ${freshNodePath}`);
|
|
620
|
+
|
|
621
|
+
if (freshNodePath && (await fs.pathExists(freshNodePath))) {
|
|
622
|
+
console.log(`Copying fresh Node.js runtime from: ${freshNodePath}`);
|
|
623
|
+
|
|
624
|
+
// Only copy the node.exe file, not entire directories
|
|
625
|
+
await fs.copyFile(freshNodePath, nodeExecutablePath);
|
|
626
|
+
|
|
627
|
+
// Verify the copy worked
|
|
628
|
+
if (await fs.pathExists(nodeExecutablePath)) {
|
|
629
|
+
console.log("✅ Fresh Node.js runtime copied successfully");
|
|
630
|
+
|
|
631
|
+
// If config is using node.exe, also create a startup script
|
|
632
|
+
if (config.executable === "node.exe") {
|
|
633
|
+
await createNodeStartupWrapper(packageDir, config);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
} catch (error) {
|
|
640
|
+
console.warn(`Warning: Could not copy fresh Node.js: ${error.message}`);
|
|
641
|
+
console.log(`Error details: ${error.stack}`);
|
|
642
|
+
|
|
643
|
+
// Fallback to basic system node lookup
|
|
644
|
+
try {
|
|
645
|
+
console.log("🔙 Falling back to basic system Node.js lookup...");
|
|
646
|
+
const { executeCommand } = require("./utils");
|
|
647
|
+
const wherePath = process.platform === "win32" ? "where" : "which";
|
|
648
|
+
const systemNodePath = executeCommand(`${wherePath} node`, {
|
|
649
|
+
silent: true,
|
|
650
|
+
})
|
|
651
|
+
.trim()
|
|
652
|
+
.split("\n")[0];
|
|
653
|
+
|
|
654
|
+
if (systemNodePath && (await fs.pathExists(systemNodePath))) {
|
|
655
|
+
console.log(`Copying system Node.js runtime from: ${systemNodePath}`);
|
|
656
|
+
|
|
657
|
+
// Only copy the node.exe file, not entire directories
|
|
658
|
+
await fs.copyFile(systemNodePath, nodeExecutablePath);
|
|
659
|
+
|
|
660
|
+
// Verify the copy worked
|
|
661
|
+
if (await fs.pathExists(nodeExecutablePath)) {
|
|
662
|
+
console.log("✅ Node.js runtime copied successfully");
|
|
663
|
+
|
|
664
|
+
// If config is using node.exe, also create a startup script
|
|
665
|
+
if (config.executable === "node.exe") {
|
|
666
|
+
await createNodeStartupWrapper(packageDir, config);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
} catch (fallbackError) {
|
|
673
|
+
console.warn(
|
|
674
|
+
`Warning: Fallback Node.js copy also failed: ${fallbackError.message}`,
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// If we can't find Node.js, try to download it
|
|
680
|
+
await downloadNodejs(packageDir, config.architecture || "x64");
|
|
681
|
+
} catch (error) {
|
|
682
|
+
console.warn(`Warning: Could not ensure Node.js runtime: ${error.message}`);
|
|
683
|
+
console.warn(
|
|
684
|
+
"📝 The application may not work without Node.js installed on the target system.",
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Creates a Node.js startup wrapper when using node.exe directly
|
|
691
|
+
* @param {string} packageDir - Package directory path
|
|
692
|
+
* @param {Object} config - Configuration object
|
|
693
|
+
*/
|
|
694
|
+
async function createNodeStartupWrapper(packageDir, config) {
|
|
695
|
+
try {
|
|
696
|
+
// Create a launcher.js that handles the startup logic
|
|
697
|
+
const launcherContent = `const { spawn } = require('child_process');
|
|
698
|
+
const path = require('path');
|
|
699
|
+
|
|
700
|
+
// Determine the main script to run
|
|
701
|
+
const mainScript = 'app.js'; // Default for our sample app
|
|
702
|
+
|
|
703
|
+
console.log('Starting ${config.displayName || config.appName}...');
|
|
704
|
+
|
|
705
|
+
// Launch the main application
|
|
706
|
+
const child = spawn(process.execPath, [mainScript], {
|
|
707
|
+
stdio: 'inherit',
|
|
708
|
+
cwd: __dirname
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
child.on('error', (err) => {
|
|
712
|
+
console.error('Failed to start application:', err.message);
|
|
713
|
+
console.log('Press any key to exit...');
|
|
714
|
+
process.stdin.once('data', () => process.exit(1));
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
child.on('exit', (code) => {
|
|
718
|
+
if (code !== 0) {
|
|
719
|
+
console.log('\\nApplication exited with code:', code);
|
|
720
|
+
console.log('Press any key to exit...');
|
|
721
|
+
process.stdin.once('data', () => process.exit(code));
|
|
722
|
+
} else {
|
|
723
|
+
process.exit(0);
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
// Handle termination gracefully
|
|
728
|
+
process.on('SIGINT', () => {
|
|
729
|
+
child.kill('SIGINT');
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
process.on('SIGTERM', () => {
|
|
733
|
+
child.kill('SIGTERM');
|
|
734
|
+
});
|
|
735
|
+
`;
|
|
736
|
+
|
|
737
|
+
const launcherPath = path.join(packageDir, "launcher.js");
|
|
738
|
+
await fs.writeFile(launcherPath, launcherContent);
|
|
739
|
+
|
|
740
|
+
console.log("📄 Created Node.js startup wrapper");
|
|
741
|
+
} catch (error) {
|
|
742
|
+
console.warn(
|
|
743
|
+
`Warning: Could not create Node.js startup wrapper: ${error.message}`,
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Downloads Node.js runtime for the specified architecture
|
|
750
|
+
* @param {string} packageDir - Package directory path
|
|
751
|
+
* @param {string} architecture - Target architecture (x64, x86)
|
|
752
|
+
*/
|
|
753
|
+
async function downloadNodejs(packageDir, architecture) {
|
|
754
|
+
try {
|
|
755
|
+
console.log(
|
|
756
|
+
`📥 Attempting to download Node.js runtime for ${architecture}...`,
|
|
757
|
+
);
|
|
758
|
+
|
|
759
|
+
// Get the latest Node.js version dynamically
|
|
760
|
+
const nodeVersion = await getLatestNodeVersion();
|
|
761
|
+
console.log(`🎯 Using Node.js version: ${nodeVersion}`);
|
|
762
|
+
|
|
763
|
+
const https = require("https");
|
|
764
|
+
const archMap = { x64: "x64", x86: "x86", arm64: "arm64" };
|
|
765
|
+
const nodeArch = archMap[architecture] || "x64";
|
|
766
|
+
|
|
767
|
+
const downloadUrl = `https://nodejs.org/dist/${nodeVersion}/win-${nodeArch}/node.exe`;
|
|
768
|
+
const nodeExecutablePath = path.join(packageDir, "node.exe");
|
|
769
|
+
|
|
770
|
+
await new Promise((resolve, reject) => {
|
|
771
|
+
const file = fs.createWriteStream(nodeExecutablePath);
|
|
772
|
+
|
|
773
|
+
https
|
|
774
|
+
.get(downloadUrl, (response) => {
|
|
775
|
+
if (response.statusCode === 200) {
|
|
776
|
+
response.pipe(file);
|
|
777
|
+
file.on("finish", () => {
|
|
778
|
+
file.close();
|
|
779
|
+
console.log("✅ Node.js runtime downloaded successfully");
|
|
780
|
+
resolve();
|
|
781
|
+
});
|
|
782
|
+
} else {
|
|
783
|
+
reject(
|
|
784
|
+
new Error(
|
|
785
|
+
`Failed to download Node.js: HTTP ${response.statusCode}`,
|
|
786
|
+
),
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
})
|
|
790
|
+
.on("error", (err) => {
|
|
791
|
+
fs.unlink(nodeExecutablePath, () => {}); // Clean up on error
|
|
792
|
+
reject(err);
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
} catch (error) {
|
|
796
|
+
console.warn(
|
|
797
|
+
`Warning: Could not download Node.js runtime: ${error.message}`,
|
|
798
|
+
);
|
|
799
|
+
console.warn(
|
|
800
|
+
"💡 Consider manually placing node.exe in your input directory.",
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Validates the prepared package
|
|
807
|
+
* @param {string} packageDir - Package directory path
|
|
808
|
+
* @returns {Object} Validation results
|
|
809
|
+
*/
|
|
810
|
+
async function validatePackage(packageDir) {
|
|
811
|
+
const issues = [];
|
|
812
|
+
const warnings = [];
|
|
813
|
+
|
|
814
|
+
try {
|
|
815
|
+
// Check for required files
|
|
816
|
+
const requiredFiles = ["AppxManifest.xml"];
|
|
817
|
+
for (const file of requiredFiles) {
|
|
818
|
+
const filePath = path.join(packageDir, file);
|
|
819
|
+
if (!(await fs.pathExists(filePath))) {
|
|
820
|
+
issues.push(`Missing required file: ${file}`);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Check for executable
|
|
825
|
+
const manifestPath = path.join(packageDir, "AppxManifest.xml");
|
|
826
|
+
if (await fs.pathExists(manifestPath)) {
|
|
827
|
+
const manifestContent = await fs.readFile(manifestPath, "utf8");
|
|
828
|
+
const executableMatch = manifestContent.match(/Executable="([^"]+)"/);
|
|
829
|
+
|
|
830
|
+
if (executableMatch) {
|
|
831
|
+
const executablePath = path.join(packageDir, executableMatch[1]);
|
|
832
|
+
if (!(await fs.pathExists(executablePath))) {
|
|
833
|
+
issues.push(`Executable not found: ${executableMatch[1]}`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Check for assets
|
|
839
|
+
const assetsDir = path.join(packageDir, "Assets");
|
|
840
|
+
const requiredAssets = [
|
|
841
|
+
"Square44x44Logo.png",
|
|
842
|
+
"Square150x150Logo.png",
|
|
843
|
+
"StoreLogo.png",
|
|
844
|
+
];
|
|
845
|
+
|
|
846
|
+
for (const asset of requiredAssets) {
|
|
847
|
+
const assetPath = path.join(assetsDir, asset);
|
|
848
|
+
if (!(await fs.pathExists(assetPath))) {
|
|
849
|
+
warnings.push(`Missing asset: ${asset}`);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// Check package size
|
|
854
|
+
const packageSize = await getDirectorySize(packageDir);
|
|
855
|
+
if (packageSize > CONSTANTS.MAX_PACKAGE_SIZE) {
|
|
856
|
+
warnings.push(
|
|
857
|
+
`Package size (${formatFileSize(packageSize)}) exceeds recommended maximum (${formatFileSize(CONSTANTS.MAX_PACKAGE_SIZE)})`,
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
return {
|
|
862
|
+
isValid: issues.length === 0,
|
|
863
|
+
issues,
|
|
864
|
+
warnings,
|
|
865
|
+
packageSize: formatFileSize(packageSize),
|
|
866
|
+
};
|
|
867
|
+
} catch (error) {
|
|
868
|
+
return {
|
|
869
|
+
isValid: false,
|
|
870
|
+
issues: [`Validation failed: ${error.message}`],
|
|
871
|
+
warnings: [],
|
|
872
|
+
packageSize: "Unknown",
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Gets the total size of a directory
|
|
879
|
+
* @param {string} dirPath - Directory path
|
|
880
|
+
* @returns {number} Size in bytes
|
|
881
|
+
*/
|
|
882
|
+
async function getDirectorySize(dirPath) {
|
|
883
|
+
let totalSize = 0;
|
|
884
|
+
|
|
885
|
+
async function calculateSize(currentPath) {
|
|
886
|
+
const stats = await fs.stat(currentPath);
|
|
887
|
+
|
|
888
|
+
if (stats.isFile()) {
|
|
889
|
+
totalSize += stats.size;
|
|
890
|
+
} else if (stats.isDirectory()) {
|
|
891
|
+
const items = await fs.readdir(currentPath);
|
|
892
|
+
for (const item of items) {
|
|
893
|
+
await calculateSize(path.join(currentPath, item));
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
await calculateSize(dirPath);
|
|
899
|
+
return totalSize;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
module.exports = {
|
|
903
|
+
createMsixPackage,
|
|
904
|
+
signMsixPackage,
|
|
905
|
+
preparePackageDirectory,
|
|
906
|
+
validatePackage,
|
|
907
|
+
installNodeDependencies,
|
|
908
|
+
ensureNodeRuntime,
|
|
909
|
+
};
|