@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.
- package/MARKETPLACE.md +406 -0
- package/README.md +218 -21
- package/build-binary.js +591 -0
- package/build-npm.js +537 -0
- package/build.js +230 -0
- package/check-binary.js +26 -0
- package/deploy.sh +95 -0
- package/index.js +5775 -0
- package/lib/agent-registry.js +1037 -0
- package/lib/args-parser.js +837 -0
- package/lib/blessed-widget-patched.js +93 -0
- package/lib/cli-markdown.js +590 -0
- package/lib/compaction.js +153 -0
- package/lib/duration.js +94 -0
- package/lib/hash.js +22 -0
- package/lib/marketplace.js +866 -0
- package/lib/memory-config.js +166 -0
- package/lib/skill-manager.js +891 -0
- package/lib/soul.js +31 -0
- package/lib/tool-output-formatter.js +180 -0
- package/package.json +35 -33
- package/start-pave.sh +149 -0
- package/status.js +271 -0
- package/test/abort-stream.test.js +445 -0
- package/test/agent-auto-compaction.test.js +552 -0
- package/test/agent-comm-abort.test.js +95 -0
- package/test/agent-comm.test.js +598 -0
- package/test/agent-inbox.test.js +576 -0
- package/test/agent-init.test.js +264 -0
- package/test/agent-interrupt.test.js +314 -0
- package/test/agent-lifecycle.test.js +520 -0
- package/test/agent-log-files.test.js +349 -0
- package/test/agent-mode.manual-test.js +392 -0
- package/test/agent-parsing.test.js +228 -0
- package/test/agent-post-stream-idle.test.js +762 -0
- package/test/agent-registry.test.js +359 -0
- package/test/agent-rm.test.js +442 -0
- package/test/agent-spawn.test.js +933 -0
- package/test/agent-status-api.test.js +624 -0
- package/test/agent-update.test.js +435 -0
- package/test/args-parser.test.js +391 -0
- package/test/auto-compaction-chat.manual-test.js +227 -0
- package/test/auto-compaction.test.js +941 -0
- package/test/build-config.test.js +120 -0
- package/test/build-npm.test.js +388 -0
- package/test/chat-command.test.js +137 -0
- package/test/chat-leading-lines.test.js +159 -0
- package/test/config-flag.test.js +272 -0
- package/test/cursor-drift.test.js +135 -0
- package/test/debug-require.js +23 -0
- package/test/dir-migration.test.js +323 -0
- package/test/duration.test.js +229 -0
- package/test/ghostty-term.test.js +202 -0
- package/test/http500-backoff.test.js +854 -0
- package/test/integration.test.js +86 -0
- package/test/memory-guard-env.test.js +220 -0
- package/test/pr233-fixes.test.js +259 -0
- package/test/run-agent-init.js +297 -0
- package/test/run-all.js +64 -0
- package/test/run-config-flag.js +159 -0
- package/test/run-cursor-drift.js +82 -0
- package/test/run-session-path.js +154 -0
- package/test/run-tests.js +643 -0
- package/test/sandbox-redirect.test.js +202 -0
- package/test/session-path.test.js +132 -0
- package/test/shebang-strip.test.js +241 -0
- package/test/soul-reinject.test.js +1027 -0
- package/test/soul-reread.test.js +281 -0
- package/test/tool-output-formatter.test.js +486 -0
- package/test/tool-output-gating.test.js +143 -0
- package/test/tool-states.test.js +167 -0
- package/test/tools-flag.test.js +65 -0
- package/test/tui-attach.test.js +1255 -0
- package/test/tui-compaction.test.js +354 -0
- package/test/tui-wrap.test.js +568 -0
- package/test-binary.js +52 -0
- package/test-binary2.js +36 -0
- package/LICENSE +0 -21
- package/pave.js +0 -2
- package/sandbox/SandboxRunner.js +0 -1
- package/sandbox/pave-run.js +0 -2
- package/sandbox/permission.js +0 -1
- package/sandbox/utils/yaml.js +0 -1
package/build-binary.js
ADDED
|
@@ -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
|
+
}
|