@nexart/cli 0.2.2 → 0.3.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/dist/index.js CHANGED
@@ -1,13 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * @nexart/cli v0.2.2 — NexArt CodeMode CLI
3
+ * @nexart/cli v0.3.0 — NexArt CLI
4
4
  *
5
- * Commands:
6
- * - nexart run <file> — Execute via remote renderer and create snapshot
5
+ * Code Mode commands:
6
+ * - nexart run <file> — Execute via remote renderer and create snapshot
7
7
  * - nexart replay <snapshot> — Re-execute from snapshot
8
8
  * - nexart verify <snapshot> — Verify snapshot hash
9
9
  *
10
- * Remote rendering via canonical Node renderer endpoint with API key auth.
10
+ * AI Certification commands:
11
+ * - nexart ai create <file> — Create a CER bundle via node API
12
+ * - nexart ai certify <file> — Certify an AI execution and attest
13
+ * - nexart ai verify <file> — Verify a CER bundle locally
11
14
  */
12
15
  import yargs from 'yargs';
13
16
  import { hideBin } from 'yargs/helpers';
@@ -16,11 +19,13 @@ import * as path from 'path';
16
19
  import * as crypto from 'crypto';
17
20
  import * as http from 'http';
18
21
  import * as https from 'https';
19
- const CLI_VERSION = '0.2.2';
22
+ import { fileURLToPath } from 'url';
23
+ const CLI_VERSION = '0.3.0';
20
24
  const SDK_VERSION = '1.8.4';
21
25
  const PROTOCOL_VERSION = '1.2.0';
22
26
  const DEFAULT_ENDPOINT = process.env.NEXART_RENDERER_ENDPOINT || 'http://localhost:5000';
23
27
  const DEFAULT_API_KEY = process.env.NEXART_API_KEY || '';
28
+ const DEFAULT_NODE_ENDPOINT = process.env.NEXART_NODE_ENDPOINT || 'https://node.nexart.art';
24
29
  function sha256(data) {
25
30
  const buffer = typeof data === 'string' ? Buffer.from(data, 'utf-8') : data;
26
31
  return crypto.createHash('sha256').update(buffer).digest('hex');
@@ -67,17 +72,49 @@ function validateApiKey(apiKey, endpoint) {
67
72
  process.exit(1);
68
73
  }
69
74
  }
