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/CHANGELOG.md +34 -2
- package/README.md +150 -248
- package/SPEC.md +81 -3
- package/dist/src/cli.js +468 -76
- package/docs/agent-clean.html +6 -24
- package/docs/agent-monitor.html +2 -1
- package/docs/agent-purge.html +111 -0
- package/docs/agent-usage.html +13 -9
- package/docs/agent-usage.md +12 -7
- package/docs/index.html +10 -5
- package/docs/install.html +28 -2
- package/docs/reference.html +75 -10
- package/docs/site.js +2 -1
- package/package.json +1 -1
- package/skills/artshelf/SKILL.md +16 -13
package/dist/src/cli.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
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
|
|
41
|
-
return 0;
|
|
48
|
+
printHelp(resolveHelpKey(parsed));
|
|
49
|
+
return maybeNotifyUpdateAndReturn(0, parsed);
|
|
42
50
|
}
|
|
43
51
|
switch (parsed.command) {
|
|
44
52
|
case "put":
|
|
45
|
-
|
|
53
|
+
status = handlePut(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
|
|
54
|
+
break;
|
|
46
55
|
case "ledgers":
|
|
47
|
-
|
|
56
|
+
status = handleLedgers(parsed, boolFlag(parsed, "json"));
|
|
57
|
+
break;
|
|
48
58
|
case "list":
|
|
49
|
-
|
|
59
|
+
status = handleList(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
|
|
60
|
+
break;
|
|
50
61
|
case "find":
|
|
51
|
-
|
|
62
|
+
status = handleFind(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
|
|
63
|
+
break;
|
|
52
64
|
case "get":
|
|
53
|
-
|
|
65
|
+
status = handleGet(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
|
|
66
|
+
break;
|
|
54
67
|
case "due":
|
|
55
|
-
|
|
68
|
+
status = handleDue(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
|
|
69
|
+
break;
|
|
56
70
|
case "validate":
|
|
57
|
-
|
|
71
|
+
status = handleValidate(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
|
|
72
|
+
break;
|
|
58
73
|
case "cleanup":
|
|
59
|
-
|
|
74
|
+
status = handleCleanup(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
|
|
75
|
+
break;
|
|
60
76
|
case "trash":
|
|
61
|
-
|
|
77
|
+
status = handleTrash(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
|
|
78
|
+
break;
|
|
62
79
|
case "review":
|
|
63
|
-
|
|
80
|
+
status = handleReview(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
|
|
81
|
+
break;
|
|
64
82
|
case "doctor":
|
|
65
|
-
|
|
83
|
+
status = handleDoctor(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
|
|
84
|
+
break;
|
|
66
85
|
case "status":
|
|
67
|
-
|
|
86
|
+
status = handleStatus(parsed, normalizeLedgerPath(stringFlag(parsed, "ledger")), boolFlag(parsed, "json"));
|
|
87
|
+
break;
|
|
68
88
|
case "resolve":
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(`
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
artshelf trash
|
|
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
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
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(`
|
|
1013
|
-
|
|
1014
|
-
|
|
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
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
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
|
-
|
|
1433
|
+
if (command === "update") {
|
|
1434
|
+
process.stdout.write(`Usage:
|
|
1435
|
+
artshelf update [--json]
|
|
1114
1436
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
artshelf
|
|
1129
|
-
artshelf
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
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
|
-
|
|
1144
|
-
--
|
|
1145
|
-
--
|
|
1146
|
-
--
|
|
1147
|
-
--
|
|
1148
|
-
--
|
|
1149
|
-
--
|
|
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
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
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
|
-
|
|
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
|
+
});
|