artshelf 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/cli.js CHANGED
@@ -1,8 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, readFileSync } from "node:fs";
2
+ import { spawnSync } from "node:child_process";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { dirname, join } from "node:path";
3
6
  import { appendPreparedRecord, createCleanupPlan, createTrashPurgePlan, dueEntries, executeCleanupPlan, executeTrashPurgePlan, filterRecordsByStatus, findRecords, getRecord, listTrashedRecords, normalizeLedgerPath, prepareRecord, previewCleanupPlan, readLedger, resolveRecord, validateLedger } from "./ledger.js";
4
7
  import { listRegisteredLedgers, normalizeRegistryPath, registerLedger } from "./registry.js";
5
8
  const VERSION = readPackageVersion();
9
+ const PACKAGE_NAME = "artshelf";
10
+ const NPM_REGISTRY_URL = process.env.ARTSHELF_NPM_REGISTRY_URL ?? `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
11
+ const UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1000;
6
12
  const BOOLEAN_FLAGS = new Set(["all", "json", "manual-review", "dry-run", "execute", "help", "version", "plain"]);
7
13
  const VALUE_FLAGS = new Set([
8
14
  "cleanup",
@@ -29,56 +35,83 @@ function readPackageVersion() {
29
35
  }
30
36
  return packageJson.version;
31
37
  }
32
- function main(argv) {
38
+ async function main(argv) {
33
39
  try {
34
40
  const parsed = parseArgs(argv);
41
+ let status = 0;
42
+ let shouldCheckForUpdate = true;
35
43
  if (parsed.command === "--version" || parsed.command === "-v" || boolFlag(parsed, "version")) {
36
44
  process.stdout.write(`artshelf ${VERSION}\n`);
37
- return 0;
45
+ return maybeNotifyUpdateAndReturn(0, parsed);
38
46
  }
39
47
  if (parsed.command === "help" || parsed.command === "--help" || parsed.command === "-h" || boolFlag(parsed, "help")) {
40
- printHelp(parsed.command === "help" ? parsed.positionals[0] : parsed.command);
41
- return 0;
48
+ printHelp(resolveHelpKey(parsed));
49
+ return maybeNotifyUpdateAndReturn(0, parsed);
42
50
  }
43
51
  switch (parsed.command) {
44
52
  case "put":
45
- return handlePut(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
53
+ status = handlePut(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
54
+ break;
46
55
  case "ledgers":
47
- return handleLedgers(parsed, boolFlag(parsed, "json"));
56
+ status = handleLedgers(parsed, boolFlag(parsed, "json"));
57
+ break;
48
58
  case "list":
49
- return handleList(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
59
+ status = handleList(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
60
+ break;
50
61
  case "find":
51
- return handleFind(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
62
+ status = handleFind(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
63
+ break;
52
64
  case "get":
53
- return handleGet(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
65
+ status = handleGet(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
66
+ break;
54
67
  case "due":
55
- return handleDue(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
68
+ status = handleDue(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
69
+ break;
56
70
  case "validate":
57
- return handleValidate(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
71
+ status = handleValidate(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
72
+ break;
58
73
  case "cleanup":
59
- return handleCleanup(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
74
+ status = handleCleanup(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
75
+ break;
60
76
  case "trash":
61
- return handleTrash(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
77
+ status = handleTrash(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
78
+ break;
62
79
  case "review":
63
- return handleReview(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
80
+ status = handleReview(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
81
+ break;
64
82
  case "doctor":
65
- return handleDoctor(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
83
+ status = handleDoctor(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
84
+ break;
66
85
  case "status":
67
- return handleStatus(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
86
+ status = handleStatus(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
87
+ break;
68
88
  case "resolve":
69
- return handleResolve(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
89
+ status = handleResolve(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
90
+ break;
91
+ case "update":
92
+ shouldCheckForUpdate = false;
93
+ status = await handleUpdate(parsed, boolFlag(parsed, "json"));
94
+ break;
70
95
  case undefined:
71
96
  printHelp();
72
- return 0;
97
+ status = 0;
98
+ break;
73
99
  default:
74
100
  throw new Error(`Unknown command: ${parsed.command}`);
75
101
  }
102
+ if (!shouldCheckForUpdate)
103
+ return status;
104
+ return maybeNotifyUpdateAndReturn(status, parsed);
76
105
  }
77
106
  catch (error) {
78
107
  process.stderr.write(`artshelf: ${error.message}\nRun \`artshelf help\` for usage.\n`);
