sphere-cli 0.1.38 → 0.1.39

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/README.md CHANGED
@@ -49,6 +49,9 @@ sh install.sh --uninstall
49
49
  ## Quick start
50
50
 
51
51
  ```sh
52
+ # Try the built-in demo (no data needed)
53
+ sphere demo
54
+
52
55
  # Activate your license (once)
53
56
  sphere license activate sphere_xxxxxxxxxxxxxxxxxxxx
54
57
 
@@ -64,8 +67,75 @@ sphere certify real.csv synth.csv -o report.html
64
67
 
65
68
  ---
66
69
 
70
+ ## First run
71
+
72
+ On the very first invocation the CLI cold-loads its bundled Python libraries (pandas, pyarrow, anonymeter, sklearn) from disk. On Apple Silicon this typically takes **15–25 seconds** and is shown in the progress bar as each library finishes:
73
+
74
+ ```
75
+ Generating synthetic data from nhanes_sample.csv …
76
+ [░░░░░░░░░░░░░░░░░] 0.0% loading pandas . .
77
+ [█░░░░░░░░░░░░░░░░] 3.0% ✓ pandas (12.4 s)
78
+ [██░░░░░░░░░░░░░░░] 6.0% ✓ pyarrow (3.1 s)
79
+ [███░░░░░░░░░░░░░░] 9.0% ✓ sphere core (1.8 s)
80
+
81
+ ✓ synth.csv 4,899 rows × 18 cols (load 17.4 s + run 1.8 s) seed 3721018536
82
+ ```
83
+
84
+ Subsequent calls in the same session skip loading entirely. The timing line always shows **load** (library startup) and **run** (actual SPHERE computation) separately so you can see which part is slow.
85
+
86
+ > Exact times vary by machine, OS page cache state, and whether the binary has been run recently.
87
+
88
+ ---
89
+
67
90
  ## Commands
68
91
 
92
+ ### `sphere demo`
93
+
94
+ Run SPHERE end-to-end on the built-in NHANES sample dataset (4,899 rows × 18 columns, mix of continuous and categorical variables). No data or license required — good for testing an installation.
95
+
96
+ ```sh
97
+ sphere demo
98
+ ```
99
+
100
+ ```
101
+ SPHERE demo — built-in NHANES dataset (4,899 rows × 18 cols, continuous + categorical)
102
+ ────────────────────────────────────────────────────
103
+
104
+ Generating synthetic data from nhanes_sample.csv …
105
+ [░░░░░░░░░░░░░░░░░] 0.0% loading pandas . .
106
+ [█░░░░░░░░░░░░░░░░] 3.0% ✓ pandas (12.4 s)
107
+ [██░░░░░░░░░░░░░░░] 6.0% ✓ pyarrow (3.1 s)
108
+ [███░░░░░░░░░░░░░░] 9.0% ✓ sphere core (1.8 s)
109
+ [████████████████░] 85.0% writing output
110
+ ✓ /tmp/synth.csv 4,899 rows × 18 cols (load 17.4 s + run 1.8 s) seed 3721018536
111
+
112
+ Evaluating nhanes_sample.csv vs synth.csv …
113
+ [████░░░░░░░░░░░░░] 16.0% loading anonymeter . .
114
+ [████░░░░░░░░░░░░░] 17.0% ✓ anonymeter (3.2 s)
115
+ [█████░░░░░░░░░░░░] 18.0% ✓ sklearn (0.8 s)
116
+ [█████████████████] 89.0% inference 9/9
117
+ ✓ Evaluation complete (load 4.0 s + run 14.2 s)
118
+
119
+ Fidelity
120
+ ────────────────────────────────────
121
+ Mean 100.0 ████████████████████
122
+ Variance 99.7 ████████████████████
123
+ Correlation 95.1 ███████████████████░
124
+ KS 96.8 ███████████████████░
125
+ ────────────────────────────────────
126
+ Composite 97.9 ████████████████████
127
+
128
+ Privacy
129
+ ────────────────────────────────────
130
+ Singling Out 100.0 ████████████████████
131
+ Linkability 97.5 ███████████████████░
132
+ Inference 96.8 ███████████████████░
133
+ ────────────────────────────────────
134
+ Composite 98.1 ████████████████████
135
+ ```
136
+
137
+ ---
138
+
69
139
  ### `sphere license`
70
140
 
71
141
  Activate and manage your SPHERE license. A valid license is required to use `generate`, `evaluate`, and `certify`.
@@ -90,7 +160,7 @@ sphere generate <real.csv> [options]
90
160
  Options:
91
161
  -o, --output PATH Output CSV path (default: <input>_sphere.csv)
92
162
  -n, --rows INT Number of synthetic rows (default: same as input)
93
- -k INT SPHERE rotations (default: 2)
163
+ -k INT Synthesis depth (default: 2)
94
164
  --seed INT Random seed for reproducibility
95
165
  --mix-prob FLOAT Mixture probability 0–1 (default: 0.75)
96
166
  --json Machine-readable JSON output
@@ -98,6 +168,8 @@ Options:
98
168
 
99
169
  A `.sphere.json` provenance file is written alongside every output CSV and is automatically read by `sphere certify`.
100
170
 
171
+ ---
172
+
101
173
  ### `sphere evaluate`
102
174
 
103
175
  ```
