codealmanac 0.1.3 → 0.1.5

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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { createRequire as createRequire4 } from "module";
4
+ import { createRequire as createRequire7 } from "module";
5
5
  import { basename as basename6 } from "path";
6
6
  import { Command } from "commander";
7
7
 
@@ -840,7 +840,8 @@ async function runCapture(options) {
840
840
  }
841
841
  };
842
842
  const now = options.now?.() ?? /* @__PURE__ */ new Date();
843
- const logName = `.capture-${formatTimestamp2(now)}.log`;
843
+ const logStem = options.sessionId !== void 0 && options.sessionId.length > 0 ? options.sessionId : formatTimestamp2(now);
844
+ const logName = `.capture-${logStem}.jsonl`;
844
845
  const logPath = join5(almanacDir, logName);
845
846
  const logStream = createWriteStream2(logPath, { flags: "w" });
846
847
  const out = process.stdout;
@@ -1061,123 +1062,12 @@ function closeStream2(stream) {
1061
1062
  }
1062
1063
 
1063
1064
  // src/commands/doctor.ts
1064
- import { existsSync as existsSync12, readdirSync, statSync as statSync3 } from "fs";
1065
- import { readFile as readFile10 } from "fs/promises";
1066
- import { createRequire as createRequire3 } from "module";
1065
+ import { existsSync as existsSync12, readFileSync as readFileSync2, readdirSync, statSync as statSync3 } from "fs";
1066
+ import { readFile as readFile12 } from "fs/promises";
1067
+ import { createRequire as createRequire4 } from "module";
1067
1068
  import { homedir as homedir5 } from "os";
1068
1069
  import path4 from "path";
1069
-
1070
- // src/indexer/schema.ts
1071
- import Database from "better-sqlite3";
1072
- var SCHEMA_DDL = `
1073
- CREATE TABLE IF NOT EXISTS pages (
1074
- slug TEXT PRIMARY KEY,
1075
- title TEXT,
1076
- file_path TEXT NOT NULL,
1077
- content_hash TEXT NOT NULL,
1078
- updated_at INTEGER NOT NULL,
1079
- archived_at INTEGER,
1080
- superseded_by TEXT
1081
- );
1082
-
1083
- CREATE TABLE IF NOT EXISTS topics (
1084
- slug TEXT PRIMARY KEY,
1085
- title TEXT,
1086
- description TEXT
1087
- );
1088
-
1089
- CREATE TABLE IF NOT EXISTS page_topics (
1090
- page_slug TEXT NOT NULL REFERENCES pages(slug) ON DELETE CASCADE,
1091
- topic_slug TEXT NOT NULL,
1092
- PRIMARY KEY (page_slug, topic_slug)
1093
- );
1094
-
1095
- CREATE TABLE IF NOT EXISTS topic_parents (
1096
- child_slug TEXT NOT NULL,
1097
- parent_slug TEXT NOT NULL,
1098
- PRIMARY KEY (child_slug, parent_slug),
1099
- CHECK (child_slug != parent_slug)
1100
- );
1101
-
1102
- CREATE TABLE IF NOT EXISTS file_refs (
1103
- page_slug TEXT NOT NULL REFERENCES pages(slug) ON DELETE CASCADE,
1104
- path TEXT NOT NULL,
1105
- original_path TEXT NOT NULL,
1106
- is_dir INTEGER NOT NULL,
1107
- PRIMARY KEY (page_slug, path)
1108
- );
1109
- CREATE INDEX IF NOT EXISTS idx_file_refs_path ON file_refs(path);
1110
-
1111
- CREATE TABLE IF NOT EXISTS wikilinks (
1112
- source_slug TEXT NOT NULL REFERENCES pages(slug) ON DELETE CASCADE,
1113
- target_slug TEXT NOT NULL,
1114
- PRIMARY KEY (source_slug, target_slug)
1115
- );
1116
-
1117
- CREATE TABLE IF NOT EXISTS cross_wiki_links (
1118
- source_slug TEXT NOT NULL REFERENCES pages(slug) ON DELETE CASCADE,
1119
- target_wiki TEXT NOT NULL,
1120
- target_slug TEXT NOT NULL,
1121
- PRIMARY KEY (source_slug, target_wiki, target_slug)
1122
- );
1123
-
1124
- -- NOTE: virtual FTS5 table \u2014 ON DELETE CASCADE from pages does NOT apply.
1125
- -- The indexer must explicitly DELETE FROM fts_pages whenever it removes
1126
- -- or replaces a page row, or we leak orphaned FTS rows.
1127
- CREATE VIRTUAL TABLE IF NOT EXISTS fts_pages USING fts5(slug, title, content);
1128
- `;
1129
- var SCHEMA_VERSION = 2;
1130
- function openIndex(dbPath) {
1131
- const db = new Database(dbPath);
1132
- const mode = db.pragma("journal_mode", { simple: true });
1133
- if (typeof mode !== "string" || mode.toLowerCase() !== "wal") {
1134
- db.pragma("journal_mode = WAL");
1135
- }
1136
- db.pragma("foreign_keys = ON");
1137
- const rawVersion = db.pragma("user_version", { simple: true });
1138
- const currentVersion = typeof rawVersion === "number" ? rawVersion : 0;
1139
- if (currentVersion < SCHEMA_VERSION) {
1140
- db.exec("DROP TABLE IF EXISTS file_refs");
1141
- try {
1142
- db.exec("UPDATE pages SET content_hash = ''");
1143
- } catch {
1144
- }
1145
- db.pragma(`user_version = ${SCHEMA_VERSION}`);
1146
- }
1147
- db.exec(SCHEMA_DDL);
1148
- return db;
1149
- }
1150
-
1151
- // src/commands/health.ts
1152
- import { existsSync as existsSync9 } from "fs";
1153
- import { readFile as readFile7 } from "fs/promises";
1154
- import { basename as basename4, join as join8 } from "path";
1155
- import fg2 from "fast-glob";
1156
-
1157
- // src/indexer/duration.ts
1158
- function parseDuration(input) {
1159
- const trimmed = input.trim();
1160
- const m = trimmed.match(/^(\d+)([mhdw])$/);
1161
- if (m === null) {
1162
- throw new Error(
1163
- `invalid duration "${input}" (expected Nw, Nd, Nh, or Nm \u2014 e.g. 2w, 30d)`
1164
- );
1165
- }
1166
- const n = Number.parseInt(m[1] ?? "0", 10);
1167
- const unit = m[2];
1168
- switch (unit) {
1169
- case "m":
1170
- return n * 60;
1171
- case "h":
1172
- return n * 60 * 60;
1173
- case "d":
1174
- return n * 60 * 60 * 24;
1175
- case "w":
1176
- return n * 60 * 60 * 24 * 7;
1177
- default:
1178
- throw new Error(`invalid duration unit "${unit ?? ""}"`);
1179
- }
1180
- }
1070
+ import { fileURLToPath as fileURLToPath4 } from "url";
1181
1071
 
1182
1072
  // src/indexer/index.ts
1183
1073
  import { createHash as createHash2 } from "crypto";
@@ -1339,6 +1229,87 @@ function looksLikeDir(raw) {
1339
1229
  return s.endsWith("/");
1340
1230
  }
1341
1231
 
1232
+ // src/indexer/schema.ts
1233
+ import Database from "better-sqlite3";
1234
+ var SCHEMA_DDL = `
1235
+ CREATE TABLE IF NOT EXISTS pages (
1236
+ slug TEXT PRIMARY KEY,
1237
+ title TEXT,
1238
+ file_path TEXT NOT NULL,
1239
+ content_hash TEXT NOT NULL,
1240
+ updated_at INTEGER NOT NULL,
1241
+ archived_at INTEGER,
1242
+ superseded_by TEXT
1243
+ );
1244
+
1245
+ CREATE TABLE IF NOT EXISTS topics (
1246
+ slug TEXT PRIMARY KEY,
1247
+ title TEXT,
1248
+ description TEXT
1249
+ );
1250
+
1251
+ CREATE TABLE IF NOT EXISTS page_topics (
1252
+ page_slug TEXT NOT NULL REFERENCES pages(slug) ON DELETE CASCADE,
1253
+ topic_slug TEXT NOT NULL,
1254
+ PRIMARY KEY (page_slug, topic_slug)
1255
+ );
1256
+
1257
+ CREATE TABLE IF NOT EXISTS topic_parents (
1258
+ child_slug TEXT NOT NULL,
1259
+ parent_slug TEXT NOT NULL,
1260
+ PRIMARY KEY (child_slug, parent_slug),
1261
+ CHECK (child_slug != parent_slug)
1262
+ );
1263
+
1264
+ CREATE TABLE IF NOT EXISTS file_refs (
1265
+ page_slug TEXT NOT NULL REFERENCES pages(slug) ON DELETE CASCADE,
1266
+ path TEXT NOT NULL,
1267
+ original_path TEXT NOT NULL,
1268
+ is_dir INTEGER NOT NULL,
1269
+ PRIMARY KEY (page_slug, path)
1270
+ );
1271
+ CREATE INDEX IF NOT EXISTS idx_file_refs_path ON file_refs(path);
1272
+
1273
+ CREATE TABLE IF NOT EXISTS wikilinks (
1274
+ source_slug TEXT NOT NULL REFERENCES pages(slug) ON DELETE CASCADE,
1275
+ target_slug TEXT NOT NULL,
1276
+ PRIMARY KEY (source_slug, target_slug)
1277
+ );
1278
+
1279
+ CREATE TABLE IF NOT EXISTS cross_wiki_links (
1280
+ source_slug TEXT NOT NULL REFERENCES pages(slug) ON DELETE CASCADE,
1281
+ target_wiki TEXT NOT NULL,
1282
+ target_slug TEXT NOT NULL,
1283
+ PRIMARY KEY (source_slug, target_wiki, target_slug)
1284
+ );
1285
+
1286
+ -- NOTE: virtual FTS5 table \u2014 ON DELETE CASCADE from pages does NOT apply.
1287
+ -- The indexer must explicitly DELETE FROM fts_pages whenever it removes
1288
+ -- or replaces a page row, or we leak orphaned FTS rows.
1289
+ CREATE VIRTUAL TABLE IF NOT EXISTS fts_pages USING fts5(slug, title, content);
1290
+ `;
1291
+ var SCHEMA_VERSION = 2;
1292
+ function openIndex(dbPath) {
1293
+ const db = new Database(dbPath);
1294
+ const mode = db.pragma("journal_mode", { simple: true });
1295
+ if (typeof mode !== "string" || mode.toLowerCase() !== "wal") {
1296
+ db.pragma("journal_mode = WAL");
1297
+ }
1298
+ db.pragma("foreign_keys = ON");
1299
+ const rawVersion = db.pragma("user_version", { simple: true });
1300
+ const currentVersion = typeof rawVersion === "number" ? rawVersion : 0;
1301
+ if (currentVersion < SCHEMA_VERSION) {
1302
+ db.exec("DROP TABLE IF EXISTS file_refs");
1303
+ try {
1304
+ db.exec("UPDATE pages SET content_hash = ''");
1305
+ } catch {
1306
+ }
1307
+ db.pragma(`user_version = ${SCHEMA_VERSION}`);
1308
+ }
1309
+ db.exec(SCHEMA_DDL);
1310
+ return db;
1311
+ }
1312
+
1342
1313
  // src/indexer/wikilinks.ts