70
- function writeFileWithAbsolutePath(filePath, data) {
71
- const absolutePath = path.resolve(process.cwd(), filePath);
75
+ function resolveOutputPath(outPath) {
76
+ if (path.isAbsolute(outPath)) {
77
+ return outPath;
78
+ }
79
+ return path.resolve(process.cwd(), outPath);
80
+ }
81
+ function deriveSnapshotPath(pngPath) {
82
+ if (pngPath.toLowerCase().endsWith('.png')) {
83
+ return pngPath.replace(/\.png$/i, '.snapshot.json');
84
+ }
85
+ return pngPath + '.snapshot.json';
86
+ }
87
+ function writeFileAndVerify(filePath, data) {
88
+ const absolutePath = resolveOutputPath(filePath);
89
+ const dir = path.dirname(absolutePath);
90
+ if (!fs.existsSync(dir)) {
91
+ try {
92
+ fs.mkdirSync(dir, { recursive: true });
93
+ }
94
+ catch (error) {
95
+ console.error(`[nexart] Error: Failed to create directory: ${dir}`);
96
+ console.error(`[nexart] ${error}`);
97
+ process.exit(1);
98
+ }
99
+ }
72
100
  try {
73
101
  fs.writeFileSync(absolutePath, data);
74
- return absolutePath;
75
102
  }
76
103
  catch (error) {
77
104
  console.error(`[nexart] Error: Failed to write file: ${absolutePath}`);
78
105
  console.error(`[nexart] ${error}`);
79
106
  process.exit(1);
80
107
  }
108
+ if (!fs.existsSync(absolutePath)) {
109
+ console.error(`[nexart] Error: File was not created: ${absolutePath}`);
110
+ process.exit(1);
111
+ }
112
+ const stats = fs.statSync(absolutePath);
113
+ if (stats.size === 0) {
114
+ console.error(`[nexart] Error: File is empty (0 bytes): ${absolutePath}`);
115
+ process.exit(1);
116
+ }
117
+ return absolutePath;
81
118
  }
82
119
  /**
83
120
  * Call remote renderer endpoint with optional API key auth
@@ -186,6 +223,7 @@ function createPlaceholderPng() {
186
223
  async function runCommand(file, options) {
187
224
  console.log(`[nexart] Running: ${file}`);
188
225
  console.log(`[nexart] Renderer: ${options.renderer}`);
226
+ console.log(`[nexart] Output: ${options.out}`);
189
227
  if (!fs.existsSync(file)) {
190
228
  console.error(`[nexart] Error: File not found: ${file}`);
191
229
  process.exit(1);
@@ -237,11 +275,11 @@ async function runCommand(file, options) {
237
275
  if (options.includeCode) {
238
276
  snapshot.code = code;
239
277
  }
240
- const outPath = options.out;
241
- const snapshotPath = outPath.replace(/\.png$/i, '.snapshot.json');
242
- const pngAbsPath = writeFileWithAbsolutePath(outPath, pngBytes);
243
- const snapshotAbsPath = writeFileWithAbsolutePath(snapshotPath, JSON.stringify(snapshot, null, 2));
244
- console.log(`[nexart] Wrote PNG: ${pngAbsPath}`);
278
+ const pngPath = resolveOutputPath(options.out);
279
+ const snapshotPath = deriveSnapshotPath(pngPath);
280
+ const pngAbsPath = writeFileAndVerify(pngPath, pngBytes);
281
+ const snapshotAbsPath = writeFileAndVerify(snapshotPath, JSON.stringify(snapshot, null, 2));
282
+ console.log(`[nexart] Wrote PNG: ${pngAbsPath} (${pngBytes.length} bytes)`);
245
283
  console.log(`[nexart] Wrote Snapshot: ${snapshotAbsPath}`);
246
284
  console.log(`[nexart] outputHash: ${outputHash}`);
247
285
  console.log(`[nexart] codeHash: ${codeHash}`);
@@ -297,8 +335,9 @@ async function replayCommand(snapshotFile, options) {
297
335
  pngBytes = createPlaceholderPng();
298
336
  }
299
337
  const outputHash = sha256(pngBytes);
300
- const pngAbsPath = writeFileWithAbsolutePath(options.out, pngBytes);
301
- console.log(`[nexart] Wrote PNG: ${pngAbsPath}`);
338
+ const pngPath = resolveOutputPath(options.out);
339
+ const pngAbsPath = writeFileAndVerify(pngPath, pngBytes);
340
+ console.log(`[nexart] Wrote PNG: ${pngAbsPath} (${pngBytes.length} bytes)`);
302
341
  console.log(`[nexart] outputHash: ${outputHash}`);
303
342
  }
304
343
  async function verifyCommand(snapshotFile, options) {
@@ -368,6 +407,266 @@ async function verifyCommand(snapshotFile, options) {
368
407
  process.exit(1);
369
408
  }
370
409
  }
410
+ // ─── AI Execution Certification ────────────────────────────────────────────
411
+ /**
412
+ * Read all bytes from stdin (non-TTY only).
413
+ */
414
+ export async function readStdinText() {
415
+ return new Promise((resolve, reject) => {
416
+ if (process.stdin.isTTY) {
417
+ reject(new Error('No input file provided and stdin is a terminal.\n' +
418
+ 'Provide a file: nexart ai create <file>\n' +
419
+ 'Or pipe input: cat execution.json | nexart ai create'));
420
+ return;
421
+ }
422
+ const chunks = [];
423
+ process.stdin.on('data', chunk => chunks.push(chunk));
424
+ process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
425
+ process.stdin.on('error', reject);
426
+ });
427
+ }
428
+ /**
429
+ * Read and parse JSON from a file path or stdin.
430
+ */
431
+ export async function readInputJson(file, stdinReader = readStdinText) {
432
+ if (file) {
433
+ if (!fs.existsSync(file)) {
434
+ console.error(`[nexart] Error: File not found: ${file}`);
435
+ process.exit(1);
436
+ }
437
+ try {
438
+ return JSON.parse(fs.readFileSync(file, 'utf-8'));
439
+ }
440
+ catch {
441
+ console.error(`[nexart] Error: Invalid JSON in file: ${file}`);
442
+ process.exit(1);
443
+ }
444
+ }
445
+ let text;
446
+ try {
447
+ text = await stdinReader();
448
+ }
449
+ catch (e) {
450
+ console.error(`[nexart] Error: ${e.message}`);
451
+ process.exit(1);
452
+ }
453
+ try {
454
+ return JSON.parse(text);
455
+ }
456
+ catch {
457
+ console.error('[nexart] Error: Invalid JSON from stdin');
458
+ process.exit(1);
459
+ }
460
+ }
461
+ /**
462
+ * POST to a NexArt node API endpoint using fetch.
463
+ * Accepts an optional fetchFn for dependency injection in tests.
464
+ */
465
+ export async function callNodeApi(endpoint, apiPath, body, apiKey, fetchFn = globalThis.fetch) {
466
+ const url = new URL(apiPath, endpoint).toString();
467
+ const headers = {
468
+ 'Content-Type': 'application/json',
469
+ };
470
+ if (apiKey) {
471
+ headers['Authorization'] = `Bearer ${apiKey}`;
472
+ }
473
+ const response = await fetchFn(url, {
474
+ method: 'POST',
475
+ headers,
476
+ body: JSON.stringify(body),
477
+ });
478
+ let data = null;
479
+ try {
480
+ data = await response.json();
481
+ }
482
+ catch {
483
+ data = null;
484
+ }
485
+ return { status: response.status, data };
486
+ }
487
+ /**
488
+ * Deterministic canonical JSON serialization matching CER v1 spec:
489
+ * - Object keys sorted lexicographically
490
+ * - Nested objects recursively sorted
491
+ * - Arrays maintain insertion order
492
+ * - undefined values omitted
493
+ */
494
+ export function canonicalJson(value) {
495
+ if (value === null || typeof value !== 'object') {
496
+ return JSON.stringify(value);
497
+ }
498
+ if (Array.isArray(value)) {
499
+ return '[' + value.map(canonicalJson).join(',') + ']';
500
+ }
501
+ const obj = value;
502
+ const keys = Object.keys(obj).sort();
503
+ const pairs = keys
504
+ .filter(k => obj[k] !== undefined)
505
+ .map(k => JSON.stringify(k) + ':' + canonicalJson(obj[k]));
506
+ return '{' + pairs.join(',') + '}';
507
+ }
508
+ /**
509
+ * Recompute the certificateHash for a CER v1 bundle.
510
+ * Protected set: { bundleType, version, createdAt, snapshot }
511
+ */
512
+ export function computeBundleHash(bundle) {
513
+ const { bundleType, version, createdAt, snapshot } = bundle;
514
+ const protectedSet = { bundleType, version, createdAt, snapshot };
515
+ return 'sha256:' + sha256(canonicalJson(protectedSet));
516
+ }
517
+ /**
518
+ * Check whether a CER bundle has a valid-looking attestation.
519
+ */
520
+ export function hasAttestation(bundle) {
521
+ return (typeof bundle['attestationId'] === 'string' &&
522
+ bundle['attestationId'].length > 0);
523
+ }
524
+ /**
525
+ * nexart ai create — Read execution input, call POST /v1/cer/ai/create,
526
+ * print CER bundle JSON to stdout, optionally save to --out file.
527
+ */
528
+ export async function aiCreateCommand(file, opts, fetchFn = globalThis.fetch, stdinReader = readStdinText) {
529
+ const input = await readInputJson(file, stdinReader);
530
+ if (isRemoteEndpoint(opts.endpoint) && !opts.apiKey) {
531
+ console.error('[nexart] Error: Missing API key for remote node endpoint.');
532
+ console.error('[nexart] Set NEXART_API_KEY environment variable or pass --api-key.');
533
+ process.exit(1);
534
+ }
535
+ let result;
536
+ try {
537
+ result = await callNodeApi(opts.endpoint, '/v1/cer/ai/create', input, opts.apiKey, fetchFn);
538
+ }
539
+ catch (e) {
540
+ console.error(`[nexart] Error: Failed to connect to node at ${opts.endpoint}: ${e.message}`);
541
+ process.exit(1);
542
+ }
543
+ if (result.status === 401) {
544
+ console.error('[nexart] Error: Authentication failed. Check your API key.');
545
+ process.exit(1);
546
+ }
547
+ if (result.status === 403) {
548
+ console.error('[nexart] Error: Access denied.');
549
+ process.exit(1);
550
+ }
551
+ if (result.status >= 400) {
552
+ const errMsg = (result.data && typeof result.data === 'object' && 'error' in result.data)
553
+ ? result.data.error
554
+ : `Server error: ${result.status}`;
555
+ console.error(`[nexart] Error: ${errMsg}`);
556
+ process.exit(1);
557
+ }
558
+ const bundleJson = JSON.stringify(result.data, null, 2);
559
+ process.stdout.write(bundleJson + '\n');
560
+ if (opts.out) {
561
+ const outPath = writeFileAndVerify(opts.out, bundleJson);
562
+ console.error(`[nexart] Saved to: ${outPath}`);
563
+ }
564
+ }
565
+ /**
566
+ * nexart ai certify — Read execution input, call POST /v1/cer/ai/certify,
567
+ * print certificateHash / verificationUrl / attestationId summary.
568
+ * --json prints full JSON response instead.
569
+ */
570
+ export async function aiCertifyCommand(file, opts, fetchFn = globalThis.fetch, stdinReader = readStdinText) {
571
+ const input = await readInputJson(file, stdinReader);
572
+ if (isRemoteEndpoint(opts.endpoint) && !opts.apiKey) {
573
+ console.error('[nexart] Error: Missing API key for remote node endpoint.');
574
+ console.error('[nexart] Set NEXART_API_KEY environment variable or pass --api-key.');
575
+ process.exit(1);
576
+ }
577
+ let result;
578
+ try {
579
+ result = await callNodeApi(opts.endpoint, '/v1/cer/ai/certify', input, opts.apiKey, fetchFn);
580
+ }
581
+ catch (e) {
582
+ console.error(`[nexart] Error: Failed to connect to node at ${opts.endpoint}: ${e.message}`);
583
+ process.exit(1);
584
+ }
585
+ if (result.status === 401) {
586
+ console.error('[nexart] Error: Authentication failed. Check your API key.');
587
+ process.exit(1);
588
+ }
589
+ if (result.status === 403) {
590
+ console.error('[nexart] Error: Access denied.');
591
+ process.exit(1);
592
+ }
593
+ if (result.status >= 400) {
594
+ const errMsg = (result.data && typeof result.data === 'object' && 'error' in result.data)
595
+ ? result.data.error
596
+ : `Server error: ${result.status}`;
597
+ console.error(`[nexart] Error: ${errMsg}`);
598
+ process.exit(1);
599
+ }
600
+ const responseData = result.data;
601
+ const fullJson = JSON.stringify(responseData, null, 2);
602
+ if (opts.json) {
603
+ process.stdout.write(fullJson + '\n');
604
+ }
605
+ else {
606
+ const certHash = typeof responseData.certificateHash === 'string'
607
+ ? responseData.certificateHash : '(not provided)';
608
+ const verifyUrl = typeof responseData.verificationUrl === 'string'
609
+ ? responseData.verificationUrl : '(not provided)';
610
+ const attestId = typeof responseData.attestationId === 'string'
611
+ ? responseData.attestationId : '(not provided)';
612
+ console.log('CER certified');
613
+ console.log(`certificateHash: ${certHash}`);
614
+ console.log(`verificationUrl: ${verifyUrl}`);
615
+ console.log(`attestationId: ${attestId}`);
616
+ }
617
+ if (opts.out) {
618
+ const outPath = writeFileAndVerify(opts.out, fullJson);
619
+ console.error(`[nexart] Saved to: ${outPath}`);
620
+ }
621
+ }
622
+ /**
623
+ * nexart ai verify — Read a CER bundle JSON from file or stdin,
624
+ * verify integrity locally, report PASS/FAIL.
625
+ * --json prints machine-readable output.
626
+ */
627
+ export async function aiVerifyCommand(file, opts, stdinReader = readStdinText) {
628
+ const raw = await readInputJson(file, stdinReader);
629
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
630
+ console.error('[nexart] Error: Input is not a valid CER bundle object');
631
+ process.exit(1);
632
+ }
633
+ const bundle = raw;
634
+ if (bundle['bundleType'] !== 'cer.ai.execution.v1') {
635
+ console.error(`[nexart] Error: Unsupported bundleType: ${bundle['bundleType']}`);
636
+ console.error('[nexart] Expected: cer.ai.execution.v1');
637
+ process.exit(1);
638
+ }
639
+ const computedHash = computeBundleHash(bundle);
640
+ const storedHash = bundle['certificateHash'];
641
+ const integrityPass = computedHash === storedHash;
642
+ const attestationPresent = hasAttestation(bundle);
643
+ const overallPass = integrityPass;
644
+ const result = overallPass ? 'PASS' : 'FAIL';
645
+ if (opts.json) {
646
+ const output = {
647
+ result,
648
+ bundleIntegrity: integrityPass ? 'PASS' : 'FAIL',
649
+ nodeAttestation: attestationPresent ? 'PRESENT' : 'ABSENT',
650
+ ...(integrityPass ? {} : {
651
+ expected: storedHash,
652
+ computed: computedHash,
653
+ }),
654
+ };
655
+ process.stdout.write(JSON.stringify(output, null, 2) + '\n');
656
+ }
657
+ else {
658
+ console.log(`Verification result: ${result}`);
659
+ console.log(`bundleIntegrity: ${integrityPass ? 'PASS' : 'FAIL'}`);
660
+ console.log(`nodeAttestation: ${attestationPresent ? 'PRESENT' : 'ABSENT'}`);
661
+ if (!integrityPass) {
662
+ console.error(`[nexart] Expected hash: ${storedHash}`);
663
+ console.error(`[nexart] Computed hash: ${computedHash}`);
664
+ }
665
+ }
666
+ process.exit(overallPass ? 0 : 1);
667
+ }
668
+ // ─── CLI Setup ─────────────────────────────────────────────────────────────
669
+ const _isMainModule = process.argv[1] === fileURLToPath(import.meta.url);
371
670
  const apiKeyOption = {
372
671
  'api-key': {
373
672
  describe: 'API key for authenticated remote rendering',
@@ -375,169 +674,260 @@ const apiKeyOption = {
375
674
  default: DEFAULT_API_KEY,
376
675
  },
377
676
  };
378
- yargs(hideBin(process.argv))
379
- .command('run <file>', 'Execute a Code Mode sketch and create snapshot', (yargs) => {
380
- return yargs
381
- .positional('file', {
382
- describe: 'Path to the sketch file',
383
- type: 'string',
384
- demandOption: true,
385
- })
386
- .option('out', {
387
- alias: 'o',
388
- describe: 'Output PNG path',
389
- type: 'string',
390
- default: 'out.png',
391
- })
392
- .option('seed', {
393
- alias: 's',
394
- describe: 'PRNG seed',
395
- type: 'number',
396
- default: Math.floor(Math.random() * 2147483647),
397
- })
398
- .option('vars', {
399
- alias: 'v',
400
- describe: 'Comma-separated VAR values (10 values, 0-100)',
401
- type: 'string',
402
- default: '0,0,0,0,0,0,0,0,0,0',
403
- })
404
- .option('width', {
405
- alias: 'w',
406
- describe: 'Canvas width',
407
- type: 'number',
408
- default: 1950,
409
- })
410
- .option('height', {
411
- describe: 'Canvas height',
412
- type: 'number',
413
- default: 2400,
414
- })
415
- .option('renderer', {
416
- describe: 'Renderer mode: remote (default, real PNG) or local (NOT implemented, placeholder only)',
417
- choices: ['local', 'remote'],
418
- default: 'remote',
419
- })
420
- .option('endpoint', {
421
- describe: 'Remote renderer endpoint URL',
422
- type: 'string',
423
- default: DEFAULT_ENDPOINT,
424
- })
425
- .options(apiKeyOption)
426
- .option('runtime-hash', {
427
- describe: 'Override runtime hash (advanced)',
428
- type: 'string',
429
- })
430
- .option('include-code', {
431
- describe: 'Embed code in snapshot for standalone replay/verify',
432
- type: 'boolean',
433
- default: false,
434
- });
435
- }, async (argv) => {
436
- await runCommand(argv.file, {
437
- out: argv.out,
438
- seed: argv.seed,
439
- vars: argv.vars,
440
- width: argv.width,
441
- height: argv.height,
442
- renderer: argv.renderer,
443
- endpoint: argv.endpoint,
444
- apiKey: argv['api-key'],
445
- runtimeHash: argv['runtime-hash'],
446
- includeCode: argv['include-code'],
447
- });
448
- })
449
- .command('replay <snapshot>', 'Re-execute from a snapshot file', (yargs) => {
450
- return yargs
451
- .positional('snapshot', {
452
- describe: 'Path to snapshot JSON file',
453
- type: 'string',
454
- demandOption: true,
455
- })
456
- .option('out', {
457
- alias: 'o',
458
- describe: 'Output PNG path',
459
- type: 'string',
460
- default: 'replay.png',
461
- })
462
- .option('code', {
463
- alias: 'c',
464
- describe: 'Path to code file (if not embedded in snapshot)',
465
- type: 'string',
466
- })
467
- .option('renderer', {
468
- describe: 'Renderer mode',
469
- choices: ['local', 'remote'],
470
- default: 'remote',
471
- })
472
- .option('endpoint', {
473
- describe: 'Remote renderer endpoint URL',
474
- type: 'string',
475
- default: DEFAULT_ENDPOINT,
476
- })
477
- .options(apiKeyOption);
478
- }, async (argv) => {
479
- await replayCommand(argv.snapshot, {
480
- out: argv.out,
481
- code: argv.code,
482
- renderer: argv.renderer,
483
- endpoint: argv.endpoint,
484
- apiKey: argv['api-key'],
485
- });
486
- })
487
- .command('verify <snapshot>', 'Verify a snapshot produces the expected output', (yargs) => {
488
- return yargs
489
- .positional('snapshot', {
490
- describe: 'Path to snapshot JSON file',
677
+ const nodeApiKeyOption = {
678
+ 'api-key': {
679
+ describe: 'API key for NexArt node (env: NEXART_API_KEY)',
491
680
  type: 'string',
492
- demandOption: true,
493
- })
494
- .option('code', {
495
- alias: 'c',
496
- describe: 'Path to code file (if not embedded in snapshot)',
681
+ default: DEFAULT_API_KEY,
682
+ },
683
+ };
684
+ const nodeEndpointOption = {
685
+ endpoint: {
686
+ describe: 'NexArt node endpoint URL (env: NEXART_NODE_ENDPOINT)',
497
687
  type: 'string',
688
+ default: DEFAULT_NODE_ENDPOINT,
689
+ },
690
+ };
691
+ if (_isMainModule) {
692
+ yargs(hideBin(process.argv))
693
+ .command('run <file>', 'Execute a Code Mode sketch and create snapshot', (yargs) => {
694
+ return yargs
695
+ .positional('file', {
696
+ describe: 'Path to the sketch file',
697
+ type: 'string',
698
+ demandOption: true,
699
+ })
700
+ .option('out', {
701
+ alias: 'o',
702
+ describe: 'Output PNG file path (writes to current directory)',
703
+ type: 'string',
704
+ default: 'out.png',
705
+ })
706
+ .option('seed', {
707
+ alias: 's',
708
+ describe: 'PRNG seed',
709
+ type: 'number',
710
+ default: Math.floor(Math.random() * 2147483647),
711
+ })
712
+ .option('vars', {
713
+ alias: 'v',
714
+ describe: 'Comma-separated VAR values (10 values, 0-100)',
715
+ type: 'string',
716
+ default: '0,0,0,0,0,0,0,0,0,0',
717
+ })
718
+ .option('width', {
719
+ alias: 'w',
720
+ describe: 'Canvas width (use default 1950 for canonical)',
721
+ type: 'number',
722
+ default: 1950,
723
+ })
724
+ .option('height', {
725
+ describe: 'Canvas height (use default 2400 for canonical)',
726
+ type: 'number',
727
+ default: 2400,
728
+ })
729
+ .option('renderer', {
730
+ describe: 'Renderer mode: remote (default) or local (placeholder)',
731
+ choices: ['local', 'remote'],
732
+ default: 'remote',
733
+ })
734
+ .option('endpoint', {
735
+ describe: 'Remote renderer endpoint URL',
736
+ type: 'string',
737
+ default: DEFAULT_ENDPOINT,
738
+ })
739
+ .options(apiKeyOption)
740
+ .option('runtime-hash', {
741
+ describe: 'Override runtime hash (advanced)',
742
+ type: 'string',
743
+ })
744
+ .option('include-code', {
745
+ describe: 'Embed code in snapshot for standalone replay/verify',
746
+ type: 'boolean',
747
+ default: false,
748
+ })
749
+ .example('$0 run sketch.js --seed 123 --out ./out.png', 'Run sketch and write to ./out.png')
750
+ .example('$0 run sketch.js --seed 123 --include-code', 'Run and embed code in snapshot');
751
+ }, async (argv) => {
752
+ await runCommand(argv.file, {
753
+ out: argv.out,
754
+ seed: argv.seed,
755
+ vars: argv.vars,
756
+ width: argv.width,
757
+ height: argv.height,
758
+ renderer: argv.renderer,
759
+ endpoint: argv.endpoint,
760
+ apiKey: argv['api-key'],
761
+ runtimeHash: argv['runtime-hash'],
762
+ includeCode: argv['include-code'],
763
+ });
498
764
  })
499
- .option('renderer', {
500
- describe: 'Renderer mode',
501
- choices: ['local', 'remote'],
502
- default: 'remote',
765
+ .command('replay <snapshot>', 'Re-execute from a snapshot file', (yargs) => {
766
+ return yargs
767
+ .positional('snapshot', {
768
+ describe: 'Path to snapshot JSON file',
769
+ type: 'string',
770
+ demandOption: true,
771
+ })
772
+ .option('out', {
773
+ alias: 'o',
774
+ describe: 'Output PNG file path',
775
+ type: 'string',
776
+ default: 'replay.png',
777
+ })
778
+ .option('code', {
779
+ alias: 'c',
780
+ describe: 'Path to code file (if not embedded in snapshot)',
781
+ type: 'string',
782
+ })
783
+ .option('renderer', {
784
+ describe: 'Renderer mode',
785
+ choices: ['local', 'remote'],
786
+ default: 'remote',
787
+ })
788
+ .option('endpoint', {
789
+ describe: 'Remote renderer endpoint URL',
790
+ type: 'string',
791
+ default: DEFAULT_ENDPOINT,
792
+ })
793
+ .options(apiKeyOption);
794
+ }, async (argv) => {
795
+ await replayCommand(argv.snapshot, {
796
+ out: argv.out,
797
+ code: argv.code,
798
+ renderer: argv.renderer,
799
+ endpoint: argv.endpoint,
800
+ apiKey: argv['api-key'],
801
+ });
503
802
  })
504
- .option('endpoint', {
505
- describe: 'Remote renderer endpoint URL',
506
- type: 'string',
507
- default: DEFAULT_ENDPOINT,
803
+ .command('verify <snapshot>', 'Verify a snapshot produces the expected output', (yargs) => {
804
+ return yargs
805
+ .positional('snapshot', {
806
+ describe: 'Path to snapshot JSON file',
807
+ type: 'string',
808
+ demandOption: true,
809
+ })
810
+ .option('code', {
811
+ alias: 'c',
812
+ describe: 'Path to code file (if not embedded in snapshot)',
813
+ type: 'string',
814
+ })
815
+ .option('renderer', {
816
+ describe: 'Renderer mode',
817
+ choices: ['local', 'remote'],
818
+ default: 'remote',
819
+ })
820
+ .option('endpoint', {
821
+ describe: 'Remote renderer endpoint URL',
822
+ type: 'string',
823
+ default: DEFAULT_ENDPOINT,
824
+ })
825
+ .options(apiKeyOption);
826
+ }, async (argv) => {
827
+ await verifyCommand(argv.snapshot, {
828
+ code: argv.code,
829
+ renderer: argv.renderer,
830
+ endpoint: argv.endpoint,
831
+ apiKey: argv['api-key'],
832
+ });
508
833
  })
509
- .options(apiKeyOption);
510
- }, async (argv) => {
511
- await verifyCommand(argv.snapshot, {
512
- code: argv.code,
513
- renderer: argv.renderer,
514
- endpoint: argv.endpoint,
515
- apiKey: argv['api-key'],
516
- });
517
- })
518
- .demandCommand(1, 'You must provide a command')
519
- .help()
520
- .version(CLI_VERSION)
521
- .epilog(`
834
+ .command('ai', 'AI execution certification commands', (yargs) => {
835
+ return yargs
836
+ .command('create [file]', 'Create a Certified Execution Record from an AI execution input', (yargs) => {
837
+ return yargs
838
+ .positional('file', {
839
+ describe: 'Path to execution input JSON (omit to read from stdin)',
840
+ type: 'string',
841
+ })
842
+ .option('out', {
843
+ alias: 'o',
844
+ describe: 'Save returned CER bundle to this file',
845
+ type: 'string',
846
+ })
847
+ .options(nodeEndpointOption)
848
+ .options(nodeApiKeyOption)
849
+ .example('$0 ai create execution.json', 'Create CER from file')
850
+ .example('cat execution.json | $0 ai create', 'Create CER from stdin')
851
+ .example('$0 ai create execution.json --out cer.json', 'Save CER to file');
852
+ }, async (argv) => {
853
+ await aiCreateCommand(argv.file, { out: argv.out, endpoint: argv.endpoint, apiKey: argv['api-key'] });
854
+ })
855
+ .command('certify [file]', 'Certify an AI execution and receive attestation from the node', (yargs) => {
856
+ return yargs
857
+ .positional('file', {
858
+ describe: 'Path to execution input JSON (omit to read from stdin)',
859
+ type: 'string',
860
+ })
861
+ .option('out', {
862
+ alias: 'o',
863
+ describe: 'Save full response JSON to this file',
864
+ type: 'string',
865
+ })
866
+ .option('json', {
867
+ alias: 'j',
868
+ describe: 'Print full JSON response instead of summary',
869
+ type: 'boolean',
870
+ default: false,
871
+ })
872
+ .options(nodeEndpointOption)
873
+ .options(nodeApiKeyOption)
874
+ .example('$0 ai certify execution.json', 'Certify and print summary')
875
+ .example('$0 ai certify execution.json --json', 'Print full JSON response')
876
+ .example('$0 ai certify execution.json --out response.json', 'Save response');
877
+ }, async (argv) => {
878
+ await aiCertifyCommand(argv.file, { out: argv.out, json: argv.json, endpoint: argv.endpoint, apiKey: argv['api-key'] });
879
+ })
880
+ .command('verify [file]', 'Verify a CER bundle locally (no network required)', (yargs) => {
881
+ return yargs
882
+ .positional('file', {
883
+ describe: 'Path to CER bundle JSON (omit to read from stdin)',
884
+ type: 'string',
885
+ })
886
+ .option('json', {
887
+ alias: 'j',
888
+ describe: 'Print machine-readable JSON result',
889
+ type: 'boolean',
890
+ default: false,
891
+ })
892
+ .example('$0 ai verify cer.json', 'Verify a CER bundle')
893
+ .example('cat cer.json | $0 ai verify', 'Verify from stdin')
894
+ .example('$0 ai verify cer.json --json', 'Machine-readable output');
895
+ }, async (argv) => {
896
+ await aiVerifyCommand(argv.file, { json: argv.json });
897
+ })
898
+ .demandCommand(1, 'You must provide a subcommand: create, certify, or verify')
899
+ .help();
900
+ }, () => { })
901
+ .demandCommand(1, 'You must provide a command')
902
+ .help()
903
+ .version(CLI_VERSION)
904
+ .epilog(`
522
905
  Environment Variables:
523
906
  NEXART_RENDERER_ENDPOINT Remote renderer URL (default: http://localhost:5000)
524
- NEXART_API_KEY API key for authenticated rendering
907
+ NEXART_NODE_ENDPOINT NexArt node API URL (default: https://node.nexart.art)
908
+ NEXART_API_KEY API key for authenticated requests
909
+
910
+ Output Files:
911
+ --out ./out.png creates:
912
+ ./out.png - The rendered PNG image
913
+ ./out.snapshot.json - Snapshot for replay/verify
525
914
 
526
915
  Examples:
527
- # Run with API key (remote)
528
- export NEXART_RENDERER_ENDPOINT=https://nexart-canonical-renderer-production.up.railway.app
529
- export NEXART_API_KEY=nx_live_...
530
- nexart run sketch.js --seed 12345 --include-code
916
+ # Run with remote renderer
917
+ npx @nexart/cli run ./sketch.js --seed 123 --include-code --out ./out.png
531
918
 
532
- # Or pass API key directly
533
- nexart run sketch.js --seed 12345 --api-key nx_live_...
919
+ # Verify Code Mode output is deterministic
920
+ npx @nexart/cli verify ./out.snapshot.json
534
921
 
535
- # Local mode (placeholder only, no auth needed)
536
- nexart run sketch.js --renderer local
922
+ # AI: Create a CER bundle
923
+ npx @nexart/cli ai create execution.json
537
924
 
538
- # Verify and replay
539
- nexart verify out.snapshot.json
540
- nexart replay out.snapshot.json --out replay.png
925
+ # AI: Certify an execution
926
+ npx @nexart/cli ai certify execution.json
927
+
928
+ # AI: Verify a CER bundle locally
929
+ npx @nexart/cli ai verify cer.json
541
930
  `)
542
- .parse();
931
+ .parse();
932
+ }
543
933
  //# sourceMappingURL=index.js.map