sphere-cli 0.1.40 → 0.2.0

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/bin/sphere.js CHANGED
@@ -2,443 +2,33 @@
2
2
  'use strict';
3
3
 
4
4
  /**
5
- * sphere — CLI entry point.
5
+ * sphere — thin launcher.
6
6
  *
7
- * This wrapper handles argument parsing, progress display, and license checks,
8
- * then delegates generation to sphere-node.jsc (V8 bytecode, bytenode-loaded).
9
- *
10
- * Commands:
11
- * sphere generate <input.csv> -o <output.csv> [options]
12
- * sphere license activate|status|clear
13
- * sphere demo
14
- * sphere evaluate — not available in this version (requires Python sidecar)
15
- * sphere certify — not available in this version (requires Python sidecar)
7
+ * The actual CLI is a sealed, signed+notarized native binary (PyInstaller bundle
8
+ * of the SPHERE engine same engine as the desktop app). The npm `postinstall`
9
+ * downloads the platform-specific binary into ./vendor/. This launcher just
10
+ * forwards argv to it. No readable algorithm code ships in the npm package.
16
11
  */
17
12
 
18
- const { spawn } = require('child_process');
19
- const path = require('path');
20
- const fs = require('fs');
21
- const https = require('https');
22
- const os = require('os');
23
-
24
- const PKG_DIR = path.join(__dirname, '..');
25
- const SPHERE_NODE_JS = path.join(PKG_DIR, 'sphere-node.js');
26
- const VERSION = require(path.join(PKG_DIR, 'package.json')).version;
27
-
28
- // ── License ───────────────────────────────────────────────────────────────────
13
+ const path = require('path');
14
+ const fs = require('fs');
15
+ const { spawnSync } = require('child_process');
29
16
 
30
- const LICENSE_WORKER_URL = process.env.SPHERE_WORKER_URL
31
- || 'https://sphere-license.statzihuai.workers.dev';
32
- const LICENSE_CACHE_DAYS = 1;
33
- const LICENSE_REQUIRED = (process.env.SPHERE_LICENSE_REQUIRED || 'true') !== 'false';
34
-
35
- function sphereConfigDir() {
36
- const d = path.join(os.homedir(), '.config', 'sphere');
37
- fs.mkdirSync(d, { recursive: true });
38
- return d;
39
- }
40
- function licenseKeyFile() { return path.join(sphereConfigDir(), 'license_key'); }
41
- function licenseCacheFile() { return path.join(sphereConfigDir(), 'license_cache.json'); }
42
-
43
- function readStoredLicenseKey() {
44
- const f = licenseKeyFile();
45
- if (!fs.existsSync(f)) return null;
46
- return fs.readFileSync(f, 'utf8').trim() || null;
47
- }
48
-
49
- function writeLicenseKey(key) {
50
- const f = licenseKeyFile();
51
- fs.writeFileSync(f, key, { encoding: 'utf8', mode: 0o600 });
52
- }
17
+ const BIN = path.join(__dirname, '..', 'vendor', 'sphere-cli', 'sphere');
53
18
 