1343
1314
  function classifyWikilink(raw) {
1344
1315
  const pipe = raw.indexOf("|");
@@ -1708,16 +1679,330 @@ async function applyTopicsYaml(db, topicsYamlPath2) {
1708
1679
  apply();
1709
1680
  }
1710
1681
 
1682
+ // src/update/config.ts
1683
+ import { mkdir as mkdir4, readFile as readFile7, rename as rename3, writeFile as writeFile4 } from "fs/promises";
1684
+ import { dirname as dirname5, join as join7 } from "path";
1685
+ function defaultConfig() {
1686
+ return { update_notifier: true };
1687
+ }
1688
+ function getConfigPath() {
1689
+ return join7(getGlobalAlmanacDir(), "config.json");
1690
+ }
1691
+ async function readConfig(path6) {
1692
+ const file = path6 ?? getConfigPath();
1693
+ let raw;
1694
+ try {
1695
+ raw = await readFile7(file, "utf8");
1696
+ } catch {
1697
+ return defaultConfig();
1698
+ }
1699
+ const trimmed = raw.trim();
1700
+ if (trimmed.length === 0) return defaultConfig();
1701
+ try {
1702
+ const parsed = JSON.parse(trimmed);
1703
+ return {
1704
+ update_notifier: typeof parsed.update_notifier === "boolean" ? parsed.update_notifier : true
1705
+ };
1706
+ } catch {
1707
+ return defaultConfig();
1708
+ }
1709
+ }
1710
+ async function writeConfig(config, path6) {
1711
+ const file = path6 ?? getConfigPath();
1712
+ await mkdir4(dirname5(file), { recursive: true });
1713
+ const body = `${JSON.stringify(config, null, 2)}
1714
+ `;
1715
+ const tmp = `${file}.tmp`;
1716
+ await writeFile4(tmp, body, "utf8");
1717
+ await rename3(tmp, file);
1718
+ }
1719
+
1720
+ // src/update/schedule.ts
1721
+ import { spawn as spawn2 } from "child_process";
1722
+ import { readFileSync } from "fs";
1723
+
1724
+ // src/update/check.ts
1725
+ import { createRequire as createRequire2 } from "module";
1726
+
1727
+ // src/update/state.ts
1728
+ import { mkdir as mkdir5, readFile as readFile8, rename as rename4, writeFile as writeFile5 } from "fs/promises";
1729
+ import { dirname as dirname6, join as join8 } from "path";
1730
+ function emptyState() {
1731
+ return {
1732
+ last_check_at: 0,
1733
+ installed_version: "",
1734
+ latest_version: "",
1735
+ dismissed_versions: []
1736
+ };
1737
+ }
1738
+ function getStatePath() {
1739
+ return join8(getGlobalAlmanacDir(), "update-state.json");
1740
+ }
1741
+ async function readState(path6) {
1742
+ const file = path6 ?? getStatePath();
1743
+ let raw;
1744
+ try {
1745
+ raw = await readFile8(file, "utf8");
1746
+ } catch {
1747
+ return emptyState();
1748
+ }
1749
+ const trimmed = raw.trim();
1750
+ if (trimmed.length === 0) return emptyState();
1751
+ try {
1752
+ const parsed = JSON.parse(trimmed);
1753
+ return {
1754
+ last_check_at: typeof parsed.last_check_at === "number" ? parsed.last_check_at : 0,
1755
+ installed_version: typeof parsed.installed_version === "string" ? parsed.installed_version : "",
1756
+ latest_version: typeof parsed.latest_version === "string" ? parsed.latest_version : "",
1757
+ dismissed_versions: Array.isArray(parsed.dismissed_versions) ? parsed.dismissed_versions.filter(
1758
+ (v) => typeof v === "string" && v.length > 0
1759
+ ) : [],
1760
+ last_fetch_failed_at: typeof parsed.last_fetch_failed_at === "number" ? parsed.last_fetch_failed_at : void 0
1761
+ };
1762
+ } catch {
1763
+ return emptyState();
1764
+ }
1765
+ }
1766
+ async function writeState(state, path6) {
1767
+ const file = path6 ?? getStatePath();
1768
+ await mkdir5(dirname6(file), { recursive: true });
1769
+ const body = `${JSON.stringify(state, null, 2)}
1770
+ `;
1771
+ const tmp = `${file}.tmp`;
1772
+ await writeFile5(tmp, body, "utf8");
1773
+ await rename4(tmp, file);
1774
+ }
1775
+
1776
+ // src/update/check.ts
1777
+ var DEFAULT_CACHE_SECONDS = 24 * 60 * 60;
1778
+ var DEFAULT_TIMEOUT_MS = 3e3;
1779
+ var REGISTRY_URL = "https://registry.npmjs.org/codealmanac";
1780
+ async function checkForUpdate(opts = {}) {
1781
+ const now = opts.now ?? (() => Math.floor(Date.now() / 1e3));
1782
+ const cacheSeconds = opts.cacheSeconds ?? DEFAULT_CACHE_SECONDS;
1783
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1784
+ const fetchFn = opts.fetchFn ?? globalThis.fetch;
1785
+ const installed = opts.installedVersion ?? readInstalledVersion();
1786
+ const state = await readState(opts.statePath);
1787
+ if (!opts.force && state.last_check_at > 0 && now() - state.last_check_at < cacheSeconds) {
1788
+ return { state, fetched: false, fetchFailed: false };
1789
+ }
1790
+ let latest = null;
1791
+ let failed = false;
1792
+ try {
1793
+ const ac = new AbortController();
1794
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
1795
+ try {
1796
+ const res = await fetchFn(REGISTRY_URL, {
1797
+ signal: ac.signal,
1798
+ headers: { accept: "application/json" }
1799
+ });
1800
+ if (!res.ok) {
1801
+ failed = true;
1802
+ } else {
1803
+ const body = await res.json();
1804
+ const tag = body["dist-tags"]?.latest;
1805
+ if (typeof tag === "string" && tag.length > 0) {
1806
+ latest = tag;
1807
+ } else {
1808
+ failed = true;
1809
+ }
1810
+ }
1811
+ } finally {
1812
+ clearTimeout(timer);
1813
+ }
1814
+ } catch {
1815
+ failed = true;
1816
+ }
1817
+ if (failed || latest === null) {
1818
+ const next2 = {
1819
+ ...state,
1820
+ // We still bump last_check_at on a failed attempt; without this,
1821
+ // every subsequent command would re-try the registry. A one-shot
1822
+ // retry on the next invocation is enough; sustained failure gets
1823
+ // retried on the 24h cadence like a success.
1824
+ last_check_at: now(),
1825
+ installed_version: installed,
1826
+ last_fetch_failed_at: now()
1827
+ };
1828
+ try {
1829
+ await writeState(next2, opts.statePath);
1830
+ } catch {
1831
+ }
1832
+ return { state: next2, fetched: true, fetchFailed: true };
1833
+ }
1834
+ const next = {
1835
+ last_check_at: now(),
1836
+ installed_version: installed,
1837
+ latest_version: latest,
1838
+ dismissed_versions: state.dismissed_versions,
1839
+ // Clear the failure marker on success.
1840
+ last_fetch_failed_at: void 0
1841
+ };
1842
+ try {
1843
+ await writeState(next, opts.statePath);
1844
+ } catch {
1845
+ }
1846
+ return { state: next, fetched: true, fetchFailed: false };
1847
+ }
1848
+ function readInstalledVersion() {
1849
+ try {
1850
+ const require2 = createRequire2(import.meta.url);
1851
+ const pkg = require2("../../package.json");
1852
+ if (typeof pkg.version === "string" && pkg.version.length > 0) {
1853
+ return pkg.version;
1854
+ }
1855
+ } catch {
1856
+ }
1857
+ try {
1858
+ const require2 = createRequire2(import.meta.url);
1859
+ const pkg = require2("../package.json");
1860
+ if (typeof pkg.version === "string" && pkg.version.length > 0) {
1861
+ return pkg.version;
1862
+ }
1863
+ } catch {
1864
+ }
1865
+ return "unknown";
1866
+ }
1867
+
1868
+ // src/update/schedule.ts
1869
+ function scheduleBackgroundUpdateCheck(argv) {
1870
+ if (!shouldSchedule(argv)) return;
1871
+ const scriptPath = argv[1];
1872
+ const nodeBin = process.execPath;
1873
+ if (scriptPath === void 0 || scriptPath.length === 0) return;
1874
+ try {
1875
+ const child = spawn2(
1876
+ nodeBin,
1877
+ [scriptPath, "--internal-check-updates"],
1878
+ {
1879
+ detached: true,
1880
+ stdio: "ignore"
1881
+ // Windows: with `detached: true` and no `stdio`, Node opens a
1882
+ // console window — `"ignore"` prevents that.
1883
+ }
1884
+ );
1885
+ child.unref();
1886
+ child.on("error", () => {
1887
+ });
1888
+ } catch {
1889
+ }
1890
+ }
1891
+ function shouldSchedule(argv) {
1892
+ if (process.env.CODEALMANAC_SKIP_UPDATE_CHECK === "1") return false;
1893
+ if (process.env.NODE_ENV === "test") return false;
1894
+ if (process.env.VITEST !== void 0) return false;
1895
+ if (argv.slice(2).includes("--internal-check-updates")) return false;
1896
+ if (!notifierEnabled()) return false;
1897
+ return true;
1898
+ }
1899
+ function notifierEnabled() {
1900
+ try {
1901
+ const raw = readFileSync(getConfigPath(), "utf8");
1902
+ const parsed = JSON.parse(raw);
1903
+ if (parsed.update_notifier === false) return false;
1904
+ return true;
1905
+ } catch {
1906
+ return true;
1907
+ }
1908
+ }
1909
+ async function runInternalUpdateCheck() {
1910
+ try {
1911
+ await checkForUpdate({});
1912
+ } catch {
1913
+ }
1914
+ }
1915
+ function readStateForDoctor(path6) {
1916
+ const file = path6 ?? getStatePath();
1917
+ try {
1918
+ const raw = readFileSync(file, "utf8");
1919
+ const trimmed = raw.trim();
1920
+ if (trimmed.length === 0) return null;
1921
+ const parsed = JSON.parse(trimmed);
1922
+ return {
1923
+ last_check_at: typeof parsed.last_check_at === "number" ? parsed.last_check_at : 0,
1924
+ installed_version: typeof parsed.installed_version === "string" ? parsed.installed_version : "",
1925
+ latest_version: typeof parsed.latest_version === "string" ? parsed.latest_version : "",
1926
+ dismissed_versions: Array.isArray(parsed.dismissed_versions) ? parsed.dismissed_versions.filter((v) => typeof v === "string") : [],
1927
+ last_fetch_failed_at: typeof parsed.last_fetch_failed_at === "number" ? parsed.last_fetch_failed_at : void 0
1928
+ };
1929
+ } catch {
1930
+ return null;
1931
+ }
1932
+ }
1933
+
1934
+ // src/update/semver.ts
1935
+ function parse(v) {
1936
+ const trimmed = v.trim().replace(/^v/i, "");
1937
+ const noBuild = trimmed.split("+")[0] ?? "";
1938
+ const dashAt = noBuild.indexOf("-");
1939
+ const core = dashAt === -1 ? noBuild : noBuild.slice(0, dashAt);
1940
+ const pre = dashAt === -1 ? "" : noBuild.slice(dashAt + 1);
1941
+ const parts = core.split(".").map((p) => Number.parseInt(p, 10));
1942
+ if (parts.length === 0 || parts.some((n) => !Number.isFinite(n) || n < 0)) {
1943
+ return null;
1944
+ }
1945
+ return {
1946
+ major: parts[0] ?? 0,
1947
+ minor: parts[1] ?? 0,
1948
+ patch: parts[2] ?? 0,
1949
+ pre
1950
+ };
1951
+ }
1952
+ function isNewer(latest, installed) {
1953
+ const a = parse(latest);
1954
+ const b = parse(installed);
1955
+ if (a === null || b === null) return false;
1956
+ if (a.major !== b.major) return a.major > b.major;
1957
+ if (a.minor !== b.minor) return a.minor > b.minor;
1958
+ if (a.patch !== b.patch) return a.patch > b.patch;
1959
+ if (a.pre === b.pre) return false;
1960
+ if (a.pre === "" && b.pre !== "") return true;
1961
+ if (a.pre !== "" && b.pre === "") return false;
1962
+ return a.pre > b.pre;
1963
+ }
1964
+
1965
+ // src/commands/health.ts
1966
+ import { existsSync as existsSync9 } from "fs";
1967
+ import { readFile as readFile9 } from "fs/promises";
1968
+ import { basename as basename4, join as join10 } from "path";
1969
+ import fg2 from "fast-glob";
1970
+
1971
+ // src/indexer/duration.ts
1972
+ function parseDuration(input) {
1973
+ const trimmed = input.trim();
1974
+ const m = trimmed.match(/^(\d+)([mhdw])$/);
1975
+ if (m === null) {
1976
+ throw new Error(
1977
+ `invalid duration "${input}" (expected Nw, Nd, Nh, or Nm \u2014 e.g. 2w, 30d)`
1978
+ );
1979
+ }
1980
+ const n = Number.parseInt(m[1] ?? "0", 10);
1981
+ const unit = m[2];
1982
+ switch (unit) {
1983
+ case "m":
1984
+ return n * 60;
1985
+ case "h":
1986
+ return n * 60 * 60;
1987
+ case "d":
1988
+ return n * 60 * 60 * 24;
1989
+ case "w":
1990
+ return n * 60 * 60 * 24 * 7;
1991
+ default:
1992
+ throw new Error(`invalid duration unit "${unit ?? ""}"`);
1993
+ }
1994
+ }
1995
+
1711
1996
  // src/indexer/resolveWiki.ts
1712
1997
  import { existsSync as existsSync8 } from "fs";
1713
- import { join as join7 } from "path";
1998
+ import { join as join9 } from "path";
1714
1999
  async function resolveWikiRoot(params) {
1715
2000
  if (params.wiki !== void 0) {
1716
2001
  const entry = await findEntry({ name: params.wiki });
1717
2002
  if (entry === null) {
1718
2003
  throw new Error(`no registered wiki named "${params.wiki}"`);
1719
2004
  }
1720
- if (!existsSync8(join7(entry.path, ".almanac"))) {
2005
+ if (!existsSync8(join9(entry.path, ".almanac"))) {
1721
2006
  throw new Error(
1722
2007
  `wiki "${params.wiki}" path is unreachable (${entry.path})`
1723
2008
  );
@@ -1779,9 +2064,9 @@ var DEFAULT_STALE_SECONDS = 90 * 24 * 60 * 60;
1779
2064
  async function runHealth(options) {
1780
2065
  const repoRoot = await resolveWikiRoot({ cwd: options.cwd, wiki: options.wiki });
1781
2066
  await ensureFreshIndex({ repoRoot });
1782
- const almanacDir = join8(repoRoot, ".almanac");
1783
- const pagesDir = join8(almanacDir, "pages");
1784
- const db = openIndex(join8(almanacDir, "index.db"));
2067
+ const almanacDir = join10(repoRoot, ".almanac");
2068
+ const pagesDir = join10(almanacDir, "pages");
2069
+ const db = openIndex(join10(almanacDir, "index.db"));
1785
2070
  try {
1786
2071
  const staleSeconds = options.stale !== void 0 ? parseDuration(options.stale) : DEFAULT_STALE_SECONDS;
1787
2072
  const scope = resolveScope(db, options);
@@ -1882,7 +2167,7 @@ async function findDeadRefs(db, scope, repoRoot) {
1882
2167
  const out = [];
1883
2168
  for (const r of rows) {
1884
2169
  if (!inPageScope(scope, r.slug)) continue;
1885
- const abs = join8(repoRoot, r.original_path);
2170
+ const abs = join10(repoRoot, r.original_path);
1886
2171
  if (!existsSync9(abs)) {
1887
2172
  out.push({ slug: r.slug, path: r.original_path });
1888
2173
  }
@@ -1918,7 +2203,7 @@ async function findBrokenXwiki(db, scope) {
1918
2203
  let ok = reachableCache.get(r.target_wiki);
1919
2204
  if (ok === void 0) {
1920
2205
  const entry = await findEntry({ name: r.target_wiki });
1921
- ok = entry !== null && existsSync9(join8(entry.path, ".almanac"));
2206
+ ok = entry !== null && existsSync9(join10(entry.path, ".almanac"));
1922
2207
  reachableCache.set(r.target_wiki, ok);
1923
2208
  }
1924
2209
  if (!ok) {
@@ -1953,7 +2238,7 @@ async function findEmptyPages(db, scope, pagesDir) {
1953
2238
  if (!inPageScope(scope, r.slug)) continue;
1954
2239
  let raw;
1955
2240
  try {
1956
- raw = await readFile7(r.file_path, "utf8");
2241
+ raw = await readFile9(r.file_path, "utf8");
1957
2242
  } catch {
1958
2243
  continue;
1959
2244
  }
@@ -2072,22 +2357,61 @@ ${lines.join("\n")}`;
2072
2357
  import { existsSync as existsSync11 } from "fs";
2073
2358
  import {
2074
2359
  copyFile,
2075
- mkdir as mkdir5,
2076
- readFile as readFile9,
2077
- writeFile as writeFile5
2360
+ mkdir as mkdir7,
2361
+ readFile as readFile11,
2362
+ writeFile as writeFile7
2078
2363
  } from "fs/promises";
2079
- import { createRequire as createRequire2 } from "module";
2364
+ import { createRequire as createRequire3 } from "module";
2080
2365
  import { homedir as homedir4 } from "os";
2081
2366
  import path3 from "path";
2082
2367
  import { fileURLToPath as fileURLToPath3 } from "url";
2083
2368
 
2084
2369
  // src/commands/hook.ts
2085
2370
  import { existsSync as existsSync10 } from "fs";
2086
- import { mkdir as mkdir4, readFile as readFile8, rename as rename3, writeFile as writeFile4 } from "fs/promises";
2371
+ import { mkdir as mkdir6, readFile as readFile10, rename as rename5, writeFile as writeFile6 } from "fs/promises";
2087
2372
  import { homedir as homedir3 } from "os";
2088
2373
  import path2 from "path";
2089
2374
  import { fileURLToPath as fileURLToPath2 } from "url";
2090
2375
  var HOOK_TIMEOUT_SECONDS = 10;
2376
+ function isOurCommandPath(command) {
2377
+ return command.endsWith("almanac-capture.sh");
2378
+ }
2379
+ function classifyEntry(raw) {
2380
+ if (raw === null || typeof raw !== "object") {
2381
+ return { kind: "unknown", entry: raw };
2382
+ }
2383
+ const obj = raw;
2384
+ if (Array.isArray(obj.hooks)) {
2385
+ const matcher = typeof obj.matcher === "string" ? obj.matcher : "";
2386
+ const hooks = [];
2387
+ for (const h of obj.hooks) {
2388
+ if (h !== null && typeof h === "object") {
2389
+ const ho = h;
2390
+ if (ho.type === "command" && typeof ho.command === "string") {
2391
+ const cmd = {
2392
+ type: "command",
2393
+ command: ho.command
2394
+ };
2395
+ if (typeof ho.timeout === "number") cmd.timeout = ho.timeout;
2396
+ hooks.push(cmd);
2397
+ }
2398
+ }
2399
+ }
2400
+ return { kind: "wrapped", entry: { matcher, hooks } };
2401
+ }
2402
+ if (obj.type === "command" && typeof obj.command === "string") {
2403
+ const cmd = {
2404
+ type: "command",
2405
+ command: obj.command
2406
+ };
2407
+ if (typeof obj.timeout === "number") cmd.timeout = obj.timeout;
2408
+ return { kind: "legacy", entry: cmd };
2409
+ }
2410
+ return { kind: "unknown", entry: raw };
2411
+ }
2412
+ function isOurWrapped(entry) {
2413
+ return entry.hooks.some((h) => isOurCommandPath(h.command));
2414
+ }
2091
2415
  async function runHookInstall(options = {}) {
2092
2416
  const script = resolveHookScriptPath(options);
2093
2417
  if (!script.ok) {
@@ -2097,26 +2421,54 @@ async function runHookInstall(options = {}) {
2097
2421
  const settingsPath = resolveSettingsPath(options);
2098
2422
  const settings = await readSettings(settingsPath);
2099
2423
  const existing = (settings.hooks?.SessionEnd ?? []).slice();
2100
- const ourEntries = existing.filter((e) => e.command === script.path);
2101
- const foreignEntries = existing.filter((e) => e.command !== script.path);
2102
- const stale = foreignEntries.filter(
2103
- (e) => e.command.endsWith("almanac-capture.sh")
2104
- );
2105
- const unrelated = foreignEntries.filter(
2106
- (e) => !e.command.endsWith("almanac-capture.sh")
2107
- );
2108
- if (unrelated.length > 0) {
2109
- const existingStr = unrelated.map((e) => ` - ${e.command}`).join("\n");
2424
+ const preserved = [];
2425
+ let oursAlready = null;
2426
+ const staleCount = { n: 0 };
2427
+ for (const raw of existing) {
2428
+ const c = classifyEntry(raw);
2429
+ if (c.kind === "wrapped") {
2430
+ if (!isOurWrapped(c.entry)) {
2431
+ preserved.push(raw);
2432
+ continue;
2433
+ }
2434
+ const exactMatch = c.entry.hooks.some(
2435
+ (h) => h.command === script.path
2436
+ );
2437
+ if (exactMatch && oursAlready === null) {
2438
+ oursAlready = c.entry;
2439
+ } else {
2440
+ staleCount.n += 1;
2441
+ }
2442
+ } else if (c.kind === "legacy") {
2443
+ if (isOurCommandPath(c.entry.command)) {
2444
+ staleCount.n += 1;
2445
+ } else {
2446
+ preserved.push(raw);
2447
+ }
2448
+ } else {
2449
+ preserved.push(raw);
2450
+ }
2451
+ }
2452
+ const foreignLegacy = preserved.filter((raw) => {
2453
+ const c = classifyEntry(raw);
2454
+ return c.kind === "legacy";
2455
+ });
2456
+ if (foreignLegacy.length > 0) {
2457
+ const lines = foreignLegacy.map((raw) => {
2458
+ const c = classifyEntry(raw);
2459
+ if (c.kind === "legacy") return ` - ${c.entry.command}`;
2460
+ return " - <unrecognized>";
2461
+ }).join("\n");
2110
2462
  return {
2111
2463
  stdout: "",
2112
- stderr: `almanac: SessionEnd hook already has a foreign entry:
2113
- ${existingStr}
2114
- Remove it manually from ${settingsPath} if you want almanac to manage the hook.
2464
+ stderr: `almanac: SessionEnd has a foreign legacy entry:
2465
+ ${lines}
2466
+ Remove or rewrap it manually in ${settingsPath} before installing.
2115
2467
  `,
2116
2468
  exitCode: 1
2117
2469
  };
2118
2470
  }
2119
- if (ourEntries.length > 0 && stale.length === 0) {
2471
+ if (oursAlready !== null && staleCount.n === 0) {
2120
2472
  return {
2121
2473
  stdout: `almanac: SessionEnd hook already installed at ${script.path}
2122
2474
  `,
@@ -2124,13 +2476,17 @@ Remove it manually from ${settingsPath} if you want almanac to manage the hook.
2124
2476
  exitCode: 0
2125
2477
  };
2126
2478
  }
2127
- const newEntries = [
2128
- {
2129
- type: "command",
2130
- command: script.path,
2131
- timeout: HOOK_TIMEOUT_SECONDS
2132
- }
2133
- ];
2479
+ const fresh = {
2480
+ matcher: "",
2481
+ hooks: [
2482
+ {
2483
+ type: "command",
2484
+ command: script.path,
2485
+ timeout: HOOK_TIMEOUT_SECONDS
2486
+ }
2487
+ ]
2488
+ };
2489
+ const newEntries = [...preserved, fresh];
2134
2490
  settings.hooks = { ...settings.hooks ?? {}, SessionEnd: newEntries };
2135
2491
  await writeSettings(settingsPath, settings);
2136
2492
  return {
@@ -2154,8 +2510,33 @@ async function runHookUninstall(options = {}) {
2154
2510
  }
2155
2511
  const settings = await readSettings(settingsPath);
2156
2512
  const existing = (settings.hooks?.SessionEnd ?? []).slice();
2157
- const kept = existing.filter((e) => !e.command.endsWith("almanac-capture.sh"));
2158
- const removed = existing.length - kept.length;
2513
+ const kept = [];
2514
+ let removed = 0;
2515
+ for (const raw of existing) {
2516
+ const c = classifyEntry(raw);
2517
+ if (c.kind === "wrapped") {
2518
+ const innerKept = c.entry.hooks.filter(
2519
+ (h) => !isOurCommandPath(h.command)
2520
+ );
2521
+ const innerRemoved = c.entry.hooks.length - innerKept.length;
2522
+ removed += innerRemoved;
2523
+ if (innerKept.length === 0) {
2524
+ if (innerRemoved === 0) kept.push(raw);
2525
+ } else if (innerRemoved === 0) {
2526
+ kept.push(raw);
2527
+ } else {
2528
+ kept.push({ matcher: c.entry.matcher, hooks: innerKept });
2529
+ }
2530
+ } else if (c.kind === "legacy") {
2531
+ if (isOurCommandPath(c.entry.command)) {
2532
+ removed += 1;
2533
+ } else {
2534
+ kept.push(raw);
2535
+ }
2536
+ } else {
2537
+ kept.push(raw);
2538
+ }
2539
+ }
2159
2540
  if (removed === 0) {
2160
2541
  return {
2161
2542
  stdout: `almanac: SessionEnd hook not installed
@@ -2199,14 +2580,33 @@ settings: ${settingsPath} (does not exist)
2199
2580
  }
2200
2581
  const settings = await readSettings(settingsPath);
2201
2582
  const existing = settings.hooks?.SessionEnd ?? [];
2202
- const ours = existing.find((e) => e.command.endsWith("almanac-capture.sh"));
2203
- if (ours === void 0) {
2204
- const foreign = existing.map((e) => ` - ${e.command}`).join("\n");
2583
+ let ourCommand = null;
2584
+ const foreignSummary = [];
2585
+ for (const raw of existing) {
2586
+ const c = classifyEntry(raw);
2587
+ if (c.kind === "wrapped") {
2588
+ for (const h of c.entry.hooks) {
2589
+ if (isOurCommandPath(h.command)) {
2590
+ ourCommand ??= h.command;
2591
+ } else {
2592
+ foreignSummary.push(h.command);
2593
+ }
2594
+ }
2595
+ } else if (c.kind === "legacy") {
2596
+ if (isOurCommandPath(c.entry.command)) {
2597
+ ourCommand ??= c.entry.command;
2598
+ } else {
2599
+ foreignSummary.push(c.entry.command);
2600
+ }
2601
+ }
2602
+ }
2603
+ if (ourCommand === null) {
2604
+ const foreignLines = foreignSummary.map((c) => ` - ${c}`).join("\n");
2205
2605
  return {
2206
2606
  stdout: `SessionEnd hook: not installed
2207
2607
  settings: ${settingsPath}
2208
- ` + (existing.length > 0 ? `(${existing.length} foreign entr${existing.length === 1 ? "y" : "ies"} present:
2209
- ${foreign})
2608
+ ` + (foreignSummary.length > 0 ? `(${foreignSummary.length} foreign entr${foreignSummary.length === 1 ? "y" : "ies"} present:
2609
+ ${foreignLines})
2210
2610
  ` : "") + (script.ok ? `script would be: ${script.path}
2211
2611
  ` : ""),
2212
2612
  stderr: "",
@@ -2215,7 +2615,7 @@ ${foreign})
2215
2615
  }
2216
2616
  return {
2217
2617
  stdout: `SessionEnd hook: installed
2218
- script: ${ours.command}
2618
+ script: ${ourCommand}
2219
2619
  settings: ${settingsPath}
2220
2620
  `,
2221
2621
  stderr: "",
@@ -2253,7 +2653,7 @@ function resolveHookScriptPath(options) {
2253
2653
  async function readSettings(settingsPath) {
2254
2654
  if (!existsSync10(settingsPath)) return {};
2255
2655
  try {
2256
- const raw = await readFile8(settingsPath, "utf8");
2656
+ const raw = await readFile10(settingsPath, "utf8");
2257
2657
  if (raw.trim().length === 0) return {};
2258
2658
  const parsed = JSON.parse(raw);
2259
2659
  if (parsed === null || typeof parsed !== "object") return {};
@@ -2265,12 +2665,12 @@ async function readSettings(settingsPath) {
2265
2665
  }
2266
2666
  async function writeSettings(settingsPath, settings) {
2267
2667
  const dir = path2.dirname(settingsPath);
2268
- await mkdir4(dir, { recursive: true });
2668
+ await mkdir6(dir, { recursive: true });
2269
2669
  const tmp = `${settingsPath}.almanac-tmp-${process.pid}`;
2270
2670
  const body = `${JSON.stringify(settings, null, 2)}
2271
2671
  `;
2272
- await writeFile4(tmp, body, "utf8");
2273
- await rename3(tmp, settingsPath);
2672
+ await writeFile6(tmp, body, "utf8");
2673
+ await rename5(tmp, settingsPath);
2274
2674
  }
2275
2675
 
2276
2676
  // src/commands/setup.ts
@@ -2434,7 +2834,7 @@ function reportAuth(out, auth) {
2434
2834
  }
2435
2835
  }
2436
2836
  async function installGuides(options) {
2437
- await mkdir5(options.claudeDir, { recursive: true });
2837
+ await mkdir7(options.claudeDir, { recursive: true });
2438
2838
  const srcMini = path3.join(options.guidesDir, "mini.md");
2439
2839
  const srcRef = path3.join(options.guidesDir, "reference.md");
2440
2840
  if (!existsSync11(srcMini)) {
@@ -2456,10 +2856,10 @@ async function installGuides(options) {
2456
2856
  return { anyChanges: filesWritten.length > 0, filesWritten };
2457
2857
  }
2458
2858
  async function copyIfChanged(src, dest) {
2459
- const srcBytes = await readFile9(src);
2859
+ const srcBytes = await readFile11(src);
2460
2860
  if (existsSync11(dest)) {
2461
2861
  try {
2462
- const destBytes = await readFile9(dest);
2862
+ const destBytes = await readFile11(dest);
2463
2863
  if (srcBytes.equals(destBytes)) return false;
2464
2864
  } catch {
2465
2865
  }
@@ -2471,13 +2871,13 @@ var IMPORT_LINE = "@~/.claude/codealmanac.md";
2471
2871
  async function ensureImport(claudeMdPath) {
2472
2872
  let existing = "";
2473
2873
  if (existsSync11(claudeMdPath)) {
2474
- existing = await readFile9(claudeMdPath, "utf8");
2874
+ existing = await readFile11(claudeMdPath, "utf8");
2475
2875
  }
2476
2876
  if (hasImportLine(existing)) return false;
2477
2877
  const sep = existing.length === 0 ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
2478
2878
  const body = `${existing}${sep}${IMPORT_LINE}
2479
2879
  `;
2480
- await writeFile5(claudeMdPath, body, "utf8");
2880
+ await writeFile7(claudeMdPath, body, "utf8");
2481
2881
  return true;
2482
2882
  }
2483
2883
  function hasImportLine(contents) {
@@ -2553,7 +2953,7 @@ function resolveGuidesDir() {
2553
2953
  if (looksLikeGuidesDir(dir)) return dir;
2554
2954
  }
2555
2955
  try {
2556
- const require2 = createRequire2(import.meta.url);
2956
+ const require2 = createRequire3(import.meta.url);
2557
2957
  const pkgJson = require2.resolve("codealmanac/package.json");
2558
2958
  const guides = path3.join(path3.dirname(pkgJson), "guides");
2559
2959
  if (looksLikeGuidesDir(guides)) return guides;
@@ -2577,8 +2977,9 @@ var BLUE2 = "\x1B[38;5;75m";
2577
2977
  async function runDoctor(options) {
2578
2978
  const version = options.versionOverride ?? readPackageVersion() ?? "unknown";
2579
2979
  const install = options.wikiOnly === true ? [] : await gatherInstallChecks(options);
2980
+ const updates = options.wikiOnly === true ? [] : await gatherUpdateChecks(options, version);
2580
2981
  const wiki = options.installOnly === true ? [] : await gatherWikiChecks(options);
2581
- const report = { version, install, wiki };
2982
+ const report = { version, install, updates, wiki };
2582
2983
  if (options.json === true) {
2583
2984
  return {
2584
2985
  stdout: `${JSON.stringify(report, null, 2)}
@@ -2660,13 +3061,21 @@ async function describeHook(settingsPath) {
2660
3061
  };
2661
3062
  }
2662
3063
  try {
2663
- const raw = await readFile10(settingsPath, "utf8");
3064
+ const raw = await readFile12(settingsPath, "utf8");
2664
3065
  const parsed = JSON.parse(raw);
2665
3066
  const entries = parsed.hooks?.SessionEnd ?? [];
2666
- const ours = entries.find(
2667
- (e) => typeof e.command === "string" && e.command.endsWith("almanac-capture.sh")
2668
- );
2669
- if (ours === void 0) {
3067
+ const found = entries.some((e) => {
3068
+ if (typeof e?.command === "string" && e.command.endsWith("almanac-capture.sh")) {
3069
+ return true;
3070
+ }
3071
+ if (Array.isArray(e?.hooks)) {
3072
+ return e.hooks.some(
3073
+ (h) => typeof h?.command === "string" && h.command.endsWith("almanac-capture.sh")
3074
+ );
3075
+ }
3076
+ return false;
3077
+ });
3078
+ if (!found) {
2670
3079
  return {
2671
3080
  status: "problem",
2672
3081
  key: "install.hook",
@@ -2723,7 +3132,7 @@ async function describeImportLine(claudeDir) {
2723
3132
  };
2724
3133
  }
2725
3134
  try {
2726
- const contents = await readFile10(claudeMd, "utf8");
3135
+ const contents = await readFile12(claudeMd, "utf8");
2727
3136
  const lines = contents.split(/\r?\n/).map((l) => l.trim());
2728
3137
  const present = lines.some((line) => {
2729
3138
  if (line === IMPORT_LINE) return true;
@@ -2753,6 +3162,63 @@ async function describeImportLine(claudeDir) {
2753
3162
  };
2754
3163
  }
2755
3164
  }
3165
+ async function gatherUpdateChecks(options, installedVersion) {
3166
+ const checks = [];
3167
+ const state = readStateForDoctor(options.updateStatePath);
3168
+ const config = await readConfig(options.updateConfigPath);
3169
+ if (state === null || state.latest_version.length === 0) {
3170
+ checks.push({
3171
+ status: "info",
3172
+ key: "update.status",
3173
+ message: `on ${installedVersion}; no update check has run yet`,
3174
+ fix: "run: almanac update --check"
3175
+ });
3176
+ } else if (isNewer(state.latest_version, installedVersion)) {
3177
+ const dismissed = state.dismissed_versions.includes(state.latest_version) ? " (dismissed \u2014 run `almanac update` to install anyway)" : "";
3178
+ checks.push({
3179
+ status: "problem",
3180
+ key: "update.status",
3181
+ message: `${state.latest_version} available (you're on ${installedVersion})${dismissed}`,
3182
+ fix: "run: almanac update"
3183
+ });
3184
+ } else {
3185
+ checks.push({
3186
+ status: "ok",
3187
+ key: "update.status",
3188
+ message: `on latest (${installedVersion})`
3189
+ });
3190
+ }
3191
+ if (state !== null && state.last_check_at > 0) {
3192
+ const now = (options.now?.() ?? /* @__PURE__ */ new Date()).getTime();
3193
+ const ageMs = now - state.last_check_at * 1e3;
3194
+ const failedSuffix = state.last_fetch_failed_at !== void 0 && state.last_fetch_failed_at === state.last_check_at ? " (last attempt failed \u2014 will retry next invocation)" : "";
3195
+ checks.push({
3196
+ status: "info",
3197
+ key: "update.last_check",
3198
+ message: `last checked: ${formatDuration(ageMs)} ago${failedSuffix}`
3199
+ });
3200
+ } else {
3201
+ checks.push({
3202
+ status: "info",
3203
+ key: "update.last_check",
3204
+ message: "last checked: never"
3205
+ });
3206
+ }
3207
+ checks.push({
3208
+ status: "info",
3209
+ key: "update.notifier",
3210
+ message: `update notifier: ${config.update_notifier ? "enabled" : "disabled"}`,
3211
+ fix: config.update_notifier ? void 0 : "run: almanac update --enable-notifier"
3212
+ });
3213
+ if (state !== null && state.dismissed_versions.length > 0) {
3214
+ checks.push({
3215
+ status: "info",
3216
+ key: "update.dismissed",
3217
+ message: `dismissed versions: ${state.dismissed_versions.join(", ")}`
3218
+ });
3219
+ }
3220
+ return checks;
3221
+ }
2756
3222
  async function gatherWikiChecks(options) {
2757
3223
  const checks = [];
2758
3224
  const repoRoot = findNearestAlmanacDir(options.cwd);
@@ -2770,14 +3236,30 @@ async function gatherWikiChecks(options) {
2770
3236
  key: "wiki.repo",
2771
3237
  message: `repo: ${repoRoot}`
2772
3238
  });
2773
- const entry = await findEntry({ path: repoRoot });
3239
+ try {
3240
+ await ensureFreshIndex({ repoRoot });
3241
+ } catch {
3242
+ }
3243
+ let entry;
3244
+ try {
3245
+ entry = await findEntry({ path: repoRoot });
3246
+ } catch (err) {
3247
+ const msg = err instanceof Error ? err.message : String(err);
3248
+ checks.push({
3249
+ status: "problem",
3250
+ key: "wiki.registered",
3251
+ message: `could not read registry: ${msg}`,
3252
+ fix: "inspect ~/.almanac/registry.json; remove or fix the malformed entry"
3253
+ });
3254
+ entry = null;
3255
+ }
2774
3256
  if (entry !== null) {
2775
3257
  checks.push({
2776
3258
  status: "ok",
2777
3259
  key: "wiki.registered",
2778
3260
  message: `registered as '${entry.name}'`
2779
3261
  });
2780
- } else {
3262
+ } else if (checks[checks.length - 1]?.key !== "wiki.registered") {
2781
3263
  checks.push({
2782
3264
  status: "info",
2783
3265
  key: "wiki.registered",
@@ -2894,7 +3376,9 @@ function describeLastCapture(almanacDir, nowFn) {
2894
3376
  message: "last capture: unknown"
2895
3377
  };
2896
3378
  }
2897
- const captures = entries.filter((e) => e.startsWith(".capture-") && e.endsWith(".log")).map((e) => {
3379
+ const captures = entries.filter(
3380
+ (e) => e.startsWith(".capture-") && (e.endsWith(".log") || e.endsWith(".jsonl"))
3381
+ ).map((e) => {
2898
3382
  try {
2899
3383
  return {
2900
3384
  name: e,
@@ -2921,12 +3405,23 @@ function describeLastCapture(almanacDir, nowFn) {
2921
3405
  message: `last capture: ${formatDuration(age)} ago (${latest.name})`
2922
3406
  };
2923
3407
  }
2924
- var req = createRequire3(import.meta.url);
3408
+ var req = createRequire4(import.meta.url);
3409
+ var HEALTH_PROBLEM_KEYS = [
3410
+ "orphans",
3411
+ "stale",
3412
+ "dead_refs",
3413
+ "broken_links",
3414
+ "broken_xwiki",
3415
+ "empty_topics",
3416
+ "empty_pages",
3417
+ "slug_collisions"
3418
+ ];
2925
3419
  function countHealthProblems(jsonStdout) {
2926
3420
  try {
2927
3421
  const report = JSON.parse(jsonStdout);
2928
3422
  let total = 0;
2929
- for (const arr of Object.values(report)) {
3423
+ for (const key of HEALTH_PROBLEM_KEYS) {
3424
+ const arr = report[key];
2930
3425
  if (Array.isArray(arr)) total += arr.length;
2931
3426
  }
2932
3427
  return total;
@@ -2936,8 +3431,23 @@ function countHealthProblems(jsonStdout) {
2936
3431
  }
2937
3432
  function detectInstallPath() {
2938
3433
  try {
2939
- const entry = req.resolve("codealmanac");
2940
- return path4.dirname(path4.dirname(entry));
3434
+ const here = fileURLToPath4(import.meta.url);
3435
+ let dir = path4.dirname(here);
3436
+ for (let i = 0; i < 5; i++) {
3437
+ const pkgPath = path4.join(dir, "package.json");
3438
+ if (existsSync12(pkgPath)) {
3439
+ try {
3440
+ const raw = readFileSync2(pkgPath, "utf-8");
3441
+ const pkg = JSON.parse(raw);
3442
+ if (pkg.name === "codealmanac") return dir;
3443
+ } catch {
3444
+ }
3445
+ }
3446
+ const parent = path4.dirname(dir);
3447
+ if (parent === dir) break;
3448
+ dir = parent;
3449
+ }
3450
+ return null;
2941
3451
  } catch {
2942
3452
  return null;
2943
3453
  }
@@ -2990,6 +3500,13 @@ function formatReport2(report, options) {
2990
3500
  }
2991
3501
  lines.push("");
2992
3502
  }
3503
+ if (report.updates.length > 0) {
3504
+ lines.push(color ? `${BOLD2}## Updates${RST2}` : "## Updates");
3505
+ for (const c of report.updates) {
3506
+ lines.push(formatCheck(c, color));
3507
+ }
3508
+ lines.push("");
3509
+ }
2993
3510
  if (report.wiki.length > 0) {
2994
3511
  lines.push(color ? `${BOLD2}## Current wiki${RST2}` : "## Current wiki");
2995
3512
  for (const c of report.wiki) {
@@ -3096,14 +3613,14 @@ async function runReindex(options) {
3096
3613
  }
3097
3614
 
3098
3615
  // src/commands/search.ts
3099
- import { join as join9 } from "path";
3616
+ import { join as join11 } from "path";
3100
3617
  async function runSearch(options) {
3101
3618
  const repoRoot = await resolveWikiRoot({
3102
3619
  cwd: options.cwd,
3103
3620
  wiki: options.wiki
3104
3621
  });
3105
3622
  await ensureFreshIndex({ repoRoot });
3106
- const dbPath = join9(repoRoot, ".almanac", "index.db");
3623
+ const dbPath = join11(repoRoot, ".almanac", "index.db");
3107
3624
  const db = openIndex(dbPath);
3108
3625
  try {
3109
3626
  const rows = executeQuery(db, options);
@@ -3275,15 +3792,15 @@ function buildStderr(rows, options) {
3275
3792
  }
3276
3793
 
3277
3794
  // src/commands/show.ts
3278
- import { readFile as readFile11 } from "fs/promises";
3279
- import { join as join10 } from "path";
3795
+ import { readFile as readFile13 } from "fs/promises";
3796
+ import { join as join12 } from "path";
3280
3797
  async function runShow(options) {
3281
3798
  const repoRoot = await resolveWikiRoot({
3282
3799
  cwd: options.cwd,
3283
3800
  wiki: options.wiki
3284
3801
  });
3285
3802
  await ensureFreshIndex({ repoRoot });
3286
- const dbPath = join10(repoRoot, ".almanac", "index.db");
3803
+ const dbPath = join12(repoRoot, ".almanac", "index.db");
3287
3804
  const db = openIndex(dbPath);
3288
3805
  try {
3289
3806
  const slugs = collectSlugs(options);
@@ -3342,7 +3859,7 @@ async function fetchRecord(db, slug) {
3342
3859
  ).all(slug).map((r) => r.slug);
3343
3860
  let body = "";
3344
3861
  try {
3345
- body = stripFrontmatter(await readFile11(pageRow.file_path, "utf8"));
3862
+ body = stripFrontmatter(await readFile13(pageRow.file_path, "utf8"));
3346
3863
  } catch {
3347
3864
  }
3348
3865
  return {
@@ -3589,18 +4106,18 @@ function collectSlugs(options) {
3589
4106
  }
3590
4107
 
3591
4108
  // src/topics/frontmatterRewrite.ts
3592
- import { readFile as readFile12, rename as rename4, writeFile as writeFile6 } from "fs/promises";
4109
+ import { readFile as readFile14, rename as rename6, writeFile as writeFile8 } from "fs/promises";
3593
4110
  import yaml3 from "js-yaml";
3594
4111
  async function rewritePageTopics(filePath, transform) {
3595
- const raw = await readFile12(filePath, "utf8");
4112
+ const raw = await readFile14(filePath, "utf8");
3596
4113
  const { before, after, output, changed } = applyTopicsTransform(
3597
4114
  raw,
3598
4115
  transform
3599
4116
  );
3600
4117
  if (changed) {
3601
4118
  const tmp = `${filePath}.tmp`;
3602
- await writeFile6(tmp, output, "utf8");
3603
- await rename4(tmp, filePath);
4119
+ await writeFile8(tmp, output, "utf8");
4120
+ await rename6(tmp, filePath);
3604
4121
  }
3605
4122
  return { before, after, changed };
3606
4123
  }
@@ -3806,12 +4323,12 @@ function arraysEqual(a, b) {
3806
4323
  }
3807
4324
 
3808
4325
  // src/topics/paths.ts
3809
- import { join as join11 } from "path";
4326
+ import { join as join13 } from "path";
3810
4327
  function topicsYamlPath(repoRoot) {
3811
- return join11(repoRoot, ".almanac", "topics.yaml");
4328
+ return join13(repoRoot, ".almanac", "topics.yaml");
3812
4329
  }
3813
4330
  function indexDbPath(repoRoot) {
3814
- return join11(repoRoot, ".almanac", "index.db");
4331
+ return join13(repoRoot, ".almanac", "index.db");
3815
4332
  }
3816
4333
 
3817
4334
  // src/commands/tag.ts
@@ -3970,8 +4487,8 @@ async function runUntag(options) {
3970
4487
  }
3971
4488
 
3972
4489
  // src/commands/topics.ts
3973
- import { readFile as readFile13 } from "fs/promises";
3974
- import { join as join12 } from "path";
4490
+ import { readFile as readFile15 } from "fs/promises";
4491
+ import { join as join14 } from "path";
3975
4492
  import fg3 from "fast-glob";
3976
4493
  async function runTopicsList(options) {
3977
4494
  const repoRoot = await resolveWikiRoot({ cwd: options.cwd, wiki: options.wiki });
@@ -4443,7 +4960,7 @@ async function runTopicsDescribe(options) {
4443
4960
  };
4444
4961
  }
4445
4962
  async function rewriteTopicOnPages(repoRoot, transform) {
4446
- const pagesDir = join12(repoRoot, ".almanac", "pages");
4963
+ const pagesDir = join14(repoRoot, ".almanac", "pages");
4447
4964
  const files = await fg3("**/*.md", {
4448
4965
  cwd: pagesDir,
4449
4966
  absolute: true,
@@ -4451,7 +4968,7 @@ async function rewriteTopicOnPages(repoRoot, transform) {
4451
4968
  });
4452
4969
  let changed = 0;
4453
4970
  for (const filePath of files) {
4454
- const raw = await readFile13(filePath, "utf8");
4971
+ const raw = await readFile15(filePath, "utf8");
4455
4972
  const applied = applyTopicsTransform(raw, transform);
4456
4973
  if (!applied.changed) continue;
4457
4974
  await rewritePageTopics(filePath, transform);
@@ -4462,7 +4979,7 @@ async function rewriteTopicOnPages(repoRoot, transform) {
4462
4979
 
4463
4980
  // src/commands/uninstall.ts
4464
4981
  import { existsSync as existsSync14 } from "fs";
4465
- import { readFile as readFile14, rm, writeFile as writeFile7 } from "fs/promises";
4982
+ import { readFile as readFile16, rm, writeFile as writeFile9 } from "fs/promises";
4466
4983
  import { homedir as homedir6 } from "os";
4467
4984
  import path5 from "path";
4468
4985
  var BLUE3 = "\x1B[38;5;75m";
@@ -4543,14 +5060,14 @@ async function removeGuideFiles(claudeDir) {
4543
5060
  touched.push("codealmanac-reference.md");
4544
5061
  }
4545
5062
  if (existsSync14(claudeMd)) {
4546
- const existing = await readFile14(claudeMd, "utf8");
5063
+ const existing = await readFile16(claudeMd, "utf8");
4547
5064
  const { changed, body } = removeImportLine(existing);
4548
5065
  if (changed) {
4549
5066
  if (body.trim().length === 0) {
4550
5067
  await rm(claudeMd, { force: true });
4551
5068
  touched.push("CLAUDE.md (deleted)");
4552
5069
  } else {
4553
- await writeFile7(claudeMd, body, "utf8");
5070
+ await writeFile9(claudeMd, body, "utf8");
4554
5071
  touched.push("CLAUDE.md");
4555
5072
  }
4556
5073
  }
@@ -4592,6 +5109,195 @@ function confirm2(out, question, defaultYes) {
4592
5109
  });
4593
5110
  }
4594
5111
 
5112
+ // src/commands/update.ts
5113
+ import { spawn as spawn3 } from "child_process";
5114
+ import { createRequire as createRequire5 } from "module";
5115
+ async function runUpdate(opts = {}) {
5116
+ if (opts.enableNotifier === true) {
5117
+ return await toggleNotifier(true, opts);
5118
+ }
5119
+ if (opts.disableNotifier === true) {
5120
+ return await toggleNotifier(false, opts);
5121
+ }
5122
+ if (opts.dismiss === true) {
5123
+ return await dismissLatest(opts);
5124
+ }
5125
+ if (opts.check === true) {
5126
+ return await forceCheck(opts);
5127
+ }
5128
+ return await installLatest(opts);
5129
+ }
5130
+ async function dismissLatest(opts) {
5131
+ const state = await readState(opts.statePath);
5132
+ if (state.latest_version.length === 0) {
5133
+ return {
5134
+ stdout: "codealmanac: no pending update to dismiss. Run `almanac update --check` to query the registry.\n",
5135
+ stderr: "",
5136
+ exitCode: 0
5137
+ };
5138
+ }
5139
+ const installed = opts.installedVersion ?? readInstalledVersion2();
5140
+ if (!isNewer(state.latest_version, installed)) {
5141
+ return {
5142
+ stdout: `codealmanac: already on latest (${installed}); nothing to dismiss.
5143
+ `,
5144
+ stderr: "",
5145
+ exitCode: 0
5146
+ };
5147
+ }
5148
+ if (state.dismissed_versions.includes(state.latest_version)) {
5149
+ return {
5150
+ stdout: `codealmanac: ${state.latest_version} already dismissed.
5151
+ `,
5152
+ stderr: "",
5153
+ exitCode: 0
5154
+ };
5155
+ }
5156
+ const next = {
5157
+ ...state,
5158
+ dismissed_versions: [...state.dismissed_versions, state.latest_version]
5159
+ };
5160
+ await writeState(next, opts.statePath);
5161
+ return {
5162
+ stdout: `codealmanac: dismissed ${state.latest_version}. The nag banner will not show for this version.
5163
+ Run \`almanac update\` to upgrade, or \`almanac update --enable-notifier\` to re-enable nags.
5164
+ `,
5165
+ stderr: "",
5166
+ exitCode: 0
5167
+ };
5168
+ }
5169
+ async function forceCheck(opts) {
5170
+ const installed = opts.installedVersion ?? readInstalledVersion2();
5171
+ const checkFn = opts.checkFn ?? checkForUpdate;
5172
+ const result = await checkFn({
5173
+ installedVersion: installed,
5174
+ force: true,
5175
+ statePath: opts.statePath,
5176
+ now: opts.now
5177
+ });
5178
+ if (result.fetchFailed) {
5179
+ return {
5180
+ stdout: "",
5181
+ stderr: `codealmanac: could not reach registry.npmjs.org (timeout or network error).
5182
+ Installed: ${installed}. No cached latest available.
5183
+ `,
5184
+ exitCode: 1
5185
+ };
5186
+ }
5187
+ const latest = result.state.latest_version;
5188
+ if (latest.length === 0) {
5189
+ return {
5190
+ stdout: `codealmanac: installed ${installed}; registry did not report a latest tag.
5191
+ `,
5192
+ stderr: "",
5193
+ exitCode: 0
5194
+ };
5195
+ }
5196
+ if (isNewer(latest, installed)) {
5197
+ const dismissed = result.state.dismissed_versions.includes(latest) ? " (dismissed \u2014 banner suppressed; `almanac update` still installs)" : "";
5198
+ return {
5199
+ stdout: `codealmanac ${latest} available (you're on ${installed})${dismissed}.
5200
+ Run: almanac update
5201
+ `,
5202
+ stderr: "",
5203
+ exitCode: 0
5204
+ };
5205
+ }
5206
+ return {
5207
+ stdout: `codealmanac: up to date (${installed}).
5208
+ `,
5209
+ stderr: "",
5210
+ exitCode: 0
5211
+ };
5212
+ }
5213
+ async function toggleNotifier(enable, opts) {
5214
+ const config = await readConfig(opts.configPath);
5215
+ const next = { ...config, update_notifier: enable };
5216
+ await writeConfig(next, opts.configPath);
5217
+ return {
5218
+ stdout: enable ? "codealmanac: update notifier enabled. The pre-command banner will show when a new version is available.\n" : "codealmanac: update notifier disabled. No more pre-command banners. Run `almanac update --check` to see status.\n",
5219
+ stderr: "",
5220
+ exitCode: 0
5221
+ };
5222
+ }
5223
+ async function installLatest(opts) {
5224
+ const spawnFn = opts.spawnFn ?? spawn3;
5225
+ const installed = opts.installedVersion ?? readInstalledVersion2();
5226
+ const spawnOpts = { stdio: "inherit" };
5227
+ return await new Promise((resolve2) => {
5228
+ const child = spawnFn(
5229
+ "npm",
5230
+ ["i", "-g", "codealmanac@latest"],
5231
+ spawnOpts
5232
+ );
5233
+ child.on("error", (err) => {
5234
+ if (err.code === "ENOENT") {
5235
+ resolve2({
5236
+ stdout: "",
5237
+ stderr: "codealmanac: `npm` not found on PATH. Install Node.js + npm, or install codealmanac via your package manager.\n",
5238
+ exitCode: 1
5239
+ });
5240
+ return;
5241
+ }
5242
+ resolve2({
5243
+ stdout: "",
5244
+ stderr: `codealmanac: failed to run npm: ${err.message}
5245
+ `,
5246
+ exitCode: 1
5247
+ });
5248
+ });
5249
+ child.on("exit", async (code, _signal) => {
5250
+ const exitCode = code ?? 1;
5251
+ if (exitCode !== 0) {
5252
+ const hint = `codealmanac: npm install failed (exit ${exitCode}).
5253
+ If you see "EACCES" above, try: sudo npm i -g codealmanac@latest
5254
+ Or install with a version manager (nvm, volta, fnm) to avoid sudo.
5255
+ `;
5256
+ resolve2({ stdout: "", stderr: hint, exitCode });
5257
+ return;
5258
+ }
5259
+ try {
5260
+ const state = await readState(opts.statePath);
5261
+ const now = opts.now ?? (() => Math.floor(Date.now() / 1e3));
5262
+ await writeState(
5263
+ {
5264
+ last_check_at: now(),
5265
+ installed_version: state.latest_version || installed,
5266
+ latest_version: state.latest_version || installed,
5267
+ dismissed_versions: state.dismissed_versions
5268
+ },
5269
+ opts.statePath
5270
+ );
5271
+ } catch {
5272
+ }
5273
+ resolve2({
5274
+ stdout: "codealmanac: updated.\n",
5275
+ stderr: "",
5276
+ exitCode: 0
5277
+ });
5278
+ });
5279
+ });
5280
+ }
5281
+ function readInstalledVersion2() {
5282
+ try {
5283
+ const require2 = createRequire5(import.meta.url);
5284
+ const pkg = require2("../../package.json");
5285
+ if (typeof pkg.version === "string" && pkg.version.length > 0) {
5286
+ return pkg.version;
5287
+ }
5288
+ } catch {
5289
+ }
5290
+ try {
5291
+ const require2 = createRequire5(import.meta.url);
5292
+ const pkg = require2("../package.json");
5293
+ if (typeof pkg.version === "string" && pkg.version.length > 0) {
5294
+ return pkg.version;
5295
+ }
5296
+ } catch {
5297
+ }
5298
+ return "unknown";
5299
+ }
5300
+
4595
5301
  // src/registry/autoregister.ts
4596
5302
  import { existsSync as existsSync15 } from "fs";
4597
5303
  import { basename as basename5 } from "path";
@@ -4642,10 +5348,104 @@ function samePath(a, b) {
4642
5348
  return a === b;
4643
5349
  }
4644
5350
 
5351
+ // src/update/announce.ts
5352
+ import { readFileSync as readFileSync3 } from "fs";
5353
+ import { createRequire as createRequire6 } from "module";
5354
+ var RST4 = "\x1B[0m";
5355
+ var BOLD3 = "\x1B[1m";
5356
+ var YELLOW = "\x1B[33m";
5357
+ function announceUpdateIfAvailable(stderr, opts = {}) {
5358
+ const statePath = opts.statePath ?? getStatePath();
5359
+ const configPath = opts.configPath ?? getConfigPath();
5360
+ const installed = opts.installedVersion ?? readInstalledVersion3();
5361
+ if (!shouldNotify(configPath)) return;
5362
+ const state = readStateSync(statePath);
5363
+ if (state === null) return;
5364
+ if (state.latest_version.length === 0) return;
5365
+ if (!isNewer(state.latest_version, installed)) return;
5366
+ if (state.dismissed_versions.includes(state.latest_version)) return;
5367
+ const useColor = opts.color ?? (process.stderr.isTTY === true && !("NO_COLOR" in process.env));
5368
+ const warn = useColor ? `${YELLOW}${BOLD3}\u26A0${RST4}` : "!";
5369
+ const cmd = useColor ? `${BOLD3}almanac update${RST4}` : "almanac update";
5370
+ stderr.write(
5371
+ `${warn} codealmanac ${state.latest_version} available (you're on ${installed}) \u2014 run: ${cmd}
5372
+ `
5373
+ );
5374
+ }
5375
+ function readStateSync(path6) {
5376
+ let raw;
5377
+ try {
5378
+ raw = readFileSync3(path6, "utf8");
5379
+ } catch {
5380
+ return null;
5381
+ }
5382
+ const trimmed = raw.trim();
5383
+ if (trimmed.length === 0) return null;
5384
+ try {
5385
+ const parsed = JSON.parse(trimmed);
5386
+ return {
5387
+ last_check_at: typeof parsed.last_check_at === "number" ? parsed.last_check_at : 0,
5388
+ installed_version: typeof parsed.installed_version === "string" ? parsed.installed_version : "",
5389
+ latest_version: typeof parsed.latest_version === "string" ? parsed.latest_version : "",
5390
+ dismissed_versions: Array.isArray(parsed.dismissed_versions) ? parsed.dismissed_versions.filter(
5391
+ (v) => typeof v === "string"
5392
+ ) : []
5393
+ };
5394
+ } catch {
5395
+ return null;
5396
+ }
5397
+ }
5398
+ function shouldNotify(configPath) {
5399
+ let raw;
5400
+ try {
5401
+ raw = readFileSync3(configPath, "utf8");
5402
+ } catch {
5403
+ return true;
5404
+ }
5405
+ const trimmed = raw.trim();
5406
+ if (trimmed.length === 0) return true;
5407
+ try {
5408
+ const parsed = JSON.parse(trimmed);
5409
+ if (parsed.update_notifier === false) return false;
5410
+ return true;
5411
+ } catch {
5412
+ return true;
5413
+ }
5414
+ }
5415
+ function readInstalledVersion3() {
5416
+ try {
5417
+ const require2 = createRequire6(import.meta.url);
5418
+ const pkg = require2("../../package.json");
5419
+ if (typeof pkg.version === "string" && pkg.version.length > 0) {
5420
+ return pkg.version;
5421
+ }
5422
+ } catch {
5423
+ }
5424
+ try {
5425
+ const require2 = createRequire6(import.meta.url);
5426
+ const pkg = require2("../package.json");
5427
+ if (typeof pkg.version === "string" && pkg.version.length > 0) {
5428
+ return pkg.version;
5429
+ }
5430
+ } catch {
5431
+ }
5432
+ return "unknown";
5433
+ }
5434
+
4645
5435
  // src/cli.ts
4646
- async function run(argv) {
5436
+ async function run(argv, deps = {}) {
5437
+ const runSetupFn = deps.runSetup ?? runSetup;
5438
+ const announceUpdateFn = deps.announceUpdate ?? announceUpdateIfAvailable;
5439
+ const scheduleUpdateCheckFn = deps.scheduleUpdateCheck ?? scheduleBackgroundUpdateCheck;
5440
+ const runInternalUpdateCheckFn = deps.runInternalUpdateCheck ?? runInternalUpdateCheck;
5441
+ if (argv.slice(2).includes("--internal-check-updates")) {
5442
+ await runInternalUpdateCheckFn();
5443
+ return;
5444
+ }
4647
5445
  const invoked = argv[1] !== void 0 ? basename6(argv[1]) : "almanac";
4648
5446
  const programName = invoked === "codealmanac" ? "codealmanac" : "almanac";
5447
+ announceUpdateFn(process.stderr);
5448
+ scheduleUpdateCheckFn(argv);
4649
5449
  const program = new Command();
4650
5450
  program.name(programName).description(
4651
5451
  "codealmanac \u2014 a living wiki for codebases, maintained by AI agents"
@@ -4653,7 +5453,7 @@ async function run(argv) {
4653
5453
  if (programName === "codealmanac") {
4654
5454
  const setupInvocation = tryParseSetupShortcut(argv.slice(2));
4655
5455
  if (setupInvocation !== null) {
4656
- const result = await runSetup(setupInvocation);
5456
+ const result = await runSetupFn(setupInvocation);
4657
5457
  if (result.stderr.length > 0) process.stderr.write(result.stderr);
4658
5458
  if (result.stdout.length > 0) process.stdout.write(result.stdout);
4659
5459
  if (result.exitCode !== 0) process.exitCode = result.exitCode;
@@ -4954,6 +5754,31 @@ async function run(argv) {
4954
5754
  emit(result);
4955
5755
  }
4956
5756
  );
5757
+ program.command("update").description(
5758
+ "install the latest codealmanac (synchronous foreground `npm i -g`)"
5759
+ ).option(
5760
+ "--dismiss",
5761
+ "silence the update banner for the current `latest_version` without installing"
5762
+ ).option(
5763
+ "--check",
5764
+ "force a registry check now (bypasses the 24h cache); no install"
5765
+ ).option(
5766
+ "--enable-notifier",
5767
+ "re-enable the pre-command update banner (writes ~/.almanac/config.json)"
5768
+ ).option(
5769
+ "--disable-notifier",
5770
+ "silence the pre-command update banner (writes ~/.almanac/config.json)"
5771
+ ).action(
5772
+ async (opts) => {
5773
+ const result = await runUpdate({
5774
+ dismiss: opts.dismiss,
5775
+ check: opts.check,
5776
+ enableNotifier: opts.enableNotifier,
5777
+ disableNotifier: opts.disableNotifier
5778
+ });
5779
+ emit(result);
5780
+ }
5781
+ );
4957
5782
  program.command("uninstall").description("remove the hook + guides + import line").option("-y, --yes", "skip confirmations; remove everything").option(
4958
5783
  "--keep-hook",
4959
5784
  "don't remove the SessionEnd hook (guides still prompted unless --yes)"
@@ -4988,7 +5813,7 @@ var HELP_GROUPS = [
4988
5813
  },
4989
5814
  {
4990
5815
  title: "Setup",
4991
- commands: ["setup", "uninstall", "doctor"]
5816
+ commands: ["setup", "uninstall", "doctor", "update"]
4992
5817
  }
4993
5818
  ];
4994
5819
  function configureGroupedHelp(program) {
@@ -5036,6 +5861,7 @@ function configureGroupedHelp(program) {
5036
5861
  }
5037
5862
  out.push("");
5038
5863
  }
5864
+ byName.delete("help");
5039
5865
  if (byName.size > 0) {
5040
5866
  out.push("Other:");
5041
5867
  for (const c of byName.values()) {
@@ -5091,7 +5917,7 @@ function renderDefault(cmd, helper) {
5091
5917
  }
5092
5918
  function readPackageVersion2() {
5093
5919
  try {
5094
- const require2 = createRequire4(import.meta.url);
5920
+ const require2 = createRequire7(import.meta.url);
5095
5921
  const pkg = require2("../package.json");
5096
5922
  if (typeof pkg.version === "string" && pkg.version.length > 0) {
5097
5923
  return pkg.version;