@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/README.md +225 -17
- 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 +557 -167
- 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');
|
|
@@ -67,17 +72,49 @@ function validateApiKey(apiKey, endpoint) {
|
|
|
67
72
|
process.exit(1);
|
|
68
73
|
}
|
|
69
74
|
}
|
|
70
|
-
function
|
|
71
|
-
|
|
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
|
|
241
|
-
const snapshotPath =
|
|
242
|
-
const pngAbsPath =
|
|
243
|
-
const snapshotAbsPath =
|
|
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
|
|
301
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
.
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
-
.
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
.
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
-
|
|
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
|
|
528
|
-
|
|
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
|
-
#
|
|
533
|
-
nexart
|
|
919
|
+
# Verify Code Mode output is deterministic
|
|
920
|
+
npx @nexart/cli verify ./out.snapshot.json
|
|
534
921
|
|
|
535
|
-
#
|
|
536
|
-
nexart
|
|
922
|
+
# AI: Create a CER bundle
|
|
923
|
+
npx @nexart/cli ai create execution.json
|
|
537
924
|
|
|
538
|
-
#
|
|
539
|
-
nexart
|
|
540
|
-
|
|
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
|
-
|
|
931
|
+
.parse();
|
|
932
|
+
}
|
|
543
933
|
//# sourceMappingURL=index.js.map
|