artshelf 0.8.0 → 0.10.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/CHANGELOG.md +47 -0
- package/README.md +13 -7
- package/SPEC.md +58 -5
- package/dist/src/cli.js +563 -82
- package/docs/agent-monitor.html +11 -3
- package/docs/agent-review.html +8 -2
- package/docs/agent-usage.html +14 -0
- package/docs/agent-usage.md +17 -0
- package/docs/index.html +1 -1
- package/docs/reference.html +69 -12
- package/package.json +1 -1
- package/skills/artshelf/SKILL.md +18 -20
package/dist/src/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ const VERSION = readPackageVersion();
|
|
|
9
9
|
const PACKAGE_NAME = "artshelf";
|
|
10
10
|
const NPM_REGISTRY_URL = process.env.ARTSHELF_NPM_REGISTRY_URL ?? `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
11
11
|
const UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1000;
|
|
12
|
-
const BOOLEAN_FLAGS = new Set(["all", "json", "manual-review", "dry-run", "execute", "help", "version", "plain"]);
|
|
12
|
+
const BOOLEAN_FLAGS = new Set(["all", "json", "agent", "manual-review", "dry-run", "execute", "help", "version", "plain"]);
|
|
13
13
|
const VALUE_FLAGS = new Set([
|
|
14
14
|
"cleanup",
|
|
15
15
|
"kind",
|
|
@@ -45,7 +45,7 @@ async function main(argv) {
|
|
|
45
45
|
return maybeNotifyUpdateAndReturn(0, parsed);
|
|
46
46
|
}
|
|
47
47
|
if (parsed.command === "help" || parsed.command === "--help" || parsed.command === "-h" || boolFlag(parsed, "help")) {
|
|
48
|
-
printHelp(parsed
|
|
48
|
+
printHelp(resolveHelpKey(parsed));
|
|
49
49
|
return maybeNotifyUpdateAndReturn(0, parsed);
|
|
50
50
|
}
|
|
51
51
|
switch (parsed.command) {
|
|
@@ -147,6 +147,10 @@ function handlePut(parsed, ledgerPath, json) {
|
|
|
147
147
|
function handleLedgers(parsed, json) {
|
|
148
148
|
const action = parsed.positionals[0] ?? "list";
|
|
149
149
|
const registryPath = normalizeRegistryPath(stringFlag(parsed, "registry"));
|
|
150
|
+
if (action === "help") {
|
|
151
|
+
printHelp("ledgers");
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
150
154
|
if (action === "add") {
|
|
151
155
|
const ledgerPath = normalizeLedgerPath(requiredStringFlag(parsed, "ledger"));
|
|
152
156
|
if (!existsSync(ledgerPath))
|
|
@@ -531,12 +535,17 @@ function handleTrashPurge(parsed, ledgerPath, json) {
|
|
|
531
535
|
return 0;
|
|
532
536
|
}
|
|
533
537
|
function handleReview(parsed, ledgerPath, json) {
|
|
538
|
+
const agent = boolFlag(parsed, "agent");
|
|
534
539
|
if (boolFlag(parsed, "all")) {
|
|
535
540
|
const registryPath = normalizeRegistryPath(stringFlag(parsed, "registry"));
|
|
536
541
|
const results = registeredLedgersOrThrow(registryPath).map((ledger) => reviewLedger(ledger));
|
|
537
542
|
const ok = results.every((entry) => entry.validate.ok);
|
|
538
543
|
const summary = summarizeReview(results);
|
|
539
|
-
|
|
544
|
+
if (agent) {
|
|
545
|
+
printCompactJson(buildReviewAgentPacketAll(results, summary, registryPath));
|
|
546
|
+
return ok ? 0 : 1;
|
|
547
|
+
}
|
|
548
|
+
const nextAction = reviewNextAction(summary, "all");
|
|
540
549
|
if (json) {
|
|
541
550
|
printJson({ ok, registryPath, summary, nextAction, ledgers: results });
|
|
542
551
|
return ok ? 0 : 1;
|
|
@@ -545,6 +554,10 @@ function handleReview(parsed, ledgerPath, json) {
|
|
|
545
554
|
return ok ? 0 : 1;
|
|
546
555
|
}
|
|
547
556
|
const result = reviewLedger({ name: "current", path: ledgerPath, scope: "other", createdAt: "", updatedAt: "" }, false);
|
|
557
|
+
if (agent) {
|
|
558
|
+
printCompactJson(buildReviewAgentPacketSingle(result, ledgerPath));
|
|
559
|
+
return result.validate.ok ? 0 : 1;
|
|
560
|
+
}
|
|
548
561
|
if (json) {
|
|
549
562
|
printJson({ ok: result.validate.ok, ledger: result });
|
|
550
563
|
return result.validate.ok ? 0 : 1;
|
|
@@ -554,6 +567,10 @@ function handleReview(parsed, ledgerPath, json) {
|
|
|
554
567
|
}
|
|
555
568
|
function handleDoctor(parsed, ledgerPath, json) {
|
|
556
569
|
const report = buildDoctorReport(ledgerPath, normalizeRegistryPath(stringFlag(parsed, "registry")));
|
|
570
|
+
if (boolFlag(parsed, "agent")) {
|
|
571
|
+
printCompactJson(buildDoctorAgentPacket(report));
|
|
572
|
+
return report.ok ? 0 : 1;
|
|
573
|
+
}
|
|
557
574
|
if (json) {
|
|
558
575
|
printJson(report);
|
|
559
576
|
return report.ok ? 0 : 1;
|
|
@@ -620,16 +637,72 @@ function buildDoctorReport(ledgerPath, registryPath) {
|
|
|
620
637
|
errors
|
|
621
638
|
};
|
|
622
639
|
}
|
|
640
|
+
// Actionable categories only — ok ledgers are healthy states, never attention.
|
|
641
|
+
// Order is fixed so the packet is byte-for-byte deterministic. Warnings surface
|
|
642
|
+
// even when health is ok (they never fail the machine), mirroring status attention.
|
|
643
|
+
const DOCTOR_ATTENTION_CATEGORIES = ["stale", "invalid", "warnings"];
|
|
644
|
+
function doctorAttention(summary) {
|
|
645
|
+
return DOCTOR_ATTENTION_CATEGORIES.filter((key) => summary[key] > 0);
|
|
646
|
+
}
|
|
647
|
+
function doctorNextAction(blockers, summary) {
|
|
648
|
+
if (blockers.length > 0) {
|
|
649
|
+
return `repair ${blockers.length} registry/ledger issue(s) above, then re-run \`artshelf doctor\``;
|
|
650
|
+
}
|
|
651
|
+
if (summary.warnings > 0) {
|
|
652
|
+
return `healthy, but ${summary.warnings} warning(s) noted — run \`artshelf validate --all\` to inspect; nothing is auto-executed`;
|
|
653
|
+
}
|
|
654
|
+
return "artshelf is healthy on this machine — cleanup safety enforced; no action needed";
|
|
655
|
+
}
|
|
656
|
+
function buildDoctorAgentPacket(report) {
|
|
657
|
+
const blockers = [];
|
|
658
|
+
if (report.registryError)
|
|
659
|
+
blockers.push(`registry unreadable: ${report.registryError}`);
|
|
660
|
+
for (const ledger of report.ledgers) {
|
|
661
|
+
if (ledger.status !== "ok") {
|
|
662
|
+
blockers.push(`${ledger.name} ${ledger.status}${ledger.errors.length ? `: ${ledger.errors[0]}` : ""}`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return {
|
|
666
|
+
schemaVersion: 1,
|
|
667
|
+
command: "doctor",
|
|
668
|
+
health: report.ok ? "ok" : "attention",
|
|
669
|
+
version: report.version,
|
|
670
|
+
node: report.node,
|
|
671
|
+
ledgerPath: report.ledgerPath,
|
|
672
|
+
registry: { path: report.registryPath, exists: report.registryExists, ok: report.registryOk, error: report.registryError },
|
|
673
|
+
ledgers: {
|
|
674
|
+
total: report.summary.ledgers,
|
|
675
|
+
ok: report.summary.ok,
|
|
676
|
+
stale: report.summary.stale,
|
|
677
|
+
invalid: report.summary.invalid,
|
|
678
|
+
warnings: report.summary.warnings
|
|
679
|
+
},
|
|
680
|
+
attention: doctorAttention(report.summary),
|
|
681
|
+
blockers,
|
|
682
|
+
cleanupSafety: report.cleanupSafety,
|
|
683
|
+
nextAction: doctorNextAction(blockers, report.summary),
|
|
684
|
+
verification: `artshelf doctor --agent --registry ${report.registryPath}`
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
// Human render (NGX-396): a scannable left-column glyph so attention state is
|
|
688
|
+
// obvious at a glance — ✓ clear, ⚠ needs attention. Plain Unicode (no ANSI
|
|
689
|
+
// color) keeps redirected/piped human output clean, and the `--agent`/`--json`
|
|
690
|
+
// renders never carry glyphs (those stay machine contracts).
|
|
691
|
+
const HUMAN_OK_GLYPH = "✓";
|
|
692
|
+
const HUMAN_ATTENTION_GLYPH = "⚠";
|
|
693
|
+
function attentionGlyph(needsAttention) {
|
|
694
|
+
return needsAttention ? HUMAN_ATTENTION_GLYPH : HUMAN_OK_GLYPH;
|
|
695
|
+
}
|
|
623
696
|
function printDoctor(report) {
|
|
624
697
|
process.stdout.write(`artshelf ${report.version} (node ${report.node})\n`);
|
|
625
|
-
process.stdout.write(
|
|
698
|
+
process.stdout.write(`${attentionGlyph(!report.ok)} health: ${report.ok ? "ok" : "needs attention"}\n`);
|
|
626
699
|
process.stdout.write(`ledger: ${report.ledgerPath}${report.ledgerExists ? "" : " (absent)"}\n`);
|
|
627
700
|
process.stdout.write(`registry: ${report.registryPath}${report.registryExists ? "" : " (absent)"}\n`);
|
|
628
701
|
if (report.registryError)
|
|
629
702
|
process.stdout.write(`registry error: ${report.registryError}\n`);
|
|
630
703
|
process.stdout.write(`registered ledgers: ${report.summary.ledgers} (${report.summary.ok} ok, ${report.summary.stale} stale, ${report.summary.invalid} invalid)\n`);
|
|
631
704
|
for (const ledger of report.ledgers) {
|
|
632
|
-
process.stdout.write(` ${ledger.status} ${ledger.name} ${ledger.path}\n`);
|
|
705
|
+
process.stdout.write(` ${attentionGlyph(ledger.status !== "ok")} ${ledger.status} ${ledger.name} ${ledger.path}\n`);
|
|
633
706
|
for (const message of ledger.errors)
|
|
634
707
|
process.stdout.write(` error: ${message}\n`);
|
|
635
708
|
}
|
|
@@ -640,8 +713,13 @@ function printDoctor(report) {
|
|
|
640
713
|
}
|
|
641
714
|
}
|
|
642
715
|
function handleStatus(parsed, ledgerPath, json) {
|
|
716
|
+
const agent = boolFlag(parsed, "agent");
|
|
643
717
|
if (boolFlag(parsed, "all")) {
|
|
644
718
|
const report = buildStatusReport(normalizeRegistryPath(stringFlag(parsed, "registry")));
|
|
719
|
+
if (agent) {
|
|
720
|
+
printCompactJson(buildStatusAgentPacketAll(report));
|
|
721
|
+
return report.ok ? 0 : 1;
|
|
722
|
+
}
|
|
645
723
|
if (json) {
|
|
646
724
|
printJson(report);
|
|
647
725
|
return report.ok ? 0 : 1;
|
|
@@ -650,6 +728,10 @@ function handleStatus(parsed, ledgerPath, json) {
|
|
|
650
728
|
return report.ok ? 0 : 1;
|
|
651
729
|
}
|
|
652
730
|
const ledger = statusLedger({ name: "current", path: ledgerPath, scope: "other", createdAt: "", updatedAt: "" }, false);
|
|
731
|
+
if (agent) {
|
|
732
|
+
printCompactJson(buildStatusAgentPacketSingle(ledger, ledgerPath));
|
|
733
|
+
return ledger.ok ? 0 : 1;
|
|
734
|
+
}
|
|
653
735
|
if (json) {
|
|
654
736
|
printJson({ ok: ledger.ok, ledger });
|
|
655
737
|
return ledger.ok ? 0 : 1;
|
|
@@ -733,23 +815,101 @@ function sumStatusCounts(ledgers, key) {
|
|
|
733
815
|
function formatStatusCounts(counts) {
|
|
734
816
|
return `active ${counts.active} · due ${counts.due} · manual-review ${counts.manualReview} · missing ${counts.missingPath} · kept ${counts.kept} · pending ${counts.pendingCleanup}`;
|
|
735
817
|
}
|
|
818
|
+
// Actionable categories only — active and kept are healthy states, never
|
|
819
|
+
// attention. Order is fixed so the packet is byte-for-byte deterministic.
|
|
820
|
+
const STATUS_ATTENTION_CATEGORIES = ["due", "manualReview", "missingPath", "pendingCleanup"];
|
|
821
|
+
function statusAttention(counts) {
|
|
822
|
+
return STATUS_ATTENTION_CATEGORIES.filter((key) => counts[key] > 0);
|
|
823
|
+
}
|
|
824
|
+
function statusCommand(scope, command, ledgerPath) {
|
|
825
|
+
if (scope === "all")
|
|
826
|
+
return `artshelf ${command} --all`;
|
|
827
|
+
return ledgerPath ? `artshelf ${command} --ledger ${ledgerPath}` : `artshelf ${command}`;
|
|
828
|
+
}
|
|
829
|
+
function statusNextAction(blockers, counts, scope, ledgerPath) {
|
|
830
|
+
if (blockers.length > 0) {
|
|
831
|
+
const verify = statusCommand(scope, "status", ledgerPath);
|
|
832
|
+
return `repair ${blockers.length} broken ledger(s) above, then re-run \`${verify}\``;
|
|
833
|
+
}
|
|
834
|
+
const review = statusCommand(scope, "review", ledgerPath);
|
|
835
|
+
if (counts.pendingCleanup > 0 || counts.due > 0) {
|
|
836
|
+
return `run \`${review}\` to preview cleanup plans; nothing is auto-executed`;
|
|
837
|
+
}
|
|
838
|
+
if (counts.manualReview > 0) {
|
|
839
|
+
return `run \`${review}\` to inspect manual-review records; nothing is auto-executed`;
|
|
840
|
+
}
|
|
841
|
+
if (counts.missingPath > 0) {
|
|
842
|
+
return "inspect missing-path records and `artshelf resolve` the ones no longer needed; nothing is auto-executable";
|
|
843
|
+
}
|
|
844
|
+
return "nothing due — no broken ledgers and no due, manual-review, missing-path, or pending cleanup entries";
|
|
845
|
+
}
|
|
846
|
+
function buildStatusAgentPacketAll(report) {
|
|
847
|
+
const blockers = [];
|
|
848
|
+
if (report.registryError)
|
|
849
|
+
blockers.push(`registry unreadable: ${report.registryError}`);
|
|
850
|
+
for (const ledger of report.ledgers) {
|
|
851
|
+
if (ledger.status !== "ok") {
|
|
852
|
+
blockers.push(`${ledger.name} ${ledger.status}${ledger.errors.length ? `: ${ledger.errors[0]}` : ""}`);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
const counts = {
|
|
856
|
+
active: report.totals.active,
|
|
857
|
+
due: report.totals.due,
|
|
858
|
+
manualReview: report.totals.manualReview,
|
|
859
|
+
missingPath: report.totals.missingPath,
|
|
860
|
+
kept: report.totals.kept,
|
|
861
|
+
pendingCleanup: report.totals.pendingCleanup
|
|
862
|
+
};
|
|
863
|
+
return {
|
|
864
|
+
schemaVersion: 1,
|
|
865
|
+
command: "status",
|
|
866
|
+
scope: "all",
|
|
867
|
+
health: report.ok ? "ok" : "attention",
|
|
868
|
+
registry: { path: report.registryPath, exists: report.registryExists, ok: report.registryOk, error: report.registryError },
|
|
869
|
+
ledgers: { total: report.totals.ledgers, ok: report.totals.ok, stale: report.totals.stale, invalid: report.totals.invalid },
|
|
870
|
+
counts,
|
|
871
|
+
attention: statusAttention(counts),
|
|
872
|
+
blockers,
|
|
873
|
+
nextAction: statusNextAction(blockers, counts, "all"),
|
|
874
|
+
verification: `artshelf status --all --agent --registry ${report.registryPath}`
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
function buildStatusAgentPacketSingle(ledger, ledgerPath) {
|
|
878
|
+
const blockers = ledger.ok
|
|
879
|
+
? []
|
|
880
|
+
: [`${ledger.status}${ledger.errors.length ? `: ${ledger.errors[0]}` : ""}`];
|
|
881
|
+
return {
|
|
882
|
+
schemaVersion: 1,
|
|
883
|
+
command: "status",
|
|
884
|
+
scope: "single",
|
|
885
|
+
health: ledger.ok ? "ok" : "attention",
|
|
886
|
+
ledgerPath,
|
|
887
|
+
counts: ledger.counts,
|
|
888
|
+
attention: statusAttention(ledger.counts),
|
|
889
|
+
blockers,
|
|
890
|
+
nextAction: statusNextAction(blockers, ledger.counts, "single", ledgerPath),
|
|
891
|
+
verification: `artshelf status --agent --ledger ${ledgerPath}`
|
|
892
|
+
};
|
|
893
|
+
}
|
|
736
894
|
function printStatusAll(report) {
|
|
737
|
-
|
|
895
|
+
const anyActionable = report.ledgers.some((ledger) => statusAttention(ledger.counts).length > 0);
|
|
896
|
+
process.stdout.write(`${attentionGlyph(!report.ok || anyActionable)} artshelf status: ${report.ok ? "ok" : "needs attention"}\n`);
|
|
738
897
|
process.stdout.write(`registry: ${report.registryPath}${report.registryExists ? "" : " (absent)"} — ${report.totals.ledgers} ledgers (${report.totals.ok} ok, ${report.totals.stale} stale, ${report.totals.invalid} invalid)\n`);
|
|
739
898
|
if (report.registryError)
|
|
740
899
|
process.stdout.write(`registry error: ${report.registryError}\n`);
|
|
741
900
|
for (const ledger of report.ledgers) {
|
|
742
901
|
if (ledger.status === "ok") {
|
|
743
|
-
process.stdout.write(
|
|
902
|
+
process.stdout.write(`${attentionGlyph(statusAttention(ledger.counts).length > 0)} [${ledger.name}] ${formatStatusCounts(ledger.counts)}\n`);
|
|
744
903
|
}
|
|
745
904
|
else {
|
|
746
|
-
process.stdout.write(
|
|
905
|
+
process.stdout.write(`${HUMAN_ATTENTION_GLYPH} [${ledger.name}] ${ledger.status}: ${ledger.errors.join("; ")}\n`);
|
|
747
906
|
}
|
|
748
907
|
}
|
|
749
908
|
process.stdout.write(`total: ${formatStatusCounts(report.totals)}\n`);
|
|
750
909
|
}
|
|
751
910
|
function printStatusSingle(ledger) {
|
|
752
|
-
|
|
911
|
+
const needsAttention = !ledger.ok || statusAttention(ledger.counts).length > 0;
|
|
912
|
+
process.stdout.write(`${attentionGlyph(needsAttention)} artshelf status: ${ledger.ok ? "ok" : ledger.status}\n`);
|
|
753
913
|
process.stdout.write(`ledger: ${ledger.path}\n`);
|
|
754
914
|
if (ledger.ok) {
|
|
755
915
|
process.stdout.write(`${formatStatusCounts(ledger.counts)}\n`);
|
|
@@ -950,6 +1110,14 @@ function parseArgs(argv) {
|
|
|
950
1110
|
const arg = rest[index];
|
|
951
1111
|
if (!arg)
|
|
952
1112
|
continue;
|
|
1113
|
+
if (arg === "-h") {
|
|
1114
|
+
flags.set("help", true);
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
if (arg === "-v") {
|
|
1118
|
+
flags.set("version", true);
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
953
1121
|
if (!arg.startsWith("--")) {
|
|
954
1122
|
positionals.push(arg);
|
|
955
1123
|
continue;
|
|
@@ -996,6 +1164,12 @@ function printJson(value) {
|
|
|
996
1164
|
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
997
1165
|
return 0;
|
|
998
1166
|
}
|
|
1167
|
+
// Agent/compact surface: a single minified JSON line. The default `--json`
|
|
1168
|
+
// stays pretty-printed for audit/debug; agent packets optimize for tokens.
|
|
1169
|
+
function printCompactJson(value) {
|
|
1170
|
+
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
1171
|
+
return 0;
|
|
1172
|
+
}
|
|
999
1173
|
function registeredLedgersOrThrow(registryPath) {
|
|
1000
1174
|
const ledgers = listRegisteredLedgers(registryPath);
|
|
1001
1175
|
if (ledgers.length === 0)
|
|
@@ -1145,13 +1319,16 @@ function summarizeReview(results) {
|
|
|
1145
1319
|
}
|
|
1146
1320
|
return summary;
|
|
1147
1321
|
}
|
|
1148
|
-
function reviewNextAction(summary) {
|
|
1322
|
+
function reviewNextAction(summary, scope, ledgerPath) {
|
|
1149
1323
|
const broken = summary.invalid + summary.stale;
|
|
1324
|
+
const review = statusCommand(scope, "review", ledgerPath);
|
|
1150
1325
|
if (broken > 0) {
|
|
1151
|
-
|
|
1326
|
+
const repair = scope === "all" ? "re-register or fix the file" : "fix the file";
|
|
1327
|
+
return `repair ${broken} broken ledger(s) above (${repair}), then re-run \`${review}\``;
|
|
1152
1328
|
}
|
|
1153
1329
|
if (summary.executable > 0) {
|
|
1154
|
-
|
|
1330
|
+
const dryRun = scope === "all" ? "artshelf cleanup --dry-run --all" : `artshelf cleanup --dry-run${ledgerPath ? ` --ledger ${ledgerPath}` : ""}`;
|
|
1331
|
+
return `run \`${dryRun}\` to generate plans, then \`artshelf cleanup --execute --plan-id <id> --ledger <path>\` for each reviewed plan`;
|
|
1155
1332
|
}
|
|
1156
1333
|
if (summary.missingPath > 0) {
|
|
1157
1334
|
return "inspect missing-path entries and `artshelf resolve` the ones no longer needed; nothing is auto-executable";
|
|
@@ -1160,7 +1337,7 @@ function reviewNextAction(summary) {
|
|
|
1160
1337
|
}
|
|
1161
1338
|
function printReviewAll(results, summary, nextAction, registryPath) {
|
|
1162
1339
|
const needsAttention = summary.invalid + summary.stale + summary.executable + summary.due + summary.manualReview + summary.missingPath > 0;
|
|
1163
|
-
process.stdout.write(
|
|
1340
|
+
process.stdout.write(`${attentionGlyph(needsAttention)} artshelf review --all: ${needsAttention ? "needs attention" : "all clear"}\n`);
|
|
1164
1341
|
process.stdout.write(`registry: ${registryPath} — ${summary.ledgers} ledgers (${summary.ok} ok, ${summary.invalid} invalid, ${summary.stale} stale)\n`);
|
|
1165
1342
|
printReview(results);
|
|
1166
1343
|
process.stdout.write(`triage: due ${summary.due} · manual-review ${summary.manualReview} · missing ${summary.missingPath} · executable ${summary.executable} · skipped ${summary.skipped}\n`);
|
|
@@ -1169,12 +1346,236 @@ function printReviewAll(results, summary, nextAction, registryPath) {
|
|
|
1169
1346
|
function printReview(results) {
|
|
1170
1347
|
for (const result of results) {
|
|
1171
1348
|
const visibleDue = result.due.filter((entry) => entry.dueStatus !== "kept");
|
|
1172
|
-
|
|
1349
|
+
const needsAttention = !result.validate.ok || visibleDue.length > 0 || result.plan.entries.length > 0;
|
|
1350
|
+
process.stdout.write(`${attentionGlyph(needsAttention)} [${result.ledger.name}] ${result.validate.ok ? "ok" : "invalid"}: ${result.validate.entries} entries, ${result.validate.errors.length} errors, ${result.validate.warnings.length} warnings\n`);
|
|
1173
1351
|
process.stdout.write(`due/manual/missing: ${visibleDue.length}; plan ${result.plan.planId}: ${result.plan.entries.length} entries, ${result.plan.skipped.length} skipped\n`);
|
|
1174
1352
|
process.stdout.write(`ledger: ${result.ledger.path}\n`);
|
|
1175
1353
|
}
|
|
1176
1354
|
}
|
|
1177
|
-
|
|
1355
|
+
// review is read-only, so every safety guarantee holds unconditionally.
|
|
1356
|
+
const REVIEW_SAFETY = {
|
|
1357
|
+
dryRunOnly: true,
|
|
1358
|
+
executeAllRefused: true,
|
|
1359
|
+
noExecuteRan: true,
|
|
1360
|
+
noResolveRan: true,
|
|
1361
|
+
noDeleteRan: true
|
|
1362
|
+
};
|
|
1363
|
+
// Classify each registered ledger's records into decision groups. Order is
|
|
1364
|
+
// fixed (registry order, then a stable per-ledger sub-order) so the packet is
|
|
1365
|
+
// byte-for-byte deterministic.
|
|
1366
|
+
function buildReviewDecisions(results, scope) {
|
|
1367
|
+
const readyForApproval = [];
|
|
1368
|
+
const needsReviewFirst = [];
|
|
1369
|
+
const blocked = [];
|
|
1370
|
+
const review = scope === "all" ? "artshelf review --all" : "artshelf review";
|
|
1371
|
+
for (const result of results) {
|
|
1372
|
+
const { ledger, validate, due } = result;
|
|
1373
|
+
if (!validate.ok) {
|
|
1374
|
+
const status = existsSync(ledger.path) ? "invalid" : "missing";
|
|
1375
|
+
const repair = scope === "all" ? `re-register or fix ${ledger.path}` : `fix ${ledger.path}`;
|
|
1376
|
+
blocked.push({
|
|
1377
|
+
label: `Repair ${ledger.name} ledger (${status})`,
|
|
1378
|
+
itemIds: [],
|
|
1379
|
+
actionType: "fix-registry",
|
|
1380
|
+
approvalTarget: null,
|
|
1381
|
+
reason: validate.errors[0] ?? `${scope === "all" ? "registered ledger" : "ledger"} is ${status}`,
|
|
1382
|
+
nextStep: `${repair}, then re-run \`${review}\``
|
|
1383
|
+
});
|
|
1384
|
+
continue;
|
|
1385
|
+
}
|
|
1386
|
+
const missingPath = due.filter((entry) => entry.dueStatus === "missing-path");
|
|
1387
|
+
const trashSafe = due.filter((entry) => entry.dueStatus === "due" && entry.cleanup === "trash");
|
|
1388
|
+
const inspectItems = due.filter((entry) => entry.dueStatus === "manual-review" ||
|
|
1389
|
+
(entry.dueStatus === "due" && (entry.cleanup === "review" || entry.cleanup === "delete")));
|
|
1390
|
+
// Ready for approval: missing-path records resolve ledger-only with an exact,
|
|
1391
|
+
// plan-less approval target. Resolution updates the ledger and never touches
|
|
1392
|
+
// files, so it is the one action review can hand an agent directly.
|
|
1393
|
+
if (missingPath.length > 0) {
|
|
1394
|
+
const ids = missingPath.map((entry) => entry.id).sort();
|
|
1395
|
+
readyForApproval.push({
|
|
1396
|
+
label: `Resolve ${ids.length} missing-path record(s) in ${ledger.name}`,
|
|
1397
|
+
itemIds: ids,
|
|
1398
|
+
actionType: "resolve-missing",
|
|
1399
|
+
approvalTarget: `approve artshelf resolve missing ledger ${ledger.path} ids ${ids.join(" ")}`,
|
|
1400
|
+
reason: "the recorded path is already missing",
|
|
1401
|
+
nextStep: "confirm the artifact is no longer needed, then approve the ledger-only resolve"
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
// Trash-safe records are cleanup-eligible, but review never mints a plan, so
|
|
1405
|
+
// they carry no approval target: the next step is the dry-run that produces
|
|
1406
|
+
// the reviewed plan id to approve.
|
|
1407
|
+
if (trashSafe.length > 0) {
|
|
1408
|
+
const ids = trashSafe.map((entry) => entry.id).sort();
|
|
1409
|
+
needsReviewFirst.push({
|
|
1410
|
+
label: `Plan cleanup for ${ids.length} trash-eligible artifact(s) in ${ledger.name}`,
|
|
1411
|
+
itemIds: ids,
|
|
1412
|
+
actionType: "cleanup",
|
|
1413
|
+
approvalTarget: null,
|
|
1414
|
+
reason: "disposable artifacts are due but no reviewed cleanup plan exists yet",
|
|
1415
|
+
nextStep: `run \`artshelf cleanup --dry-run --ledger ${ledger.path} --json\`, then approve \`approve artshelf cleanup ledger ${ledger.path} plan <plan-id>\``
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
// manual-review and cleanup=review records need a human decision before any
|
|
1419
|
+
// cleanup; cleanup=delete is refused outright. None carry an approval target.
|
|
1420
|
+
if (inspectItems.length > 0) {
|
|
1421
|
+
const ids = inspectItems.map((entry) => entry.id).sort();
|
|
1422
|
+
const hasDelete = inspectItems.some((entry) => entry.cleanup === "delete");
|
|
1423
|
+
needsReviewFirst.push({
|
|
1424
|
+
label: `Inspect ${ids.length} record(s) in ${ledger.name} before cleanup`,
|
|
1425
|
+
itemIds: ids,
|
|
1426
|
+
actionType: "inspect",
|
|
1427
|
+
approvalTarget: null,
|
|
1428
|
+
reason: hasDelete
|
|
1429
|
+
? "records need manual review; cleanup=delete is refused and never deletes files"
|
|
1430
|
+
: "records are held for manual review before any cleanup",
|
|
1431
|
+
nextStep: "inspect each path, then keep, change retention, resolve, or set cleanup=trash and plan a cleanup"
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
return { readyForApproval, needsReviewFirst, blocked };
|
|
1436
|
+
}
|
|
1437
|
+
function reviewCounts(summary) {
|
|
1438
|
+
return {
|
|
1439
|
+
due: summary.due,
|
|
1440
|
+
manualReview: summary.manualReview,
|
|
1441
|
+
missingPath: summary.missingPath,
|
|
1442
|
+
executable: summary.executable,
|
|
1443
|
+
skipped: summary.skipped
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
function buildReviewAgentPacketAll(results, summary, registryPath) {
|
|
1447
|
+
const groups = buildReviewDecisions(results, "all");
|
|
1448
|
+
return {
|
|
1449
|
+
schemaVersion: 1,
|
|
1450
|
+
command: "review",
|
|
1451
|
+
scope: "all",
|
|
1452
|
+
health: summary.invalid + summary.stale > 0 ? "attention" : "ok",
|
|
1453
|
+
registry: { path: registryPath, exists: existsSync(registryPath) },
|
|
1454
|
+
ledgers: { total: summary.ledgers, ok: summary.ok, stale: summary.stale, invalid: summary.invalid },
|
|
1455
|
+
counts: reviewCounts(summary),
|
|
1456
|
+
decisionSummary: {
|
|
1457
|
+
readyForApproval: groups.readyForApproval.length,
|
|
1458
|
+
needsReviewFirst: groups.needsReviewFirst.length,
|
|
1459
|
+
blocked: groups.blocked.length
|
|
1460
|
+
},
|
|
1461
|
+
readyForApproval: groups.readyForApproval,
|
|
1462
|
+
needsReviewFirst: groups.needsReviewFirst,
|
|
1463
|
+
blocked: groups.blocked,
|
|
1464
|
+
safety: REVIEW_SAFETY,
|
|
1465
|
+
nextAction: reviewNextAction(summary, "all"),
|
|
1466
|
+
verification: `artshelf review --all --agent --registry ${registryPath}`
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
function buildReviewAgentPacketSingle(result, ledgerPath) {
|
|
1470
|
+
const summary = summarizeReview([result]);
|
|
1471
|
+
const groups = buildReviewDecisions([result], "single");
|
|
1472
|
+
return {
|
|
1473
|
+
schemaVersion: 1,
|
|
1474
|
+
command: "review",
|
|
1475
|
+
scope: "single",
|
|
1476
|
+
health: summary.invalid + summary.stale > 0 ? "attention" : "ok",
|
|
1477
|
+
ledgerPath,
|
|
1478
|
+
counts: reviewCounts(summary),
|
|
1479
|
+
decisionSummary: {
|
|
1480
|
+
readyForApproval: groups.readyForApproval.length,
|
|
1481
|
+
needsReviewFirst: groups.needsReviewFirst.length,
|
|
1482
|
+
blocked: groups.blocked.length
|
|
1483
|
+
},
|
|
1484
|
+
readyForApproval: groups.readyForApproval,
|
|
1485
|
+
needsReviewFirst: groups.needsReviewFirst,
|
|
1486
|
+
blocked: groups.blocked,
|
|
1487
|
+
safety: REVIEW_SAFETY,
|
|
1488
|
+
nextAction: reviewNextAction(summary, "single", ledgerPath),
|
|
1489
|
+
verification: `artshelf review --agent --ledger ${ledgerPath}`
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
const COMMAND_GROUPS = [
|
|
1493
|
+
{
|
|
1494
|
+
group: "Create",
|
|
1495
|
+
commands: [{ name: "put", summary: "Record an artifact with a reason and retention" }]
|
|
1496
|
+
},
|
|
1497
|
+
{
|
|
1498
|
+
group: "Inspect",
|
|
1499
|
+
commands: [
|
|
1500
|
+
{ name: "list", summary: "List ledger records" },
|
|
1501
|
+
{ name: "find", summary: "Find records by path, owner, label, or status" },
|
|
1502
|
+
{ name: "get", summary: "Show one record by id" },
|
|
1503
|
+
{ name: "due", summary: "Show due, manual-review, and missing-path records" },
|
|
1504
|
+
{ name: "status", summary: "Summarize ledger and registry counts" }
|
|
1505
|
+
]
|
|
1506
|
+
},
|
|
1507
|
+
{
|
|
1508
|
+
group: "Review",
|
|
1509
|
+
commands: [
|
|
1510
|
+
{ name: "validate", summary: "Check ledger shape and report warnings" },
|
|
1511
|
+
{ name: "review", summary: "Preview validate, due, and cleanup plans (read-only)" }
|
|
1512
|
+
]
|
|
1513
|
+
},
|
|
1514
|
+
{
|
|
1515
|
+
group: "Clean",
|
|
1516
|
+
commands: [
|
|
1517
|
+
{ name: "cleanup", summary: "Plan and execute approved cleanups" },
|
|
1518
|
+
{ name: "trash", summary: "Inspect and purge Artshelf trash" },
|
|
1519
|
+
{ name: "resolve", summary: "Mark a record manually resolved" }
|
|
1520
|
+
]
|
|
1521
|
+
},
|
|
1522
|
+
{
|
|
1523
|
+
group: "System",
|
|
1524
|
+
commands: [
|
|
1525
|
+
{ name: "ledgers", summary: "Manage the ledger registry" },
|
|
1526
|
+
{ name: "doctor", summary: "Report Artshelf health on this machine" },
|
|
1527
|
+
{ name: "update", summary: "Update the Artshelf CLI" }
|
|
1528
|
+
]
|
|
1529
|
+
}
|
|
1530
|
+
];
|
|
1531
|
+
// Commands with subcommands that carry their own focused help. Used to route
|
|
1532
|
+
// `artshelf <command> <subcommand> --help` to a nested help key.
|
|
1533
|
+
const NESTED_HELP = new Map([
|
|
1534
|
+
["trash", new Set(["list", "purge"])],
|
|
1535
|
+
["ledgers", new Set(["list", "add"])]
|
|
1536
|
+
]);
|
|
1537
|
+
function resolveHelpKey(parsed) {
|
|
1538
|
+
// `artshelf help [command [subcommand]]`
|
|
1539
|
+
if (parsed.command === "help") {
|
|
1540
|
+
return joinHelpKey(parsed.positionals[0], parsed.positionals[1]);
|
|
1541
|
+
}
|
|
1542
|
+
// `artshelf [--help|-h]` with no command resolves to the top-level help.
|
|
1543
|
+
if (!parsed.command || parsed.command === "--help" || parsed.command === "-h") {
|
|
1544
|
+
return "";
|
|
1545
|
+
}
|
|
1546
|
+
// `artshelf <command> [subcommand] --help`
|
|
1547
|
+
return joinHelpKey(parsed.command, parsed.positionals[0]);
|
|
1548
|
+
}
|
|
1549
|
+
function joinHelpKey(command, subcommand) {
|
|
1550
|
+
if (!command)
|
|
1551
|
+
return "";
|
|
1552
|
+
const subcommands = NESTED_HELP.get(command);
|
|
1553
|
+
if (subcommands && subcommand && subcommands.has(subcommand)) {
|
|
1554
|
+
return `${command} ${subcommand}`;
|
|
1555
|
+
}
|
|
1556
|
+
return command;
|
|
1557
|
+
}
|
|
1558
|
+
function renderTopLevelHelp() {
|
|
1559
|
+
const names = COMMAND_GROUPS.flatMap((entry) => entry.commands.map((command) => command.name));
|
|
1560
|
+
const width = Math.max(...names.map((name) => name.length)) + 2;
|
|
1561
|
+
const lines = [
|
|
1562
|
+
`Artshelf ${VERSION} — approval-first retention for the temporary files agents leave behind.`,
|
|
1563
|
+
"",
|
|
1564
|
+
"Usage:",
|
|
1565
|
+
" artshelf <command> [options]",
|
|
1566
|
+
"",
|
|
1567
|
+
"Available Commands:"
|
|
1568
|
+
];
|
|
1569
|
+
for (const { group, commands } of COMMAND_GROUPS) {
|
|
1570
|
+
lines.push(` ${group}`);
|
|
1571
|
+
for (const command of commands) {
|
|
1572
|
+
lines.push(` ${command.name.padEnd(width)}${command.summary}`);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
lines.push("", "Global Options:", " -h, --help Show help for artshelf or a specific command", " -v, --version Show the Artshelf version", "", "Output:", " --json Emit machine-readable JSON on commands that return data", "", "Scope (command-specific):", " --ledger <path> Target an explicit JSONL ledger", " --registry <path> Target an explicit ledger registry", " --all Read every registered ledger (on commands that support it)", "", `Use "artshelf <command> --help" for more information about a command.`, "");
|
|
1576
|
+
return lines.join("\n");
|
|
1577
|
+
}
|
|
1578
|
+
function printHelp(command = "") {
|
|
1178
1579
|
if (command === "put") {
|
|
1179
1580
|
process.stdout.write(`Usage:
|
|
1180
1581
|
artshelf put <path> --reason <text> (--ttl <ttl>|--retain-until <date>|--manual-review) [options]
|
|
@@ -1208,30 +1609,36 @@ Global --all mode is dry-run only.
|
|
|
1208
1609
|
return;
|
|
1209
1610
|
}
|
|
1210
1611
|
if (command === "trash") {
|
|
1211
|
-
process.stdout.write(`
|
|
1212
|
-
artshelf trash list [--ledger <path>] [--all] [--json]
|
|
1213
|
-
artshelf trash purge --older-than <ttl> --dry-run [--ledger <path>] [--json]
|
|
1214
|
-
artshelf trash purge --execute --plan-id <id> [--ledger <path>] [--json]
|
|
1612
|
+
process.stdout.write(`Inspect and purge Artshelf trash.
|
|
1215
1613
|
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1614
|
+
Usage:
|
|
1615
|
+
artshelf trash [command]
|
|
1616
|
+
|
|
1617
|
+
Available Commands:
|
|
1618
|
+
list List records currently held in Artshelf trash
|
|
1619
|
+
purge Plan or execute approved permanent trash deletion
|
|
1620
|
+
|
|
1621
|
+
Flags:
|
|
1622
|
+
-h, --help help for trash
|
|
1623
|
+
|
|
1624
|
+
Use "artshelf trash <command> --help" for more information about a command.
|
|
1224
1625
|
`);
|
|
1225
1626
|
return;
|
|
1226
1627
|
}
|
|
1227
1628
|
if (command === "ledgers") {
|
|
1228
|
-
process.stdout.write(`
|
|
1229
|
-
|
|
1230
|
-
|
|
1629
|
+
process.stdout.write(`Manage the ledger registry.
|
|
1630
|
+
|
|
1631
|
+
Usage:
|
|
1632
|
+
artshelf ledgers [command]
|
|
1231
1633
|
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1634
|
+
Available Commands:
|
|
1635
|
+
list List and validate registered ledgers
|
|
1636
|
+
add Register an existing ledger file
|
|
1637
|
+
|
|
1638
|
+
Flags:
|
|
1639
|
+
-h, --help help for ledgers
|
|
1640
|
+
|
|
1641
|
+
Use "artshelf ledgers <command> --help" for more information about a command.
|
|
1235
1642
|
`);
|
|
1236
1643
|
return;
|
|
1237
1644
|
}
|
|
@@ -1283,17 +1690,28 @@ Resolved records stay in the audit trail but no longer participate in due or cle
|
|
|
1283
1690
|
}
|
|
1284
1691
|
if (command === "review") {
|
|
1285
1692
|
process.stdout.write(`Usage:
|
|
1286
|
-
artshelf review [--ledger <path>] [--json]
|
|
1287
|
-
artshelf review --all [--registry <path>] [--json]
|
|
1693
|
+
artshelf review [--ledger <path>] [--json|--agent]
|
|
1694
|
+
artshelf review --all [--registry <path>] [--json|--agent]
|
|
1288
1695
|
|
|
1289
|
-
Review runs validate, due, and cleanup plan preview without moving files or
|
|
1290
|
-
With --all, review adds aggregate triage counts and the next
|
|
1696
|
+
Review runs validate, due, and cleanup plan preview without moving files or
|
|
1697
|
+
writing a plan. With --all, review adds aggregate triage counts and the next
|
|
1698
|
+
safe action.
|
|
1699
|
+
|
|
1700
|
+
Render modes:
|
|
1701
|
+
(default) Human summary of validation, triage counts, and the next safe action.
|
|
1702
|
+
--json Full read-only audit report (backward-compatible).
|
|
1703
|
+
--agent Compact single-line JSON decision packet for agents: health, triage
|
|
1704
|
+
counts, and classified decision groups (ready for approval, needs
|
|
1705
|
+
review first, blocked) with exact approval targets where they are
|
|
1706
|
+
safe. Review is read-only, so cleanup approval targets are minted by
|
|
1707
|
+
\`cleanup --dry-run\`, never leaked from a preview plan id.
|
|
1708
|
+
Token-efficient; --agent takes precedence over --json.
|
|
1291
1709
|
`);
|
|
1292
1710
|
return;
|
|
1293
1711
|
}
|
|
1294
1712
|
if (command === "doctor") {
|
|
1295
1713
|
process.stdout.write(`Usage:
|
|
1296
|
-
artshelf doctor [--registry <path>] [--ledger <path>] [--json]
|
|
1714
|
+
artshelf doctor [--registry <path>] [--ledger <path>] [--json|--agent]
|
|
1297
1715
|
|
|
1298
1716
|
Doctor reports whether Artshelf is healthy on this machine: CLI version, selected
|
|
1299
1717
|
or default ledger path, selected or global registry path, registered ledger health
|
|
@@ -1302,6 +1720,14 @@ selected or default ledger and still requires a reviewed plan id; --all execute
|
|
|
1302
1720
|
and cleanup=delete are refused, while physical trash purge requires a separate
|
|
1303
1721
|
reviewed purge plan.
|
|
1304
1722
|
|
|
1723
|
+
Render modes:
|
|
1724
|
+
(default) Human summary of machine health and cleanup safety.
|
|
1725
|
+
--json Full audit report (backward-compatible; suitable for cron/reporting).
|
|
1726
|
+
--agent Compact single-line JSON decision packet for agents: health, registry
|
|
1727
|
+
and registered-ledger health, blockers, cleanup-safety posture, next
|
|
1728
|
+
action, and a verify command. Token-efficient; --agent takes
|
|
1729
|
+
precedence over --json.
|
|
1730
|
+
|
|
1305
1731
|
Run it after install, when --all commands behave unexpectedly, or on a schedule to
|
|
1306
1732
|
catch stale registry entries. Doctor is read-only. A healthy machine exits 0; a
|
|
1307
1733
|
broken registry or registered ledger exits non-zero with actionable errors.
|
|
@@ -1310,8 +1736,8 @@ broken registry or registered ledger exits non-zero with actionable errors.
|
|
|
1310
1736
|
}
|
|
1311
1737
|
if (command === "status") {
|
|
1312
1738
|
process.stdout.write(`Usage:
|
|
1313
|
-
artshelf status [--ledger <path>] [--json]
|
|
1314
|
-
artshelf status --all [--registry <path>] [--json]
|
|
1739
|
+
artshelf status [--ledger <path>] [--json|--agent]
|
|
1740
|
+
artshelf status --all [--registry <path>] [--json|--agent]
|
|
1315
1741
|
|
|
1316
1742
|
Status is the lightweight daily "what is going on?" view. Without --all, it
|
|
1317
1743
|
reports counts for the selected or default ledger only. With --all, it adds
|
|
@@ -1319,10 +1745,16 @@ registry health, total ledgers, and aggregated counts across registered ledgers.
|
|
|
1319
1745
|
Counts include active artifacts, kept, due, manual-review, missing-path, and
|
|
1320
1746
|
pending cleanup entries.
|
|
1321
1747
|
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
--
|
|
1748
|
+
Render modes:
|
|
1749
|
+
(default) Human summary, short enough to paste into a chat.
|
|
1750
|
+
--json Full audit report (backward-compatible; suitable for cron/reporting).
|
|
1751
|
+
--agent Compact single-line JSON decision packet for agents: health, counts,
|
|
1752
|
+
attention categories, blockers, next action, and a verify command.
|
|
1753
|
+
Token-efficient; --agent takes precedence over --json.
|
|
1754
|
+
|
|
1755
|
+
Status is read-only: it never creates plans or receipts and never mutates
|
|
1756
|
+
records. A healthy selected ledger exits 0; with --all, a broken registry or any
|
|
1757
|
+
stale or invalid registered ledger exits non-zero.
|
|
1326
1758
|
`);
|
|
1327
1759
|
return;
|
|
1328
1760
|
}
|
|
@@ -1341,50 +1773,99 @@ installs should update by pulling, rebuilding, and linking the checkout.
|
|
|
1341
1773
|
`);
|
|
1342
1774
|
return;
|
|
1343
1775
|
}
|
|
1344
|
-
|
|
1776
|
+
if (command === "due") {
|
|
1777
|
+
process.stdout.write(`Usage:
|
|
1778
|
+
artshelf due [--ledger <path>] [--json]
|
|
1779
|
+
artshelf due --all [--registry <path>] [--json]
|
|
1345
1780
|
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
artshelf
|
|
1355
|
-
artshelf
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
artshelf
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1781
|
+
Due lists records whose retention has elapsed or that need attention: due,
|
|
1782
|
+
manual-review, and missing-path entries. Kept entries are hidden in human output.
|
|
1783
|
+
Due is read-only and never moves files or writes plans.
|
|
1784
|
+
`);
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
if (command === "validate") {
|
|
1788
|
+
process.stdout.write(`Usage:
|
|
1789
|
+
artshelf validate [--ledger <path>] [--json]
|
|
1790
|
+
artshelf validate --all [--registry <path>] [--json]
|
|
1791
|
+
|
|
1792
|
+
Validate checks ledger shape and reports errors and warnings, such as records
|
|
1793
|
+
that point at missing artifact paths, without changing anything. A clean ledger
|
|
1794
|
+
exits 0; shape errors exit non-zero. With --all it validates every registered
|
|
1795
|
+
ledger.
|
|
1796
|
+
`);
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
if (command === "trash list") {
|
|
1800
|
+
process.stdout.write(`Usage:
|
|
1801
|
+
artshelf trash list [--ledger <path>] [--all] [--registry <path>] [--json]
|
|
1802
|
+
|
|
1803
|
+
Options:
|
|
1804
|
+
--ledger <path> Use a specific ledger file
|
|
1805
|
+
--all Include records from all registered ledgers
|
|
1806
|
+
--registry <path> Registry path used with --all
|
|
1807
|
+
--json Emit machine-readable output
|
|
1808
|
+
|
|
1809
|
+
Trash list shows records currently held in Artshelf trash without deleting anything.
|
|
1810
|
+
With --all it reports trashed records across every registered ledger.
|
|
1811
|
+
`);
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
if (command === "trash purge") {
|
|
1815
|
+
process.stdout.write(`Usage:
|
|
1371
1816
|
artshelf trash purge --older-than <ttl> --dry-run [--ledger <path>] [--json]
|
|
1372
1817
|
artshelf trash purge --execute --plan-id <id> [--ledger <path>] [--json]
|
|
1373
|
-
artshelf resolve <id> --status resolved --reason <text> [--json]
|
|
1374
1818
|
|
|
1375
|
-
|
|
1376
|
-
--
|
|
1377
|
-
--
|
|
1378
|
-
--
|
|
1379
|
-
--
|
|
1380
|
-
--
|
|
1381
|
-
--
|
|
1819
|
+
Options:
|
|
1820
|
+
--older-than <ttl> Purge trashed records older than this duration
|
|
1821
|
+
--dry-run Build a reviewed purge plan and output a plan id
|
|
1822
|
+
--execute Execute a reviewed purge plan
|
|
1823
|
+
--plan-id <id> Execute only this reviewed purge plan
|
|
1824
|
+
--ledger <path> Target one specific ledger
|
|
1825
|
+
--json Emit machine-readable output
|
|
1382
1826
|
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1827
|
+
Trash purge permanently deletes aged trash from a reviewed plan. --dry-run turns
|
|
1828
|
+
--older-than into a reviewed purge plan id; --execute deletes only that one reviewed
|
|
1829
|
+
plan id. Purge is always scoped to one --ledger; --all is not supported for purge.
|
|
1830
|
+
Completed receipts are refused on repeat execute; an interrupted purge may be resumed
|
|
1831
|
+
and reconciled.
|
|
1387
1832
|
`);
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
if (command === "ledgers list") {
|
|
1836
|
+
process.stdout.write(`Usage:
|
|
1837
|
+
artshelf ledgers list [--plain] [--registry <path>] [--json]
|
|
1838
|
+
|
|
1839
|
+
Options:
|
|
1840
|
+
--plain Skip ledger validation and list registrations directly
|
|
1841
|
+
--registry <path> Registry path to use
|
|
1842
|
+
--json Emit machine-readable output
|
|
1843
|
+
|
|
1844
|
+
Ledgers list validates every registered ledger and reports ok/missing/invalid
|
|
1845
|
+
status, entry counts, and warnings so agents can spot stale registry entries
|
|
1846
|
+
without a separate validate pass. Use --plain for the fast path that lists
|
|
1847
|
+
registered ledgers without reading them. It exits non-zero when the registry or
|
|
1848
|
+
any registered ledger is broken.
|
|
1849
|
+
`);
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
if (command === "ledgers add") {
|
|
1853
|
+
process.stdout.write(`Usage:
|
|
1854
|
+
artshelf ledgers add --ledger <path> [--name <name>] [--scope repo|user|other] [--registry <path>] [--json]
|
|
1855
|
+
|
|
1856
|
+
Options:
|
|
1857
|
+
--ledger <path> Register this ledger file
|
|
1858
|
+
--name <name> Override the ledger display name
|
|
1859
|
+
--scope <scope> Registry scope: repo, user, or other
|
|
1860
|
+
--registry <path> Registry path to update
|
|
1861
|
+
--json Emit machine-readable output
|
|
1862
|
+
|
|
1863
|
+
Ledgers add registers an existing ledger file in the global registry so --all
|
|
1864
|
+
commands and the registry index can find it. The ledger file must already exist.
|
|
1865
|
+
`);
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
process.stdout.write(renderTopLevelHelp());
|
|
1388
1869
|
}
|
|
1389
1870
|
main(process.argv.slice(2))
|
|
1390
1871
|
.then((status) => {
|