@cnrai/pave 0.3.32 → 0.3.34

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.
Files changed (83) hide show
  1. package/MARKETPLACE.md +406 -0
  2. package/README.md +218 -21
  3. package/build-binary.js +591 -0
  4. package/build-npm.js +537 -0
  5. package/build.js +230 -0
  6. package/check-binary.js +26 -0
  7. package/deploy.sh +95 -0
  8. package/index.js +5775 -0
  9. package/lib/agent-registry.js +1037 -0
  10. package/lib/args-parser.js +837 -0
  11. package/lib/blessed-widget-patched.js +93 -0
  12. package/lib/cli-markdown.js +590 -0
  13. package/lib/compaction.js +153 -0
  14. package/lib/duration.js +94 -0
  15. package/lib/hash.js +22 -0
  16. package/lib/marketplace.js +866 -0
  17. package/lib/memory-config.js +166 -0
  18. package/lib/skill-manager.js +891 -0
  19. package/lib/soul.js +31 -0
  20. package/lib/tool-output-formatter.js +180 -0
  21. package/package.json +35 -33
  22. package/start-pave.sh +149 -0
  23. package/status.js +271 -0
  24. package/test/abort-stream.test.js +445 -0
  25. package/test/agent-auto-compaction.test.js +552 -0
  26. package/test/agent-comm-abort.test.js +95 -0
  27. package/test/agent-comm.test.js +598 -0
  28. package/test/agent-inbox.test.js +576 -0
  29. package/test/agent-init.test.js +264 -0
  30. package/test/agent-interrupt.test.js +314 -0
  31. package/test/agent-lifecycle.test.js +520 -0
  32. package/test/agent-log-files.test.js +349 -0
  33. package/test/agent-mode.manual-test.js +392 -0
  34. package/test/agent-parsing.test.js +228 -0
  35. package/test/agent-post-stream-idle.test.js +762 -0
  36. package/test/agent-registry.test.js +359 -0
  37. package/test/agent-rm.test.js +442 -0
  38. package/test/agent-spawn.test.js +933 -0
  39. package/test/agent-status-api.test.js +624 -0
  40. package/test/agent-update.test.js +435 -0
  41. package/test/args-parser.test.js +391 -0
  42. package/test/auto-compaction-chat.manual-test.js +227 -0
  43. package/test/auto-compaction.test.js +941 -0
  44. package/test/build-config.test.js +120 -0
  45. package/test/build-npm.test.js +388 -0
  46. package/test/chat-command.test.js +137 -0
  47. package/test/chat-leading-lines.test.js +159 -0
  48. package/test/config-flag.test.js +272 -0
  49. package/test/cursor-drift.test.js +135 -0
  50. package/test/debug-require.js +23 -0
  51. package/test/dir-migration.test.js +323 -0
  52. package/test/duration.test.js +229 -0
  53. package/test/ghostty-term.test.js +202 -0
  54. package/test/http500-backoff.test.js +854 -0
  55. package/test/integration.test.js +86 -0
  56. package/test/memory-guard-env.test.js +220 -0
  57. package/test/pr233-fixes.test.js +259 -0
  58. package/test/run-agent-init.js +297 -0
  59. package/test/run-all.js +64 -0
  60. package/test/run-config-flag.js +159 -0
  61. package/test/run-cursor-drift.js +82 -0
  62. package/test/run-session-path.js +154 -0
  63. package/test/run-tests.js +643 -0
  64. package/test/sandbox-redirect.test.js +202 -0
  65. package/test/session-path.test.js +132 -0
  66. package/test/shebang-strip.test.js +241 -0
  67. package/test/soul-reinject.test.js +1027 -0
  68. package/test/soul-reread.test.js +281 -0
  69. package/test/tool-output-formatter.test.js +486 -0
  70. package/test/tool-output-gating.test.js +143 -0
  71. package/test/tool-states.test.js +167 -0
  72. package/test/tools-flag.test.js +65 -0
  73. package/test/tui-attach.test.js +1255 -0
  74. package/test/tui-compaction.test.js +354 -0
  75. package/test/tui-wrap.test.js +568 -0
  76. package/test-binary.js +52 -0
  77. package/test-binary2.js +36 -0
  78. package/LICENSE +0 -21
  79. package/pave.js +0 -2
  80. package/sandbox/SandboxRunner.js +0 -1
  81. package/sandbox/pave-run.js +0 -2
  82. package/sandbox/permission.js +0 -1
  83. package/sandbox/utils/yaml.js +0 -1
