akm-cli 0.5.0-rc2 → 0.5.0-rc4
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/cli.js +95 -18
- package/dist/indexer.js +8 -2
- package/dist/installed-kits.js +2 -0
- package/dist/local-search.js +53 -7
- package/dist/metadata.js +24 -4
- package/dist/stash-add.js +32 -9
- package/dist/stash-show.js +56 -8
- package/dist/wiki.js +103 -42
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -320,6 +320,12 @@ function formatPlain(command, result, detail) {
|
|
|
320
320
|
case "index": {
|
|
321
321
|
const indexResult = result;
|
|
322
322
|
let out = `Indexed ${indexResult.totalEntries ?? 0} entries from ${indexResult.directoriesScanned ?? 0} directories (mode: ${indexResult.mode ?? "unknown"})`;
|
|
323
|
+
const warnings = indexResult.warnings;
|
|
324
|
+
if (Array.isArray(warnings) && warnings.length > 0) {
|
|
325
|
+
out += `\nWarnings (${warnings.length}):`;
|
|
326
|
+
for (const message of warnings)
|
|
327
|
+
out += `\n - ${String(message)}`;
|
|
328
|
+
}
|
|
323
329
|
const verification = indexResult.verification;
|
|
324
330
|
if (verification?.ok === false && verification.message) {
|
|
325
331
|
out += `\nVerification: ${String(verification.message)}`;
|
|
@@ -458,6 +464,8 @@ function formatPlain(command, result, detail) {
|
|
|
458
464
|
const ver = typeof src.version === "string" ? ` v${src.version}` : "";
|
|
459
465
|
const prov = typeof src.provider === "string" ? ` (${src.provider})` : "";
|
|
460
466
|
const flags = [];
|
|
467
|
+
if (typeof src.wiki === "string")
|
|
468
|
+
flags.push(`wiki:${src.wiki}`);
|
|
461
469
|
if (src.updatable === true)
|
|
462
470
|
flags.push("updatable");
|
|
463
471
|
if (src.writable === true)
|
|
@@ -472,6 +480,12 @@ function formatPlain(command, result, detail) {
|
|
|
472
480
|
const scanned = index?.directoriesScanned ?? 0;
|
|
473
481
|
const total = index?.totalEntries ?? 0;
|
|
474
482
|
const lines = [`Installed ${r.ref} (${scanned} directories scanned, ${total} total assets indexed)`];
|
|
483
|
+
const warnings = index?.warnings;
|
|
484
|
+
if (Array.isArray(warnings) && warnings.length > 0) {
|
|
485
|
+
lines.push(`Warnings (${warnings.length}):`);
|
|
486
|
+
for (const message of warnings)
|
|
487
|
+
lines.push(` - ${String(message)}`);
|
|
488
|
+
}
|
|
475
489
|
const installed = r.installed;
|
|
476
490
|
const audit = installed?.audit;
|
|
477
491
|
if (audit && typeof audit === "object") {
|
|
@@ -651,7 +665,7 @@ function formatSearchPlain(r, detail) {
|
|
|
651
665
|
function formatWikiListPlain(r) {
|
|
652
666
|
const wikis = Array.isArray(r.wikis) ? r.wikis : [];
|
|
653
667
|
if (wikis.length === 0)
|
|
654
|
-
return "No wikis. Create one with `akm wiki create <name>`.";
|
|
668
|
+
return "No wikis. Create one with `akm wiki create <name>` or register one with `akm wiki register <name> <path-or-repo>`.";
|
|
655
669
|
const lines = ["NAME\tPAGES\tRAWS\tLAST-MODIFIED"];
|
|
656
670
|
for (const w of wikis) {
|
|
657
671
|
const name = typeof w.name === "string" ? w.name : "?";
|
|
@@ -1205,11 +1219,19 @@ const addCommand = defineCommand({
|
|
|
1205
1219
|
if (shouldWarnOnPlainHttp(ref)) {
|
|
1206
1220
|
warn("Warning: source URL uses plain HTTP (not HTTPS). For security, prefer https:// to protect against eavesdropping and tampering.");
|
|
1207
1221
|
}
|
|
1208
|
-
const websiteOptions =
|
|
1209
|
-
if (args
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1222
|
+
const websiteOptions = buildWebsiteOptions(args);
|
|
1223
|
+
if (args.type === "wiki") {
|
|
1224
|
+
const { registerWikiSource } = await import("./stash-add");
|
|
1225
|
+
const result = await registerWikiSource({
|
|
1226
|
+
ref,
|
|
1227
|
+
name: args.name,
|
|
1228
|
+
options: Object.keys(websiteOptions).length > 0 ? websiteOptions : undefined,
|
|
1229
|
+
trustThisInstall: args.trust,
|
|
1230
|
+
writable: args.writable,
|
|
1231
|
+
});
|
|
1232
|
+
output("add", result);
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1213
1235
|
const result = await akmAdd({
|
|
1214
1236
|
ref,
|
|
1215
1237
|
name: args.name,
|
|
@@ -1222,6 +1244,14 @@ const addCommand = defineCommand({
|
|
|
1222
1244
|
});
|
|
1223
1245
|
},
|
|
1224
1246
|
});
|
|
1247
|
+
function buildWebsiteOptions(args) {
|
|
1248
|
+
const websiteOptions = {};
|
|
1249
|
+
if (typeof args["max-pages"] === "string" && args["max-pages"].length > 0)
|
|
1250
|
+
websiteOptions.maxPages = args["max-pages"];
|
|
1251
|
+
if (typeof args["max-depth"] === "string" && args["max-depth"].length > 0)
|
|
1252
|
+
websiteOptions.maxDepth = args["max-depth"];
|
|
1253
|
+
return websiteOptions;
|
|
1254
|
+
}
|
|
1225
1255
|
const VALID_SOURCE_KINDS = new Set(["local", "managed", "remote"]);
|
|
1226
1256
|
function parseKindFilter(raw) {
|
|
1227
1257
|
if (!raw)
|
|
@@ -2464,6 +2494,41 @@ const wikiCreateCommand = defineCommand({
|
|
|
2464
2494
|
});
|
|
2465
2495
|
},
|
|
2466
2496
|
});
|
|
2497
|
+
const wikiRegisterCommand = defineCommand({
|
|
2498
|
+
meta: {
|
|
2499
|
+
name: "register",
|
|
2500
|
+
description: "Register an existing directory or repo as a first-class wiki without copying or mutating it; refreshes source and wiki search state immediately",
|
|
2501
|
+
},
|
|
2502
|
+
args: {
|
|
2503
|
+
name: { type: "positional", description: "Wiki name (lowercase, digits, hyphens)", required: true },
|
|
2504
|
+
ref: { type: "positional", description: "Path or repo ref for the external wiki source", required: true },
|
|
2505
|
+
writable: {
|
|
2506
|
+
type: "boolean",
|
|
2507
|
+
description: "Mark a git-backed source as writable so changes can be pushed back",
|
|
2508
|
+
default: false,
|
|
2509
|
+
},
|
|
2510
|
+
trust: {
|
|
2511
|
+
type: "boolean",
|
|
2512
|
+
description: "Bypass install-audit blocking for this registration only",
|
|
2513
|
+
default: false,
|
|
2514
|
+
},
|
|
2515
|
+
"max-pages": { type: "string", description: "Maximum pages to crawl for website sources (default: 50)" },
|
|
2516
|
+
"max-depth": { type: "string", description: "Maximum crawl depth for website sources (default: 3)" },
|
|
2517
|
+
},
|
|
2518
|
+
run({ args }) {
|
|
2519
|
+
return runWithJsonErrors(async () => {
|
|
2520
|
+
const { registerWikiSource } = await import("./stash-add");
|
|
2521
|
+
const result = await registerWikiSource({
|
|
2522
|
+
ref: args.ref.trim(),
|
|
2523
|
+
name: args.name,
|
|
2524
|
+
options: Object.keys(buildWebsiteOptions(args)).length > 0 ? buildWebsiteOptions(args) : undefined,
|
|
2525
|
+
trustThisInstall: args.trust,
|
|
2526
|
+
writable: args.writable,
|
|
2527
|
+
});
|
|
2528
|
+
output("wiki-register", result);
|
|
2529
|
+
});
|
|
2530
|
+
},
|
|
2531
|
+
});
|
|
2467
2532
|
const wikiListCommand = defineCommand({
|
|
2468
2533
|
meta: { name: "list", description: "List wikis with page/raw counts and last-modified timestamps" },
|
|
2469
2534
|
run() {
|
|
@@ -2492,7 +2557,7 @@ const wikiShowCommand = defineCommand({
|
|
|
2492
2557
|
const wikiRemoveCommand = defineCommand({
|
|
2493
2558
|
meta: {
|
|
2494
2559
|
name: "remove",
|
|
2495
|
-
description: "Remove a wiki. Preserves raw/ by default; pass --with-sources to also delete raw/",
|
|
2560
|
+
description: "Remove a wiki and refresh the index. Preserves raw/ by default; pass --with-sources to also delete raw/",
|
|
2496
2561
|
},
|
|
2497
2562
|
args: {
|
|
2498
2563
|
name: { type: "positional", description: "Wiki name", required: true },
|
|
@@ -2514,8 +2579,10 @@ const wikiRemoveCommand = defineCommand({
|
|
|
2514
2579
|
}
|
|
2515
2580
|
const withSources = Boolean(args["with-sources"]);
|
|
2516
2581
|
const { removeWiki } = await import("./wiki.js");
|
|
2582
|
+
const { akmIndex } = await import("./indexer");
|
|
2517
2583
|
const stashDir = resolveStashDir();
|
|
2518
2584
|
const result = removeWiki(stashDir, args.name, { withSources });
|
|
2585
|
+
await akmIndex({ stashDir });
|
|
2519
2586
|
output("wiki-remove", result);
|
|
2520
2587
|
});
|
|
2521
2588
|
},
|
|
@@ -2540,7 +2607,7 @@ const wikiPagesCommand = defineCommand({
|
|
|
2540
2607
|
const wikiSearchCommand = defineCommand({
|
|
2541
2608
|
meta: {
|
|
2542
2609
|
name: "search",
|
|
2543
|
-
description: "Search wiki pages within a single wiki (scoped wrapper over `akm search --type wiki`; excludes raw/schema/index/log)",
|
|
2610
|
+
description: "Search wiki pages within a single wiki (scoped wrapper over `akm search --type wiki`; excludes raw/schema/index/log and returns canonical wiki refs)",
|
|
2544
2611
|
},
|
|
2545
2612
|
args: {
|
|
2546
2613
|
name: { type: "positional", description: "Wiki name", required: true },
|
|
@@ -2549,12 +2616,9 @@ const wikiSearchCommand = defineCommand({
|
|
|
2549
2616
|
},
|
|
2550
2617
|
run({ args }) {
|
|
2551
2618
|
return runWithJsonErrors(async () => {
|
|
2552
|
-
const { searchInWiki } = await import("./wiki.js");
|
|
2619
|
+
const { resolveWikiSource, searchInWiki } = await import("./wiki.js");
|
|
2553
2620
|
const stashDir = resolveStashDir();
|
|
2554
|
-
|
|
2555
|
-
if (!fs.existsSync(wikiDir)) {
|
|
2556
|
-
throw new NotFoundError(`Wiki not found: ${args.name}`);
|
|
2557
|
-
}
|
|
2621
|
+
resolveWikiSource(stashDir, args.name);
|
|
2558
2622
|
const parsedLimit = args.limit ? Number(args.limit) : undefined;
|
|
2559
2623
|
const limit = typeof parsedLimit === "number" && Number.isFinite(parsedLimit) && parsedLimit > 0 ? parsedLimit : undefined;
|
|
2560
2624
|
const response = await searchInWiki({ stashDir, wikiName: args.name, query: args.query, limit });
|
|
@@ -2633,6 +2697,7 @@ const wikiCommand = defineCommand({
|
|
|
2633
2697
|
},
|
|
2634
2698
|
subCommands: {
|
|
2635
2699
|
create: wikiCreateCommand,
|
|
2700
|
+
register: wikiRegisterCommand,
|
|
2636
2701
|
list: wikiListCommand,
|
|
2637
2702
|
show: wikiShowCommand,
|
|
2638
2703
|
remove: wikiRemoveCommand,
|
|
@@ -2695,7 +2760,18 @@ const main = defineCommand({
|
|
|
2695
2760
|
});
|
|
2696
2761
|
const CONFIG_SUBCOMMAND_SET = new Set(["path", "list", "get", "set", "unset"]);
|
|
2697
2762
|
const VAULT_SUBCOMMAND_SET = new Set(["list", "show", "create", "set", "unset", "load"]);
|
|
2698
|
-
const WIKI_SUBCOMMAND_SET = new Set([
|
|
2763
|
+
const WIKI_SUBCOMMAND_SET = new Set([
|
|
2764
|
+
"create",
|
|
2765
|
+
"register",
|
|
2766
|
+
"list",
|
|
2767
|
+
"show",
|
|
2768
|
+
"remove",
|
|
2769
|
+
"pages",
|
|
2770
|
+
"search",
|
|
2771
|
+
"stash",
|
|
2772
|
+
"lint",
|
|
2773
|
+
"ingest",
|
|
2774
|
+
]);
|
|
2699
2775
|
const SHOW_VIEW_MODES = new Set(["toc", "frontmatter", "full", "section", "lines"]);
|
|
2700
2776
|
// citty reads process.argv directly and does not accept a custom argv array,
|
|
2701
2777
|
// so we must replace process.argv with the normalized version before runMain.
|
|
@@ -2974,14 +3050,15 @@ ranking can learn from actual usage.
|
|
|
2974
3050
|
|
|
2975
3051
|
## Wikis
|
|
2976
3052
|
|
|
2977
|
-
Multi-wiki knowledge bases (Karpathy-style).
|
|
2978
|
-
\`<stashDir>/wikis/<name
|
|
2979
|
-
|
|
2980
|
-
|
|
3053
|
+
Multi-wiki knowledge bases (Karpathy-style). A stash-owned wiki lives at
|
|
3054
|
+
\`<stashDir>/wikis/<name>/\`; external directories or repos can also be registered
|
|
3055
|
+
as first-class wikis. akm owns lifecycle + raw-slug + lint + index regeneration
|
|
3056
|
+
for stash-owned wikis; page edits use your native Read/Write/Edit tools.
|
|
2981
3057
|
|
|
2982
3058
|
\`\`\`sh
|
|
2983
3059
|
akm wiki list # List wikis (name, pages, raws, last-modified)
|
|
2984
3060
|
akm wiki create research # Scaffold a new wiki
|
|
3061
|
+
akm wiki register ics-docs ~/code/ics-documentation # Register an external wiki
|
|
2985
3062
|
akm wiki show research # Path, description, counts, last 3 log entries
|
|
2986
3063
|
akm wiki pages research # Page refs + descriptions (excludes schema/index/log/raw)
|
|
2987
3064
|
akm wiki search research "attention" # Scoped search (equivalent to --type wiki --wiki research)
|
package/dist/indexer.js
CHANGED
|
@@ -80,7 +80,7 @@ export async function akmIndex(options) {
|
|
|
80
80
|
// doFullDelete=true merges the wipe into the same transaction as the
|
|
81
81
|
// inserts so readers never see an empty database mid-rebuild.
|
|
82
82
|
const doFullDelete = options?.full || !isIncremental;
|
|
83
|
-
const { scannedDirs, skippedDirs, generatedCount, dirsNeedingLlm } = await indexEntries(db, allStashSources, isIncremental, builtAtMs, doFullDelete);
|
|
83
|
+
const { scannedDirs, skippedDirs, generatedCount, dirsNeedingLlm, warnings } = await indexEntries(db, allStashSources, isIncremental, builtAtMs, doFullDelete);
|
|
84
84
|
onProgress({
|
|
85
85
|
phase: "scan",
|
|
86
86
|
message: `Scanned ${scannedDirs} ${scannedDirs === 1 ? "directory" : "directories"} and skipped ${skippedDirs}.`,
|
|
@@ -166,6 +166,7 @@ export async function akmIndex(options) {
|
|
|
166
166
|
mode: isIncremental ? "incremental" : "full",
|
|
167
167
|
directoriesScanned: scannedDirs,
|
|
168
168
|
directoriesSkipped: skippedDirs,
|
|
169
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
169
170
|
verification,
|
|
170
171
|
timing: {
|
|
171
172
|
totalMs: tEnd - t0,
|
|
@@ -185,6 +186,7 @@ async function indexEntries(db, allStashSources, isIncremental, builtAtMs, doFul
|
|
|
185
186
|
let scannedDirs = 0;
|
|
186
187
|
let skippedDirs = 0;
|
|
187
188
|
let generatedCount = 0;
|
|
189
|
+
const warnings = [];
|
|
188
190
|
const seenPaths = new Set();
|
|
189
191
|
const dirsNeedingLlm = [];
|
|
190
192
|
const dirRecords = [];
|
|
@@ -261,6 +263,8 @@ async function indexEntries(db, allStashSources, isIncremental, builtAtMs, doFul
|
|
|
261
263
|
const uncoveredFiles = files.filter((f) => !coveredFiles.has(path.basename(f)));
|
|
262
264
|
if (uncoveredFiles.length > 0) {
|
|
263
265
|
const generated = await generateMetadataFlat(currentStashDir, uncoveredFiles);
|
|
266
|
+
if (generated.warnings?.length)
|
|
267
|
+
warnings.push(...generated.warnings);
|
|
264
268
|
if (generated.entries.length > 0) {
|
|
265
269
|
stash = { entries: [...stash.entries, ...generated.entries] };
|
|
266
270
|
generatedCount += generated.entries.length;
|
|
@@ -269,6 +273,8 @@ async function indexEntries(db, allStashSources, isIncremental, builtAtMs, doFul
|
|
|
269
273
|
}
|
|
270
274
|
if (!stash) {
|
|
271
275
|
const generated = await generateMetadataFlat(currentStashDir, files);
|
|
276
|
+
if (generated.warnings?.length)
|
|
277
|
+
warnings.push(...generated.warnings);
|
|
272
278
|
if (generated.entries.length > 0) {
|
|
273
279
|
stash = { entries: generated.entries };
|
|
274
280
|
generatedCount += generated.entries.length;
|
|
@@ -351,7 +357,7 @@ async function indexEntries(db, allStashSources, isIncremental, builtAtMs, doFul
|
|
|
351
357
|
}
|
|
352
358
|
});
|
|
353
359
|
insertTransaction();
|
|
354
|
-
return { scannedDirs, skippedDirs, generatedCount, dirsNeedingLlm };
|
|
360
|
+
return { scannedDirs, skippedDirs, generatedCount, warnings, dirsNeedingLlm };
|
|
355
361
|
}
|
|
356
362
|
async function enhanceDirsWithLlm(db, config, dirsNeedingLlm) {
|
|
357
363
|
if (!config.llm || dirsNeedingLlm.length === 0)
|
package/dist/installed-kits.js
CHANGED
|
@@ -29,6 +29,7 @@ export async function akmListSources(input) {
|
|
|
29
29
|
sources.push({
|
|
30
30
|
name,
|
|
31
31
|
kind,
|
|
32
|
+
wiki: stash.wikiName,
|
|
32
33
|
path: stash.path,
|
|
33
34
|
provider: isRemote ? stash.type : undefined,
|
|
34
35
|
updatable: false,
|
|
@@ -44,6 +45,7 @@ export async function akmListSources(input) {
|
|
|
44
45
|
sources.push({
|
|
45
46
|
name: entry.id,
|
|
46
47
|
kind,
|
|
48
|
+
wiki: entry.wikiName,
|
|
47
49
|
path: entry.stashRoot,
|
|
48
50
|
ref: entry.ref,
|
|
49
51
|
version: entry.resolvedVersion,
|
package/dist/local-search.js
CHANGED
|
@@ -29,6 +29,15 @@ export function buildLocalAction(type, ref) {
|
|
|
29
29
|
const builder = ACTION_BUILDERS[type];
|
|
30
30
|
return builder ? builder(ref) : `akm show ${ref}`;
|
|
31
31
|
}
|
|
32
|
+
function resolveSearchHitRef(entry, refName, source) {
|
|
33
|
+
if (source?.wikiName) {
|
|
34
|
+
return makeAssetRef(entry.type, entry.name);
|
|
35
|
+
}
|
|
36
|
+
return makeAssetRef(entry.type, refName, source?.registryId);
|
|
37
|
+
}
|
|
38
|
+
function resolveSearchHitOrigin(source) {
|
|
39
|
+
return source?.wikiName ? null : (source?.registryId ?? null);
|
|
40
|
+
}
|
|
32
41
|
// ── Main search entrypoint ───────────────────────────────────────────────────
|
|
33
42
|
export async function searchLocal(input) {
|
|
34
43
|
const { query, searchType, limit, stashDir, sources, config } = input;
|
|
@@ -393,7 +402,7 @@ async function tryVecScores(db, query, k, config) {
|
|
|
393
402
|
}
|
|
394
403
|
// ── Substring fallback (no index) ───────────────────────────────────────────
|
|
395
404
|
async function substringSearch(query, searchType, limit, stashDir, sources, config) {
|
|
396
|
-
const assets = await indexAssets(stashDir, searchType);
|
|
405
|
+
const assets = await indexAssets(stashDir, searchType, sources);
|
|
397
406
|
const matched = assets.filter((asset) => !query || buildSearchText(asset.entry).includes(query));
|
|
398
407
|
if (!query) {
|
|
399
408
|
const sorted = matched.sort(compareAssets);
|
|
@@ -448,20 +457,21 @@ export async function buildDbHit(input) {
|
|
|
448
457
|
const score = Math.round(input.score * 10000) / 10000;
|
|
449
458
|
const whyMatched = buildWhyMatched(input.entry, input.query, input.rankingMode, qualityBoost, confidenceBoost, input.utilityBoosted);
|
|
450
459
|
const source = findSourceForPath(input.path, input.sources);
|
|
460
|
+
const ref = resolveSearchHitRef(input.entry, refName, source);
|
|
451
461
|
const editable = isEditable(input.path, input.config);
|
|
452
462
|
const estimatedTokens = typeof input.entry.fileSize === "number" ? Math.round(input.entry.fileSize / 4) : undefined;
|
|
453
463
|
const hit = {
|
|
454
464
|
type: input.entry.type,
|
|
455
465
|
name: input.entry.name,
|
|
456
466
|
path: input.path,
|
|
457
|
-
ref
|
|
458
|
-
origin: source
|
|
467
|
+
ref,
|
|
468
|
+
origin: resolveSearchHitOrigin(source),
|
|
459
469
|
editable,
|
|
460
470
|
...(!editable ? { editHint: buildEditHint(input.path, input.entry.type, refName, source?.registryId) } : {}),
|
|
461
471
|
description: input.entry.description,
|
|
462
472
|
tags: input.entry.tags,
|
|
463
473
|
size: deriveSize(input.entry.fileSize),
|
|
464
|
-
action: buildLocalAction(input.entry.type,
|
|
474
|
+
action: buildLocalAction(input.entry.type, ref),
|
|
465
475
|
score,
|
|
466
476
|
whyMatched,
|
|
467
477
|
...(estimatedTokens !== undefined ? { estimatedTokens } : {}),
|
|
@@ -523,7 +533,7 @@ rankingMode, qualityBoost, confidenceBoost, utilityBoosted) {
|
|
|
523
533
|
async function assetToSearchHit(asset, stashDir, sources, config, score) {
|
|
524
534
|
const source = findSourceForPath(asset.path, sources);
|
|
525
535
|
const editable = isEditable(asset.path, config);
|
|
526
|
-
const ref =
|
|
536
|
+
const ref = resolveSearchHitRef(asset.entry, asset.entry.name, source);
|
|
527
537
|
const fileSize = readFileSize(asset.path);
|
|
528
538
|
const size = deriveSize(fileSize);
|
|
529
539
|
const estimatedTokens = typeof fileSize === "number" ? Math.round(fileSize / 4) : undefined;
|
|
@@ -532,7 +542,7 @@ async function assetToSearchHit(asset, stashDir, sources, config, score) {
|
|
|
532
542
|
name: asset.entry.name,
|
|
533
543
|
path: asset.path,
|
|
534
544
|
ref,
|
|
535
|
-
origin: source
|
|
545
|
+
origin: resolveSearchHitOrigin(source),
|
|
536
546
|
editable,
|
|
537
547
|
...(!editable
|
|
538
548
|
? { editHint: buildEditHint(asset.path, asset.entry.type, asset.entry.name, source?.registryId) }
|
|
@@ -568,7 +578,12 @@ function readFileSize(filePath) {
|
|
|
568
578
|
return undefined;
|
|
569
579
|
}
|
|
570
580
|
}
|
|
571
|
-
async function indexAssets(stashDir, type) {
|
|
581
|
+
async function indexAssets(stashDir, type, sources) {
|
|
582
|
+
const resolvedStashDir = realpathOrResolve(stashDir);
|
|
583
|
+
const source = sources?.find((entry) => realpathOrResolve(entry.path) === resolvedStashDir);
|
|
584
|
+
if (source?.wikiName) {
|
|
585
|
+
return indexWikiRootAssets(stashDir, source.wikiName, type);
|
|
586
|
+
}
|
|
572
587
|
const assets = [];
|
|
573
588
|
const filterType = type === "any" ? undefined : type;
|
|
574
589
|
const fileContexts = walkStashFlat(stashDir);
|
|
@@ -626,6 +641,29 @@ async function indexAssets(stashDir, type) {
|
|
|
626
641
|
}
|
|
627
642
|
return assets;
|
|
628
643
|
}
|
|
644
|
+
async function indexWikiRootAssets(wikiRoot, wikiName, type) {
|
|
645
|
+
if (type !== "any" && type !== "wiki")
|
|
646
|
+
return [];
|
|
647
|
+
const assets = [];
|
|
648
|
+
for (const ctx of walkStashFlat(wikiRoot)) {
|
|
649
|
+
if (ctx.ext !== ".md")
|
|
650
|
+
continue;
|
|
651
|
+
if (!shouldIndexStashFile(wikiRoot, ctx.absPath, { treatStashRootAsWikiRoot: true }))
|
|
652
|
+
continue;
|
|
653
|
+
const relNoExt = ctx.relPath.replace(/\.md$/, "");
|
|
654
|
+
assets.push({
|
|
655
|
+
entry: {
|
|
656
|
+
name: `${wikiName}/${relNoExt}`,
|
|
657
|
+
type: "wiki",
|
|
658
|
+
filename: ctx.fileName,
|
|
659
|
+
description: ctx.frontmatter()?.description,
|
|
660
|
+
source: "frontmatter",
|
|
661
|
+
},
|
|
662
|
+
path: ctx.absPath,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
return assets;
|
|
666
|
+
}
|
|
629
667
|
function compareAssets(a, b) {
|
|
630
668
|
if (a.entry.type !== b.entry.type)
|
|
631
669
|
return a.entry.type.localeCompare(b.entry.type);
|
|
@@ -659,3 +697,11 @@ function deduplicateAssetsByPath(assets) {
|
|
|
659
697
|
return true;
|
|
660
698
|
});
|
|
661
699
|
}
|
|
700
|
+
function realpathOrResolve(targetPath) {
|
|
701
|
+
try {
|
|
702
|
+
return fs.realpathSync(targetPath);
|
|
703
|
+
}
|
|
704
|
+
catch {
|
|
705
|
+
return path.resolve(targetPath);
|
|
706
|
+
}
|
|
707
|
+
}
|
package/dist/metadata.js
CHANGED
|
@@ -362,6 +362,7 @@ function mergeParameters(existing, additional) {
|
|
|
362
362
|
// ── Metadata Generation ─────────────────────────────────────────────────────
|
|
363
363
|
export async function generateMetadata(dirPath, assetType, files, typeRoot = dirPath) {
|
|
364
364
|
const entries = [];
|
|
365
|
+
const warnings = [];
|
|
365
366
|
const pkgMeta = extractPackageMetadata(dirPath);
|
|
366
367
|
for (const file of files) {
|
|
367
368
|
const ext = path.extname(file).toLowerCase();
|
|
@@ -428,7 +429,13 @@ export async function generateMetadata(dirPath, assetType, files, typeRoot = dir
|
|
|
428
429
|
const renderer = await getRenderer(match.renderer);
|
|
429
430
|
if (renderer?.extractMetadata) {
|
|
430
431
|
const renderCtx = buildRenderContext(fileCtx, match, [typeRoot]);
|
|
431
|
-
|
|
432
|
+
try {
|
|
433
|
+
renderer.extractMetadata(entry, renderCtx);
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
warnings.push(buildMetadataSkipWarning(file, assetType, error));
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
432
439
|
}
|
|
433
440
|
}
|
|
434
441
|
// Priority 4: Filename heuristics (fallback)
|
|
@@ -447,7 +454,7 @@ export async function generateMetadata(dirPath, assetType, files, typeRoot = dir
|
|
|
447
454
|
entry.filename = path.basename(file);
|
|
448
455
|
entries.push(entry);
|
|
449
456
|
}
|
|
450
|
-
return { entries };
|
|
457
|
+
return warnings.length > 0 ? { entries, warnings } : { entries };
|
|
451
458
|
}
|
|
452
459
|
/**
|
|
453
460
|
* Generate metadata for files using the matcher system instead of a fixed asset type.
|
|
@@ -458,6 +465,7 @@ export async function generateMetadata(dirPath, assetType, files, typeRoot = dir
|
|
|
458
465
|
*/
|
|
459
466
|
export async function generateMetadataFlat(stashRoot, files) {
|
|
460
467
|
const entries = [];
|
|
468
|
+
const warnings = [];
|
|
461
469
|
const pkgMetaCache = new Map();
|
|
462
470
|
for (const file of files) {
|
|
463
471
|
if (!shouldIndexStashFile(stashRoot, file))
|
|
@@ -534,7 +542,13 @@ export async function generateMetadataFlat(stashRoot, files) {
|
|
|
534
542
|
const renderer = await getRenderer(match.renderer);
|
|
535
543
|
if (renderer?.extractMetadata) {
|
|
536
544
|
const renderCtx = buildRenderContext(ctx, match, [stashRoot]);
|
|
537
|
-
|
|
545
|
+
try {
|
|
546
|
+
renderer.extractMetadata(entry, renderCtx);
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
warnings.push(buildMetadataSkipWarning(file, assetType, error));
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
538
552
|
}
|
|
539
553
|
// Filename heuristics fallback
|
|
540
554
|
if (!entry.description) {
|
|
@@ -550,7 +564,13 @@ export async function generateMetadataFlat(stashRoot, files) {
|
|
|
550
564
|
entry.filename = path.basename(file);
|
|
551
565
|
entries.push(entry);
|
|
552
566
|
}
|
|
553
|
-
return { entries };
|
|
567
|
+
return warnings.length > 0 ? { entries, warnings } : { entries };
|
|
568
|
+
}
|
|
569
|
+
function buildMetadataSkipWarning(filePath, assetType, error) {
|
|
570
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
571
|
+
const warning = `Skipped malformed ${assetType} asset at ${filePath}: ${detail}`;
|
|
572
|
+
warn(warning);
|
|
573
|
+
return warning;
|
|
554
574
|
}
|
|
555
575
|
function normalizeTerms(values) {
|
|
556
576
|
const normalized = new Set();
|
package/dist/stash-add.js
CHANGED
|
@@ -9,7 +9,7 @@ import { detectStashRoot, installRegistryRef, upsertInstalledRegistryEntry } fro
|
|
|
9
9
|
import { parseRegistryRef } from "./registry-resolve";
|
|
10
10
|
import { ensureWebsiteMirror, validateWebsiteInputUrl } from "./stash-providers/website";
|
|
11
11
|
import { warn } from "./warn";
|
|
12
|
-
import { validateWikiName } from "./wiki";
|
|
12
|
+
import { ensureWikiNameAvailable, validateWikiName } from "./wiki";
|
|
13
13
|
const VALID_OVERRIDE_TYPES = new Set(["wiki"]);
|
|
14
14
|
export async function akmAdd(input) {
|
|
15
15
|
const ref = input.ref.trim();
|
|
@@ -47,6 +47,20 @@ export async function akmAdd(input) {
|
|
|
47
47
|
}
|
|
48
48
|
return addRegistryKit(ref, stashDir, input.trustThisInstall, input.writable, wikiName);
|
|
49
49
|
}
|
|
50
|
+
export async function registerWikiSource(input) {
|
|
51
|
+
const stashDir = resolveStashDir();
|
|
52
|
+
const name = input.name ?? deriveWikiNameFromRef(input.ref);
|
|
53
|
+
validateWikiName(name);
|
|
54
|
+
ensureWikiNameAvailable(stashDir, name);
|
|
55
|
+
return akmAdd({
|
|
56
|
+
ref: input.ref,
|
|
57
|
+
name,
|
|
58
|
+
overrideType: "wiki",
|
|
59
|
+
options: input.options,
|
|
60
|
+
trustThisInstall: input.trustThisInstall,
|
|
61
|
+
writable: input.writable,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
50
64
|
/**
|
|
51
65
|
* Add a local directory as a filesystem stash source.
|
|
52
66
|
* Creates a stashes[] entry instead of an installed[] entry.
|
|
@@ -58,31 +72,36 @@ async function addLocalStashSource(ref, sourcePath, stashDir, wikiName) {
|
|
|
58
72
|
// Check for duplicates in stashes[]
|
|
59
73
|
const stashes = [...(config.stashes ?? [])];
|
|
60
74
|
const existing = stashes.find((s) => s.type === "filesystem" && s.path && path.resolve(s.path) === resolvedPath);
|
|
75
|
+
let persistedEntry;
|
|
61
76
|
if (!existing) {
|
|
62
|
-
|
|
77
|
+
persistedEntry = {
|
|
63
78
|
type: "filesystem",
|
|
64
79
|
path: resolvedPath,
|
|
65
80
|
name: wikiName ?? toReadableId(resolvedPath),
|
|
66
81
|
...(wikiName ? { wikiName } : {}),
|
|
67
82
|
};
|
|
68
|
-
stashes.push(
|
|
83
|
+
stashes.push(persistedEntry);
|
|
69
84
|
saveConfig({ ...config, stashes });
|
|
70
85
|
}
|
|
71
|
-
else
|
|
72
|
-
existing.wikiName
|
|
73
|
-
|
|
86
|
+
else {
|
|
87
|
+
if (wikiName && existing.wikiName !== wikiName) {
|
|
88
|
+
existing.wikiName = wikiName;
|
|
89
|
+
saveConfig({ ...config, stashes });
|
|
90
|
+
}
|
|
91
|
+
persistedEntry = existing;
|
|
74
92
|
}
|
|
75
93
|
const index = await akmIndex({ stashDir });
|
|
76
94
|
const updatedConfig = loadConfig();
|
|
77
95
|
return {
|
|
78
96
|
schemaVersion: 1,
|
|
79
97
|
stashDir,
|
|
80
|
-
ref,
|
|
98
|
+
ref: wikiName ?? ref,
|
|
81
99
|
stashSource: {
|
|
82
100
|
type: "filesystem",
|
|
83
101
|
path: resolvedPath,
|
|
84
|
-
name: toReadableId(resolvedPath),
|
|
102
|
+
name: persistedEntry.name ?? toReadableId(resolvedPath),
|
|
85
103
|
stashRoot: resolvedPath,
|
|
104
|
+
...(persistedEntry.wikiName ? { wiki: persistedEntry.wikiName } : {}),
|
|
86
105
|
},
|
|
87
106
|
config: {
|
|
88
107
|
stashCount: updatedConfig.stashes?.length ?? 0,
|
|
@@ -93,6 +112,7 @@ async function addLocalStashSource(ref, sourcePath, stashDir, wikiName) {
|
|
|
93
112
|
totalEntries: index.totalEntries,
|
|
94
113
|
directoriesScanned: index.directoriesScanned,
|
|
95
114
|
directoriesSkipped: index.directoriesSkipped,
|
|
115
|
+
...(index.warnings?.length ? { warnings: index.warnings } : {}),
|
|
96
116
|
},
|
|
97
117
|
};
|
|
98
118
|
}
|
|
@@ -131,12 +151,13 @@ async function addWebsiteStashSource(ref, stashDir, name, options, wikiName) {
|
|
|
131
151
|
return {
|
|
132
152
|
schemaVersion: 1,
|
|
133
153
|
stashDir,
|
|
134
|
-
ref,
|
|
154
|
+
ref: wikiName ?? ref,
|
|
135
155
|
stashSource: {
|
|
136
156
|
type: "website",
|
|
137
157
|
url: normalizedUrl,
|
|
138
158
|
name: entry.name,
|
|
139
159
|
stashRoot: cachePaths.stashDir,
|
|
160
|
+
...(entry.wikiName ? { wiki: entry.wikiName } : {}),
|
|
140
161
|
},
|
|
141
162
|
config: {
|
|
142
163
|
stashCount: updatedConfig.stashes?.length ?? 0,
|
|
@@ -147,6 +168,7 @@ async function addWebsiteStashSource(ref, stashDir, name, options, wikiName) {
|
|
|
147
168
|
totalEntries: index.totalEntries,
|
|
148
169
|
directoriesScanned: index.directoriesScanned,
|
|
149
170
|
directoriesSkipped: index.directoriesSkipped,
|
|
171
|
+
...(index.warnings?.length ? { warnings: index.warnings } : {}),
|
|
150
172
|
},
|
|
151
173
|
};
|
|
152
174
|
}
|
|
@@ -213,6 +235,7 @@ async function addRegistryKit(ref, stashDir, trustThisInstall, writable, wikiNam
|
|
|
213
235
|
totalEntries: index.totalEntries,
|
|
214
236
|
directoriesScanned: index.directoriesScanned,
|
|
215
237
|
directoriesSkipped: index.directoriesSkipped,
|
|
238
|
+
...(index.warnings?.length ? { warnings: index.warnings } : {}),
|
|
216
239
|
},
|
|
217
240
|
};
|
|
218
241
|
}
|
package/dist/stash-show.js
CHANGED
|
@@ -22,11 +22,7 @@ import "./stash-providers/index";
|
|
|
22
22
|
* `/`, e.g. `wiki:research`.
|
|
23
23
|
*/
|
|
24
24
|
async function showWikiRoot(stashDir, wikiName) {
|
|
25
|
-
const { showWiki
|
|
26
|
-
const wikiDir = resolveWikiDir(stashDir, wikiName);
|
|
27
|
-
if (!fs.existsSync(wikiDir)) {
|
|
28
|
-
throw new NotFoundError(`Wiki not found: ${wikiName}. Run \`akm wiki create ${wikiName}\` to create it.`);
|
|
29
|
-
}
|
|
25
|
+
const { showWiki } = await import("./wiki.js");
|
|
30
26
|
const result = showWiki(stashDir, wikiName);
|
|
31
27
|
// Shape the WikiShowResult into a ShowResponse-compatible object.
|
|
32
28
|
// The payload mirrors what `akm wiki show <name>` returns.
|
|
@@ -43,6 +39,44 @@ async function showWikiRoot(stashDir, wikiName) {
|
|
|
43
39
|
recentLog: result.recentLog,
|
|
44
40
|
};
|
|
45
41
|
}
|
|
42
|
+
async function showWikiRootForSource(stashDir, source, wikiName) {
|
|
43
|
+
const { showWikiAtPath } = await import("./wiki.js");
|
|
44
|
+
if (source.wikiName === wikiName) {
|
|
45
|
+
const result = showWikiAtPath(wikiName, source.path);
|
|
46
|
+
return {
|
|
47
|
+
type: "wiki",
|
|
48
|
+
name: result.ref,
|
|
49
|
+
path: result.path,
|
|
50
|
+
...(result.description ? { description: result.description } : {}),
|
|
51
|
+
origin: null,
|
|
52
|
+
editable: false,
|
|
53
|
+
pages: result.pages,
|
|
54
|
+
raws: result.raws,
|
|
55
|
+
...(result.lastModified ? { lastModified: result.lastModified } : {}),
|
|
56
|
+
recentLog: result.recentLog,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return showWikiRoot(stashDir, wikiName);
|
|
60
|
+
}
|
|
61
|
+
function resolveRegisteredWikiAssetPath(wikiRoot, wikiName, assetName) {
|
|
62
|
+
const pageName = assetName === wikiName ? "" : assetName.slice(wikiName.length + 1);
|
|
63
|
+
if (!pageName) {
|
|
64
|
+
throw new NotFoundError(`Wiki page not found: wiki:${assetName}`);
|
|
65
|
+
}
|
|
66
|
+
const candidate = path.resolve(wikiRoot, `${pageName}.md`);
|
|
67
|
+
const resolvedRoot = fs.realpathSync(wikiRoot);
|
|
68
|
+
if (!candidate.startsWith(resolvedRoot + path.sep)) {
|
|
69
|
+
throw new UsageError("Ref resolves outside the stash root.");
|
|
70
|
+
}
|
|
71
|
+
if (!fs.existsSync(candidate) || !fs.statSync(candidate).isFile()) {
|
|
72
|
+
throw new NotFoundError(`Stash asset not found for ref: wiki:${assetName}`);
|
|
73
|
+
}
|
|
74
|
+
const realTarget = fs.realpathSync(candidate);
|
|
75
|
+
if (!realTarget.startsWith(resolvedRoot + path.sep)) {
|
|
76
|
+
throw new UsageError("Ref resolves outside the stash root.");
|
|
77
|
+
}
|
|
78
|
+
return realTarget;
|
|
79
|
+
}
|
|
46
80
|
/**
|
|
47
81
|
* Unified show: tries local FTS5 index first, then remote providers.
|
|
48
82
|
*
|
|
@@ -63,7 +97,7 @@ export async function akmShowUnified(input) {
|
|
|
63
97
|
let lastError;
|
|
64
98
|
for (const source of searchSources) {
|
|
65
99
|
try {
|
|
66
|
-
return await
|
|
100
|
+
return await showWikiRootForSource(allSources[0]?.path ?? source.path, source, parsed.name);
|
|
67
101
|
}
|
|
68
102
|
catch (err) {
|
|
69
103
|
if (!(err instanceof NotFoundError))
|
|
@@ -145,8 +179,19 @@ export async function showLocal(input) {
|
|
|
145
179
|
const searchSources = resolveSourcesForOrigin(parsed.origin, allSources);
|
|
146
180
|
const allStashDirs = searchSources.map((s) => s.path);
|
|
147
181
|
let assetPath;
|
|
182
|
+
const matchedSource = parsed.type === "wiki" ? searchSources.find((source) => parsed.name.startsWith(`${source.wikiName}/`)) : undefined;
|
|
148
183
|
let lastError;
|
|
184
|
+
if (parsed.type === "wiki" && matchedSource?.wikiName) {
|
|
185
|
+
try {
|
|
186
|
+
assetPath = resolveRegisteredWikiAssetPath(matchedSource.path, matchedSource.wikiName, parsed.name);
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
149
192
|
for (const dir of allStashDirs) {
|
|
193
|
+
if (assetPath)
|
|
194
|
+
break;
|
|
150
195
|
try {
|
|
151
196
|
assetPath = await resolveAssetPath(dir, parsed.type, parsed.name);
|
|
152
197
|
break;
|
|
@@ -165,14 +210,17 @@ export async function showLocal(input) {
|
|
|
165
210
|
new NotFoundError(`Stash asset not found for ref: ${displayType}:${parsed.name}. ` +
|
|
166
211
|
"Check the name with `akm search` or verify the asset exists in your stash."));
|
|
167
212
|
}
|
|
168
|
-
const source = findSourceForPath(assetPath, allSources);
|
|
213
|
+
const source = matchedSource ?? findSourceForPath(assetPath, allSources);
|
|
169
214
|
const sourceStashDir = source?.path ?? allStashDirs[0];
|
|
170
215
|
if (!sourceStashDir) {
|
|
171
216
|
throw new UsageError(`Could not determine stash root for asset: ${displayType}:${parsed.name}. ` +
|
|
172
217
|
"Run `akm init` to create the stash directory, or check `akm stash list` for configured paths.");
|
|
173
218
|
}
|
|
174
219
|
const fileCtx = buildFileContext(sourceStashDir, assetPath);
|
|
175
|
-
const
|
|
220
|
+
const forcedWikiMatch = parsed.type === "wiki" && source?.wikiName && parsed.name.startsWith(`${source.wikiName}/`)
|
|
221
|
+
? { type: "wiki", specificity: 20, renderer: "wiki-md", meta: {} }
|
|
222
|
+
: undefined;
|
|
223
|
+
const match = forcedWikiMatch ?? (await runMatchers(fileCtx));
|
|
176
224
|
if (!match) {
|
|
177
225
|
throw new UsageError(`Could not display asset "${displayType}:${parsed.name}" — unsupported file type or unrecognized layout`);
|
|
178
226
|
}
|
package/dist/wiki.js
CHANGED
|
@@ -16,8 +16,10 @@ import fs from "node:fs";
|
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import { parse as yamlParse } from "yaml";
|
|
18
18
|
import { isWithin } from "./common";
|
|
19
|
+
import { loadUserConfig, saveConfig } from "./config";
|
|
19
20
|
import { NotFoundError, UsageError } from "./errors";
|
|
20
21
|
import { parseFrontmatter, parseFrontmatterBlock } from "./frontmatter";
|
|
22
|
+
import { resolveStashSources } from "./search-source";
|
|
21
23
|
import { akmSearch } from "./stash-search";
|
|
22
24
|
import { buildIndexMd, buildLogMd, buildSchemaMd } from "./templates/wiki-templates";
|
|
23
25
|
// ── Constants ───────────────────────────────────────────────────────────────
|
|
@@ -58,6 +60,41 @@ export function extractWikiNameFromRef(ref) {
|
|
|
58
60
|
const match = ref.match(/^wiki:([a-z0-9][a-z0-9-]*)(?:\/|$)/);
|
|
59
61
|
return match?.[1];
|
|
60
62
|
}
|
|
63
|
+
function wikiNotFoundMessage(name) {
|
|
64
|
+
return `Wiki not found: ${name}. Run \`akm wiki create ${name}\` to create it or \`akm wiki register ${name} <path-or-repo>\` to register an external wiki.`;
|
|
65
|
+
}
|
|
66
|
+
function registeredWikiSources(stashDir) {
|
|
67
|
+
return resolveStashSources(stashDir)
|
|
68
|
+
.filter((source) => typeof source.wikiName === "string")
|
|
69
|
+
.map((source) => ({
|
|
70
|
+
name: source.wikiName,
|
|
71
|
+
path: source.path,
|
|
72
|
+
mode: "external",
|
|
73
|
+
source,
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
export function resolveWikiSource(stashDir, name) {
|
|
77
|
+
validateWikiName(name);
|
|
78
|
+
const wikiDir = resolveWikiDir(stashDir, name);
|
|
79
|
+
if (fs.existsSync(wikiDir)) {
|
|
80
|
+
return { name, path: wikiDir, mode: "stash" };
|
|
81
|
+
}
|
|
82
|
+
const external = registeredWikiSources(stashDir).find((source) => source.name === name);
|
|
83
|
+
if (external)
|
|
84
|
+
return external;
|
|
85
|
+
throw new NotFoundError(wikiNotFoundMessage(name));
|
|
86
|
+
}
|
|
87
|
+
export function ensureWikiNameAvailable(stashDir, name) {
|
|
88
|
+
validateWikiName(name);
|
|
89
|
+
const wikiDir = resolveWikiDir(stashDir, name);
|
|
90
|
+
if (fs.existsSync(wikiDir)) {
|
|
91
|
+
throw new UsageError(`Wiki already exists: ${name}.`);
|
|
92
|
+
}
|
|
93
|
+
const external = registeredWikiSources(stashDir).find((source) => source.name === name);
|
|
94
|
+
if (external) {
|
|
95
|
+
throw new UsageError(`Wiki already registered: ${name}.`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
61
98
|
/**
|
|
62
99
|
* Walk a wiki directory and bucket files into pages vs raws.
|
|
63
100
|
*
|
|
@@ -158,25 +195,20 @@ function toIsoDate(ms) {
|
|
|
158
195
|
*/
|
|
159
196
|
export function listWikis(stashDir) {
|
|
160
197
|
const wikisRoot = resolveWikisRoot(stashDir);
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
198
|
+
const summaries = new Map();
|
|
199
|
+
let entries = [];
|
|
200
|
+
if (fs.existsSync(wikisRoot)) {
|
|
201
|
+
try {
|
|
202
|
+
entries = fs.readdirSync(wikisRoot, { withFileTypes: true });
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
entries = [];
|
|
206
|
+
}
|
|
169
207
|
}
|
|
170
|
-
const
|
|
171
|
-
for (const entry of entries) {
|
|
172
|
-
if (!entry.isDirectory())
|
|
173
|
-
continue;
|
|
174
|
-
if (!WIKI_NAME_RE.test(entry.name))
|
|
175
|
-
continue;
|
|
176
|
-
const dir = path.join(wikisRoot, entry.name);
|
|
208
|
+
const summarize = (name, dir) => {
|
|
177
209
|
const buckets = scanWikiFiles(dir);
|
|
178
210
|
const summary = {
|
|
179
|
-
name
|
|
211
|
+
name,
|
|
180
212
|
path: dir,
|
|
181
213
|
pages: buckets.pages.length,
|
|
182
214
|
raws: buckets.raws.length,
|
|
@@ -186,10 +218,21 @@ export function listWikis(stashDir) {
|
|
|
186
218
|
summary.description = description;
|
|
187
219
|
if (buckets.lastModifiedMs !== undefined)
|
|
188
220
|
summary.lastModified = toIsoDate(buckets.lastModifiedMs);
|
|
189
|
-
summaries.
|
|
221
|
+
summaries.set(name, summary);
|
|
222
|
+
};
|
|
223
|
+
for (const entry of entries) {
|
|
224
|
+
if (!entry.isDirectory())
|
|
225
|
+
continue;
|
|
226
|
+
if (!WIKI_NAME_RE.test(entry.name))
|
|
227
|
+
continue;
|
|
228
|
+
summarize(entry.name, path.join(wikisRoot, entry.name));
|
|
229
|
+
}
|
|
230
|
+
for (const source of registeredWikiSources(stashDir)) {
|
|
231
|
+
if (summaries.has(source.name))
|
|
232
|
+
continue;
|
|
233
|
+
summarize(source.name, source.path);
|
|
190
234
|
}
|
|
191
|
-
summaries.sort((a, b) => a.name.localeCompare(b.name));
|
|
192
|
-
return summaries;
|
|
235
|
+
return Array.from(summaries.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
193
236
|
}
|
|
194
237
|
// ── Show ────────────────────────────────────────────────────────────────────
|
|
195
238
|
/**
|
|
@@ -238,11 +281,7 @@ function readRecentLog(wikiDir, limit = 3) {
|
|
|
238
281
|
// Newest-first convention: the top `limit` `##` blocks are the most recent.
|
|
239
282
|
return sections.slice(0, limit);
|
|
240
283
|
}
|
|
241
|
-
export function
|
|
242
|
-
const wikiDir = resolveWikiDir(stashDir, name);
|
|
243
|
-
if (!fs.existsSync(wikiDir)) {
|
|
244
|
-
throw new NotFoundError(`Wiki not found: ${name}. Run \`akm wiki create ${name}\` to create it.`);
|
|
245
|
-
}
|
|
284
|
+
export function showWikiAtPath(name, wikiDir) {
|
|
246
285
|
const buckets = scanWikiFiles(wikiDir);
|
|
247
286
|
const result = {
|
|
248
287
|
name,
|
|
@@ -259,8 +298,15 @@ export function showWiki(stashDir, name) {
|
|
|
259
298
|
result.lastModified = toIsoDate(buckets.lastModifiedMs);
|
|
260
299
|
return result;
|
|
261
300
|
}
|
|
301
|
+
export function showWiki(stashDir, name) {
|
|
302
|
+
return showWikiAtPath(name, resolveWikiSource(stashDir, name).path);
|
|
303
|
+
}
|
|
262
304
|
// ── Create ──────────────────────────────────────────────────────────────────
|
|
263
305
|
export function createWiki(stashDir, name) {
|
|
306
|
+
const existing = registeredWikiSources(stashDir).find((source) => source.name === name);
|
|
307
|
+
if (existing) {
|
|
308
|
+
throw new UsageError(`Wiki already registered: ${name}.`);
|
|
309
|
+
}
|
|
264
310
|
const wikiDir = resolveWikiDir(stashDir, name);
|
|
265
311
|
fs.mkdirSync(wikiDir, { recursive: true });
|
|
266
312
|
const files = [
|
|
@@ -307,7 +353,25 @@ export function createWiki(stashDir, name) {
|
|
|
307
353
|
* ignore that (e.g. idempotent cleanup) by catching.
|
|
308
354
|
*/
|
|
309
355
|
export function removeWiki(stashDir, name, options = {}) {
|
|
310
|
-
const
|
|
356
|
+
const resolved = resolveWikiSource(stashDir, name);
|
|
357
|
+
const wikiDir = resolved.path;
|
|
358
|
+
if (resolved.mode === "external") {
|
|
359
|
+
const config = loadUserConfig();
|
|
360
|
+
const stashes = (config.stashes ?? []).filter((entry) => entry.wikiName !== name);
|
|
361
|
+
const installed = (config.installed ?? []).filter((entry) => entry.wikiName !== name);
|
|
362
|
+
saveConfig({
|
|
363
|
+
...config,
|
|
364
|
+
stashes: stashes.length > 0 ? stashes : undefined,
|
|
365
|
+
installed: installed.length > 0 ? installed : undefined,
|
|
366
|
+
});
|
|
367
|
+
return {
|
|
368
|
+
name,
|
|
369
|
+
path: wikiDir,
|
|
370
|
+
removed: [],
|
|
371
|
+
preservedRaw: false,
|
|
372
|
+
unregistered: true,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
311
375
|
if (!fs.existsSync(wikiDir)) {
|
|
312
376
|
throw new NotFoundError(`Wiki not found: ${name}.`);
|
|
313
377
|
}
|
|
@@ -420,10 +484,7 @@ function readPageFrontmatter(absPath) {
|
|
|
420
484
|
* path, and frontmatter-derived fields for orientation.
|
|
421
485
|
*/
|
|
422
486
|
export function listPages(stashDir, name) {
|
|
423
|
-
const wikiDir =
|
|
424
|
-
if (!fs.existsSync(wikiDir)) {
|
|
425
|
-
throw new NotFoundError(`Wiki not found: ${name}.`);
|
|
426
|
-
}
|
|
487
|
+
const wikiDir = resolveWikiSource(stashDir, name).path;
|
|
427
488
|
const { pages } = scanWikiFiles(wikiDir);
|
|
428
489
|
const result = [];
|
|
429
490
|
for (const abs of pages) {
|
|
@@ -447,13 +508,22 @@ export function listPages(stashDir, name) {
|
|
|
447
508
|
*/
|
|
448
509
|
export async function searchInWiki(input) {
|
|
449
510
|
validateWikiName(input.wikiName);
|
|
450
|
-
const wikiDir = resolveWikiDir(input.stashDir, input.wikiName);
|
|
451
511
|
const response = await akmSearch({
|
|
452
512
|
query: input.query,
|
|
453
513
|
type: "wiki",
|
|
454
514
|
limit: input.limit,
|
|
455
515
|
source: "stash",
|
|
456
516
|
});
|
|
517
|
+
let wikiDir;
|
|
518
|
+
try {
|
|
519
|
+
wikiDir = resolveWikiSource(input.stashDir, input.wikiName).path;
|
|
520
|
+
}
|
|
521
|
+
catch (err) {
|
|
522
|
+
if (err instanceof NotFoundError) {
|
|
523
|
+
return { ...response, hits: [], registryHits: undefined };
|
|
524
|
+
}
|
|
525
|
+
throw err;
|
|
526
|
+
}
|
|
457
527
|
const rawDir = path.join(wikiDir, RAW_SUBDIR);
|
|
458
528
|
const filtered = [];
|
|
459
529
|
for (const hit of response.hits) {
|
|
@@ -564,10 +634,7 @@ function ensureTrailingNewline(value) {
|
|
|
564
634
|
* job (see `akm wiki ingest <name>` for the workflow).
|
|
565
635
|
*/
|
|
566
636
|
export function stashRaw(input) {
|
|
567
|
-
const wikiDir =
|
|
568
|
-
if (!fs.existsSync(wikiDir)) {
|
|
569
|
-
throw new NotFoundError(`Wiki not found: ${input.wikiName}. Run \`akm wiki create ${input.wikiName}\` first.`);
|
|
570
|
-
}
|
|
637
|
+
const wikiDir = resolveWikiSource(input.stashDir, input.wikiName).path;
|
|
571
638
|
const rawDir = path.join(wikiDir, RAW_SUBDIR);
|
|
572
639
|
fs.mkdirSync(rawDir, { recursive: true });
|
|
573
640
|
const baseSlug = slugifyForWiki(input.preferredName ?? deriveQueryFromSource(input.content) ?? "source");
|
|
@@ -598,10 +665,7 @@ export function stashRaw(input) {
|
|
|
598
665
|
* - `stale-index`: `index.md` mtime is older than the newest page mtime
|
|
599
666
|
*/
|
|
600
667
|
export function lintWiki(stashDir, name) {
|
|
601
|
-
const wikiDir =
|
|
602
|
-
if (!fs.existsSync(wikiDir)) {
|
|
603
|
-
throw new NotFoundError(`Wiki not found: ${name}.`);
|
|
604
|
-
}
|
|
668
|
+
const wikiDir = resolveWikiSource(stashDir, name).path;
|
|
605
669
|
const pages = listPages(stashDir, name);
|
|
606
670
|
const { raws, pagesLastModifiedMs } = scanWikiFiles(wikiDir);
|
|
607
671
|
const pageRefs = new Set(pages.map((p) => p.ref));
|
|
@@ -822,10 +886,7 @@ export function regenerateAllWikiIndexes(stashDir) {
|
|
|
822
886
|
* a verb here and in the printer stays colocated.
|
|
823
887
|
*/
|
|
824
888
|
export function buildIngestWorkflow(stashDir, name) {
|
|
825
|
-
const wikiDir =
|
|
826
|
-
if (!fs.existsSync(wikiDir)) {
|
|
827
|
-
throw new NotFoundError(`Wiki not found: ${name}. Run \`akm wiki create ${name}\` first.`);
|
|
828
|
-
}
|
|
889
|
+
const wikiDir = resolveWikiSource(stashDir, name).path;
|
|
829
890
|
const schemaPath = path.join(wikiDir, SCHEMA_MD);
|
|
830
891
|
const workflow = `# Ingest workflow for wiki:${name}
|
|
831
892
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.5.0-
|
|
3
|
+
"version": "0.5.0-rc4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "akm (Agent Kit Manager) — A package manager for AI agent skills, commands, tools, and knowledge. Works with Claude Code, OpenCode, Cursor, and any AI coding assistant.",
|
|
6
6
|
"keywords": [
|