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