79
108
  return 1;
80
109
  }
81
110
  }
111
+ async function maybeNotifyUpdateAndReturn(status, parsed) {
112
+ await maybeNotifyAvailableUpdate(parsed);
113
+ return status;
114
+ }
82
115
  function handlePut(parsed, ledgerPath, json) {
83
116
  const path = parsed.positionals[0];
84
117
  if (!path)
@@ -114,6 +147,10 @@ function handlePut(parsed, ledgerPath, json) {
114
147
  function handleLedgers(parsed, json) {
115
148
  const action = parsed.positionals[0] ?? "list";
116
149
  const registryPath = normalizeRegistryPath(stringFlag(parsed, "registry"));
150
+ if (action === "help") {
151
+ printHelp("ledgers");
152
+ return 0;
153
+ }
117
154
  if (action === "add") {
118
155
  const ledgerPath = normalizeLedgerPath(requiredStringFlag(parsed, "ledger"));
119
156
  if (!existsSync(ledgerPath))
@@ -726,6 +763,189 @@ function printStatusSingle(ledger) {
726
763
  process.stdout.write(`error: ${message}\n`);
727
764
  }
728
765
  }
766
+ async function handleUpdate(parsed, json) {
767
+ if (parsed.positionals.length > 0)
768
+ throw new Error("update does not accept positional arguments");
769
+ const info = await getUpdateInfo({ force: true });
770
+ if (!info)
771
+ throw new Error("Could not check npm for the latest Artshelf version");
772
+ if (!info.updateAvailable) {
773
+ if (json)
774
+ return printJson({ ok: true, updated: false, current: info.current, latest: info.latest });
775
+ process.stdout.write(`artshelf is already up to date: v${info.current}\n`);
776
+ return 0;
777
+ }
778
+ if (process.env.ARTSHELF_UPDATE_DRY_RUN === "1") {
779
+ if (json) {
780
+ return printJson({
781
+ ok: true,
782
+ updated: false,
783
+ dryRun: true,
784
+ current: info.current,
785
+ latest: info.latest,
786
+ command: ["npm", "install", "-g", `${PACKAGE_NAME}@latest`]
787
+ });
788
+ }
789
+ process.stdout.write(`A new version of artshelf is available: v${info.current} -> v${info.latest}\n`);
790
+ process.stdout.write(`Dry run: would run "npm install -g ${PACKAGE_NAME}@latest"\n`);
791
+ return 0;
792
+ }
793
+ if (!json) {
794
+ process.stdout.write(`A new version of artshelf is available: v${info.current} -> v${info.latest}\n`);
795
+ process.stdout.write(`Updating with "npm install -g ${PACKAGE_NAME}@latest"...\n`);
796
+ }
797
+ const result = json
798
+ ? spawnSync("npm", ["install", "-g", `${PACKAGE_NAME}@latest`], { encoding: "utf8" })
799
+ : spawnSync("npm", ["install", "-g", `${PACKAGE_NAME}@latest`], { stdio: "inherit" });
800
+ const status = result.status ?? 1;
801
+ const spawnError = result.error instanceof Error ? result.error.message : "";
802
+ if (json) {
803
+ const stderr = typeof result.stderr === "string" ? result.stderr : "";
804
+ printJson({
805
+ ok: status === 0,
806
+ updated: status === 0,
807
+ current: info.current,
808
+ latest: info.latest,
809
+ stdout: typeof result.stdout === "string" ? result.stdout : "",
810
+ stderr: appendOutputMessage(stderr, spawnError)
811
+ });
812
+ return status;
813
+ }
814
+ if (spawnError)
815
+ process.stderr.write(`Update failed: ${spawnError}\n`);
816
+ if (status === 0)
817
+ process.stdout.write(`artshelf updated to v${info.latest}\n`);
818
+ return status;
819
+ }
820
+ function appendOutputMessage(output, message) {
821
+ if (!message)
822
+ return output;
823
+ if (!output)
824
+ return message;
825
+ return `${output}${output.endsWith("\n") ? "" : "\n"}${message}`;
826
+ }
827
+ async function maybeNotifyAvailableUpdate(parsed) {
828
+ if (process.env.ARTSHELF_NO_UPDATE_CHECK === "1")
829
+ return;
830
+ if (parsed.command === "update")
831
+ return;
832
+ const info = await getUpdateInfo({ force: false });
833
+ if (!info?.updateAvailable)
834
+ return;
835
+ process.stderr.write(`A new version of artshelf is available: v${info.current} -> v${info.latest}\n`);
836
+ process.stderr.write(`Run "artshelf update" to update npm installs\n`);
837
+ }
838
+ async function getUpdateInfo(options) {
839
+ const latest = await getLatestVersion(options);
840
+ if (!latest)
841
+ return null;
842
+ return {
843
+ current: VERSION,
844
+ latest,
845
+ updateAvailable: compareVersions(latest, VERSION) > 0
846
+ };
847
+ }
848
+ async function getLatestVersion(options) {
849
+ const override = process.env.ARTSHELF_LATEST_VERSION;
850
+ if (override)
851
+ return normalizeVersion(override);
852
+ if (!options.force) {
853
+ const cached = readUpdateCache();
854
+ if (cached)
855
+ return cached.latest;
856
+ }
857
+ const latest = await fetchLatestNpmVersion();
858
+ writeUpdateCache(latest);
859
+ return latest;
860
+ }
861
+ function readUpdateCache() {
862
+ const ttl = Number(process.env.ARTSHELF_UPDATE_CHECK_TTL_MS ?? UPDATE_CHECK_TTL_MS);
863
+ if (ttl < 0)
864
+ return null;
865
+ const cachePath = updateCachePath();
866
+ if (!existsSync(cachePath))
867
+ return null;
868
+ try {
869
+ const cache = JSON.parse(readFileSync(cachePath, "utf8"));
870
+ if (cache.latest !== null && typeof cache.latest !== "string")
871
+ return null;
872
+ if (typeof cache.checkedAt !== "number")
873
+ return null;
874
+ if (Date.now() - cache.checkedAt > ttl)
875
+ return null;
876
+ return { latest: cache.latest === null ? null : normalizeVersion(cache.latest) };
877
+ }
878
+ catch {
879
+ return null;
880
+ }
881
+ }
882
+ function writeUpdateCache(latest) {
883
+ try {
884
+ const cachePath = updateCachePath();
885
+ const dir = dirname(cachePath);
886
+ if (dir) {
887
+ mkdirSync(dir, { recursive: true });
888
+ writeFileSync(cachePath, `${JSON.stringify({ latest, checkedAt: Date.now() }, null, 2)}\n`);
889
+ }
890
+ }
891
+ catch {
892
+ // Update checks should never affect normal CLI behavior.
893
+ }
894
+ }
895
+ async function fetchLatestNpmVersion() {
896
+ const controller = new AbortController();
897
+ const timeout = setTimeout(() => controller.abort(), 750);
898
+ try {
899
+ const response = await fetch(NPM_REGISTRY_URL, {
900
+ signal: controller.signal,
901
+ headers: { accept: "application/json", "user-agent": `artshelf/${VERSION}` }
902
+ });
903
+ if (!response.ok)
904
+ return null;
905
+ const body = await response.json();
906
+ if (!body || typeof body !== "object" || typeof body.version !== "string")
907
+ return null;
908
+ return normalizeVersion(body.version);
909
+ }
910
+ catch {
911
+ return null;
912
+ }
913
+ finally {
914
+ clearTimeout(timeout);
915
+ }
916
+ }
917
+ function updateCachePath() {
918
+ return process.env.ARTSHELF_UPDATE_CACHE ?? join(homedir(), ".artshelf", "update-check.json");
919
+ }
920
+ function normalizeVersion(version) {
921
+ return version.trim().replace(/^v/i, "");
922
+ }
923
+ function compareVersions(left, right) {
924
+ const a = parseVersion(left);
925
+ const b = parseVersion(right);
926
+ for (let index = 0; index < Math.max(a.numbers.length, b.numbers.length); index += 1) {
927
+ const diff = (a.numbers[index] ?? 0) - (b.numbers[index] ?? 0);
928
+ if (diff !== 0)
929
+ return diff;
930
+ }
931
+ if (a.prerelease === b.prerelease)
932
+ return 0;
933
+ if (!a.prerelease)
934
+ return 1;
935
+ if (!b.prerelease)
936
+ return -1;
937
+ return a.prerelease.localeCompare(b.prerelease);
938
+ }
939
+ function parseVersion(version) {
940
+ const [main = "", prerelease = ""] = normalizeVersion(version).split("-", 2);
941
+ return {
942
+ numbers: main.split(".").map((part) => {
943
+ const parsed = Number.parseInt(part, 10);
944
+ return Number.isFinite(parsed) ? parsed : 0;
945
+ }),
946
+ prerelease
947
+ };
948
+ }
729
949
  function parseArgs(argv) {
730
950
  const [command, ...rest] = argv;
731
951
  const flags = new Map();
@@ -734,6 +954,14 @@ function parseArgs(argv) {
734
954
  const arg = rest[index];
735
955
  if (!arg)
736
956
  continue;
957
+ if (arg === "-h") {
958
+ flags.set("help", true);
959
+ continue;
960
+ }
961
+ if (arg === "-v") {
962
+ flags.set("version", true);
963
+ continue;
964
+ }
737
965
  if (!arg.startsWith("--")) {
738
966
  positionals.push(arg);
739
967
  continue;
@@ -958,7 +1186,93 @@ function printReview(results) {
958
1186
  process.stdout.write(`ledger: ${result.ledger.path}\n`);
959
1187
  }
960
1188
  }
961
- function printHelp(command) {
1189
+ const COMMAND_GROUPS = [
1190
+ {
1191
+ group: "Create",
1192
+ commands: [{ name: "put", summary: "Record an artifact with a reason and retention" }]
1193
+ },
1194
+ {
1195
+ group: "Inspect",
1196
+ commands: [
1197
+ { name: "list", summary: "List ledger records" },
1198
+ { name: "find", summary: "Find records by path, owner, label, or status" },
1199
+ { name: "get", summary: "Show one record by id" },
1200
+ { name: "due", summary: "Show due, manual-review, and missing-path records" },
1201
+ { name: "status", summary: "Summarize ledger and registry counts" }
1202
+ ]
1203
+ },
1204
+ {
1205
+ group: "Review",
1206
+ commands: [
1207
+ { name: "validate", summary: "Check ledger shape and report warnings" },
1208
+ { name: "review", summary: "Preview validate, due, and cleanup plans (read-only)" }
1209
+ ]
1210
+ },
1211
+ {
1212
+ group: "Clean",
1213
+ commands: [
1214
+ { name: "cleanup", summary: "Plan and execute approved cleanups" },
1215
+ { name: "trash", summary: "Inspect and purge Artshelf trash" },
1216
+ { name: "resolve", summary: "Mark a record manually resolved" }
1217
+ ]
1218
+ },
1219
+ {
1220
+ group: "System",
1221
+ commands: [
1222
+ { name: "ledgers", summary: "Manage the ledger registry" },
1223
+ { name: "doctor", summary: "Report Artshelf health on this machine" },
1224
+ { name: "update", summary: "Update the Artshelf CLI" }
1225
+ ]
1226
+ }
1227
+ ];
1228
+ // Commands with subcommands that carry their own focused help. Used to route
1229
+ // `artshelf <command> <subcommand> --help` to a nested help key.
1230
+ const NESTED_HELP = new Map([
1231
+ ["trash", new Set(["list", "purge"])],
1232
+ ["ledgers", new Set(["list", "add"])]
1233
+ ]);
1234
+ function resolveHelpKey(parsed) {
1235
+ // `artshelf help [command [subcommand]]`
1236
+ if (parsed.command === "help") {
1237
+ return joinHelpKey(parsed.positionals[0], parsed.positionals[1]);
1238
+ }
1239
+ // `artshelf [--help|-h]` with no command resolves to the top-level help.
1240
+ if (!parsed.command || parsed.command === "--help" || parsed.command === "-h") {
1241
+ return "";
1242
+ }
1243
+ // `artshelf <command> [subcommand] --help`
1244
+ return joinHelpKey(parsed.command, parsed.positionals[0]);
1245
+ }
1246
+ function joinHelpKey(command, subcommand) {
1247
+ if (!command)
1248
+ return "";
1249
+ const subcommands = NESTED_HELP.get(command);
1250
+ if (subcommands && subcommand && subcommands.has(subcommand)) {
1251
+ return `${command} ${subcommand}`;
1252
+ }
1253
+ return command;
1254
+ }
1255
+ function renderTopLevelHelp() {
1256
+ const names = COMMAND_GROUPS.flatMap((entry) => entry.commands.map((command) => command.name));
1257
+ const width = Math.max(...names.map((name) => name.length)) + 2;
1258
+ const lines = [
1259
+ `Artshelf ${VERSION} — approval-first retention for the temporary files agents leave behind.`,
1260
+ "",
1261
+ "Usage:",
1262
+ " artshelf <command> [options]",
1263
+ "",
1264
+ "Available Commands:"
1265
+ ];
1266
+ for (const { group, commands } of COMMAND_GROUPS) {
1267
+ lines.push(` ${group}`);
1268
+ for (const command of commands) {
1269
+ lines.push(` ${command.name.padEnd(width)}${command.summary}`);
1270
+ }
1271
+ }
1272
+ 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.`, "");
1273
+ return lines.join("\n");
1274
+ }
1275
+ function printHelp(command = "") {
962
1276
  if (command === "put") {
963
1277
  process.stdout.write(`Usage:
964
1278
  artshelf put <path> --reason <text> (--ttl <ttl>|--retain-until <date>|--manual-review) [options]
@@ -992,30 +1306,36 @@ Global --all mode is dry-run only.
992
1306
  return;
993
1307
  }
994
1308
  if (command === "trash") {
995
- process.stdout.write(`Usage:
996
- artshelf trash list [--ledger <path>] [--all] [--json]
997
- artshelf trash purge --older-than <ttl> --dry-run [--ledger <path>] [--json]
998
- artshelf trash purge --execute --plan-id <id> [--ledger <path>] [--json]
1309
+ process.stdout.write(`Inspect and purge Artshelf trash.
1310
+
1311
+ Usage:
1312
+ artshelf trash [command]
1313
+
1314
+ Available Commands:
1315
+ list List records currently held in Artshelf trash
1316
+ purge Plan or execute approved permanent trash deletion
999
1317
 
1000
- Trash is approval-first. Use list to inspect what is currently in Artshelf trash and
1001
- dry-run purge to generate a reviewed plan id for age-based deletion. Purge
1002
- requires either --dry-run or --execute. Execute requires a reviewed plan id, and
1003
- trash purge is always scoped to one --ledger; --all is not supported for purge
1004
- (only for trash list).
1005
- Trash receipt artifacts are registered when purge executes. Completed receipts are
1006
- refused on repeat execute; started receipts from interrupted purges may be resumed
1007
- and reconciled. Purged records are resolved and no longer reappear as trashed.
1318
+ Flags:
1319
+ -h, --help help for trash
1320
+
1321
+ Use "artshelf trash <command> --help" for more information about a command.
1008
1322
  `);
1009
1323
  return;
1010
1324
  }
1011
1325
  if (command === "ledgers") {
1012
- process.stdout.write(`Usage:
1013
- artshelf ledgers list [--plain] [--registry <path>] [--json]
1014
- artshelf ledgers add --ledger <path> [--name <name>] [--scope repo|user|other] [--registry <path>] [--json]
1326
+ process.stdout.write(`Manage the ledger registry.
1327
+
1328
+ Usage:
1329
+ artshelf ledgers [command]
1330
+
1331
+ Available Commands:
1332
+ list List and validate registered ledgers
1333
+ add Register an existing ledger file
1015
1334
 
1016
- The ledger registry is a global index of known ledgers. It gives Artshelf one read-only entry point without moving project records into one global ledger.
1017
- By default \`list\` validates each registered ledger and reports ok/missing/invalid status, entry counts, and warning/error counts so agents can spot stale registry entries without a separate validate pass; it exits non-zero when the registry or any registered ledger is broken.
1018
- Use \`--plain\` for the fast path that lists registered ledgers without reading them.
1335
+ Flags:
1336
+ -h, --help help for ledgers
1337
+
1338
+ Use "artshelf ledgers <command> --help" for more information about a command.
1019
1339
  `);
1020
1340
  return;
1021
1341
  }
@@ -1110,48 +1430,120 @@ or receipts and never mutates records. A healthy selected ledger exits 0; with
1110
1430
  `);
1111
1431
  return;
1112
1432
  }
1113
- process.stdout.write(`Artshelf ${VERSION}
1433
+ if (command === "update") {
1434
+ process.stdout.write(`Usage:
1435
+ artshelf update [--json]
1114
1436
 
1115
- Usage:
1116
- artshelf put <path> --reason <text> (--ttl <ttl>|--retain-until <date>|--manual-review)
1117
- artshelf ledgers list [--plain] [--json]
1118
- artshelf ledgers add --ledger <path> [--name <name>] [--json]
1119
- artshelf list [--json]
1120
- artshelf list --all [--json]
1121
- artshelf list --status active [--json]
1122
- artshelf find --path <path> [--json]
1123
- artshelf find --all --owner <name> [--json]
1124
- artshelf get <id> [--json]
1125
- artshelf get <id> --all [--json]
1126
- artshelf due [--json]
1127
- artshelf due --all [--json]
1128
- artshelf validate [--json]
1129
- artshelf validate --all [--json]
1130
- artshelf review [--json]
1131
- artshelf review --all [--json]
1132
- artshelf doctor [--json]
1133
- artshelf status [--json]
1134
- artshelf status --all [--json]
1135
- artshelf cleanup --dry-run [--json]
1136
- artshelf cleanup --dry-run --all [--json]
1137
- artshelf cleanup --execute --plan-id <id> [--json]
1138
- artshelf trash list [--all] [--ledger <path>] [--json]
1437
+ Update checks compare the current CLI version with the latest published npm
1438
+ version. Normal commands may print a non-blocking update notice to stderr when a
1439
+ newer version is available. Run update to upgrade npm global installs only:
1440
+
1441
+ npm install -g artshelf@latest
1442
+
1443
+ pnpm global installs should update with pnpm add -g artshelf@latest; source
1444
+ installs should update by pulling, rebuilding, and linking the checkout.
1445
+ `);
1446
+ return;
1447
+ }
1448
+ if (command === "due") {
1449
+ process.stdout.write(`Usage:
1450
+ artshelf due [--ledger <path>] [--json]
1451
+ artshelf due --all [--registry <path>] [--json]
1452
+
1453
+ Due lists records whose retention has elapsed or that need attention: due,
1454
+ manual-review, and missing-path entries. Kept entries are hidden in human output.
1455
+ Due is read-only and never moves files or writes plans.
1456
+ `);
1457
+ return;
1458
+ }
1459
+ if (command === "validate") {
1460
+ process.stdout.write(`Usage:
1461
+ artshelf validate [--ledger <path>] [--json]
1462
+ artshelf validate --all [--registry <path>] [--json]
1463
+
1464
+ Validate checks ledger shape and reports errors and warnings, such as records
1465
+ that point at missing artifact paths, without changing anything. A clean ledger
1466
+ exits 0; shape errors exit non-zero. With --all it validates every registered
1467
+ ledger.
1468
+ `);
1469
+ return;
1470
+ }
1471
+ if (command === "trash list") {
1472
+ process.stdout.write(`Usage:
1473
+ artshelf trash list [--ledger <path>] [--all] [--registry <path>] [--json]
1474
+
1475
+ Options:
1476
+ --ledger <path> Use a specific ledger file
1477
+ --all Include records from all registered ledgers
1478
+ --registry <path> Registry path used with --all
1479
+ --json Emit machine-readable output
1480
+
1481
+ Trash list shows records currently held in Artshelf trash without deleting anything.
1482
+ With --all it reports trashed records across every registered ledger.
1483
+ `);
1484
+ return;
1485
+ }
1486
+ if (command === "trash purge") {
1487
+ process.stdout.write(`Usage:
1139
1488
  artshelf trash purge --older-than <ttl> --dry-run [--ledger <path>] [--json]
1140
1489
  artshelf trash purge --execute --plan-id <id> [--ledger <path>] [--json]
1141
- artshelf resolve <id> --status resolved --reason <text> [--json]
1142
1490
 
1143
- Global options:
1144
- --ledger <path> Use an explicit JSONL ledger
1145
- --registry <path> Use an explicit ledger registry
1146
- --all Read all registered ledgers for supported commands
1147
- --json Emit machine-readable JSON
1148
- --help Show help
1149
- --version Show version
1491
+ Options:
1492
+ --older-than <ttl> Purge trashed records older than this duration
1493
+ --dry-run Build a reviewed purge plan and output a plan id
1494
+ --execute Execute a reviewed purge plan
1495
+ --plan-id <id> Execute only this reviewed purge plan
1496
+ --ledger <path> Target one specific ledger
1497
+ --json Emit machine-readable output
1498
+
1499
+ Trash purge permanently deletes aged trash from a reviewed plan. --dry-run turns
1500
+ --older-than into a reviewed purge plan id; --execute deletes only that one reviewed
1501
+ plan id. Purge is always scoped to one --ledger; --all is not supported for purge.
1502
+ Completed receipts are refused on repeat execute; an interrupted purge may be resumed
1503
+ and reconciled.
1504
+ `);
1505
+ return;
1506
+ }
1507
+ if (command === "ledgers list") {
1508
+ process.stdout.write(`Usage:
1509
+ artshelf ledgers list [--plain] [--registry <path>] [--json]
1510
+
1511
+ Options:
1512
+ --plain Skip ledger validation and list registrations directly
1513
+ --registry <path> Registry path to use
1514
+ --json Emit machine-readable output
1150
1515
 
1151
- Examples:
1152
- artshelf put tmp/run-output --reason "debug parser output" --ttl 3d --kind scratch
1153
- artshelf cleanup --dry-run --json
1154
- artshelf cleanup --execute --plan-id plan_20260601_120000_ab12
1516
+ Ledgers list validates every registered ledger and reports ok/missing/invalid
1517
+ status, entry counts, and warnings so agents can spot stale registry entries
1518
+ without a separate validate pass. Use --plain for the fast path that lists
1519
+ registered ledgers without reading them. It exits non-zero when the registry or
1520
+ any registered ledger is broken.
1155
1521
  `);
1522
+ return;
1523
+ }
1524
+ if (command === "ledgers add") {
1525
+ process.stdout.write(`Usage:
1526
+ artshelf ledgers add --ledger <path> [--name <name>] [--scope repo|user|other] [--registry <path>] [--json]
1527
+
1528
+ Options:
1529
+ --ledger <path> Register this ledger file
1530
+ --name <name> Override the ledger display name
1531
+ --scope <scope> Registry scope: repo, user, or other
1532
+ --registry <path> Registry path to update
1533
+ --json Emit machine-readable output
1534
+
1535
+ Ledgers add registers an existing ledger file in the global registry so --all
1536
+ commands and the registry index can find it. The ledger file must already exist.
1537
+ `);
1538
+ return;
1539
+ }
1540
+ process.stdout.write(renderTopLevelHelp());
1156
1541
  }
1157
- process.exitCode = main(process.argv.slice(2));
1542
+ main(process.argv.slice(2))
1543
+ .then((status) => {
1544
+ process.exitCode = status;
1545
+ })
1546
+ .catch((error) => {
1547
+ process.stderr.write(`artshelf: ${error.message}\nRun \`artshelf help\` for usage.\n`);
1548
+ process.exitCode = 1;
1549
+ });