@attest-it/cli 0.1.0 → 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.cjs CHANGED
@@ -3,15 +3,19 @@
3
3
  var commander = require('commander');
4
4
  var fs = require('fs');
5
5
  var path = require('path');
6
- var pc = require('picocolors');
6
+ var chromaterm = require('chromaterm');
7
7
  var prompts = require('@inquirer/prompts');
8
8
  var core = require('@attest-it/core');
9
9
  var child_process = require('child_process');
10
10
  var os = require('os');
11
11
  var shellQuote = require('shell-quote');
12
+ var React7 = require('react');
13
+ var ink = require('ink');
14
+ var jsxRuntime = require('react/jsx-runtime');
15
+ var promises = require('fs/promises');
16
+ var url = require('url');
12
17
 
13
- function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
14
-
18
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
15
19
  function _interopNamespace(e) {
16
20
  if (e && e.__esModule) return e;
17
21
  var n = Object.create(null);
@@ -32,11 +36,33 @@ function _interopNamespace(e) {
32
36
 
33
37
  var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
34
38
  var path__namespace = /*#__PURE__*/_interopNamespace(path);
35
- var pc__default = /*#__PURE__*/_interopDefault(pc);
36
39
  var os__namespace = /*#__PURE__*/_interopNamespace(os);
40
+ var React7__namespace = /*#__PURE__*/_interopNamespace(React7);
37
41
 
38
42
  // src/index.ts
39
43
  var globalOptions = {};
44
+ var theme;
45
+ async function initTheme() {
46
+ theme = await chromaterm.detectTheme();
47
+ }
48
+ function getTheme() {
49
+ if (!theme) {
50
+ const noopFn = (str) => str;
51
+ const chainable = () => noopFn;
52
+ theme = {
53
+ red: Object.assign(noopFn, { bold: chainable, dim: chainable }),
54
+ green: Object.assign(noopFn, { bold: chainable, dim: chainable }),
55
+ yellow: Object.assign(noopFn, { bold: chainable, dim: chainable }),
56
+ blue: Object.assign(noopFn, { bold: chainable, dim: chainable }),
57
+ success: noopFn,
58
+ error: noopFn,
59
+ warning: noopFn,
60
+ info: noopFn,
61
+ muted: noopFn
62
+ };
63
+ }
64
+ return theme;
65
+ }
40
66
  function setOutputOptions(options) {
41
67
  globalOptions = options;
42
68
  }
@@ -47,22 +73,22 @@ function log(message) {
47
73
  }
48
74
  function verbose(message) {
49
75
  if (globalOptions.verbose && !globalOptions.quiet) {
50
- console.log(pc__default.default.dim(message));
76
+ console.log(getTheme().muted(message));
51
77
  }
52
78
  }
53
79
  function success(message) {
54
- log(pc__default.default.green("\u2713 " + message));
80
+ log(getTheme().success("\u2713 " + message));
55
81
  }
56
82
  function error(message) {
57
- console.error(pc__default.default.red("\u2717 " + message));
83
+ console.error(getTheme().error("\u2717 " + message));
58
84
  }
59
85
  function warn(message) {
60
86
  if (!globalOptions.quiet) {
61
- console.warn(pc__default.default.yellow("\u26A0 " + message));
87
+ console.warn(getTheme().warning("\u26A0 " + message));
62
88
  }
63
89
  }
64
90
  function info(message) {
65
- log(pc__default.default.blue("\u2139 " + message));
91
+ log(getTheme().info("\u2139 " + message));
66
92
  }
67
93
  function formatTable(rows) {
68
94
  const headers = ["Suite", "Status", "Fingerprint", "Age"];
@@ -91,17 +117,18 @@ function formatTable(rows) {
91
117
  return lines.join("\n");
92
118
  }
93
119
  function colorizeStatus(status) {
120
+ const t = getTheme();
94
121
  switch (status) {
95
122
  case "VALID":
96
- return pc__default.default.green(status);
123
+ return t.green(status);
97
124
  case "NEEDS_ATTESTATION":
98
125
  case "FINGERPRINT_CHANGED":
99
- return pc__default.default.yellow(status);
126
+ return t.yellow(status);
100
127
  case "EXPIRED":
101
128
  case "INVALIDATED_BY_PARENT":
102
- return pc__default.default.red(status);
129
+ return t.red(status);
103
130
  case "SIGNATURE_INVALID":
104
- return pc__default.default.red(pc__default.default.bold(status));
131
+ return t.red.bold()(status);
105
132
  default:
106
133
  return status;
107
134
  }
@@ -122,12 +149,14 @@ var ExitCode = {
122
149
  SUCCESS: 0,
123
150
  /** Tests failed or attestation invalid */
124
151
  FAILURE: 1,
152
+ /** Nothing needed attestation */
153
+ NO_WORK: 2,
125
154
  /** Configuration or validation error */
126
- CONFIG_ERROR: 2,
155
+ CONFIG_ERROR: 3,
127
156
  /** User cancelled the operation */
128
- CANCELLED: 3,
157
+ CANCELLED: 4,
129
158
  /** Missing required key file */
130
- MISSING_KEY: 4
159
+ MISSING_KEY: 5
131
160
  };
132
161
 
133
162
  // src/commands/init.ts
@@ -321,92 +350,797 @@ function formatAge(result) {
321
350
  }
322
351
  return "-";
323
352
  }
324
- var runCommand = new commander.Command("run").description("Execute tests and create attestation").option("-s, --suite <name>", "Run specific suite (required unless --all)").option("-a, --all", "Run all suites needing attestation").option("--no-attest", "Run tests without creating attestation").option("-y, --yes", "Skip confirmation prompt").action(async (options) => {
325
- await runTests(options);
326
- });
327
- async function runTests(options) {
328
- try {
329
- if (!options.suite && !options.all) {
330
- error("Either --suite or --all is required");
331
- process.exit(ExitCode.CONFIG_ERROR);
353
+ function Header({ pendingCount }) {
354
+ const message = `${pendingCount.toString()} suite${pendingCount === 1 ? "" : "s"} need${pendingCount === 1 ? "s" : ""} attestation`;
355
+ return /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { borderStyle: "single", paddingX: 1, children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: message }) });
356
+ }
357
+ function StatusBadge({ status }) {
358
+ const statusConfig = getStatusConfig(status);
359
+ if (statusConfig.bold) {
360
+ return /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: statusConfig.color, bold: true, children: statusConfig.text });
361
+ }
362
+ return /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: statusConfig.color, children: statusConfig.text });
363
+ }
364
+ function getStatusConfig(status) {
365
+ switch (status) {
366
+ case "VALID":
367
+ return { text: "\u2713 VALID", color: "green" };
368
+ case "NEEDS_ATTESTATION":
369
+ return { text: "MISSING", color: "yellow" };
370
+ case "FINGERPRINT_CHANGED":
371
+ return { text: "CHANGED", color: "yellow" };
372
+ case "EXPIRED":
373
+ return { text: "STALE", color: "red" };
374
+ case "SIGNATURE_INVALID":
375
+ return { text: "INVALID", color: "red", bold: true };
376
+ case "INVALIDATED_BY_PARENT":
377
+ return { text: "INVALIDATED", color: "red" };
378
+ default: {
379
+ const _exhaustive = status;
380
+ return { text: String(_exhaustive), color: "yellow" };
332
381
  }
333
- const config = await core.loadConfig();
334
- const suitesToRun = options.all ? Object.keys(config.suites) : options.suite ? [options.suite] : [];
335
- if (options.suite && !config.suites[options.suite]) {
336
- error(`Suite "${options.suite}" not found in config`);
337
- process.exit(ExitCode.CONFIG_ERROR);
382
+ }
383
+ }
384
+ function SuiteTable({
385
+ suites,
386
+ selectable = false,
387
+ selected = /* @__PURE__ */ new Set()
388
+ }) {
389
+ const columnWidths = calculateColumnWidths(suites);
390
+ return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
391
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { children: [
392
+ selectable && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: " ".repeat(4) }),
393
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: padEnd("Status", columnWidths.status) }),
394
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: " " }),
395
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: padEnd("Suite", columnWidths.suite) }),
396
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: " " }),
397
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Reason" })
398
+ ] }),
399
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "gray", children: "\u2500".repeat(
400
+ (selectable ? 4 : 0) + columnWidths.status + columnWidths.suite + columnWidths.reason
401
+ ) }) }),
402
+ suites.map((suite) => /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { children: [
403
+ selectable && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: selected.has(suite.name) ? "[\u2713] " : "[ ] " }),
404
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { width: columnWidths.status, children: /* @__PURE__ */ jsxRuntime.jsx(StatusBadge, { status: suite.status }) }),
405
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: " " }),
406
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: padEnd(suite.name, columnWidths.suite) }),
407
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: " " }),
408
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "gray", children: suite.reason })
409
+ ] }, suite.name))
410
+ ] });
411
+ }
412
+ function calculateColumnWidths(suites, _selectable) {
413
+ const statusHeader = "Status";
414
+ const suiteHeader = "Suite";
415
+ const reasonHeader = "Reason";
416
+ const statusWidth = Math.max(statusHeader.length, 13);
417
+ const suiteWidth = Math.max(suiteHeader.length, ...suites.map((s) => s.name.length));
418
+ const reasonWidth = Math.max(reasonHeader.length, ...suites.map((s) => s.reason.length));
419
+ return {
420
+ status: statusWidth,
421
+ suite: suiteWidth,
422
+ reason: reasonWidth
423
+ };
424
+ }
425
+ function padEnd(str, width) {
426
+ return str.padEnd(width, " ");
427
+ }
428
+ function SelectionPrompt({
429
+ message,
430
+ options,
431
+ onSelect,
432
+ groups
433
+ }) {
434
+ ink.useInput((input) => {
435
+ const matchedOption = options.find((opt) => opt.hint === input);
436
+ if (matchedOption) {
437
+ onSelect(matchedOption.value);
438
+ return;
338
439
  }
339
- const isDirty = await checkDirtyWorkingTree();
340
- if (isDirty) {
341
- error("Working tree has uncommitted changes. Please commit or stash before attesting.");
342
- process.exit(ExitCode.CONFIG_ERROR);
440
+ if (groups) {
441
+ const matchedGroup = groups.find((group) => group.name === input);
442
+ if (matchedGroup) {
443
+ onSelect(matchedGroup.name);
444
+ }
343
445
  }
344
- for (const suiteName of suitesToRun) {
345
- const suiteConfig = config.suites[suiteName];
346
- if (!suiteConfig) continue;
347
- log(`
348
- === Running suite: ${suiteName} ===
349
- `);
350
- const fingerprintOptions = {
351
- packages: suiteConfig.packages,
352
- ...suiteConfig.ignore && { ignore: suiteConfig.ignore }
353
- };
354
- const fingerprintResult = await core.computeFingerprint(fingerprintOptions);
355
- verbose(`Fingerprint: ${fingerprintResult.fingerprint}`);
356
- verbose(`Files: ${String(fingerprintResult.fileCount)}`);
357
- const command = buildCommand(config, suiteConfig.command, suiteConfig.files);
358
- log(`Running: ${command}`);
359
- log("");
360
- const exitCode = await executeCommand(command);
361
- if (exitCode !== 0) {
362
- error(`Tests failed with exit code ${String(exitCode)}`);
363
- process.exit(ExitCode.FAILURE);
446
+ });
447
+ return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
448
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: message }),
449
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { marginTop: 1, gap: 2, children: options.map((option) => /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
450
+ option.hint && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
451
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "cyan", children: [
452
+ "[",
453
+ option.hint,
454
+ "]"
455
+ ] }),
456
+ " "
457
+ ] }),
458
+ option.label
459
+ ] }, option.value)) }),
460
+ groups && groups.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { marginTop: 1, gap: 2, children: groups.map((group) => /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
461
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "cyan", children: [
462
+ "[",
463
+ group.name,
464
+ "]"
465
+ ] }),
466
+ " ",
467
+ group.label
468
+ ] }, group.name)) })
469
+ ] });
470
+ }
471
+ function SuiteSelector({
472
+ pendingSuites,
473
+ validSuites,
474
+ groups,
475
+ onSelect,
476
+ onExit
477
+ }) {
478
+ const [selectedSuites, setSelectedSuites] = React7__namespace.useState(/* @__PURE__ */ new Set());
479
+ const [cursorIndex, setCursorIndex] = React7__namespace.useState(0);
480
+ const toggleSuite = React7__namespace.useCallback((suiteName) => {
481
+ setSelectedSuites((prev) => {
482
+ const next = new Set(prev);
483
+ if (next.has(suiteName)) {
484
+ next.delete(suiteName);
485
+ } else {
486
+ next.add(suiteName);
364
487
  }
365
- success("Tests passed!");
366
- if (options.attest === false) {
367
- log("Skipping attestation (--no-attest)");
368
- continue;
488
+ return next;
489
+ });
490
+ }, []);
491
+ ink.useInput((input, key) => {
492
+ if (input === "a") {
493
+ setSelectedSuites(new Set(pendingSuites.map((s) => s.name)));
494
+ return;
495
+ }
496
+ if (input === "n") {
497
+ onExit();
498
+ return;
499
+ }
500
+ if (/^[1-9]$/.test(input)) {
501
+ const idx = parseInt(input, 10) - 1;
502
+ if (idx < pendingSuites.length) {
503
+ const suite = pendingSuites[idx];
504
+ if (suite) {
505
+ toggleSuite(suite.name);
506
+ }
369
507
  }
370
- const shouldAttest = options.yes ?? await confirmAction({
371
- message: "Create attestation?",
372
- default: true
373
- });
374
- if (!shouldAttest) {
375
- warn("Attestation cancelled");
376
- process.exit(ExitCode.CANCELLED);
508
+ return;
509
+ }
510
+ if (input.startsWith("g") && groups) {
511
+ const groupIdx = parseInt(input.slice(1), 10) - 1;
512
+ const groupNames = Object.keys(groups);
513
+ if (groupIdx >= 0 && groupIdx < groupNames.length) {
514
+ const groupName = groupNames[groupIdx];
515
+ if (groupName) {
516
+ const groupSuites = groups[groupName] ?? [];
517
+ const newSelected = new Set(selectedSuites);
518
+ groupSuites.forEach((s) => newSelected.add(s));
519
+ setSelectedSuites(newSelected);
520
+ }
377
521
  }
378
- const attestation = core.createAttestation({
379
- suite: suiteName,
380
- fingerprint: fingerprintResult.fingerprint,
381
- command,
382
- attestedBy: os__namespace.userInfo().username
383
- });
384
- const attestationsPath = config.settings.attestationsPath;
385
- const existingFile = await core.readAttestations(attestationsPath);
386
- const existingAttestations = existingFile?.attestations ?? [];
387
- const newAttestations = core.upsertAttestation(existingAttestations, attestation);
388
- const privateKeyPath = core.getDefaultPrivateKeyPath();
389
- if (!fs__namespace.existsSync(privateKeyPath)) {
390
- error(`Private key not found: ${privateKeyPath}`);
391
- error('Run "attest-it keygen" first to generate a keypair.');
392
- process.exit(ExitCode.MISSING_KEY);
393
- }
394
- await core.writeSignedAttestations({
395
- filePath: attestationsPath,
396
- attestations: newAttestations,
397
- privateKeyPath
398
- });
399
- success(`Attestation created for ${suiteName}`);
400
- log(` Fingerprint: ${fingerprintResult.fingerprint}`);
401
- log(` Attested by: ${attestation.attestedBy}`);
402
- log(` Attested at: ${attestation.attestedAt}`);
522
+ return;
403
523
  }
404
- log("");
405
- success("All suites completed!");
406
- log(
407
- `
408
- To commit: git add ${config.settings.attestationsPath} && git commit -m "Update attestations"`
524
+ if (key.return) {
525
+ onSelect(Array.from(selectedSuites));
526
+ return;
527
+ }
528
+ if (input === " ") {
529
+ const currentSuite = pendingSuites[cursorIndex];
530
+ if (currentSuite) {
531
+ toggleSuite(currentSuite.name);
532
+ }
533
+ return;
534
+ }
535
+ if (key.upArrow) {
536
+ setCursorIndex(Math.max(0, cursorIndex - 1));
537
+ return;
538
+ }
539
+ if (key.downArrow) {
540
+ setCursorIndex(Math.min(pendingSuites.length - 1, cursorIndex + 1));
541
+ return;
542
+ }
543
+ });
544
+ return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
545
+ /* @__PURE__ */ jsxRuntime.jsx(Header, { pendingCount: pendingSuites.length }),
546
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { marginY: 1, children: /* @__PURE__ */ jsxRuntime.jsx(SuiteTable, { suites: pendingSuites, selectable: true, selected: selectedSuites }) }),
547
+ validSuites.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginY: 1, flexDirection: "column", children: [
548
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Already valid:" }),
549
+ validSuites.map((s) => /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
550
+ " ",
551
+ "\u2713 ",
552
+ s.name,
553
+ " (attested ",
554
+ String(s.age ?? 0),
555
+ " days ago)"
556
+ ] }, s.name))
557
+ ] }),
558
+ /* @__PURE__ */ jsxRuntime.jsx(
559
+ SelectionPrompt,
560
+ {
561
+ message: "Select suites to run:",
562
+ options: [
563
+ { label: "All pending", value: "all", hint: "a" },
564
+ { label: "By number", value: "number", hint: "1-9" },
565
+ { label: "None/exit", value: "none", hint: "n" }
566
+ ],
567
+ groups: groups ? Object.keys(groups).map((name, i) => ({
568
+ name: `g${String(i + 1)}`,
569
+ label: name
570
+ })) : void 0,
571
+ onSelect: () => {
572
+ }
573
+ }
574
+ ),
575
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "cyan", children: [
576
+ selectedSuites.size,
577
+ " selected. Press Enter to confirm."
578
+ ] })
579
+ ] });
580
+ }
581
+ function ProgressSummary({
582
+ completed,
583
+ remaining,
584
+ failed,
585
+ skipped
586
+ }) {
587
+ return /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { borderStyle: "single", paddingX: 1, children: /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
588
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "green", children: [
589
+ "Completed: ",
590
+ completed
591
+ ] }),
592
+ " ",
593
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "yellow", children: [
594
+ "Remaining: ",
595
+ remaining
596
+ ] }),
597
+ " ",
598
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "red", children: [
599
+ "Failed: ",
600
+ failed
601
+ ] }),
602
+ " ",
603
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "gray", children: [
604
+ "Skipped: ",
605
+ skipped
606
+ ] })
607
+ ] }) });
608
+ }
609
+ function TestRunner({
610
+ suites,
611
+ executeTest,
612
+ createAttestation: createAttestation3,
613
+ onComplete
614
+ }) {
615
+ const [currentIndex, setCurrentIndex] = React7__namespace.useState(0);
616
+ const [phase, setPhase] = React7__namespace.useState("running");
617
+ const [results, setResults] = React7__namespace.useState({
618
+ completed: [],
619
+ failed: [],
620
+ skipped: []
621
+ });
622
+ const [_testPassed, setTestPassed] = React7__namespace.useState(false);
623
+ const resultsRef = React7__namespace.useRef(results);
624
+ React7__namespace.useEffect(() => {
625
+ resultsRef.current = results;
626
+ }, [results]);
627
+ React7__namespace.useEffect(() => {
628
+ if (phase !== "running") return;
629
+ const currentSuite2 = suites[currentIndex];
630
+ if (!currentSuite2) {
631
+ onComplete(resultsRef.current);
632
+ setPhase("complete");
633
+ return;
634
+ }
635
+ let cancelled = false;
636
+ executeTest(currentSuite2).then((passed) => {
637
+ if (cancelled) return;
638
+ setTestPassed(passed);
639
+ if (passed) {
640
+ setPhase("confirming");
641
+ } else {
642
+ setResults((prev) => ({
643
+ ...prev,
644
+ failed: [...prev.failed, currentSuite2]
645
+ }));
646
+ setCurrentIndex((prev) => prev + 1);
647
+ }
648
+ }).catch(() => {
649
+ if (cancelled) return;
650
+ setResults((prev) => ({
651
+ ...prev,
652
+ failed: [...prev.failed, currentSuite2]
653
+ }));
654
+ setCurrentIndex((prev) => prev + 1);
655
+ });
656
+ return () => {
657
+ cancelled = true;
658
+ };
659
+ }, [currentIndex, phase, suites, executeTest, onComplete]);
660
+ ink.useInput(
661
+ (input, key) => {
662
+ if (phase !== "confirming") return;
663
+ const currentSuite2 = suites[currentIndex];
664
+ if (!currentSuite2) return;
665
+ if (input.toLowerCase() === "y" || key.return) {
666
+ createAttestation3(currentSuite2).then(() => {
667
+ setResults((prev) => ({
668
+ ...prev,
669
+ completed: [...prev.completed, currentSuite2]
670
+ }));
671
+ setCurrentIndex((prev) => prev + 1);
672
+ setPhase("running");
673
+ }).catch(() => {
674
+ setResults((prev) => ({
675
+ ...prev,
676
+ skipped: [...prev.skipped, currentSuite2]
677
+ }));
678
+ setCurrentIndex((prev) => prev + 1);
679
+ setPhase("running");
680
+ });
681
+ }
682
+ if (input.toLowerCase() === "n") {
683
+ setResults((prev) => ({
684
+ ...prev,
685
+ skipped: [...prev.skipped, currentSuite2]
686
+ }));
687
+ setCurrentIndex((prev) => prev + 1);
688
+ setPhase("running");
689
+ }
690
+ },
691
+ { isActive: phase === "confirming" }
692
+ );
693
+ const currentSuite = suites[currentIndex];
694
+ return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
695
+ /* @__PURE__ */ jsxRuntime.jsx(
696
+ ProgressSummary,
697
+ {
698
+ completed: results.completed.length,
699
+ failed: results.failed.length,
700
+ remaining: suites.length - currentIndex,
701
+ skipped: results.skipped.length
702
+ }
703
+ ),
704
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginY: 1, children: [
705
+ phase === "running" && currentSuite && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { children: [
706
+ /* @__PURE__ */ jsxRuntime.jsx(SimpleSpinner, {}),
707
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
708
+ " Running ",
709
+ currentSuite,
710
+ "..."
711
+ ] })
712
+ ] }),
713
+ phase === "confirming" && currentSuite && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
714
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "green", children: "\u2713 Tests passed!" }),
715
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
716
+ "Create attestation for ",
717
+ currentSuite,
718
+ "? [Y/n]: "
719
+ ] })
720
+ ] }),
721
+ phase === "complete" && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
722
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "green", children: "\u2713 All suites processed" }),
723
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
724
+ "Completed: ",
725
+ results.completed.length,
726
+ ", Failed: ",
727
+ results.failed.length,
728
+ ", Skipped:",
729
+ " ",
730
+ results.skipped.length
731
+ ] })
732
+ ] })
733
+ ] })
734
+ ] });
735
+ }
736
+ function SimpleSpinner() {
737
+ const [frame, setFrame] = React7__namespace.useState(0);
738
+ const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
739
+ React7__namespace.useEffect(() => {
740
+ const timer = setInterval(() => {
741
+ setFrame((f) => (f + 1) % frames.length);
742
+ }, 80);
743
+ return () => {
744
+ clearInterval(timer);
745
+ };
746
+ }, []);
747
+ return /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "cyan", children: frames[frame] });
748
+ }
749
+ function InteractiveRun({
750
+ allSuites,
751
+ config,
752
+ executeTest,
753
+ createAttestation: createAttestation3,
754
+ saveSession: saveSession2,
755
+ preSelected
756
+ }) {
757
+ const { exit } = ink.useApp();
758
+ const [phase, setPhase] = React7__namespace.useState(preSelected ? "running" : "selecting");
759
+ const [selectedSuites, setSelectedSuites] = React7__namespace.useState(preSelected ?? []);
760
+ const [results, setResults] = React7__namespace.useState({
761
+ completed: [],
762
+ failed: [],
763
+ skipped: []
764
+ });
765
+ const pendingSuites = React7__namespace.useMemo(
766
+ () => allSuites.filter((s) => s.status !== "VALID"),
767
+ [allSuites]
768
+ );
769
+ const validSuites = React7__namespace.useMemo(
770
+ () => allSuites.filter((s) => s.status === "VALID"),
771
+ [allSuites]
772
+ );
773
+ const handleSelect = React7__namespace.useCallback(
774
+ (selected) => {
775
+ if (selected.length === 0) {
776
+ exit();
777
+ return;
778
+ }
779
+ setSelectedSuites(selected);
780
+ setPhase("running");
781
+ },
782
+ [exit]
783
+ );
784
+ const handleRunComplete = React7__namespace.useCallback(
785
+ (runResults) => {
786
+ void (async () => {
787
+ setResults(runResults);
788
+ if (runResults.failed.length === 0 && runResults.skipped.length === 0) {
789
+ await saveSession2([], [], []);
790
+ } else {
791
+ await saveSession2(runResults.completed, runResults.failed, []);
792
+ }
793
+ setPhase("complete");
794
+ })();
795
+ },
796
+ [saveSession2]
797
+ );
798
+ if (pendingSuites.length === 0) {
799
+ return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
800
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "green", children: "\u2713 All suites are valid. Nothing to run." }),
801
+ validSuites.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
802
+ validSuites.length,
803
+ " suite(s) already attested."
804
+ ] })
805
+ ] });
806
+ }
807
+ return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
808
+ phase === "selecting" && /* @__PURE__ */ jsxRuntime.jsx(
809
+ SuiteSelector,
810
+ {
811
+ pendingSuites,
812
+ validSuites,
813
+ groups: config.groups,
814
+ onSelect: handleSelect,
815
+ onExit: () => {
816
+ exit();
817
+ }
818
+ }
819
+ ),
820
+ phase === "running" && /* @__PURE__ */ jsxRuntime.jsx(
821
+ TestRunner,
822
+ {
823
+ suites: selectedSuites,
824
+ executeTest,
825
+ createAttestation: createAttestation3,
826
+ onComplete: handleRunComplete
827
+ }
828
+ ),
829
+ phase === "complete" && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
830
+ /* @__PURE__ */ jsxRuntime.jsx(
831
+ ProgressSummary,
832
+ {
833
+ completed: results.completed.length,
834
+ failed: results.failed.length,
835
+ remaining: 0,
836
+ skipped: results.skipped.length
837
+ }
838
+ ),
839
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { marginY: 1, children: results.failed.length === 0 && results.skipped.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "green", children: "\u2713 All suites attested successfully!" }) : /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
840
+ results.failed.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "red", children: [
841
+ "\u2717 ",
842
+ results.failed.length,
843
+ " suite(s) failed: ",
844
+ results.failed.join(", ")
845
+ ] }),
846
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: "Run `attest-it run` again to continue with remaining suites." })
847
+ ] }) })
848
+ ] })
849
+ ] });
850
+ }
851
+ function determineStatus2(attestation, currentFingerprint, maxAgeDays) {
852
+ if (!attestation) {
853
+ return "NEEDS_ATTESTATION";
854
+ }
855
+ if (attestation.fingerprint !== currentFingerprint) {
856
+ return "FINGERPRINT_CHANGED";
857
+ }
858
+ const attestedAt = new Date(attestation.attestedAt);
859
+ const ageInDays = Math.floor((Date.now() - attestedAt.getTime()) / (1e3 * 60 * 60 * 24));
860
+ if (ageInDays > maxAgeDays) {
861
+ return "EXPIRED";
862
+ }
863
+ return "VALID";
864
+ }
865
+ async function getAllSuiteStatuses(config) {
866
+ let attestationsFile = null;
867
+ try {
868
+ attestationsFile = await core.readAttestations(config.settings.attestationsPath);
869
+ } catch (err) {
870
+ if (err instanceof Error && !err.message.includes("ENOENT")) {
871
+ throw err;
872
+ }
873
+ }
874
+ const attestations = attestationsFile?.attestations ?? [];
875
+ const results = [];
876
+ for (const [suiteName, suiteConfig] of Object.entries(config.suites)) {
877
+ const fingerprintResult = await core.computeFingerprint({
878
+ packages: suiteConfig.packages,
879
+ ...suiteConfig.ignore && { ignore: suiteConfig.ignore }
880
+ });
881
+ const attestation = core.findAttestation(
882
+ { schemaVersion: "1", attestations, signature: "" },
883
+ suiteName
409
884
  );
885
+ const status = determineStatus2(
886
+ attestation,
887
+ fingerprintResult.fingerprint,
888
+ config.settings.maxAgeDays
889
+ );
890
+ let age;
891
+ if (attestation) {
892
+ const attestedAt = new Date(attestation.attestedAt);
893
+ age = Math.floor((Date.now() - attestedAt.getTime()) / (1e3 * 60 * 60 * 24));
894
+ }
895
+ results.push({
896
+ name: suiteName,
897
+ status,
898
+ reason: formatStatusReason(status, age, config.settings.maxAgeDays),
899
+ currentFingerprint: fingerprintResult.fingerprint,
900
+ attestedFingerprint: attestation?.fingerprint,
901
+ attestedAt: attestation?.attestedAt,
902
+ age
903
+ });
904
+ }
905
+ return results;
906
+ }
907
+ function formatStatusReason(status, age, maxAgeDays) {
908
+ switch (status) {
909
+ case "VALID":
910
+ return `Attested ${String(age ?? 0)} days ago`;
911
+ case "NEEDS_ATTESTATION":
912
+ return "No attestation found";
913
+ case "FINGERPRINT_CHANGED":
914
+ return "Source files modified";
915
+ case "EXPIRED":
916
+ return `${String(age ?? 0)} days old (max: ${String(maxAgeDays ?? 30)})`;
917
+ case "SIGNATURE_INVALID":
918
+ return "Signature verification failed";
919
+ case "INVALIDATED_BY_PARENT":
920
+ return "Invalidated by parent suite";
921
+ default:
922
+ return status;
923
+ }
924
+ }
925
+ function getSessionPath() {
926
+ return path.join(process.cwd(), ".attest-it", "session.json");
927
+ }
928
+ async function loadSession() {
929
+ try {
930
+ const content = await promises.readFile(getSessionPath(), "utf-8");
931
+ const data = JSON.parse(content);
932
+ if (!isValidSession(data)) {
933
+ return null;
934
+ }
935
+ return data;
936
+ } catch {
937
+ return null;
938
+ }
939
+ }
940
+ async function saveSession(session) {
941
+ const sessionPath = getSessionPath();
942
+ const dir = path.dirname(sessionPath);
943
+ await promises.mkdir(dir, { recursive: true });
944
+ await promises.writeFile(sessionPath, JSON.stringify(session, null, 2), "utf-8");
945
+ }
946
+ async function clearSession() {
947
+ try {
948
+ await promises.unlink(getSessionPath());
949
+ } catch {
950
+ }
951
+ }
952
+ function isValidSession(data) {
953
+ if (typeof data !== "object" || data === null) {
954
+ return false;
955
+ }
956
+ const obj = data;
957
+ return typeof obj.started === "string" && Array.isArray(obj.selected) && obj.selected.every((item) => typeof item === "string") && Array.isArray(obj.completed) && obj.completed.every((item) => typeof item === "string") && Array.isArray(obj.failed) && obj.failed.every((item) => typeof item === "string") && Array.isArray(obj.remaining) && obj.remaining.every((item) => typeof item === "string");
958
+ }
959
+ async function runInteractive(options) {
960
+ const config = await core.loadConfig();
961
+ const allSuites = await getAllSuiteStatuses(config);
962
+ let preSelected;
963
+ if (options.continue) {
964
+ const session = await loadSession();
965
+ if (session && session.remaining.length > 0) {
966
+ preSelected = session.remaining;
967
+ log(`Resuming session with ${String(preSelected.length)} remaining suite(s)`);
968
+ }
969
+ }
970
+ if (options.dryRun) {
971
+ handleDryRun(allSuites, config, options.filter);
972
+ return;
973
+ }
974
+ const pendingSuites = allSuites.filter((s) => s.status !== "VALID");
975
+ if (pendingSuites.length === 0) {
976
+ log("All suites are valid. Nothing to run.");
977
+ process.exit(ExitCode.NO_WORK);
978
+ }
979
+ const isDirty = await checkDirtyWorkingTree();
980
+ if (isDirty) {
981
+ error("Working tree has uncommitted changes. Please commit or stash before attesting.");
982
+ process.exit(ExitCode.CONFIG_ERROR);
983
+ }
984
+ const executeTest = createTestExecutor(config);
985
+ const createAttestationFn = createAttestationCreator(config);
986
+ const saveSessionFn = createSessionSaver();
987
+ const interactiveRunProps = {
988
+ allSuites,
989
+ config,
990
+ executeTest,
991
+ createAttestation: createAttestationFn,
992
+ saveSession: saveSessionFn,
993
+ ...preSelected !== void 0 && { preSelected }
994
+ };
995
+ const { waitUntilExit } = ink.render(/* @__PURE__ */ jsxRuntime.jsx(InteractiveRun, { ...interactiveRunProps }));
996
+ await waitUntilExit();
997
+ }
998
+ function handleDryRun(allSuites, config, filterPattern) {
999
+ let pendingSuites = allSuites.filter((s) => s.status !== "VALID");
1000
+ if (filterPattern) {
1001
+ const regex = new RegExp("^" + filterPattern.replace(/\*/g, ".*") + "$", "i");
1002
+ pendingSuites = pendingSuites.filter((s) => regex.test(s.name));
1003
+ }
1004
+ if (pendingSuites.length === 0) {
1005
+ log("No suites would run (all valid or filtered out).");
1006
+ process.exit(ExitCode.NO_WORK);
1007
+ }
1008
+ log(`Would run ${String(pendingSuites.length)} suite(s):`);
1009
+ pendingSuites.forEach((s, i) => {
1010
+ log(` ${String(i + 1)}. ${s.name} (${s.status})`);
1011
+ });
1012
+ log("");
1013
+ log("Use `attest-it run` to execute.");
1014
+ process.exit(ExitCode.SUCCESS);
1015
+ }
1016
+ function createTestExecutor(config) {
1017
+ return async (suiteName) => {
1018
+ const suiteConfig = config.suites[suiteName];
1019
+ if (!suiteConfig) {
1020
+ error(`Suite "${suiteName}" not found in config`);
1021
+ return false;
1022
+ }
1023
+ let command = suiteConfig.command ?? config.settings.defaultCommand;
1024
+ if (!command) {
1025
+ error(`No command specified for suite "${suiteName}"`);
1026
+ return false;
1027
+ }
1028
+ if (command.includes("${files}") && suiteConfig.files) {
1029
+ command = command.replaceAll("${files}", suiteConfig.files.join(" "));
1030
+ }
1031
+ log(`Running: ${command}`);
1032
+ const exitCode = await executeCommand(command);
1033
+ return exitCode === 0;
1034
+ };
1035
+ }
1036
+ function createAttestationCreator(config) {
1037
+ return async (suiteName) => {
1038
+ const suiteConfig = config.suites[suiteName];
1039
+ if (!suiteConfig) {
1040
+ throw new Error(`Suite "${suiteName}" not found`);
1041
+ }
1042
+ const fingerprintResult = await core.computeFingerprint({
1043
+ packages: suiteConfig.packages,
1044
+ ...suiteConfig.ignore && { ignore: suiteConfig.ignore }
1045
+ });
1046
+ const attestation = core.createAttestation({
1047
+ suite: suiteName,
1048
+ fingerprint: fingerprintResult.fingerprint,
1049
+ command: suiteConfig.command ?? config.settings.defaultCommand ?? "",
1050
+ attestedBy: os__namespace.userInfo().username
1051
+ });
1052
+ const attestationsPath = config.settings.attestationsPath;
1053
+ const existingFile = await core.readAttestations(attestationsPath).catch(() => null);
1054
+ const existingAttestations = existingFile?.attestations ?? [];
1055
+ const newAttestations = core.upsertAttestation(existingAttestations, attestation);
1056
+ const privateKeyPath = core.getDefaultPrivateKeyPath();
1057
+ if (!fs__namespace.existsSync(privateKeyPath)) {
1058
+ throw new Error(`Private key not found: ${privateKeyPath}. Run "attest-it keygen" first.`);
1059
+ }
1060
+ await core.writeSignedAttestations({
1061
+ filePath: attestationsPath,
1062
+ attestations: newAttestations,
1063
+ privateKeyPath
1064
+ });
1065
+ log(`\u2713 Attestation created for ${suiteName}`);
1066
+ };
1067
+ }
1068
+ function createSessionSaver() {
1069
+ return async (completed, failed, remaining) => {
1070
+ if (completed.length === 0 && failed.length === 0 && remaining.length === 0) {
1071
+ await clearSession();
1072
+ } else {
1073
+ await saveSession({
1074
+ started: (/* @__PURE__ */ new Date()).toISOString(),
1075
+ selected: [...completed, ...failed, ...remaining],
1076
+ completed,
1077
+ failed,
1078
+ remaining
1079
+ });
1080
+ }
1081
+ };
1082
+ }
1083
+ async function executeCommand(command) {
1084
+ return new Promise((resolve2) => {
1085
+ const parsed = shellQuote.parse(command);
1086
+ const stringArgs = parsed.filter((t) => typeof t === "string");
1087
+ if (stringArgs.length === 0) {
1088
+ error("Empty command");
1089
+ resolve2(1);
1090
+ return;
1091
+ }
1092
+ const [executable, ...args] = stringArgs;
1093
+ if (!executable) {
1094
+ resolve2(1);
1095
+ return;
1096
+ }
1097
+ const child = child_process.spawn(executable, args, { stdio: "inherit" });
1098
+ child.on("close", (code) => {
1099
+ resolve2(code ?? 1);
1100
+ });
1101
+ child.on("error", (err) => {
1102
+ error(`Command failed: ${err.message}`);
1103
+ resolve2(1);
1104
+ });
1105
+ });
1106
+ }
1107
+ async function checkDirtyWorkingTree() {
1108
+ return new Promise((resolve2) => {
1109
+ const child = child_process.spawn("git", ["status", "--porcelain"], {
1110
+ stdio: ["ignore", "pipe", "pipe"]
1111
+ });
1112
+ let output = "";
1113
+ child.stdout.on("data", (data) => {
1114
+ output += data.toString();
1115
+ });
1116
+ child.on("close", () => {
1117
+ resolve2(output.trim().length > 0);
1118
+ });
1119
+ child.on("error", () => {
1120
+ resolve2(false);
1121
+ });
1122
+ });
1123
+ }
1124
+
1125
+ // src/commands/run.ts
1126
+ var runCommand = new commander.Command("run").description("Execute tests and create attestation").option("-s, --suite <name>", "Run specific suite (required unless --all or interactive mode)").option("-a, --all", "Run all suites needing attestation").option("--no-attest", "Run tests without creating attestation").option("-y, --yes", "Skip confirmation prompt").option("--dry-run", "Show what would run without executing").option("-c, --continue", "Resume interrupted session").option("--filter <pattern>", "Filter suites by pattern (glob-style)").action(async (options) => {
1127
+ await runTests(options);
1128
+ });
1129
+ async function runTests(options) {
1130
+ try {
1131
+ if (options.suite) {
1132
+ await runDirectMode(options);
1133
+ return;
1134
+ }
1135
+ if (options.all) {
1136
+ await runAllPending(options);
1137
+ return;
1138
+ }
1139
+ await runInteractive({
1140
+ dryRun: options.dryRun,
1141
+ continue: options.continue,
1142
+ filter: options.filter
1143
+ });
410
1144
  } catch (err) {
411
1145
  if (err instanceof Error) {
412
1146
  error(err.message);
@@ -442,7 +1176,7 @@ function parseCommand(command) {
442
1176
  }
443
1177
  return { executable, args };
444
1178
  }
445
- async function executeCommand(command) {
1179
+ async function executeCommand2(command) {
446
1180
  return new Promise((resolve2) => {
447
1181
  let parsed;
448
1182
  try {
@@ -469,7 +1203,7 @@ async function executeCommand(command) {
469
1203
  });
470
1204
  });
471
1205
  }
472
- async function checkDirtyWorkingTree() {
1206
+ async function checkDirtyWorkingTree2() {
473
1207
  return new Promise((resolve2) => {
474
1208
  const child = child_process.spawn("git", ["status", "--porcelain"], {
475
1209
  stdio: ["ignore", "pipe", "pipe"]
@@ -486,6 +1220,131 @@ async function checkDirtyWorkingTree() {
486
1220
  });
487
1221
  });
488
1222
  }
1223
+ async function runDirectMode(options) {
1224
+ if (!options.suite) {
1225
+ error("Suite name is required for direct mode");
1226
+ process.exit(ExitCode.CONFIG_ERROR);
1227
+ }
1228
+ const config = await core.loadConfig();
1229
+ if (!config.suites[options.suite]) {
1230
+ error(`Suite "${options.suite}" not found in config`);
1231
+ process.exit(ExitCode.CONFIG_ERROR);
1232
+ }
1233
+ const isDirty = await checkDirtyWorkingTree2();
1234
+ if (isDirty) {
1235
+ error("Working tree has uncommitted changes. Please commit or stash before attesting.");
1236
+ process.exit(ExitCode.CONFIG_ERROR);
1237
+ }
1238
+ await runSingleSuite(options.suite, config, options);
1239
+ log("");
1240
+ success("Suite completed!");
1241
+ log(
1242
+ `
1243
+ To commit: git add ${config.settings.attestationsPath} && git commit -m "Update attestations"`
1244
+ );
1245
+ }
1246
+ async function runAllPending(options) {
1247
+ const config = await core.loadConfig();
1248
+ const allSuites = await getAllSuiteStatuses(config);
1249
+ const pendingSuites = allSuites.filter((s) => s.status !== "VALID");
1250
+ if (pendingSuites.length === 0) {
1251
+ log("All suites are valid. Nothing to run.");
1252
+ process.exit(ExitCode.NO_WORK);
1253
+ }
1254
+ let suitesToRun = pendingSuites;
1255
+ if (options.filter) {
1256
+ const regex = new RegExp("^" + options.filter.replace(/\*/g, ".*") + "$", "i");
1257
+ suitesToRun = pendingSuites.filter((s) => regex.test(s.name));
1258
+ if (suitesToRun.length === 0) {
1259
+ log(`No suites match filter: ${options.filter}`);
1260
+ process.exit(ExitCode.NO_WORK);
1261
+ }
1262
+ }
1263
+ if (options.dryRun) {
1264
+ log(`Would run ${String(suitesToRun.length)} suite(s):`);
1265
+ suitesToRun.forEach((s, i) => {
1266
+ log(` ${String(i + 1)}. ${s.name} (${s.status})`);
1267
+ });
1268
+ process.exit(ExitCode.SUCCESS);
1269
+ }
1270
+ const isDirty = await checkDirtyWorkingTree2();
1271
+ if (isDirty) {
1272
+ error("Working tree has uncommitted changes. Please commit or stash before attesting.");
1273
+ process.exit(ExitCode.CONFIG_ERROR);
1274
+ }
1275
+ for (const suite of suitesToRun) {
1276
+ await runSingleSuite(suite.name, config, options);
1277
+ }
1278
+ log("");
1279
+ success("All suites completed!");
1280
+ log(
1281
+ `
1282
+ To commit: git add ${config.settings.attestationsPath} && git commit -m "Update attestations"`
1283
+ );
1284
+ }
1285
+ async function runSingleSuite(suiteName, config, options) {
1286
+ const suiteConfig = config.suites[suiteName];
1287
+ if (!suiteConfig) {
1288
+ error(`Suite "${suiteName}" not found in config`);
1289
+ process.exit(ExitCode.CONFIG_ERROR);
1290
+ }
1291
+ log(`
1292
+ === Running suite: ${suiteName} ===
1293
+ `);
1294
+ const fingerprintOptions = {
1295
+ packages: suiteConfig.packages,
1296
+ ...suiteConfig.ignore && { ignore: suiteConfig.ignore }
1297
+ };
1298
+ const fingerprintResult = await core.computeFingerprint(fingerprintOptions);
1299
+ verbose(`Fingerprint: ${fingerprintResult.fingerprint}`);
1300
+ verbose(`Files: ${String(fingerprintResult.fileCount)}`);
1301
+ const command = buildCommand(config, suiteConfig.command, suiteConfig.files);
1302
+ log(`Running: ${command}`);
1303
+ log("");
1304
+ const exitCode = await executeCommand2(command);
1305
+ if (exitCode !== 0) {
1306
+ error(`Tests failed with exit code ${String(exitCode)}`);
1307
+ process.exit(ExitCode.FAILURE);
1308
+ }
1309
+ success("Tests passed!");
1310
+ if (options.attest === false) {
1311
+ log("Skipping attestation (--no-attest)");
1312
+ return;
1313
+ }
1314
+ const shouldAttest = options.yes ?? await confirmAction({
1315
+ message: "Create attestation?",
1316
+ default: true
1317
+ });
1318
+ if (!shouldAttest) {
1319
+ warn("Attestation cancelled");
1320
+ process.exit(ExitCode.CANCELLED);
1321
+ }
1322
+ const attestation = core.createAttestation({
1323
+ suite: suiteName,
1324
+ fingerprint: fingerprintResult.fingerprint,
1325
+ command,
1326
+ attestedBy: os__namespace.userInfo().username
1327
+ });
1328
+ const attestationsPath = config.settings.attestationsPath;
1329
+ const existingFile = await core.readAttestations(attestationsPath);
1330
+ const existingAttestations = existingFile?.attestations ?? [];
1331
+ const newAttestations = core.upsertAttestation(existingAttestations, attestation);
1332
+ const privateKeyPath = core.getDefaultPrivateKeyPath();
1333
+ if (!fs__namespace.existsSync(privateKeyPath)) {
1334
+ error(`Private key not found: ${privateKeyPath}`);
1335
+ error('Run "attest-it keygen" first to generate a keypair.');
1336
+ process.exit(ExitCode.MISSING_KEY);
1337
+ }
1338
+ await core.writeSignedAttestations({
1339
+ filePath: attestationsPath,
1340
+ attestations: newAttestations,
1341
+ privateKeyPath
1342
+ });
1343
+ success(`Attestation created for ${suiteName}`);
1344
+ log(` Fingerprint: ${fingerprintResult.fingerprint}`);
1345
+ log(` Attested by: ${attestation.attestedBy}`);
1346
+ log(` Attested at: ${attestation.attestedAt}`);
1347
+ }
489
1348
  var keygenCommand = new commander.Command("keygen").description("Generate a new RSA keypair for signing attestations").option("-o, --output <path>", "Private key output path").option("-p, --public <path>", "Public key output path").option("-f, --force", "Overwrite existing keys").action(async (options) => {
490
1349
  await runKeygen(options);
491
1350
  });
@@ -634,7 +1493,7 @@ async function runPrune(options) {
634
1493
  } else {
635
1494
  error("Unknown error occurred");
636
1495
  }
637
- process.exit(2);
1496
+ process.exit(ExitCode.CONFIG_ERROR);
638
1497
  return;
639
1498
  }
640
1499
  }
@@ -695,7 +1554,7 @@ async function runVerify(options) {
695
1554
  } else {
696
1555
  error("Unknown error occurred");
697
1556
  }
698
- process.exit(2);
1557
+ process.exit(ExitCode.CONFIG_ERROR);
699
1558
  }
700
1559
  }
701
1560
  function displayResults(result, maxAgeDays, strict) {
@@ -771,17 +1630,51 @@ function hasWarnings(result, maxAgeDays) {
771
1630
  (s) => s.status === "VALID" && (s.age ?? 0) > maxAgeDays - warningThreshold
772
1631
  );
773
1632
  }
774
-
775
- // src/index.ts
1633
+ function hasVersion(data) {
1634
+ return typeof data === "object" && data !== null && "version" in data && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
1635
+ typeof data.version === "string";
1636
+ }
1637
+ var cachedVersion;
1638
+ function getPackageVersion() {
1639
+ if (cachedVersion !== void 0) {
1640
+ return cachedVersion;
1641
+ }
1642
+ const __filename = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
1643
+ const __dirname = path.dirname(__filename);
1644
+ const possiblePaths = [path.join(__dirname, "../package.json"), path.join(__dirname, "../../package.json")];
1645
+ for (const packageJsonPath of possiblePaths) {
1646
+ try {
1647
+ const content = fs.readFileSync(packageJsonPath, "utf-8");
1648
+ const packageJsonData = JSON.parse(content);
1649
+ if (!hasVersion(packageJsonData)) {
1650
+ throw new Error(`Invalid package.json at ${packageJsonPath}: missing version field`);
1651
+ }
1652
+ cachedVersion = packageJsonData.version;
1653
+ return cachedVersion;
1654
+ } catch (error2) {
1655
+ if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") {
1656
+ continue;
1657
+ }
1658
+ throw error2;
1659
+ }
1660
+ }
1661
+ throw new Error("Could not find package.json");
1662
+ }
776
1663
  var program = new commander.Command();
777
- program.name("attest-it").description("Human-gated test attestation system").version("0.0.1").option("-c, --config <path>", "Path to config file").option("-v, --verbose", "Verbose output").option("-q, --quiet", "Minimal output");
1664
+ program.name("attest-it").description("Human-gated test attestation system").option("-c, --config <path>", "Path to config file").option("-v, --verbose", "Verbose output").option("-q, --quiet", "Minimal output");
1665
+ program.option("-V, --version", "output the version number");
778
1666
  program.addCommand(initCommand);
779
1667
  program.addCommand(statusCommand);
780
1668
  program.addCommand(runCommand);
781
1669
  program.addCommand(keygenCommand);
782
1670
  program.addCommand(pruneCommand);
783
1671
  program.addCommand(verifyCommand);
784
- function run() {
1672
+ async function run() {
1673
+ if (process.argv.includes("--version") || process.argv.includes("-V")) {
1674
+ console.log(getPackageVersion());
1675
+ process.exit(0);
1676
+ }
1677
+ await initTheme();
785
1678
  program.parse();
786
1679
  const options = program.opts();
787
1680
  const outputOptions = {};