@@ -0,0 +1,591 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Build script for PAVE native binary
4
+ *
5
+ * Process:
6
+ * 1. Bundle ALL dependencies with esbuild (including blessed, SandboxRunner)
7
+ * 2. Obfuscate the bundle (hide SandboxRunner code)
8
+ * 3. Compile to native binary with Bun
9
+ *
10
+ * The resulting binary contains ALL code - no external JS files needed!
11
+ *
12
+ * Usage:
13
+ * node build-binary.js # Build for current platform
14
+ * node build-binary.js --skip-obfuscate # Skip obfuscation (faster, for testing)
15
+ * node build-binary.js --target=all # Build for all platforms
16
+ */
17
+
18
+ const esbuild = require('esbuild');
19
+ const { execSync, spawnSync } = require('child_process');
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+
23
+ const DIST_DIR = path.join(__dirname, 'dist');
24
+ const BIN_DIR = path.join(DIST_DIR, 'bin');
25
+
26
+ // Parse arguments
27
+ const args = process.argv.slice(2);
28
+ const skipObfuscate = args.includes('--skip-obfuscate'); // Obfuscate by default, use --skip-obfuscate to disable
29
+ const targetArg = args.find((a) => a.startsWith('--target='));
30
+ const target = targetArg ? targetArg.split('=')[1] : 'current';
31
+
32
+ // Bun compile targets
33
+ // Note: We use baseline builds for x64 platforms to avoid AVX instruction requirement
34
+ // This fixes the "CPU lacks AVX support" warning on older CPUs and VMs
35
+ // See: https://github.com/cnrai/openpave/issues/73
36
+ const TARGETS = {
37
+ 'darwin-arm64': 'bun-darwin-arm64',
38
+ 'darwin-x64': 'bun-darwin-x64-baseline', // baseline = no AVX requirement
39
+ 'linux-x64': 'bun-linux-x64-baseline', // baseline = no AVX requirement
40
+ 'linux-arm64': 'bun-linux-arm64',
41
+ };
42
+
43
+ // Export TARGETS for testing (Issue #73 regression tests)
44
+ // Use exports.TARGETS to avoid clobbering module.exports
45
+ exports.TARGETS = TARGETS;
46
+
47
+ async function checkBun() {
48
+ try {
49
+ const result = spawnSync('bun', ['--version'], { encoding: 'utf-8' });
50
+ if (result.status === 0) {
51
+ console.log(' Bun version:', result.stdout.trim());
52
+ return true;
53
+ }
54
+ } catch (e) {}
55
+
56
+ console.error('Error: Bun is not installed');
57
+ console.error('Install with: brew install bun');
58
+ process.exit(1);
59
+ }
60
+
61
+ async function bundleAll() {
62
+ console.log('\n[1/3] Creating fully bundled JS (all dependencies inline)...');
63
+
64
+ // Read version from package.json
65
+ const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
66
+ const VERSION = packageJson.version || '0.0.0';
67
+ console.log(' Version:', VERSION);
68
+
69
+ const fullBundlePath = path.join(DIST_DIR, 'pave-full.js');
70
+
71
+ // Find blessed's widget directory to pre-bundle all widgets
72
+ const tuiNodeModules = path.join(__dirname, '..', 'tui', 'node_modules');
73
+ const blessedLibDir = path.join(tuiNodeModules, 'blessed', 'lib');
74
+
75
+ // Plugin to replace node-fetch with native fetch for Bun binary compatibility
76
+ // Note: The pave-full.js bundle will use native fetch which works in:
77
+ // - Bun (native fetch)
78
+ // - Node 18+ (native fetch via undici)
79
+ // The regular pave.js bundle (from build.js) still uses node-fetch for Node 16 compat
80
+ const nodeFetchNativePlugin = {
81
+ name: 'node-fetch-native',
82
+ setup(build) {
83
+ // Replace node-fetch with native fetch wrapper
84
+ build.onResolve({ filter: /^node-fetch$/ }, () => {
85
+ return {
86
+ path: 'node-fetch',
87
+ namespace: 'node-fetch-native',
88
+ };
89
+ });
90
+
91
+ build.onLoad({ filter: /.*/, namespace: 'node-fetch-native' }, () => {
92
+ return {
93
+ contents: `
94
+ // Native fetch wrapper for Bun/Node 18+ binary
95
+ // This replaces node-fetch in the pave-full.js bundle
96
+
97
+ const nativeFetch = globalThis.fetch;
98
+
99
+ // Wrapper to ensure consistent behavior
100
+ async function fetchWrapper(url, options) {
101
+ const response = await nativeFetch(url, options);
102
+
103
+ // Add .body stream compatibility for SSE
104
+ // Native fetch response.body is a ReadableStream, not a Node stream
105
+ // We need to add .on() methods for compatibility with opencode-client.js
106
+ //
107
+ // IMPORTANT: We must NOT consume the body (via getReader()) until .on() is called
108
+ // Otherwise response.text() and response.json() will fail for non-streaming requests
109
+ if (response.body && !response.body.on) {
110
+ const originalBody = response.body;
111
+ let reader = null;
112
+ let streamConsumed = false;
113
+
114
+ const nodeStyleBody = {
115
+ _listeners: { data: [], end: [], error: [] },
116
+ _reading: false,
117
+ _destroyed: false,
118
+
119
+ on(event, handler) {
120
+ this._listeners[event] = this._listeners[event] || [];
121
+ this._listeners[event].push(handler);
122
+
123
+ // Start reading only when we have a data handler
124
+ // This is when we actually consume the stream
125
+ if (event === 'data' && !this._reading) {
126
+ this._reading = true;
127
+ streamConsumed = true;
128
+ reader = originalBody.getReader();
129
+ this._read();
130
+ }
131
+ return this;
132
+ },
133
+
134
+ // Node.js stream compatibility - destroy the stream
135
+ destroy() {
136
+ this._destroyed = true;
137
+ this._listeners = { data: [], end: [], error: [] };
138
+ if (reader) {
139
+ try {
140
+ reader.cancel();
141
+ } catch (e) {
142
+ // Ignore cancel errors
143
+ }
144
+ }
145
+ },
146
+
147
+ async _read() {
148
+ try {
149
+ while (true) {
150
+ if (this._destroyed) break;
151
+ const { done, value } = await reader.read();
152
+ if (done || this._destroyed) {
153
+ if (!this._destroyed) {
154
+ this._listeners.end.forEach(h => h());
155
+ }
156
+ break;
157
+ }
158
+ // Convert Uint8Array to string
159
+ const chunk = new TextDecoder().decode(value);
160
+ this._listeners.data.forEach(h => h(chunk));
161
+ }
162
+ } catch (err) {
163
+ if (!this._destroyed) {
164
+ this._listeners.error.forEach(h => h(err));
165
+ }
166
+ }
167
+ }
168
+ };
169
+
170
+ // Create a new response with our compatible body
171
+ // text() and json() call the original response methods ONLY if stream wasn't consumed
172
+ return {
173
+ ok: response.ok,
174
+ status: response.status,
175
+ statusText: response.statusText,
176
+ headers: response.headers,
177
+ body: nodeStyleBody,
178
+ async text() {
179
+ if (streamConsumed) {
180
+ throw new Error('Body already consumed by stream reader');
181
+ }
182
+ streamConsumed = true;
183
+ return response.text();
184
+ },
185
+ async json() {
186
+ if (streamConsumed) {
187
+ throw new Error('Body already consumed by stream reader');
188
+ }
189
+ streamConsumed = true;
190
+ return response.json();
191
+ },
192
+ async arrayBuffer() {
193
+ if (streamConsumed) {
194
+ throw new Error('Body already consumed by stream reader');
195
+ }
196
+ streamConsumed = true;
197
+ return response.arrayBuffer();
198
+ },
199
+ async blob() {
200
+ if (streamConsumed) {
201
+ throw new Error('Body already consumed by stream reader');
202
+ }
203
+ streamConsumed = true;
204
+ return response.blob();
205
+ }
206
+ };
207
+ }
208
+
209
+ return response;
210
+ }
211
+
212
+ // Export as function (node-fetch style)
213
+ module.exports = fetchWrapper;
214
+ module.exports.default = fetchWrapper;
215
+
216
+ // Export types
217
+ module.exports.Headers = globalThis.Headers;
218
+ module.exports.Request = globalThis.Request;
219
+ module.exports.Response = globalThis.Response;
220
+
221
+ // FetchError class
222
+ class FetchError extends Error {
223
+ constructor(message, type, systemError) {
224
+ super(message);
225
+ this.name = 'FetchError';
226
+ this.type = type;
227
+ if (systemError) this.code = systemError.code;
228
+ }
229
+ }
230
+ module.exports.FetchError = FetchError;
231
+
232
+ // AbortError class
233
+ class AbortError extends Error {
234
+ constructor(message) {
235
+ super(message);
236
+ this.name = 'AbortError';
237
+ this.type = 'aborted';
238
+ }
239
+ }
240
+ module.exports.AbortError = AbortError;
241
+ `,
242
+ loader: 'js',
243
+ };
244
+ });
245
+ },
246
+ };
247
+
248
+ // Plugin to replace blessed's dynamic widget loading with static requires
249
+ const blessedShimPlugin = {
250
+ name: 'blessed-shim',
251
+ setup(build) {
252
+ // Redirect blessed's widget.js to our patched version with static requires
253
+ build.onResolve({ filter: /^\.\/widget$/ }, (args) => {
254
+ // Only intercept if it's from blessed's blessed.js
255
+ if (args.importer && args.importer.includes('blessed') && args.importer.endsWith('blessed.js')) {
256
+ return {
257
+ path: path.join(__dirname, 'lib', 'blessed-widget-patched.js'),
258
+ };
259
+ }
260
+ return null;
261
+ });
262
+
263
+ // Resolve ./widgets/* requires in our patched widget.js to blessed's actual widgets
264
+ build.onResolve({ filter: /^\.\/widgets\// }, (args) => {
265
+ // If it's from our patched widget file, resolve to blessed's widgets
266
+ if (args.importer && args.importer.includes('blessed-widget-patched.js')) {
267
+ const widgetName = args.path.replace('./widgets/', '');
268
+ const widgetPath = path.join(blessedLibDir, 'widgets', widgetName + '.js');
269
+ if (fs.existsSync(widgetPath)) {
270
+ return { path: widgetPath };
271
+ }
272
+ }
273
+ return null;
274
+ });
275
+
276
+ // Stub out optional blessed dependencies that don't exist
277
+ // Instead of marking external (which fails Bun compile), return empty modules
278
+ build.onResolve({ filter: /^(term\.js|pty\.js)$/ }, (args) => {
279
+ return {
280
+ path: args.path,
281
+ namespace: 'blessed-stub',
282
+ };
283
+ });
284
+
285
+ // Also catch them when required with explicit paths
286
+ build.onResolve({ filter: /term\.js$|pty\.js$/ }, (args) => {
287
+ return {
288
+ path: args.path,
289
+ namespace: 'blessed-stub',
290
+ };
291
+ });
292
+
293
+ // Return empty stub for these modules
294
+ build.onLoad({ filter: /.*/, namespace: 'blessed-stub' }, (args) => {
295
+ return {
296
+ contents: `
297
+ // Stub for ${args.path} - not available in bundled binary
298
+ module.exports = function() {
299
+ throw new Error('${args.path} is not available in bundled binary');
300
+ };
301
+ `,
302
+ loader: 'js',
303
+ };
304
+ });
305
+ },
306
+ };
307
+
308
+ try {
309
+ // Bundle EVERYTHING - no externals
310
+ // This includes SandboxRunner, blessed, and all other dependencies
311
+ await esbuild.build({
312
+ entryPoints: [path.join(__dirname, 'index.js')],
313
+ bundle: true,
314
+ platform: 'node',
315
+ target: 'node16', // Target Node 16 for iSH compatibility
316
+ outfile: fullBundlePath,
317
+ // Inject version at build time
318
+ define: {
319
+ PAVE_VERSION: JSON.stringify(VERSION),
320
+ },
321
+ // Help esbuild find modules
322
+ nodePaths: [path.join(__dirname, 'node_modules')],
323
+ // No external dependencies - everything bundled or stubbed
324
+ external: [],
325
+ // Use plugins to handle dependencies
326
+ plugins: [nodeFetchNativePlugin, blessedShimPlugin],
327
+ // Minify
328
+ minify: true,
329
+ keepNames: true,
330
+ sourcemap: false,
331
+ format: 'cjs',
332
+ // Handle dynamic requires in blessed
333
+ mainFields: ['module', 'main'],
334
+ // Bundle all dependencies
335
+ packages: 'bundle',
336
+ });
337
+
338
+ // Add shebang
339
+ let content = fs.readFileSync(fullBundlePath, 'utf-8');
340
+ content = content.replace(/^(#!.*\n)+/, '');
341
+ content = '#!/usr/bin/env node\n' + content;
342
+ fs.writeFileSync(fullBundlePath, content);
343
+
344
+ const stats = fs.statSync(fullBundlePath);
345
+ console.log(' Full bundle: ' + fullBundlePath);
346
+ console.log(' Size: ' + (stats.size / 1024).toFixed(1) + ' KB');
347
+
348
+ return fullBundlePath;
349
+ } catch (err) {
350
+ console.error(' Bundle failed:', err.message);
351
+
352
+ // Fallback: try bundling with blessed as external and warn
353
+ console.log(' Retrying with blessed as external...');
354
+
355
+ await esbuild.build({
356
+ entryPoints: [path.join(__dirname, 'index.js')],
357
+ bundle: true,
358
+ platform: 'node',
359
+ target: 'node16', // Target Node 16 for iSH compatibility
360
+ outfile: fullBundlePath,
361
+ // Inject version at build time
362
+ define: {
363
+ PAVE_VERSION: JSON.stringify(VERSION),
364
+ },
365
+ nodePaths: [path.join(__dirname, 'node_modules')],
366
+ external: ['blessed'], // Only blessed external
367
+ minify: true,
368
+ keepNames: true,
369
+ sourcemap: false,
370
+ format: 'cjs',
371
+ });
372
+
373
+ let content = fs.readFileSync(fullBundlePath, 'utf-8');
374
+ content = content.replace(/^(#!.*\n)+/, '');
375
+ content = '#!/usr/bin/env node\n' + content;
376
+ fs.writeFileSync(fullBundlePath, content);
377
+
378
+ console.log(' Warning: blessed still external - binary may not work standalone');
379
+ return fullBundlePath;
380
+ }
381
+ }
382
+
383
+ async function obfuscate(inputPath) {
384
+ if (skipObfuscate) {
385
+ console.log('\n[2/3] Skipping obfuscation (--skip-obfuscate)');
386
+ return inputPath;
387
+ }
388
+
389
+ console.log('\n[2/3] Obfuscating bundle (hiding SandboxRunner code)...');
390
+
391
+ const obfuscatedPath = path.join(DIST_DIR, 'pave-obfuscated.js');
392
+
393
+ try {
394
+ // Check if javascript-obfuscator is available
395
+ try {
396
+ execSync('npx javascript-obfuscator --version', { stdio: 'pipe' });
397
+ } catch (e) {
398
+ console.log(' Installing javascript-obfuscator...');
399
+ execSync('npm install --save-dev javascript-obfuscator', { stdio: 'inherit', cwd: __dirname });
400
+ }
401
+
402
+ // Obfuscate with safer settings that preserve function references
403
+ // Use ES2020 target for Node 16 compatibility
404
+ const cmd = [
405
+ 'npx', 'javascript-obfuscator',
406
+ inputPath,
407
+ '--output', obfuscatedPath,
408
+ '--compact', 'true',
409
+ '--control-flow-flattening', 'false', // Disable - breaks dynamic calls
410
+ '--dead-code-injection', 'false', // Disable - can break code
411
+ '--identifier-names-generator', 'mangled', // More predictable than hex
412
+ '--rename-globals', 'false', // Don't rename globals
413
+ '--self-defending', 'false', // Disable
414
+ '--string-array', 'true',
415
+ '--string-array-encoding', 'base64',
416
+ '--string-array-threshold', '0.3', // Lower threshold
417
+ '--transform-object-keys', 'false', // Disable - breaks obj[key] calls
418
+ '--unicode-escape-sequence', 'false',
419
+ '--target', 'node', // Target Node.js environment
420
+ '--options-preset', 'low-obfuscation', // Use low preset to avoid breaking code
421
+ // Preserve critical identifiers and strings
422
+ '--reserved-names', 'authenticated,valid,token,copilot,github,OpenCode,blessed,require,module,exports,process,console,Buffer,__dirname,__filename,program,tput,screen,cursor,options,element,children,parent,box,text,list,input,key,mouse,emit,on,off,render,destroy,focus,append,remove,hide,show,toggle,scroll,setContent,getContent,pushLine,setLine,insertLine,deleteLine,width,height,left,right,top,bottom,rows,cols',
423
+ '--reserved-strings', 'authenticated,valid,token,GitHub Copilot,Saved token is valid,OpenCode',
424
+ // '--ignore-require-imports', 'false',
425
+ '--split-strings', 'false', // Don't split strings that might be function names
426
+ ].join(' ');
427
+
428
+ console.log(' Running obfuscator (this may take a minute)...');
429
+ execSync(cmd, { stdio: 'inherit' });
430
+
431
+ const stats = fs.statSync(obfuscatedPath);
432
+ console.log(' Obfuscated: ' + obfuscatedPath);
433
+ console.log(' Size: ' + (stats.size / 1024).toFixed(1) + ' KB');
434
+
435
+ // Verify obfuscation worked
436
+ const content = fs.readFileSync(obfuscatedPath, 'utf-8');
437
+ const sensitiveStrings = ['newGlobal', 'os.system', 'SandboxRunner', 'drainJobQueue'];
438
+ const found = sensitiveStrings.filter((s) => content.includes(s));
439
+
440
+ if (found.length > 0) {
441
+ console.log(' ⚠️ Warning: Some sensitive strings may still be visible:', found.join(', '));
442
+ } else {
443
+ console.log(' ✅ Sensitive strings successfully obfuscated');
444
+ }
445
+
446
+ return obfuscatedPath;
447
+ } catch (err) {
448
+ console.error(' Obfuscation failed:', err.message);
449
+ console.log(' Continuing without obfuscation...');
450
+ return inputPath;
451
+ }
452
+ }
453
+
454
+ async function buildBinary(bundlePath, targetName, bunTarget) {
455
+ const outputPath = path.join(BIN_DIR, `pave-${targetName}`);
456
+
457
+ console.log(` Building: pave-${targetName}`);
458
+
459
+ try {
460
+ // Don't use --minify to preserve function names/references that esbuild already optimized
461
+ const cmd = `bun build ${bundlePath} --compile --target=${bunTarget} --outfile=${outputPath}`;
462
+ execSync(cmd, { stdio: 'inherit' });
463
+
464
+ if (fs.existsSync(outputPath)) {
465
+ const stats = fs.statSync(outputPath);
466
+ console.log(` Output: ${outputPath}`);
467
+ console.log(` Size: ${(stats.size / 1024 / 1024).toFixed(1)} MB`);
468
+ return true;
469
+ }
470
+ } catch (err) {
471
+ console.error(` Failed: ${err.message}`);
472
+ }
473
+
474
+ return false;
475
+ }
476
+
477
+ async function verifyBinary(binaryPath) {
478
+ console.log('\n Verifying binary...');
479
+
480
+ try {
481
+ const binary = fs.readFileSync(binaryPath);
482
+ const str = binary.toString('utf8');
483
+
484
+ const sensitiveStrings = ['newGlobal', 'os.system', 'SandboxRunner', '__ipc__', 'drainJobQueue'];
485
+ const found = sensitiveStrings.filter((s) => str.includes(s));
486
+
487
+ if (found.length > 0) {
488
+ console.log(' ⚠️ Warning: These strings are visible in binary:', found.join(', '));
489
+ console.log(' Consider running without --skip-obfuscate');
490
+ } else {
491
+ console.log(' ✅ Binary verification passed - sensitive strings are hidden');
492
+ }
493
+ } catch (err) {
494
+ console.log(' Could not verify binary:', err.message);
495
+ }
496
+ }
497
+
498
+ async function main() {
499
+ console.log('='.repeat(60));
500
+ console.log('PAVE Native Binary Build');
501
+ console.log('='.repeat(60));
502
+ console.log('');
503
+ console.log('Process: Bundle → Obfuscate → Compile to Binary');
504
+ console.log('');
505
+ console.log('All code (including SandboxRunner) will be bundled inside the binary.');
506
+ console.log('No external JavaScript files will be needed!');
507
+ console.log('');
508
+
509
+ await checkBun();
510
+
511
+ // Ensure dist directory exists
512
+ fs.mkdirSync(BIN_DIR, { recursive: true });
513
+
514
+ // Step 1: Bundle everything (including SandboxRunner)
515
+ const fullBundle = await bundleAll();
516
+
517
+ // Step 2: Obfuscate (hide SandboxRunner internals)
518
+ const obfuscatedBundle = await obfuscate(fullBundle);
519
+
520
+ // Step 3: Compile to binary
521
+ console.log('\n[3/3] Compiling to native binary with Bun...');
522
+
523
+ let success = false;
524
+ let builtBinary = null;
525
+
526
+ if (target === 'all') {
527
+ for (const [targetName, bunTarget] of Object.entries(TARGETS)) {
528
+ const result = await buildBinary(obfuscatedBundle, targetName, bunTarget);
529
+ if (result) {
530
+ success = true;
531
+ builtBinary = path.join(BIN_DIR, `pave-${targetName}`);
532
+ }
533
+ }
534
+ } else if (target === 'current') {
535
+ const platform = process.platform === 'darwin' ? 'darwin' : 'linux';
536
+ const arch = process.arch === 'arm64' ? 'arm64' : 'x64';
537
+ const targetName = `${platform}-${arch}`;
538
+ const bunTarget = TARGETS[targetName];
539
+ success = await buildBinary(obfuscatedBundle, targetName, bunTarget);
540
+ if (success) {
541
+ builtBinary = path.join(BIN_DIR, `pave-${targetName}`);
542
+ }
543
+ } else if (TARGETS[target]) {
544
+ success = await buildBinary(obfuscatedBundle, target, TARGETS[target]);
545
+ if (success) {
546
+ builtBinary = path.join(BIN_DIR, `pave-${target}`);
547
+ }
548
+ }
549
+
550
+ // Verify the binary
551
+ if (builtBinary && fs.existsSync(builtBinary)) {
552
+ await verifyBinary(builtBinary);
553
+ }
554
+
555
+ console.log('\n' + '='.repeat(60));
556
+
557
+ if (success) {
558
+ console.log('✅ Build complete!');
559
+ console.log('');
560
+ console.log('📍 Binary location: ' + BIN_DIR);
561
+ console.log('');
562
+ console.log('🧪 Test commands:');
563
+ const platform = process.platform === 'darwin' ? 'darwin' : 'linux';
564
+ const arch = process.arch === 'arm64' ? 'arm64' : 'x64';
565
+ console.log(` ${BIN_DIR}/pave-${platform}-${arch} --help`);
566
+ console.log(` ${BIN_DIR}/pave-${platform}-${arch} --version`);
567
+ console.log('');
568
+ console.log('🔍 Verify code is hidden:');
569
+ console.log(` strings ${BIN_DIR}/pave-${platform}-${arch} | grep -i "newGlobal"`);
570
+ console.log(' (should return nothing)');
571
+ console.log('');
572
+ console.log('📦 Distribution:');
573
+ console.log(' The binary is self-contained - no external JS files needed!');
574
+ console.log(' Only SpiderMonkey is required for sandbox execution:');
575
+ console.log(' brew install spidermonkey');
576
+ console.log('');
577
+ console.log('='.repeat(60));
578
+ } else {
579
+ console.log('❌ Build failed!');
580
+ process.exit(1);
581
+ }
582
+ }
583
+
584
+ // Only run main() when script is executed directly, not when imported
585
+ // This allows tests to import TARGETS without triggering the build process
586
+ if (require.main === module) {
587
+ main().catch((err) => {
588
+ console.error('Build error:', err);
589
+ process.exit(1);
590
+ });
591
+ }