@nexart/cli 0.2.3 → 0.3.1

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