codealmanac 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/codealmanac.js +983 -391
- package/dist/codealmanac.js.map +1 -1
- package/guides/mini.md +187 -0
- package/guides/reference.md +599 -0
- package/package.json +2 -1
- package/prompts/bootstrap.md +55 -10
package/dist/codealmanac.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
+
import { createRequire as createRequire3 } from "module";
|
|
4
5
|
import { basename as basename6 } from "path";
|
|
5
6
|
import { Command } from "commander";
|
|
6
7
|
|
|
@@ -225,7 +226,7 @@ function findNearestAlmanacDir(startDir) {
|
|
|
225
226
|
}
|
|
226
227
|
}
|
|
227
228
|
|
|
228
|
-
// src/commands/
|
|
229
|
+
// src/commands/_init.ts
|
|
229
230
|
import { existsSync as existsSync3 } from "fs";
|
|
230
231
|
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
231
232
|
import { basename, join as join3 } from "path";
|
|
@@ -239,10 +240,10 @@ function toKebabCase(input) {
|
|
|
239
240
|
import { mkdir, readFile as readFile2, rename, writeFile } from "fs/promises";
|
|
240
241
|
import { dirname as dirname3 } from "path";
|
|
241
242
|
async function readRegistry() {
|
|
242
|
-
const
|
|
243
|
+
const path5 = getRegistryPath();
|
|
243
244
|
let raw;
|
|
244
245
|
try {
|
|
245
|
-
raw = await readFile2(
|
|
246
|
+
raw = await readFile2(path5, "utf8");
|
|
246
247
|
} catch (err) {
|
|
247
248
|
if (isNodeError(err) && err.code === "ENOENT") {
|
|
248
249
|
return [];
|
|
@@ -258,10 +259,10 @@ async function readRegistry() {
|
|
|
258
259
|
parsed = JSON.parse(trimmed);
|
|
259
260
|
} catch (err) {
|
|
260
261
|
const message = err instanceof Error ? err.message : String(err);
|
|
261
|
-
throw new Error(`registry at ${
|
|
262
|
+
throw new Error(`registry at ${path5} is not valid JSON: ${message}`);
|
|
262
263
|
}
|
|
263
264
|
if (!Array.isArray(parsed)) {
|
|
264
|
-
throw new Error(`registry at ${
|
|
265
|
+
throw new Error(`registry at ${path5} must be a JSON array`);
|
|
265
266
|
}
|
|
266
267
|
return parsed.map((item, idx) => {
|
|
267
268
|
if (typeof item !== "object" || item === null) {
|
|
@@ -269,29 +270,29 @@ async function readRegistry() {
|
|
|
269
270
|
}
|
|
270
271
|
const e = item;
|
|
271
272
|
const name = typeof e.name === "string" ? e.name : "";
|
|
272
|
-
const
|
|
273
|
+
const path6 = typeof e.path === "string" ? e.path : "";
|
|
273
274
|
if (name.length === 0) {
|
|
274
275
|
throw new Error(`registry entry ${idx} is missing a non-empty "name"`);
|
|
275
276
|
}
|
|
276
|
-
if (
|
|
277
|
+
if (path6.length === 0) {
|
|
277
278
|
throw new Error(`registry entry ${idx} is missing a non-empty "path"`);
|
|
278
279
|
}
|
|
279
280
|
return {
|
|
280
281
|
name,
|
|
281
282
|
description: typeof e.description === "string" ? e.description : "",
|
|
282
|
-
path:
|
|
283
|
+
path: path6,
|
|
283
284
|
registered_at: typeof e.registered_at === "string" ? e.registered_at : ""
|
|
284
285
|
};
|
|
285
286
|
});
|
|
286
287
|
}
|
|
287
288
|
async function writeRegistry(entries) {
|
|
288
|
-
const
|
|
289
|
-
await mkdir(dirname3(
|
|
289
|
+
const path5 = getRegistryPath();
|
|
290
|
+
await mkdir(dirname3(path5), { recursive: true });
|
|
290
291
|
const body = `${JSON.stringify(entries, null, 2)}
|
|
291
292
|
`;
|
|
292
|
-
const tmpPath = `${
|
|
293
|
+
const tmpPath = `${path5}.tmp`;
|
|
293
294
|
await writeFile(tmpPath, body, "utf8");
|
|
294
|
-
await rename(tmpPath,
|
|
295
|
+
await rename(tmpPath, path5);
|
|
295
296
|
}
|
|
296
297
|
function pathsEqual(a, b) {
|
|
297
298
|
if (process.platform === "darwin" || process.platform === "win32") {
|
|
@@ -335,7 +336,7 @@ function isNodeError(err) {
|
|
|
335
336
|
return err instanceof Error && "code" in err;
|
|
336
337
|
}
|
|
337
338
|
|
|
338
|
-
// src/commands/
|
|
339
|
+
// src/commands/_init.ts
|
|
339
340
|
async function initWiki(options) {
|
|
340
341
|
const repoRoot = findNearestAlmanacDir(options.cwd) ?? options.cwd;
|
|
341
342
|
const almanacDir = getRepoAlmanacDir(repoRoot);
|
|
@@ -365,15 +366,15 @@ async function initWiki(options) {
|
|
|
365
366
|
return { entry, almanacDir, created: !alreadyExisted };
|
|
366
367
|
}
|
|
367
368
|
async function ensureGitignoreHasIndexDb(cwd) {
|
|
368
|
-
const
|
|
369
|
+
const path5 = join3(cwd, ".gitignore");
|
|
369
370
|
const targets = [
|
|
370
371
|
".almanac/index.db",
|
|
371
372
|
".almanac/index.db-wal",
|
|
372
373
|
".almanac/index.db-shm"
|
|
373
374
|
];
|
|
374
375
|
let existing = "";
|
|
375
|
-
if (existsSync3(
|
|
376
|
-
existing = await readFile3(
|
|
376
|
+
if (existsSync3(path5)) {
|
|
377
|
+
existing = await readFile3(path5, "utf8");
|
|
377
378
|
}
|
|
378
379
|
const lines = existing.split(/\r?\n/).map((l) => l.trim());
|
|
379
380
|
const missing = targets.filter((t) => !lines.includes(t));
|
|
@@ -383,7 +384,7 @@ async function ensureGitignoreHasIndexDb(cwd) {
|
|
|
383
384
|
${missing.join("\n")}
|
|
384
385
|
`;
|
|
385
386
|
const sep = existing.length === 0 ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
386
|
-
await writeFile2(
|
|
387
|
+
await writeFile2(path5, `${existing}${sep}${block}`, "utf8");
|
|
387
388
|
}
|
|
388
389
|
function starterReadme() {
|
|
389
390
|
return `# Wiki
|
|
@@ -986,8 +987,8 @@ async function filterTranscriptsByCwd(transcripts, repoRoot) {
|
|
|
986
987
|
}
|
|
987
988
|
return hits;
|
|
988
989
|
}
|
|
989
|
-
async function readHead(
|
|
990
|
-
const content = await readFile4(
|
|
990
|
+
async function readHead(path5, bytes) {
|
|
991
|
+
const content = await readFile4(path5, "utf8");
|
|
991
992
|
return content.length > bytes ? content.slice(0, bytes) : content;
|
|
992
993
|
}
|
|
993
994
|
async function snapshotPages(pagesDir) {
|
|
@@ -1294,13 +1295,13 @@ import { existsSync as existsSync7 } from "fs";
|
|
|
1294
1295
|
import { mkdir as mkdir4, readFile as readFile6, rename as rename3, writeFile as writeFile4 } from "fs/promises";
|
|
1295
1296
|
import { dirname as dirname4 } from "path";
|
|
1296
1297
|
import yaml2 from "js-yaml";
|
|
1297
|
-
async function loadTopicsFile(
|
|
1298
|
-
if (!existsSync7(
|
|
1298
|
+
async function loadTopicsFile(path5) {
|
|
1299
|
+
if (!existsSync7(path5)) {
|
|
1299
1300
|
return { topics: [] };
|
|
1300
1301
|
}
|
|
1301
1302
|
let raw;
|
|
1302
1303
|
try {
|
|
1303
|
-
raw = await readFile6(
|
|
1304
|
+
raw = await readFile6(path5, "utf8");
|
|
1304
1305
|
} catch (err) {
|
|
1305
1306
|
if (isNodeError2(err) && err.code === "ENOENT") {
|
|
1306
1307
|
return { topics: [] };
|
|
@@ -1316,13 +1317,13 @@ async function loadTopicsFile(path3) {
|
|
|
1316
1317
|
parsed = yaml2.load(raw);
|
|
1317
1318
|
} catch (err) {
|
|
1318
1319
|
const message = err instanceof Error ? err.message : String(err);
|
|
1319
|
-
throw new Error(`topics.yaml at ${
|
|
1320
|
+
throw new Error(`topics.yaml at ${path5} is not valid YAML: ${message}`);
|
|
1320
1321
|
}
|
|
1321
1322
|
if (parsed === null || parsed === void 0) {
|
|
1322
1323
|
return { topics: [] };
|
|
1323
1324
|
}
|
|
1324
1325
|
if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1325
|
-
throw new Error(`topics.yaml at ${
|
|
1326
|
+
throw new Error(`topics.yaml at ${path5} must be a mapping`);
|
|
1326
1327
|
}
|
|
1327
1328
|
const obj = parsed;
|
|
1328
1329
|
const rawTopics = obj.topics;
|
|
@@ -1330,7 +1331,7 @@ async function loadTopicsFile(path3) {
|
|
|
1330
1331
|
return { topics: [] };
|
|
1331
1332
|
}
|
|
1332
1333
|
if (!Array.isArray(rawTopics)) {
|
|
1333
|
-
throw new Error(`topics.yaml at ${
|
|
1334
|
+
throw new Error(`topics.yaml at ${path5} \u2014 "topics" must be a list`);
|
|
1334
1335
|
}
|
|
1335
1336
|
const topics = [];
|
|
1336
1337
|
for (const item of rawTopics) {
|
|
@@ -1359,7 +1360,7 @@ async function loadTopicsFile(path3) {
|
|
|
1359
1360
|
}
|
|
1360
1361
|
return { topics };
|
|
1361
1362
|
}
|
|
1362
|
-
async function writeTopicsFile(
|
|
1363
|
+
async function writeTopicsFile(path5, file) {
|
|
1363
1364
|
const sorted = [...file.topics].sort((a, b) => a.slug.localeCompare(b.slug));
|
|
1364
1365
|
const doc = {
|
|
1365
1366
|
topics: sorted.map((t) => {
|
|
@@ -1384,13 +1385,13 @@ async function writeTopicsFile(path3, file) {
|
|
|
1384
1385
|
sortKeys: false
|
|
1385
1386
|
});
|
|
1386
1387
|
const content = `${header}${body}`;
|
|
1387
|
-
const tmpPath = `${
|
|
1388
|
-
const parent = dirname4(
|
|
1388
|
+
const tmpPath = `${path5}.tmp`;
|
|
1389
|
+
const parent = dirname4(path5);
|
|
1389
1390
|
if (!existsSync7(parent)) {
|
|
1390
1391
|
await mkdir4(parent, { recursive: true });
|
|
1391
1392
|
}
|
|
1392
1393
|
await writeFile4(tmpPath, content, "utf8");
|
|
1393
|
-
await rename3(tmpPath,
|
|
1394
|
+
await rename3(tmpPath, path5);
|
|
1394
1395
|
}
|
|
1395
1396
|
function findTopic(file, slug) {
|
|
1396
1397
|
for (const t of file.topics) {
|
|
@@ -1539,10 +1540,10 @@ function classifyWikilink(raw) {
|
|
|
1539
1540
|
}
|
|
1540
1541
|
if (firstSlash !== -1) {
|
|
1541
1542
|
const isDir = looksLikeDir(body);
|
|
1542
|
-
const
|
|
1543
|
+
const path5 = normalizePath(body, isDir);
|
|
1543
1544
|
const originalPath = normalizePathPreservingCase(body, isDir);
|
|
1544
|
-
if (
|
|
1545
|
-
return isDir ? { kind: "folder", path:
|
|
1545
|
+
if (path5.length === 0) return null;
|
|
1546
|
+
return isDir ? { kind: "folder", path: path5, originalPath } : { kind: "file", path: path5, originalPath };
|
|
1546
1547
|
}
|
|
1547
1548
|
const target = toKebabCase(body);
|
|
1548
1549
|
if (target.length === 0) return null;
|
|
@@ -1762,10 +1763,10 @@ async function indexPagesInto(db, pagesDir) {
|
|
|
1762
1763
|
}
|
|
1763
1764
|
for (const raw of p.frontmatterFiles) {
|
|
1764
1765
|
const isDir = looksLikeDir(raw);
|
|
1765
|
-
const
|
|
1766
|
+
const path5 = normalizePath(raw, isDir);
|
|
1766
1767
|
const originalPath = normalizePathPreservingCase(raw, isDir);
|
|
1767
|
-
if (
|
|
1768
|
-
insertFileRef.run(p.slug,
|
|
1768
|
+
if (path5.length === 0) continue;
|
|
1769
|
+
insertFileRef.run(p.slug, path5, originalPath, isDir ? 1 : 0);
|
|
1769
1770
|
}
|
|
1770
1771
|
for (const ref of p.wikilinks) {
|
|
1771
1772
|
switch (ref.kind) {
|
|
@@ -1821,8 +1822,8 @@ function hashContent(raw) {
|
|
|
1821
1822
|
return createHash2("sha256").update(raw).digest("hex");
|
|
1822
1823
|
}
|
|
1823
1824
|
function topicsYamlNewerThan(almanacDir, dbPath) {
|
|
1824
|
-
const
|
|
1825
|
-
if (!existsSync8(
|
|
1825
|
+
const path5 = join6(almanacDir, "topics.yaml");
|
|
1826
|
+
if (!existsSync8(path5)) return false;
|
|
1826
1827
|
let dbMtime;
|
|
1827
1828
|
try {
|
|
1828
1829
|
dbMtime = statSync2(dbPath).mtimeMs;
|
|
@@ -1830,7 +1831,7 @@ function topicsYamlNewerThan(almanacDir, dbPath) {
|
|
|
1830
1831
|
return true;
|
|
1831
1832
|
}
|
|
1832
1833
|
try {
|
|
1833
|
-
const st = statSync2(
|
|
1834
|
+
const st = statSync2(path5);
|
|
1834
1835
|
return st.mtimeMs > dbMtime;
|
|
1835
1836
|
} catch {
|
|
1836
1837
|
return false;
|
|
@@ -2252,153 +2253,6 @@ function section(label, count, lines) {
|
|
|
2252
2253
|
${lines.join("\n")}`;
|
|
2253
2254
|
}
|
|
2254
2255
|
|
|
2255
|
-
// src/commands/info.ts
|
|
2256
|
-
import { join as join9 } from "path";
|
|
2257
|
-
async function runInfo(options) {
|
|
2258
|
-
const repoRoot = await resolveWikiRoot({
|
|
2259
|
-
cwd: options.cwd,
|
|
2260
|
-
wiki: options.wiki
|
|
2261
|
-
});
|
|
2262
|
-
await ensureFreshIndex({ repoRoot });
|
|
2263
|
-
const dbPath = join9(repoRoot, ".almanac", "index.db");
|
|
2264
|
-
const db = openIndex(dbPath);
|
|
2265
|
-
try {
|
|
2266
|
-
const slugs = collectSlugs(options);
|
|
2267
|
-
if (slugs.length === 0) {
|
|
2268
|
-
return {
|
|
2269
|
-
stdout: "",
|
|
2270
|
-
stderr: "almanac: info requires a slug (or --stdin)\n",
|
|
2271
|
-
exitCode: 1
|
|
2272
|
-
};
|
|
2273
|
-
}
|
|
2274
|
-
const records = [];
|
|
2275
|
-
const missing = [];
|
|
2276
|
-
for (const slug of slugs) {
|
|
2277
|
-
const rec = fetchInfo(db, slug);
|
|
2278
|
-
if (rec === null) {
|
|
2279
|
-
missing.push(slug);
|
|
2280
|
-
continue;
|
|
2281
|
-
}
|
|
2282
|
-
records.push(rec);
|
|
2283
|
-
}
|
|
2284
|
-
const bulk = options.stdin === true;
|
|
2285
|
-
const jsonOut = options.json === true || bulk;
|
|
2286
|
-
let stdout;
|
|
2287
|
-
if (jsonOut) {
|
|
2288
|
-
if (bulk) {
|
|
2289
|
-
stdout = `${JSON.stringify(records, null, 2)}
|
|
2290
|
-
`;
|
|
2291
|
-
} else {
|
|
2292
|
-
const only = records[0] ?? null;
|
|
2293
|
-
stdout = `${JSON.stringify(only, null, 2)}
|
|
2294
|
-
`;
|
|
2295
|
-
}
|
|
2296
|
-
} else {
|
|
2297
|
-
stdout = records.map(formatHumanReadable).join("\n");
|
|
2298
|
-
}
|
|
2299
|
-
const stderr = missing.map((s) => `almanac: no such page "${s}"
|
|
2300
|
-
`).join("");
|
|
2301
|
-
return {
|
|
2302
|
-
stdout,
|
|
2303
|
-
stderr,
|
|
2304
|
-
exitCode: missing.length > 0 ? 1 : 0
|
|
2305
|
-
};
|
|
2306
|
-
} finally {
|
|
2307
|
-
db.close();
|
|
2308
|
-
}
|
|
2309
|
-
}
|
|
2310
|
-
function fetchInfo(db, slug) {
|
|
2311
|
-
const pageRow = db.prepare(
|
|
2312
|
-
"SELECT slug, title, file_path, updated_at, archived_at, superseded_by FROM pages WHERE slug = ?"
|
|
2313
|
-
).get(slug);
|
|
2314
|
-
if (pageRow === void 0) return null;
|
|
2315
|
-
const topics = db.prepare(
|
|
2316
|
-
"SELECT topic_slug FROM page_topics WHERE page_slug = ? ORDER BY topic_slug"
|
|
2317
|
-
).all(slug).map((r) => r.topic_slug);
|
|
2318
|
-
const refs = db.prepare(
|
|
2319
|
-
// Display the author's casing (`original_path`), not the
|
|
2320
|
-
// lowercased lookup form. The lowercased `path` column is the
|
|
2321
|
-
// query key for `--mentions`; it's not a user-facing string.
|
|
2322
|
-
"SELECT original_path, is_dir FROM file_refs WHERE page_slug = ? ORDER BY original_path"
|
|
2323
|
-
).all(slug).map((r) => ({ path: r.original_path, is_dir: r.is_dir === 1 }));
|
|
2324
|
-
const linksOut = db.prepare(
|
|
2325
|
-
"SELECT target_slug FROM wikilinks WHERE source_slug = ? ORDER BY target_slug"
|
|
2326
|
-
).all(slug).map((r) => r.target_slug);
|
|
2327
|
-
const linksIn = db.prepare(
|
|
2328
|
-
"SELECT source_slug FROM wikilinks WHERE target_slug = ? ORDER BY source_slug"
|
|
2329
|
-
).all(slug).map((r) => r.source_slug);
|
|
2330
|
-
const xwiki = db.prepare(
|
|
2331
|
-
"SELECT target_wiki, target_slug FROM cross_wiki_links WHERE source_slug = ? ORDER BY target_wiki, target_slug"
|
|
2332
|
-
).all(slug).map((r) => ({ wiki: r.target_wiki, target: r.target_slug }));
|
|
2333
|
-
const supersedesRows = db.prepare(
|
|
2334
|
-
"SELECT slug FROM pages WHERE superseded_by = ? ORDER BY slug"
|
|
2335
|
-
).all(slug).map((r) => r.slug);
|
|
2336
|
-
return {
|
|
2337
|
-
slug: pageRow.slug,
|
|
2338
|
-
title: pageRow.title,
|
|
2339
|
-
file_path: pageRow.file_path,
|
|
2340
|
-
updated_at: pageRow.updated_at,
|
|
2341
|
-
archived_at: pageRow.archived_at,
|
|
2342
|
-
superseded_by: pageRow.superseded_by,
|
|
2343
|
-
supersedes: supersedesRows,
|
|
2344
|
-
topics,
|
|
2345
|
-
file_refs: refs,
|
|
2346
|
-
wikilinks_out: linksOut,
|
|
2347
|
-
wikilinks_in: linksIn,
|
|
2348
|
-
cross_wiki_links: xwiki
|
|
2349
|
-
};
|
|
2350
|
-
}
|
|
2351
|
-
function collectSlugs(options) {
|
|
2352
|
-
if (options.stdin === true && options.stdinInput !== void 0) {
|
|
2353
|
-
return options.stdinInput.split(/\r?\n/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
2354
|
-
}
|
|
2355
|
-
if (options.slug !== void 0 && options.slug.length > 0) {
|
|
2356
|
-
return [options.slug];
|
|
2357
|
-
}
|
|
2358
|
-
return [];
|
|
2359
|
-
}
|
|
2360
|
-
function formatHumanReadable(rec) {
|
|
2361
|
-
const lines = [];
|
|
2362
|
-
lines.push(`slug: ${rec.slug}`);
|
|
2363
|
-
lines.push(`title: ${rec.title ?? "\u2014"}`);
|
|
2364
|
-
lines.push(`file: ${rec.file_path}`);
|
|
2365
|
-
lines.push(`updated_at: ${new Date(rec.updated_at * 1e3).toISOString()}`);
|
|
2366
|
-
if (rec.archived_at !== null) {
|
|
2367
|
-
lines.push(
|
|
2368
|
-
`archived_at: ${new Date(rec.archived_at * 1e3).toISOString()}`
|
|
2369
|
-
);
|
|
2370
|
-
}
|
|
2371
|
-
if (rec.superseded_by !== null) {
|
|
2372
|
-
lines.push(`superseded_by: ${rec.superseded_by}`);
|
|
2373
|
-
}
|
|
2374
|
-
if (rec.supersedes.length > 0) {
|
|
2375
|
-
lines.push(`supersedes: ${rec.supersedes.join(", ")}`);
|
|
2376
|
-
}
|
|
2377
|
-
lines.push(`topics: ${rec.topics.length > 0 ? rec.topics.join(", ") : "\u2014"}`);
|
|
2378
|
-
lines.push("file_refs:");
|
|
2379
|
-
if (rec.file_refs.length === 0) {
|
|
2380
|
-
lines.push(" \u2014");
|
|
2381
|
-
} else {
|
|
2382
|
-
for (const r of rec.file_refs) {
|
|
2383
|
-
lines.push(` ${r.path}${r.is_dir ? " (dir)" : ""}`);
|
|
2384
|
-
}
|
|
2385
|
-
}
|
|
2386
|
-
lines.push("wikilinks_out:");
|
|
2387
|
-
if (rec.wikilinks_out.length === 0) lines.push(" \u2014");
|
|
2388
|
-
else for (const t of rec.wikilinks_out) lines.push(` ${t}`);
|
|
2389
|
-
lines.push("wikilinks_in:");
|
|
2390
|
-
if (rec.wikilinks_in.length === 0) lines.push(" \u2014");
|
|
2391
|
-
else for (const s of rec.wikilinks_in) lines.push(` ${s}`);
|
|
2392
|
-
if (rec.cross_wiki_links.length > 0) {
|
|
2393
|
-
lines.push("cross_wiki_links:");
|
|
2394
|
-
for (const x of rec.cross_wiki_links) {
|
|
2395
|
-
lines.push(` ${x.wiki}:${x.target}`);
|
|
2396
|
-
}
|
|
2397
|
-
}
|
|
2398
|
-
return `${lines.join("\n")}
|
|
2399
|
-
`;
|
|
2400
|
-
}
|
|
2401
|
-
|
|
2402
2256
|
// src/commands/list.ts
|
|
2403
2257
|
import { existsSync as existsSync11 } from "fs";
|
|
2404
2258
|
async function listWikis(options) {
|
|
@@ -2451,61 +2305,6 @@ function formatPretty(entries) {
|
|
|
2451
2305
|
`;
|
|
2452
2306
|
}
|
|
2453
2307
|
|
|
2454
|
-
// src/commands/path.ts
|
|
2455
|
-
import { join as join10 } from "path";
|
|
2456
|
-
async function runPath(options) {
|
|
2457
|
-
const repoRoot = await resolveWikiRoot({
|
|
2458
|
-
cwd: options.cwd,
|
|
2459
|
-
wiki: options.wiki
|
|
2460
|
-
});
|
|
2461
|
-
await ensureFreshIndex({ repoRoot });
|
|
2462
|
-
const dbPath = join10(repoRoot, ".almanac", "index.db");
|
|
2463
|
-
const db = openIndex(dbPath);
|
|
2464
|
-
try {
|
|
2465
|
-
const slugs = collectSlugs2(options);
|
|
2466
|
-
if (slugs.length === 0) {
|
|
2467
|
-
return {
|
|
2468
|
-
stdout: "",
|
|
2469
|
-
stderr: "almanac: path requires a slug (or --stdin)\n",
|
|
2470
|
-
exitCode: 1
|
|
2471
|
-
};
|
|
2472
|
-
}
|
|
2473
|
-
const stmt = db.prepare(
|
|
2474
|
-
"SELECT file_path FROM pages WHERE slug = ?"
|
|
2475
|
-
);
|
|
2476
|
-
const resolved = [];
|
|
2477
|
-
const missing = [];
|
|
2478
|
-
for (const slug of slugs) {
|
|
2479
|
-
const row = stmt.get(slug);
|
|
2480
|
-
if (row === void 0) {
|
|
2481
|
-
missing.push(slug);
|
|
2482
|
-
continue;
|
|
2483
|
-
}
|
|
2484
|
-
resolved.push(row.file_path);
|
|
2485
|
-
}
|
|
2486
|
-
const stdout = resolved.length > 0 ? `${resolved.join("\n")}
|
|
2487
|
-
` : "";
|
|
2488
|
-
const stderr = missing.map((s) => `almanac: no such page "${s}"
|
|
2489
|
-
`).join("");
|
|
2490
|
-
return {
|
|
2491
|
-
stdout,
|
|
2492
|
-
stderr,
|
|
2493
|
-
exitCode: missing.length > 0 ? 1 : 0
|
|
2494
|
-
};
|
|
2495
|
-
} finally {
|
|
2496
|
-
db.close();
|
|
2497
|
-
}
|
|
2498
|
-
}
|
|
2499
|
-
function collectSlugs2(options) {
|
|
2500
|
-
if (options.stdin === true && options.stdinInput !== void 0) {
|
|
2501
|
-
return options.stdinInput.split(/\r?\n/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
2502
|
-
}
|
|
2503
|
-
if (options.slug !== void 0 && options.slug.length > 0) {
|
|
2504
|
-
return [options.slug];
|
|
2505
|
-
}
|
|
2506
|
-
return [];
|
|
2507
|
-
}
|
|
2508
|
-
|
|
2509
2308
|
// src/commands/reindex.ts
|
|
2510
2309
|
async function runReindex(options) {
|
|
2511
2310
|
const repoRoot = await resolveWikiRoot({
|
|
@@ -2520,14 +2319,14 @@ async function runReindex(options) {
|
|
|
2520
2319
|
}
|
|
2521
2320
|
|
|
2522
2321
|
// src/commands/search.ts
|
|
2523
|
-
import { join as
|
|
2322
|
+
import { join as join9 } from "path";
|
|
2524
2323
|
async function runSearch(options) {
|
|
2525
2324
|
const repoRoot = await resolveWikiRoot({
|
|
2526
2325
|
cwd: options.cwd,
|
|
2527
2326
|
wiki: options.wiki
|
|
2528
2327
|
});
|
|
2529
2328
|
await ensureFreshIndex({ repoRoot });
|
|
2530
|
-
const dbPath =
|
|
2329
|
+
const dbPath = join9(repoRoot, ".almanac", "index.db");
|
|
2531
2330
|
const db = openIndex(dbPath);
|
|
2532
2331
|
try {
|
|
2533
2332
|
const rows = executeQuery(db, options);
|
|
@@ -2695,19 +2494,313 @@ function buildStderr(rows, options) {
|
|
|
2695
2494
|
return "";
|
|
2696
2495
|
}
|
|
2697
2496
|
|
|
2497
|
+
// src/commands/setup.ts
|
|
2498
|
+
import { existsSync as existsSync12 } from "fs";
|
|
2499
|
+
import {
|
|
2500
|
+
copyFile,
|
|
2501
|
+
mkdir as mkdir5,
|
|
2502
|
+
readFile as readFile9,
|
|
2503
|
+
writeFile as writeFile5
|
|
2504
|
+
} from "fs/promises";
|
|
2505
|
+
import { createRequire as createRequire2 } from "module";
|
|
2506
|
+
import { homedir as homedir4 } from "os";
|
|
2507
|
+
import path3 from "path";
|
|
2508
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
2509
|
+
var RST = "\x1B[0m";
|
|
2510
|
+
var BOLD = "\x1B[1m";
|
|
2511
|
+
var DIM = "\x1B[2m";
|
|
2512
|
+
var WHITE_BOLD = "\x1B[1;37m";
|
|
2513
|
+
var BLUE = "\x1B[38;5;75m";
|
|
2514
|
+
var BLUE_DIM = "\x1B[38;5;69m";
|
|
2515
|
+
var ACCENT_BG = "\x1B[48;5;252m\x1B[38;5;16m";
|
|
2516
|
+
var GRADIENT = [
|
|
2517
|
+
"\x1B[38;5;255m",
|
|
2518
|
+
"\x1B[38;5;253m",
|
|
2519
|
+
"\x1B[38;5;251m",
|
|
2520
|
+
"\x1B[38;5;249m",
|
|
2521
|
+
"\x1B[38;5;246m",
|
|
2522
|
+
"\x1B[38;5;243m"
|
|
2523
|
+
];
|
|
2524
|
+
var LOGO_LINES = [
|
|
2525
|
+
" ___ ___ ___ ___ _ _ __ __ _ _ _ _ ___ ",
|
|
2526
|
+
" / __/ _ \\| \\| __| /_\\ | | | \\/ | /_\\ | \\| | /_\\ / __|",
|
|
2527
|
+
"| (_| (_) | |) | _| / _ \\| |__| |\\/| |/ _ \\| .` |/ _ \\ (__ ",
|
|
2528
|
+
" \\___\\___/|___/|___/_/ \\_\\____|_| |_/_/ \\_\\_|\\_/_/ \\_\\___|",
|
|
2529
|
+
" ",
|
|
2530
|
+
" a living wiki for codebases, for your agent "
|
|
2531
|
+
];
|
|
2532
|
+
var BAR = ` ${DIM}\u2502${RST}`;
|
|
2533
|
+
function printBanner(out) {
|
|
2534
|
+
out.write("\n");
|
|
2535
|
+
for (let i = 0; i < LOGO_LINES.length; i++) {
|
|
2536
|
+
const color = GRADIENT[Math.min(i, GRADIENT.length - 1)] ?? "";
|
|
2537
|
+
out.write(`${color}${LOGO_LINES[i]}${RST}
|
|
2538
|
+
`);
|
|
2539
|
+
}
|
|
2540
|
+
out.write(`
|
|
2541
|
+
${WHITE_BOLD} Install the hook + agent guides${RST}
|
|
2542
|
+
`);
|
|
2543
|
+
}
|
|
2544
|
+
function printBadge(out) {
|
|
2545
|
+
out.write(`
|
|
2546
|
+
${ACCENT_BG} codealmanac ${RST}
|
|
2547
|
+
|
|
2548
|
+
`);
|
|
2549
|
+
}
|
|
2550
|
+
function stepDone(out, msg) {
|
|
2551
|
+
out.write(` ${BLUE}\u25C7${RST} ${msg}
|
|
2552
|
+
`);
|
|
2553
|
+
}
|
|
2554
|
+
function stepActive(out, msg) {
|
|
2555
|
+
out.write(` ${BLUE}\u25C6${RST} ${msg}
|
|
2556
|
+
`);
|
|
2557
|
+
}
|
|
2558
|
+
function stepSkipped(out, msg) {
|
|
2559
|
+
out.write(` ${DIM}\u25CB ${msg}${RST}
|
|
2560
|
+
`);
|
|
2561
|
+
}
|
|
2562
|
+
async function runSetup(options = {}) {
|
|
2563
|
+
const out = options.stdout ?? process.stdout;
|
|
2564
|
+
const isTTY = options.isTTY ?? process.stdin.isTTY === true;
|
|
2565
|
+
const interactive = isTTY && options.yes !== true;
|
|
2566
|
+
printBanner(out);
|
|
2567
|
+
printBadge(out);
|
|
2568
|
+
const auth = await safeCheckAuth(options.spawnCli);
|
|
2569
|
+
reportAuth(out, auth);
|
|
2570
|
+
out.write(BAR + "\n");
|
|
2571
|
+
let hookAction = "install";
|
|
2572
|
+
if (options.skipHook === true) {
|
|
2573
|
+
hookAction = "skip";
|
|
2574
|
+
} else if (interactive) {
|
|
2575
|
+
hookAction = await confirm(
|
|
2576
|
+
out,
|
|
2577
|
+
"Install the SessionEnd hook so capture runs at the end of every Claude Code session?",
|
|
2578
|
+
true
|
|
2579
|
+
);
|
|
2580
|
+
}
|
|
2581
|
+
let hookResultLine = "";
|
|
2582
|
+
if (hookAction === "install") {
|
|
2583
|
+
const res = await runHookInstall({
|
|
2584
|
+
settingsPath: options.settingsPath,
|
|
2585
|
+
hookScriptPath: options.hookScriptPath
|
|
2586
|
+
});
|
|
2587
|
+
if (res.exitCode !== 0) {
|
|
2588
|
+
stepActive(out, `SessionEnd hook: ${res.stderr.trim()}`);
|
|
2589
|
+
return {
|
|
2590
|
+
stdout: "",
|
|
2591
|
+
stderr: res.stderr,
|
|
2592
|
+
exitCode: res.exitCode
|
|
2593
|
+
};
|
|
2594
|
+
}
|
|
2595
|
+
hookResultLine = res.stdout.includes("already installed") ? `SessionEnd hook ${DIM}already installed${RST}` : `SessionEnd hook installed`;
|
|
2596
|
+
stepDone(out, hookResultLine);
|
|
2597
|
+
} else {
|
|
2598
|
+
stepSkipped(out, `SessionEnd hook ${DIM}skipped${RST}`);
|
|
2599
|
+
}
|
|
2600
|
+
out.write(BAR + "\n");
|
|
2601
|
+
let guidesAction = "install";
|
|
2602
|
+
if (options.skipGuides === true) {
|
|
2603
|
+
guidesAction = "skip";
|
|
2604
|
+
} else if (interactive) {
|
|
2605
|
+
guidesAction = await confirm(
|
|
2606
|
+
out,
|
|
2607
|
+
"Install the codealmanac usage guides into ~/.claude/ and import them from CLAUDE.md?",
|
|
2608
|
+
true
|
|
2609
|
+
);
|
|
2610
|
+
}
|
|
2611
|
+
let guidesSummary;
|
|
2612
|
+
if (guidesAction === "install") {
|
|
2613
|
+
try {
|
|
2614
|
+
const summary = await installGuides({
|
|
2615
|
+
claudeDir: options.claudeDir ?? path3.join(homedir4(), ".claude"),
|
|
2616
|
+
guidesDir: options.guidesDir ?? resolveGuidesDir()
|
|
2617
|
+
});
|
|
2618
|
+
guidesSummary = summary.anyChanges ? `Guides installed (${summary.filesWritten.join(", ")})` : `Guides ${DIM}already installed${RST}`;
|
|
2619
|
+
stepDone(out, guidesSummary);
|
|
2620
|
+
} catch (err) {
|
|
2621
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2622
|
+
return {
|
|
2623
|
+
stdout: "",
|
|
2624
|
+
stderr: `almanac: guide install failed: ${msg}
|
|
2625
|
+
`,
|
|
2626
|
+
exitCode: 1
|
|
2627
|
+
};
|
|
2628
|
+
}
|
|
2629
|
+
} else {
|
|
2630
|
+
stepSkipped(out, `Guides ${DIM}skipped${RST}`);
|
|
2631
|
+
}
|
|
2632
|
+
out.write(BAR + "\n");
|
|
2633
|
+
stepDone(out, `${BLUE}Setup complete${RST}`);
|
|
2634
|
+
out.write("\n");
|
|
2635
|
+
printNextSteps(out);
|
|
2636
|
+
return { stdout: "", stderr: "", exitCode: 0 };
|
|
2637
|
+
}
|
|
2638
|
+
async function safeCheckAuth(spawnCli) {
|
|
2639
|
+
try {
|
|
2640
|
+
return await checkClaudeAuth(spawnCli);
|
|
2641
|
+
} catch {
|
|
2642
|
+
return { loggedIn: false };
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
function reportAuth(out, auth) {
|
|
2646
|
+
if (auth.loggedIn) {
|
|
2647
|
+
const who = auth.email ?? "Claude account";
|
|
2648
|
+
const plan = auth.subscriptionType !== void 0 ? ` ${DIM}(${auth.subscriptionType})${RST}` : "";
|
|
2649
|
+
stepDone(out, `Claude auth: ${WHITE_BOLD}${who}${RST}${plan}`);
|
|
2650
|
+
return;
|
|
2651
|
+
}
|
|
2652
|
+
if (process.env.ANTHROPIC_API_KEY !== void 0 && process.env.ANTHROPIC_API_KEY.length > 0) {
|
|
2653
|
+
stepDone(out, `Claude auth: ${WHITE_BOLD}ANTHROPIC_API_KEY${RST} set`);
|
|
2654
|
+
return;
|
|
2655
|
+
}
|
|
2656
|
+
stepActive(out, `Claude auth: ${DIM}not signed in${RST}`);
|
|
2657
|
+
for (const line of UNAUTHENTICATED_MESSAGE.split("\n")) {
|
|
2658
|
+
out.write(` ${DIM}\u2502 ${line}${RST}
|
|
2659
|
+
`);
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
async function installGuides(options) {
|
|
2663
|
+
await mkdir5(options.claudeDir, { recursive: true });
|
|
2664
|
+
const srcMini = path3.join(options.guidesDir, "mini.md");
|
|
2665
|
+
const srcRef = path3.join(options.guidesDir, "reference.md");
|
|
2666
|
+
if (!existsSync12(srcMini)) {
|
|
2667
|
+
throw new Error(`missing bundled guide: ${srcMini}`);
|
|
2668
|
+
}
|
|
2669
|
+
if (!existsSync12(srcRef)) {
|
|
2670
|
+
throw new Error(`missing bundled guide: ${srcRef}`);
|
|
2671
|
+
}
|
|
2672
|
+
const destMini = path3.join(options.claudeDir, "codealmanac.md");
|
|
2673
|
+
const destRef = path3.join(options.claudeDir, "codealmanac-reference.md");
|
|
2674
|
+
const miniChanged = await copyIfChanged(srcMini, destMini);
|
|
2675
|
+
const refChanged = await copyIfChanged(srcRef, destRef);
|
|
2676
|
+
const claudeMd = path3.join(options.claudeDir, "CLAUDE.md");
|
|
2677
|
+
const importChanged = await ensureImport(claudeMd);
|
|
2678
|
+
const filesWritten = [];
|
|
2679
|
+
if (miniChanged) filesWritten.push("codealmanac.md");
|
|
2680
|
+
if (refChanged) filesWritten.push("codealmanac-reference.md");
|
|
2681
|
+
if (importChanged) filesWritten.push("CLAUDE.md");
|
|
2682
|
+
return { anyChanges: filesWritten.length > 0, filesWritten };
|
|
2683
|
+
}
|
|
2684
|
+
async function copyIfChanged(src, dest) {
|
|
2685
|
+
const srcBytes = await readFile9(src);
|
|
2686
|
+
if (existsSync12(dest)) {
|
|
2687
|
+
try {
|
|
2688
|
+
const destBytes = await readFile9(dest);
|
|
2689
|
+
if (srcBytes.equals(destBytes)) return false;
|
|
2690
|
+
} catch {
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
await copyFile(src, dest);
|
|
2694
|
+
return true;
|
|
2695
|
+
}
|
|
2696
|
+
var IMPORT_LINE = "@~/.claude/codealmanac.md";
|
|
2697
|
+
async function ensureImport(claudeMdPath) {
|
|
2698
|
+
let existing = "";
|
|
2699
|
+
if (existsSync12(claudeMdPath)) {
|
|
2700
|
+
existing = await readFile9(claudeMdPath, "utf8");
|
|
2701
|
+
}
|
|
2702
|
+
if (hasImportLine(existing)) return false;
|
|
2703
|
+
const sep = existing.length === 0 ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
2704
|
+
const body = `${existing}${sep}${IMPORT_LINE}
|
|
2705
|
+
`;
|
|
2706
|
+
await writeFile5(claudeMdPath, body, "utf8");
|
|
2707
|
+
return true;
|
|
2708
|
+
}
|
|
2709
|
+
function hasImportLine(contents) {
|
|
2710
|
+
const lines = contents.split(/\r?\n/).map((l) => l.trim());
|
|
2711
|
+
return lines.includes(IMPORT_LINE);
|
|
2712
|
+
}
|
|
2713
|
+
function confirm(out, question, defaultYes) {
|
|
2714
|
+
return new Promise((resolve2) => {
|
|
2715
|
+
const hint = defaultYes ? "[Y/n]" : "[y/N]";
|
|
2716
|
+
out.write(` ${BLUE}\u25C6${RST} ${question} ${DIM}${hint}${RST} `);
|
|
2717
|
+
let buf = "";
|
|
2718
|
+
const onData = (chunk) => {
|
|
2719
|
+
buf += chunk.toString("utf8");
|
|
2720
|
+
const nl = buf.indexOf("\n");
|
|
2721
|
+
if (nl === -1) return;
|
|
2722
|
+
process.stdin.removeListener("data", onData);
|
|
2723
|
+
process.stdin.pause();
|
|
2724
|
+
const answer = buf.slice(0, nl).trim().toLowerCase();
|
|
2725
|
+
const accepted = answer.length === 0 ? defaultYes : answer === "y" || answer === "yes";
|
|
2726
|
+
resolve2(accepted ? "install" : "skip");
|
|
2727
|
+
};
|
|
2728
|
+
process.stdin.resume();
|
|
2729
|
+
process.stdin.on("data", onData);
|
|
2730
|
+
});
|
|
2731
|
+
}
|
|
2732
|
+
function printNextSteps(out) {
|
|
2733
|
+
const innerW = 62;
|
|
2734
|
+
const vis = (s) => s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
2735
|
+
const row = (content) => {
|
|
2736
|
+
const padding = Math.max(0, innerW - vis(content));
|
|
2737
|
+
return ` ${BLUE_DIM}\u2502${RST}${content}${" ".repeat(padding)}${BLUE_DIM}\u2502${RST}
|
|
2738
|
+
`;
|
|
2739
|
+
};
|
|
2740
|
+
const empty = row("");
|
|
2741
|
+
out.write(` ${BLUE_DIM}\u256D${"\u2500".repeat(innerW)}\u256E${RST}
|
|
2742
|
+
`);
|
|
2743
|
+
out.write(empty);
|
|
2744
|
+
out.write(row(` ${WHITE_BOLD}Next steps${RST}`));
|
|
2745
|
+
out.write(empty);
|
|
2746
|
+
out.write(
|
|
2747
|
+
row(` ${BLUE}1.${RST} ${BOLD}cd${RST} into a repo you want to document`)
|
|
2748
|
+
);
|
|
2749
|
+
out.write(
|
|
2750
|
+
row(
|
|
2751
|
+
` ${BLUE}2.${RST} ${BOLD}almanac bootstrap${RST} ${DIM}# scaffold the wiki${RST}`
|
|
2752
|
+
)
|
|
2753
|
+
);
|
|
2754
|
+
out.write(
|
|
2755
|
+
row(
|
|
2756
|
+
` ${BLUE}3.${RST} Work normally \u2014 capture runs on session end`
|
|
2757
|
+
)
|
|
2758
|
+
);
|
|
2759
|
+
out.write(empty);
|
|
2760
|
+
out.write(` ${BLUE_DIM}\u2570${"\u2500".repeat(innerW)}\u256F${RST}
|
|
2761
|
+
|
|
2762
|
+
`);
|
|
2763
|
+
}
|
|
2764
|
+
function resolveGuidesDir() {
|
|
2765
|
+
const here = path3.dirname(fileURLToPath3(import.meta.url));
|
|
2766
|
+
const candidates = [
|
|
2767
|
+
path3.resolve(here, "..", "guides"),
|
|
2768
|
+
// dist layout
|
|
2769
|
+
path3.resolve(here, "..", "..", "guides"),
|
|
2770
|
+
// src layout
|
|
2771
|
+
path3.resolve(here, "..", "..", "..", "guides")
|
|
2772
|
+
];
|
|
2773
|
+
for (const dir of candidates) {
|
|
2774
|
+
if (looksLikeGuidesDir(dir)) return dir;
|
|
2775
|
+
}
|
|
2776
|
+
try {
|
|
2777
|
+
const require2 = createRequire2(import.meta.url);
|
|
2778
|
+
const pkgJson = require2.resolve("codealmanac/package.json");
|
|
2779
|
+
const guides = path3.join(path3.dirname(pkgJson), "guides");
|
|
2780
|
+
if (looksLikeGuidesDir(guides)) return guides;
|
|
2781
|
+
} catch {
|
|
2782
|
+
}
|
|
2783
|
+
throw new Error(
|
|
2784
|
+
"could not locate bundled guides/ directory. Tried:\n" + candidates.map((c) => ` - ${c}`).join("\n")
|
|
2785
|
+
);
|
|
2786
|
+
}
|
|
2787
|
+
function looksLikeGuidesDir(dir) {
|
|
2788
|
+
return existsSync12(path3.join(dir, "mini.md"));
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2698
2791
|
// src/commands/show.ts
|
|
2699
|
-
import { readFile as
|
|
2700
|
-
import { join as
|
|
2792
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
2793
|
+
import { join as join10 } from "path";
|
|
2701
2794
|
async function runShow(options) {
|
|
2702
2795
|
const repoRoot = await resolveWikiRoot({
|
|
2703
2796
|
cwd: options.cwd,
|
|
2704
2797
|
wiki: options.wiki
|
|
2705
2798
|
});
|
|
2706
2799
|
await ensureFreshIndex({ repoRoot });
|
|
2707
|
-
const dbPath =
|
|
2800
|
+
const dbPath = join10(repoRoot, ".almanac", "index.db");
|
|
2708
2801
|
const db = openIndex(dbPath);
|
|
2709
2802
|
try {
|
|
2710
|
-
const slugs =
|
|
2803
|
+
const slugs = collectSlugs(options);
|
|
2711
2804
|
if (slugs.length === 0) {
|
|
2712
2805
|
return {
|
|
2713
2806
|
stdout: "",
|
|
@@ -2715,44 +2808,289 @@ async function runShow(options) {
|
|
|
2715
2808
|
exitCode: 1
|
|
2716
2809
|
};
|
|
2717
2810
|
}
|
|
2718
|
-
const stmt = db.prepare(
|
|
2719
|
-
"SELECT file_path FROM pages WHERE slug = ?"
|
|
2720
|
-
);
|
|
2721
2811
|
const records = [];
|
|
2722
2812
|
const missing = [];
|
|
2723
2813
|
for (const slug of slugs) {
|
|
2724
|
-
const
|
|
2725
|
-
if (
|
|
2814
|
+
const rec = await fetchRecord(db, slug);
|
|
2815
|
+
if (rec === null) {
|
|
2726
2816
|
missing.push(slug);
|
|
2727
2817
|
continue;
|
|
2728
2818
|
}
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2819
|
+
records.push(rec);
|
|
2820
|
+
}
|
|
2821
|
+
const bulk = options.stdin === true;
|
|
2822
|
+
const stdout = bulk ? formatBulk(records) : formatSingle(records, options);
|
|
2823
|
+
const stderr = missing.map((s) => `almanac: no such page "${s}"
|
|
2824
|
+
`).join("");
|
|
2825
|
+
return {
|
|
2826
|
+
stdout,
|
|
2827
|
+
stderr,
|
|
2828
|
+
exitCode: missing.length > 0 ? 1 : 0
|
|
2829
|
+
};
|
|
2830
|
+
} finally {
|
|
2831
|
+
db.close();
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
async function fetchRecord(db, slug) {
|
|
2835
|
+
const pageRow = db.prepare(
|
|
2836
|
+
"SELECT slug, title, file_path, updated_at, archived_at, superseded_by FROM pages WHERE slug = ?"
|
|
2837
|
+
).get(slug);
|
|
2838
|
+
if (pageRow === void 0) return null;
|
|
2839
|
+
const topics = db.prepare(
|
|
2840
|
+
"SELECT topic_slug FROM page_topics WHERE page_slug = ? ORDER BY topic_slug"
|
|
2841
|
+
).all(slug).map((r) => r.topic_slug);
|
|
2842
|
+
const refs = db.prepare(
|
|
2843
|
+
"SELECT original_path, is_dir FROM file_refs WHERE page_slug = ? ORDER BY original_path"
|
|
2844
|
+
).all(slug).map((r) => ({ path: r.original_path, is_dir: r.is_dir === 1 }));
|
|
2845
|
+
const linksOut = db.prepare(
|
|
2846
|
+
"SELECT target_slug FROM wikilinks WHERE source_slug = ? ORDER BY target_slug"
|
|
2847
|
+
).all(slug).map((r) => r.target_slug);
|
|
2848
|
+
const linksIn = db.prepare(
|
|
2849
|
+
"SELECT source_slug FROM wikilinks WHERE target_slug = ? ORDER BY source_slug"
|
|
2850
|
+
).all(slug).map((r) => r.source_slug);
|
|
2851
|
+
const xwiki = db.prepare(
|
|
2852
|
+
"SELECT target_wiki, target_slug FROM cross_wiki_links WHERE source_slug = ? ORDER BY target_wiki, target_slug"
|
|
2853
|
+
).all(slug).map((r) => ({ wiki: r.target_wiki, target: r.target_slug }));
|
|
2854
|
+
const supersedesRows = db.prepare(
|
|
2855
|
+
"SELECT slug FROM pages WHERE superseded_by = ? ORDER BY slug"
|
|
2856
|
+
).all(slug).map((r) => r.slug);
|
|
2857
|
+
let body = "";
|
|
2858
|
+
try {
|
|
2859
|
+
body = stripFrontmatter(await readFile10(pageRow.file_path, "utf8"));
|
|
2860
|
+
} catch {
|
|
2861
|
+
}
|
|
2862
|
+
return {
|
|
2863
|
+
slug: pageRow.slug,
|
|
2864
|
+
title: pageRow.title,
|
|
2865
|
+
file_path: pageRow.file_path,
|
|
2866
|
+
updated_at: pageRow.updated_at,
|
|
2867
|
+
archived_at: pageRow.archived_at,
|
|
2868
|
+
superseded_by: pageRow.superseded_by,
|
|
2869
|
+
supersedes: supersedesRows,
|
|
2870
|
+
topics,
|
|
2871
|
+
file_refs: refs,
|
|
2872
|
+
wikilinks_out: linksOut,
|
|
2873
|
+
wikilinks_in: linksIn,
|
|
2874
|
+
cross_wiki_links: xwiki,
|
|
2875
|
+
body
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
function formatBulk(records) {
|
|
2879
|
+
if (records.length === 0) return "";
|
|
2880
|
+
return records.map((r) => JSON.stringify(r)).join("\n") + "\n";
|
|
2881
|
+
}
|
|
2882
|
+
function formatSingle(records, options) {
|
|
2883
|
+
if (options.json === true) {
|
|
2884
|
+
const only = records[0] ?? null;
|
|
2885
|
+
return `${JSON.stringify(only, null, 2)}
|
|
2886
|
+
`;
|
|
2887
|
+
}
|
|
2888
|
+
return records.map((r) => formatRecord(r, options)).join("");
|
|
2889
|
+
}
|
|
2890
|
+
var FIELD_ORDER = [
|
|
2891
|
+
"title",
|
|
2892
|
+
"topics",
|
|
2893
|
+
"files",
|
|
2894
|
+
"links",
|
|
2895
|
+
"backlinks",
|
|
2896
|
+
"xwiki",
|
|
2897
|
+
"lineage",
|
|
2898
|
+
"updated",
|
|
2899
|
+
"path"
|
|
2900
|
+
];
|
|
2901
|
+
function selectedFields(options) {
|
|
2902
|
+
const selected = [];
|
|
2903
|
+
for (const f of FIELD_ORDER) {
|
|
2904
|
+
if (options[f] === true) selected.push(f);
|
|
2905
|
+
}
|
|
2906
|
+
return selected;
|
|
2907
|
+
}
|
|
2908
|
+
function formatRecord(rec, options) {
|
|
2909
|
+
if (options.raw === true) {
|
|
2910
|
+
return rec.body;
|
|
2911
|
+
}
|
|
2912
|
+
const fields = selectedFields(options);
|
|
2913
|
+
if (fields.length > 0) {
|
|
2914
|
+
if (fields.length === 1) {
|
|
2915
|
+
return bareField(rec, fields[0]);
|
|
2916
|
+
}
|
|
2917
|
+
return labeledFields(rec, fields);
|
|
2918
|
+
}
|
|
2919
|
+
if (options.meta === true) {
|
|
2920
|
+
return metadataHeader(rec) + "\n";
|
|
2921
|
+
}
|
|
2922
|
+
if (options.lead === true) {
|
|
2923
|
+
return firstParagraph(rec.body) + "\n";
|
|
2924
|
+
}
|
|
2925
|
+
const header = metadataHeader(rec);
|
|
2926
|
+
const body = rec.body;
|
|
2927
|
+
const sep = body.length > 0 ? "\n\n---\n\n" : "\n";
|
|
2928
|
+
return header + sep + body;
|
|
2929
|
+
}
|
|
2930
|
+
function bareField(rec, field) {
|
|
2931
|
+
switch (field) {
|
|
2932
|
+
case "title":
|
|
2933
|
+
return (rec.title ?? "") + "\n";
|
|
2934
|
+
case "topics":
|
|
2935
|
+
return rec.topics.map((t) => `${t}
|
|
2936
|
+
`).join("");
|
|
2937
|
+
case "files":
|
|
2938
|
+
return rec.file_refs.map((r) => `${r.path}
|
|
2939
|
+
`).join("");
|
|
2940
|
+
case "links":
|
|
2941
|
+
return rec.wikilinks_out.map((t) => `${t}
|
|
2942
|
+
`).join("");
|
|
2943
|
+
case "backlinks":
|
|
2944
|
+
return rec.wikilinks_in.map((t) => `${t}
|
|
2945
|
+
`).join("");
|
|
2946
|
+
case "xwiki":
|
|
2947
|
+
return rec.cross_wiki_links.map((x) => `${x.wiki}:${x.target}
|
|
2948
|
+
`).join("");
|
|
2949
|
+
case "lineage": {
|
|
2950
|
+
const lines = [];
|
|
2951
|
+
if (rec.archived_at !== null) {
|
|
2952
|
+
lines.push(
|
|
2953
|
+
`archived_at: ${new Date(rec.archived_at * 1e3).toISOString()}`
|
|
2954
|
+
);
|
|
2955
|
+
}
|
|
2956
|
+
if (rec.superseded_by !== null) {
|
|
2957
|
+
lines.push(`superseded_by: ${rec.superseded_by}`);
|
|
2958
|
+
}
|
|
2959
|
+
if (rec.supersedes.length > 0) {
|
|
2960
|
+
lines.push(`supersedes: ${rec.supersedes.join(", ")}`);
|
|
2961
|
+
}
|
|
2962
|
+
return lines.length > 0 ? `${lines.join("\n")}
|
|
2963
|
+
` : "";
|
|
2964
|
+
}
|
|
2965
|
+
case "updated":
|
|
2966
|
+
return `${new Date(rec.updated_at * 1e3).toISOString()}
|
|
2967
|
+
`;
|
|
2968
|
+
case "path":
|
|
2969
|
+
return `${rec.file_path}
|
|
2970
|
+
`;
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
function labeledFields(rec, fields) {
|
|
2974
|
+
const parts = [];
|
|
2975
|
+
for (const f of fields) {
|
|
2976
|
+
parts.push(labeledSection(rec, f));
|
|
2977
|
+
}
|
|
2978
|
+
return parts.join("\n") + (parts.length > 0 ? "" : "");
|
|
2979
|
+
}
|
|
2980
|
+
function labeledSection(rec, field) {
|
|
2981
|
+
switch (field) {
|
|
2982
|
+
case "title":
|
|
2983
|
+
return `title: ${rec.title ?? "\u2014"}
|
|
2984
|
+
`;
|
|
2985
|
+
case "topics":
|
|
2986
|
+
return rec.topics.length > 0 ? `topics: ${rec.topics.join(", ")}
|
|
2987
|
+
` : `topics: \u2014
|
|
2988
|
+
`;
|
|
2989
|
+
case "files":
|
|
2990
|
+
return formatListSection(
|
|
2991
|
+
"files",
|
|
2992
|
+
rec.file_refs.map((r) => `${r.path}`)
|
|
2993
|
+
);
|
|
2994
|
+
case "links":
|
|
2995
|
+
return formatListSection("links", rec.wikilinks_out);
|
|
2996
|
+
case "backlinks":
|
|
2997
|
+
return formatListSection("backlinks", rec.wikilinks_in);
|
|
2998
|
+
case "xwiki":
|
|
2999
|
+
return formatListSection(
|
|
3000
|
+
"xwiki",
|
|
3001
|
+
rec.cross_wiki_links.map((x) => `${x.wiki}:${x.target}`)
|
|
3002
|
+
);
|
|
3003
|
+
case "lineage": {
|
|
3004
|
+
const lines = ["lineage:"];
|
|
3005
|
+
if (rec.archived_at !== null) {
|
|
3006
|
+
lines.push(
|
|
3007
|
+
` archived_at: ${new Date(rec.archived_at * 1e3).toISOString()}`
|
|
3008
|
+
);
|
|
3009
|
+
}
|
|
3010
|
+
if (rec.superseded_by !== null) {
|
|
3011
|
+
lines.push(` superseded_by: ${rec.superseded_by}`);
|
|
2734
3012
|
}
|
|
3013
|
+
if (rec.supersedes.length > 0) {
|
|
3014
|
+
lines.push(` supersedes: ${rec.supersedes.join(", ")}`);
|
|
3015
|
+
}
|
|
3016
|
+
if (lines.length === 1) lines.push(" \u2014");
|
|
3017
|
+
return lines.join("\n") + "\n";
|
|
2735
3018
|
}
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
3019
|
+
case "updated":
|
|
3020
|
+
return `updated: ${new Date(rec.updated_at * 1e3).toISOString()}
|
|
3021
|
+
`;
|
|
3022
|
+
case "path":
|
|
3023
|
+
return `path: ${rec.file_path}
|
|
3024
|
+
`;
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
function formatListSection(label, items) {
|
|
3028
|
+
if (items.length === 0) return `${label}: \u2014
|
|
3029
|
+
`;
|
|
3030
|
+
if (items.length <= 3) return `${label}: ${items.join(", ")}
|
|
3031
|
+
`;
|
|
3032
|
+
return `${label}:
|
|
3033
|
+
${items.map((i) => ` ${i}`).join("\n")}
|
|
3034
|
+
`;
|
|
3035
|
+
}
|
|
3036
|
+
function metadataHeader(rec) {
|
|
3037
|
+
const lines = [];
|
|
3038
|
+
lines.push(`slug: ${rec.slug}`);
|
|
3039
|
+
lines.push(`title: ${rec.title ?? "\u2014"}`);
|
|
3040
|
+
lines.push(
|
|
3041
|
+
`topics: ${rec.topics.length > 0 ? rec.topics.join(", ") : "\u2014"}`
|
|
3042
|
+
);
|
|
3043
|
+
if (rec.file_refs.length > 0) {
|
|
3044
|
+
const parts = rec.file_refs.map(
|
|
3045
|
+
(r) => `${r.path}`
|
|
3046
|
+
);
|
|
3047
|
+
lines.push(`files: ${parts.join(", ")}`);
|
|
3048
|
+
}
|
|
3049
|
+
lines.push(
|
|
3050
|
+
`updated: ${new Date(rec.updated_at * 1e3).toISOString()}`
|
|
3051
|
+
);
|
|
3052
|
+
if (rec.wikilinks_out.length > 0) {
|
|
3053
|
+
lines.push(`links: ${rec.wikilinks_out.join(", ")}`);
|
|
3054
|
+
}
|
|
3055
|
+
if (rec.wikilinks_in.length > 0) {
|
|
3056
|
+
lines.push(`backlinks: ${rec.wikilinks_in.join(", ")}`);
|
|
3057
|
+
}
|
|
3058
|
+
if (rec.cross_wiki_links.length > 0) {
|
|
3059
|
+
lines.push(
|
|
3060
|
+
`xwiki: ${rec.cross_wiki_links.map((x) => `${x.wiki}:${x.target}`).join(", ")}`
|
|
3061
|
+
);
|
|
3062
|
+
}
|
|
3063
|
+
if (rec.archived_at !== null) {
|
|
3064
|
+
lines.push(
|
|
3065
|
+
`archived: ${new Date(rec.archived_at * 1e3).toISOString()}`
|
|
3066
|
+
);
|
|
3067
|
+
}
|
|
3068
|
+
if (rec.superseded_by !== null) {
|
|
3069
|
+
lines.push(`superseded_by: ${rec.superseded_by}`);
|
|
3070
|
+
}
|
|
3071
|
+
if (rec.supersedes.length > 0) {
|
|
3072
|
+
lines.push(`supersedes: ${rec.supersedes.join(", ")}`);
|
|
3073
|
+
}
|
|
3074
|
+
return lines.join("\n");
|
|
3075
|
+
}
|
|
3076
|
+
function stripFrontmatter(src) {
|
|
3077
|
+
if (!src.startsWith("---\n") && !src.startsWith("---\r\n")) return src;
|
|
3078
|
+
const rest = src.slice(src.indexOf("\n") + 1);
|
|
3079
|
+
const endMatch = rest.match(/^---[ \t]*\r?\n/m);
|
|
3080
|
+
if (endMatch === null || endMatch.index === void 0) return src;
|
|
3081
|
+
return rest.slice(endMatch.index + endMatch[0].length);
|
|
3082
|
+
}
|
|
3083
|
+
function firstParagraph(body) {
|
|
3084
|
+
let src = body.trimStart();
|
|
3085
|
+
if (src.startsWith("# ")) {
|
|
3086
|
+
const nl = src.indexOf("\n");
|
|
3087
|
+
src = nl === -1 ? "" : src.slice(nl + 1).trimStart();
|
|
2753
3088
|
}
|
|
3089
|
+
const blank = src.search(/\n[ \t]*\n/);
|
|
3090
|
+
if (blank === -1) return src.trimEnd();
|
|
3091
|
+
return src.slice(0, blank).trimEnd();
|
|
2754
3092
|
}
|
|
2755
|
-
function
|
|
3093
|
+
function collectSlugs(options) {
|
|
2756
3094
|
if (options.stdin === true && options.stdinInput !== void 0) {
|
|
2757
3095
|
return options.stdinInput.split(/\r?\n/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
2758
3096
|
}
|
|
@@ -2763,17 +3101,17 @@ function collectSlugs3(options) {
|
|
|
2763
3101
|
}
|
|
2764
3102
|
|
|
2765
3103
|
// src/topics/frontmatterRewrite.ts
|
|
2766
|
-
import { readFile as
|
|
3104
|
+
import { readFile as readFile11, rename as rename4, writeFile as writeFile6 } from "fs/promises";
|
|
2767
3105
|
import yaml3 from "js-yaml";
|
|
2768
3106
|
async function rewritePageTopics(filePath, transform) {
|
|
2769
|
-
const raw = await
|
|
3107
|
+
const raw = await readFile11(filePath, "utf8");
|
|
2770
3108
|
const { before, after, output, changed } = applyTopicsTransform(
|
|
2771
3109
|
raw,
|
|
2772
3110
|
transform
|
|
2773
3111
|
);
|
|
2774
3112
|
if (changed) {
|
|
2775
3113
|
const tmp = `${filePath}.tmp`;
|
|
2776
|
-
await
|
|
3114
|
+
await writeFile6(tmp, output, "utf8");
|
|
2777
3115
|
await rename4(tmp, filePath);
|
|
2778
3116
|
}
|
|
2779
3117
|
return { before, after, changed };
|
|
@@ -2980,12 +3318,12 @@ function arraysEqual(a, b) {
|
|
|
2980
3318
|
}
|
|
2981
3319
|
|
|
2982
3320
|
// src/topics/paths.ts
|
|
2983
|
-
import { join as
|
|
3321
|
+
import { join as join11 } from "path";
|
|
2984
3322
|
function topicsYamlPath(repoRoot) {
|
|
2985
|
-
return
|
|
3323
|
+
return join11(repoRoot, ".almanac", "topics.yaml");
|
|
2986
3324
|
}
|
|
2987
3325
|
function indexDbPath(repoRoot) {
|
|
2988
|
-
return
|
|
3326
|
+
return join11(repoRoot, ".almanac", "index.db");
|
|
2989
3327
|
}
|
|
2990
3328
|
|
|
2991
3329
|
// src/commands/tag.ts
|
|
@@ -3144,8 +3482,8 @@ async function runUntag(options) {
|
|
|
3144
3482
|
}
|
|
3145
3483
|
|
|
3146
3484
|
// src/commands/topics.ts
|
|
3147
|
-
import { readFile as
|
|
3148
|
-
import { join as
|
|
3485
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
3486
|
+
import { join as join12 } from "path";
|
|
3149
3487
|
import fg3 from "fast-glob";
|
|
3150
3488
|
async function runTopicsList(options) {
|
|
3151
3489
|
const repoRoot = await resolveWikiRoot({ cwd: options.cwd, wiki: options.wiki });
|
|
@@ -3617,7 +3955,7 @@ async function runTopicsDescribe(options) {
|
|
|
3617
3955
|
};
|
|
3618
3956
|
}
|
|
3619
3957
|
async function rewriteTopicOnPages(repoRoot, transform) {
|
|
3620
|
-
const pagesDir =
|
|
3958
|
+
const pagesDir = join12(repoRoot, ".almanac", "pages");
|
|
3621
3959
|
const files = await fg3("**/*.md", {
|
|
3622
3960
|
cwd: pagesDir,
|
|
3623
3961
|
absolute: true,
|
|
@@ -3625,7 +3963,7 @@ async function rewriteTopicOnPages(repoRoot, transform) {
|
|
|
3625
3963
|
});
|
|
3626
3964
|
let changed = 0;
|
|
3627
3965
|
for (const filePath of files) {
|
|
3628
|
-
const raw = await
|
|
3966
|
+
const raw = await readFile12(filePath, "utf8");
|
|
3629
3967
|
const applied = applyTopicsTransform(raw, transform);
|
|
3630
3968
|
if (!applied.changed) continue;
|
|
3631
3969
|
await rewritePageTopics(filePath, transform);
|
|
@@ -3634,14 +3972,146 @@ async function rewriteTopicOnPages(repoRoot, transform) {
|
|
|
3634
3972
|
return changed;
|
|
3635
3973
|
}
|
|
3636
3974
|
|
|
3975
|
+
// src/commands/uninstall.ts
|
|
3976
|
+
import { existsSync as existsSync13 } from "fs";
|
|
3977
|
+
import { readFile as readFile13, rm, writeFile as writeFile7 } from "fs/promises";
|
|
3978
|
+
import { homedir as homedir5 } from "os";
|
|
3979
|
+
import path4 from "path";
|
|
3980
|
+
var BLUE2 = "\x1B[38;5;75m";
|
|
3981
|
+
var DIM2 = "\x1B[2m";
|
|
3982
|
+
var RST2 = "\x1B[0m";
|
|
3983
|
+
async function runUninstall(options = {}) {
|
|
3984
|
+
const out = options.stdout ?? process.stdout;
|
|
3985
|
+
const isTTY = options.isTTY ?? process.stdin.isTTY === true;
|
|
3986
|
+
const interactive = isTTY && options.yes !== true;
|
|
3987
|
+
const claudeDir = options.claudeDir ?? path4.join(homedir5(), ".claude");
|
|
3988
|
+
out.write("\n");
|
|
3989
|
+
let removeHook = true;
|
|
3990
|
+
if (options.keepHook === true) {
|
|
3991
|
+
removeHook = false;
|
|
3992
|
+
} else if (interactive) {
|
|
3993
|
+
removeHook = await confirm2(
|
|
3994
|
+
out,
|
|
3995
|
+
"Remove the SessionEnd hook from ~/.claude/settings.json?",
|
|
3996
|
+
true
|
|
3997
|
+
);
|
|
3998
|
+
}
|
|
3999
|
+
if (removeHook) {
|
|
4000
|
+
const res = await runHookUninstall({
|
|
4001
|
+
settingsPath: options.settingsPath,
|
|
4002
|
+
hookScriptPath: options.hookScriptPath
|
|
4003
|
+
});
|
|
4004
|
+
if (res.exitCode !== 0) {
|
|
4005
|
+
return { stdout: "", stderr: res.stderr, exitCode: res.exitCode };
|
|
4006
|
+
}
|
|
4007
|
+
out.write(` ${BLUE2}\u25C7${RST2} ${res.stdout.trim()}
|
|
4008
|
+
`);
|
|
4009
|
+
} else {
|
|
4010
|
+
out.write(` ${DIM2}\u25CB Hook kept${RST2}
|
|
4011
|
+
`);
|
|
4012
|
+
}
|
|
4013
|
+
let removeGuides = true;
|
|
4014
|
+
if (options.keepGuides === true) {
|
|
4015
|
+
removeGuides = false;
|
|
4016
|
+
} else if (interactive) {
|
|
4017
|
+
removeGuides = await confirm2(
|
|
4018
|
+
out,
|
|
4019
|
+
"Remove the guides + CLAUDE.md import line?",
|
|
4020
|
+
true
|
|
4021
|
+
);
|
|
4022
|
+
}
|
|
4023
|
+
if (removeGuides) {
|
|
4024
|
+
const summary = await removeGuideFiles(claudeDir);
|
|
4025
|
+
if (summary.anyChanges) {
|
|
4026
|
+
out.write(
|
|
4027
|
+
` ${BLUE2}\u25C7${RST2} Guides removed (${summary.filesTouched.join(", ")})
|
|
4028
|
+
`
|
|
4029
|
+
);
|
|
4030
|
+
} else {
|
|
4031
|
+
out.write(` ${DIM2}\u25CB Guides not installed${RST2}
|
|
4032
|
+
`);
|
|
4033
|
+
}
|
|
4034
|
+
} else {
|
|
4035
|
+
out.write(` ${DIM2}\u25CB Guides kept${RST2}
|
|
4036
|
+
`);
|
|
4037
|
+
}
|
|
4038
|
+
out.write(`
|
|
4039
|
+
${BLUE2}\u25C7${RST2} ${BLUE2}Uninstall complete${RST2}
|
|
4040
|
+
|
|
4041
|
+
`);
|
|
4042
|
+
return { stdout: "", stderr: "", exitCode: 0 };
|
|
4043
|
+
}
|
|
4044
|
+
async function removeGuideFiles(claudeDir) {
|
|
4045
|
+
const touched = [];
|
|
4046
|
+
const mini = path4.join(claudeDir, "codealmanac.md");
|
|
4047
|
+
const ref = path4.join(claudeDir, "codealmanac-reference.md");
|
|
4048
|
+
const claudeMd = path4.join(claudeDir, "CLAUDE.md");
|
|
4049
|
+
if (existsSync13(mini)) {
|
|
4050
|
+
await rm(mini, { force: true });
|
|
4051
|
+
touched.push("codealmanac.md");
|
|
4052
|
+
}
|
|
4053
|
+
if (existsSync13(ref)) {
|
|
4054
|
+
await rm(ref, { force: true });
|
|
4055
|
+
touched.push("codealmanac-reference.md");
|
|
4056
|
+
}
|
|
4057
|
+
if (existsSync13(claudeMd)) {
|
|
4058
|
+
const existing = await readFile13(claudeMd, "utf8");
|
|
4059
|
+
const { changed, body } = removeImportLine(existing);
|
|
4060
|
+
if (changed) {
|
|
4061
|
+
if (body.trim().length === 0) {
|
|
4062
|
+
await rm(claudeMd, { force: true });
|
|
4063
|
+
touched.push("CLAUDE.md (deleted)");
|
|
4064
|
+
} else {
|
|
4065
|
+
await writeFile7(claudeMd, body, "utf8");
|
|
4066
|
+
touched.push("CLAUDE.md");
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
4070
|
+
return { anyChanges: touched.length > 0, filesTouched: touched };
|
|
4071
|
+
}
|
|
4072
|
+
function removeImportLine(contents) {
|
|
4073
|
+
const eol = contents.includes("\r\n") ? "\r\n" : "\n";
|
|
4074
|
+
const lines = contents.split(/\r?\n/);
|
|
4075
|
+
const indices = [];
|
|
4076
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4077
|
+
if (lines[i].trim() === IMPORT_LINE) indices.push(i);
|
|
4078
|
+
}
|
|
4079
|
+
if (indices.length === 0) return { changed: false, body: contents };
|
|
4080
|
+
for (let i = indices.length - 1; i >= 0; i--) {
|
|
4081
|
+
lines.splice(indices[i], 1);
|
|
4082
|
+
}
|
|
4083
|
+
let body = lines.join(eol);
|
|
4084
|
+
body = body.replace(/\n\n\n+/g, "\n\n");
|
|
4085
|
+
return { changed: true, body };
|
|
4086
|
+
}
|
|
4087
|
+
function confirm2(out, question, defaultYes) {
|
|
4088
|
+
return new Promise((resolve2) => {
|
|
4089
|
+
const hint = defaultYes ? "[Y/n]" : "[y/N]";
|
|
4090
|
+
out.write(` ${BLUE2}\u25C6${RST2} ${question} ${DIM2}${hint}${RST2} `);
|
|
4091
|
+
let buf = "";
|
|
4092
|
+
const onData = (chunk) => {
|
|
4093
|
+
buf += chunk.toString("utf8");
|
|
4094
|
+
const nl = buf.indexOf("\n");
|
|
4095
|
+
if (nl === -1) return;
|
|
4096
|
+
process.stdin.removeListener("data", onData);
|
|
4097
|
+
process.stdin.pause();
|
|
4098
|
+
const answer = buf.slice(0, nl).trim().toLowerCase();
|
|
4099
|
+
const accepted = answer.length === 0 ? defaultYes : answer === "y" || answer === "yes";
|
|
4100
|
+
resolve2(accepted);
|
|
4101
|
+
};
|
|
4102
|
+
process.stdin.resume();
|
|
4103
|
+
process.stdin.on("data", onData);
|
|
4104
|
+
});
|
|
4105
|
+
}
|
|
4106
|
+
|
|
3637
4107
|
// src/registry/autoregister.ts
|
|
3638
|
-
import { existsSync as
|
|
4108
|
+
import { existsSync as existsSync14 } from "fs";
|
|
3639
4109
|
import { basename as basename5 } from "path";
|
|
3640
4110
|
async function autoRegisterIfNeeded(cwd) {
|
|
3641
4111
|
try {
|
|
3642
4112
|
const repoRoot = findNearestAlmanacDir(cwd);
|
|
3643
4113
|
if (repoRoot === null) return null;
|
|
3644
|
-
if (!
|
|
4114
|
+
if (!existsSync14(repoRoot)) return null;
|
|
3645
4115
|
const entries = await readRegistry();
|
|
3646
4116
|
const existing = entries.find((e) => samePath(e.path, repoRoot));
|
|
3647
4117
|
if (existing !== void 0) return existing;
|
|
@@ -3686,38 +4156,20 @@ function samePath(a, b) {
|
|
|
3686
4156
|
|
|
3687
4157
|
// src/cli.ts
|
|
3688
4158
|
async function run(argv) {
|
|
3689
|
-
const program = new Command();
|
|
3690
4159
|
const invoked = argv[1] !== void 0 ? basename6(argv[1]) : "almanac";
|
|
3691
4160
|
const programName = invoked === "codealmanac" ? "codealmanac" : "almanac";
|
|
4161
|
+
const program = new Command();
|
|
3692
4162
|
program.name(programName).description(
|
|
3693
4163
|
"codealmanac \u2014 a living wiki for codebases, maintained by AI agents"
|
|
3694
|
-
).version(
|
|
3695
|
-
|
|
3696
|
-
const result = await
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
`${verb} wiki "${result.entry.name}" at ${result.almanacDir}
|
|
3704
|
-
`
|
|
3705
|
-
);
|
|
3706
|
-
});
|
|
3707
|
-
program.command("list").description("list registered wikis").option("--json", "emit structured JSON").option(
|
|
3708
|
-
"--drop <name>",
|
|
3709
|
-
"remove a wiki from the registry (the only way entries are ever removed)"
|
|
3710
|
-
).action(async (opts) => {
|
|
3711
|
-
if (opts.drop === void 0) {
|
|
3712
|
-
await autoRegisterIfNeeded(process.cwd());
|
|
3713
|
-
}
|
|
3714
|
-
const result = await listWikis(opts);
|
|
3715
|
-
process.stdout.write(result.stdout);
|
|
3716
|
-
if (result.exitCode !== 0) {
|
|
3717
|
-
process.exitCode = result.exitCode;
|
|
3718
|
-
}
|
|
3719
|
-
});
|
|
3720
|
-
program.command("search [query]").description("query pages by text, topic, file mentions, or freshness").option(
|
|
4164
|
+
).version(readPackageVersion(), "-v, --version", "print version");
|
|
4165
|
+
if (programName === "codealmanac" && argv.length === 2) {
|
|
4166
|
+
const result = await runSetup({});
|
|
4167
|
+
if (result.stderr.length > 0) process.stderr.write(result.stderr);
|
|
4168
|
+
if (result.stdout.length > 0) process.stdout.write(result.stdout);
|
|
4169
|
+
if (result.exitCode !== 0) process.exitCode = result.exitCode;
|
|
4170
|
+
return;
|
|
4171
|
+
}
|
|
4172
|
+
program.command("search [query]").description("find pages by text, topic, file mentions, freshness").option(
|
|
3721
4173
|
"--topic <name...>",
|
|
3722
4174
|
"filter by topic (repeat for intersection)",
|
|
3723
4175
|
collectOption,
|
|
@@ -3748,12 +4200,10 @@ async function run(argv) {
|
|
|
3748
4200
|
json: opts.json,
|
|
3749
4201
|
limit: opts.limit
|
|
3750
4202
|
});
|
|
3751
|
-
|
|
3752
|
-
process.stdout.write(result.stdout);
|
|
3753
|
-
if (result.exitCode !== 0) process.exitCode = result.exitCode;
|
|
4203
|
+
emit(result);
|
|
3754
4204
|
}
|
|
3755
4205
|
);
|
|
3756
|
-
program.command("show [slug]").description("print
|
|
4206
|
+
program.command("show [slug]").description("print a page (metadata + body; flags to narrow)").option("--stdin", "read slugs from stdin (one per line)").option("--wiki <name>", "target a specific registered wiki").option("--json", "structured JSON (overrides other view/field flags)").option("--raw", "body only (alias: --body)").option("--body", "body only (alias: --raw)").option("--meta", "metadata only, no body").option("--lead", "first paragraph of the body only").option("--title", "print title").option("--topics", "print topics").option("--files", "print file refs").option("--links", "print outgoing wikilinks").option("--backlinks", "print incoming wikilinks").option("--xwiki", "print cross-wiki links").option("--lineage", "print archived_at / supersedes / superseded_by").option("--updated", "print updated timestamp").option("--path", "print absolute file path").action(
|
|
3757
4207
|
async (slug, opts) => {
|
|
3758
4208
|
await autoRegisterIfNeeded(process.cwd());
|
|
3759
4209
|
const result = await runShow({
|
|
@@ -3761,54 +4211,85 @@ async function run(argv) {
|
|
|
3761
4211
|
slug,
|
|
3762
4212
|
stdin: opts.stdin,
|
|
3763
4213
|
stdinInput: opts.stdin === true ? await readStdin() : void 0,
|
|
3764
|
-
wiki: opts.wiki
|
|
4214
|
+
wiki: opts.wiki,
|
|
4215
|
+
json: opts.json,
|
|
4216
|
+
// `--body` is a surface alias for `--raw`. Fold it into one
|
|
4217
|
+
// field at the CLI boundary so the implementation never has
|
|
4218
|
+
// to care which spelling the user typed.
|
|
4219
|
+
raw: opts.raw === true || opts.body === true,
|
|
4220
|
+
meta: opts.meta,
|
|
4221
|
+
lead: opts.lead,
|
|
4222
|
+
title: opts.title,
|
|
4223
|
+
topics: opts.topics,
|
|
4224
|
+
files: opts.files,
|
|
4225
|
+
links: opts.links,
|
|
4226
|
+
backlinks: opts.backlinks,
|
|
4227
|
+
xwiki: opts.xwiki,
|
|
4228
|
+
lineage: opts.lineage,
|
|
4229
|
+
updated: opts.updated,
|
|
4230
|
+
path: opts.path
|
|
3765
4231
|
});
|
|
3766
|
-
|
|
3767
|
-
process.stdout.write(result.stdout);
|
|
3768
|
-
if (result.exitCode !== 0) process.exitCode = result.exitCode;
|
|
4232
|
+
emit(result);
|
|
3769
4233
|
}
|
|
3770
4234
|
);
|
|
3771
|
-
program.command("
|
|
3772
|
-
async (
|
|
4235
|
+
program.command("health").description("report graph integrity problems").option("--topic <name>", "scope to a topic + its descendants").option("--stale <duration>", "stale threshold (default 90d)").option("--stdin", "read page slugs from stdin (limit to these pages)").option("--json", "emit structured JSON").option("--wiki <name>", "target a specific registered wiki").action(
|
|
4236
|
+
async (opts) => {
|
|
3773
4237
|
await autoRegisterIfNeeded(process.cwd());
|
|
3774
|
-
const result = await
|
|
4238
|
+
const result = await runHealth({
|
|
3775
4239
|
cwd: process.cwd(),
|
|
3776
|
-
|
|
4240
|
+
topic: opts.topic,
|
|
4241
|
+
stale: opts.stale,
|
|
3777
4242
|
stdin: opts.stdin,
|
|
3778
4243
|
stdinInput: opts.stdin === true ? await readStdin() : void 0,
|
|
4244
|
+
json: opts.json,
|
|
3779
4245
|
wiki: opts.wiki
|
|
3780
4246
|
});
|
|
3781
|
-
|
|
3782
|
-
process.stdout.write(result.stdout);
|
|
3783
|
-
if (result.exitCode !== 0) process.exitCode = result.exitCode;
|
|
4247
|
+
emit(result);
|
|
3784
4248
|
}
|
|
3785
4249
|
);
|
|
3786
|
-
program.command("
|
|
3787
|
-
|
|
4250
|
+
program.command("list").description("list registered wikis").option("--json", "emit structured JSON").option(
|
|
4251
|
+
"--drop <name>",
|
|
4252
|
+
"remove a wiki from the registry (the only way entries are ever removed)"
|
|
4253
|
+
).action(async (opts) => {
|
|
4254
|
+
if (opts.drop === void 0) {
|
|
4255
|
+
await autoRegisterIfNeeded(process.cwd());
|
|
4256
|
+
}
|
|
4257
|
+
const result = await listWikis(opts);
|
|
4258
|
+
process.stdout.write(result.stdout);
|
|
4259
|
+
if (result.exitCode !== 0) {
|
|
4260
|
+
process.exitCode = result.exitCode;
|
|
4261
|
+
}
|
|
4262
|
+
});
|
|
4263
|
+
program.command("tag [page] [topics...]").description("add topics to a page (auto-creates missing topics)").option("--stdin", "read page slugs from stdin (one per line)").option("--wiki <name>", "target a specific registered wiki").action(
|
|
4264
|
+
async (page, topicsArg, opts) => {
|
|
3788
4265
|
await autoRegisterIfNeeded(process.cwd());
|
|
3789
|
-
const
|
|
4266
|
+
const resolvedTopics = opts.stdin === true ? [page, ...topicsArg].filter(
|
|
4267
|
+
(t) => typeof t === "string" && t.length > 0
|
|
4268
|
+
) : topicsArg;
|
|
4269
|
+
const result = await runTag({
|
|
3790
4270
|
cwd: process.cwd(),
|
|
3791
|
-
|
|
4271
|
+
page: opts.stdin === true ? void 0 : page,
|
|
4272
|
+
topics: resolvedTopics,
|
|
3792
4273
|
stdin: opts.stdin,
|
|
3793
4274
|
stdinInput: opts.stdin === true ? await readStdin() : void 0,
|
|
3794
|
-
json: opts.json,
|
|
3795
4275
|
wiki: opts.wiki
|
|
3796
4276
|
});
|
|
3797
|
-
|
|
3798
|
-
process.stdout.write(result.stdout);
|
|
3799
|
-
if (result.exitCode !== 0) process.exitCode = result.exitCode;
|
|
4277
|
+
emit(result);
|
|
3800
4278
|
}
|
|
3801
4279
|
);
|
|
3802
|
-
program.command("
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
4280
|
+
program.command("untag <page> <topic>").description("remove a topic from a page's frontmatter").option("--wiki <name>", "target a specific registered wiki").action(
|
|
4281
|
+
async (page, topic, opts) => {
|
|
4282
|
+
await autoRegisterIfNeeded(process.cwd());
|
|
4283
|
+
const result = await runUntag({
|
|
4284
|
+
cwd: process.cwd(),
|
|
4285
|
+
page,
|
|
4286
|
+
topic,
|
|
4287
|
+
wiki: opts.wiki
|
|
4288
|
+
});
|
|
4289
|
+
emit(result);
|
|
4290
|
+
}
|
|
4291
|
+
);
|
|
4292
|
+
const topics = program.command("topics").description("manage the topic DAG");
|
|
3812
4293
|
topics.command("list", { isDefault: true }).description("list all topics with page counts").option("--wiki <name>", "target a specific registered wiki").option("--json", "emit structured JSON").action(async (opts) => {
|
|
3813
4294
|
await autoRegisterIfNeeded(process.cwd());
|
|
3814
4295
|
const result = await runTopicsList({
|
|
@@ -3905,37 +4386,8 @@ async function run(argv) {
|
|
|
3905
4386
|
emit(result);
|
|
3906
4387
|
}
|
|
3907
4388
|
);
|
|
3908
|
-
program.command("tag [page] [topics...]").description("add topics to a page (auto-creates missing topics)").option("--stdin", "read page slugs from stdin (one per line)").option("--wiki <name>", "target a specific registered wiki").action(
|
|
3909
|
-
async (page, topicsArg, opts) => {
|
|
3910
|
-
await autoRegisterIfNeeded(process.cwd());
|
|
3911
|
-
const resolvedTopics = opts.stdin === true ? [page, ...topicsArg].filter(
|
|
3912
|
-
(t) => typeof t === "string" && t.length > 0
|
|
3913
|
-
) : topicsArg;
|
|
3914
|
-
const result = await runTag({
|
|
3915
|
-
cwd: process.cwd(),
|
|
3916
|
-
page: opts.stdin === true ? void 0 : page,
|
|
3917
|
-
topics: resolvedTopics,
|
|
3918
|
-
stdin: opts.stdin,
|
|
3919
|
-
stdinInput: opts.stdin === true ? await readStdin() : void 0,
|
|
3920
|
-
wiki: opts.wiki
|
|
3921
|
-
});
|
|
3922
|
-
emit(result);
|
|
3923
|
-
}
|
|
3924
|
-
);
|
|
3925
|
-
program.command("untag <page> <topic>").description("remove a topic from a page's frontmatter").option("--wiki <name>", "target a specific registered wiki").action(
|
|
3926
|
-
async (page, topic, opts) => {
|
|
3927
|
-
await autoRegisterIfNeeded(process.cwd());
|
|
3928
|
-
const result = await runUntag({
|
|
3929
|
-
cwd: process.cwd(),
|
|
3930
|
-
page,
|
|
3931
|
-
topic,
|
|
3932
|
-
wiki: opts.wiki
|
|
3933
|
-
});
|
|
3934
|
-
emit(result);
|
|
3935
|
-
}
|
|
3936
|
-
);
|
|
3937
4389
|
program.command("bootstrap").description(
|
|
3938
|
-
"
|
|
4390
|
+
"scaffold a wiki in this repo via an AI agent (requires ANTHROPIC_API_KEY or Claude subscription)"
|
|
3939
4391
|
).option("--quiet", "suppress per-tool streaming; print only the final line").option("--model <model>", "override the agent model").option(
|
|
3940
4392
|
"--force",
|
|
3941
4393
|
"overwrite an existing populated wiki (default: refuse)"
|
|
@@ -3951,7 +4403,7 @@ async function run(argv) {
|
|
|
3951
4403
|
}
|
|
3952
4404
|
);
|
|
3953
4405
|
program.command("capture [transcript]").description(
|
|
3954
|
-
"
|
|
4406
|
+
"run the writer/reviewer pipeline on a session (usually automatic)"
|
|
3955
4407
|
).option("--session <id>", "target a specific session by ID").option(
|
|
3956
4408
|
"--quiet",
|
|
3957
4409
|
"suppress per-tool streaming; print only the final summary"
|
|
@@ -3968,9 +4420,7 @@ async function run(argv) {
|
|
|
3968
4420
|
emit(result);
|
|
3969
4421
|
}
|
|
3970
4422
|
);
|
|
3971
|
-
const hook = program.command("hook").description(
|
|
3972
|
-
"install, uninstall, or inspect the SessionEnd hook in ~/.claude/settings.json"
|
|
3973
|
-
);
|
|
4423
|
+
const hook = program.command("hook").description("manage the SessionEnd auto-capture hook");
|
|
3974
4424
|
hook.command("install").description("add a SessionEnd entry that runs 'almanac capture' on session end").action(async () => {
|
|
3975
4425
|
const result = await runHookInstall();
|
|
3976
4426
|
emit(result);
|
|
@@ -3983,23 +4433,165 @@ async function run(argv) {
|
|
|
3983
4433
|
const result = await runHookStatus();
|
|
3984
4434
|
emit(result);
|
|
3985
4435
|
});
|
|
3986
|
-
program.command("
|
|
4436
|
+
program.command("reindex").description("force a full rebuild of .almanac/index.db").option("--wiki <name>", "target a specific registered wiki").action(async (opts) => {
|
|
4437
|
+
await autoRegisterIfNeeded(process.cwd());
|
|
4438
|
+
const result = await runReindex({
|
|
4439
|
+
cwd: process.cwd(),
|
|
4440
|
+
wiki: opts.wiki
|
|
4441
|
+
});
|
|
4442
|
+
process.stdout.write(result.stdout);
|
|
4443
|
+
if (result.exitCode !== 0) process.exitCode = result.exitCode;
|
|
4444
|
+
});
|
|
4445
|
+
program.command("setup").description("install the hook + CLAUDE.md guides (bare codealmanac alias)").option("-y, --yes", "skip prompts; install everything").option("--skip-hook", "opt out of the SessionEnd hook").option("--skip-guides", "opt out of the CLAUDE.md guides").action(
|
|
3987
4446
|
async (opts) => {
|
|
3988
|
-
await
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
stale: opts.stale,
|
|
3993
|
-
stdin: opts.stdin,
|
|
3994
|
-
stdinInput: opts.stdin === true ? await readStdin() : void 0,
|
|
3995
|
-
json: opts.json,
|
|
3996
|
-
wiki: opts.wiki
|
|
4447
|
+
const result = await runSetup({
|
|
4448
|
+
yes: opts.yes,
|
|
4449
|
+
skipHook: opts.skipHook,
|
|
4450
|
+
skipGuides: opts.skipGuides
|
|
3997
4451
|
});
|
|
3998
4452
|
emit(result);
|
|
3999
4453
|
}
|
|
4000
4454
|
);
|
|
4455
|
+
program.command("uninstall").description("remove the hook + guides + import line").option("-y, --yes", "skip confirmations; remove everything").option("--keep-hook", "leave the hook alone").option("--keep-guides", "leave the guides + CLAUDE.md import alone").action(
|
|
4456
|
+
async (opts) => {
|
|
4457
|
+
const result = await runUninstall({
|
|
4458
|
+
yes: opts.yes,
|
|
4459
|
+
keepHook: opts.keepHook,
|
|
4460
|
+
keepGuides: opts.keepGuides
|
|
4461
|
+
});
|
|
4462
|
+
emit(result);
|
|
4463
|
+
}
|
|
4464
|
+
);
|
|
4465
|
+
configureGroupedHelp(program);
|
|
4001
4466
|
await program.parseAsync(argv);
|
|
4002
4467
|
}
|
|
4468
|
+
var HELP_GROUPS = [
|
|
4469
|
+
{
|
|
4470
|
+
title: "Query",
|
|
4471
|
+
commands: ["search", "show", "health", "list"]
|
|
4472
|
+
},
|
|
4473
|
+
{
|
|
4474
|
+
title: "Edit",
|
|
4475
|
+
commands: ["tag", "untag", "topics"]
|
|
4476
|
+
},
|
|
4477
|
+
{
|
|
4478
|
+
title: "Wiki lifecycle",
|
|
4479
|
+
commands: ["bootstrap", "capture", "hook", "reindex"]
|
|
4480
|
+
},
|
|
4481
|
+
{
|
|
4482
|
+
title: "Setup",
|
|
4483
|
+
commands: ["setup", "uninstall"]
|
|
4484
|
+
}
|
|
4485
|
+
];
|
|
4486
|
+
function configureGroupedHelp(program) {
|
|
4487
|
+
program.configureHelp({
|
|
4488
|
+
formatHelp(cmd, helper) {
|
|
4489
|
+
if (cmd.parent !== null) {
|
|
4490
|
+
return renderDefault(cmd, helper);
|
|
4491
|
+
}
|
|
4492
|
+
const termWidth = helper.padWidth(cmd, helper);
|
|
4493
|
+
const helpWidth = helper.helpWidth ?? process.stdout.columns ?? 80;
|
|
4494
|
+
const itemSepWidth = 2;
|
|
4495
|
+
const out = [];
|
|
4496
|
+
out.push(`Usage: ${helper.commandUsage(cmd)}
|
|
4497
|
+
`);
|
|
4498
|
+
const description = helper.commandDescription(cmd);
|
|
4499
|
+
if (description.length > 0) {
|
|
4500
|
+
out.push(
|
|
4501
|
+
helper.wrap(description, helpWidth, 0) + "\n"
|
|
4502
|
+
);
|
|
4503
|
+
}
|
|
4504
|
+
const optionList = helper.visibleOptions(cmd).map(
|
|
4505
|
+
(o) => `${helper.optionTerm(o)}${" ".repeat(Math.max(0, termWidth - helper.optionTerm(o).length) + itemSepWidth)}${helper.optionDescription(o)}`
|
|
4506
|
+
);
|
|
4507
|
+
if (optionList.length > 0) {
|
|
4508
|
+
out.push("Options:");
|
|
4509
|
+
for (const l of optionList) out.push(` ${l}`);
|
|
4510
|
+
out.push("");
|
|
4511
|
+
}
|
|
4512
|
+
const visible = helper.visibleCommands(cmd);
|
|
4513
|
+
const byName = /* @__PURE__ */ new Map();
|
|
4514
|
+
for (const c of visible) byName.set(c.name(), c);
|
|
4515
|
+
for (const group of HELP_GROUPS) {
|
|
4516
|
+
const members = group.commands.map((n) => byName.get(n)).filter((c) => c !== void 0);
|
|
4517
|
+
if (members.length === 0) continue;
|
|
4518
|
+
out.push(`${group.title}:`);
|
|
4519
|
+
for (const c of members) {
|
|
4520
|
+
const term = helper.subcommandTerm(c);
|
|
4521
|
+
const desc = helper.subcommandDescription(c);
|
|
4522
|
+
const padding = Math.max(
|
|
4523
|
+
0,
|
|
4524
|
+
termWidth - term.length + itemSepWidth
|
|
4525
|
+
);
|
|
4526
|
+
out.push(` ${term}${" ".repeat(padding)}${desc}`);
|
|
4527
|
+
byName.delete(c.name());
|
|
4528
|
+
}
|
|
4529
|
+
out.push("");
|
|
4530
|
+
}
|
|
4531
|
+
if (byName.size > 0) {
|
|
4532
|
+
out.push("Other:");
|
|
4533
|
+
for (const c of byName.values()) {
|
|
4534
|
+
const term = helper.subcommandTerm(c);
|
|
4535
|
+
const desc = helper.subcommandDescription(c);
|
|
4536
|
+
const padding = Math.max(
|
|
4537
|
+
0,
|
|
4538
|
+
termWidth - term.length + itemSepWidth
|
|
4539
|
+
);
|
|
4540
|
+
out.push(` ${term}${" ".repeat(padding)}${desc}`);
|
|
4541
|
+
}
|
|
4542
|
+
out.push("");
|
|
4543
|
+
}
|
|
4544
|
+
return out.join("\n");
|
|
4545
|
+
}
|
|
4546
|
+
});
|
|
4547
|
+
}
|
|
4548
|
+
function renderDefault(cmd, helper) {
|
|
4549
|
+
const termWidth = helper.padWidth(cmd, helper);
|
|
4550
|
+
const helpWidth = helper.helpWidth ?? process.stdout.columns ?? 80;
|
|
4551
|
+
const itemSepWidth = 2;
|
|
4552
|
+
const lines = [`Usage: ${helper.commandUsage(cmd)}
|
|
4553
|
+
`];
|
|
4554
|
+
const description = helper.commandDescription(cmd);
|
|
4555
|
+
if (description.length > 0) {
|
|
4556
|
+
lines.push(helper.wrap(description, helpWidth, 0) + "\n");
|
|
4557
|
+
}
|
|
4558
|
+
const args = helper.visibleArguments(cmd).map(
|
|
4559
|
+
(a) => `${helper.argumentTerm(a)}${" ".repeat(Math.max(0, termWidth - helper.argumentTerm(a).length) + itemSepWidth)}${helper.argumentDescription(a)}`
|
|
4560
|
+
);
|
|
4561
|
+
if (args.length > 0) {
|
|
4562
|
+
lines.push("Arguments:");
|
|
4563
|
+
for (const a of args) lines.push(` ${a}`);
|
|
4564
|
+
lines.push("");
|
|
4565
|
+
}
|
|
4566
|
+
const opts = helper.visibleOptions(cmd).map(
|
|
4567
|
+
(o) => `${helper.optionTerm(o)}${" ".repeat(Math.max(0, termWidth - helper.optionTerm(o).length) + itemSepWidth)}${helper.optionDescription(o)}`
|
|
4568
|
+
);
|
|
4569
|
+
if (opts.length > 0) {
|
|
4570
|
+
lines.push("Options:");
|
|
4571
|
+
for (const o of opts) lines.push(` ${o}`);
|
|
4572
|
+
lines.push("");
|
|
4573
|
+
}
|
|
4574
|
+
const subs = helper.visibleCommands(cmd).map(
|
|
4575
|
+
(c) => `${helper.subcommandTerm(c)}${" ".repeat(Math.max(0, termWidth - helper.subcommandTerm(c).length) + itemSepWidth)}${helper.subcommandDescription(c)}`
|
|
4576
|
+
);
|
|
4577
|
+
if (subs.length > 0) {
|
|
4578
|
+
lines.push("Commands:");
|
|
4579
|
+
for (const s of subs) lines.push(` ${s}`);
|
|
4580
|
+
lines.push("");
|
|
4581
|
+
}
|
|
4582
|
+
return lines.join("\n");
|
|
4583
|
+
}
|
|
4584
|
+
function readPackageVersion() {
|
|
4585
|
+
try {
|
|
4586
|
+
const require2 = createRequire3(import.meta.url);
|
|
4587
|
+
const pkg = require2("../package.json");
|
|
4588
|
+
if (typeof pkg.version === "string" && pkg.version.length > 0) {
|
|
4589
|
+
return pkg.version;
|
|
4590
|
+
}
|
|
4591
|
+
} catch {
|
|
4592
|
+
}
|
|
4593
|
+
return "unknown";
|
|
4594
|
+
}
|
|
4003
4595
|
function emit(result) {
|
|
4004
4596
|
if (result.stderr.length > 0) process.stderr.write(result.stderr);
|
|
4005
4597
|
if (result.stdout.length > 0) process.stdout.write(result.stdout);
|