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.
Files changed (38) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +8 -6
  3. package/SPEC.md +16 -7
  4. package/dist/src/adapters/process.js +7 -0
  5. package/dist/src/adapters/update.js +143 -0
  6. package/dist/src/cli.js +44 -1831
  7. package/dist/src/commands/cleanup.js +52 -0
  8. package/dist/src/commands/doctor.js +79 -0
  9. package/dist/src/commands/due.js +32 -0
  10. package/dist/src/commands/find.js +44 -0
  11. package/dist/src/commands/get.js +31 -0
  12. package/dist/src/commands/index.js +69 -0
  13. package/dist/src/commands/ledgers.js +111 -0
  14. package/dist/src/commands/list.js +36 -0
  15. package/dist/src/commands/put.js +36 -0
  16. package/dist/src/commands/resolve.js +17 -0
  17. package/dist/src/commands/review.js +38 -0
  18. package/dist/src/commands/shared.js +160 -0
  19. package/dist/src/commands/status.js +101 -0
  20. package/dist/src/commands/trash.js +78 -0
  21. package/dist/src/commands/update.js +75 -0
  22. package/dist/src/commands/validate.js +35 -0
  23. package/dist/src/config/env.js +24 -0
  24. package/dist/src/config/package.js +17 -0
  25. package/dist/src/config/paths.js +5 -0
  26. package/dist/src/renderers/attention.js +3 -0
  27. package/dist/src/renderers/doctor.js +64 -0
  28. package/dist/src/renderers/json.js +10 -0
  29. package/dist/src/renderers/review.js +159 -0
  30. package/dist/src/renderers/status.js +112 -0
  31. package/dist/src/shared/cli-types.js +1 -0
  32. package/dist/src/shared/errors.js +4 -0
  33. package/dist/src/shared/flags.js +41 -0
  34. package/dist/src/shared/help-text.js +355 -0
  35. package/docs/agent-usage.html +1 -1
  36. package/docs/agent-usage.md +2 -2
  37. package/docs/reference.html +12 -6
  38. 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,4 @@
1
+ export function formatCliError(error) {
2
+ const message = error instanceof Error ? error.message : String(error);
3
+ return `artshelf: ${message}\nRun \`artshelf help\` for usage.\n`;
4
+ }
@@ -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
+ }