@@ -105,9 +177,14 @@ sphere evaluate <real.csv> <synth.csv> [options]
105
177
 
106
178
  Options:
107
179
  --skip-privacy Skip privacy metrics (faster)
180
+ --seed INT Fix the random seed for reproducible attack results
108
181
  --json Machine-readable JSON output
109
182
  ```
110
183
 
184
+ Reports four fidelity metrics (mean, variance, correlation, KS) and three privacy metrics (singling-out, linkability, inference), each scored 0–100. Scores are normalised against a column-shuffled baseline so 100 = no measurable privacy leakage relative to a random permutation of the data.
185
+
186
+ ---
187
+
111
188
  ### `sphere certify`
112
189
 
113
190
  ```
@@ -118,7 +195,7 @@ Options:
118
195
  --json Machine-readable JSON output
119
196
  ```
120
197
 
121
- Generation parameters (`k`, `seed`, `theta`, etc.) are loaded automatically from the `.sphere.json` sidecar. Pass flags explicitly to override.
198
+ Produces a self-contained HTML certificate with fidelity and privacy scores, dataset metadata, and generation provenance. Generation parameters (`k`, `seed`, `theta`, etc.) are loaded automatically from the `.sphere.json` sidecar; pass flags explicitly to override.
122
199
 
123
200
  ---
124
201
 
@@ -127,8 +204,9 @@ Generation parameters (`k`, `seed`, `theta`, etc.) are loaded automatically from
127
204
  Every command supports `--json` for pipeline integration:
128
205
 
129
206
  ```sh
130
- sphere generate real.csv -o synth.csv --json | jq .fidelity
207
+ sphere generate real.csv -o synth.csv --json | jq .seed
131
208
  sphere evaluate real.csv synth.csv --json > metrics.json
209
+ sphere evaluate real.csv synth.csv --json | jq '.privacy.composite'
132
210
  ```
133
211
 
134
212
  ---
@@ -140,7 +218,7 @@ sphere evaluate real.csv synth.csv --json > metrics.json
140
218
  | `SPHERE_LICENSE_REQUIRED` | Set to `false` to bypass license checks (research / unlocked builds) |
141
219
  | `SPHERE_WORKER_URL` | Override the license validation endpoint |
142
220
  | `SPHERE_PREFIX` | Override install prefix |
143
- | `SPHERE_VERSION` | Pin a release tag, e.g. `v0.1.0` |
221
+ | `SPHERE_VERSION` | Pin a release tag, e.g. `v0.1.38` |
144
222
  | `SPHERE_BUNDLE_URL` | Full URL to a `sphere-cli-*.tar.gz` (skip auto-detect) |
145
223
  | `SPHERE_GITHUB_REPO` | Override GitHub repo for downloads |
146
224
 
package/bin/sphere.js CHANGED
@@ -1,24 +1,444 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- const { spawnSync } = require('child_process');
5
- const path = require('path');
6
- const fs = require('fs');
4
+ /**
5
+ * sphere — CLI entry point.
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)
16
+ */
7
17
 
8
- const binaryPath = path.join(__dirname, '..', 'native', 'sphere-cli', 'sphere');
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');
9
23
 
10
- if (!fs.existsSync(binaryPath)) {
11
- console.error(
12
- 'SPHERE CLI binary not found.\n' +
13
- 'This usually means the postinstall step failed or was skipped.\n' +
14
- 'Try reinstalling: npm install -g sphere-cli'
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 ───────────────────────────────────────────────────────────────────
29
+
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
+ }
53
+
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',
15
69
  );
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');
16
382
  process.exit(1);
17
383
  }
18
384
 
19
- const result = spawnSync(binaryPath, process.argv.slice(2), {
20
- stdio: 'inherit',
21
- env: process.env,
22
- });
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
+ }
23
418
 
24
- process.exit(result.status ?? 1);
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`);
438
+ process.exit(1);
439
+ }
440
+
441
+ main().catch((err) => {
442
+ process.stderr.write(`\nError: ${err.message || err}\n`);
443
+ process.exit(1);
444
+ });