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.
- package/dist/codealmanac.js +1016 -206
- package/dist/codealmanac.js.map +1 -1
- package/guides/mini.md +27 -1
- package/guides/reference.md +80 -6
- package/package.json +1 -1
- package/prompts/reviewer.md +1 -1
- package/prompts/writer.md +1 -1
package/dist/codealmanac.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { createRequire as
|
|
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
|
|
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
|
|
1066
|
-
import { createRequire as
|
|
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
|
|
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(
|
|
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 =
|
|
1784
|
-
const pagesDir =
|
|
1785
|
-
const db = openIndex(
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
2077
|
-
readFile as
|
|
2078
|
-
writeFile as
|
|
2360
|
+
mkdir as mkdir7,
|
|
2361
|
+
readFile as readFile11,
|
|
2362
|
+
writeFile as writeFile7
|
|
2079
2363
|
} from "fs/promises";
|
|
2080
|
-
import { createRequire as
|
|
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
|
|
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
|
|
2102
|
-
|
|
2103
|
-
const
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
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
|
|
2114
|
-
${
|
|
2115
|
-
Remove it manually
|
|
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 (
|
|
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
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
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 =
|
|
2159
|
-
|
|
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
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
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
|
-
` + (
|
|
2210
|
-
${
|
|
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: ${
|
|
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
|
|
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
|
|
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
|
|
2274
|
-
await
|
|
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
|
|
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
|
|
2859
|
+
const srcBytes = await readFile11(src);
|
|
2461
2860
|
if (existsSync11(dest)) {
|
|
2462
2861
|
try {
|
|
2463
|
-
const destBytes = await
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
3064
|
+
const raw = await readFile12(settingsPath, "utf8");
|
|
2665
3065
|
const parsed = JSON.parse(raw);
|
|
2666
3066
|
const entries = parsed.hooks?.SessionEnd ?? [];
|
|
2667
|
-
const
|
|
2668
|
-
(
|
|
2669
|
-
|
|
2670
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
3295
|
-
import { join as
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3619
|
-
await
|
|
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
|
|
4326
|
+
import { join as join13 } from "path";
|
|
3826
4327
|
function topicsYamlPath(repoRoot) {
|
|
3827
|
-
return
|
|
4328
|
+
return join13(repoRoot, ".almanac", "topics.yaml");
|
|
3828
4329
|
}
|
|
3829
4330
|
function indexDbPath(repoRoot) {
|
|
3830
|
-
return
|
|
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
|
|
3990
|
-
import { join as
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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;
|