artshelf 0.10.0 → 0.10.2
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 +32 -0
- package/README.md +8 -6
- package/SPEC.md +16 -7
- package/dist/src/adapters/process.js +7 -0
- package/dist/src/adapters/update.js +143 -0
- package/dist/src/cli.js +44 -1831
- package/dist/src/commands/cleanup.js +52 -0
- package/dist/src/commands/doctor.js +79 -0
- package/dist/src/commands/due.js +32 -0
- package/dist/src/commands/find.js +44 -0
- package/dist/src/commands/get.js +31 -0
- package/dist/src/commands/index.js +69 -0
- package/dist/src/commands/ledgers.js +111 -0
- package/dist/src/commands/list.js +36 -0
- package/dist/src/commands/put.js +36 -0
- package/dist/src/commands/resolve.js +17 -0
- package/dist/src/commands/review.js +38 -0
- package/dist/src/commands/shared.js +160 -0
- package/dist/src/commands/status.js +101 -0
- package/dist/src/commands/trash.js +78 -0
- package/dist/src/commands/update.js +75 -0
- package/dist/src/commands/validate.js +35 -0
- package/dist/src/config/env.js +24 -0
- package/dist/src/config/package.js +17 -0
- package/dist/src/config/paths.js +5 -0
- package/dist/src/renderers/attention.js +3 -0
- package/dist/src/renderers/doctor.js +64 -0
- package/dist/src/renderers/json.js +10 -0
- package/dist/src/renderers/review.js +159 -0
- package/dist/src/renderers/status.js +112 -0
- package/dist/src/shared/cli-types.js +1 -0
- package/dist/src/shared/errors.js +4 -0
- package/dist/src/shared/flags.js +41 -0
- package/dist/src/shared/help-text.js +355 -0
- package/docs/agent-usage.html +1 -1
- package/docs/agent-usage.md +2 -2
- package/docs/reference.html +12 -6
- package/package.json +1 -1
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { attentionGlyph } from "./attention.js";
|
|
2
|
+
import { statusCommand } from "./status.js";
|
|
3
|
+
const REVIEW_SAFETY = {
|
|
4
|
+
dryRunOnly: true,
|
|
5
|
+
executeAllRefused: true,
|
|
6
|
+
noExecuteRan: true,
|
|
7
|
+
noResolveRan: true,
|
|
8
|
+
noDeleteRan: true
|
|
9
|
+
};
|
|
10
|
+
export function reviewNextAction(summary, scope, ledgerPath) {
|
|
11
|
+
const broken = summary.invalid + summary.stale;
|
|
12
|
+
const review = statusCommand(scope, "review", ledgerPath);
|
|
13
|
+
if (broken > 0) {
|
|
14
|
+
const repair = scope === "all" ? "re-register or fix the file" : "fix the file";
|
|
15
|
+
return `repair ${broken} broken ledger(s) above (${repair}), then re-run \`${review}\``;
|
|
16
|
+
}
|
|
17
|
+
if (summary.executable > 0) {
|
|
18
|
+
const dryRun = scope === "all" ? "artshelf cleanup --dry-run --all" : `artshelf cleanup --dry-run${ledgerPath ? ` --ledger ${ledgerPath}` : ""}`;
|
|
19
|
+
return `run \`${dryRun}\` to generate plans, then \`artshelf cleanup --execute --plan-id <id> --ledger <path>\` for each reviewed plan`;
|
|
20
|
+
}
|
|
21
|
+
if (summary.missingPath > 0) {
|
|
22
|
+
return "inspect missing-path entries and `artshelf resolve` the ones no longer needed; nothing is auto-executable";
|
|
23
|
+
}
|
|
24
|
+
return "nothing to do — no broken ledgers and no due, manual-review, missing-path, or executable cleanup entries";
|
|
25
|
+
}
|
|
26
|
+
export function printReviewAll(results, summary, nextAction, registryPath) {
|
|
27
|
+
const needsAttention = summary.invalid + summary.stale + summary.executable + summary.due + summary.manualReview + summary.missingPath > 0;
|
|
28
|
+
process.stdout.write(`${attentionGlyph(needsAttention)} artshelf review --all: ${needsAttention ? "needs attention" : "all clear"}\n`);
|
|
29
|
+
process.stdout.write(`registry: ${registryPath} — ${summary.ledgers} ledgers (${summary.ok} ok, ${summary.invalid} invalid, ${summary.stale} stale)\n`);
|
|
30
|
+
printReview(results);
|
|
31
|
+
process.stdout.write(`triage: due ${summary.due} · manual-review ${summary.manualReview} · missing ${summary.missingPath} · executable ${summary.executable} · skipped ${summary.skipped}\n`);
|
|
32
|
+
process.stdout.write(`next: ${nextAction}\n`);
|
|
33
|
+
}
|
|
34
|
+
export function printReview(results) {
|
|
35
|
+
for (const result of results) {
|
|
36
|
+
const visibleDue = result.due.filter((entry) => entry.dueStatus !== "kept");
|
|
37
|
+
const needsAttention = !result.validate.ok || visibleDue.length > 0 || result.plan.entries.length > 0;
|
|
38
|
+
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`);
|
|
39
|
+
process.stdout.write(`due/manual/missing: ${visibleDue.length}; plan ${result.plan.planId}: ${result.plan.entries.length} entries, ${result.plan.skipped.length} skipped\n`);
|
|
40
|
+
process.stdout.write(`ledger: ${result.ledger.path}\n`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function buildReviewDecisions(results, scope) {
|
|
44
|
+
const readyForApproval = [];
|
|
45
|
+
const needsReviewFirst = [];
|
|
46
|
+
const blocked = [];
|
|
47
|
+
const review = scope === "all" ? "artshelf review --all" : "artshelf review";
|
|
48
|
+
for (const result of results) {
|
|
49
|
+
const { ledger, validate, due } = result;
|
|
50
|
+
if (!validate.ok) {
|
|
51
|
+
const status = result.ledgerExists ? "invalid" : "missing";
|
|
52
|
+
const repair = scope === "all" ? `re-register or fix ${ledger.path}` : `fix ${ledger.path}`;
|
|
53
|
+
blocked.push({
|
|
54
|
+
label: `Repair ${ledger.name} ledger (${status})`,
|
|
55
|
+
itemIds: [],
|
|
56
|
+
actionType: "fix-registry",
|
|
57
|
+
approvalTarget: null,
|
|
58
|
+
reason: validate.errors[0] ?? `${scope === "all" ? "registered ledger" : "ledger"} is ${status}`,
|
|
59
|
+
nextStep: `${repair}, then re-run \`${review}\``
|
|
60
|
+
});
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const missingPath = due.filter((entry) => entry.dueStatus === "missing-path");
|
|
64
|
+
const trashSafe = due.filter((entry) => entry.dueStatus === "due" && entry.cleanup === "trash");
|
|
65
|
+
const inspectItems = due.filter((entry) => entry.dueStatus === "manual-review" ||
|
|
66
|
+
(entry.dueStatus === "due" && (entry.cleanup === "review" || entry.cleanup === "delete")));
|
|
67
|
+
if (missingPath.length > 0) {
|
|
68
|
+
const ids = missingPath.map((entry) => entry.id).sort();
|
|
69
|
+
readyForApproval.push({
|
|
70
|
+
label: `Resolve ${ids.length} missing-path record(s) in ${ledger.name}`,
|
|
71
|
+
itemIds: ids,
|
|
72
|
+
actionType: "resolve-missing",
|
|
73
|
+
approvalTarget: `approve artshelf resolve missing ledger ${ledger.path} ids ${ids.join(" ")}`,
|
|
74
|
+
reason: "the recorded path is already missing",
|
|
75
|
+
nextStep: "confirm the artifact is no longer needed, then approve the ledger-only resolve"
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (trashSafe.length > 0) {
|
|
79
|
+
const ids = trashSafe.map((entry) => entry.id).sort();
|
|
80
|
+
needsReviewFirst.push({
|
|
81
|
+
label: `Plan cleanup for ${ids.length} trash-eligible artifact(s) in ${ledger.name}`,
|
|
82
|
+
itemIds: ids,
|
|
83
|
+
actionType: "cleanup",
|
|
84
|
+
approvalTarget: null,
|
|
85
|
+
reason: "disposable artifacts are due but no reviewed cleanup plan exists yet",
|
|
86
|
+
nextStep: `run \`artshelf cleanup --dry-run --ledger ${ledger.path} --json\`, then approve \`approve artshelf cleanup ledger ${ledger.path} plan <plan-id>\``
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (inspectItems.length > 0) {
|
|
90
|
+
const ids = inspectItems.map((entry) => entry.id).sort();
|
|
91
|
+
const hasDelete = inspectItems.some((entry) => entry.cleanup === "delete");
|
|
92
|
+
needsReviewFirst.push({
|
|
93
|
+
label: `Inspect ${ids.length} record(s) in ${ledger.name} before cleanup`,
|
|
94
|
+
itemIds: ids,
|
|
95
|
+
actionType: "inspect",
|
|
96
|
+
approvalTarget: null,
|
|
97
|
+
reason: hasDelete
|
|
98
|
+
? "records need manual review; cleanup=delete is refused and never deletes files"
|
|
99
|
+
: "records are held for manual review before any cleanup",
|
|
100
|
+
nextStep: "inspect each path, then keep, change retention, resolve, or set cleanup=trash and plan a cleanup"
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { readyForApproval, needsReviewFirst, blocked };
|
|
105
|
+
}
|
|
106
|
+
function reviewCounts(summary) {
|
|
107
|
+
return {
|
|
108
|
+
due: summary.due,
|
|
109
|
+
manualReview: summary.manualReview,
|
|
110
|
+
missingPath: summary.missingPath,
|
|
111
|
+
executable: summary.executable,
|
|
112
|
+
skipped: summary.skipped
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
export function buildReviewAgentPacketAll(results, summary, registry) {
|
|
116
|
+
const groups = buildReviewDecisions(results, "all");
|
|
117
|
+
return {
|
|
118
|
+
schemaVersion: 1,
|
|
119
|
+
command: "review",
|
|
120
|
+
scope: "all",
|
|
121
|
+
health: summary.invalid + summary.stale > 0 ? "attention" : "ok",
|
|
122
|
+
registry,
|
|
123
|
+
ledgers: { total: summary.ledgers, ok: summary.ok, stale: summary.stale, invalid: summary.invalid },
|
|
124
|
+
counts: reviewCounts(summary),
|
|
125
|
+
decisionSummary: {
|
|
126
|
+
readyForApproval: groups.readyForApproval.length,
|
|
127
|
+
needsReviewFirst: groups.needsReviewFirst.length,
|
|
128
|
+
blocked: groups.blocked.length
|
|
129
|
+
},
|
|
130
|
+
readyForApproval: groups.readyForApproval,
|
|
131
|
+
needsReviewFirst: groups.needsReviewFirst,
|
|
132
|
+
blocked: groups.blocked,
|
|
133
|
+
safety: REVIEW_SAFETY,
|
|
134
|
+
nextAction: reviewNextAction(summary, "all"),
|
|
135
|
+
verification: `artshelf review --all --agent --registry ${registry.path}`
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
export function buildReviewAgentPacketSingle(result, summary, ledgerPath) {
|
|
139
|
+
const groups = buildReviewDecisions([result], "single");
|
|
140
|
+
return {
|
|
141
|
+
schemaVersion: 1,
|
|
142
|
+
command: "review",
|
|
143
|
+
scope: "single",
|
|
144
|
+
health: summary.invalid + summary.stale > 0 ? "attention" : "ok",
|
|
145
|
+
ledgerPath,
|
|
146
|
+
counts: reviewCounts(summary),
|
|
147
|
+
decisionSummary: {
|
|
148
|
+
readyForApproval: groups.readyForApproval.length,
|
|
149
|
+
needsReviewFirst: groups.needsReviewFirst.length,
|
|
150
|
+
blocked: groups.blocked.length
|
|
151
|
+
},
|
|
152
|
+
readyForApproval: groups.readyForApproval,
|
|
153
|
+
needsReviewFirst: groups.needsReviewFirst,
|
|
154
|
+
blocked: groups.blocked,
|
|
155
|
+
safety: REVIEW_SAFETY,
|
|
156
|
+
nextAction: reviewNextAction(summary, "single", ledgerPath),
|
|
157
|
+
verification: `artshelf review --agent --ledger ${ledgerPath}`
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { attentionGlyph } from "./attention.js";
|
|
2
|
+
const STATUS_ATTENTION_CATEGORIES = ["due", "manualReview", "missingPath", "pendingCleanup"];
|
|
3
|
+
export function emptyStatusCounts() {
|
|
4
|
+
return { active: 0, due: 0, manualReview: 0, missingPath: 0, kept: 0, pendingCleanup: 0 };
|
|
5
|
+
}
|
|
6
|
+
export function sumStatusCounts(ledgers, key) {
|
|
7
|
+
return ledgers.reduce((total, ledger) => total + ledger.counts[key], 0);
|
|
8
|
+
}
|
|
9
|
+
export function statusAttention(counts) {
|
|
10
|
+
return STATUS_ATTENTION_CATEGORIES.filter((key) => counts[key] > 0);
|
|
11
|
+
}
|
|
12
|
+
export function statusCommand(scope, command, ledgerPath) {
|
|
13
|
+
if (scope === "all")
|
|
14
|
+
return `artshelf ${command} --all`;
|
|
15
|
+
return ledgerPath ? `artshelf ${command} --ledger ${ledgerPath}` : `artshelf ${command}`;
|
|
16
|
+
}
|
|
17
|
+
function statusNextAction(blockers, counts, scope, ledgerPath) {
|
|
18
|
+
if (blockers.length > 0) {
|
|
19
|
+
const verify = statusCommand(scope, "status", ledgerPath);
|
|
20
|
+
return `repair ${blockers.length} broken ledger(s) above, then re-run \`${verify}\``;
|
|
21
|
+
}
|
|
22
|
+
const review = statusCommand(scope, "review", ledgerPath);
|
|
23
|
+
if (counts.pendingCleanup > 0 || counts.due > 0) {
|
|
24
|
+
return `run \`${review}\` to preview cleanup plans; nothing is auto-executed`;
|
|
25
|
+
}
|
|
26
|
+
if (counts.manualReview > 0) {
|
|
27
|
+
return `run \`${review}\` to inspect manual-review records; nothing is auto-executed`;
|
|
28
|
+
}
|
|
29
|
+
if (counts.missingPath > 0) {
|
|
30
|
+
return "inspect missing-path records and `artshelf resolve` the ones no longer needed; nothing is auto-executable";
|
|
31
|
+
}
|
|
32
|
+
return "nothing due — no broken ledgers and no due, manual-review, missing-path, or pending cleanup entries";
|
|
33
|
+
}
|
|
34
|
+
export function buildStatusAgentPacketAll(report) {
|
|
35
|
+
const blockers = [];
|
|
36
|
+
if (report.registryError)
|
|
37
|
+
blockers.push(`registry unreadable: ${report.registryError}`);
|
|
38
|
+
for (const ledger of report.ledgers) {
|
|
39
|
+
if (ledger.status !== "ok") {
|
|
40
|
+
blockers.push(`${ledger.name} ${ledger.status}${ledger.errors.length ? `: ${ledger.errors[0]}` : ""}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const counts = {
|
|
44
|
+
active: report.totals.active,
|
|
45
|
+
due: report.totals.due,
|
|
46
|
+
manualReview: report.totals.manualReview,
|
|
47
|
+
missingPath: report.totals.missingPath,
|
|
48
|
+
kept: report.totals.kept,
|
|
49
|
+
pendingCleanup: report.totals.pendingCleanup
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
schemaVersion: 1,
|
|
53
|
+
command: "status",
|
|
54
|
+
scope: "all",
|
|
55
|
+
health: report.ok ? "ok" : "attention",
|
|
56
|
+
registry: { path: report.registryPath, exists: report.registryExists, ok: report.registryOk, error: report.registryError },
|
|
57
|
+
ledgers: { total: report.totals.ledgers, ok: report.totals.ok, stale: report.totals.stale, invalid: report.totals.invalid },
|
|
58
|
+
counts,
|
|
59
|
+
attention: statusAttention(counts),
|
|
60
|
+
blockers,
|
|
61
|
+
nextAction: statusNextAction(blockers, counts, "all"),
|
|
62
|
+
verification: `artshelf status --all --agent --registry ${report.registryPath}`
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function buildStatusAgentPacketSingle(ledger, ledgerPath) {
|
|
66
|
+
const blockers = ledger.ok
|
|
67
|
+
? []
|
|
68
|
+
: [`${ledger.status}${ledger.errors.length ? `: ${ledger.errors[0]}` : ""}`];
|
|
69
|
+
return {
|
|
70
|
+
schemaVersion: 1,
|
|
71
|
+
command: "status",
|
|
72
|
+
scope: "single",
|
|
73
|
+
health: ledger.ok ? "ok" : "attention",
|
|
74
|
+
ledgerPath,
|
|
75
|
+
counts: ledger.counts,
|
|
76
|
+
attention: statusAttention(ledger.counts),
|
|
77
|
+
blockers,
|
|
78
|
+
nextAction: statusNextAction(blockers, ledger.counts, "single", ledgerPath),
|
|
79
|
+
verification: `artshelf status --agent --ledger ${ledgerPath}`
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export function printStatusAll(report) {
|
|
83
|
+
const anyActionable = report.ledgers.some((ledger) => statusAttention(ledger.counts).length > 0);
|
|
84
|
+
process.stdout.write(`${attentionGlyph(!report.ok || anyActionable)} artshelf status: ${report.ok ? "ok" : "needs attention"}\n`);
|
|
85
|
+
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`);
|
|
86
|
+
if (report.registryError)
|
|
87
|
+
process.stdout.write(`registry error: ${report.registryError}\n`);
|
|
88
|
+
for (const ledger of report.ledgers) {
|
|
89
|
+
if (ledger.status === "ok") {
|
|
90
|
+
process.stdout.write(`${attentionGlyph(statusAttention(ledger.counts).length > 0)} [${ledger.name}] ${formatStatusCounts(ledger.counts)}\n`);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
process.stdout.write(`⚠ [${ledger.name}] ${ledger.status}: ${ledger.errors.join("; ")}\n`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
process.stdout.write(`total: ${formatStatusCounts(report.totals)}\n`);
|
|
97
|
+
}
|
|
98
|
+
export function printStatusSingle(ledger) {
|
|
99
|
+
const needsAttention = !ledger.ok || statusAttention(ledger.counts).length > 0;
|
|
100
|
+
process.stdout.write(`${attentionGlyph(needsAttention)} artshelf status: ${ledger.ok ? "ok" : ledger.status}\n`);
|
|
101
|
+
process.stdout.write(`ledger: ${ledger.path}\n`);
|
|
102
|
+
if (ledger.ok) {
|
|
103
|
+
process.stdout.write(`${formatStatusCounts(ledger.counts)}\n`);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
for (const message of ledger.errors)
|
|
107
|
+
process.stdout.write(`error: ${message}\n`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function formatStatusCounts(counts) {
|
|
111
|
+
return `active ${counts.active} · due ${counts.due} · manual-review ${counts.manualReview} · missing ${counts.missingPath} · kept ${counts.kept} · pending ${counts.pendingCleanup}`;
|
|
112
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const BOOLEAN_FLAGS = new Set(["all", "json", "agent", "manual-review", "dry-run", "execute", "help", "version", "plain"]);
|
|
2
|
+
export const VALUE_FLAGS = new Set([
|
|
3
|
+
"cleanup",
|
|
4
|
+
"kind",
|
|
5
|
+
"label",
|
|
6
|
+
"ledger",
|
|
7
|
+
"name",
|
|
8
|
+
"owner",
|
|
9
|
+
"path",
|
|
10
|
+
"plan-id",
|
|
11
|
+
"older-than",
|
|
12
|
+
"registry",
|
|
13
|
+
"reason",
|
|
14
|
+
"retain-until",
|
|
15
|
+
"scope",
|
|
16
|
+
"status",
|
|
17
|
+
"ttl"
|
|
18
|
+
]);
|
|
19
|
+
export function requiredStringFlag(parsed, name) {
|
|
20
|
+
const value = stringFlag(parsed, name);
|
|
21
|
+
if (!value)
|
|
22
|
+
throw new Error(`Missing required --${name}`);
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
export function stringFlag(parsed, name) {
|
|
26
|
+
const value = parsed.flags.get(name);
|
|
27
|
+
if (Array.isArray(value))
|
|
28
|
+
return value[value.length - 1];
|
|
29
|
+
return typeof value === "string" ? value : undefined;
|
|
30
|
+
}
|
|
31
|
+
export function boolFlag(parsed, name) {
|
|
32
|
+
return parsed.flags.get(name) === true;
|
|
33
|
+
}
|
|
34
|
+
export function arrayFlag(parsed, name) {
|
|
35
|
+
const value = parsed.flags.get(name);
|
|
36
|
+
if (Array.isArray(value))
|
|
37
|
+
return value;
|
|
38
|
+
if (typeof value === "string")
|
|
39
|
+
return [value];
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
export const LEDGERS_HELP = `Manage the ledger registry.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
artshelf ledgers [command]
|
|
5
|
+
|
|
6
|
+
Available Commands:
|
|
7
|
+
list List and validate registered ledgers
|
|
8
|
+
add Register an existing ledger file
|
|
9
|
+
|
|
10
|
+
Flags:
|
|
11
|
+
-h, --help help for ledgers
|
|
12
|
+
|
|
13
|
+
Use "artshelf ledgers <command> --help" for more information about a command.
|
|
14
|
+
`;
|
|
15
|
+
export const TRASH_HELP = `Inspect and purge Artshelf trash.
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
artshelf trash [command]
|
|
19
|
+
|
|
20
|
+
Available Commands:
|
|
21
|
+
list List records currently held in Artshelf trash
|
|
22
|
+
purge Plan or execute approved permanent trash deletion
|
|
23
|
+
|
|
24
|
+
Flags:
|
|
25
|
+
-h, --help help for trash
|
|
26
|
+
|
|
27
|
+
Use "artshelf trash <command> --help" for more information about a command.
|
|
28
|
+
`;
|
|
29
|
+
const COMMAND_GROUPS = [
|
|
30
|
+
{
|
|
31
|
+
group: "Create",
|
|
32
|
+
commands: [{ name: "put", summary: "Record an artifact with a reason and retention" }]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
group: "Inspect",
|
|
36
|
+
commands: [
|
|
37
|
+
{ name: "list", summary: "List ledger records" },
|
|
38
|
+
{ name: "find", summary: "Find records by path, owner, label, or status" },
|
|
39
|
+
{ name: "get", summary: "Show one record by id" },
|
|
40
|
+
{ name: "due", summary: "Show due, manual-review, and missing-path records" },
|
|
41
|
+
{ name: "status", summary: "Summarize ledger and registry counts" }
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
group: "Review",
|
|
46
|
+
commands: [
|
|
47
|
+
{ name: "validate", summary: "Check ledger shape and report warnings" },
|
|
48
|
+
{ name: "review", summary: "Preview validate, due, and cleanup plans (read-only)" }
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
group: "Clean",
|
|
53
|
+
commands: [
|
|
54
|
+
{ name: "cleanup", summary: "Plan and execute approved cleanups" },
|
|
55
|
+
{ name: "trash", summary: "Inspect and purge Artshelf trash" },
|
|
56
|
+
{ name: "resolve", summary: "Mark a record manually resolved" }
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
group: "System",
|
|
61
|
+
commands: [
|
|
62
|
+
{ name: "ledgers", summary: "Manage the ledger registry" },
|
|
63
|
+
{ name: "doctor", summary: "Report Artshelf health on this machine" },
|
|
64
|
+
{ name: "update", summary: "Update the Artshelf CLI" }
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
];
|
|
68
|
+
const NESTED_HELP = new Map([
|
|
69
|
+
["trash", new Set(["list", "purge"])],
|
|
70
|
+
["ledgers", new Set(["list", "add"])]
|
|
71
|
+
]);
|
|
72
|
+
export function resolveHelpKey(parsed) {
|
|
73
|
+
if (parsed.command === "help") {
|
|
74
|
+
return joinHelpKey(parsed.positionals[0], parsed.positionals[1]);
|
|
75
|
+
}
|
|
76
|
+
if (!parsed.command || parsed.command === "--help" || parsed.command === "-h") {
|
|
77
|
+
return "";
|
|
78
|
+
}
|
|
79
|
+
return joinHelpKey(parsed.command, parsed.positionals[0]);
|
|
80
|
+
}
|
|
81
|
+
export function renderHelp(command, version) {
|
|
82
|
+
if (command === "put") {
|
|
83
|
+
return `Usage:
|
|
84
|
+
artshelf put <path> --reason <text> (--ttl <ttl>|--retain-until <date>|--manual-review) [options]
|
|
85
|
+
|
|
86
|
+
Options:
|
|
87
|
+
--kind scratch|backup|run-artifact|evidence|cache|quarantine|other
|
|
88
|
+
--cleanup trash|review|delete (cleanup=delete is refused; trash purge needs a reviewed plan)
|
|
89
|
+
--owner <name>
|
|
90
|
+
--label <label> Repeatable
|
|
91
|
+
--ledger <path>
|
|
92
|
+
--registry <path>
|
|
93
|
+
--json
|
|
94
|
+
`;
|
|
95
|
+
}
|
|
96
|
+
if (command === "cleanup") {
|
|
97
|
+
return `Usage:
|
|
98
|
+
artshelf cleanup --dry-run [--ledger <path>] [--json]
|
|
99
|
+
artshelf cleanup --dry-run --all [--registry <path>] [--json]
|
|
100
|
+
artshelf cleanup --execute --plan-id <id> [--ledger <path>] [--json]
|
|
101
|
+
|
|
102
|
+
Cleanup execution is approval-only. There is no daemon, no auto-execute, and no
|
|
103
|
+
global execute path: review a dry-run plan, then execute that one reviewed plan id.
|
|
104
|
+
Cleanup is ledger-first. Execute never computes a fresh live set; it only uses a reviewed plan id.
|
|
105
|
+
cleanup=delete records cleanup-refused instead of deleting files; physical trash purge needs a separate reviewed plan.
|
|
106
|
+
Dry-run writes and registers a plan only when executable cleanup entries exist; no-op dry-runs report not-created.
|
|
107
|
+
Matching dry-runs reuse the existing plan id and refresh its Artshelf-owned plan artifact.
|
|
108
|
+
Execute writes and registers an Artshelf-owned receipt artifact.
|
|
109
|
+
Global --all mode is dry-run only.
|
|
110
|
+
`;
|
|
111
|
+
}
|
|
112
|
+
if (command === "trash")
|
|
113
|
+
return TRASH_HELP;
|
|
114
|
+
if (command === "ledgers")
|
|
115
|
+
return LEDGERS_HELP;
|
|
116
|
+
if (command === "list") {
|
|
117
|
+
return `Usage:
|
|
118
|
+
artshelf list [--status <status>] [--ledger <path>] [--json]
|
|
119
|
+
artshelf list --all [--status <status>] [--registry <path>] [--json]
|
|
120
|
+
|
|
121
|
+
Statuses:
|
|
122
|
+
active, review-required, trashed, cleanup-refused, resolved
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
125
|
+
if (command === "find") {
|
|
126
|
+
return `Usage:
|
|
127
|
+
artshelf find (--path <path>|--owner <name>|--label <label>|--status <status>) [options]
|
|
128
|
+
artshelf find --all (--path <path>|--owner <name>|--label <label>|--status <status>) [options]
|
|
129
|
+
|
|
130
|
+
Options:
|
|
131
|
+
--path <path> Match an exact artifact path after path normalization
|
|
132
|
+
--owner <name>
|
|
133
|
+
--label <label> Repeatable; all labels must match
|
|
134
|
+
--status <status>
|
|
135
|
+
--ledger <path>
|
|
136
|
+
--registry <path>
|
|
137
|
+
--json
|
|
138
|
+
|
|
139
|
+
Find is read-only. Use it before put when an integration needs idempotent artifact registration.
|
|
140
|
+
`;
|
|
141
|
+
}
|
|
142
|
+
if (command === "get") {
|
|
143
|
+
return `Usage:
|
|
144
|
+
artshelf get <id> [--ledger <path>] [--json]
|
|
145
|
+
artshelf get <id> --all [--registry <path>] [--json]
|
|
146
|
+
|
|
147
|
+
Get is read-only and returns one ledger record by Artshelf id.
|
|
148
|
+
`;
|
|
149
|
+
}
|
|
150
|
+
if (command === "resolve") {
|
|
151
|
+
return `Usage:
|
|
152
|
+
artshelf resolve <id> --status resolved --reason <text> [--ledger <path>] [--json]
|
|
153
|
+
|
|
154
|
+
Resolve marks a handled, missing, or no-longer-needed record as manually resolved.
|
|
155
|
+
Resolved records stay in the audit trail but no longer participate in due or cleanup planning.
|
|
156
|
+
`;
|
|
157
|
+
}
|
|
158
|
+
if (command === "review") {
|
|
159
|
+
return `Usage:
|
|
160
|
+
artshelf review [--ledger <path>] [--json|--agent]
|
|
161
|
+
artshelf review --all [--registry <path>] [--json|--agent]
|
|
162
|
+
|
|
163
|
+
Review runs validate, due, and cleanup plan preview without moving files or
|
|
164
|
+
writing a plan. With --all, review adds aggregate triage counts and the next
|
|
165
|
+
safe action.
|
|
166
|
+
|
|
167
|
+
Render modes:
|
|
168
|
+
(default) Human summary of validation, triage counts, and the next safe action.
|
|
169
|
+
--json Full read-only audit report (backward-compatible).
|
|
170
|
+
--agent Compact single-line JSON decision packet for agents: health, triage
|
|
171
|
+
counts, and classified decision groups (ready for approval, needs
|
|
172
|
+
review first, blocked) with exact approval targets where they are
|
|
173
|
+
safe. Review is read-only, so cleanup approval targets are minted by
|
|
174
|
+
\`cleanup --dry-run\`, never leaked from a preview plan id.
|
|
175
|
+
Token-efficient; --agent takes precedence over --json.
|
|
176
|
+
`;
|
|
177
|
+
}
|
|
178
|
+
if (command === "doctor") {
|
|
179
|
+
return `Usage:
|
|
180
|
+
artshelf doctor [--registry <path>] [--ledger <path>] [--json|--agent]
|
|
181
|
+
|
|
182
|
+
Doctor reports whether Artshelf is healthy on this machine: CLI version, selected
|
|
183
|
+
or default ledger path, selected or global registry path, registered ledger health
|
|
184
|
+
(stale/missing/invalid), and the cleanup safety posture. Execute is scoped to one
|
|
185
|
+
selected or default ledger and still requires a reviewed plan id; --all execute
|
|
186
|
+
and cleanup=delete are refused, while physical trash purge requires a separate
|
|
187
|
+
reviewed purge plan.
|
|
188
|
+
|
|
189
|
+
Render modes:
|
|
190
|
+
(default) Human summary of machine health and cleanup safety.
|
|
191
|
+
--json Full audit report (backward-compatible; suitable for cron/reporting).
|
|
192
|
+
--agent Compact single-line JSON decision packet for agents: health, registry
|
|
193
|
+
and registered-ledger health, blockers, cleanup-safety posture, next
|
|
194
|
+
action, and a verify command. Token-efficient; --agent takes
|
|
195
|
+
precedence over --json.
|
|
196
|
+
|
|
197
|
+
Run it after install, when --all commands behave unexpectedly, or on a schedule to
|
|
198
|
+
catch stale registry entries. Doctor is read-only. A healthy machine exits 0; a
|
|
199
|
+
broken registry or registered ledger exits non-zero with actionable errors.
|
|
200
|
+
`;
|
|
201
|
+
}
|
|
202
|
+
if (command === "status") {
|
|
203
|
+
return `Usage:
|
|
204
|
+
artshelf status [--ledger <path>] [--json|--agent]
|
|
205
|
+
artshelf status --all [--registry <path>] [--json|--agent]
|
|
206
|
+
|
|
207
|
+
Status is the lightweight daily "what is going on?" view. Without --all, it
|
|
208
|
+
reports counts for the selected or default ledger only. With --all, it adds
|
|
209
|
+
registry health, total ledgers, and aggregated counts across registered ledgers.
|
|
210
|
+
Counts include active artifacts, kept, due, manual-review, missing-path, and
|
|
211
|
+
pending cleanup entries.
|
|
212
|
+
|
|
213
|
+
Render modes:
|
|
214
|
+
(default) Human summary, short enough to paste into a chat.
|
|
215
|
+
--json Full audit report (backward-compatible; suitable for cron/reporting).
|
|
216
|
+
--agent Compact single-line JSON decision packet for agents: health, counts,
|
|
217
|
+
attention categories, blockers, next action, and a verify command.
|
|
218
|
+
Token-efficient; --agent takes precedence over --json.
|
|
219
|
+
|
|
220
|
+
Status is read-only: it never creates plans or receipts and never mutates
|
|
221
|
+
records. A healthy selected ledger exits 0; with --all, a broken registry or any
|
|
222
|
+
stale or invalid registered ledger exits non-zero.
|
|
223
|
+
`;
|
|
224
|
+
}
|
|
225
|
+
if (command === "update") {
|
|
226
|
+
return `Usage:
|
|
227
|
+
artshelf update [--json]
|
|
228
|
+
|
|
229
|
+
Update checks compare the current CLI version with the latest published npm
|
|
230
|
+
version. Normal commands may print a non-blocking update notice to stderr when a
|
|
231
|
+
newer version is available. Run update to upgrade npm global installs only:
|
|
232
|
+
|
|
233
|
+
npm install -g artshelf@latest
|
|
234
|
+
|
|
235
|
+
pnpm global installs should update with pnpm add -g artshelf@latest; source
|
|
236
|
+
installs should update by pulling, rebuilding, and linking the checkout.
|
|
237
|
+
`;
|
|
238
|
+
}
|
|
239
|
+
if (command === "due") {
|
|
240
|
+
return `Usage:
|
|
241
|
+
artshelf due [--ledger <path>] [--json]
|
|
242
|
+
artshelf due --all [--registry <path>] [--json]
|
|
243
|
+
|
|
244
|
+
Due lists records whose retention has elapsed or that need attention: due,
|
|
245
|
+
manual-review, and missing-path entries. Kept entries are hidden in human output.
|
|
246
|
+
Due is read-only and never moves files or writes plans.
|
|
247
|
+
`;
|
|
248
|
+
}
|
|
249
|
+
if (command === "validate") {
|
|
250
|
+
return `Usage:
|
|
251
|
+
artshelf validate [--ledger <path>] [--json]
|
|
252
|
+
artshelf validate --all [--registry <path>] [--json]
|
|
253
|
+
|
|
254
|
+
Validate checks ledger shape and reports errors and warnings, such as records
|
|
255
|
+
that point at missing artifact paths, without changing anything. A clean ledger
|
|
256
|
+
exits 0; shape errors exit non-zero. With --all it validates every registered
|
|
257
|
+
ledger.
|
|
258
|
+
`;
|
|
259
|
+
}
|
|
260
|
+
if (command === "trash list") {
|
|
261
|
+
return `Usage:
|
|
262
|
+
artshelf trash list [--ledger <path>] [--all] [--registry <path>] [--json]
|
|
263
|
+
|
|
264
|
+
Options:
|
|
265
|
+
--ledger <path> Use a specific ledger file
|
|
266
|
+
--all Include records from all registered ledgers
|
|
267
|
+
--registry <path> Registry path used with --all
|
|
268
|
+
--json Emit machine-readable output
|
|
269
|
+
|
|
270
|
+
Trash list shows records currently held in Artshelf trash without deleting anything.
|
|
271
|
+
With --all it reports trashed records across every registered ledger.
|
|
272
|
+
`;
|
|
273
|
+
}
|
|
274
|
+
if (command === "trash purge") {
|
|
275
|
+
return `Usage:
|
|
276
|
+
artshelf trash purge --older-than <ttl> --dry-run [--ledger <path>] [--json]
|
|
277
|
+
artshelf trash purge --execute --plan-id <id> [--ledger <path>] [--json]
|
|
278
|
+
|
|
279
|
+
Options:
|
|
280
|
+
--older-than <ttl> Purge trashed records older than this duration
|
|
281
|
+
--dry-run Build a reviewed purge plan and output a plan id
|
|
282
|
+
--execute Execute a reviewed purge plan
|
|
283
|
+
--plan-id <id> Execute only this reviewed purge plan
|
|
284
|
+
--ledger <path> Target one specific ledger
|
|
285
|
+
--json Emit machine-readable output
|
|
286
|
+
|
|
287
|
+
Trash purge permanently deletes aged trash from a reviewed plan. --dry-run turns
|
|
288
|
+
--older-than into a reviewed purge plan id; --execute deletes only that one reviewed
|
|
289
|
+
plan id. Purge is always scoped to one --ledger; --all is not supported for purge.
|
|
290
|
+
Completed receipts are refused on repeat execute; an interrupted purge may be resumed
|
|
291
|
+
and reconciled.
|
|
292
|
+
`;
|
|
293
|
+
}
|
|
294
|
+
if (command === "ledgers list") {
|
|
295
|
+
return `Usage:
|
|
296
|
+
artshelf ledgers list [--plain] [--registry <path>] [--json]
|
|
297
|
+
|
|
298
|
+
Options:
|
|
299
|
+
--plain Skip ledger validation and list registrations directly
|
|
300
|
+
--registry <path> Registry path to use
|
|
301
|
+
--json Emit machine-readable output
|
|
302
|
+
|
|
303
|
+
Ledgers list validates every registered ledger and reports ok/missing/invalid
|
|
304
|
+
status, entry counts, and warnings so agents can spot stale registry entries
|
|
305
|
+
without a separate validate pass. Use --plain for the fast path that lists
|
|
306
|
+
registered ledgers without reading them. It exits non-zero when the registry or
|
|
307
|
+
any registered ledger is broken.
|
|
308
|
+
`;
|
|
309
|
+
}
|
|
310
|
+
if (command === "ledgers add") {
|
|
311
|
+
return `Usage:
|
|
312
|
+
artshelf ledgers add --ledger <path> [--name <name>] [--scope repo|user|other] [--registry <path>] [--json]
|
|
313
|
+
|
|
314
|
+
Options:
|
|
315
|
+
--ledger <path> Register this ledger file
|
|
316
|
+
--name <name> Override the ledger display name
|
|
317
|
+
--scope <scope> Registry scope: repo, user, or other
|
|
318
|
+
--registry <path> Registry path to update
|
|
319
|
+
--json Emit machine-readable output
|
|
320
|
+
|
|
321
|
+
Ledgers add registers an existing ledger file in the global registry so --all
|
|
322
|
+
commands and the registry index can find it. The ledger file must already exist.
|
|
323
|
+
`;
|
|
324
|
+
}
|
|
325
|
+
return renderTopLevelHelp(version);
|
|
326
|
+
}
|
|
327
|
+
function joinHelpKey(command, subcommand) {
|
|
328
|
+
if (!command)
|
|
329
|
+
return "";
|
|
330
|
+
const subcommands = NESTED_HELP.get(command);
|
|
331
|
+
if (subcommands && subcommand && subcommands.has(subcommand)) {
|
|
332
|
+
return `${command} ${subcommand}`;
|
|
333
|
+
}
|
|
334
|
+
return command;
|
|
335
|
+
}
|
|
336
|
+
function renderTopLevelHelp(version) {
|
|
337
|
+
const names = COMMAND_GROUPS.flatMap((entry) => entry.commands.map((command) => command.name));
|
|
338
|
+
const width = Math.max(...names.map((name) => name.length)) + 2;
|
|
339
|
+
const lines = [
|
|
340
|
+
`Artshelf ${version} — approval-first retention for the temporary files agents leave behind.`,
|
|
341
|
+
"",
|
|
342
|
+
"Usage:",
|
|
343
|
+
" artshelf <command> [options]",
|
|
344
|
+
"",
|
|
345
|
+
"Available Commands:"
|
|
346
|
+
];
|
|
347
|
+
for (const { group, commands } of COMMAND_GROUPS) {
|
|
348
|
+
lines.push(` ${group}`);
|
|
349
|
+
for (const command of commands) {
|
|
350
|
+
lines.push(` ${command.name.padEnd(width)}${command.summary}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
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.`, "");
|
|
354
|
+
return lines.join("\n");
|
|
355
|
+
}
|