codealmanac 0.1.4 → 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,125 +1062,13 @@ function closeStream2(stream) {
1061
1062
  }
1062
1063
 
1063
1064
  // src/commands/doctor.ts
1064
- import { existsSync as existsSync12, readFileSync, 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
  import { fileURLToPath as fileURLToPath4 } from "url";
1070
1071
 
1071
- // src/indexer/schema.ts
1072
- import Database from "better-sqlite3";
1073
- var SCHEMA_DDL = `
1074
- CREATE TABLE IF NOT EXISTS pages (
1075
- slug TEXT PRIMARY KEY,
1076
- title TEXT,
1077
- file_path TEXT NOT NULL,
1078
- content_hash TEXT NOT NULL,
1079
- updated_at INTEGER NOT NULL,
1080
- archived_at INTEGER,
1081
- superseded_by TEXT
1082
- );
1083
-
1084
- CREATE TABLE IF NOT EXISTS topics (
1085
- slug TEXT PRIMARY KEY,
1086
- title TEXT,
1087
- description TEXT
1088
- );
1089
-
1090
- CREATE TABLE IF NOT EXISTS page_topics (
1091
- page_slug TEXT NOT NULL REFERENCES pages(slug) ON DELETE CASCADE,
1092
- topic_slug TEXT NOT NULL,
1093
- PRIMARY KEY (page_slug, topic_slug)
1094
- );
1095
-
1096
- CREATE TABLE IF NOT EXISTS topic_parents (
1097
- child_slug TEXT NOT NULL,
1098
- parent_slug TEXT NOT NULL,
1099
- PRIMARY KEY (child_slug, parent_slug),
1100
- CHECK (child_slug != parent_slug)
1101
- );
1102
-
1103
- CREATE TABLE IF NOT EXISTS file_refs (
1104
- page_slug TEXT NOT NULL REFERENCES pages(slug) ON DELETE CASCADE,
1105
- path TEXT NOT NULL,
1106
- original_path TEXT NOT NULL,
1107
- is_dir INTEGER NOT NULL,
1108
- PRIMARY KEY (page_slug, path)
1109
- );
1110
- CREATE INDEX IF NOT EXISTS idx_file_refs_path ON file_refs(path);
1111
-
1112
- CREATE TABLE IF NOT EXISTS wikilinks (
1113
- source_slug TEXT NOT NULL REFERENCES pages(slug) ON DELETE CASCADE,
1114
- target_slug TEXT NOT NULL,
1115
- PRIMARY KEY (source_slug, target_slug)
1116
- );
1117
-
1118
- CREATE TABLE IF NOT EXISTS cross_wiki_links (
1119
- source_slug TEXT NOT NULL REFERENCES pages(slug) ON DELETE CASCADE,
1120
- target_wiki TEXT NOT NULL,
1121
- target_slug TEXT NOT NULL,
1122
- PRIMARY KEY (source_slug, target_wiki, target_slug)
1123
- );
1124
-
1125
- -- NOTE: virtual FTS5 table \u2014 ON DELETE CASCADE from pages does NOT apply.
1126
- -- The indexer must explicitly DELETE FROM fts_pages whenever it removes
1127
- -- or replaces a page row, or we leak orphaned FTS rows.
1128
- CREATE VIRTUAL TABLE IF NOT EXISTS fts_pages USING fts5(slug, title, content);
1129
- `;
1130
- var SCHEMA_VERSION = 2;
1131
- function openIndex(dbPath) {
1132
- const db = new Database(dbPath);
1133
- const mode = db.pragma("journal_mode", { simple: true });
1134
- if (typeof mode !== "string" || mode.toLowerCase() !== "wal") {
1135
- db.pragma("journal_mode = WAL");
1136
- }
1137
- db.pragma("foreign_keys = ON");
1138
- const rawVersion = db.pragma("user_version", { simple: true });
1139
- const currentVersion = typeof rawVersion === "number" ? rawVersion : 0;
1140
- if (currentVersion < SCHEMA_VERSION) {
1141
- db.exec("DROP TABLE IF EXISTS file_refs");
1142
- try {
1143
- db.exec("UPDATE pages SET content_hash = ''");
1144
- } catch {
1145
- }
1146
- db.pragma(`user_version = ${SCHEMA_VERSION}`);
1147
- }
1148
- db.exec(SCHEMA_DDL);
1149
- return db;
1150
- }
1151
-
1152
- // src/commands/health.ts
1153
- import { existsSync as existsSync9 } from "fs";
1154
- import { readFile as readFile7 } from "fs/promises";
1155
- import { basename as basename4, join as join8 } from "path";
1156
- import fg2 from "fast-glob";
1157
-
1158
- // src/indexer/duration.ts
1159
- function parseDuration(input) {
1160
- const trimmed = input.trim();
1161
- const m = trimmed.match(/^(\d+)([mhdw])$/);
1162
- if (m === null) {
1163
- throw new Error(
1164
- `invalid duration "${input}" (expected Nw, Nd, Nh, or Nm \u2014 e.g. 2w, 30d)`
1165
- );
1166
- }
1167
- const n = Number.parseInt(m[1] ?? "0", 10);
1168
- const unit = m[2];
1169
- switch (unit) {
1170
- case "m":
1171
- return n * 60;
1172
- case "h":
1173
- return n * 60 * 60;
1174
- case "d":
1175
- return n * 60 * 60 * 24;
1176
- case "w":
1177
- return n * 60 * 60 * 24 * 7;
1178
- default:
1179
- throw new Error(`invalid duration unit "${unit ?? ""}"`);
1180
- }
1181
- }
1182
-
1183
1072
  // src/indexer/index.ts
1184
1073
  import { createHash as createHash2 } from "crypto";
1185
1074
  import { existsSync as existsSync7, statSync as statSync2 } from "fs";
@@ -1340,6 +1229,87 @@ function looksLikeDir(raw) {
1340
1229
  return s.endsWith("/");
1341
1230
  }
1342
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
+
1343
1313
  // src/indexer/wikilinks.ts
1344
1314
  function classifyWikilink(raw) {
1345
1315
  const pipe = raw.indexOf("|");
@@ -1709,16 +1679,330 @@ async function applyTopicsYaml(db, topicsYamlPath2) {
1709
1679
  apply();
1710
1680
  }
1711
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
+
1712
1996
  // src/indexer/resolveWiki.ts
1713
1997
  import { existsSync as existsSync8 } from "fs";
1714
- import { join as join7 } from "path";
1998
+ import { join as join9 } from "path";
1715
1999
  async function resolveWikiRoot(params) {
1716
2000
  if (params.wiki !== void 0) {
1717
2001
  const entry = await findEntry({ name: params.wiki });
1718
2002
  if (entry === null) {
1719
2003
  throw new Error(`no registered wiki named "${params.wiki}"`);
1720
2004
  }
1721
- if (!existsSync8(join7(entry.path, ".almanac"))) {
2005
+ if (!existsSync8(join9(entry.path, ".almanac"))) {
1722
2006
  throw new Error(
1723
2007
  `wiki "${params.wiki}" path is unreachable (${entry.path})`
1724
2008
  );
@@ -1780,9 +2064,9 @@ var DEFAULT_STALE_SECONDS = 90 * 24 * 60 * 60;
1780
2064
  async function runHealth(options) {
1781
2065
  const repoRoot = await resolveWikiRoot({ cwd: options.cwd, wiki: options.wiki });
1782
2066
  await ensureFreshIndex({ repoRoot });
1783
- const almanacDir = join8(repoRoot, ".almanac");
1784
- const pagesDir = join8(almanacDir, "pages");
1785
- 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"));
1786
2070
  try {
1787
2071
  const staleSeconds = options.stale !== void 0 ? parseDuration(options.stale) : DEFAULT_STALE_SECONDS;
1788
2072
  const scope = resolveScope(db, options);
@@ -1883,7 +2167,7 @@ async function findDeadRefs(db, scope, repoRoot) {
1883
2167
  const out = [];
1884
2168
  for (const r of rows) {
1885
2169
  if (!inPageScope(scope, r.slug)) continue;
1886
- const abs = join8(repoRoot, r.original_path);
2170
+ const abs = join10(repoRoot, r.original_path);
1887
2171
  if (!existsSync9(abs)) {
1888
2172
  out.push({ slug: r.slug, path: r.original_path });
1889
2173
  }
@@ -1919,7 +2203,7 @@ async function findBrokenXwiki(db, scope) {
1919
2203
  let ok = reachableCache.get(r.target_wiki);
1920
2204
  if (ok === void 0) {
1921
2205
  const entry = await findEntry({ name: r.target_wiki });
1922
- ok = entry !== null && existsSync9(join8(entry.path, ".almanac"));
2206
+ ok = entry !== null && existsSync9(join10(entry.path, ".almanac"));
1923
2207
  reachableCache.set(r.target_wiki, ok);
1924
2208
  }
1925
2209
  if (!ok) {
@@ -1954,7 +2238,7 @@ async function findEmptyPages(db, scope, pagesDir) {
1954
2238
  if (!inPageScope(scope, r.slug)) continue;
1955
2239
  let raw;
1956
2240
  try {
1957
- raw = await readFile7(r.file_path, "utf8");
2241
+ raw = await readFile9(r.file_path, "utf8");
1958
2242
  } catch {
1959
2243
  continue;
1960
2244
  }
@@ -2073,22 +2357,61 @@ ${lines.join("\n")}`;
2073
2357
  import { existsSync as existsSync11 } from "fs";
2074
2358
  import {
2075
2359
  copyFile,
2076
- mkdir as mkdir5,
2077
- readFile as readFile9,
2078
- writeFile as writeFile5
2360
+ mkdir as mkdir7,
2361
+ readFile as readFile11,
2362
+ writeFile as writeFile7
2079
2363
  } from "fs/promises";
2080
- import { createRequire as createRequire2 } from "module";
2364
+ import { createRequire as createRequire3 } from "module";
2081
2365
  import { homedir as homedir4 } from "os";
2082
2366
  import path3 from "path";
2083
2367
  import { fileURLToPath as fileURLToPath3 } from "url";
2084
2368
 
2085
2369
  // src/commands/hook.ts
2086
2370
  import { existsSync as existsSync10 } from "fs";
2087
- 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";
2088
2372
  import { homedir as homedir3 } from "os";
2089
2373
  import path2 from "path";
2090
2374
  import { fileURLToPath as fileURLToPath2 } from "url";
2091
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
+ }
2092
2415
  async function runHookInstall(options = {}) {
2093
2416
  const script = resolveHookScriptPath(options);
2094
2417
  if (!script.ok) {
@@ -2098,26 +2421,54 @@ async function runHookInstall(options = {}) {
2098
2421
  const settingsPath = resolveSettingsPath(options);
2099
2422
  const settings = await readSettings(settingsPath);
2100
2423
  const existing = (settings.hooks?.SessionEnd ?? []).slice();
2101
- const ourEntries = existing.filter((e) => e.command === script.path);
2102
- const foreignEntries = existing.filter((e) => e.command !== script.path);
2103
- const stale = foreignEntries.filter(
2104
- (e) => e.command.endsWith("almanac-capture.sh")
2105
- );
2106
- const unrelated = foreignEntries.filter(
2107
- (e) => !e.command.endsWith("almanac-capture.sh")
2108
- );
2109
- if (unrelated.length > 0) {
2110
- 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");
2111
2462
  return {
2112
2463
  stdout: "",
2113
- stderr: `almanac: SessionEnd hook already has a foreign entry:
2114
- ${existingStr}
2115
- 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.
2116
2467
  `,
2117
2468
  exitCode: 1
2118
2469
  };
2119
2470
  }
2120
- if (ourEntries.length > 0 && stale.length === 0) {
2471
+ if (oursAlready !== null && staleCount.n === 0) {
2121
2472
  return {
2122
2473
  stdout: `almanac: SessionEnd hook already installed at ${script.path}
2123
2474
  `,
@@ -2125,13 +2476,17 @@ Remove it manually from ${settingsPath} if you want almanac to manage the hook.
2125
2476
  exitCode: 0
2126
2477
  };
2127
2478
  }
2128
- const newEntries = [
2129
- {
2130
- type: "command",
2131
- command: script.path,
2132
- timeout: HOOK_TIMEOUT_SECONDS
2133
- }
2134
- ];
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];
2135
2490
  settings.hooks = { ...settings.hooks ?? {}, SessionEnd: newEntries };
2136
2491
  await writeSettings(settingsPath, settings);
2137
2492
  return {
@@ -2155,8 +2510,33 @@ async function runHookUninstall(options = {}) {
2155
2510
  }
2156
2511
  const settings = await readSettings(settingsPath);
2157
2512
  const existing = (settings.hooks?.SessionEnd ?? []).slice();
2158
- const kept = existing.filter((e) => !e.command.endsWith("almanac-capture.sh"));
2159
- 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
+ }
2160
2540
  if (removed === 0) {
2161
2541
  return {
2162
2542
  stdout: `almanac: SessionEnd hook not installed
@@ -2200,14 +2580,33 @@ settings: ${settingsPath} (does not exist)
2200
2580
  }
2201
2581
  const settings = await readSettings(settingsPath);
2202
2582
  const existing = settings.hooks?.SessionEnd ?? [];
2203
- const ours = existing.find((e) => e.command.endsWith("almanac-capture.sh"));
2204
- if (ours === void 0) {
2205
- 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");
2206
2605
  return {
2207
2606
  stdout: `SessionEnd hook: not installed
2208
2607
  settings: ${settingsPath}
2209
- ` + (existing.length > 0 ? `(${existing.length} foreign entr${existing.length === 1 ? "y" : "ies"} present:
2210
- ${foreign})
2608
+ ` + (foreignSummary.length > 0 ? `(${foreignSummary.length} foreign entr${foreignSummary.length === 1 ? "y" : "ies"} present:
2609
+ ${foreignLines})
2211
2610
  ` : "") + (script.ok ? `script would be: ${script.path}
2212
2611
  ` : ""),
2213
2612
  stderr: "",
@@ -2216,7 +2615,7 @@ ${foreign})
2216
2615
  }
2217
2616
  return {
2218
2617
  stdout: `SessionEnd hook: installed
2219
- script: ${ours.command}
2618
+ script: ${ourCommand}
2220
2619
  settings: ${settingsPath}
2221
2620
  `,
2222
2621
  stderr: "",
@@ -2254,7 +2653,7 @@ function resolveHookScriptPath(options) {
2254
2653
  async function readSettings(settingsPath) {
2255
2654
  if (!existsSync10(settingsPath)) return {};
2256
2655
  try {
2257
- const raw = await readFile8(settingsPath, "utf8");
2656
+ const raw = await readFile10(settingsPath, "utf8");
2258
2657
  if (raw.trim().length === 0) return {};
2259
2658
  const parsed = JSON.parse(raw);
2260
2659
  if (parsed === null || typeof parsed !== "object") return {};
@@ -2266,12 +2665,12 @@ async function readSettings(settingsPath) {
2266
2665
  }
2267
2666
  async function writeSettings(settingsPath, settings) {
2268
2667
  const dir = path2.dirname(settingsPath);
2269
- await mkdir4(dir, { recursive: true });
2668
+ await mkdir6(dir, { recursive: true });
2270
2669
  const tmp = `${settingsPath}.almanac-tmp-${process.pid}`;
2271
2670
  const body = `${JSON.stringify(settings, null, 2)}
2272
2671
  `;
2273
- await writeFile4(tmp, body, "utf8");
2274
- await rename3(tmp, settingsPath);
2672
+ await writeFile6(tmp, body, "utf8");
2673
+ await rename5(tmp, settingsPath);
2275
2674
  }
2276
2675
 
2277
2676
  // src/commands/setup.ts
@@ -2435,7 +2834,7 @@ function reportAuth(out, auth) {
2435
2834
  }
2436
2835
  }
2437
2836
  async function installGuides(options) {
2438
- await mkdir5(options.claudeDir, { recursive: true });
2837
+ await mkdir7(options.claudeDir, { recursive: true });
2439
2838
  const srcMini = path3.join(options.guidesDir, "mini.md");
2440
2839
  const srcRef = path3.join(options.guidesDir, "reference.md");
2441
2840
  if (!existsSync11(srcMini)) {
@@ -2457,10 +2856,10 @@ async function installGuides(options) {
2457
2856
  return { anyChanges: filesWritten.length > 0, filesWritten };
2458
2857
  }
2459
2858
  async function copyIfChanged(src, dest) {
2460
- const srcBytes = await readFile9(src);
2859
+ const srcBytes = await readFile11(src);
2461
2860
  if (existsSync11(dest)) {
2462
2861
  try {
2463
- const destBytes = await readFile9(dest);
2862
+ const destBytes = await readFile11(dest);
2464
2863
  if (srcBytes.equals(destBytes)) return false;
2465
2864
  } catch {
2466
2865
  }
@@ -2472,13 +2871,13 @@ var IMPORT_LINE = "@~/.claude/codealmanac.md";
2472
2871
  async function ensureImport(claudeMdPath) {
2473
2872
  let existing = "";
2474
2873
  if (existsSync11(claudeMdPath)) {
2475
- existing = await readFile9(claudeMdPath, "utf8");
2874
+ existing = await readFile11(claudeMdPath, "utf8");
2476
2875
  }
2477
2876
  if (hasImportLine(existing)) return false;
2478
2877
  const sep = existing.length === 0 ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
2479
2878
  const body = `${existing}${sep}${IMPORT_LINE}
2480
2879
  `;
2481
- await writeFile5(claudeMdPath, body, "utf8");
2880
+ await writeFile7(claudeMdPath, body, "utf8");
2482
2881
  return true;
2483
2882
  }
2484
2883
  function hasImportLine(contents) {
@@ -2554,7 +2953,7 @@ function resolveGuidesDir() {
2554
2953
  if (looksLikeGuidesDir(dir)) return dir;
2555
2954
  }
2556
2955
  try {
2557
- const require2 = createRequire2(import.meta.url);
2956
+ const require2 = createRequire3(import.meta.url);
2558
2957
  const pkgJson = require2.resolve("codealmanac/package.json");
2559
2958
  const guides = path3.join(path3.dirname(pkgJson), "guides");
2560
2959
  if (looksLikeGuidesDir(guides)) return guides;
@@ -2578,8 +2977,9 @@ var BLUE2 = "\x1B[38;5;75m";
2578
2977
  async function runDoctor(options) {
2579
2978
  const version = options.versionOverride ?? readPackageVersion() ?? "unknown";
2580
2979
  const install = options.wikiOnly === true ? [] : await gatherInstallChecks(options);
2980
+ const updates = options.wikiOnly === true ? [] : await gatherUpdateChecks(options, version);
2581
2981
  const wiki = options.installOnly === true ? [] : await gatherWikiChecks(options);
2582
- const report = { version, install, wiki };
2982
+ const report = { version, install, updates, wiki };
2583
2983
  if (options.json === true) {
2584
2984
  return {
2585
2985
  stdout: `${JSON.stringify(report, null, 2)}
@@ -2661,13 +3061,21 @@ async function describeHook(settingsPath) {
2661
3061
  };
2662
3062
  }
2663
3063
  try {
2664
- const raw = await readFile10(settingsPath, "utf8");
3064
+ const raw = await readFile12(settingsPath, "utf8");
2665
3065
  const parsed = JSON.parse(raw);
2666
3066
  const entries = parsed.hooks?.SessionEnd ?? [];
2667
- const ours = entries.find(
2668
- (e) => typeof e.command === "string" && e.command.endsWith("almanac-capture.sh")
2669
- );
2670
- 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) {
2671
3079
  return {
2672
3080
  status: "problem",
2673
3081
  key: "install.hook",
@@ -2724,7 +3132,7 @@ async function describeImportLine(claudeDir) {
2724
3132
  };
2725
3133
  }
2726
3134
  try {
2727
- const contents = await readFile10(claudeMd, "utf8");
3135
+ const contents = await readFile12(claudeMd, "utf8");
2728
3136
  const lines = contents.split(/\r?\n/).map((l) => l.trim());
2729
3137
  const present = lines.some((line) => {
2730
3138
  if (line === IMPORT_LINE) return true;
@@ -2754,6 +3162,63 @@ async function describeImportLine(claudeDir) {
2754
3162
  };
2755
3163
  }
2756
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
+ }
2757
3222
  async function gatherWikiChecks(options) {
2758
3223
  const checks = [];
2759
3224
  const repoRoot = findNearestAlmanacDir(options.cwd);
@@ -2771,14 +3236,30 @@ async function gatherWikiChecks(options) {
2771
3236
  key: "wiki.repo",
2772
3237
  message: `repo: ${repoRoot}`
2773
3238
  });
2774
- 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
+ }
2775
3256
  if (entry !== null) {
2776
3257
  checks.push({
2777
3258
  status: "ok",
2778
3259
  key: "wiki.registered",
2779
3260
  message: `registered as '${entry.name}'`
2780
3261
  });
2781
- } else {
3262
+ } else if (checks[checks.length - 1]?.key !== "wiki.registered") {
2782
3263
  checks.push({
2783
3264
  status: "info",
2784
3265
  key: "wiki.registered",
@@ -2895,7 +3376,9 @@ function describeLastCapture(almanacDir, nowFn) {
2895
3376
  message: "last capture: unknown"
2896
3377
  };
2897
3378
  }
2898
- 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) => {
2899
3382
  try {
2900
3383
  return {
2901
3384
  name: e,
@@ -2922,12 +3405,23 @@ function describeLastCapture(almanacDir, nowFn) {
2922
3405
  message: `last capture: ${formatDuration(age)} ago (${latest.name})`
2923
3406
  };
2924
3407
  }
2925
- 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
+ ];
2926
3419
  function countHealthProblems(jsonStdout) {
2927
3420
  try {
2928
3421
  const report = JSON.parse(jsonStdout);
2929
3422
  let total = 0;
2930
- for (const arr of Object.values(report)) {
3423
+ for (const key of HEALTH_PROBLEM_KEYS) {
3424
+ const arr = report[key];
2931
3425
  if (Array.isArray(arr)) total += arr.length;
2932
3426
  }
2933
3427
  return total;
@@ -2943,7 +3437,7 @@ function detectInstallPath() {
2943
3437
  const pkgPath = path4.join(dir, "package.json");
2944
3438
  if (existsSync12(pkgPath)) {
2945
3439
  try {
2946
- const raw = readFileSync(pkgPath, "utf-8");
3440
+ const raw = readFileSync2(pkgPath, "utf-8");
2947
3441
  const pkg = JSON.parse(raw);
2948
3442
  if (pkg.name === "codealmanac") return dir;
2949
3443
  } catch {
@@ -3006,6 +3500,13 @@ function formatReport2(report, options) {
3006
3500
  }
3007
3501
  lines.push("");
3008
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
+ }
3009
3510
  if (report.wiki.length > 0) {
3010
3511
  lines.push(color ? `${BOLD2}## Current wiki${RST2}` : "## Current wiki");
3011
3512
  for (const c of report.wiki) {
@@ -3112,14 +3613,14 @@ async function runReindex(options) {
3112
3613
  }
3113
3614
 
3114
3615
  // src/commands/search.ts
3115
- import { join as join9 } from "path";
3616
+ import { join as join11 } from "path";
3116
3617
  async function runSearch(options) {
3117
3618
  const repoRoot = await resolveWikiRoot({
3118
3619
  cwd: options.cwd,
3119
3620
  wiki: options.wiki
3120
3621
  });
3121
3622
  await ensureFreshIndex({ repoRoot });
3122
- const dbPath = join9(repoRoot, ".almanac", "index.db");
3623
+ const dbPath = join11(repoRoot, ".almanac", "index.db");
3123
3624
  const db = openIndex(dbPath);
3124
3625
  try {
3125
3626
  const rows = executeQuery(db, options);
@@ -3291,15 +3792,15 @@ function buildStderr(rows, options) {
3291
3792
  }
3292
3793
 
3293
3794
  // src/commands/show.ts
3294
- import { readFile as readFile11 } from "fs/promises";
3295
- import { join as join10 } from "path";
3795
+ import { readFile as readFile13 } from "fs/promises";
3796
+ import { join as join12 } from "path";
3296
3797
  async function runShow(options) {
3297
3798
  const repoRoot = await resolveWikiRoot({
3298
3799
  cwd: options.cwd,
3299
3800
  wiki: options.wiki
3300
3801
  });
3301
3802
  await ensureFreshIndex({ repoRoot });
3302
- const dbPath = join10(repoRoot, ".almanac", "index.db");
3803
+ const dbPath = join12(repoRoot, ".almanac", "index.db");
3303
3804
  const db = openIndex(dbPath);
3304
3805
  try {
3305
3806
  const slugs = collectSlugs(options);
@@ -3358,7 +3859,7 @@ async function fetchRecord(db, slug) {
3358
3859
  ).all(slug).map((r) => r.slug);
3359
3860
  let body = "";
3360
3861
  try {
3361
- body = stripFrontmatter(await readFile11(pageRow.file_path, "utf8"));
3862
+ body = stripFrontmatter(await readFile13(pageRow.file_path, "utf8"));
3362
3863
  } catch {
3363
3864
  }
3364
3865
  return {
@@ -3605,18 +4106,18 @@ function collectSlugs(options) {
3605
4106
  }
3606
4107
 
3607
4108
  // src/topics/frontmatterRewrite.ts
3608
- 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";
3609
4110
  import yaml3 from "js-yaml";
3610
4111
  async function rewritePageTopics(filePath, transform) {
3611
- const raw = await readFile12(filePath, "utf8");
4112
+ const raw = await readFile14(filePath, "utf8");
3612
4113
  const { before, after, output, changed } = applyTopicsTransform(
3613
4114
  raw,
3614
4115
  transform
3615
4116
  );
3616
4117
  if (changed) {
3617
4118
  const tmp = `${filePath}.tmp`;
3618
- await writeFile6(tmp, output, "utf8");
3619
- await rename4(tmp, filePath);
4119
+ await writeFile8(tmp, output, "utf8");
4120
+ await rename6(tmp, filePath);
3620
4121
  }
3621
4122
  return { before, after, changed };
3622
4123
  }
@@ -3822,12 +4323,12 @@ function arraysEqual(a, b) {
3822
4323
  }
3823
4324
 
3824
4325
  // src/topics/paths.ts
3825
- import { join as join11 } from "path";
4326
+ import { join as join13 } from "path";
3826
4327
  function topicsYamlPath(repoRoot) {
3827
- return join11(repoRoot, ".almanac", "topics.yaml");
4328
+ return join13(repoRoot, ".almanac", "topics.yaml");
3828
4329
  }
3829
4330
  function indexDbPath(repoRoot) {
3830
- return join11(repoRoot, ".almanac", "index.db");
4331
+ return join13(repoRoot, ".almanac", "index.db");
3831
4332
  }
3832
4333
 
3833
4334
  // src/commands/tag.ts
@@ -3986,8 +4487,8 @@ async function runUntag(options) {
3986
4487
  }
3987
4488
 
3988
4489
  // src/commands/topics.ts
3989
- import { readFile as readFile13 } from "fs/promises";
3990
- import { join as join12 } from "path";
4490
+ import { readFile as readFile15 } from "fs/promises";
4491
+ import { join as join14 } from "path";
3991
4492
  import fg3 from "fast-glob";
3992
4493
  async function runTopicsList(options) {
3993
4494
  const repoRoot = await resolveWikiRoot({ cwd: options.cwd, wiki: options.wiki });
@@ -4459,7 +4960,7 @@ async function runTopicsDescribe(options) {
4459
4960
  };
4460
4961
  }
4461
4962
  async function rewriteTopicOnPages(repoRoot, transform) {
4462
- const pagesDir = join12(repoRoot, ".almanac", "pages");
4963
+ const pagesDir = join14(repoRoot, ".almanac", "pages");
4463
4964
  const files = await fg3("**/*.md", {
4464
4965
  cwd: pagesDir,
4465
4966
  absolute: true,
@@ -4467,7 +4968,7 @@ async function rewriteTopicOnPages(repoRoot, transform) {
4467
4968
  });
4468
4969
  let changed = 0;
4469
4970
  for (const filePath of files) {
4470
- const raw = await readFile13(filePath, "utf8");
4971
+ const raw = await readFile15(filePath, "utf8");
4471
4972
  const applied = applyTopicsTransform(raw, transform);
4472
4973
  if (!applied.changed) continue;
4473
4974
  await rewritePageTopics(filePath, transform);
@@ -4478,7 +4979,7 @@ async function rewriteTopicOnPages(repoRoot, transform) {
4478
4979
 
4479
4980
  // src/commands/uninstall.ts
4480
4981
  import { existsSync as existsSync14 } from "fs";
4481
- import { readFile as readFile14, rm, writeFile as writeFile7 } from "fs/promises";
4982
+ import { readFile as readFile16, rm, writeFile as writeFile9 } from "fs/promises";
4482
4983
  import { homedir as homedir6 } from "os";
4483
4984
  import path5 from "path";
4484
4985
  var BLUE3 = "\x1B[38;5;75m";
@@ -4559,14 +5060,14 @@ async function removeGuideFiles(claudeDir) {
4559
5060
  touched.push("codealmanac-reference.md");
4560
5061
  }
4561
5062
  if (existsSync14(claudeMd)) {
4562
- const existing = await readFile14(claudeMd, "utf8");
5063
+ const existing = await readFile16(claudeMd, "utf8");
4563
5064
  const { changed, body } = removeImportLine(existing);
4564
5065
  if (changed) {
4565
5066
  if (body.trim().length === 0) {
4566
5067
  await rm(claudeMd, { force: true });
4567
5068
  touched.push("CLAUDE.md (deleted)");
4568
5069
  } else {
4569
- await writeFile7(claudeMd, body, "utf8");
5070
+ await writeFile9(claudeMd, body, "utf8");
4570
5071
  touched.push("CLAUDE.md");
4571
5072
  }
4572
5073
  }
@@ -4608,6 +5109,195 @@ function confirm2(out, question, defaultYes) {
4608
5109
  });
4609
5110
  }
4610
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
+
4611
5301
  // src/registry/autoregister.ts
4612
5302
  import { existsSync as existsSync15 } from "fs";
4613
5303
  import { basename as basename5 } from "path";
@@ -4658,10 +5348,104 @@ function samePath(a, b) {
4658
5348
  return a === b;
4659
5349
  }
4660
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
+
4661
5435
  // src/cli.ts
4662
- 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
+ }
4663
5445
  const invoked = argv[1] !== void 0 ? basename6(argv[1]) : "almanac";
4664
5446
  const programName = invoked === "codealmanac" ? "codealmanac" : "almanac";
5447
+ announceUpdateFn(process.stderr);
5448
+ scheduleUpdateCheckFn(argv);
4665
5449
  const program = new Command();
4666
5450
  program.name(programName).description(
4667
5451
  "codealmanac \u2014 a living wiki for codebases, maintained by AI agents"
@@ -4669,7 +5453,7 @@ async function run(argv) {
4669
5453
  if (programName === "codealmanac") {
4670
5454
  const setupInvocation = tryParseSetupShortcut(argv.slice(2));
4671
5455
  if (setupInvocation !== null) {
4672
- const result = await runSetup(setupInvocation);
5456
+ const result = await runSetupFn(setupInvocation);
4673
5457
  if (result.stderr.length > 0) process.stderr.write(result.stderr);
4674
5458
  if (result.stdout.length > 0) process.stdout.write(result.stdout);
4675
5459
  if (result.exitCode !== 0) process.exitCode = result.exitCode;
@@ -4970,6 +5754,31 @@ async function run(argv) {
4970
5754
  emit(result);
4971
5755
  }
4972
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
+ );
4973
5782
  program.command("uninstall").description("remove the hook + guides + import line").option("-y, --yes", "skip confirmations; remove everything").option(
4974
5783
  "--keep-hook",
4975
5784
  "don't remove the SessionEnd hook (guides still prompted unless --yes)"
@@ -5004,7 +5813,7 @@ var HELP_GROUPS = [
5004
5813
  },
5005
5814
  {
5006
5815
  title: "Setup",
5007
- commands: ["setup", "uninstall", "doctor"]
5816
+ commands: ["setup", "uninstall", "doctor", "update"]
5008
5817
  }
5009
5818
  ];
5010
5819
  function configureGroupedHelp(program) {
@@ -5052,6 +5861,7 @@ function configureGroupedHelp(program) {
5052
5861
  }
5053
5862
  out.push("");
5054
5863
  }
5864
+ byName.delete("help");
5055
5865
  if (byName.size > 0) {
5056
5866
  out.push("Other:");
5057
5867
  for (const c of byName.values()) {
@@ -5107,7 +5917,7 @@ function renderDefault(cmd, helper) {
5107
5917
  }
5108
5918
  function readPackageVersion2() {
5109
5919
  try {
5110
- const require2 = createRequire4(import.meta.url);
5920
+ const require2 = createRequire7(import.meta.url);
5111
5921
  const pkg = require2("../package.json");
5112
5922
  if (typeof pkg.version === "string" && pkg.version.length > 0) {
5113
5923
  return pkg.version;