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 +19 -429
- package/package.json +7 -11
- package/scripts/postinstall.js +82 -69
- package/examples/nhanes_sample.csv +0 -4900
- package/sphere-node.js +0 -7
package/bin/sphere.js
CHANGED
|
@@ -2,443 +2,33 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* sphere —
|
|
5
|
+
* sphere — thin launcher.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
19
|
-
const
|
|
20
|
-
const
|
|
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
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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.
|
|
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": "
|
|
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
|
-
"
|
|
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"
|
package/scripts/postinstall.js
CHANGED
|
@@ -1,93 +1,106 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* SPHERE CLI postinstall —
|
|
4
|
+
* SPHERE CLI postinstall — download the platform-specific sealed binary.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
*
|
|
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
|
|
19
|
+
const crypto = require('crypto');
|
|
20
|
+
const { execFileSync } = require('child_process');
|
|
17
21
|
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
const
|
|
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
|
-
|
|
41
|
+
log('SPHERE_SKIP_POSTINSTALL=1 — skipping binary download.');
|
|
28
42
|
process.exit(0);
|
|
29
43
|
}
|
|
30
44
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
73
|
-
.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
+
}
|