@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,1124 @@
1
+ const path = require("path");
2
+ const fs = require("fs-extra");
3
+ const os = require("os");
4
+ const { executeCommand } = require("./utils");
5
+
6
+ /**
7
+ * Creates a Single Executable Application (SEA) using Node.js built-in capabilities with NCC bundling
8
+ * @param {string} packageDir - Directory cont // If postject fails due to multiple sentinels and we haven't tried overwrite yet
9
+ if (postjectError.message.includes('Multiple occurences of sentinel')) {
10
+ console.log('🔧 Detected multiple sentinels, attempting overwrite recovery...');
11
+
12
+ try {
13
+ const overwriteCommand = `npx postject "${executableName}" NODE_SEA_BLOB sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 --overwrite`;
14
+ executeCommand(overwriteCommand);
15
+ console.log('✅ SEA blob injection successful with overwrite');
16
+ } catch (overwriteError) {
17
+ console.error(`❌ SEA injection failed even with overwrite: ${overwriteError.message}`);
18
+
19
+ // If still failing, try a completely different approach - create a truly fresh binary
20
+ console.log('🔧 Attempting to create completely fresh binary...');
21
+ const superFreshPath = await createSuperFreshBinary();
22
+ if (superFreshPath && await fs.pathExists(superFreshPath)) {
23
+ console.log('📋 Copying super fresh binary...');
24
+ await fs.copyFile(superFreshPath, executablePath);
25
+
26
+ // Try injection one more time
27
+ try {
28
+ const finalCommand = `npx postject "${executableName}" NODE_SEA_BLOB sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2`;
29
+ executeCommand(finalCommand);
30
+ console.log('✅ SEA blob injection successful with super fresh binary');
31
+ } catch (finalError) {
32
+ throw new Error(`All SEA injection attempts failed: ${finalError.message}`);
33
+ }
34
+ } else {
35
+ throw new Error(`Could not create super fresh binary: ${overwriteError.message}`);
36
+ }
37
+ }ckage
38
+ * @param {Object} config - Configuration object
39
+ * @param {Object} packageJson - package.json content
40
+ * @returns {Promise<boolean>} Success status
41
+ */
42
+ async function createSingleExecutableApp(packageDir, config, packageJson) {
43
+ try {
44
+ console.log(
45
+ "🔨 Creating Single Executable Application (SEA) with NCC bundling...",
46
+ );
47
+
48
+ const appName = config.appName || "app";
49
+ const executableName = `${appName.replace(/[^a-zA-Z0-9.-]/g, "_")}.exe`;
50
+ const executablePath = path.join(packageDir, executableName);
51
+ const tempNodeBinary = path.join(packageDir, "temp-node.exe");
52
+
53
+ // Step 1: Install NCC and TypeScript dependencies if needed
54
+ console.log("📦 Installing @vercel/ncc...");
55
+ try {
56
+ executeCommand("bun install @vercel/ncc --no-save", { cwd: packageDir });
57
+ } catch (nccError) {
58
+ console.log("Installing NCC globally...");
59
+ executeCommand("bun install -g @vercel/ncc", { cwd: packageDir });
60
+ }
61
+
62
+ // Install TypeScript if TypeScript files are detected
63
+ const hasTypeScript =
64
+ (await fs.pathExists(path.join(packageDir, "tsconfig.json"))) ||
65
+ (await fs.readdir(packageDir)).some((file) => file.endsWith(".ts"));
66
+
67
+ if (hasTypeScript) {
68
+ console.log(
69
+ "📝 TypeScript detected - ensuring TypeScript compiler is available",
70
+ );
71
+ try {
72
+ executeCommand("bun install typescript --no-save", { cwd: packageDir });
73
+ } catch (tsError) {
74
+ console.log("Installing TypeScript globally...");
75
+ executeCommand("bun install -g typescript", { cwd: packageDir });
76
+ }
77
+ }
78
+
79
+ // Step 2: Detect entry point and handle TypeScript
80
+ const mainScript = packageJson.main || "index.js";
81
+ const isTypeScript =
82
+ mainScript.endsWith(".ts") ||
83
+ (await fs.pathExists(path.join(packageDir, "tsconfig.json")));
84
+
85
+ // Auto-detect TypeScript entry point if main is JS but TS files exist
86
+ let entryScript = mainScript;
87
+ if (!isTypeScript && !mainScript.endsWith(".ts")) {
88
+ const tsAlternatives = [
89
+ mainScript.replace(/\.js$/, ".ts"),
90
+ "index.ts",
91
+ "src/index.ts",
92
+ "app.ts",
93
+ "src/app.ts",
94
+ ];
95
+
96
+ for (const tsFile of tsAlternatives) {
97
+ if (await fs.pathExists(path.join(packageDir, tsFile))) {
98
+ entryScript = tsFile;
99
+ console.log(`📝 Detected TypeScript entry: ${tsFile}`);
100
+ break;
101
+ }
102
+ }
103
+ }
104
+
105
+ const entryContent = `#!/usr/bin/env node
106
+ // SEA Entry Point for ${config.displayName || config.appName}
107
+
108
+ console.log('🚀 Starting ${config.displayName || config.appName}...');
109
+
110
+ // Check if running as SEA
111
+ let isSEA = false;
112
+ try {
113
+ const { sea } = require('node:sea');
114
+ isSEA = sea.isSea();
115
+ if (isSEA) {
116
+ console.log('Running in SEA mode');
117
+ // Set process title
118
+ process.title = '${config.displayName || config.appName}';
119
+ }
120
+ } catch (err) {
121
+ // SEA module not available, running in regular Node.js
122
+ console.log('Running in development mode');
123
+ }
124
+
125
+ // Set working directory to executable directory
126
+ try {
127
+ const path = require('path');
128
+ const execDir = path.dirname(process.execPath);
129
+ process.chdir(execDir);
130
+ } catch (err) {
131
+ console.warn('Could not change working directory:', err.message);
132
+ }
133
+
134
+ // Load the main application
135
+ try {
136
+ require('./${entryScript}');
137
+ } catch (error) {
138
+ console.error('❌ Failed to start application:', error.message);
139
+ console.error(error.stack);
140
+ process.exit(1);
141
+ }
142
+ `;
143
+
144
+ const entryPath = path.join(packageDir, "sea-entry.js");
145
+ await fs.writeFile(entryPath, entryContent);
146
+
147
+ // Step 3: Bundle with NCC (with TypeScript support)
148
+ console.log("📦 Bundling application with NCC...");
149
+
150
+ // NCC automatically handles TypeScript files - no additional config needed!
151
+ let bundleCommand = `npx ncc build sea-entry.js -o sea-dist --minify --no-source-map-register`;
152
+
153
+ // Add TypeScript-specific options if needed
154
+ if (isTypeScript || entryScript.endsWith(".ts")) {
155
+ console.log(
156
+ "📝 TypeScript detected - NCC will handle transpilation automatically",
157
+ );
158
+ // NCC will automatically detect and use tsconfig.json if present
159
+ bundleCommand += ` --target es2020`; // Ensure compatibility with Node.js SEA
160
+ }
161
+
162
+ // If there's a tsconfig.json, NCC will use it automatically
163
+ const tsconfigPath = path.join(packageDir, "tsconfig.json");
164
+ if (await fs.pathExists(tsconfigPath)) {
165
+ console.log("📄 Using existing tsconfig.json for TypeScript compilation");
166
+ } else if (isTypeScript || entryScript.endsWith(".ts")) {
167
+ // Create a basic tsconfig.json for SEA compatibility
168
+ console.log("📄 Creating basic tsconfig.json for SEA compatibility");
169
+ const basicTsConfig = {
170
+ compilerOptions: {
171
+ target: "ES2020",
172
+ module: "CommonJS",
173
+ lib: ["ES2020"],
174
+ outDir: "./dist",
175
+ rootDir: "./src",
176
+ strict: true,
177
+ esModuleInterop: true,
178
+ skipLibCheck: true,
179
+ forceConsistentCasingInFileNames: true,
180
+ moduleResolution: "node",
181
+ resolveJsonModule: true,
182
+ declaration: false,
183
+ declarationMap: false,
184
+ sourceMap: false,
185
+ },
186
+ include: ["**/*.ts"],
187
+ exclude: [
188
+ "node_modules",
189
+ "dist",
190
+ "sea-dist",
191
+ "**/*.test.ts",
192
+ "**/*.spec.ts",
193
+ ],
194
+ };
195
+ await fs.writeJson(tsconfigPath, basicTsConfig, { spaces: 2 });
196
+ }
197
+
198
+ executeCommand(bundleCommand, { cwd: packageDir });
199
+
200
+ const bundledPath = path.join(packageDir, "sea-dist", "index.js");
201
+ if (!(await fs.pathExists(bundledPath))) {
202
+ throw new Error("NCC bundling failed - bundled file not found");
203
+ }
204
+
205
+ // Step 4: Create SEA configuration
206
+ const seaConfig = {
207
+ main: "sea-dist/index.js",
208
+ output: "sea-prep.blob",
209
+ disableExperimentalSEAWarning: true,
210
+ useSnapshot: false,
211
+ useCodeCache: true,
212
+ };
213
+
214
+ const seaConfigPath = path.join(packageDir, "sea-config.json");
215
+ await fs.writeJson(seaConfigPath, seaConfig, { spaces: 2 });
216
+
217
+ // Step 5: Generate SEA blob
218
+ console.log("🗜️ Generating SEA blob...");
219
+ const originalCwd = process.cwd();
220
+ process.chdir(packageDir);
221
+
222
+ try {
223
+ executeCommand("node --experimental-sea-config sea-config.json");
224
+
225
+ const blobPath = path.join(packageDir, "sea-prep.blob");
226
+ if (!(await fs.pathExists(blobPath))) {
227
+ throw new Error("SEA blob generation failed");
228
+ }
229
+
230
+ console.log("✅ SEA blob created successfully");
231
+
232
+ // Step 6: Get a completely fresh Node.js executable (ensuring no SEA markers)
233
+ console.log("📄 Creating executable base with fresh Node.js binary...");
234
+
235
+ // Always get a fresh Node.js binary from alternative sources
236
+ const freshNodePath = await getFreshNodeBinary();
237
+ console.log(`📥 Using Node.js binary from: ${freshNodePath}`);
238
+
239
+ // Remove any existing executable to ensure clean state
240
+ if (await fs.pathExists(executablePath)) {
241
+ console.log(
242
+ "🗑️ Removing existing executable to ensure clean state...",
243
+ );
244
+ await fs.remove(executablePath);
245
+ }
246
+
247
+ // Remove any temporary files
248
+ if (await fs.pathExists(tempNodeBinary)) {
249
+ await fs.remove(tempNodeBinary);
250
+ }
251
+
252
+ // Copy the fresh Node.js binary to temporary location first
253
+ await fs.copyFile(freshNodePath, tempNodeBinary);
254
+
255
+ // Verify the temporary binary exists and is valid
256
+ if (!(await fs.pathExists(tempNodeBinary))) {
257
+ throw new Error("Failed to create temporary Node.js binary");
258
+ }
259
+
260
+ // Test that it's a valid Node.js binary
261
+ try {
262
+ const testCommand = `"${tempNodeBinary}" --version`;
263
+ const version = executeCommand(testCommand, { silent: true });
264
+ console.log(`✅ Fresh Node.js binary verified: ${version.trim()}`);
265
+ } catch (testError) {
266
+ throw new Error(
267
+ `Fresh Node.js binary is invalid: ${testError.message}`,
268
+ );
269
+ }
270
+
271
+ // Move to final executable name
272
+ await fs.move(tempNodeBinary, executablePath);
273
+
274
+ if (!(await fs.pathExists(executablePath))) {
275
+ throw new Error("Failed to create executable base");
276
+ }
277
+
278
+ console.log(`✅ Fresh Node.js binary prepared: ${executableName}`);
279
+
280
+ // Verify this is truly a fresh binary (no SEA markers)
281
+ try {
282
+ const { executeCommand: execCmd } = require("./utils");
283
+ const checkCommand = `npx postject "${executableName}" NODE_SEA_BLOB --info 2>nul || echo "NO_SEA_MARKERS"`;
284
+ const checkResult = execCmd(checkCommand, { silent: true });
285
+ if (checkResult.includes("NO_SEA_MARKERS")) {
286
+ console.log("✅ Confirmed: Binary has no existing SEA markers");
287
+ } else {
288
+ console.warn("⚠️ Warning: Binary may have existing SEA markers");
289
+ }
290
+ } catch (checkError) {
291
+ // This is fine, just means postject check failed
292
+ console.log("✅ Binary check completed");
293
+ }
294
+
295
+ // Step 7: Remove signature (Windows) to prepare for SEA injection
296
+ console.log("🔓 Removing executable signature...");
297
+ try {
298
+ const { findWindowsSDKTools } = require("./utils");
299
+ const tools = findWindowsSDKTools();
300
+ const removeSignCommand = `"${tools.signtool}" remove /s "${executableName}"`;
301
+ executeCommand(removeSignCommand);
302
+ console.log("✅ Signature removed successfully");
303
+ } catch (sigError) {
304
+ console.warn("⚠️ Could not remove signature (this is usually fine)");
305
+ }
306
+
307
+ // Step 8: Inject blob with postject (with enhanced error handling)
308
+ console.log("💉 Injecting SEA blob into executable...");
309
+
310
+ // First, verify that our executable doesn't have multiple sentinels
311
+ let sentinelCount = 0;
312
+ try {
313
+ const binaryData = await fs.readFile(executablePath);
314
+ const sentinelPattern = Buffer.from(
315
+ "NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2",
316
+ "utf8",
317
+ );
318
+ let searchPos = 0;
319
+ while (true) {
320
+ const index = binaryData.indexOf(sentinelPattern, searchPos);
321
+ if (index === -1) break;
322
+ sentinelCount++;
323
+ searchPos = index + 1;
324
+ }
325
+ console.log(`🔍 Found ${sentinelCount} sentinel markers in executable`);
326
+ } catch (readError) {
327
+ console.warn("⚠️ Could not analyze binary for sentinels");
328
+ }
329
+
330
+ try {
331
+ let postjectCommand;
332
+
333
+ if (sentinelCount > 1) {
334
+ // Use overwrite flag immediately if multiple sentinels detected
335
+ console.log(
336
+ "⚠️ Multiple sentinels detected, using overwrite mode...",
337
+ );
338
+ postjectCommand = `npx postject "${executableName}" NODE_SEA_BLOB sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 --overwrite`;
339
+ } else {
340
+ // Use normal injection for clean binaries
341
+ console.log(
342
+ `Start injection of NODE_SEA_BLOB in ${executableName}...`,
343
+ );
344
+ postjectCommand = `npx postject "${executableName}" NODE_SEA_BLOB sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2`;
345
+ }
346
+
347
+ executeCommand(postjectCommand);
348
+ console.log("✅ SEA blob injection successful");
349
+ } catch (postjectError) {
350
+ console.error(`❌ SEA blob injection failed: ${postjectError.message}`);
351
+
352
+ // If postject fails due to multiple sentinels, this means our fresh binary isn't actually fresh
353
+ if (postjectError.message.includes("Multiple occurences of sentinel")) {
354
+ console.error(
355
+ "❌ Fresh binary still contains SEA markers - this should not happen!",
356
+ );
357
+ console.log("� Attempting emergency recovery with overwrite...");
358
+
359
+ try {
360
+ // Try with overwrite flag as last resort
361
+ const overwriteCommand = `npx postject "${executableName}" NODE_SEA_BLOB sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 --overwrite`;
362
+ executeCommand(overwriteCommand);
363
+ console.log("✅ SEA blob injection successful with overwrite");
364
+ } catch (overwriteError) {
365
+ throw new Error(
366
+ `SEA injection failed even with overwrite: ${overwriteError.message}`,
367
+ );
368
+ }
369
+ } else {
370
+ // For other postject errors, re-throw
371
+ throw postjectError;
372
+ }
373
+ }
374
+
375
+ // Step 9: Sign the executable
376
+ await signSEAExecutable(executablePath, config);
377
+
378
+ // Step 10: Cleanup temporary files
379
+ await fs.remove(entryPath);
380
+ await fs.remove(path.join(packageDir, "sea-dist"));
381
+ await fs.remove(seaConfigPath);
382
+ await fs.remove(blobPath);
383
+
384
+ // Clean up any temporary Node.js binaries
385
+ if (await fs.pathExists(tempNodeBinary)) {
386
+ await fs.remove(tempNodeBinary);
387
+ }
388
+
389
+ // Remove temporary tsconfig.json if we created it
390
+ const tsconfigPath = path.join(packageDir, "tsconfig.json");
391
+ if (
392
+ !(await fs.pathExists(tsconfigPath.replace(".json", ".original.json")))
393
+ ) {
394
+ // Only remove if we didn't backup an existing one
395
+ const tsconfigContent = await fs
396
+ .readJson(tsconfigPath)
397
+ .catch(() => null);
398
+ if (
399
+ tsconfigContent &&
400
+ tsconfigContent.compilerOptions &&
401
+ tsconfigContent.compilerOptions.target === "ES2020"
402
+ ) {
403
+ console.log("🧹 Cleaning up temporary tsconfig.json");
404
+ await fs.remove(tsconfigPath);
405
+ }
406
+ }
407
+
408
+ // Update config
409
+ config.executable = executableName;
410
+ delete config.executableArgs;
411
+
412
+ console.log(`✅ SEA executable created: ${executableName}`);
413
+ return true;
414
+ } finally {
415
+ process.chdir(originalCwd);
416
+ }
417
+ } catch (error) {
418
+ console.warn(`⚠️ SEA creation failed: ${error.message}`);
419
+ console.log("Falling back to PKG...");
420
+ return false;
421
+ }
422
+ }
423
+
424
+ /**
425
+ * Gets the Node.js binary path for the current platform
426
+ * @returns {Promise<string>} Path to Node.js binary
427
+ */
428
+ async function getNodeBinaryPath() {
429
+ return process.execPath;
430
+ }
431
+
432
+ /**
433
+ * Gets a fresh Node.js binary path, trying alternative sources if needed
434
+ * @returns {Promise<string>} Path to a fresh Node.js binary
435
+ */
436
+ async function getFreshNodeBinary() {
437
+ const { executeCommand } = require("./utils");
438
+
439
+ // Always try to get a completely fresh Node.js binary
440
+ const freshSources = [];
441
+
442
+ // 1. Try to find Node.js installation via where command (different from current process)
443
+ try {
444
+ const whereResult = executeCommand("where node", { silent: true });
445
+ const nodePaths = whereResult
446
+ .split("\n")
447
+ .map((p) => p.trim())
448
+ .filter((p) => p && p.includes("node.exe") && p !== process.execPath);
449
+ freshSources.push(...nodePaths);
450
+ } catch (err) {
451
+ // Ignore where command failures
452
+ }
453
+
454
+ // 2. Try common Node.js installation paths (avoid current process path)
455
+ const commonPaths = [
456
+ "C:\\Program Files\\nodejs\\node.exe",
457
+ "C:\\Program Files (x86)\\nodejs\\node.exe",
458
+ process.env.PROGRAMFILES
459
+ ? path.join(process.env.PROGRAMFILES, "nodejs", "node.exe")
460
+ : null,
461
+ process.env["PROGRAMFILES(X86)"]
462
+ ? path.join(process.env["PROGRAMFILES(X86)"], "nodejs", "node.exe")
463
+ : null,
464
+ // Try appdata paths
465
+ process.env.APPDATA
466
+ ? path.join(process.env.APPDATA, "npm", "node.exe")
467
+ : null,
468
+ process.env.LOCALAPPDATA
469
+ ? path.join(process.env.LOCALAPPDATA, "npm", "node.exe")
470
+ : null,
471
+ ]
472
+ .filter(Boolean)
473
+ .filter((p) => p !== process.execPath);
474
+
475
+ freshSources.push(...commonPaths);
476
+
477
+ // 3. Check if we can find Node.js through PowerShell Get-Command
478
+ try {
479
+ const psResult = executeCommand(
480
+ 'powershell -Command "Get-Command node -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source"',
481
+ { silent: true },
482
+ );
483
+ const psPath = psResult.trim();
484
+ if (psPath && psPath !== process.execPath && psPath.includes("node.exe")) {
485
+ freshSources.push(psPath);
486
+ }
487
+ } catch (err) {
488
+ // Ignore PowerShell command failures
489
+ }
490
+
491
+ // Remove duplicates and current process path
492
+ const uniquePaths = [...new Set(freshSources)].filter(
493
+ (p) => p !== process.execPath,
494
+ );
495
+
496
+ console.log(
497
+ `🔍 Found ${uniquePaths.length} alternative Node.js binary sources`,
498
+ );
499
+
500
+ // Test each path to find a working one WITHOUT existing SEA markers
501
+ for (const nodePath of uniquePaths) {
502
+ try {
503
+ if (await fs.pathExists(nodePath)) {
504
+ // Verify it's a valid Node.js executable by checking version
505
+ const versionResult = executeCommand(`"${nodePath}" --version`, {
506
+ silent: true,
507
+ });
508
+ if (versionResult.includes("v")) {
509
+ // Check if this binary already has SEA markers
510
+ const hasSeaMarkers = await checkForSeaMarkers(nodePath);
511
+ if (!hasSeaMarkers) {
512
+ console.log(
513
+ `✅ Found clean Node.js binary (no SEA markers): ${nodePath}`,
514
+ );
515
+ return nodePath;
516
+ } else {
517
+ console.log(
518
+ `⚠️ Skipping binary with existing SEA markers: ${nodePath}`,
519
+ );
520
+ }
521
+ }
522
+ }
523
+ } catch (err) {
524
+ // Skip invalid binaries
525
+ continue;
526
+ }
527
+ }
528
+
529
+ // If no clean alternative found, create a fresh copy by downloading immediately
530
+ console.log(
531
+ "🔧 No clean Node.js binary found locally, downloading fresh copy...",
532
+ );
533
+ const superFreshPath = await createSuperFreshBinary();
534
+ if (superFreshPath) {
535
+ return superFreshPath;
536
+ }
537
+
538
+ // Absolute fallback - at least log the issue
539
+ console.error(
540
+ "❌ Unable to create fresh Node.js binary - using current process (may have SEA markers)",
541
+ );
542
+ throw new Error(
543
+ "Could not obtain a fresh Node.js binary without SEA markers",
544
+ );
545
+ }
546
+
547
+ /**
548
+ * Checks if a Node.js binary already contains SEA markers
549
+ * @param {string} binaryPath - Path to the Node.js binary
550
+ * @returns {Promise<boolean>} True if SEA markers are found
551
+ */
552
+ async function checkForSeaMarkers(binaryPath) {
553
+ try {
554
+ // Method 1: Use postject to check for existing SEA blob
555
+ const { executeCommand } = require("./utils");
556
+ try {
557
+ const checkCommand = `npx postject "${binaryPath}" NODE_SEA_BLOB --info`;
558
+ const result = executeCommand(checkCommand, { silent: true });
559
+
560
+ // If postject finds info about SEA blob, it means markers exist
561
+ if (
562
+ result.includes("NODE_SEA_BLOB") ||
563
+ result.includes("NODE_SEA_FUSE")
564
+ ) {
565
+ return true;
566
+ }
567
+ } catch (postjectError) {
568
+ // Postject error usually means no SEA markers, but let's do binary check too
569
+ }
570
+
571
+ // Method 2: Direct binary content check for SEA markers
572
+ const binaryData = await fs.readFile(binaryPath);
573
+ const seaFusePattern = Buffer.from(
574
+ "NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2",
575
+ "utf8",
576
+ );
577
+ const seaBlobPattern = Buffer.from("NODE_SEA_BLOB", "utf8");
578
+
579
+ // Check for any occurrence of SEA markers
580
+ if (
581
+ binaryData.indexOf(seaFusePattern) !== -1 ||
582
+ binaryData.indexOf(seaBlobPattern) !== -1
583
+ ) {
584
+ return true;
585
+ }
586
+
587
+ return false;
588
+ } catch (error) {
589
+ // If we can't check, assume no SEA markers (safer for injection)
590
+ console.warn(`⚠️ Could not check for SEA markers: ${error.message}`);
591
+ return false;
592
+ }
593
+ }
594
+
595
+ /**
596
+ * Creates a guaranteed fresh Node.js binary by downloading or extracting from system
597
+ * @returns {Promise<string>} Path to the fresh binary
598
+ */
599
+ async function createFreshNodeBinary() {
600
+ const os = require("os");
601
+ const crypto = require("crypto");
602
+
603
+ // Create a unique temporary path for our fresh binary
604
+ const tempDir = os.tmpdir();
605
+ const uniqueId = crypto.randomBytes(8).toString("hex");
606
+ const freshBinaryPath = path.join(tempDir, `node-fresh-${uniqueId}.exe`);
607
+
608
+ try {
609
+ // Strategy 1: Try to extract Node.js from an MSI installer if available
610
+ const msiPath = await findNodeMsiInstaller();
611
+ if (msiPath) {
612
+ console.log("📦 Extracting fresh Node.js from MSI installer...");
613
+ const extractedPath = await extractNodeFromMsi(msiPath, freshBinaryPath);
614
+ if (extractedPath) {
615
+ return extractedPath;
616
+ }
617
+ }
618
+
619
+ // Strategy 2: Copy from current process but strip SEA markers
620
+ console.log(
621
+ "🔧 Creating fresh copy by stripping SEA markers from current binary...",
622
+ );
623
+ await fs.copyFile(process.execPath, freshBinaryPath);
624
+
625
+ // Remove any existing SEA markers using binary manipulation
626
+ const cleaned = await stripSeaMarkers(freshBinaryPath);
627
+ if (cleaned) {
628
+ console.log("✅ Successfully created clean Node.js binary");
629
+ return freshBinaryPath;
630
+ }
631
+
632
+ // Strategy 3: As absolute last resort, download Node.js
633
+ console.log("⬇️ Downloading fresh Node.js binary...");
634
+ const downloadedPath = await downloadFreshNodeBinary(freshBinaryPath);
635
+ if (downloadedPath) {
636
+ return downloadedPath;
637
+ }
638
+ } catch (error) {
639
+ console.warn(`⚠️ Failed to create fresh binary: ${error.message}`);
640
+ }
641
+
642
+ // Absolute fallback - at least log the issue
643
+ console.error(
644
+ "❌ Unable to create fresh Node.js binary - using current process (may have SEA markers)",
645
+ );
646
+ throw new Error(
647
+ "Could not obtain a fresh Node.js binary without SEA markers",
648
+ );
649
+ }
650
+
651
+ /**
652
+ * Strips SEA markers from a Node.js binary using multiple methods
653
+ * @param {string} binaryPath - Path to the binary to clean
654
+ * @returns {Promise<boolean>} True if successful
655
+ */
656
+ async function stripSeaMarkers(binaryPath) {
657
+ try {
658
+ console.log(`🧹 Attempting to strip SEA markers from: ${binaryPath}`);
659
+
660
+ // Read the binary file
661
+ const binaryData = await fs.readFile(binaryPath);
662
+ const originalSize = binaryData.length;
663
+
664
+ // Define all possible SEA-related patterns to remove
665
+ const patterns = [
666
+ "NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2",
667
+ "NODE_SEA_BLOB",
668
+ "NODE_SEA_FUSE",
669
+ "sea_blob_size",
670
+ "SEA_BLOB_START",
671
+ "SEA_BLOB_END",
672
+ ];
673
+
674
+ let modified = false;
675
+ let totalReplacements = 0;
676
+
677
+ // Replace all patterns with null bytes
678
+ for (const patternStr of patterns) {
679
+ const pattern = Buffer.from(patternStr, "utf8");
680
+ const nullReplacement = Buffer.alloc(pattern.length, 0);
681
+ let searchStart = 0;
682
+ let patternReplacements = 0;
683
+
684
+ while (true) {
685
+ const index = binaryData.indexOf(pattern, searchStart);
686
+ if (index === -1) break;
687
+
688
+ console.log(`🧹 Removing "${patternStr}" marker at offset ${index}`);
689
+ binaryData.set(nullReplacement, index);
690
+ modified = true;
691
+ patternReplacements++;
692
+ totalReplacements++;
693
+ searchStart = index + pattern.length;
694
+ }
695
+
696
+ if (patternReplacements > 0) {
697
+ console.log(
698
+ ` ✅ Removed ${patternReplacements} instances of "${patternStr}"`,
699
+ );
700
+ }
701
+ }
702
+
703
+ if (modified) {
704
+ // Create backup before modifying
705
+ const backupPath = `${binaryPath}.backup`;
706
+ await fs.copyFile(binaryPath, backupPath);
707
+ console.log(`📋 Created backup: ${backupPath}`);
708
+
709
+ // Write the cleaned binary back
710
+ await fs.writeFile(binaryPath, binaryData);
711
+ console.log(
712
+ `✅ Successfully stripped ${totalReplacements} SEA markers from binary`,
713
+ );
714
+
715
+ // Verify the binary still works
716
+ const { executeCommand } = require("./utils");
717
+ try {
718
+ const versionResult = executeCommand(`"${binaryPath}" --version`, {
719
+ silent: true,
720
+ });
721
+ if (versionResult.includes("v")) {
722
+ console.log(`✅ Cleaned binary verified: ${versionResult.trim()}`);
723
+
724
+ // Clean up backup if verification successful
725
+ await fs.remove(backupPath);
726
+
727
+ return true;
728
+ } else {
729
+ throw new Error("Binary verification failed");
730
+ }
731
+ } catch (verifyError) {
732
+ console.warn(
733
+ `⚠️ Binary verification failed, restoring backup: ${verifyError.message}`,
734
+ );
735
+ await fs.copyFile(backupPath, binaryPath);
736
+ await fs.remove(backupPath);
737
+ return false;
738
+ }
739
+ } else {
740
+ console.log(
741
+ `✅ No SEA markers found in binary (size: ${originalSize} bytes)`,
742
+ );
743
+ return true; // No markers to strip is success
744
+ }
745
+ } catch (error) {
746
+ console.warn(`⚠️ Failed to strip SEA markers: ${error.message}`);
747
+ return false;
748
+ }
749
+ }
750
+
751
+ /**
752
+ * Finds Node.js MSI installer files on the system
753
+ * @returns {Promise<string|null>} Path to MSI installer or null
754
+ */
755
+ async function findNodeMsiInstaller() {
756
+ // Common locations where Node.js MSI installers might be found
757
+ const searchPaths = [
758
+ path.join(os.homedir(), "Downloads"),
759
+ path.join(os.homedir(), "Desktop"),
760
+ "C:\\Temp",
761
+ "C:\\Downloads",
762
+ ];
763
+
764
+ for (const searchPath of searchPaths) {
765
+ try {
766
+ if (await fs.pathExists(searchPath)) {
767
+ const files = await fs.readdir(searchPath);
768
+ const msiFile = files.find(
769
+ (file) =>
770
+ file.toLowerCase().includes("node") &&
771
+ file.toLowerCase().endsWith(".msi"),
772
+ );
773
+ if (msiFile) {
774
+ return path.join(searchPath, msiFile);
775
+ }
776
+ }
777
+ } catch (error) {
778
+ // Ignore search errors
779
+ }
780
+ }
781
+
782
+ return null;
783
+ }
784
+
785
+ /**
786
+ * Extracts Node.js binary from MSI installer
787
+ * @param {string} msiPath - Path to MSI installer
788
+ * @param {string} outputPath - Where to extract the binary
789
+ * @returns {Promise<string|null>} Path to extracted binary or null
790
+ */
791
+ async function extractNodeFromMsi(msiPath, outputPath) {
792
+ try {
793
+ const { executeCommand } = require("./utils");
794
+ const tempExtractDir = path.join(os.tmpdir(), `node-extract-${Date.now()}`);
795
+
796
+ // Extract MSI contents
797
+ const extractCommand = `msiexec /a "${msiPath}" /qn TARGETDIR="${tempExtractDir}"`;
798
+ executeCommand(extractCommand, { silent: true });
799
+
800
+ // Find node.exe in extracted contents
801
+ const findNodePath = async (dir) => {
802
+ const items = await fs.readdir(dir);
803
+ for (const item of items) {
804
+ const itemPath = path.join(dir, item);
805
+ const stat = await fs.stat(itemPath);
806
+ if (stat.isDirectory()) {
807
+ const found = await findNodePath(itemPath);
808
+ if (found) return found;
809
+ } else if (item === "node.exe") {
810
+ return itemPath;
811
+ }
812
+ }
813
+ return null;
814
+ };
815
+
816
+ const extractedNodePath = await findNodePath(tempExtractDir);
817
+ if (extractedNodePath) {
818
+ await fs.copyFile(extractedNodePath, outputPath);
819
+ await fs.remove(tempExtractDir);
820
+ return outputPath;
821
+ }
822
+
823
+ await fs.remove(tempExtractDir);
824
+ return null;
825
+ } catch (error) {
826
+ console.warn(`⚠️ MSI extraction failed: ${error.message}`);
827
+ return null;
828
+ }
829
+ }
830
+
831
+ /**
832
+ * Downloads a fresh Node.js binary from the official website
833
+ * @param {string} outputPath - Where to save the downloaded binary
834
+ * @returns {Promise<string|null>} Path to downloaded binary or null
835
+ */
836
+ async function downloadFreshNodeBinary(outputPath) {
837
+ try {
838
+ const https = require("https");
839
+ const { executeCommand } = require("./utils");
840
+
841
+ // Get current Node.js version to download the same version
842
+ const currentVersion = process.version; // e.g., v18.17.0
843
+
844
+ // Download URL for Windows x64 Node.js binary
845
+ const downloadUrl = `https://nodejs.org/dist/${currentVersion}/win-x64/node.exe`;
846
+
847
+ console.log(
848
+ `📥 Downloading Node.js ${currentVersion} from official source...`,
849
+ );
850
+
851
+ // Use curl or PowerShell to download
852
+ try {
853
+ const curlCommand = `curl -L -o "${outputPath}" "${downloadUrl}"`;
854
+ executeCommand(curlCommand, { silent: true });
855
+ } catch (curlError) {
856
+ // Fallback to PowerShell
857
+ const psCommand = `powershell -Command "Invoke-WebRequest -Uri '${downloadUrl}' -OutFile '${outputPath}'"`;
858
+ executeCommand(psCommand, { silent: true });
859
+ }
860
+
861
+ // Verify the download
862
+ if (await fs.pathExists(outputPath)) {
863
+ const versionResult = executeCommand(`"${outputPath}" --version`, {
864
+ silent: true,
865
+ });
866
+ if (versionResult.includes("v")) {
867
+ console.log(
868
+ `✅ Downloaded fresh Node.js binary: ${versionResult.trim()}`,
869
+ );
870
+ return outputPath;
871
+ }
872
+ }
873
+
874
+ return null;
875
+ } catch (error) {
876
+ console.warn(`⚠️ Download failed: ${error.message}`);
877
+ return null;
878
+ }
879
+ }
880
+
881
+ /**
882
+ * Creates a super fresh Node.js binary using direct download
883
+ * @returns {Promise<string|null>} Path to the super fresh binary or null
884
+ */
885
+ async function createSuperFreshBinary() {
886
+ try {
887
+ const os = require("os");
888
+ const crypto = require("crypto");
889
+ const https = require("https");
890
+
891
+ // Create a unique temporary path for our super fresh binary
892
+ const tempDir = os.tmpdir();
893
+ const uniqueId = crypto.randomBytes(8).toString("hex");
894
+ const superFreshBinaryPath = path.join(
895
+ tempDir,
896
+ `node-super-fresh-${uniqueId}.exe`,
897
+ );
898
+
899
+ // Get current Node.js version to download the same version
900
+ const currentVersion = process.version; // e.g., v18.17.0
901
+ const downloadUrl = `https://nodejs.org/dist/${currentVersion}/win-x64/node.exe`;
902
+
903
+ console.log(
904
+ `📥 Downloading guaranteed fresh Node.js ${currentVersion} binary from official source...`,
905
+ );
906
+
907
+ // Use Node.js built-in https to download (most reliable method)
908
+ await new Promise((resolve, reject) => {
909
+ const file = fs.createWriteStream(superFreshBinaryPath);
910
+
911
+ https
912
+ .get(downloadUrl, (response) => {
913
+ if (response.statusCode === 200) {
914
+ response.pipe(file);
915
+ file.on("finish", () => {
916
+ file.close();
917
+ resolve();
918
+ });
919
+ } else if (
920
+ response.statusCode === 302 ||
921
+ response.statusCode === 301
922
+ ) {
923
+ // Handle redirects
924
+ file.close();
925
+ fs.unlink(superFreshBinaryPath, () => {});
926
+ https
927
+ .get(response.headers.location, (redirectResponse) => {
928
+ if (redirectResponse.statusCode === 200) {
929
+ const redirectFile =
930
+ fs.createWriteStream(superFreshBinaryPath);
931
+ redirectResponse.pipe(redirectFile);
932
+ redirectFile.on("finish", () => {
933
+ redirectFile.close();
934
+ resolve();
935
+ });
936
+ } else {
937
+ reject(
938
+ new Error(
939
+ `Failed to download after redirect: HTTP ${redirectResponse.statusCode}`,
940
+ ),
941
+ );
942
+ }
943
+ })
944
+ .on("error", reject);
945
+ } else {
946
+ reject(
947
+ new Error(
948
+ `Failed to download Node.js: HTTP ${response.statusCode}`,
949
+ ),
950
+ );
951
+ }
952
+ })
953
+ .on("error", (err) => {
954
+ fs.unlink(superFreshBinaryPath, () => {});
955
+ reject(err);
956
+ });
957
+ });
958
+
959
+ // Verify the download
960
+ if (await fs.pathExists(superFreshBinaryPath)) {
961
+ const { executeCommand } = require("./utils");
962
+ const versionResult = executeCommand(
963
+ `"${superFreshBinaryPath}" --version`,
964
+ { silent: true },
965
+ );
966
+ if (versionResult.includes("v")) {
967
+ console.log(
968
+ `✅ Downloaded super fresh Node.js binary: ${versionResult.trim()}`,
969
+ );
970
+
971
+ // Verify it has no SEA markers
972
+ const hasSeaMarkers = await checkForSeaMarkers(superFreshBinaryPath);
973
+ if (!hasSeaMarkers) {
974
+ console.log("✅ Confirmed: Super fresh binary has no SEA markers");
975
+ return superFreshBinaryPath;
976
+ } else {
977
+ console.warn(
978
+ "⚠️ Warning: Even downloaded binary has SEA markers (very unusual)",
979
+ );
980
+ return superFreshBinaryPath; // Still return it, might work with overwrite
981
+ }
982
+ }
983
+ }
984
+
985
+ return null;
986
+ } catch (error) {
987
+ console.warn(`⚠️ Super fresh binary download failed: ${error.message}`);
988
+ return null;
989
+ }
990
+ }
991
+
992
+ /**
993
+ * Signs the SEA executable
994
+ * @param {string} executablePath - Path to executable
995
+ * @param {Object} config - Configuration object
996
+ */
997
+ async function signSEAExecutable(executablePath, config) {
998
+ try {
999
+ const { determineSigningMethod } = require("./certificates");
1000
+ const { findWindowsSDKTools } = require("./utils");
1001
+
1002
+ const signingMethod = await determineSigningMethod(config);
1003
+ if (!signingMethod) {
1004
+ console.log("No signing configuration found, skipping...");
1005
+ return;
1006
+ }
1007
+
1008
+ const tools = findWindowsSDKTools();
1009
+ let signCommand;
1010
+
1011
+ if (signingMethod.method === "pfx") {
1012
+ const { pfxPath, password, timestampUrl } = signingMethod.parameters;
1013
+ signCommand = `"${tools.signtool}" sign /f "${pfxPath}" /p "${password}" /tr "${timestampUrl}" /td SHA256 /fd SHA256 "${executablePath}"`;
1014
+ } else if (signingMethod.method === "store") {
1015
+ const { thumbprint, store, timestampUrl } = signingMethod.parameters;
1016
+ const storeLocation = store.toLowerCase() === "localmachine" ? "/sm" : "";
1017
+ signCommand = `"${tools.signtool}" sign /sha1 "${thumbprint}" /s "My" ${storeLocation} /fd SHA256 "${executablePath}"`;
1018
+ }
1019
+
1020
+ if (signCommand) {
1021
+ executeCommand(signCommand);
1022
+ console.log("✅ SEA executable signed successfully");
1023
+ }
1024
+ } catch (error) {
1025
+ console.warn(`Warning: Could not sign SEA executable: ${error.message}`);
1026
+ }
1027
+ }
1028
+
1029
+ /**
1030
+ * Creates a PKG fallback executable
1031
+ * @param {string} packageDir - Package directory path
1032
+ * @param {Object} config - Configuration object
1033
+ * @param {Object} packageJson - package.json content
1034
+ */
1035
+ async function createFallbackLauncher(packageDir, config, packageJson) {
1036
+ try {
1037
+ console.log("📦 Creating PKG fallback executable...");
1038
+
1039
+ const executableName = `${config.appName || "app"}.exe`;
1040
+ const executablePath = path.join(packageDir, executableName);
1041
+ const mainScript = packageJson.main || "index.js";
1042
+
1043
+ // Create launcher script with proper directory handling
1044
+ const launcherContent = `#!/usr/bin/env node
1045
+ // PKG Launcher for ${config.displayName || config.appName}
1046
+
1047
+ console.log('🚀 Starting ${config.displayName || config.appName}...');
1048
+
1049
+ // Set working directory - handle PKG snapshot case
1050
+ try {
1051
+ if (__dirname.includes('snapshot')) {
1052
+ // Running from PKG, use the executable's directory
1053
+ const path = require('path');
1054
+ const execDir = path.dirname(process.execPath);
1055
+ process.chdir(execDir);
1056
+ } else {
1057
+ // Running in development mode
1058
+ process.chdir(__dirname);
1059
+ }
1060
+ } catch (err) {
1061
+ console.warn('Could not change working directory:', err.message);
1062
+ }
1063
+
1064
+ // Load the main application
1065
+ try {
1066
+ require('./${mainScript}');
1067
+ } catch (error) {
1068
+ console.error('❌ Failed to start application:', error.message);
1069
+ console.error(error.stack);
1070
+ process.exit(1);
1071
+ }
1072
+ `;
1073
+
1074
+ const launcherScript = path.join(packageDir, "launcher.js");
1075
+ await fs.writeFile(launcherScript, launcherContent);
1076
+
1077
+ // Create PKG configuration
1078
+ const pkgConfig = {
1079
+ name: config.appName || "app",
1080
+ version: packageJson.version || "1.0.0",
1081
+ main: "launcher.js",
1082
+ bin: "launcher.js",
1083
+ pkg: {
1084
+ scripts: [mainScript],
1085
+ targets: ["node18-win-x64"],
1086
+ outputPath: ".",
1087
+ },
1088
+ dependencies: packageJson.dependencies || {},
1089
+ };
1090
+
1091
+ const pkgConfigPath = path.join(packageDir, "pkg-config.json");
1092
+ await fs.writeJson(pkgConfigPath, pkgConfig, { spaces: 2 });
1093
+
1094
+ // Create executable with PKG
1095
+ const finalExeName = path.basename(executablePath, ".exe");
1096
+ const pkgCommand = `npx pkg launcher.js --target node18-win-x64 --output "${finalExeName}" --config pkg-config.json`;
1097
+
1098
+ executeCommand(pkgCommand, { cwd: packageDir });
1099
+
1100
+ // Verify executable was created
1101
+ if (await fs.pathExists(executablePath)) {
1102
+ console.log(`✅ Created executable with PKG: ${executableName}`);
1103
+
1104
+ // Clean up temporary files
1105
+ await fs.remove(launcherScript);
1106
+ await fs.remove(pkgConfigPath);
1107
+
1108
+ // Update config
1109
+ config.executable = executableName;
1110
+ delete config.executableArgs;
1111
+
1112
+ return true;
1113
+ }
1114
+ } catch (pkgError) {
1115
+ console.warn(`⚠️ PKG fallback failed: ${pkgError.message}`);
1116
+ return false;
1117
+ }
1118
+ }
1119
+
1120
+ module.exports = {
1121
+ createSingleExecutableApp,
1122
+ createFallbackLauncher,
1123
+ getFreshNodeBinary,
1124
+ };