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
package/CHANGELOG.md CHANGED
@@ -72,6 +72,38 @@
72
72
  backward-compatible audit report. The default human renders of these three
73
73
  commands now lead each ledger and summary line with a `✓`/`⚠` attention glyph
74
74
  (plain Unicode, no color) so redirected output stays clean.
75
+ - Shortened the automatic update-check cache so no-update, failed, missing, or
76
+ null results expire after 1 hour while update-available results keep the
77
+ 24-hour TTL, letting newly published releases surface sooner. `artshelf update`
78
+ forces a fresh latest-version check instead of trusting a stale no-update
79
+ cache, `ARTSHELF_NO_UPDATE_CHECK_TTL_MS` overrides the no-update/failed TTL
80
+ (falling back to `ARTSHELF_UPDATE_CHECK_TTL_MS` for compatibility), and a
81
+ non-numeric TTL value falls back to the default instead of disabling expiry.
82
+
83
+ ## [0.10.2](https://github.com/calvinnwq/artshelf/compare/artshelf-v0.10.1...artshelf-v0.10.2) (2026-06-13)
84
+
85
+
86
+ ### Code Refactoring
87
+
88
+ * **cli:** extract command dispatch and shared modules from the monolithic
89
+ entrypoint ([c198c19](https://github.com/calvinnwq/artshelf/commit/c198c194693e756dd02b2525e2f6abbee5741d59))
90
+ * **cli:** separate status, doctor, review, and JSON renderers from command
91
+ orchestration ([4ec76b0](https://github.com/calvinnwq/artshelf/commit/4ec76b0b0e4f45562d0e98a1237602bc5d41ca67))
92
+ * **cli:** extract update environment, package, path, and process adapter seams
93
+ ([4ec76b0](https://github.com/calvinnwq/artshelf/commit/4ec76b0b0e4f45562d0e98a1237602bc5d41ca67))
94
+ * **cli:** restore real per-command modules, add the validate command module,
95
+ and strengthen architecture guardrails
96
+ ([a617ba3](https://github.com/calvinnwq/artshelf/commit/a617ba36e7de7d8a5725d1ba47eb3419ae3c6329))
97
+ * **cli:** move help rendering out of the entrypoint and into shared help text
98
+ ([8e2698b](https://github.com/calvinnwq/artshelf/commit/8e2698bce1b47073d434113cbe1e8cfb32ec34e2))
99
+
100
+ ## [0.10.1](https://github.com/calvinnwq/artshelf/compare/artshelf-v0.10.0...artshelf-v0.10.1) (2026-06-12)
101
+
102
+
103
+ ### Bug Fixes
104
+
105
+ * **cli:** shorten no-update cache TTL for update checks ([d41e49e](https://github.com/calvinnwq/artshelf/commit/d41e49e7d5da02dfaa86fb70eaa7d5e7fb3d543e))
106
+ * **cli:** split update-check cache TTL so new releases surface sooner ([5afcfaa](https://github.com/calvinnwq/artshelf/commit/5afcfaafac4941b71f6a84c694139a64774a1d59))
75
107
 
76
108
  ## [0.10.0](https://github.com/calvinnwq/artshelf/compare/artshelf-v0.9.0...artshelf-v0.10.0) (2026-06-12)
77
109
 
package/README.md CHANGED
@@ -62,12 +62,14 @@ install with `npm unlink -g artshelf`.
62
62
  </details>
63
63
 
64
64
  Artshelf checks npm occasionally and prints a non-blocking notice to stderr when
65
- a newer published version is available. Run `artshelf update` only for npm
66
- global installs; it upgrades with `npm install -g artshelf@latest`. pnpm global
67
- installs should update with `pnpm add -g artshelf@latest`, and source installs
68
- still update by pulling, rebuilding, and linking the checkout. Set
69
- `ARTSHELF_NO_UPDATE_CHECK=1` for scheduled jobs that must avoid network and
70
- update-cache writes.
65
+ a newer published version is available. Available-update results are cached for
66
+ 24 hours by default; failed, missing, or no-update results are cached for 1 hour
67
+ so a newly published release is noticed sooner. Run `artshelf update` only for
68
+ npm global installs; it forces a fresh latest-version check before upgrading
69
+ with `npm install -g artshelf@latest`. pnpm global installs should update with
70
+ `pnpm add -g artshelf@latest`, and source installs still update by pulling,
71
+ rebuilding, and linking the checkout. Set `ARTSHELF_NO_UPDATE_CHECK=1` for
72
+ scheduled jobs that must avoid network and update-cache writes.
71
73
 
72
74
  ### Recommended agent setup
73
75
 
package/SPEC.md CHANGED
@@ -261,7 +261,7 @@ the next action always points at an explicit follow-up command.
261
261
 
262
262
  `review`, `status`, and `doctor` share three render modes. The default human
263
263
  render leads each ledger and summary line with a `✓`/`⚠` attention glyph; `--json`
264
- stays the full, backward-compatible audit report; and `--agent` emits a compact,
264
+ stays the full, backward-compatible public audit report; and `--agent` emits a compact,
265
265
  deterministic single-line JSON decision packet for agents, taking precedence over
266
266
  `--json` when both are passed. For `review`, the packet sorts records into
267
267
  ready-for-approval, needs-review-first, and blocked groups. Because review is
@@ -353,12 +353,17 @@ Rules:
353
353
  - Read-only command guarantees refer to ledger and artifact mutation; automatic
354
354
  update-check cache writes are separate and can be disabled.
355
355
  - Update notices must never pollute JSON stdout.
356
- - Automatic checks cache successful and failed latest-version lookups at
357
- `~/.artshelf/update-check.json` by default, with a 24-hour TTL.
356
+ - Automatic checks cache latest-version lookups at
357
+ `~/.artshelf/update-check.json` by default. Cached update-available results
358
+ (`latest > current`) keep the long 24-hour TTL; cached no-update, failed,
359
+ missing, or null results use a shorter 1-hour TTL so newly published releases
360
+ are noticed sooner.
358
361
  - `ARTSHELF_NO_UPDATE_CHECK=1` disables automatic checks for scheduled jobs,
359
362
  tests, and no-network environments.
360
363
  - `ARTSHELF_UPDATE_CACHE` overrides the update-cache path,
361
- `ARTSHELF_UPDATE_CHECK_TTL_MS` overrides the cache TTL, and
364
+ `ARTSHELF_UPDATE_CHECK_TTL_MS` overrides the update-available cache TTL,
365
+ `ARTSHELF_NO_UPDATE_CHECK_TTL_MS` overrides the no-update/failed cache TTL
366
+ (falling back to `ARTSHELF_UPDATE_CHECK_TTL_MS` for compatibility), and
362
367
  `ARTSHELF_NPM_REGISTRY_URL` overrides the npm latest-version endpoint.
363
368
  - `ARTSHELF_LATEST_VERSION` overrides the discovered latest version for tests.
364
369
  - `ARTSHELF_UPDATE_DRY_RUN=1` makes `artshelf update` report the npm command it
@@ -541,9 +546,13 @@ V1 also supports a user-level registry of known ledgers:
541
546
  overrides it for tests and controlled runs; legacy `SHELF_NOW` is read only
542
547
  when `ARTSHELF_NOW` is unset.
543
548
  - Automatic npm update checks cache their latest-version result at
544
- `~/.artshelf/update-check.json` by default. `ARTSHELF_NO_UPDATE_CHECK=1`
545
- disables automatic checks, `ARTSHELF_UPDATE_CACHE` overrides the cache path,
546
- and `ARTSHELF_UPDATE_CHECK_TTL_MS` overrides the cache TTL.
549
+ `~/.artshelf/update-check.json` by default. Cached update-available results
550
+ use the long 24-hour TTL; cached no-update, failed, missing, or null results
551
+ use a shorter 1-hour TTL. `ARTSHELF_NO_UPDATE_CHECK=1` disables automatic
552
+ checks, `ARTSHELF_UPDATE_CACHE` overrides the cache path,
553
+ `ARTSHELF_UPDATE_CHECK_TTL_MS` overrides the update-available TTL, and
554
+ `ARTSHELF_NO_UPDATE_CHECK_TTL_MS` overrides the no-update/failed TTL
555
+ (falling back to `ARTSHELF_UPDATE_CHECK_TTL_MS` for compatibility).
547
556
  - `put` registers the ledger it writes to.
548
557
  - `ledgers add` registers an existing ledger explicitly.
549
558
  - `--all` reads registered ledgers as one review surface.
@@ -0,0 +1,7 @@
1
+ import { spawnSync } from "node:child_process";
2
+ export function installGlobalNpmPackage(packageSpec, mode) {
3
+ if (mode === "pipe") {
4
+ return spawnSync("npm", ["install", "-g", packageSpec], { encoding: "utf8" });
5
+ }
6
+ return spawnSync("npm", ["install", "-g", packageSpec], { encoding: "utf8", stdio: "inherit" });
7
+ }
@@ -0,0 +1,143 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { latestVersionOverride, noUpdateCheckTtlMs, updateCheckTtlMs } from "../config/env.js";
4
+ import { NO_UPDATE_CHECK_TTL_MS, UPDATE_CHECK_TTL_MS, VERSION, npmRegistryUrl } from "../config/package.js";
5
+ import { updateCachePath } from "../config/paths.js";
6
+ export async function getUpdateInfo(options) {
7
+ return createDefaultUpdateAdapter().getUpdateInfo(options);
8
+ }
9
+ export function createUpdateAdapter(options) {
10
+ async function getUpdateInfo(optionsForCheck) {
11
+ const latest = await getLatestVersion(optionsForCheck);
12
+ if (!latest)
13
+ return null;
14
+ return {
15
+ current: options.currentVersion,
16
+ latest,
17
+ updateAvailable: compareVersions(latest, options.currentVersion) > 0
18
+ };
19
+ }
20
+ async function getLatestVersion(optionsForCheck) {
21
+ const override = latestVersionOverride(options.env);
22
+ if (override)
23
+ return normalizeVersion(override);
24
+ if (!optionsForCheck.force) {
25
+ const cached = readUpdateCache();
26
+ if (cached)
27
+ return cached.latest;
28
+ }
29
+ const latest = await options.fetchLatestVersion(options.registryUrl);
30
+ writeUpdateCache(latest);
31
+ return latest;
32
+ }
33
+ function readUpdateCache() {
34
+ const cachePath = options.cachePath();
35
+ if (!options.fileExists(cachePath))
36
+ return null;
37
+ try {
38
+ const cache = JSON.parse(options.readTextFile(cachePath));
39
+ if (!("latest" in cache))
40
+ cache.latest = null;
41
+ if (cache.latest !== null && typeof cache.latest !== "string")
42
+ return null;
43
+ if (typeof cache.checkedAt !== "number")
44
+ return null;
45
+ const latest = cache.latest === null ? null : normalizeVersion(cache.latest);
46
+ const ttl = updateCacheTtlFor(latest);
47
+ if (ttl < 0)
48
+ return null;
49
+ if (options.now() - cache.checkedAt > ttl)
50
+ return null;
51
+ return { latest };
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ function updateCacheTtlFor(latest) {
58
+ if (latest && compareVersions(latest, options.currentVersion) > 0) {
59
+ return updateCheckTtlMs(options.env, UPDATE_CHECK_TTL_MS);
60
+ }
61
+ return noUpdateCheckTtlMs(options.env, NO_UPDATE_CHECK_TTL_MS);
62
+ }
63
+ function writeUpdateCache(latest) {
64
+ try {
65
+ const cachePath = options.cachePath();
66
+ const dir = dirname(cachePath);
67
+ if (dir) {
68
+ options.ensureDirectory(dir);
69
+ options.writeTextFile(cachePath, `${JSON.stringify({ latest, checkedAt: options.now() }, null, 2)}\n`);
70
+ }
71
+ }
72
+ catch {
73
+ // Update checks should never affect normal CLI behavior.
74
+ }
75
+ }
76
+ return { getUpdateInfo };
77
+ }
78
+ function createDefaultUpdateAdapter() {
79
+ const registryUrl = npmRegistryUrl();
80
+ return createUpdateAdapter({
81
+ currentVersion: VERSION,
82
+ registryUrl,
83
+ env: process.env,
84
+ now: () => Date.now(),
85
+ cachePath: () => updateCachePath(),
86
+ fileExists: existsSync,
87
+ readTextFile: (path) => readFileSync(path, "utf8"),
88
+ writeTextFile: writeFileSync,
89
+ ensureDirectory: (path) => mkdirSync(path, { recursive: true }),
90
+ fetchLatestVersion: (url) => fetchLatestNpmVersion(url)
91
+ });
92
+ }
93
+ async function fetchLatestNpmVersion(registryUrl) {
94
+ const controller = new AbortController();
95
+ const timeout = setTimeout(() => controller.abort(), 750);
96
+ try {
97
+ const response = await fetch(registryUrl, {
98
+ signal: controller.signal,
99
+ headers: { accept: "application/json", "user-agent": `artshelf/${VERSION}` }
100
+ });
101
+ if (!response.ok)
102
+ return null;
103
+ const body = await response.json();
104
+ if (!body || typeof body !== "object" || typeof body.version !== "string")
105
+ return null;
106
+ return normalizeVersion(body.version);
107
+ }
108
+ catch {
109
+ return null;
110
+ }
111
+ finally {
112
+ clearTimeout(timeout);
113
+ }
114
+ }
115
+ function normalizeVersion(version) {
116
+ return version.trim().replace(/^v/i, "");
117
+ }
118
+ function compareVersions(left, right) {
119
+ const a = parseVersion(left);
120
+ const b = parseVersion(right);
121
+ for (let index = 0; index < Math.max(a.numbers.length, b.numbers.length); index += 1) {
122
+ const diff = (a.numbers[index] ?? 0) - (b.numbers[index] ?? 0);
123
+ if (diff !== 0)
124
+ return diff;
125
+ }
126
+ if (a.prerelease === b.prerelease)
127
+ return 0;
128
+ if (!a.prerelease)
129
+ return 1;
130
+ if (!b.prerelease)
131
+ return -1;
132
+ return a.prerelease.localeCompare(b.prerelease);
133
+ }
134
+ function parseVersion(version) {
135
+ const [main = "", prerelease = ""] = normalizeVersion(version).split("-", 2);
136
+ return {
137
+ numbers: main.split(".").map((part) => {
138
+ const parsed = Number.parseInt(part, 10);
139
+ return Number.isFinite(parsed) ? parsed : 0;
140
+ }),
141
+ prerelease
142
+ };
143
+ }