54
- function readLicenseCache() {
55
- const f = licenseCacheFile();
56
- if (!fs.existsSync(f)) return null;
57
- try {
58
- const data = JSON.parse(fs.readFileSync(f, 'utf8'));
59
- const ageDays = (Date.now() / 1000 - (data.cachedAt || 0)) / 86400;
60
- return ageDays <= LICENSE_CACHE_DAYS ? data : null;
61
- } catch { return null; }
62
- }
63
-
64
- function writeLicenseCache(data) {
65
- fs.writeFileSync(
66
- licenseCacheFile(),
67
- JSON.stringify({ ...data, cachedAt: Math.floor(Date.now() / 1000) }),
68
- 'utf8',
19
+ if (!fs.existsSync(BIN)) {
20
+ process.stderr.write(
21
+ '\nSPHERE binary not found.\n' +
22
+ 'The platform binary is downloaded by the install step. Try reinstalling:\n' +
23
+ ' npm install -g sphere-cli\n' +
24
+ 'If your platform is unsupported, see https://github.com/statzihuai/sphere-cli\n',
69
25
  );
70
- }
71
-
72
- function validateKeyOnline(key) {
73
- return new Promise((resolve, reject) => {
74
- const body = Buffer.from(JSON.stringify({ key }));
75
- const req = https.request(
76
- `${LICENSE_WORKER_URL}/validate`,
77
- {
78
- method: 'POST',
79
- headers: {
80
- 'Content-Type': 'application/json',
81
- 'Content-Length': body.length,
82
- 'User-Agent': 'sphere-cli/1.0',
83
- },
84
- },
85
- (res) => {
86
- let buf = '';
87
- res.on('data', (d) => (buf += d));
88
- res.on('end', () => {
89
- try { resolve(JSON.parse(buf)); }
90
- catch { reject(new Error('Invalid JSON from license server')); }
91
- });
92
- },
93
- );
94
- req.on('error', reject);
95
- req.setTimeout(10000, () => { req.destroy(new Error('License server timeout')); });
96
- req.write(body);
97
- req.end();
98
- });
99
- }
100
-
101
- async function checkLicense() {
102
- if (!LICENSE_REQUIRED) return;
103
- const key = readStoredLicenseKey();
104
- if (!key) {
105
- process.stderr.write(
106
- '✗ No SPHERE license found.\n' +
107
- ' Run: sphere license activate <key>\n' +
108
- ' Contact zihuai@stanford.edu to get a license.\n',
109
- );
110
- process.exit(1);
111
- }
112
-
113
- let result;
114
- try {
115
- result = await validateKeyOnline(key);
116
- writeLicenseCache(result);
117
- } catch {
118
- result = readLicenseCache();
119
- if (!result) {
120
- process.stderr.write(
121
- '✗ License server unreachable and local cache has expired.\n' +
122
- ' Connect to the internet and re-run to refresh your license.\n',
123
- );
124
- process.exit(1);
125
- }
126
- }
127
-
128
- if (!result.valid) {
129
- process.stderr.write(
130
- `✗ License invalid: ${result.error || 'unknown error'}\n` +
131
- ' Run: sphere license activate <key>\n',
132
- );
133
- process.exit(1);
134
- }
135
- }
136
-
137
- // ── Progress bar ──────────────────────────────────────────────────────────────
138
-
139
- const BAR_WIDTH = 28;
140
-
141
- function progressBar(frac, msg) {
142
- const f = Math.min(1, Math.max(0, frac));
143
- const filled = Math.round(BAR_WIDTH * f);
144
- const bar = '█'.repeat(filled) + '░'.repeat(BAR_WIDTH - filled);
145
- const pct = (f * 100).toFixed(1).padStart(5);
146
- process.stderr.write(`\r [${bar}] ${pct}% ${(msg || '').slice(0, 45).padEnd(45)}\x1b[K`);
147
- }
148
-
149
- function clearLine() {
150
- process.stderr.write(`\r${' '.repeat(BAR_WIDTH + 60)}\r`);
151
- }
152
-
153
- // ── Argument parsing helpers ──────────────────────────────────────────────────
154
-
155
- function parseArgs(argv) {
156
- const args = argv.slice(2);
157
- const pos = [];
158
- const flags = {};
159
- for (let i = 0; i < args.length; i++) {
160
- const a = args[i];
161
- if (a === '-o') {
162
- flags['output'] = args[++i];
163
- } else if (a.startsWith('--')) {
164
- const key = a.slice(2);
165
- const next = args[i + 1];
166
- if (next === undefined || next.startsWith('-')) {
167
- flags[key] = true;
168
- } else {
169
- flags[key] = next;
170
- i++;
171
- }
172
- } else if (a.startsWith('-') && a.length === 2) {
173
- flags[a.slice(1)] = args[++i];
174
- } else {
175
- pos.push(a);
176
- }
177
- }
178
- return { pos, flags };
179
- }
180
-
181
- // ── sphere generate ───────────────────────────────────────────────────────────
182
-
183
- async function cmdGenerate(pos, flags) {
184
- await checkLicense();
185
-
186
- const input = pos[0];
187
- const output = flags.output;
188
-
189
- if (!input) { process.stderr.write('Error: missing input CSV\n'); process.exit(1); }
190
- if (!output) { process.stderr.write('Error: missing -o / --output path\n'); process.exit(1); }
191
- if (!fs.existsSync(input)) {
192
- process.stderr.write(`Error: file not found: ${input}\n`);
193
- process.exit(1);
194
- }
195
-
196
- if (!flags.json) {
197
- process.stderr.write(`Generating synthetic data from ${path.basename(input)} …\n`);
198
- }
199
-
200
- // Build args for sphere-node.js
201
- const nodeArgs = [SPHERE_NODE_JS, input, output];
202
- if (flags.k) nodeArgs.push('--k', String(flags.k));
203
- if (flags.theta) nodeArgs.push('--theta', String(flags.theta));
204
- if (flags.delta) nodeArgs.push('--delta', String(flags.delta));
205
- if (flags['mix-prob']) nodeArgs.push('--mix-prob', String(flags['mix-prob']));
206
- if (flags.seed) nodeArgs.push('--seed', String(flags.seed));
207
-
208
- return new Promise((resolve) => {
209
- const proc = spawn(process.execPath, nodeArgs, { stdio: ['ignore', 'pipe', 'pipe'] });
210
-
211
- let resultLine = '';
212
- let errorLine = '';
213
-
214
- proc.stdout.on('data', (d) => { resultLine += d.toString(); });
215
-
216
- proc.stderr.on('data', (d) => {
217
- for (const line of d.toString().split('\n')) {
218
- const s = line.trim();
219
- if (!s) continue;
220
- try {
221
- const msg = JSON.parse(s);
222
- if (typeof msg.progress === 'number' && !flags.json) {
223
- progressBar(msg.progress, '');
224
- } else if (msg.error) {
225
- errorLine = msg.error;
226
- }
227
- } catch { /* ignore non-JSON lines */ }
228
- }
229
- });
230
-
231
- proc.on('close', (code) => {
232
- if (!flags.json) clearLine();
233
-
234
- if (code !== 0) {
235
- process.stderr.write(`Error: ${errorLine || 'sphere-node exited with code ' + code}\n`);
236
- process.exit(1);
237
- }
238
-
239
- let result;
240
- try { result = JSON.parse(resultLine.trim()); }
241
- catch {
242
- process.stderr.write('Error: could not parse sphere-node output\n');
243
- process.exit(1);
244
- }
245
-
246
- if (flags.json) {
247
- process.stdout.write(JSON.stringify(result) + '\n');
248
- resolve();
249
- return;
250
- }
251
-
252
- const elapsedS = ((result.elapsedMs || 0) / 1000).toFixed(1);
253
- process.stdout.write(
254
- `✓ ${output} ${(result.rows || 0).toLocaleString()} rows \xd7 ${result.cols || '?'} cols` +
255
- ` (${elapsedS} s) seed ${result.seed}\n`,
256
- );
257
- if (result.idColDetected) {
258
- process.stdout.write(` ID columns excluded: ${result.idColName}\n`);
259
- }
260
- resolve();
261
- });
262
-
263
- proc.on('error', (err) => {
264
- process.stderr.write(`Error: failed to start sphere-node: ${err.message}\n`);
265
- process.exit(1);
266
- });
267
- });
268
- }
269
-
270
- // ── sphere demo ───────────────────────────────────────────────────────────────
271
-
272
- async function cmdDemo() {
273
- const exampleCsv = path.join(PKG_DIR, 'examples', 'nhanes_sample.csv');
274
- if (!fs.existsSync(exampleCsv)) {
275
- process.stderr.write('Error: built-in example not found at ' + exampleCsv + '\n');
276
- process.exit(1);
277
- }
278
-
279
- const { randomUUID } = require('crypto');
280
- const tmpOut = path.join(os.tmpdir(), `sphere-demo-${randomUUID()}.csv`);
281
-
282
- process.stdout.write('SPHERE demo — built-in NHANES dataset (4,899 rows \xd7 18 cols)\n');
283
- process.stdout.write('─'.repeat(52) + '\n\n');
284
-
285
- try {
286
- await cmdGenerate([exampleCsv], { output: tmpOut });
287
- } finally {
288
- try { fs.unlinkSync(tmpOut); } catch {}
289
- try { fs.unlinkSync(tmpOut + '.sphere.json'); } catch {}
290
- }
291
-
292
- process.stdout.write('\nTry it on your own data:\n');
293
- process.stdout.write(' sphere generate your_data.csv -o synthetic.csv\n\n');
294
- }
295
-
296
- // ── sphere license ────────────────────────────────────────────────────────────
297
-
298
- async function cmdLicense(sub, pos, flags) {
299
- if (sub === 'activate') {
300
- let key = (pos[0] || flags.key || '').trim();
301
- if (!key) {
302
- // Simple stdin prompt (no readline dependency)
303
- process.stdout.write('SPHERE license key (sphere_…): ');
304
- key = await new Promise((resolve) => {
305
- let buf = '';
306
- process.stdin.resume();
307
- process.stdin.setEncoding('utf8');
308
- process.stdin.on('data', (ch) => {
309
- const c = ch.toString();
310
- if (c.includes('\n') || c.includes('\r')) {
311
- process.stdin.pause();
312
- process.stdout.write('\n');
313
- resolve(buf.trim());
314
- } else {
315
- buf += c;
316
- }
317
- });
318
- });
319
- }
320
- if (!key.startsWith('sphere_')) {
321
- process.stderr.write("Error: key must start with 'sphere_'\n");
322
- process.exit(1);
323
- }
324
- process.stdout.write('Validating …');
325
- let result;
326
- try { result = await validateKeyOnline(key); }
327
- catch (e) {
328
- process.stderr.write(`\nError: could not reach license server — ${e.message}\n`);
329
- process.exit(1);
330
- }
331
- if (!result.valid) {
332
- process.stderr.write(`\r✗ Invalid key: ${result.error || 'unknown error'}\n`);
333
- process.exit(1);
334
- }
335
- writeLicenseKey(key);
336
- writeLicenseCache(result);
337
- process.stdout.write(`\r✓ License activated — ${result.customer || ''}\n`);
338
- if (result.expiry) process.stdout.write(` Expires: ${result.expiry}\n`);
339
- return;
340
- }
341
-
342
- if (sub === 'status') {
343
- const key = readStoredLicenseKey();
344
- if (!key) {
345
- process.stdout.write('✗ No license key configured.\n Run: sphere license activate <key>\n');
346
- return;
347
- }
348
- process.stdout.write('Checking …');
349
- let offline = false;
350
- let result;
351
- try {
352
- result = await validateKeyOnline(key);
353
- writeLicenseCache(result);
354
- } catch {
355
- result = readLicenseCache();
356
- offline = true;
357
- if (!result) {
358
- process.stdout.write('\r✗ License server unreachable and cache expired.\n');
359
- return;
360
- }
361
- }
362
- if (result.valid) {
363
- const suffix = offline ? ' (offline — cached)' : '';
364
- process.stdout.write(`\r✓ License valid — ${result.customer || ''}${suffix}\n`);
365
- if (result.expiry) process.stdout.write(` Expires: ${result.expiry}\n`);
366
- } else {
367
- process.stdout.write(`\r✗ License invalid: ${result.error || 'unknown'}\n`);
368
- }
369
- return;
370
- }
371
-
372
- if (sub === 'clear') {
373
- let removed = false;
374
- for (const f of [licenseKeyFile(), licenseCacheFile()]) {
375
- if (fs.existsSync(f)) { fs.unlinkSync(f); removed = true; }
376
- }
377
- process.stdout.write(removed ? '✓ License cleared.\n' : 'No license stored.\n');
378
- return;
379
- }
380
-
381
- process.stderr.write('Usage: sphere license activate|status|clear\n');
382
26
  process.exit(1);
383
27
  }
384
28
 
385
- // ── Main dispatch ─────────────────────────────────────────────────────────────
386
-
387
- async function main() {
388
- const { pos, flags } = parseArgs(process.argv);
389
- const cmd = pos[0];
390
-
391
- if (flags.version || flags.v) {
392
- process.stdout.write(`${VERSION}\n`);
393
- process.exit(0);
394
- }
395
-
396
- if (!cmd || flags.help || flags.h) {
397
- process.stdout.write(
398
- `SPHERE CLI v${VERSION} — synthetic data generation\n\n` +
399
- 'Usage:\n' +
400
- ' sphere generate <input.csv> -o <output.csv> [options]\n' +
401
- ' sphere license activate|status|clear\n' +
402
- ' sphere demo\n\n' +
403
- 'Generate options:\n' +
404
- ' --k <int> Synthesis depth (default: 2)\n' +
405
- ' --theta <float> Rotation angle in radians (default: π/6 ≈ 0.524)\n' +
406
- ' --delta <float> Per-pair angle jitter (default: 5° ≈ 0.087)\n' +
407
- ' --mix-prob <float> Privacy/utility trade-off (default: 0.75)\n' +
408
- ' --seed <int> Integer seed for reproducible output\n' +
409
- ' --json Machine-readable JSON output\n',
410
- );
411
- process.exit(0);
412
- }
413
-
414
- if (cmd === 'generate') {
415
- await cmdGenerate(pos.slice(1), flags);
416
- return;
417
- }
418
-
419
- if (cmd === 'demo') {
420
- await cmdDemo();
421
- return;
422
- }
423
-
424
- if (cmd === 'license') {
425
- await cmdLicense(pos[1], pos.slice(2), flags);
426
- return;
427
- }
428
-
429
- if (cmd === 'evaluate' || cmd === 'certify') {
430
- process.stderr.write(
431
- `'sphere ${cmd}' requires the Python evaluation engine, which is not included\n` +
432
- 'in this version. Coming soon.\n',
433
- );
434
- process.exit(1);
435
- }
436
-
437
- process.stderr.write(`Unknown command: ${cmd}\nRun 'sphere --help' for usage.\n`);
29
+ const res = spawnSync(BIN, process.argv.slice(2), { stdio: 'inherit' });
30
+ if (res.error) {
31
+ process.stderr.write(`Failed to launch sphere: ${res.error.message}\n`);
438
32
  process.exit(1);
439
33
  }
440
-
441
- main().catch((err) => {
442
- process.stderr.write(`\nError: ${err.message || err}\n`);
443
- process.exit(1);
444
- });
34
+ process.exit(res.status === null ? 1 : res.status);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sphere-cli",
3
- "version": "0.1.40",
4
- "description": "SPHERE CLI — synthetic data generation, evaluation, and certification",
3
+ "version": "0.2.0",
4
+ "description": "SPHERE CLI — synthetic data generation, evaluation, and certification (sealed native binary)",
5
5
  "keywords": ["synthetic-data", "privacy", "cli", "data-science"],
6
6
  "homepage": "https://github.com/statzihuai/sphere-cli",
7
7
  "bugs": "https://github.com/statzihuai/sphere-cli/issues",
@@ -11,19 +11,15 @@
11
11
  },
12
12
  "license": "SEE LICENSE IN LICENSE",
13
13
  "bin": {
14
- "sphere": "./bin/sphere.js"
14
+ "sphere": "bin/sphere.js"
15
15
  },
16
16
  "scripts": {
17
- "postinstall": "node scripts/postinstall.js"
18
- },
19
- "dependencies": {
20
- "bytenode": "^1.5.7"
17
+ "postinstall": "node scripts/postinstall.js",
18
+ "release": "bash scripts/release.sh"
21
19
  },
22
20
  "files": [
23
- "bin/",
24
- "examples/",
25
- "scripts/postinstall.js",
26
- "sphere-node.js"
21
+ "bin/sphere.js",
22
+ "scripts/postinstall.js"
27
23
  ],
28
24
  "engines": {
29
25
  "node": ">=18.0.0"
@@ -1,93 +1,106 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * SPHERE CLI postinstall — compiles sphere-node.js to V8 bytecode.
4
+ * SPHERE CLI postinstall — download the platform-specific sealed binary.
5
5
  *
6
- * sphere-node.js in this package is an obfuscated JS bundle of the SPHERE
7
- * algorithm. Compiling it to V8 bytecode (.jsc) at install time ensures the
8
- * bytecode matches the Node.js version on this machine, preventing
9
- * decompilation. After compilation the source file is replaced by a tiny
10
- * loader stub so only bytecode remains on disk.
6
+ * The SPHERE engine ships as a signed + notarized native binary (one per
7
+ * platform) hosted on GitHub Releases. This script detects the platform,
8
+ * downloads the matching tarball, verifies its SHA-256 against the published
9
+ * SHA256SUMS.txt, and extracts it into ./vendor/. No algorithm source ships in
10
+ * the npm package only this downloader + a thin launcher.
11
11
  *
12
- * This mirrors exactly how the SPHERE desktop app protects its code.
12
+ * Env:
13
+ * SPHERE_SKIP_POSTINSTALL=1 skip download (CI / offline)
14
+ * SPHERE_BINARY_BASEURL=... override the release base URL (testing)
13
15
  */
14
16
 
17
+ const fs = require('fs');
15
18
  const path = require('path');
16
- const fs = require('fs');
19
+ const crypto = require('crypto');
20
+ const { execFileSync } = require('child_process');
17
21
 
18
- const PKG_DIR = path.join(__dirname, '..');
19
- const SRC = path.join(PKG_DIR, 'sphere-node.js');
20
- const OUT = path.join(PKG_DIR, 'sphere-node.jsc');
21
- const STUB = "'use strict';\nrequire('bytenode');\nrequire('./sphere-node.jsc');\n";
22
- const VERSION = require('../package.json').version;
23
- const MARKER = path.join(PKG_DIR, '.sphere-node-version');
22
+ const PKG = require('../package.json');
23
+ const VERSION = PKG.version;
24
+ const REPO = 'statzihuai/sphere-cli';
25
+
26
+ const PLATFORM = process.platform; // 'darwin' | 'linux'
27
+ const ARCH = process.arch; // 'arm64' | 'x64'
28
+ const KEY = `${PLATFORM}-${ARCH}`;
29
+ const SUPPORTED = new Set(['darwin-arm64', 'darwin-x64', 'linux-x64']);
30
+
31
+ const PKG_ROOT = path.join(__dirname, '..');
32
+ const VENDOR = path.join(PKG_ROOT, 'vendor');
33
+ const ASSET = `sphere-${KEY}.tar.gz`;
34
+ const BASE = process.env.SPHERE_BINARY_BASEURL
35
+ || `https://github.com/${REPO}/releases/download/v${VERSION}`;
36
+
37
+ function log(m) { process.stdout.write(`sphere-cli: ${m}\n`); }
38
+ function fail(m) { process.stderr.write(`sphere-cli: ${m}\n`); process.exit(1); }
24
39
 
25
- // ── Skip flag ─────────────────────────────────────────────────────────────────
26
40
  if (process.env.SPHERE_SKIP_POSTINSTALL === '1') {
27
- process.stdout.write('SPHERE_SKIP_POSTINSTALL=1 — skipping bytecode compilation.\n');
41
+ log('SPHERE_SKIP_POSTINSTALL=1 — skipping binary download.');
28
42
  process.exit(0);
29
43
  }
30
44
 
31
- // ── Already compiled for this version? ───────────────────────────────────────
32
- const installedVer = fs.existsSync(MARKER)
33
- ? fs.readFileSync(MARKER, 'utf8').trim()
34
- : null;
35
-
36
- if (fs.existsSync(OUT) && installedVer === VERSION) {
37
- process.stdout.write(`✓ SPHERE algorithm already compiled for v${VERSION}.\n`);
45
+ if (!SUPPORTED.has(KEY)) {
46
+ // Don't hard-fail the install; the launcher prints guidance if run.
47
+ process.stderr.write(
48
+ `sphere-cli: no prebuilt binary for ${KEY} yet ` +
49
+ `(supported: ${[...SUPPORTED].join(', ')}).\n`,
50
+ );
38
51
  process.exit(0);
39
52
  }
40
53
 
41
- // ── Source check ──────────────────────────────────────────────────────────────
42
- if (!fs.existsSync(SRC)) {
43
- process.stderr.write('sphere-node.js not found in package — reinstall sphere-cli.\n');
44
- process.exit(1);
54
+ // Already installed for this version?
55
+ const STAMP = path.join(VENDOR, '.version');
56
+ const BIN = path.join(VENDOR, 'sphere-cli', 'sphere');
57
+ if (fs.existsSync(BIN) && fs.existsSync(STAMP) &&
58
+ fs.readFileSync(STAMP, 'utf8').trim() === VERSION) {
59
+ log(`binary already present for v${VERSION}.`);
60
+ process.exit(0);
45
61
  }
46
62
 
47
- const srcText = fs.readFileSync(SRC, 'utf8');
48
- if (srcText.includes("require('bytenode')") && srcText.length < 200) {
49
- // Already stubbed from a previous install .jsc must exist
50
- if (fs.existsSync(OUT)) {
51
- process.stdout.write('✓ SPHERE algorithm already compiled.\n');
52
- process.exit(0);
53
- }
54
- process.stderr.write('sphere-node.js is a stub but sphere-node.jsc is missing. Reinstall sphere-cli.\n');
55
- process.exit(1);
63
+ function curl(url, dest) {
64
+ execFileSync('curl', ['-fL', '--retry', '3', '--retry-delay', '2',
65
+ '--connect-timeout', '30', '-o', dest, url], { stdio: ['ignore', 'ignore', 'inherit'] });
56
66
  }
57
67
 
58
- // ── Compile ───────────────────────────────────────────────────────────────────
59
- process.stdout.write('Compiling SPHERE algorithm for your Node.js version …\n');
68
+ function sha256(file) {
69
+ const h = crypto.createHash('sha256');
70
+ h.update(fs.readFileSync(file));
71
+ return h.digest('hex');
72
+ }
60
73
 
61
- let bytenode;
62
74
  try {
63
- bytenode = require('bytenode');
64
- } catch {
65
- process.stderr.write(
66
- 'bytenode is not installed — this is unexpected.\n' +
67
- 'Try: cd "$(npm root -g)/sphere-cli" && npm install bytenode\n',
68
- );
69
- process.exit(1);
70
- }
75
+ fs.mkdirSync(VENDOR, { recursive: true });
76
+ const tarball = path.join(VENDOR, ASSET);
77
+
78
+ log(`downloading ${ASSET} (v${VERSION}) …`);
79
+ curl(`${BASE}/${ASSET}`, tarball);
71
80
 
72
- Promise.resolve(bytenode.compileFile({ filename: SRC, output: OUT }))
73
- .then(() => {
74
- // Replace source with loader stub so no readable code remains on disk
75
- fs.writeFileSync(SRC, STUB, 'utf8');
76
- // Write version marker
77
- fs.writeFileSync(MARKER, VERSION, 'utf8');
78
-
79
- const sizeKB = (fs.statSync(OUT).size / 1024).toFixed(1);
80
- process.stdout.write(`✓ SPHERE algorithm compiled (${sizeKB} KB).\n\n`);
81
- process.stdout.write(' Quick start:\n');
82
- process.stdout.write(' sphere generate data.csv -o synthetic.csv\n');
83
- process.stdout.write(" Run 'sphere --help' for all options.\n\n");
84
- })
85
- .catch((err) => {
86
- process.stderr.write(`\n✗ SPHERE compilation failed: ${err.message}\n`);
87
- process.stderr.write(
88
- 'The package was installed but the algorithm could not be compiled.\n' +
89
- 'Try reinstalling: npm install -g sphere-cli\n\n',
90
- );
91
- // Exit 0 so npm install doesn't fail — will error on first use
92
- process.exit(0);
93
- });
81
+ // Verify checksum against the published SHA256SUMS.txt
82
+ const sumsFile = path.join(VENDOR, 'SHA256SUMS.txt');
83
+ curl(`${BASE}/SHA256SUMS.txt`, sumsFile);
84
+ const sums = fs.readFileSync(sumsFile, 'utf8');
85
+ const expected = sums.split('\n')
86
+ .map((l) => l.trim().split(/\s+/))
87
+ .find((p) => p[1] && p[1].endsWith(ASSET));
88
+ if (!expected) fail(`no checksum for ${ASSET} in SHA256SUMS.txt`);
89
+ const got = sha256(tarball);
90
+ if (got !== expected[0]) {
91
+ fail(`checksum mismatch for ${ASSET}\n expected ${expected[0]}\n got ${got}`);
92
+ }
93
+ log('checksum verified ✓');
94
+
95
+ // Extract
96
+ fs.rmSync(path.join(VENDOR, 'sphere-cli'), { recursive: true, force: true });
97
+ execFileSync('tar', ['-xzf', tarball, '-C', VENDOR], { stdio: 'inherit' });
98
+ fs.chmodSync(BIN, 0o755);
99
+ fs.unlinkSync(tarball);
100
+ fs.writeFileSync(STAMP, VERSION);
101
+
102
+ log(`installed sealed binary for ${KEY} ✓`);
103
+ } catch (e) {
104
+ fail(`failed to install binary: ${e.message}\n` +
105
+ `You can retry with: npm rebuild sphere-cli`);
106
+ }