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,160 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dueEntries, previewCleanupPlan, readLedger, validateLedger } from "../ledger.js";
3
+ import { listRegisteredLedgers } from "../registry.js";
4
+ import { printJson } from "../renderers/json.js";
5
+ export function registeredLedgersOrThrow(registryPath) {
6
+ const ledgers = listRegisteredLedgers(registryPath);
7
+ if (ledgers.length === 0)
8
+ throw new Error("No registered Artshelf ledgers. Run `artshelf ledgers add --ledger <path>` first.");
9
+ return ledgers;
10
+ }
11
+ export function validateRegisteredLedgersOrThrow(registryPath) {
12
+ const results = registeredLedgersOrThrow(registryPath).map((ledger) => ({ ledger, result: validateRegisteredLedger(ledger) }));
13
+ return { ok: results.every((entry) => entry.result.ok), results };
14
+ }
15
+ export function printRegisteredLedgerValidation(registryPath, results, json) {
16
+ if (json) {
17
+ printJson({ ok: false, registryPath, ledgers: results });
18
+ return 1;
19
+ }
20
+ for (const entry of results.filter((item) => !item.result.ok)) {
21
+ process.stdout.write(`invalid ${entry.ledger.name}: ${entry.result.errors.join("; ")}\nledger: ${entry.ledger.path}\n`);
22
+ }
23
+ process.stdout.write(`registry: ${registryPath}\n`);
24
+ return 1;
25
+ }
26
+ export function validateRegisteredLedger(ledger) {
27
+ if (!existsSync(ledger.path)) {
28
+ return {
29
+ ok: false,
30
+ errors: [`registered ledger is missing: ${ledger.path}`],
31
+ warnings: [],
32
+ entries: 0
33
+ };
34
+ }
35
+ return validateLedger(ledger.path);
36
+ }
37
+ export function reviewLedger(ledger, registered = true) {
38
+ const validate = registered ? validateRegisteredLedger(ledger) : validateLedger(ledger.path);
39
+ const ledgerExists = existsSync(ledger.path);
40
+ if (!validate.ok) {
41
+ return {
42
+ ledger,
43
+ ledgerExists,
44
+ validate,
45
+ due: [],
46
+ plan: emptyReviewPlan(ledger.path)
47
+ };
48
+ }
49
+ return {
50
+ ledger,
51
+ ledgerExists,
52
+ validate,
53
+ due: dueEntries(readLedger(ledger.path)),
54
+ plan: previewCleanupPlan(ledger.path)
55
+ };
56
+ }
57
+ export function reviewJsonResult(result) {
58
+ const { ledgerExists: _ledgerExists, ...jsonResult } = result;
59
+ return jsonResult;
60
+ }
61
+ export function emptyReviewPlan(ledgerPath) {
62
+ return {
63
+ planId: "not-created",
64
+ generatedAt: "",
65
+ ledgerPath,
66
+ entries: [],
67
+ skipped: [],
68
+ planPath: null
69
+ };
70
+ }
71
+ export function printLedgerEntries(results, status) {
72
+ const total = results.reduce((count, result) => count + result.entries.length, 0);
73
+ if (total === 0) {
74
+ process.stdout.write(`no artshelf entries${status ? ` with status ${status}` : ""}\n`);
75
+ return;
76
+ }
77
+ for (const result of results) {
78
+ if (result.entries.length === 0)
79
+ continue;
80
+ process.stdout.write(`\n[${result.ledger.name}] ${result.ledger.path}\n`);
81
+ for (const record of result.entries) {
82
+ process.stdout.write(`${record.id} ${record.kind} ${record.status} ${record.cleanup} ${record.path} :: ${record.reason}\n`);
83
+ }
84
+ }
85
+ }
86
+ export function printDueEntries(results) {
87
+ const visible = results.flatMap((result) => result.entries.filter((entry) => entry.dueStatus !== "kept").map((entry) => ({ ledger: result.ledger, entry })));
88
+ if (visible.length === 0) {
89
+ process.stdout.write("nothing due\n");
90
+ return;
91
+ }
92
+ for (const item of visible) {
93
+ process.stdout.write(`${item.entry.dueStatus} ${item.entry.id} ${item.entry.cleanup} ${item.entry.path} :: ${item.entry.reason}\nledger: ${item.ledger.path}\n`);
94
+ }
95
+ }
96
+ export function printPlans(results) {
97
+ for (const result of results) {
98
+ process.stdout.write(`plan ${result.plan.planId} [${result.ledger.name}]: ${result.plan.entries.length} entries, ${result.plan.skipped.length} skipped\n`);
99
+ process.stdout.write(`plan: ${result.plan.planPath ?? "not created"}\nledger: ${result.ledger.path}\n`);
100
+ }
101
+ }
102
+ export function printPlan(plan, ledgerPath) {
103
+ process.stdout.write(`plan ${plan.planId}: ${plan.entries.length} entries, ${plan.skipped.length} skipped\n`);
104
+ process.stdout.write(`plan: ${plan.planPath ?? "not created"}\nledger: ${ledgerPath}\n`);
105
+ }
106
+ export function printTrashListEntries(results) {
107
+ const total = results.reduce((count, result) => count + result.entries.length, 0);
108
+ if (total === 0) {
109
+ process.stdout.write("no trashed records\n");
110
+ return;
111
+ }
112
+ for (const result of results) {
113
+ if (result.entries.length === 0)
114
+ continue;
115
+ process.stdout.write(`\n[${result.ledger.name}] ${result.ledger.path}\n`);
116
+ for (const entry of result.entries) {
117
+ process.stdout.write(`trash ${entry.id} ${entry.age} ${entry.cleanedAt} ${entry.targetPath} -> ${entry.receiptPath} (${entry.cleanupPlanId})\n`);
118
+ }
119
+ }
120
+ }
121
+ export function summarizeReview(results) {
122
+ const summary = {
123
+ ledgers: results.length,
124
+ ok: 0,
125
+ invalid: 0,
126
+ stale: 0,
127
+ affected: 0,
128
+ due: 0,
129
+ manualReview: 0,
130
+ missingPath: 0,
131
+ executable: 0,
132
+ skipped: 0,
133
+ previewPlanIds: []
134
+ };
135
+ for (const result of results) {
136
+ if (result.validate.ok) {
137
+ summary.ok += 1;
138
+ }
139
+ else if (result.ledgerExists) {
140
+ summary.invalid += 1;
141
+ }
142
+ else {
143
+ summary.stale += 1;
144
+ }
145
+ const due = result.due.filter((entry) => entry.dueStatus === "due").length;
146
+ const manualReview = result.due.filter((entry) => entry.dueStatus === "manual-review").length;
147
+ const missingPath = result.due.filter((entry) => entry.dueStatus === "missing-path").length;
148
+ summary.due += due;
149
+ summary.manualReview += manualReview;
150
+ summary.missingPath += missingPath;
151
+ summary.executable += result.plan.entries.length;
152
+ summary.skipped += result.plan.skipped.length;
153
+ if (result.plan.planId !== "not-created")
154
+ summary.previewPlanIds.push(result.plan.planId);
155
+ if (!result.validate.ok || due + manualReview + missingPath > 0 || result.plan.entries.length > 0) {
156
+ summary.affected += 1;
157
+ }
158
+ }
159
+ return summary;
160
+ }
@@ -0,0 +1,101 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dueEntries, previewCleanupPlan, readLedger, validateLedger } from "../ledger.js";
3
+ import { listRegisteredLedgers, normalizeRegistryPath } from "../registry.js";
4
+ import { printCompactJson, printJson } from "../renderers/json.js";
5
+ import { buildStatusAgentPacketAll, buildStatusAgentPacketSingle, emptyStatusCounts, printStatusAll, printStatusSingle, sumStatusCounts } from "../renderers/status.js";
6
+ import { boolFlag, stringFlag } from "../shared/flags.js";
7
+ import { validateRegisteredLedger } from "./shared.js";
8
+ export function handleStatus(parsed, ledgerPath, json) {
9
+ const agent = boolFlag(parsed, "agent");
10
+ if (boolFlag(parsed, "all")) {
11
+ const report = buildStatusReport(normalizeRegistryPath(stringFlag(parsed, "registry")));
12
+ if (agent) {
13
+ printCompactJson(buildStatusAgentPacketAll(report));
14
+ return report.ok ? 0 : 1;
15
+ }
16
+ if (json) {
17
+ printJson(report);
18
+ return report.ok ? 0 : 1;
19
+ }
20
+ printStatusAll(report);
21
+ return report.ok ? 0 : 1;
22
+ }
23
+ const ledger = statusLedger({ name: "current", path: ledgerPath, scope: "other", createdAt: "", updatedAt: "" }, false);
24
+ if (agent) {
25
+ printCompactJson(buildStatusAgentPacketSingle(ledger, ledgerPath));
26
+ return ledger.ok ? 0 : 1;
27
+ }
28
+ if (json) {
29
+ printJson({ ok: ledger.ok, ledger });
30
+ return ledger.ok ? 0 : 1;
31
+ }
32
+ printStatusSingle(ledger);
33
+ return ledger.ok ? 0 : 1;
34
+ }
35
+ function buildStatusReport(registryPath) {
36
+ let registryOk = true;
37
+ let registryError = null;
38
+ let entries = [];
39
+ try {
40
+ entries = listRegisteredLedgers(registryPath);
41
+ }
42
+ catch (error) {
43
+ registryOk = false;
44
+ registryError = error.message;
45
+ }
46
+ const ledgers = entries.map((entry) => statusLedger(entry));
47
+ const totals = {
48
+ ledgers: ledgers.length,
49
+ ok: ledgers.filter((ledger) => ledger.status === "ok").length,
50
+ stale: ledgers.filter((ledger) => ledger.status === "missing").length,
51
+ invalid: ledgers.filter((ledger) => ledger.status === "invalid").length,
52
+ active: sumStatusCounts(ledgers, "active"),
53
+ due: sumStatusCounts(ledgers, "due"),
54
+ manualReview: sumStatusCounts(ledgers, "manualReview"),
55
+ missingPath: sumStatusCounts(ledgers, "missingPath"),
56
+ kept: sumStatusCounts(ledgers, "kept"),
57
+ pendingCleanup: sumStatusCounts(ledgers, "pendingCleanup")
58
+ };
59
+ return {
60
+ ok: registryOk && totals.stale === 0 && totals.invalid === 0,
61
+ registryPath,
62
+ registryExists: existsSync(registryPath),
63
+ registryOk,
64
+ registryError,
65
+ ledgers,
66
+ totals
67
+ };
68
+ }
69
+ function statusLedger(ledger, registered = true) {
70
+ const validate = registered ? validateRegisteredLedger(ledger) : validateLedger(ledger.path);
71
+ if (!validate.ok) {
72
+ return {
73
+ name: ledger.name,
74
+ path: ledger.path,
75
+ scope: ledger.scope,
76
+ status: existsSync(ledger.path) ? "invalid" : "missing",
77
+ ok: false,
78
+ counts: emptyStatusCounts(),
79
+ errors: validate.errors
80
+ };
81
+ }
82
+ const records = readLedger(ledger.path);
83
+ const due = dueEntries(records);
84
+ const counts = {
85
+ active: records.filter((record) => record.status === "active").length,
86
+ due: due.filter((entry) => entry.dueStatus === "due").length,
87
+ manualReview: due.filter((entry) => entry.dueStatus === "manual-review").length,
88
+ missingPath: due.filter((entry) => entry.dueStatus === "missing-path").length,
89
+ kept: due.filter((entry) => entry.dueStatus === "kept").length,
90
+ pendingCleanup: previewCleanupPlan(ledger.path).entries.length
91
+ };
92
+ return {
93
+ name: ledger.name,
94
+ path: ledger.path,
95
+ scope: ledger.scope,
96
+ status: "ok",
97
+ ok: true,
98
+ counts,
99
+ errors: []
100
+ };
101
+ }
@@ -0,0 +1,78 @@
1
+ import { createTrashPurgePlan, executeTrashPurgePlan, listTrashedRecords } from "../ledger.js";
2
+ import { normalizeRegistryPath } from "../registry.js";
3
+ import { printJson } from "../renderers/json.js";
4
+ import { boolFlag, requiredStringFlag, stringFlag } from "../shared/flags.js";
5
+ import { TRASH_HELP } from "../shared/help-text.js";
6
+ import { printRegisteredLedgerValidation, printTrashListEntries, validateRegisteredLedgersOrThrow } from "./shared.js";
7
+ export function handleTrash(parsed, ledgerPath, json) {
8
+ const action = parsed.positionals[0];
9
+ if (!action)
10
+ throw new Error("trash requires a subcommand: list or purge");
11
+ if (action === "list") {
12
+ return handleTrashList(parsed, ledgerPath, json);
13
+ }
14
+ if (action === "purge") {
15
+ return handleTrashPurge(parsed, ledgerPath, json);
16
+ }
17
+ if (action === "help") {
18
+ process.stdout.write(TRASH_HELP);
19
+ return 0;
20
+ }
21
+ throw new Error(`Unknown trash subcommand: ${action}`);
22
+ }
23
+ export function handleTrashList(parsed, ledgerPath, json) {
24
+ if (boolFlag(parsed, "all")) {
25
+ const registryPath = normalizeRegistryPath(stringFlag(parsed, "registry"));
26
+ const validation = validateRegisteredLedgersOrThrow(registryPath);
27
+ if (!validation.ok)
28
+ return printRegisteredLedgerValidation(registryPath, validation.results, json);
29
+ const results = validation.results.map(({ ledger }) => ({ ledger, entries: listTrashedRecords(ledger.path) }));
30
+ if (json)
31
+ return printJson({ ok: true, registryPath, ledgers: results });
32
+ printTrashListEntries(results);
33
+ process.stdout.write(`registry: ${registryPath}\n`);
34
+ return 0;
35
+ }
36
+ const entries = listTrashedRecords(ledgerPath);
37
+ if (json)
38
+ return printJson({ ok: true, ledgerPath, entries });
39
+ if (entries.length === 0) {
40
+ process.stdout.write(`no trashed records\nledger: ${ledgerPath}\n`);
41
+ return 0;
42
+ }
43
+ for (const entry of entries) {
44
+ process.stdout.write(`${entry.id} age ${entry.age} target ${entry.targetPath} cleaned ${entry.cleanedAt} receipt ${entry.receiptPath} plan ${entry.cleanupPlanId}\n`);
45
+ }
46
+ process.stdout.write(`ledger: ${ledgerPath}\n`);
47
+ return 0;
48
+ }
49
+ export function handleTrashPurge(parsed, ledgerPath, json) {
50
+ const execute = boolFlag(parsed, "execute");
51
+ const dryRun = boolFlag(parsed, "dry-run");
52
+ if (dryRun && execute)
53
+ throw new Error("trash purge accepts either --dry-run or --execute, not both");
54
+ if (boolFlag(parsed, "all")) {
55
+ throw new Error("trash purge --all is not supported; scope the purge to one --ledger and review the plan id before execute");
56
+ }
57
+ if (!dryRun && !execute)
58
+ throw new Error("trash purge requires either --dry-run or --execute");
59
+ if (execute) {
60
+ const planId = requiredStringFlag(parsed, "plan-id");
61
+ const receipt = executeTrashPurgePlan(ledgerPath, planId);
62
+ if (json)
63
+ return printJson({ ok: true, receipt });
64
+ process.stdout.write(`trash receipt ${receipt.purgePlanId}: ${receipt.results.length} results\nreceipt: ${receipt.receiptPath}\nledger: ${ledgerPath}\n`);
65
+ return 0;
66
+ }
67
+ const olderThan = requiredStringFlag(parsed, "older-than");
68
+ const plan = createTrashPurgePlan(ledgerPath, olderThan);
69
+ if (json)
70
+ return printJson({ ok: true, plan });
71
+ if (plan.entries.length === 0) {
72
+ process.stdout.write(`trash purge plan ${plan.purgePlanId}: no matching trashed records\nledger: ${ledgerPath}\n`);
73
+ return 0;
74
+ }
75
+ process.stdout.write(`trash purge plan ${plan.purgePlanId}: ${plan.entries.length} entries, ${plan.skipped.length} skipped\n`);
76
+ process.stdout.write(`plan: ${plan.planPath ?? "not-created"}\nledger: ${ledgerPath}\n`);
77
+ return 0;
78
+ }
@@ -0,0 +1,75 @@
1
+ import { PACKAGE_NAME } from "../config/package.js";
2
+ import { updateCheckDisabled, updateDryRunEnabled } from "../config/env.js";
3
+ import { installGlobalNpmPackage } from "../adapters/process.js";
4
+ import { printJson } from "../renderers/json.js";
5
+ import { getUpdateInfo } from "../adapters/update.js";
6
+ export async function handleUpdate(parsed, json) {
7
+ if (parsed.positionals.length > 0)
8
+ throw new Error("update does not accept positional arguments");
9
+ const info = await getUpdateInfo({ force: true });
10
+ if (!info)
11
+ throw new Error("Could not check npm for the latest Artshelf version");
12
+ if (!info.updateAvailable) {
13
+ if (json)
14
+ return printJson({ ok: true, updated: false, current: info.current, latest: info.latest });
15
+ process.stdout.write(`artshelf is already up to date: v${info.current}\n`);
16
+ return 0;
17
+ }
18
+ if (updateDryRunEnabled()) {
19
+ if (json) {
20
+ return printJson({
21
+ ok: true,
22
+ updated: false,
23
+ dryRun: true,
24
+ current: info.current,
25
+ latest: info.latest,
26
+ command: ["npm", "install", "-g", `${PACKAGE_NAME}@latest`]
27
+ });
28
+ }
29
+ process.stdout.write(`A new version of artshelf is available: v${info.current} -> v${info.latest}\n`);
30
+ process.stdout.write(`Dry run: would run "npm install -g ${PACKAGE_NAME}@latest"\n`);
31
+ return 0;
32
+ }
33
+ if (!json) {
34
+ process.stdout.write(`A new version of artshelf is available: v${info.current} -> v${info.latest}\n`);
35
+ process.stdout.write(`Updating with "npm install -g ${PACKAGE_NAME}@latest"...\n`);
36
+ }
37
+ const result = installGlobalNpmPackage(`${PACKAGE_NAME}@latest`, json ? "pipe" : "inherit");
38
+ const status = result.status ?? 1;
39
+ const spawnError = result.error instanceof Error ? result.error.message : "";
40
+ if (json) {
41
+ const stderr = typeof result.stderr === "string" ? result.stderr : "";
42
+ printJson({
43
+ ok: status === 0,
44
+ updated: status === 0,
45
+ current: info.current,
46
+ latest: info.latest,
47
+ stdout: typeof result.stdout === "string" ? result.stdout : "",
48
+ stderr: appendOutputMessage(stderr, spawnError)
49
+ });
50
+ return status;
51
+ }
52
+ if (spawnError)
53
+ process.stderr.write(`Update failed: ${spawnError}\n`);
54
+ if (status === 0)
55
+ process.stdout.write(`artshelf updated to v${info.latest}\n`);
56
+ return status;
57
+ }
58
+ function appendOutputMessage(output, message) {
59
+ if (!message)
60
+ return output;
61
+ if (!output)
62
+ return message;
63
+ return `${output}${output.endsWith("\n") ? "" : "\n"}${message}`;
64
+ }
65
+ export async function maybeNotifyAvailableUpdate(parsed) {
66
+ if (updateCheckDisabled())
67
+ return;
68
+ if (parsed.command === "update")
69
+ return;
70
+ const info = await getUpdateInfo({ force: false });
71
+ if (!info?.updateAvailable)
72
+ return;
73
+ process.stderr.write(`A new version of artshelf is available: v${info.current} -> v${info.latest}\n`);
74
+ process.stderr.write(`Run "artshelf update" to update npm installs\n`);
75
+ }
@@ -0,0 +1,35 @@
1
+ import { validateLedger } from "../ledger.js";
2
+ import { normalizeRegistryPath } from "../registry.js";
3
+ import { printJson } from "../renderers/json.js";
4
+ import { boolFlag, stringFlag } from "../shared/flags.js";
5
+ import { registeredLedgersOrThrow, validateRegisteredLedger } from "./shared.js";
6
+ export function handleValidate(parsed, ledgerPath, json) {
7
+ if (boolFlag(parsed, "all")) {
8
+ const registryPath = normalizeRegistryPath(stringFlag(parsed, "registry"));
9
+ const results = registeredLedgersOrThrow(registryPath).map((ledger) => ({ ledger, result: validateRegisteredLedger(ledger) }));
10
+ const ok = results.every((entry) => entry.result.ok);
11
+ if (json) {
12
+ printJson({ ok, registryPath, ledgers: results });
13
+ return ok ? 0 : 1;
14
+ }
15
+ for (const entry of results) {
16
+ process.stdout.write(`${entry.result.ok ? "ok" : "invalid"} ${entry.ledger.name}: ${entry.result.entries} entries, ${entry.result.errors.length} errors, ${entry.result.warnings.length} warnings\nledger: ${entry.ledger.path}\n`);
17
+ for (const error of entry.result.errors)
18
+ process.stdout.write(`error: ${error}\n`);
19
+ for (const warning of entry.result.warnings)
20
+ process.stdout.write(`warning: ${warning}\n`);
21
+ }
22
+ process.stdout.write(`registry: ${registryPath}\n`);
23
+ return ok ? 0 : 1;
24
+ }
25
+ const result = validateLedger(ledgerPath);
26
+ if (json)
27
+ return printJson({ ledgerPath, ...result });
28
+ process.stdout.write(`${result.ok ? "ok" : "invalid"}: ${result.entries} entries, ${result.errors.length} errors, ${result.warnings.length} warnings\n`);
29
+ for (const error of result.errors)
30
+ process.stdout.write(`error: ${error}\n`);
31
+ for (const warning of result.warnings)
32
+ process.stdout.write(`warning: ${warning}\n`);
33
+ process.stdout.write(`ledger: ${ledgerPath}\n`);
34
+ return result.ok ? 0 : 1;
35
+ }
@@ -0,0 +1,24 @@
1
+ export function latestVersionOverride(env = process.env) {
2
+ return env.ARTSHELF_LATEST_VERSION;
3
+ }
4
+ export function npmRegistryUrlFromEnv(packageName, env = process.env) {
5
+ return env.ARTSHELF_NPM_REGISTRY_URL ?? `https://registry.npmjs.org/${packageName}/latest`;
6
+ }
7
+ export function updateCheckDisabled(env = process.env) {
8
+ return env.ARTSHELF_NO_UPDATE_CHECK === "1";
9
+ }
10
+ export function updateDryRunEnabled(env = process.env) {
11
+ return env.ARTSHELF_UPDATE_DRY_RUN === "1";
12
+ }
13
+ export function updateCheckTtlMs(env, fallback) {
14
+ return resolveTtlMs(env.ARTSHELF_UPDATE_CHECK_TTL_MS, fallback);
15
+ }
16
+ export function noUpdateCheckTtlMs(env, fallback) {
17
+ return resolveTtlMs(env.ARTSHELF_NO_UPDATE_CHECK_TTL_MS ?? env.ARTSHELF_UPDATE_CHECK_TTL_MS, fallback);
18
+ }
19
+ function resolveTtlMs(value, fallback) {
20
+ if (value === undefined)
21
+ return fallback;
22
+ const parsed = Number(value);
23
+ return Number.isFinite(parsed) ? parsed : fallback;
24
+ }
@@ -0,0 +1,17 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { npmRegistryUrlFromEnv } from "./env.js";
3
+ export const PACKAGE_NAME = "artshelf";
4
+ export const UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1000;
5
+ export const NO_UPDATE_CHECK_TTL_MS = 60 * 60 * 1000;
6
+ export const VERSION = readPackageVersion();
7
+ export function npmRegistryUrl() {
8
+ return npmRegistryUrlFromEnv(PACKAGE_NAME);
9
+ }
10
+ export function readPackageVersion() {
11
+ const packageJsonPath = decodeURIComponent(new URL("../../../package.json", import.meta.url).pathname);
12
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
13
+ if (typeof packageJson.version !== "string") {
14
+ throw new Error("package.json version must be a string");
15
+ }
16
+ return packageJson.version;
17
+ }
@@ -0,0 +1,5 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ export function updateCachePath(env = process.env) {
4
+ return env.ARTSHELF_UPDATE_CACHE ?? join(homedir(), ".artshelf", "update-check.json");
5
+ }
@@ -0,0 +1,3 @@
1
+ export function attentionGlyph(needsAttention) {
2
+ return needsAttention ? "⚠" : "✓";
3
+ }
@@ -0,0 +1,64 @@
1
+ import { attentionGlyph } from "./attention.js";
2
+ const DOCTOR_ATTENTION_CATEGORIES = ["stale", "invalid", "warnings"];
3
+ function doctorAttention(summary) {
4
+ return DOCTOR_ATTENTION_CATEGORIES.filter((key) => summary[key] > 0);
5
+ }
6
+ function doctorNextAction(blockers, summary) {
7
+ if (blockers.length > 0) {
8
+ return `repair ${blockers.length} registry/ledger issue(s) above, then re-run \`artshelf doctor\``;
9
+ }
10
+ if (summary.warnings > 0) {
11
+ return `healthy, but ${summary.warnings} warning(s) noted — run \`artshelf validate --all\` to inspect; nothing is auto-executed`;
12
+ }
13
+ return "artshelf is healthy on this machine — cleanup safety enforced; no action needed";
14
+ }
15
+ export function buildDoctorAgentPacket(report) {
16
+ const blockers = [];
17
+ if (report.registryError)
18
+ blockers.push(`registry unreadable: ${report.registryError}`);
19
+ for (const ledger of report.ledgers) {
20
+ if (ledger.status !== "ok") {
21
+ blockers.push(`${ledger.name} ${ledger.status}${ledger.errors.length ? `: ${ledger.errors[0]}` : ""}`);
22
+ }
23
+ }
24
+ return {
25
+ schemaVersion: 1,
26
+ command: "doctor",
27
+ health: report.ok ? "ok" : "attention",
28
+ version: report.version,
29
+ node: report.node,
30
+ ledgerPath: report.ledgerPath,
31
+ registry: { path: report.registryPath, exists: report.registryExists, ok: report.registryOk, error: report.registryError },
32
+ ledgers: {
33
+ total: report.summary.ledgers,
34
+ ok: report.summary.ok,
35
+ stale: report.summary.stale,
36
+ invalid: report.summary.invalid,
37
+ warnings: report.summary.warnings
38
+ },
39
+ attention: doctorAttention(report.summary),
40
+ blockers,
41
+ cleanupSafety: report.cleanupSafety,
42
+ nextAction: doctorNextAction(blockers, report.summary),
43
+ verification: `artshelf doctor --agent --registry ${report.registryPath}`
44
+ };
45
+ }
46
+ export function printDoctor(report) {
47
+ process.stdout.write(`artshelf ${report.version} (node ${report.node})\n`);
48
+ process.stdout.write(`${attentionGlyph(!report.ok)} health: ${report.ok ? "ok" : "needs attention"}\n`);
49
+ process.stdout.write(`ledger: ${report.ledgerPath}${report.ledgerExists ? "" : " (absent)"}\n`);
50
+ process.stdout.write(`registry: ${report.registryPath}${report.registryExists ? "" : " (absent)"}\n`);
51
+ if (report.registryError)
52
+ process.stdout.write(`registry error: ${report.registryError}\n`);
53
+ process.stdout.write(`registered ledgers: ${report.summary.ledgers} (${report.summary.ok} ok, ${report.summary.stale} stale, ${report.summary.invalid} invalid)\n`);
54
+ for (const ledger of report.ledgers) {
55
+ process.stdout.write(` ${attentionGlyph(ledger.status !== "ok")} ${ledger.status} ${ledger.name} ${ledger.path}\n`);
56
+ for (const message of ledger.errors)
57
+ process.stdout.write(` error: ${message}\n`);
58
+ }
59
+ process.stdout.write("cleanup safety: execute requires a reviewed plan id against a single ledger; --all execute is refused; cleanup=delete is refused; physical trash purge requires a separate reviewed purge plan\n");
60
+ if (!report.ok) {
61
+ for (const message of report.errors)
62
+ process.stdout.write(`error: ${message}\n`);
63
+ }
64
+ }
@@ -0,0 +1,10 @@
1
+ export function printJson(value) {
2
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
3
+ return 0;
4
+ }
5
+ // Agent/compact surface: a single minified JSON line. The default `--json`
6
+ // stays pretty-printed for audit/debug; agent packets optimize for tokens.
7
+ export function printCompactJson(value) {
8
+ process.stdout.write(`${JSON.stringify(value)}\n`);
9
+ return 0;
10
+ }