codealmanac 0.2.0 → 0.2.1
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/README.md +96 -91
- package/dist/{chunk-D3B2EEHL.js → chunk-B2AGSRXL.js} +2 -2
- package/dist/{chunk-3C5SY5SE.js → chunk-KQUVMF27.js} +5 -2
- package/dist/chunk-KQUVMF27.js.map +1 -0
- package/dist/{cli-CMGYLJSN.js → cli-MZEXRV6E.js} +189 -14
- package/dist/cli-MZEXRV6E.js.map +1 -0
- package/dist/codealmanac.js +1 -1
- package/dist/{doctor-BTH7GCFV.js → doctor-3BYSF3JD.js} +2 -2
- package/dist/{register-commands-7SKQLQW4.js → register-commands-DPH4ZWEE.js} +294 -31
- package/dist/register-commands-DPH4ZWEE.js.map +1 -0
- package/dist/{wiki-IPSRRGOT.js → wiki-IGNRNLUZ.js} +2 -2
- package/package.json +3 -2
- package/dist/chunk-3C5SY5SE.js.map +0 -1
- package/dist/cli-CMGYLJSN.js.map +0 -1
- package/dist/register-commands-7SKQLQW4.js.map +0 -1
- /package/dist/{chunk-D3B2EEHL.js.map → chunk-B2AGSRXL.js.map} +0 -0
- /package/dist/{doctor-BTH7GCFV.js.map → doctor-3BYSF3JD.js.map} +0 -0
- /package/dist/{wiki-IPSRRGOT.js.map → wiki-IGNRNLUZ.js.map} +0 -0
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
findEntry,
|
|
5
5
|
openIndex,
|
|
6
6
|
runHealth
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-KQUVMF27.js";
|
|
8
8
|
import {
|
|
9
9
|
formatDuration
|
|
10
10
|
} from "./chunk-4CODZRHH.js";
|
|
@@ -234,4 +234,4 @@ function countHealthProblems(jsonStdout) {
|
|
|
234
234
|
export {
|
|
235
235
|
gatherWikiChecks
|
|
236
236
|
};
|
|
237
|
-
//# sourceMappingURL=wiki-
|
|
237
|
+
//# sourceMappingURL=wiki-IGNRNLUZ.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codealmanac",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A living wiki for codebases, maintained by AI agents. Documents what the code can't say: decisions, flows, invariants, incidents, gotchas.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wiki",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"type": "module",
|
|
24
24
|
"bin": {
|
|
25
25
|
"codealmanac": "./dist/codealmanac.js",
|
|
26
|
-
"almanac": "./dist/codealmanac.js"
|
|
26
|
+
"almanac": "./dist/codealmanac.js",
|
|
27
|
+
"alm": "./dist/codealmanac.js"
|
|
27
28
|
},
|
|
28
29
|
"files": [
|
|
29
30
|
"dist",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/indexer/schema.ts","../src/indexer/index.ts","../src/slug.ts","../src/topics/yaml.ts","../src/indexer/frontmatter.ts","../src/indexer/freshness.ts","../src/indexer/paths.ts","../src/indexer/topics-yaml.ts","../src/indexer/wikilinks.ts","../src/registry/index.ts","../src/commands/health.ts","../src/indexer/duration.ts","../src/indexer/resolve-wiki.ts","../src/topics/dag.ts"],"sourcesContent":["import Database from \"better-sqlite3\";\n\n/**\n * Schema DDL, applied on every open. All statements are `CREATE ... IF NOT\n * EXISTS` so this is idempotent — handy when the file already exists but\n * was written by an older version, and tolerable because the schema is\n * append-only (new tables don't collide).\n *\n * Departures from the raw spec, explained:\n * - `page_topics.topic_slug` has no FK to `topics(slug)`. Topics are\n * created lazily when a page declares them; a strict FK would force us\n * to upsert topic rows before the page rows, which doesn't buy us\n * anything in slice 2 and locks us out of slice 3's \"no explicit topic\n * registration needed\" behavior.\n * - `wikilinks.target_slug` / `cross_wiki_links.target_slug` also have\n * no FK — these can be intentionally broken (unwritten target page),\n * and `almanac health` will surface them in slice 3.\n *\n * `file_refs` carries TWO forms of each path:\n * - `path` — normalized + lowercased, used for GLOB/equality\n * queries (`--mentions`). Stable across casing\n * choices on macOS/Windows.\n * - `original_path` — as-written (normalized slashes, no `./`, trailing\n * `/` for dirs), preserving the author's casing.\n * Used for filesystem stats (dead-refs on\n * case-sensitive filesystems like Linux) and for\n * user-facing display (`almanac info`).\n *\n * See also: `SCHEMA_VERSION` below and the migration logic in `openIndex`.\n */\nconst SCHEMA_DDL = `\nCREATE TABLE IF NOT EXISTS pages (\n slug TEXT PRIMARY KEY,\n title TEXT,\n file_path TEXT NOT NULL,\n content_hash TEXT NOT NULL,\n updated_at INTEGER NOT NULL,\n archived_at INTEGER,\n superseded_by TEXT\n);\n\nCREATE TABLE IF NOT EXISTS topics (\n slug TEXT PRIMARY KEY,\n title TEXT,\n description TEXT\n);\n\nCREATE TABLE IF NOT EXISTS page_topics (\n page_slug TEXT NOT NULL REFERENCES pages(slug) ON DELETE CASCADE,\n topic_slug TEXT NOT NULL,\n PRIMARY KEY (page_slug, topic_slug)\n);\n\nCREATE TABLE IF NOT EXISTS topic_parents (\n child_slug TEXT NOT NULL,\n parent_slug TEXT NOT NULL,\n PRIMARY KEY (child_slug, parent_slug),\n CHECK (child_slug != parent_slug)\n);\n\nCREATE TABLE IF NOT EXISTS file_refs (\n page_slug TEXT NOT NULL REFERENCES pages(slug) ON DELETE CASCADE,\n path TEXT NOT NULL,\n original_path TEXT NOT NULL,\n is_dir INTEGER NOT NULL,\n PRIMARY KEY (page_slug, path)\n);\nCREATE INDEX IF NOT EXISTS idx_file_refs_path ON file_refs(path);\n\nCREATE TABLE IF NOT EXISTS wikilinks (\n source_slug TEXT NOT NULL REFERENCES pages(slug) ON DELETE CASCADE,\n target_slug TEXT NOT NULL,\n PRIMARY KEY (source_slug, target_slug)\n);\n\nCREATE TABLE IF NOT EXISTS cross_wiki_links (\n source_slug TEXT NOT NULL REFERENCES pages(slug) ON DELETE CASCADE,\n target_wiki TEXT NOT NULL,\n target_slug TEXT NOT NULL,\n PRIMARY KEY (source_slug, target_wiki, target_slug)\n);\n\n-- NOTE: virtual FTS5 table — ON DELETE CASCADE from pages does NOT apply.\n-- The indexer must explicitly DELETE FROM fts_pages whenever it removes\n-- or replaces a page row, or we leak orphaned FTS rows.\nCREATE VIRTUAL TABLE IF NOT EXISTS fts_pages USING fts5(slug, title, content);\n`;\n\n/**\n * Bump this whenever the schema changes in a backwards-incompatible way.\n * On open we compare the stored `user_version` against this constant; if\n * it's lower, we drop the affected tables so the next `runIndexer` can\n * rebuild them. Full reindex is cheap (everything lives on disk as\n * markdown), so \"drop + recreate\" is simpler than ALTER TABLE migrations.\n *\n * Version history:\n * 1 — initial slice-2 schema\n * 2 — slice-3-review: added `file_refs.original_path`\n */\nconst SCHEMA_VERSION = 2;\n\n/**\n * Open `index.db` and apply the schema. Foreign keys are off by default in\n * SQLite; we turn them on per-connection so the ON DELETE CASCADE on\n * `pages` actually fires when we delete stale rows during incremental\n * reindex.\n *\n * We don't wrap this open in a transaction — `CREATE ... IF NOT EXISTS` is\n * safe to run repeatedly and the FTS5 virtual-table creation is already\n * atomic.\n *\n * Migration: if the DB was created by an older schema (`user_version` <\n * `SCHEMA_VERSION`), we drop the tables whose shape changed and let the\n * CREATE IF NOT EXISTS below rebuild them. The next `runIndexer` repopulates\n * from the filesystem — cheap and avoids the ALTER TABLE dance.\n */\nexport function openIndex(dbPath: string): Database.Database {\n const db = new Database(dbPath);\n // WAL journal mode is persistent — once set, it's recorded in the DB\n // header and survives close/open cycles. Check first and only switch if\n // we're not already there; this avoids a redundant pragma write on every\n // query command.\n const mode = db.pragma(\"journal_mode\", { simple: true });\n if (typeof mode !== \"string\" || mode.toLowerCase() !== \"wal\") {\n db.pragma(\"journal_mode = WAL\");\n }\n db.pragma(\"foreign_keys = ON\");\n\n const rawVersion = db.pragma(\"user_version\", { simple: true });\n const currentVersion = typeof rawVersion === \"number\" ? rawVersion : 0;\n if (currentVersion < SCHEMA_VERSION) {\n // Drop tables whose shape changed. `file_refs` got `original_path`\n // as of v2; easiest to drop it entirely so CREATE IF NOT EXISTS\n // runs with the new definition. Pages/topics/links are untouched.\n db.exec(\"DROP TABLE IF EXISTS file_refs\");\n // The indexer's fast-path skips pages whose content_hash matches,\n // which means a migration-dropped `file_refs` wouldn't get\n // repopulated until a page changed. Clear the hash column so the\n // next reindex treats every page as changed and rebuilds its\n // file_refs/wikilinks/cross-wiki rows. Table may not exist yet on\n // a brand-new DB, so swallow errors.\n try {\n db.exec(\"UPDATE pages SET content_hash = ''\");\n } catch {\n // pages table didn't exist yet; the upcoming CREATE IF NOT EXISTS\n // takes care of a fresh install.\n }\n db.pragma(`user_version = ${SCHEMA_VERSION}`);\n }\n\n db.exec(SCHEMA_DDL);\n return db;\n}\n","import { createHash } from \"node:crypto\";\nimport { existsSync, statSync } from \"node:fs\";\nimport { readFile, utimes } from \"node:fs/promises\";\nimport { basename, join } from \"node:path\";\n\nimport fg from \"fast-glob\";\nimport type Database from \"better-sqlite3\";\n\nimport { toKebabCase } from \"../slug.js\";\nimport { titleCase } from \"../topics/yaml.js\";\nimport { firstH1, parseFrontmatter } from \"./frontmatter.js\";\nimport {\n PAGES_GLOB,\n pagesNewerThan,\n topicsYamlNewerThan,\n} from \"./freshness.js\";\nimport {\n normalizePath,\n normalizePathPreservingCase,\n looksLikeDir,\n} from \"./paths.js\";\nimport { openIndex } from \"./schema.js\";\nimport { applyTopicsYaml, TOPICS_YAML_FILENAME } from \"./topics-yaml.js\";\nimport { extractWikilinks } from \"./wikilinks.js\";\n\nexport interface IndexContext {\n /** Absolute path to the repo root (the dir containing `.almanac/`). */\n repoRoot: string;\n}\n\nexport interface IndexResult {\n /** Pages parsed or re-parsed during this run. Zero when the DB was already up to date. */\n changed: number;\n /** Pages present in the DB before this run but missing from disk. */\n removed: number;\n /**\n * Pages on disk at the end of this run — i.e. files that made it all the\n * way through to the index. Skipped files (slug collisions, unreadable,\n * un-sluggable filenames) are NOT counted here. Use `filesSeen` for the\n * raw count of `.md` files encountered on disk.\n *\n * Alias retained for backwards-compat with existing tests/consumers; new\n * code should prefer `pagesIndexed` for clarity.\n */\n total: number;\n /** Pages that made it into the index. Same number as `total`. */\n pagesIndexed: number;\n /**\n * Count of `.md` files found under `pages/` before any filtering. Always\n * `>= pagesIndexed`; the difference is `filesSkipped`.\n */\n filesSeen: number;\n /**\n * Files dropped before making it into the index — slug collisions,\n * un-sluggable filenames, or filesystem races (deleted/unreadable mid-run).\n * Covered by stderr warnings when non-zero.\n */\n filesSkipped: number;\n}\n\n/**\n * The \"front door\" for query commands. Runs the indexer only if the DB is\n * missing or at least one page is newer than it. Meant to be cheap — the\n * common case is \"nothing changed, mtime check returns fast, we're done\".\n *\n * The spec is explicit: \"Reindex is implicit and invisible. If the user\n * didn't didn't explicitly run `reindex`, they shouldn't see reindex\n * output. Silent by default.\" So this function never writes to stdout;\n * warnings (slug collisions, bad frontmatter) still go to stderr.\n */\nexport async function ensureFreshIndex(ctx: IndexContext): Promise<IndexResult> {\n const almanacDir = join(ctx.repoRoot, \".almanac\");\n const dbPath = join(almanacDir, \"index.db\");\n const pagesDir = join(almanacDir, \"pages\");\n\n if (!existsSync(pagesDir)) {\n // No pages dir = nothing to index. Open/create the DB so downstream\n // queries can run against an empty schema rather than crashing on a\n // missing file.\n const db = openIndex(dbPath);\n db.close();\n return emptyResult();\n }\n\n if (\n !existsSync(dbPath) ||\n pagesNewerThan(pagesDir, dbPath) ||\n topicsYamlNewerThan(almanacDir, dbPath)\n ) {\n return runIndexer(ctx);\n }\n return emptyResult();\n}\n\nfunction emptyResult(): IndexResult {\n return {\n changed: 0,\n removed: 0,\n total: 0,\n pagesIndexed: 0,\n filesSeen: 0,\n filesSkipped: 0,\n };\n}\n\n/**\n * Force a full reindex. Identical to `ensureFreshIndex` except it runs\n * the indexer unconditionally. Exposed for `almanac reindex`.\n */\nexport async function runIndexer(ctx: IndexContext): Promise<IndexResult> {\n const almanacDir = join(ctx.repoRoot, \".almanac\");\n const dbPath = join(almanacDir, \"index.db\");\n const pagesDir = join(almanacDir, \"pages\");\n\n const db = openIndex(dbPath);\n let result: IndexResult;\n try {\n result = await indexPagesInto(db, pagesDir);\n // After pages are indexed, reconcile the topics table against\n // `.almanac/topics.yaml` (if present). `indexPagesInto` has already\n // lazily inserted rows for every topic slug mentioned in page\n // frontmatter with a title-cased title; `applyTopicsYaml` now\n // promotes the declared title/description and rewrites parent edges\n // for those topics that live in the file.\n await applyTopicsYaml(db, join(almanacDir, TOPICS_YAML_FILENAME));\n } finally {\n db.close();\n }\n\n // Bump the DB mtime to \"now\" after a successful reindex (even a no-op\n // one). Otherwise, a page file with a future mtime (clock skew,\n // `git checkout` preserving source mtimes) would trigger `ensureFreshIndex`\n // on every query: the freshness check sees `page.mtime > db.mtime`,\n // reindex runs, finds no content-hash changes, and the DB mtime stays\n // stale — locking us into a reindex-on-every-query loop. Touching the\n // DB mtime makes the comparison monotonic.\n try {\n const now = new Date();\n await utimes(dbPath, now, now);\n } catch {\n // Touching mtime is a freshness optimization; failures here are\n // non-fatal and the reindex result is still correct.\n }\n return result;\n}\n\ninterface ExistingRow {\n slug: string;\n content_hash: string;\n file_path: string;\n}\n\nasync function indexPagesInto(\n db: Database.Database,\n pagesDir: string,\n): Promise<IndexResult> {\n const files = await fg(PAGES_GLOB, {\n cwd: pagesDir,\n absolute: false,\n onlyFiles: true,\n caseSensitiveMatch: true,\n });\n\n // Load the current state of the index into memory so we can diff against\n // what's on disk. This is cheap even at 10k pages (one INTEGER + two\n // short strings per row).\n const existingRows = db\n .prepare<[], ExistingRow>(\"SELECT slug, content_hash, file_path FROM pages\")\n .all();\n const existingBySlug = new Map<string, ExistingRow>();\n for (const row of existingRows) existingBySlug.set(row.slug, row);\n\n // First pass: decide what to do with each file on disk. We record the\n // intent here so the transaction below can run synchronously — mixing\n // async file reads into a better-sqlite3 transaction doesn't work\n // (transactions are sync).\n const planned: Array<{\n slug: string;\n title: string;\n filePath: string;\n fullPath: string;\n contentHash: string;\n updatedAt: number;\n archivedAt: number | null;\n supersededBy: string | null;\n topics: string[];\n frontmatterFiles: string[];\n wikilinks: ReturnType<typeof extractWikilinks>;\n content: string;\n }> = [];\n const seenSlugs = new Set<string>();\n let filesSkipped = 0;\n\n for (const rel of files) {\n const fullPath = join(pagesDir, rel);\n const base = basename(rel, \".md\");\n const slug = toKebabCase(base);\n if (slug.length === 0) {\n process.stderr.write(\n `almanac: skipping \"${rel}\" — filename has no slug-able characters\\n`,\n );\n filesSkipped++;\n continue;\n }\n if (slug !== base) {\n // Filename isn't already canonical kebab-case. Warn, but still\n // index under the canonical slug. `almanac health` (slice 3) will\n // surface these as a proper report.\n process.stderr.write(\n `almanac: warning — \"${rel}\" is not canonical; indexed as slug \"${slug}\"\\n`,\n );\n }\n if (seenSlugs.has(slug)) {\n // Two files slugify to the same slug. Keep the first, skip the\n // rest — health will flag this properly in slice 3.\n process.stderr.write(\n `almanac: warning — slug \"${slug}\" collides with an earlier file; skipping \"${rel}\"\\n`,\n );\n filesSkipped++;\n continue;\n }\n\n // `fast-glob` gave us the list in one shot, but by the time we stat\n // and read each file it can have been deleted, renamed, or swapped\n // (editors that save via rename-swap expose this briefly). A single\n // such race shouldn't tank the whole reindex — matches the malformed-\n // YAML behavior (\"one bad file doesn't stop the others\"). We narrow\n // to ENOENT/EACCES so genuine I/O failures (EIO, EMFILE, etc.) still\n // surface.\n let st: ReturnType<typeof statSync>;\n let raw: string;\n try {\n st = statSync(fullPath);\n raw = await readFile(fullPath, \"utf8\");\n } catch (err: unknown) {\n if (\n err instanceof Error &&\n \"code\" in err &&\n (err.code === \"ENOENT\" || err.code === \"EACCES\")\n ) {\n process.stderr.write(\n `almanac: skipping \"${rel}\" — ${err.message}\\n`,\n );\n filesSkipped++;\n continue;\n }\n throw err;\n }\n\n seenSlugs.add(slug);\n const updatedAt = Math.floor(st.mtimeMs / 1000);\n\n // Content-hash skip: if the hash matches what's in the DB and the\n // file path hasn't moved, we can leave this page's rows alone. This\n // is the fast-path for \"user ran a query; one page was touched\".\n const contentHash = hashContent(raw);\n const existing = existingBySlug.get(slug);\n if (\n existing !== undefined &&\n existing.content_hash === contentHash &&\n existing.file_path === fullPath\n ) {\n continue;\n }\n\n const fm = parseFrontmatter(raw);\n const title = fm.title ?? firstH1(fm.body) ?? base;\n const links = extractWikilinks(fm.body);\n\n planned.push({\n slug,\n title,\n filePath: rel,\n fullPath,\n contentHash,\n updatedAt,\n archivedAt: fm.archived_at,\n supersededBy: fm.superseded_by,\n topics: fm.topics,\n frontmatterFiles: fm.files,\n wikilinks: links,\n content: fm.body,\n });\n }\n\n // Compute deletions: anything in the DB whose slug isn't on disk\n // anymore (or whose file slugifies to a different slug now).\n const toDelete: string[] = [];\n for (const slug of existingBySlug.keys()) {\n if (!seenSlugs.has(slug)) toDelete.push(slug);\n }\n\n const deleteByPage = db.prepare<[string]>(\"DELETE FROM pages WHERE slug = ?\");\n const deleteFtsByPage = db.prepare<[string]>(\n \"DELETE FROM fts_pages WHERE slug = ?\",\n );\n\n const replacePage = db.prepare<\n [string, string, string, string, number, number | null, string | null]\n >(\n `INSERT INTO pages (slug, title, file_path, content_hash, updated_at, archived_at, superseded_by)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(slug) DO UPDATE SET\n title = excluded.title,\n file_path = excluded.file_path,\n content_hash = excluded.content_hash,\n updated_at = excluded.updated_at,\n archived_at = excluded.archived_at,\n superseded_by = excluded.superseded_by`,\n );\n\n const deletePageTopics = db.prepare<[string]>(\n \"DELETE FROM page_topics WHERE page_slug = ?\",\n );\n const insertPageTopic = db.prepare<[string, string]>(\n \"INSERT OR IGNORE INTO page_topics (page_slug, topic_slug) VALUES (?, ?)\",\n );\n // Seed ad-hoc topics with a title-cased default. If the topic is\n // later declared in `.almanac/topics.yaml`, `applyTopicsYaml` will\n // promote the title/description to whatever the file says. We set the\n // title here (rather than leaving NULL) so `topics list` and\n // `health --topic` have a display name even before a user writes to\n // topics.yaml.\n const insertTopic = db.prepare<[string, string]>(\n \"INSERT OR IGNORE INTO topics (slug, title) VALUES (?, ?)\",\n );\n\n const deleteFileRefs = db.prepare<[string]>(\n \"DELETE FROM file_refs WHERE page_slug = ?\",\n );\n const insertFileRef = db.prepare<[string, string, string, number]>(\n \"INSERT OR IGNORE INTO file_refs (page_slug, path, original_path, is_dir) VALUES (?, ?, ?, ?)\",\n );\n\n const deleteWikilinks = db.prepare<[string]>(\n \"DELETE FROM wikilinks WHERE source_slug = ?\",\n );\n const insertWikilink = db.prepare<[string, string]>(\n \"INSERT OR IGNORE INTO wikilinks (source_slug, target_slug) VALUES (?, ?)\",\n );\n\n const deleteXwiki = db.prepare<[string]>(\n \"DELETE FROM cross_wiki_links WHERE source_slug = ?\",\n );\n const insertXwiki = db.prepare<[string, string, string]>(\n \"INSERT OR IGNORE INTO cross_wiki_links (source_slug, target_wiki, target_slug) VALUES (?, ?, ?)\",\n );\n\n const insertFts = db.prepare<[string, string, string]>(\n \"INSERT INTO fts_pages (slug, title, content) VALUES (?, ?, ?)\",\n );\n\n const apply = db.transaction(() => {\n for (const slug of toDelete) {\n // `fts_pages` is an FTS5 virtual table — FK cascades do NOT propagate\n // into it, so we must delete FTS rows explicitly before relying on\n // `DELETE FROM pages` to cascade-clean the four real tables\n // (page_topics, file_refs, wikilinks, cross_wiki_links). If this\n // explicit delete ever gets removed, orphaned FTS rows will show up\n // as phantom search hits pointing at non-existent slugs.\n deleteFtsByPage.run(slug);\n deleteByPage.run(slug); // CASCADE cleans page_topics, file_refs, wikilinks, cross_wiki_links\n }\n\n for (const p of planned) {\n // page_topics/file_refs/wikilinks/cross_wiki_links all cascade on\n // delete, so the cleanest \"replace\" story is: delete-then-insert\n // the per-page rows under the same transaction. Doing it this way\n // (rather than `ON CONFLICT DO UPDATE` per row) keeps the logic\n // uniform and makes \"remove a topic from frontmatter\" work.\n deletePageTopics.run(p.slug);\n deleteFileRefs.run(p.slug);\n deleteWikilinks.run(p.slug);\n deleteXwiki.run(p.slug);\n // Same virtual-table reason as the deletion branch above — FTS5\n // rows do not cascade, so clean them by hand before reinserting.\n deleteFtsByPage.run(p.slug);\n\n replacePage.run(\n p.slug,\n p.title,\n p.fullPath,\n p.contentHash,\n p.updatedAt,\n p.archivedAt,\n p.supersededBy,\n );\n\n for (const topic of p.topics) {\n const topicSlug = toKebabCase(topic);\n if (topicSlug.length === 0) continue;\n insertTopic.run(topicSlug, titleCase(topicSlug));\n insertPageTopic.run(p.slug, topicSlug);\n }\n\n // Frontmatter `files:` — normalize each entry, inferring directness\n // from its trailing slash. Authors who write `src/payments` (no\n // trailing slash) are asserting a file; this matches how `[[...]]`\n // classifies the same string. We store both the lowercased form\n // (for `--mentions` GLOB queries) and the casing-preserving form\n // (for dead-ref `existsSync` on case-sensitive filesystems).\n for (const raw of p.frontmatterFiles) {\n const isDir = looksLikeDir(raw);\n const path = normalizePath(raw, isDir);\n const originalPath = normalizePathPreservingCase(raw, isDir);\n if (path.length === 0) continue;\n insertFileRef.run(p.slug, path, originalPath, isDir ? 1 : 0);\n }\n\n // Inline `[[...]]` extracted from body.\n for (const ref of p.wikilinks) {\n switch (ref.kind) {\n case \"page\":\n insertWikilink.run(p.slug, ref.target);\n break;\n case \"file\":\n insertFileRef.run(p.slug, ref.path, ref.originalPath, 0);\n break;\n case \"folder\":\n insertFileRef.run(p.slug, ref.path, ref.originalPath, 1);\n break;\n case \"xwiki\":\n insertXwiki.run(p.slug, ref.wiki, ref.target);\n break;\n }\n }\n\n insertFts.run(p.slug, p.title, p.content);\n }\n });\n apply();\n\n const pagesIndexed = seenSlugs.size;\n return {\n changed: planned.length,\n removed: toDelete.length,\n total: pagesIndexed,\n pagesIndexed,\n filesSeen: files.length,\n filesSkipped,\n };\n}\n\nfunction hashContent(raw: string): string {\n return createHash(\"sha256\").update(raw).digest(\"hex\");\n}\n","/**\n * Canonical kebab-case slugifier used across the codebase.\n *\n * One function, three callers:\n * - `registry/index.ts` — wiki name slugs (both auto-derived and\n * user-supplied via `--name`)\n * - `indexer/index.ts` — page filename → slug and topic → slug\n * - `indexer/wikilinks.ts` — wikilink target → slug for resolution\n *\n * All three want the same behavior: lowercased, non-alphanumeric runs\n * collapse to a single hyphen, leading/trailing hyphens trimmed. Keeping\n * this in one place avoids a class of bug where an unusual input (e.g.\n * `Checkout_Flow`) produces different slugs depending on which layer\n * slugified it.\n *\n * Rules:\n * - Lowercase\n * - Non-alphanumeric runs collapse to a single hyphen\n * - Leading/trailing hyphens trimmed\n */\nexport function toKebabCase(input: string): string {\n return input\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n}\n","import { existsSync } from \"node:fs\";\nimport { mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\n\nimport yaml from \"js-yaml\";\n\nimport { toKebabCase } from \"../slug.js\";\n\n/**\n * One entry in `.almanac/topics.yaml` — the source of truth for topic\n * metadata (title, description, DAG parents). Pages are still the source\n * of truth for which pages belong to which topics; this file only holds\n * the topic-level attributes.\n *\n * `slug` is the canonical kebab-case key used everywhere downstream\n * (SQLite `topics.slug`, page frontmatter `topics:` entries, wikilink\n * targets). `title` is the human-readable name the user typed at create\n * time. `description` is a free-form one-liner (or null when unset).\n * `parents` is the DAG edge list — kept as an array of slugs rather than\n * a nested structure so round-tripping stays boring and a user eyeballing\n * the file can see the full graph.\n */\nexport interface TopicEntry {\n slug: string;\n title: string;\n description: string | null;\n parents: string[];\n}\n\nexport interface TopicsFile {\n topics: TopicEntry[];\n}\n\n/**\n * Load `.almanac/topics.yaml` into a `TopicsFile`. A missing file is not\n * an error — it's the first-run state, which we treat as \"no topic\n * metadata, only whatever the pages declare in frontmatter\". Malformed\n * YAML IS an error; we surface it rather than silently clobbering the\n * user's committed source of truth.\n *\n * The return shape is always normalized — callers don't have to guard\n * for missing `topics` key, wrong types, or absent `parents` arrays.\n */\nexport async function loadTopicsFile(path: string): Promise<TopicsFile> {\n if (!existsSync(path)) {\n return { topics: [] };\n }\n let raw: string;\n try {\n raw = await readFile(path, \"utf8\");\n } catch (err: unknown) {\n if (isNodeError(err) && err.code === \"ENOENT\") {\n return { topics: [] };\n }\n throw err;\n }\n\n const trimmed = raw.trim();\n if (trimmed.length === 0) {\n return { topics: [] };\n }\n\n let parsed: unknown;\n try {\n parsed = yaml.load(raw);\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n throw new Error(`topics.yaml at ${path} is not valid YAML: ${message}`);\n }\n\n if (parsed === null || parsed === undefined) {\n return { topics: [] };\n }\n\n if (typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new Error(`topics.yaml at ${path} must be a mapping`);\n }\n\n const obj = parsed as Record<string, unknown>;\n const rawTopics = obj.topics;\n if (rawTopics === undefined || rawTopics === null) {\n return { topics: [] };\n }\n if (!Array.isArray(rawTopics)) {\n throw new Error(`topics.yaml at ${path} — \"topics\" must be a list`);\n }\n\n const topics: TopicEntry[] = [];\n for (const item of rawTopics) {\n if (typeof item !== \"object\" || item === null || Array.isArray(item)) {\n continue;\n }\n const entry = item as Record<string, unknown>;\n const slugRaw = entry.slug;\n if (typeof slugRaw !== \"string\" || slugRaw.trim().length === 0) continue;\n const slug = toKebabCase(slugRaw);\n if (slug.length === 0) continue;\n const title =\n typeof entry.title === \"string\" && entry.title.trim().length > 0\n ? entry.title.trim()\n : titleCase(slug);\n const description =\n typeof entry.description === \"string\" &&\n entry.description.trim().length > 0\n ? entry.description.trim()\n : null;\n const parents: string[] = [];\n if (Array.isArray(entry.parents)) {\n for (const p of entry.parents) {\n if (typeof p === \"string\" && p.trim().length > 0) {\n const ps = toKebabCase(p);\n if (ps.length > 0 && ps !== slug && !parents.includes(ps)) {\n parents.push(ps);\n }\n }\n }\n }\n topics.push({ slug, title, description, parents });\n }\n\n return { topics };\n}\n\n/**\n * Write a `TopicsFile` atomically — tmp file + rename, same pattern as\n * the registry. A half-written topics.yaml would corrupt the user's\n * committed source of truth, so we never write in place.\n *\n * Ordering: topics are sorted by slug for stable diffs. Parents within\n * each entry stay in the order the caller passed them (semantically an\n * ordered list — topics.yaml is the place a user can visibly reason\n * about \"primary parent first\", even though SQLite treats them as a\n * set).\n *\n * We emit a leading comment so first-time readers know the file is\n * edited by the CLI and what its role is.\n */\nexport async function writeTopicsFile(\n path: string,\n file: TopicsFile,\n): Promise<void> {\n const sorted = [...file.topics].sort((a, b) => a.slug.localeCompare(b.slug));\n const doc = {\n topics: sorted.map((t) => {\n // Emit all four keys in a stable order: slug, title, description,\n // parents. description is emitted as `null` in YAML when unset so\n // the schema stays consistent across entries (js-yaml renders the\n // literal word `null`, not the `~` shorthand).\n return {\n slug: t.slug,\n title: t.title,\n description: t.description,\n parents: t.parents,\n };\n }),\n };\n\n const header =\n `# .almanac/topics.yaml — source of truth for topic metadata.\\n` +\n `# Managed by \\`almanac topics\\` commands. User-added comments\\n` +\n `# between entries will be stripped on the next write (js-yaml\\n` +\n `# doesn't round-trip comments). Edit at your own risk — or use the\\n` +\n `# CLI (\\`almanac topics create|link|describe|rename|delete\\`)\\n` +\n `# which preserves the structure correctly.\\n`;\n const body = yaml.dump(doc, {\n lineWidth: 100,\n noRefs: true,\n sortKeys: false,\n });\n const content = `${header}${body}`;\n const tmpPath = `${path}.tmp`;\n // mkdir parent in case `.almanac/` vanished (shouldn't, but cheap insurance)\n const parent = dirname(path);\n if (!existsSync(parent)) {\n await mkdir(parent, { recursive: true });\n }\n await writeFile(tmpPath, content, \"utf8\");\n await rename(tmpPath, path);\n}\n\n/**\n * Look up a topic by slug. Returns `null` when the slug is absent —\n * callers distinguish \"declared in topics.yaml\" from \"ad-hoc (only\n * appears in page frontmatter)\" based on this.\n */\nexport function findTopic(file: TopicsFile, slug: string): TopicEntry | null {\n for (const t of file.topics) {\n if (t.slug === slug) return t;\n }\n return null;\n}\n\n/**\n * Ensure a topic entry exists. If missing, inserts a minimal entry with\n * title-cased title and null description. Returns the (possibly new)\n * entry. Used by `tag`, `topics create` (with `--parent auto-creating`),\n * and `topics link` (auto-creating child/parent on demand).\n */\nexport function ensureTopic(file: TopicsFile, slug: string): TopicEntry {\n const existing = findTopic(file, slug);\n if (existing !== null) return existing;\n const entry: TopicEntry = {\n slug,\n title: titleCase(slug),\n description: null,\n parents: [],\n };\n file.topics.push(entry);\n return entry;\n}\n\n/**\n * Convert a slug back to a human-ish title: `auth-flow` → `Auth Flow`.\n * Used as the fallback title when the caller didn't provide one\n * (auto-creation paths, ad-hoc slugs coming from page frontmatter).\n */\nexport function titleCase(slug: string): string {\n if (slug.length === 0) return slug;\n return slug\n .split(\"-\")\n .filter((s) => s.length > 0)\n .map((s) => `${s[0]?.toUpperCase() ?? \"\"}${s.slice(1)}`)\n .join(\" \");\n}\n\nfunction isNodeError(err: unknown): err is NodeJS.ErrnoException {\n return err instanceof Error && \"code\" in err;\n}\n","import yaml from \"js-yaml\";\n\nexport interface Frontmatter {\n title?: string;\n topics: string[];\n files: string[];\n archived_at: number | null;\n superseded_by: string | null;\n supersedes: string | null;\n /**\n * The body of the file with frontmatter removed, for FTS5 and H1 fallback.\n * Always populated even when the file has no frontmatter.\n */\n body: string;\n}\n\n/**\n * Pull YAML frontmatter off the top of a markdown file and coerce the\n * relevant fields. Unknown fields are tolerated silently — the wiki should\n * accept fields we don't understand yet without spewing warnings at the\n * user (future slices might consume them).\n *\n * Failure modes:\n * - No frontmatter at all → `{ topics: [], files: [], ..., body: raw }`.\n * This is legal; a heading + prose is a valid page.\n * - Malformed YAML → warning to stderr, treated as \"no frontmatter\". We\n * choose not to throw so a single bad file doesn't tank a reindex.\n *\n * Note on `archived_at`: authors write this as a YAML date (`2026-04-15`),\n * which `js-yaml` parses to a JS `Date`. We also tolerate ISO-8601 strings\n * and raw numbers. Everything else gets dropped (treated as \"not\n * archived\"). Storing epoch seconds keeps `--since`/`--stale`/`archived`\n * arithmetic trivial at query time.\n */\nexport function parseFrontmatter(raw: string): Frontmatter {\n const empty: Frontmatter = {\n topics: [],\n files: [],\n archived_at: null,\n superseded_by: null,\n supersedes: null,\n body: raw,\n };\n\n // Frontmatter fence MUST start on line 1 — a `---` partway through the\n // document is just a horizontal rule. Be strict about the opening delim\n // so we don't accidentally strip section headers.\n if (!raw.startsWith(\"---\")) {\n return empty;\n }\n\n // Tolerate either Unix or Windows line endings. We read the first line\n // explicitly to confirm it's only `---` (no trailing content).\n const match = raw.match(/^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?([\\s\\S]*)$/);\n if (match === null) {\n return empty;\n }\n\n const yamlBody = match[1] ?? \"\";\n const body = match[2] ?? \"\";\n\n let parsed: unknown;\n try {\n parsed = yaml.load(yamlBody);\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`almanac: malformed frontmatter (${message})\\n`);\n return empty;\n }\n\n if (parsed === null || parsed === undefined) {\n return { ...empty, body };\n }\n if (typeof parsed !== \"object\" || Array.isArray(parsed)) {\n // Someone wrote a YAML scalar or list as the document root — not a\n // mapping, so no fields for us to extract. Treat as empty but keep the\n // post-fence body so FTS5 still gets content.\n return { ...empty, body };\n }\n\n const obj = parsed as Record<string, unknown>;\n\n return {\n title: coerceString(obj.title),\n topics: coerceStringArray(obj.topics),\n files: coerceStringArray(obj.files),\n archived_at: coerceEpochSeconds(obj.archived_at),\n superseded_by: coerceString(obj.superseded_by) ?? null,\n supersedes: coerceString(obj.supersedes) ?? null,\n body,\n };\n}\n\n/**\n * H1 fallback for title when frontmatter has none.\n *\n * Only considers the first 40 lines of the body — any real wiki page has\n * its H1 near the top. NOTE: `String.prototype.split(sep, limit)` still\n * splits the whole string internally and then truncates; it's not an\n * early-bail iteration. For the multi-megabyte files we might see in\n * practice this is still cheap (one regex pass, no allocation per-line\n * beyond the 40 we keep), so we favor the clearer code over hand-rolled\n * line iteration.\n */\nexport function firstH1(body: string): string | undefined {\n const lines = body.split(/\\r?\\n/, 40);\n for (const line of lines) {\n const m = line.match(/^#\\s+(.+?)\\s*#*\\s*$/);\n if (m !== null) {\n return m[1];\n }\n }\n return undefined;\n}\n\nfunction coerceString(v: unknown): string | undefined {\n if (typeof v === \"string\" && v.trim().length > 0) return v.trim();\n return undefined;\n}\n\nfunction coerceStringArray(v: unknown): string[] {\n if (!Array.isArray(v)) return [];\n const out: string[] = [];\n for (const item of v) {\n if (typeof item === \"string\" && item.trim().length > 0) {\n out.push(item.trim());\n }\n }\n return out;\n}\n\n/**\n * Coerce a frontmatter `archived_at` value (YAML Date, ISO string, or raw\n * epoch number) into epoch seconds. Returns `null` for anything we can't\n * make sense of — pages with an unrecognizable `archived_at` are treated as\n * active rather than silently marked archived, which is the safer default.\n */\nfunction coerceEpochSeconds(v: unknown): number | null {\n if (v instanceof Date) {\n return Math.floor(v.getTime() / 1000);\n }\n if (typeof v === \"number\" && Number.isFinite(v)) {\n return Math.floor(v);\n }\n if (typeof v === \"string\" && v.trim().length > 0) {\n const t = Date.parse(v.trim());\n if (!Number.isNaN(t)) {\n return Math.floor(t / 1000);\n }\n }\n return null;\n}\n","import { existsSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nimport fg from \"fast-glob\";\n\n// Glob is relative to `.almanac/pages/`, so this is every markdown page\n// beneath pages without repeating the `pages/` prefix.\nexport const PAGES_GLOB = \"**/*.md\";\n\n/**\n * Return true if any page file has an mtime strictly greater than the\n * index DB's mtime. This is intentionally cheap: `fast-glob` with\n * `stats: true` gives us mtimes without a second `stat` round-trip, and\n * the synchronous walk keeps the decision path simple for CLI entrypoints.\n */\nexport function pagesNewerThan(pagesDir: string, dbPath: string): boolean {\n let dbMtime: number;\n try {\n dbMtime = statSync(dbPath).mtimeMs;\n } catch {\n return true;\n }\n\n const entries = fg.sync(PAGES_GLOB, {\n cwd: pagesDir,\n absolute: true,\n onlyFiles: true,\n stats: true,\n }) as Array<{ path: string; stats?: { mtimeMs: number } }>;\n\n for (const entry of entries) {\n const mtime = entry.stats?.mtimeMs;\n if (mtime !== undefined && mtime > dbMtime) return true;\n }\n return false;\n}\n\n/**\n * Return true if `topics.yaml` has an mtime strictly greater than the\n * index DB's mtime. Missing `topics.yaml` is legal: it means \"no topic\n * metadata yet\", not \"the index is stale\".\n */\nexport function topicsYamlNewerThan(\n almanacDir: string,\n dbPath: string,\n): boolean {\n const path = join(almanacDir, \"topics.yaml\");\n if (!existsSync(path)) return false;\n let dbMtime: number;\n try {\n dbMtime = statSync(dbPath).mtimeMs;\n } catch {\n return true;\n }\n try {\n const st = statSync(path);\n return st.mtimeMs > dbMtime;\n } catch {\n return false;\n }\n}\n","/**\n * Path normalization for the file/folder references stored in `file_refs`\n * and for the query input passed to `--mentions`.\n *\n * The same function runs over both sides so a value written at index time\n * and a value looked up at query time compare byte-for-byte. If this ever\n * drifts between writers and readers, `--mentions` starts silently missing\n * matches — apply one canonicalization, not two.\n *\n * Rules (from the spec, Correctness):\n * - Lowercase (macOS filesystems are case-insensitive, so the wiki treats\n * `Src/Checkout/` and `src/checkout/` as the same path)\n * - Forward slashes only (never backslashes from Windows-authored content)\n * - No leading `./`\n * - Collapse redundant slashes (`src//checkout/` → `src/checkout/`)\n * - Trailing `/` iff the caller says it's a directory\n *\n * The `isDir` flag is a signal carried alongside the path — we don't infer\n * it from the raw string here, because frontmatter `files:` entries and the\n * inline `[[...]]` classifier both decide directness themselves and pass\n * the answer in. Having one place decide and one place normalize keeps the\n * directory inference rule testable in isolation.\n */\nexport function normalizePath(raw: string, isDir: boolean): string {\n const normalized = normalizeShape(raw, isDir);\n return normalized.toLowerCase();\n}\n\n/**\n * Normalize shape without lowercasing — preserves the author's casing.\n * Used to store `original_path` in `file_refs` so dead-ref checks on\n * case-sensitive filesystems (Linux, `git` checkouts with core.ignorecase\n * false) stat the actual path on disk rather than a lowercased alias.\n *\n * Everything else about the result is identical to `normalizePath`:\n * forward slashes, no `./`, no duplicate slashes, trailing `/` iff\n * `isDir`. The ONLY difference is the final `.toLowerCase()` is skipped.\n */\nexport function normalizePathPreservingCase(raw: string, isDir: boolean): string {\n return normalizeShape(raw, isDir);\n}\n\nfunction normalizeShape(raw: string, isDir: boolean): string {\n let s = raw.trim();\n\n // Windows-style backslashes → forward slashes. We never want to store\n // backslashes; a path authored on Windows and checked in should match\n // the same path authored on macOS.\n s = s.replace(/\\\\+/g, \"/\");\n\n // Drop a leading `./` — it's syntactic noise and authors inconsistently\n // include it. `./src/checkout/` and `src/checkout/` must hash equal.\n while (s.startsWith(\"./\")) s = s.slice(2);\n\n // Collapse any run of slashes to a single slash. This also normalizes\n // `src//checkout/` and accidental doubled slashes from string concat.\n s = s.replace(/\\/+/g, \"/\");\n\n // Strip any trailing slashes before re-applying the directory marker —\n // this way we don't care if the caller fed us `src/checkout` or\n // `src/checkout/` as a directory; we impose our own rule.\n s = s.replace(/\\/+$/, \"\");\n\n if (isDir) {\n // Directories ALWAYS end with a trailing slash. This is what lets the\n // GLOB queries distinguish `src/checkout/` (the directory) from\n // `src/checkout` (a file with no extension) without ambiguity.\n return `${s}/`;\n }\n return s;\n}\n\n/**\n * Infer `isDir` from the raw string the author wrote. Only used at the\n * point of parsing frontmatter `files:` entries and inline `[[...]]`\n * references — everywhere else, `isDir` is already known from context.\n *\n * Rule: trailing `/` (after backslash normalization) means directory.\n */\nexport function looksLikeDir(raw: string): boolean {\n const s = raw.trim().replace(/\\\\+/g, \"/\");\n return s.endsWith(\"/\");\n}\n","import { existsSync } from \"node:fs\";\n\nimport type Database from \"better-sqlite3\";\n\nimport { loadTopicsFile } from \"../topics/yaml.js\";\n\nexport const TOPICS_YAML_FILENAME = \"topics.yaml\";\n\n/**\n * Apply the contents of `.almanac/topics.yaml` to SQLite.\n *\n * Called at the tail of every reindex. For each entry in the file we\n * upsert a row into `topics` and rewrite that topic's parent edges.\n *\n * Important invariants:\n * - Missing `topics.yaml` is a no-op. Absence is legal and means \"no\n * topic metadata yet\".\n * - Topics mentioned only in page frontmatter are legal ad-hoc topics.\n * Do not delete them just because they are absent from `topics.yaml`.\n * - Stale topic rows are pruned only after upserting declared topics and\n * collecting current `page_topics`, so `health` does not flag topics\n * that were removed from every page after a rename/delete.\n */\nexport async function applyTopicsYaml(\n db: Database.Database,\n topicsYamlPath: string,\n): Promise<void> {\n if (!existsSync(topicsYamlPath)) return;\n let file;\n try {\n file = await loadTopicsFile(topicsYamlPath);\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`almanac: ${message}\\n`);\n return;\n }\n\n const upsertTopic = db.prepare<[string, string, string | null]>(\n `INSERT INTO topics (slug, title, description) VALUES (?, ?, ?)\n ON CONFLICT(slug) DO UPDATE SET\n title = excluded.title,\n description = excluded.description`,\n );\n const clearParents = db.prepare<[string]>(\n \"DELETE FROM topic_parents WHERE child_slug = ?\",\n );\n const insertParent = db.prepare<[string, string]>(\n \"INSERT OR IGNORE INTO topic_parents (child_slug, parent_slug) VALUES (?, ?)\",\n );\n\n // Declared means either explicitly present in topics.yaml or currently\n // referenced by page frontmatter. Anything outside this set is stale.\n const declared = new Set<string>();\n for (const t of file.topics) declared.add(t.slug);\n const adHoc = db\n .prepare<[], { topic_slug: string }>(\n \"SELECT DISTINCT topic_slug FROM page_topics\",\n )\n .all();\n for (const r of adHoc) declared.add(r.topic_slug);\n\n const apply = db.transaction(() => {\n for (const t of file.topics) {\n upsertTopic.run(t.slug, t.title, t.description);\n clearParents.run(t.slug);\n for (const parent of t.parents) {\n if (parent === t.slug) continue;\n insertParent.run(t.slug, parent);\n }\n }\n\n // Prune stale topic rows + any edges attached to them last, after\n // the upserts above have promoted declared slugs.\n const existing = db\n .prepare<[], { slug: string }>(\"SELECT slug FROM topics\")\n .all();\n const deleteTopic = db.prepare<[string]>(\"DELETE FROM topics WHERE slug = ?\");\n const deleteEdgesByChild = db.prepare<[string]>(\n \"DELETE FROM topic_parents WHERE child_slug = ?\",\n );\n const deleteEdgesByParent = db.prepare<[string]>(\n \"DELETE FROM topic_parents WHERE parent_slug = ?\",\n );\n for (const r of existing) {\n if (declared.has(r.slug)) continue;\n deleteEdgesByChild.run(r.slug);\n deleteEdgesByParent.run(r.slug);\n deleteTopic.run(r.slug);\n }\n });\n apply();\n}\n","import { toKebabCase } from \"../slug.js\";\nimport {\n looksLikeDir,\n normalizePath,\n normalizePathPreservingCase,\n} from \"./paths.js\";\n\n/**\n * One parsed `[[...]]` reference from a page body. Classification is\n * deterministic and content-based — see `classifyWikilink` below.\n *\n * Callers dispatch on `kind`:\n * - `page` → row in `wikilinks`\n * - `file` → row in `file_refs` with `is_dir = 0`\n * - `folder` → row in `file_refs` with `is_dir = 1`\n * - `xwiki` → row in `cross_wiki_links`\n *\n * File/folder refs carry TWO forms of the path:\n * - `path` — lowercased (for `--mentions` lookups)\n * - `originalPath` — as-written (for filesystem stats on case-sensitive\n * systems and for user-facing display)\n */\nexport type WikilinkRef =\n | { kind: \"page\"; target: string }\n | { kind: \"file\"; path: string; originalPath: string }\n | { kind: \"folder\"; path: string; originalPath: string }\n | { kind: \"xwiki\"; wiki: string; target: string };\n\n/**\n * Rules from the spec (\"Classification rules\"), applied in order:\n *\n * 1. Contains `:` before any `/` → cross-wiki reference (`wiki:slug`)\n * 2. Contains `/` → file or folder reference\n * - Trailing `/` = folder\n * - Otherwise = file\n * 3. Otherwise → page slug wikilink\n *\n * Edge cases the test suite pins down:\n * - `[[a:b/c]]` → xwiki (colon is before the slash, rule 1 wins)\n * - `[[src/a:b]]` → file (slash is before the colon, rule 2 wins)\n * - `[[./x]]` → the leading `./` is stripped by `normalizePath`,\n * so this lands in `file_refs` as `x`. A bare `./x`\n * with no inner slash would classify as a file.\n * - `[[foo|display]]`→ Obsidian-style display text is stripped; we key\n * on the target only. A future slice could surface\n * display text in `almanac info`.\n */\nexport function classifyWikilink(raw: string): WikilinkRef | null {\n // Strip Obsidian-style `|display` suffix — we don't index display text\n // in slice 2, but we want the classifier to see the real target.\n const pipe = raw.indexOf(\"|\");\n let body = pipe === -1 ? raw : raw.slice(0, pipe);\n body = body.trim();\n if (body.length === 0) return null;\n\n const firstColon = body.indexOf(\":\");\n const firstSlash = body.indexOf(\"/\");\n\n // Rule 1: cross-wiki, `wiki:slug`. Only if the colon comes before any\n // slash — otherwise `src/urls.ts:42` (hypothetical) would wrongly\n // classify as xwiki.\n if (firstColon !== -1 && (firstSlash === -1 || firstColon < firstSlash)) {\n const wiki = body.slice(0, firstColon).trim();\n const target = body.slice(firstColon + 1).trim();\n if (wiki.length === 0 || target.length === 0) return null;\n return { kind: \"xwiki\", wiki, target };\n }\n\n // Rule 2: file or folder. The `/` may be anywhere including trailing.\n if (firstSlash !== -1) {\n const isDir = looksLikeDir(body);\n const path = normalizePath(body, isDir);\n const originalPath = normalizePathPreservingCase(body, isDir);\n if (path.length === 0) return null;\n return isDir\n ? { kind: \"folder\", path, originalPath }\n : { kind: \"file\", path, originalPath };\n }\n\n // Rule 3: page slug wikilink. Authors might write `Checkout Flow` or\n // `Checkout_Flow` by accident — slugify defensively so backlinks still\n // resolve in those cases.\n const target = toKebabCase(body);\n if (target.length === 0) return null;\n return { kind: \"page\", target };\n}\n\n/**\n * Walk a markdown body and pull every `[[...]]` reference. We scan the\n * whole body rather than try to skip code blocks — the spec is explicit:\n * \"Prose outside `[[...]]` is just prose. No backtick-path heuristics, no\n * false positives from code blocks or log output.\" A `[[foo]]` inside a\n * fenced code block is still a wikilink. Authors who genuinely need a\n * literal `[[x]]` in code can escape one of the brackets.\n */\nexport function extractWikilinks(body: string): WikilinkRef[] {\n const out: WikilinkRef[] = [];\n const re = /\\[\\[([^\\]\\n]+)\\]\\]/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(body)) !== null) {\n const ref = classifyWikilink(m[1] ?? \"\");\n if (ref !== null) out.push(ref);\n }\n return out;\n}\n","import { mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\n\nimport { getGlobalAlmanacDir, getRegistryPath } from \"../paths.js\";\nimport { toKebabCase } from \"../slug.js\";\n\n// Re-export so existing import sites (`from \"../registry/index.js\"`) keep\n// working without a mechanical fan-out. The canonical home is `../slug.js`.\nexport { toKebabCase };\n\n/**\n * One entry in `~/.almanac/registry.json`.\n *\n * `name` is the canonical kebab-case slug the user types. `path` is the\n * absolute repo root (the directory that contains `.almanac/`). We store\n * absolute paths so cross-wiki resolution works regardless of the caller's\n * cwd.\n */\nexport interface RegistryEntry {\n name: string;\n description: string;\n path: string;\n registered_at: string;\n}\n\n/**\n * Read the registry file into memory.\n *\n * A missing file is not an error — it's the first-run state, which we\n * treat as an empty registry. A malformed file IS an error; we surface it\n * rather than silently clobbering the user's data.\n */\nexport async function readRegistry(): Promise<RegistryEntry[]> {\n const path = getRegistryPath();\n let raw: string;\n try {\n raw = await readFile(path, \"utf8\");\n } catch (err: unknown) {\n if (isNodeError(err) && err.code === \"ENOENT\") {\n return [];\n }\n throw err;\n }\n\n const trimmed = raw.trim();\n if (trimmed.length === 0) {\n return [];\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed);\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n throw new Error(`registry at ${path} is not valid JSON: ${message}`);\n }\n\n if (!Array.isArray(parsed)) {\n throw new Error(`registry at ${path} must be a JSON array`);\n }\n\n // Validate every entry. We do NOT silently coerce missing `name` or\n // `path` — an entry with `name: \"\"` would be unremovable via `--drop`\n // and an empty `path` would match any `findEntry({ path: \"\" })` call.\n // If someone hand-edited the registry into a bad state, surfacing the\n // error is strictly better than limping along with corrupt data.\n return parsed.map((item, idx) => {\n if (typeof item !== \"object\" || item === null) {\n throw new Error(`registry entry ${idx} is not an object`);\n }\n const e = item as Record<string, unknown>;\n const name = typeof e.name === \"string\" ? e.name : \"\";\n const path = typeof e.path === \"string\" ? e.path : \"\";\n if (name.length === 0) {\n throw new Error(`registry entry ${idx} is missing a non-empty \"name\"`);\n }\n if (path.length === 0) {\n throw new Error(`registry entry ${idx} is missing a non-empty \"path\"`);\n }\n return {\n name,\n description: typeof e.description === \"string\" ? e.description : \"\",\n path,\n registered_at:\n typeof e.registered_at === \"string\" ? e.registered_at : \"\",\n };\n });\n}\n\n/**\n * Persist the registry to disk. Creates `~/.almanac/` if it doesn't exist.\n *\n * We write with a trailing newline and 2-space indentation so the file is\n * diff-friendly if someone ever commits or inspects it manually.\n *\n * The write is atomic: we write to `registry.json.tmp` and then rename,\n * which is an atomic operation on every mainstream filesystem. This\n * matters because two concurrent `almanac init` (or autoregister) calls\n * from different shells would otherwise race on a partial write and\n * corrupt the file — a single `rename` means one wins cleanly and the\n * other's contents are simply dropped.\n */\nexport async function writeRegistry(entries: RegistryEntry[]): Promise<void> {\n const path = getRegistryPath();\n await mkdir(dirname(path), { recursive: true });\n const body = `${JSON.stringify(entries, null, 2)}\\n`;\n const tmpPath = `${path}.tmp`;\n await writeFile(tmpPath, body, \"utf8\");\n await rename(tmpPath, path);\n}\n\n/**\n * macOS (HFS+/APFS default) and Windows (NTFS default) are case-insensitive\n * but case-preserving. `/Users/x/Project` and `/Users/x/project` are the\n * same directory. We must treat them as the same registry entry, or a\n * single `almanac init` from a differently-cased cwd would duplicate the\n * row. Linux is case-sensitive — do not normalize there.\n *\n * Callers still store the original casing; only comparisons are lowercased.\n */\nfunction pathsEqual(a: string, b: string): boolean {\n if (process.platform === \"darwin\" || process.platform === \"win32\") {\n return a.toLowerCase() === b.toLowerCase();\n }\n return a === b;\n}\n\n/**\n * Add (or replace) an entry in the registry.\n *\n * Uniqueness is enforced on BOTH `name` and `path`: a repo can only appear\n * once, and a name can only refer to one repo. If either matches, we\n * replace the existing entry rather than creating a duplicate. This is\n * what makes auto-registration idempotent.\n */\nexport async function addEntry(entry: RegistryEntry): Promise<RegistryEntry[]> {\n const existing = await readRegistry();\n const filtered = existing.filter(\n (e) => e.name !== entry.name && !pathsEqual(e.path, entry.path),\n );\n filtered.push(entry);\n await writeRegistry(filtered);\n return filtered;\n}\n\n/**\n * Remove an entry by name. Returns the removed entry (or `null` if none\n * matched). Only `almanac list --drop <name>` calls this — we never drop\n * automatically, even for unreachable paths.\n */\nexport async function dropEntry(name: string): Promise<RegistryEntry | null> {\n const existing = await readRegistry();\n const idx = existing.findIndex((e) => e.name === name);\n if (idx === -1) {\n return null;\n }\n const [removed] = existing.splice(idx, 1);\n await writeRegistry(existing);\n return removed ?? null;\n}\n\n/**\n * Find an entry by either name or absolute path. Used by auto-registration\n * to decide whether the current repo is already known.\n *\n * Path comparison is case-insensitive on macOS/Windows (see `pathsEqual`).\n */\nexport async function findEntry(params: {\n name?: string;\n path?: string;\n}): Promise<RegistryEntry | null> {\n const entries = await readRegistry();\n for (const entry of entries) {\n if (params.name !== undefined && entry.name === params.name) return entry;\n if (params.path !== undefined && pathsEqual(entry.path, params.path)) {\n return entry;\n }\n }\n return null;\n}\n\n/**\n * Ensure the global `.almanac/` directory exists. Safe to call repeatedly;\n * `mkdir recursive` is a no-op when the directory already exists.\n */\nexport async function ensureGlobalDir(): Promise<void> {\n await mkdir(getGlobalAlmanacDir(), { recursive: true });\n}\n\nfunction isNodeError(err: unknown): err is NodeJS.ErrnoException {\n return err instanceof Error && \"code\" in err;\n}\n","import { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { basename, join } from \"node:path\";\n\nimport fg from \"fast-glob\";\nimport type Database from \"better-sqlite3\";\n\nimport { BLUE, BOLD, DIM, GREEN, RED, RST } from \"../ansi.js\";\nimport { parseDuration } from \"../indexer/duration.js\";\nimport { ensureFreshIndex } from \"../indexer/index.js\";\nimport { resolveWikiRoot } from \"../indexer/resolve-wiki.js\";\nimport { openIndex } from \"../indexer/schema.js\";\nimport { findEntry } from \"../registry/index.js\";\nimport { toKebabCase } from \"../slug.js\";\nimport { subtreeInDb } from \"../topics/dag.js\";\n\n/**\n * `almanac health` — flag problems in the wiki.\n *\n * Eight independent categories, each checked against the current index\n * and filesystem. Categories never throw each other off; one failing\n * is not a reason to skip the others.\n *\n * Scoping:\n * - `--topic <slug>` narrows every page-scoped category to pages\n * tagged with that topic OR any descendant topic (DAG traversal).\n * Topic-level categories (`empty_topics`) are narrowed to the\n * subtree itself.\n * - `--stdin` reads page slugs from stdin and limits page-scoped\n * categories to that set.\n *\n * Output:\n * - default: human-readable, grouped by category with counts.\n * - `--json`: one big object, shape = `HealthReport`.\n */\n\nexport interface HealthReport {\n orphans: { slug: string }[];\n stale: { slug: string; days_since_update: number }[];\n dead_refs: { slug: string; path: string }[];\n broken_links: { source_slug: string; target_slug: string }[];\n broken_xwiki: { source_slug: string; target_wiki: string; target_slug: string }[];\n empty_topics: { slug: string }[];\n empty_pages: { slug: string }[];\n slug_collisions: { slug: string; paths: string[] }[];\n}\n\nexport interface HealthOptions {\n cwd: string;\n wiki?: string;\n topic?: string;\n stale?: string;\n stdin?: boolean;\n stdinInput?: string;\n json?: boolean;\n}\n\nexport interface HealthCommandOutput {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\n/**\n * Default `--stale` window. 90 days matches the spec. Users can tune\n * with `--stale <duration>` using the shared parser.\n */\nconst DEFAULT_STALE_SECONDS = 90 * 24 * 60 * 60;\n\nexport async function runHealth(\n options: HealthOptions,\n): Promise<HealthCommandOutput> {\n const repoRoot = await resolveWikiRoot({ cwd: options.cwd, wiki: options.wiki });\n await ensureFreshIndex({ repoRoot });\n\n const almanacDir = join(repoRoot, \".almanac\");\n const pagesDir = join(almanacDir, \"pages\");\n const db = openIndex(join(almanacDir, \"index.db\"));\n\n try {\n const staleSeconds = options.stale !== undefined\n ? parseDuration(options.stale)\n : DEFAULT_STALE_SECONDS;\n\n const scope = resolveScope(db, options);\n\n const report: HealthReport = {\n orphans: findOrphans(db, scope),\n stale: findStale(db, scope, staleSeconds),\n dead_refs: await findDeadRefs(db, scope, repoRoot),\n broken_links: findBrokenLinks(db, scope),\n broken_xwiki: await findBrokenXwiki(db, scope),\n empty_topics: findEmptyTopics(db, scope),\n empty_pages: await findEmptyPages(db, scope, pagesDir),\n slug_collisions: await findSlugCollisions(pagesDir),\n };\n\n if (options.json === true) {\n return {\n stdout: `${JSON.stringify(report, null, 2)}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n\n return {\n stdout: formatReport(report),\n stderr: \"\",\n exitCode: 0,\n };\n } finally {\n db.close();\n }\n}\n\ninterface HealthScope {\n /** When non-null, restrict page-scoped checks to these slugs. */\n pages: Set<string> | null;\n /** When non-null, restrict topic-scoped checks to these slugs. */\n topics: Set<string> | null;\n}\n\n/**\n * Compute the active page/topic scope from `--topic` and `--stdin`\n * flags. Both null = no restriction (report everything).\n */\nfunction resolveScope(db: Database.Database, options: HealthOptions): HealthScope {\n let pages: Set<string> | null = null;\n let topics: Set<string> | null = null;\n\n if (options.topic !== undefined) {\n const rootSlug = toKebabCase(options.topic);\n if (rootSlug.length > 0) {\n const subtree = subtreeInDb(db, rootSlug);\n topics = new Set(subtree);\n const placeholders = subtree.map(() => \"?\").join(\", \");\n const rows = db\n .prepare<unknown[], { page_slug: string }>(\n `SELECT DISTINCT page_slug FROM page_topics\n WHERE topic_slug IN (${placeholders})`,\n )\n .all(...subtree);\n pages = new Set(rows.map((r) => r.page_slug));\n }\n }\n\n if (options.stdin === true && options.stdinInput !== undefined) {\n const stdinPages = new Set<string>();\n for (const line of options.stdinInput.split(/\\r?\\n/)) {\n const s = line.trim();\n if (s.length > 0) stdinPages.add(s);\n }\n // Intersect with any existing topic-scoped set.\n if (pages === null) pages = stdinPages;\n else {\n const out = new Set<string>();\n for (const s of stdinPages) if (pages.has(s)) out.add(s);\n pages = out;\n }\n }\n\n return { pages, topics };\n}\n\nfunction inPageScope(scope: HealthScope, slug: string): boolean {\n if (scope.pages === null) return true;\n return scope.pages.has(slug);\n}\n\n// ─────────────────────────────────────────────────────────────────────\n// individual checks\n// ─────────────────────────────────────────────────────────────────────\n\n/**\n * Pages with zero `topics:`. Archived pages are exempt — the spec\n * excludes them from search by default and they're inherently\n * \"retired\", not \"abandoned\".\n */\nfunction findOrphans(\n db: Database.Database,\n scope: HealthScope,\n): { slug: string }[] {\n const rows = db\n .prepare<[], { slug: string }>(\n `SELECT p.slug FROM pages p\n WHERE p.archived_at IS NULL\n AND NOT EXISTS (\n SELECT 1 FROM page_topics pt WHERE pt.page_slug = p.slug\n )\n ORDER BY p.slug`,\n )\n .all();\n return rows.filter((r) => inPageScope(scope, r.slug));\n}\n\n/**\n * Active pages whose `updated_at` is older than `staleSeconds`. We\n * report `days_since_update` rather than a raw timestamp because the\n * spec's example output (\"old-architecture (124 days)\") shows that.\n */\nfunction findStale(\n db: Database.Database,\n scope: HealthScope,\n staleSeconds: number,\n): { slug: string; days_since_update: number }[] {\n const now = Math.floor(Date.now() / 1000);\n const threshold = now - staleSeconds;\n const rows = db\n .prepare<[number], { slug: string; updated_at: number }>(\n `SELECT slug, updated_at FROM pages\n WHERE archived_at IS NULL AND updated_at < ?\n ORDER BY updated_at ASC`,\n )\n .all(threshold);\n return rows\n .filter((r) => inPageScope(scope, r.slug))\n .map((r) => ({\n slug: r.slug,\n days_since_update: Math.floor((now - r.updated_at) / (60 * 60 * 24)),\n }));\n}\n\n/**\n * `file_refs` whose target paths no longer exist on disk. We `stat`\n * each referenced path, relative to the repo root, and report misses.\n *\n * Only checks active pages — archived pages are allowed to reference\n * files that have since been deleted (that's often why they were\n * archived in the first place).\n *\n * We stat the `original_path` (author's casing) rather than the\n * lowercased `path` — on case-sensitive filesystems like Linux, stat\n * of a lowercased alias of `src/Dockerfile` returns ENOENT even\n * though the file exists. macOS and Windows are case-insensitive so\n * either form resolves there; using the original consistently means\n * the code behaves identically on every host.\n */\nasync function findDeadRefs(\n db: Database.Database,\n scope: HealthScope,\n repoRoot: string,\n): Promise<{ slug: string; path: string }[]> {\n const rows = db\n .prepare<\n [],\n { slug: string; path: string; original_path: string; is_dir: number }\n >(\n `SELECT p.slug, r.path, r.original_path, r.is_dir\n FROM file_refs r\n JOIN pages p ON p.slug = r.page_slug\n WHERE p.archived_at IS NULL\n ORDER BY p.slug, r.path`,\n )\n .all();\n const out: { slug: string; path: string }[] = [];\n for (const r of rows) {\n if (!inPageScope(scope, r.slug)) continue;\n const abs = join(repoRoot, r.original_path);\n if (!existsSync(abs)) {\n // Surface the author's casing in the report — matches what's in\n // the user's frontmatter/wikilink, which is what they'll search\n // for when fixing the miss.\n out.push({ slug: r.slug, path: r.original_path });\n }\n }\n return out;\n}\n\n/**\n * Wikilinks whose target slug has no row in `pages`. Every other\n * page-scoped check filters archived source pages out; this one and\n * `findBrokenXwiki` follow the same rule so the report doesn't flag\n * broken links from pages that have been retired.\n */\nfunction findBrokenLinks(\n db: Database.Database,\n scope: HealthScope,\n): { source_slug: string; target_slug: string }[] {\n const rows = db\n .prepare<[], { source_slug: string; target_slug: string }>(\n `SELECT w.source_slug, w.target_slug\n FROM wikilinks w\n JOIN pages src ON src.slug = w.source_slug\n LEFT JOIN pages tgt ON tgt.slug = w.target_slug\n WHERE tgt.slug IS NULL AND src.archived_at IS NULL\n ORDER BY w.source_slug, w.target_slug`,\n )\n .all();\n return rows.filter((r) => inPageScope(scope, r.source_slug));\n}\n\n/**\n * Cross-wiki links whose target wiki isn't registered OR whose path\n * is unreachable. Per the plan we stop at \"wiki unregistered or path\n * missing\" — walking into the other wiki's `index.db` to check the\n * slug exists is explicitly out of scope for slice 3 (documented in\n * the plan). A follow-up slice can deepen this.\n */\nasync function findBrokenXwiki(\n db: Database.Database,\n scope: HealthScope,\n): Promise<{ source_slug: string; target_wiki: string; target_slug: string }[]> {\n const rows = db\n .prepare<\n [],\n { source_slug: string; target_wiki: string; target_slug: string }\n >(\n // Same archived-source filter as `findBrokenLinks`. Retired pages\n // shouldn't spam the report with links to wikis that may have\n // been intentionally retired too.\n `SELECT x.source_slug, x.target_wiki, x.target_slug\n FROM cross_wiki_links x\n JOIN pages src ON src.slug = x.source_slug\n WHERE src.archived_at IS NULL\n ORDER BY x.source_slug, x.target_wiki, x.target_slug`,\n )\n .all();\n const out: { source_slug: string; target_wiki: string; target_slug: string }[] = [];\n // Cache the registry lookup so we only resolve each wiki once.\n const reachableCache = new Map<string, boolean>();\n for (const r of rows) {\n if (!inPageScope(scope, r.source_slug)) continue;\n let ok = reachableCache.get(r.target_wiki);\n if (ok === undefined) {\n const entry = await findEntry({ name: r.target_wiki });\n ok = entry !== null && existsSync(join(entry.path, \".almanac\"));\n reachableCache.set(r.target_wiki, ok);\n }\n if (!ok) {\n out.push({\n source_slug: r.source_slug,\n target_wiki: r.target_wiki,\n target_slug: r.target_slug,\n });\n }\n }\n return out;\n}\n\n/** Topics with zero pages. */\nfunction findEmptyTopics(\n db: Database.Database,\n scope: HealthScope,\n): { slug: string }[] {\n const rows = db\n .prepare<[], { slug: string }>(\n `SELECT t.slug FROM topics t\n WHERE NOT EXISTS (\n SELECT 1 FROM page_topics pt WHERE pt.topic_slug = t.slug\n )\n ORDER BY t.slug`,\n )\n .all();\n if (scope.topics === null) return rows;\n return rows.filter((r) => scope.topics!.has(r.slug));\n}\n\n/**\n * Pages whose body is effectively empty — only frontmatter, maybe a\n * heading, no prose. \"Empty\" = after dropping frontmatter and heading\n * lines, the remaining non-blank non-whitespace content is < 40\n * characters. This matches the test from the plan: \"a page with only\n * frontmatter + heading is empty; with a paragraph it's not.\"\n *\n * Archived pages are exempt — deliberately minimal archive stubs\n * shouldn't be flagged.\n */\nasync function findEmptyPages(\n db: Database.Database,\n scope: HealthScope,\n pagesDir: string,\n): Promise<{ slug: string }[]> {\n const rows = db\n .prepare<[], { slug: string; file_path: string }>(\n `SELECT slug, file_path FROM pages\n WHERE archived_at IS NULL\n ORDER BY slug`,\n )\n .all();\n const out: { slug: string }[] = [];\n for (const r of rows) {\n if (!inPageScope(scope, r.slug)) continue;\n let raw: string;\n try {\n raw = await readFile(r.file_path, \"utf8\");\n } catch {\n continue;\n }\n // Strip frontmatter if present.\n const m = raw.match(/^---\\r?\\n[\\s\\S]*?\\r?\\n---\\r?\\n?([\\s\\S]*)$/);\n const body = m !== null ? (m[1] ?? \"\") : raw;\n // \"Empty\" = after dropping frontmatter, heading lines, and blank\n // lines, nothing non-trivial remains. A single-line wikilink or\n // one-sentence paragraph counts as content; a page with only a\n // heading (or a heading + whitespace) does not.\n //\n // `pagesDir` is accepted for parity with future content-resolution\n // checks (e.g., resolving includes); referenced so lint doesn't\n // complain about an unused parameter.\n void pagesDir;\n const hasSubstance = body\n .split(/\\r?\\n/)\n .some((l) => {\n const t = l.trim();\n if (t.length === 0) return false;\n if (t.startsWith(\"#\")) return false;\n return true;\n });\n if (!hasSubstance) {\n out.push({ slug: r.slug });\n }\n }\n return out;\n}\n\n/**\n * Walk `.almanac/pages/` and group filenames by their kebab-cased\n * slug. Any slug with >1 filename is a collision. We rescan rather\n * than reading a persisted table — indexing surfaces collisions only\n * as warnings, so a dedicated rescan gives us a definitive answer\n * without adding a new table.\n */\nasync function findSlugCollisions(\n pagesDir: string,\n): Promise<{ slug: string; paths: string[] }[]> {\n if (!existsSync(pagesDir)) return [];\n const files = await fg(\"**/*.md\", {\n cwd: pagesDir,\n absolute: false,\n onlyFiles: true,\n caseSensitiveMatch: true,\n });\n const bySlug = new Map<string, string[]>();\n for (const rel of files) {\n const slug = toKebabCase(basename(rel, \".md\"));\n if (slug.length === 0) continue;\n const list = bySlug.get(slug) ?? [];\n list.push(rel);\n bySlug.set(slug, list);\n }\n const out: { slug: string; paths: string[] }[] = [];\n for (const [slug, paths] of bySlug.entries()) {\n if (paths.length > 1) {\n out.push({ slug, paths: paths.sort() });\n }\n }\n out.sort((a, b) => a.slug.localeCompare(b.slug));\n return out;\n}\n\n// ─────────────────────────────────────────────────────────────────────\n// pretty-print\n// ─────────────────────────────────────────────────────────────────────\n\nfunction formatReport(r: HealthReport): string {\n const sections: string[] = [];\n sections.push(\n section(\n \"orphans\",\n r.orphans.length,\n r.orphans.map((o) => ` ${BLUE}${o.slug}${RST}`),\n ),\n );\n sections.push(\n section(\n \"stale\",\n r.stale.length,\n r.stale.map((s) => ` ${BLUE}${s.slug}${RST} ${DIM}(${s.days_since_update} days)${RST}`),\n ),\n );\n sections.push(\n section(\n \"dead-refs\",\n r.dead_refs.length,\n r.dead_refs.map((d) => ` ${BLUE}${d.slug}${RST} references ${d.path} ${DIM}(missing)${RST}`),\n ),\n );\n sections.push(\n section(\n \"broken-links\",\n r.broken_links.length,\n r.broken_links.map(\n (b) => ` ${BLUE}${b.source_slug}${RST} → ${b.target_slug} ${DIM}(target does not exist)${RST}`,\n ),\n ),\n );\n sections.push(\n section(\n \"broken-xwiki\",\n r.broken_xwiki.length,\n r.broken_xwiki.map(\n (b) =>\n ` ${BLUE}${b.source_slug}${RST} → ${b.target_wiki}:${b.target_slug} ${DIM}(wiki unregistered or unreachable)${RST}`,\n ),\n ),\n );\n sections.push(\n section(\n \"empty-topics\",\n r.empty_topics.length,\n r.empty_topics.map((e) => ` ${BLUE}${e.slug}${RST}`),\n ),\n );\n sections.push(\n section(\n \"empty-pages\",\n r.empty_pages.length,\n r.empty_pages.map((e) => ` ${BLUE}${e.slug}${RST}`),\n ),\n );\n sections.push(\n section(\n \"slug-collisions\",\n r.slug_collisions.length,\n r.slug_collisions.map((c) => ` ${BLUE}${c.slug}${RST}: ${c.paths.join(\", \")}`),\n ),\n );\n return `${sections.join(\"\\n\\n\")}\\n`;\n}\n\nfunction section(label: string, count: number, lines: string[]): string {\n if (count === 0) return `${BOLD}${label}${RST} ${GREEN}(0): (ok)${RST}`;\n return `${BOLD}${label}${RST} ${RED}(${count})${RST}:\\n${lines.join(\"\\n\")}`;\n}\n","/**\n * Parse a compact duration string of the form `<N><unit>` into seconds.\n *\n * Accepted units (from the spec, `--since` / `--stale`):\n * - `m` — minutes\n * - `h` — hours\n * - `d` — days\n * - `w` — weeks\n *\n * Examples: `2w` → 1209600, `30d` → 2592000, `12h` → 43200.\n *\n * Anything else throws — the CLI surfaces the error with the usual\n * `almanac: <message>` prefix, which is clearer than silently treating\n * `2weeks` or `30 days` as zero.\n */\nexport function parseDuration(input: string): number {\n const trimmed = input.trim();\n const m = trimmed.match(/^(\\d+)([mhdw])$/);\n if (m === null) {\n throw new Error(\n `invalid duration \"${input}\" (expected Nw, Nd, Nh, or Nm — e.g. 2w, 30d)`,\n );\n }\n const n = Number.parseInt(m[1] ?? \"0\", 10);\n const unit = m[2];\n switch (unit) {\n case \"m\":\n return n * 60;\n case \"h\":\n return n * 60 * 60;\n case \"d\":\n return n * 60 * 60 * 24;\n case \"w\":\n return n * 60 * 60 * 24 * 7;\n default:\n // Unreachable — regex pins the unit — but satisfies exhaustiveness.\n throw new Error(`invalid duration unit \"${unit ?? \"\"}\"`);\n }\n}\n","import { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nimport { findNearestAlmanacDir } from \"../paths.js\";\nimport { findEntry } from \"../registry/index.js\";\n\n/**\n * Figure out which repo root a query command should run against.\n *\n * Two modes, in order of precedence:\n * 1. `--wiki <name>` — look it up in the global registry. Fails\n * explicitly if the name isn't registered or its path has gone\n * missing (unmounted drive, deleted repo). No silent fallback, which\n * would hide the real problem from the user.\n * 2. default — walk up from `cwd` like git does. Fails if we're not\n * inside a `.almanac/` repo.\n *\n * Returns the absolute path to the repo root (the directory containing\n * `.almanac/`).\n *\n * NOTE (spec contract, not yet implemented): when `--all` lands in a\n * future slice, it must silently skip wikis whose paths have gone\n * unreachable — the asymmetry with `--wiki <name>` is deliberate.\n * Explicit lookup is loud about failures (user named a specific wiki);\n * bulk `--all` is quiet (user asked \"whatever's available\"). Don't\n * unify the error behavior when adding `--all`.\n */\nexport async function resolveWikiRoot(params: {\n cwd: string;\n wiki?: string;\n}): Promise<string> {\n if (params.wiki !== undefined) {\n const entry = await findEntry({ name: params.wiki });\n if (entry === null) {\n throw new Error(`no registered wiki named \"${params.wiki}\"`);\n }\n if (!existsSync(join(entry.path, \".almanac\"))) {\n throw new Error(\n `wiki \"${params.wiki}\" path is unreachable (${entry.path})`,\n );\n }\n return entry.path;\n }\n\n const nearest = findNearestAlmanacDir(params.cwd);\n if (nearest === null) {\n throw new Error(\n \"no .almanac/ found in this directory or any parent; run `almanac bootstrap` first\",\n );\n }\n return nearest;\n}\n","import type Database from \"better-sqlite3\";\n\nimport type { TopicsFile } from \"./yaml.js\";\n\n/**\n * Depth cap for all recursive traversals of the topics DAG. Belt and\n * suspenders alongside the `CHECK (child_slug != parent_slug)` on the\n * `topic_parents` table — even if a cycle somehow slipped into the data\n * (hand-edited `topics.yaml`, past bug), the CTE can't runaway.\n *\n * 32 is chosen as \"deeper than any real human-authored taxonomy will\n * ever go\". A 32-level topic hierarchy is absurd; anything hitting this\n * cap is almost certainly a cycle.\n */\nexport const DAG_DEPTH_CAP = 32;\n\n/**\n * Given a `topics.yaml` in memory, compute the set of ancestors of a\n * given slug (not including the slug itself). Used by `topics link`\n * to check whether a proposed edge would create a cycle.\n *\n * Running off the in-memory file lets `link` validate BEFORE touching\n * either the DB or the YAML, so a refusal doesn't leave half the state\n * mutated. Depth-capped with the same constant as the SQLite CTE.\n */\nexport function ancestorsInFile(\n file: TopicsFile,\n slug: string,\n): Set<string> {\n // Build a child → parents map once.\n const parentsOf = new Map<string, string[]>();\n for (const t of file.topics) {\n parentsOf.set(t.slug, t.parents);\n }\n const ancestors = new Set<string>();\n // BFS, depth-capped. We stop descending when we've hit the cap or\n // revisit an already-seen node (self-loop defense).\n let frontier: string[] = parentsOf.get(slug) ?? [];\n let depth = 0;\n while (frontier.length > 0 && depth < DAG_DEPTH_CAP) {\n const next: string[] = [];\n for (const node of frontier) {\n if (ancestors.has(node)) continue;\n ancestors.add(node);\n const ps = parentsOf.get(node);\n if (ps !== undefined) next.push(...ps);\n }\n frontier = next;\n depth += 1;\n }\n return ancestors;\n}\n\n/**\n * Return all descendants of a given topic slug via the SQLite\n * `topic_parents` table. Depth-capped at `DAG_DEPTH_CAP`.\n *\n * Used by `topics show --descendants` to expand a topic's page list\n * through its subtopics. The query is a canonical recursive CTE; we\n * `UNION` (not `UNION ALL`) so cycles in the data don't spin forever.\n */\nexport function descendantsInDb(\n db: Database.Database,\n slug: string,\n): string[] {\n const rows = db\n .prepare<[string, number], { slug: string }>(\n `WITH RECURSIVE desc(slug, depth) AS (\n SELECT child_slug, 1 FROM topic_parents WHERE parent_slug = ?\n UNION\n SELECT tp.child_slug, d.depth + 1\n FROM topic_parents tp\n JOIN desc d ON tp.parent_slug = d.slug\n WHERE d.depth < ?\n )\n SELECT DISTINCT slug FROM desc ORDER BY slug`,\n )\n .all(slug, DAG_DEPTH_CAP)\n .map((r) => r.slug);\n return rows;\n}\n\n/**\n * Return the subtree rooted at `slug` (the slug itself + all\n * descendants). Convenience wrapper used by `health --topic` to scope\n * reports through the DAG.\n */\nexport function subtreeInDb(db: Database.Database, slug: string): string[] {\n return [slug, ...descendantsInDb(db, slug)];\n}\n"],"mappings":"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","names":["existsSync","statSync","readFile","join","fg","yaml","existsSync","existsSync","existsSync","target","join","existsSync","fg","statSync","readFile","mkdir","readFile","rename","writeFile","dirname","readFile","isNodeError","path","mkdir","dirname","writeFile","rename","existsSync","readFile","basename","join","fg","existsSync","join","existsSync","join","join","existsSync","readFile","fg","basename"]}
|
package/dist/cli-CMGYLJSN.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts","../src/cli/help.ts","../src/update/announce.ts"],"sourcesContent":["import { createRequire } from \"node:module\";\nimport { basename } from \"node:path\";\n\nimport { Command } from \"commander\";\n\nimport { runSetup } from \"./commands/setup.js\";\nimport { configureGroupedHelp } from \"./cli/help.js\";\nimport { emit } from \"./cli/helpers.js\";\nimport { announceUpdateIfAvailable } from \"./update/announce.js\";\nimport {\n runInternalUpdateCheck,\n scheduleBackgroundUpdateCheck,\n} from \"./update/schedule.js\";\n\n/**\n * Optional dependency overrides for `run`. Tests use these to avoid\n * spawning the real setup wizard, the real update background check,\n * and the real update banner. Production callers pass nothing.\n */\nexport interface RunDeps {\n /** Replace the setup wizard (bare `codealmanac` / `almanac setup`). */\n runSetup?: typeof runSetup;\n /** Replace the pre-command update-nag banner. */\n announceUpdate?: (stderr: NodeJS.WritableStream) => void;\n /** Replace the post-command background update check scheduler. */\n scheduleUpdateCheck?: (argv: string[]) => void;\n /** Replace the internal update-check worker (run on --internal-check-updates). */\n runInternalUpdateCheck?: () => Promise<void>;\n}\n\n/**\n * Process-level CLI entrypoint. This owns invocation-level behavior:\n * update checks, bare `codealmanac` setup routing, Commander creation,\n * grouped help, and parsing. Individual command wiring lives in\n * `src/cli/register-commands.ts`.\n */\nexport async function run(argv: string[], deps: RunDeps = {}): Promise<void> {\n const runSetupFn = deps.runSetup ?? runSetup;\n const announceUpdateFn = deps.announceUpdate ?? announceUpdateIfAvailable;\n const scheduleUpdateCheckFn =\n deps.scheduleUpdateCheck ?? scheduleBackgroundUpdateCheck;\n const runInternalUpdateCheckFn =\n deps.runInternalUpdateCheck ?? runInternalUpdateCheck;\n\n if (argv.slice(2).includes(\"--internal-check-updates\")) {\n await runInternalUpdateCheckFn();\n return;\n }\n\n const programName = getProgramName(argv);\n\n announceUpdateFn(process.stderr);\n scheduleUpdateCheckFn(argv);\n\n const program = new Command();\n program\n .name(programName)\n .description(\n \"codealmanac — a living wiki for codebases, maintained by AI agents\",\n )\n .version(readPackageVersion(), \"-v, --version\", \"print version\");\n\n if (isRootVersionInvocation(argv.slice(2))) {\n await program.parseAsync(argv);\n return;\n }\n\n if (programName === \"codealmanac\") {\n const setupInvocation = tryParseSetupShortcut(argv.slice(2));\n if (setupInvocation !== null) {\n emit(await runSetupFn(setupInvocation));\n return;\n }\n }\n\n if (await tryRunSqliteFreeCommand(argv.slice(2), runSetupFn)) {\n return;\n }\n\n const { registerCommands } = await import(\"./cli/register-commands.js\");\n registerCommands(program);\n configureGroupedHelp(program);\n\n await program.parseAsync(argv);\n}\n\nfunction getProgramName(argv: string[]): \"almanac\" | \"codealmanac\" {\n const invoked = argv[1] !== undefined ? basename(argv[1]) : \"almanac\";\n return invoked === \"codealmanac\" ? \"codealmanac\" : \"almanac\";\n}\n\nfunction isRootVersionInvocation(args: string[]): boolean {\n return args.length === 1 && (args[0] === \"--version\" || args[0] === \"-v\");\n}\n\nfunction parseSetupFlags(args: string[]): {\n yes?: boolean;\n agent?: string;\n skipHook?: boolean;\n skipGuides?: boolean;\n} {\n const agentIdx = args.indexOf(\"--agent\");\n return {\n yes: args.includes(\"--yes\") || args.includes(\"-y\"),\n agent: agentIdx === -1 ? undefined : args[agentIdx + 1],\n skipHook: args.includes(\"--skip-hook\"),\n skipGuides: args.includes(\"--skip-guides\"),\n };\n}\n\nfunction parseUpdateFlags(args: string[]): {\n dismiss?: boolean;\n check?: boolean;\n enableNotifier?: boolean;\n disableNotifier?: boolean;\n} {\n return {\n dismiss: args.includes(\"--dismiss\"),\n check: args.includes(\"--check\"),\n enableNotifier: args.includes(\"--enable-notifier\"),\n disableNotifier: args.includes(\"--disable-notifier\"),\n };\n}\n\nfunction parseUninstallFlags(args: string[]): {\n yes?: boolean;\n keepHook?: boolean;\n keepGuides?: boolean;\n} {\n return {\n yes: args.includes(\"--yes\") || args.includes(\"-y\"),\n keepHook: args.includes(\"--keep-hook\"),\n keepGuides: args.includes(\"--keep-guides\"),\n };\n}\n\nfunction parseDoctorFlags(args: string[]): {\n json?: boolean;\n installOnly?: boolean;\n wikiOnly?: boolean;\n} {\n return {\n json: args.includes(\"--json\"),\n installOnly: args.includes(\"--install-only\"),\n wikiOnly: args.includes(\"--wiki-only\"),\n };\n}\n\nasync function tryRunSqliteFreeCommand(\n args: string[],\n runSetupFn: typeof runSetup,\n): Promise<boolean> {\n if (args.includes(\"--help\") || args.includes(\"-h\")) return false;\n\n const [command, subcommand] = args;\n if (command === undefined) return false;\n\n if (command === \"setup\") {\n emit(await runSetupFn(parseSetupFlags(args.slice(1))));\n return true;\n }\n\n if (command === \"hook\") {\n const { runHookInstall, runHookStatus, runHookUninstall } = await import(\n \"./commands/hook.js\"\n );\n if (subcommand === \"install\") {\n emit(await runHookInstall({ source: parseHookSource(args.slice(2)) }));\n return true;\n }\n if (subcommand === \"uninstall\") {\n emit(await runHookUninstall());\n return true;\n }\n if (subcommand === \"status\") {\n emit(await runHookStatus());\n return true;\n }\n return false;\n }\n\n if (command === \"agents\") {\n const { runAgentsList } = await import(\"./commands/agents.js\");\n if (subcommand === \"list\" || subcommand === undefined) {\n emit(await runAgentsList());\n return true;\n }\n return false;\n }\n\n if (command === \"set\") {\n const { runSetAgentModel, runSetDefaultAgent } = await import(\n \"./commands/agents.js\"\n );\n if (subcommand === \"default-agent\") {\n emit(await runSetDefaultAgent({ provider: args[2] ?? \"\" }));\n return true;\n }\n if (subcommand === \"model\") {\n emit(await runSetAgentModel({ provider: args[2] ?? \"\", model: args[3] }));\n return true;\n }\n return false;\n }\n\n if (command === \"update\") {\n const { runUpdate } = await import(\"./commands/update.js\");\n emit(await runUpdate(parseUpdateFlags(args.slice(1))));\n return true;\n }\n\n if (command === \"doctor\") {\n const { runDoctor } = await import(\"./commands/doctor.js\");\n emit(await runDoctor({\n cwd: process.cwd(),\n ...parseDoctorFlags(args.slice(1)),\n }));\n return true;\n }\n\n if (command === \"uninstall\") {\n const { runUninstall } = await import(\"./commands/uninstall.js\");\n emit(await runUninstall(parseUninstallFlags(args.slice(1))));\n return true;\n }\n\n return false;\n}\n\nfunction parseHookSource(\n args: string[],\n): \"claude\" | \"codex\" | \"cursor\" | \"all\" | undefined {\n const idx = args.indexOf(\"--source\");\n const value = idx === -1 ? undefined : args[idx + 1];\n if (\n value === \"claude\" ||\n value === \"codex\" ||\n value === \"cursor\" ||\n value === \"all\"\n ) {\n return value;\n }\n return undefined;\n}\n\nfunction readPackageVersion(): string {\n try {\n const require = createRequire(import.meta.url);\n const pkg = require(\"../package.json\") as { version?: unknown };\n if (typeof pkg.version === \"string\" && pkg.version.length > 0) {\n return pkg.version;\n }\n } catch {\n // Fall back to \"unknown\" rather than crashing the CLI on a broken install.\n }\n return \"unknown\";\n}\n\nexport interface SetupShortcutOptions {\n yes?: boolean;\n agent?: string;\n skipHook?: boolean;\n skipGuides?: boolean;\n}\n\n/**\n * Decide whether a bare `codealmanac [...args]` invocation should route\n * straight to `runSetup` (and if so, with which flags). Returns the\n * options object when it's a setup shortcut, or `null` when Commander\n * should parse the invocation normally.\n */\nexport function tryParseSetupShortcut(args: string[]): SetupShortcutOptions | null {\n if (args.length === 0) return {};\n\n const opts: SetupShortcutOptions = {};\n for (let i = 0; i < args.length; i++) {\n const arg = args[i]!;\n if (arg === \"--yes\" || arg === \"-y\") {\n opts.yes = true;\n continue;\n }\n if (arg === \"--agent\") {\n opts.agent = args[i + 1];\n i += 1;\n continue;\n }\n if (arg === \"--skip-hook\") {\n opts.skipHook = true;\n continue;\n }\n if (arg === \"--skip-guides\") {\n opts.skipGuides = true;\n continue;\n }\n return null;\n }\n return opts;\n}\n","import { Command, type Help } from \"commander\";\n\nimport { BLUE, BOLD, DIM, RST } from \"../ansi.js\";\n\nconst HELP_GROUPS: Array<{ title: string; commands: string[] }> = [\n {\n title: \"Query\",\n commands: [\"search\", \"show\", \"health\", \"list\"],\n },\n {\n title: \"Edit\",\n commands: [\"tag\", \"untag\", \"topics\"],\n },\n {\n title: \"Wiki lifecycle\",\n commands: [\"bootstrap\", \"capture\", \"hook\", \"reindex\"],\n },\n {\n title: \"Setup\",\n commands: [\"setup\", \"uninstall\", \"doctor\", \"update\"],\n },\n];\n\n/**\n * Install a custom `formatHelp` that replaces commander's flat\n * \"Commands:\" section with grouped headings. Keeps usage + options +\n * per-command short descriptions; only the commands section changes.\n */\nexport function configureGroupedHelp(program: Command): void {\n program.configureHelp({\n formatHelp(cmd, helper): string {\n if (cmd.parent !== null) {\n return renderDefault(cmd, helper);\n }\n\n const termWidth = helper.padWidth(cmd, helper);\n const helpWidth =\n helper.helpWidth ?? process.stdout.columns ?? 80;\n const itemSepWidth = 2;\n\n const out: string[] = [];\n out.push(`${BOLD}Usage:${RST} ${helper.commandUsage(cmd)}\\n`);\n\n const description = helper.commandDescription(cmd);\n if (description.length > 0) {\n out.push(\n helper.wrap(description, helpWidth, 0) + \"\\n\",\n );\n }\n\n const optionList = helper\n .visibleOptions(cmd)\n .map((o) => {\n const term = helper.optionTerm(o);\n const pad = \" \".repeat(Math.max(0, termWidth - term.length) + itemSepWidth);\n return `${BLUE}${term}${RST}${pad}${DIM}${helper.optionDescription(o)}${RST}`;\n });\n if (optionList.length > 0) {\n out.push(`${BOLD}Options:${RST}`);\n for (const l of optionList) out.push(` ${l}`);\n out.push(\"\");\n }\n\n const visible = helper.visibleCommands(cmd);\n const byName = new Map<string, (typeof visible)[number]>();\n for (const c of visible) byName.set(c.name(), c);\n\n for (const group of HELP_GROUPS) {\n const members = group.commands\n .map((n) => byName.get(n))\n .filter((c): c is (typeof visible)[number] => c !== undefined);\n if (members.length === 0) continue;\n out.push(`${BOLD}${group.title}:${RST}`);\n for (const c of members) {\n const term = helper.subcommandTerm(c);\n const desc = helper.subcommandDescription(c);\n const padding = Math.max(\n 0,\n termWidth - term.length + itemSepWidth,\n );\n out.push(` ${BLUE}${term}${RST}${\" \".repeat(padding)}${DIM}${desc}${RST}`);\n byName.delete(c.name());\n }\n out.push(\"\");\n }\n\n byName.delete(\"help\");\n if (byName.size > 0) {\n out.push(`${BOLD}Other:${RST}`);\n for (const c of byName.values()) {\n const term = helper.subcommandTerm(c);\n const desc = helper.subcommandDescription(c);\n const padding = Math.max(\n 0,\n termWidth - term.length + itemSepWidth,\n );\n out.push(` ${BLUE}${term}${RST}${\" \".repeat(padding)}${DIM}${desc}${RST}`);\n }\n out.push(\"\");\n }\n\n return out.join(\"\\n\");\n },\n });\n}\n\nfunction renderDefault(cmd: Command, helper: Help): string {\n const termWidth = helper.padWidth(cmd, helper);\n const helpWidth = helper.helpWidth ?? process.stdout.columns ?? 80;\n const itemSepWidth = 2;\n\n const lines: string[] = [`${BOLD}Usage:${RST} ${helper.commandUsage(cmd)}\\n`];\n const description = helper.commandDescription(cmd);\n if (description.length > 0) {\n lines.push(helper.wrap(description, helpWidth, 0) + \"\\n\");\n }\n\n const args = helper.visibleArguments(cmd).map((a) => {\n const term = helper.argumentTerm(a);\n const pad = \" \".repeat(Math.max(0, termWidth - term.length) + itemSepWidth);\n return `${BLUE}${term}${RST}${pad}${DIM}${helper.argumentDescription(a)}${RST}`;\n });\n if (args.length > 0) {\n lines.push(`${BOLD}Arguments:${RST}`);\n for (const a of args) lines.push(` ${a}`);\n lines.push(\"\");\n }\n\n const opts = helper.visibleOptions(cmd).map((o) => {\n const term = helper.optionTerm(o);\n const pad = \" \".repeat(Math.max(0, termWidth - term.length) + itemSepWidth);\n return `${BLUE}${term}${RST}${pad}${DIM}${helper.optionDescription(o)}${RST}`;\n });\n if (opts.length > 0) {\n lines.push(`${BOLD}Options:${RST}`);\n for (const o of opts) lines.push(` ${o}`);\n lines.push(\"\");\n }\n\n const subs = helper.visibleCommands(cmd).map((c) => {\n const term = helper.subcommandTerm(c);\n const pad = \" \".repeat(Math.max(0, termWidth - term.length) + itemSepWidth);\n return `${BLUE}${term}${RST}${pad}${DIM}${helper.subcommandDescription(c)}${RST}`;\n });\n if (subs.length > 0) {\n lines.push(`${BOLD}Commands:${RST}`);\n for (const s of subs) lines.push(` ${s}`);\n lines.push(\"\");\n }\n\n return lines.join(\"\\n\");\n}\n","import { readFileSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\n\nimport { getConfigPath } from \"./config.js\";\nimport { isNewer } from \"./semver.js\";\nimport { getStatePath, type UpdateState } from \"./state.js\";\n\n/**\n * Pre-command update-nag banner. Runs synchronously at the very top of\n * every `run()` invocation, before commander even touches argv. Prints\n * one line to stderr if:\n *\n * 1. `~/.almanac/update-state.json` exists and parses.\n * 2. `latest_version` is strictly newer than `installed_version`.\n * 3. `latest_version` is NOT in `dismissed_versions`.\n * 4. `~/.almanac/config.json`.`update_notifier` is not `false`.\n *\n * Otherwise silent. Deliberately synchronous and filesystem-blocking:\n * this runs in the CLI critical path and waiting on a Promise would\n * force every command to become async upfront. The files are tiny\n * (<1KB each); a sync read is cheap.\n *\n * Why stderr: agents and scripts parse stdout; stderr is the\n * conventional channel for diagnostics and nags. A tool piping\n * `almanac search` into `xargs` shouldn't see the banner mixed into\n * its slug list. Users running interactively see stderr anyway, so\n * the nag is visible in practice.\n */\n\nexport interface AnnounceOptions {\n statePath?: string;\n configPath?: string;\n /** Override for tests — normally reads from package.json at import time. */\n installedVersion?: string;\n /** Enable ANSI coloring regardless of isTTY. Tests pass `false`. */\n color?: boolean;\n}\n\nconst RST = \"\\x1b[0m\";\nconst BOLD = \"\\x1b[1m\";\nconst YELLOW = \"\\x1b[33m\";\n\nexport function announceUpdateIfAvailable(\n stderr: NodeJS.WritableStream,\n opts: AnnounceOptions = {},\n): void {\n const statePath = opts.statePath ?? getStatePath();\n const configPath = opts.configPath ?? getConfigPath();\n const installed = opts.installedVersion ?? readInstalledVersion();\n\n // Config gate. Must be checked before state: a user who disabled the\n // notifier shouldn't pay even a state-file read.\n if (!shouldNotify(configPath)) return;\n\n const state = readStateSync(statePath);\n if (state === null) return;\n if (state.latest_version.length === 0) return;\n if (!isNewer(state.latest_version, installed)) return;\n if (state.dismissed_versions.includes(state.latest_version)) return;\n\n const useColor =\n opts.color ?? (process.stderr.isTTY === true && !(\"NO_COLOR\" in process.env));\n const warn = useColor ? `${YELLOW}${BOLD}\\u26a0${RST}` : \"!\";\n const cmd = useColor ? `${BOLD}almanac update${RST}` : \"almanac update\";\n stderr.write(\n `${warn} codealmanac ${state.latest_version} available ` +\n `(you're on ${installed}) — run: ${cmd}\\n`,\n );\n}\n\n/**\n * Sync-read the state file. Returns `null` when missing, empty, or\n * malformed — the announce path MUST NOT throw into the CLI critical\n * path. Avoids the `async readState` used by the worker because\n * `run()` would otherwise need `await announceUpdateIfAvailable(...)`\n * on every invocation, which turns into a multi-millisecond penalty\n * on commands that don't care.\n */\nfunction readStateSync(path: string): UpdateState | null {\n let raw: string;\n try {\n raw = readFileSync(path, \"utf8\");\n } catch {\n return null;\n }\n const trimmed = raw.trim();\n if (trimmed.length === 0) return null;\n try {\n const parsed = JSON.parse(trimmed) as Partial<UpdateState>;\n return {\n last_check_at:\n typeof parsed.last_check_at === \"number\" ? parsed.last_check_at : 0,\n installed_version:\n typeof parsed.installed_version === \"string\"\n ? parsed.installed_version\n : \"\",\n latest_version:\n typeof parsed.latest_version === \"string\" ? parsed.latest_version : \"\",\n dismissed_versions: Array.isArray(parsed.dismissed_versions)\n ? parsed.dismissed_versions.filter(\n (v): v is string => typeof v === \"string\",\n )\n : [],\n };\n } catch {\n return null;\n }\n}\n\nfunction shouldNotify(configPath: string): boolean {\n let raw: string;\n try {\n raw = readFileSync(configPath, \"utf8\");\n } catch {\n return true; // no config file → default notify on\n }\n const trimmed = raw.trim();\n if (trimmed.length === 0) return true;\n try {\n const parsed = JSON.parse(trimmed) as { update_notifier?: unknown };\n if (parsed.update_notifier === false) return false;\n return true;\n } catch {\n return true;\n }\n}\n\nfunction readInstalledVersion(): string {\n // Dev: `src/update/announce.ts` → `../../package.json`. Bundled:\n // `dist/codealmanac.js` → `../package.json`. Try both.\n try {\n const require = createRequire(import.meta.url);\n const pkg = require(\"../../package.json\") as { version?: unknown };\n if (typeof pkg.version === \"string\" && pkg.version.length > 0) {\n return pkg.version;\n }\n } catch {\n // Fall through.\n }\n try {\n const require = createRequire(import.meta.url);\n const pkg = require(\"../package.json\") as { version?: unknown };\n if (typeof pkg.version === \"string\" && pkg.version.length > 0) {\n return pkg.version;\n }\n } catch {\n // Fall through.\n }\n return \"unknown\";\n}\n"],"mappings":"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","names":["createRequire","RST","BOLD","require","require","createRequire"]}
|