@nexart/cli 0.2.3 → 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/README.md +171 -14
- package/dist/__tests__/ai.test.d.ts +24 -0
- package/dist/__tests__/ai.test.d.ts.map +1 -0
- package/dist/__tests__/ai.test.js +703 -0
- package/dist/__tests__/ai.test.js.map +1 -0
- package/dist/index.d.ts +68 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +506 -151
- package/dist/index.js.map +1 -1
- package/package.json +7 -4
package/dist/index.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* @nexart/cli v0.
|
|
3
|
+
* @nexart/cli v0.3.0 — NexArt CLI
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* - nexart run <file>
|
|
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
|
-
*
|
|
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
|
-
|
|
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');
|
|
@@ -402,6 +407,266 @@ async function verifyCommand(snapshotFile, options) {
|
|
|
402
407
|
process.exit(1);
|
|
403
408
|
}
|
|
404
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);
|
|
405
670
|
const apiKeyOption = {
|
|
406
671
|
'api-key': {
|
|
407
672
|
describe: 'API key for authenticated remote rendering',
|
|
@@ -409,155 +674,238 @@ const apiKeyOption = {
|
|
|
409
674
|
default: DEFAULT_API_KEY,
|
|
410
675
|
},
|
|
411
676
|
};
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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',
|
|
677
|
+
const nodeApiKeyOption = {
|
|
678
|
+
'api-key': {
|
|
679
|
+
describe: 'API key for NexArt node (env: NEXART_API_KEY)',
|
|
527
680
|
type: 'string',
|
|
528
|
-
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
681
|
+
default: DEFAULT_API_KEY,
|
|
682
|
+
},
|
|
683
|
+
};
|
|
684
|
+
const nodeEndpointOption = {
|
|
685
|
+
endpoint: {
|
|
686
|
+
describe: 'NexArt node endpoint URL (env: NEXART_NODE_ENDPOINT)',
|
|
533
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
|
+
});
|
|
534
764
|
})
|
|
535
|
-
.
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
+
});
|
|
539
802
|
})
|
|
540
|
-
.
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
+
});
|
|
544
833
|
})
|
|
545
|
-
.
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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(`
|
|
558
905
|
Environment Variables:
|
|
559
906
|
NEXART_RENDERER_ENDPOINT Remote renderer URL (default: http://localhost:5000)
|
|
560
|
-
|
|
907
|
+
NEXART_NODE_ENDPOINT NexArt node API URL (default: https://node.nexart.art)
|
|
908
|
+
NEXART_API_KEY API key for authenticated requests
|
|
561
909
|
|
|
562
910
|
Output Files:
|
|
563
911
|
--out ./out.png creates:
|
|
@@ -568,11 +916,18 @@ Examples:
|
|
|
568
916
|
# Run with remote renderer
|
|
569
917
|
npx @nexart/cli run ./sketch.js --seed 123 --include-code --out ./out.png
|
|
570
918
|
|
|
571
|
-
# Verify output is deterministic
|
|
919
|
+
# Verify Code Mode output is deterministic
|
|
572
920
|
npx @nexart/cli verify ./out.snapshot.json
|
|
573
921
|
|
|
574
|
-
#
|
|
575
|
-
npx @nexart/cli
|
|
922
|
+
# AI: Create a CER bundle
|
|
923
|
+
npx @nexart/cli ai create execution.json
|
|
924
|
+
|
|
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
|
|
576
930
|
`)
|
|
577
|
-
|
|
931
|
+
.parse();
|
|
932
|
+
}
|
|
578
933
|
//# sourceMappingURL=index.js.map
|