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