akm-cli 0.5.0-rc2 → 0.5.0-rc3
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 +65 -15
- package/dist/local-search.js +38 -2
- package/dist/stash-add.js +15 -1
- package/dist/stash-show.js +56 -8
- package/dist/wiki.js +103 -42
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -651,7 +651,7 @@ function formatSearchPlain(r, detail) {
|
|
|
651
651
|
function formatWikiListPlain(r) {
|
|
652
652
|
const wikis = Array.isArray(r.wikis) ? r.wikis : [];
|
|
653
653
|
if (wikis.length === 0)
|
|
654
|
-
return "No wikis. Create one with `akm wiki create <name>`.";
|
|
654
|
+
return "No wikis. Create one with `akm wiki create <name>` or register one with `akm wiki register <name> <path-or-repo>`.";
|
|
655
655
|
const lines = ["NAME\tPAGES\tRAWS\tLAST-MODIFIED"];
|
|
656
656
|
for (const w of wikis) {
|
|
657
657
|
const name = typeof w.name === "string" ? w.name : "?";
|
|
@@ -1205,11 +1205,19 @@ const addCommand = defineCommand({
|
|
|
1205
1205
|
if (shouldWarnOnPlainHttp(ref)) {
|
|
1206
1206
|
warn("Warning: source URL uses plain HTTP (not HTTPS). For security, prefer https:// to protect against eavesdropping and tampering.");
|
|
1207
1207
|
}
|
|
1208
|
-
const websiteOptions =
|
|
1209
|
-
if (args
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1208
|
+
const websiteOptions = buildWebsiteOptions(args);
|
|
1209
|
+
if (args.type === "wiki") {
|
|
1210
|
+
const { registerWikiSource } = await import("./stash-add");
|
|
1211
|
+
const result = await registerWikiSource({
|
|
1212
|
+
ref,
|
|
1213
|
+
name: args.name,
|
|
1214
|
+
options: Object.keys(websiteOptions).length > 0 ? websiteOptions : undefined,
|
|
1215
|
+
trustThisInstall: args.trust,
|
|
1216
|
+
writable: args.writable,
|
|
1217
|
+
});
|
|
1218
|
+
output("add", result);
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1213
1221
|
const result = await akmAdd({
|
|
1214
1222
|
ref,
|
|
1215
1223
|
name: args.name,
|
|
@@ -1222,6 +1230,14 @@ const addCommand = defineCommand({
|
|
|
1222
1230
|
});
|
|
1223
1231
|
},
|
|
1224
1232
|
});
|
|
1233
|
+
function buildWebsiteOptions(args) {
|
|
1234
|
+
const websiteOptions = {};
|
|
1235
|
+
if (typeof args["max-pages"] === "string" && args["max-pages"].length > 0)
|
|
1236
|
+
websiteOptions.maxPages = args["max-pages"];
|
|
1237
|
+
if (typeof args["max-depth"] === "string" && args["max-depth"].length > 0)
|
|
1238
|
+
websiteOptions.maxDepth = args["max-depth"];
|
|
1239
|
+
return websiteOptions;
|
|
1240
|
+
}
|
|
1225
1241
|
const VALID_SOURCE_KINDS = new Set(["local", "managed", "remote"]);
|
|
1226
1242
|
function parseKindFilter(raw) {
|
|
1227
1243
|
if (!raw)
|
|
@@ -2464,6 +2480,41 @@ const wikiCreateCommand = defineCommand({
|
|
|
2464
2480
|
});
|
|
2465
2481
|
},
|
|
2466
2482
|
});
|
|
2483
|
+
const wikiRegisterCommand = defineCommand({
|
|
2484
|
+
meta: {
|
|
2485
|
+
name: "register",
|
|
2486
|
+
description: "Register an existing directory or repo as a first-class wiki without copying or mutating it",
|
|
2487
|
+
},
|
|
2488
|
+
args: {
|
|
2489
|
+
name: { type: "positional", description: "Wiki name (lowercase, digits, hyphens)", required: true },
|
|
2490
|
+
ref: { type: "positional", description: "Path or repo ref for the external wiki source", required: true },
|
|
2491
|
+
writable: {
|
|
2492
|
+
type: "boolean",
|
|
2493
|
+
description: "Mark a git-backed source as writable so changes can be pushed back",
|
|
2494
|
+
default: false,
|
|
2495
|
+
},
|
|
2496
|
+
trust: {
|
|
2497
|
+
type: "boolean",
|
|
2498
|
+
description: "Bypass install-audit blocking for this registration only",
|
|
2499
|
+
default: false,
|
|
2500
|
+
},
|
|
2501
|
+
"max-pages": { type: "string", description: "Maximum pages to crawl for website sources (default: 50)" },
|
|
2502
|
+
"max-depth": { type: "string", description: "Maximum crawl depth for website sources (default: 3)" },
|
|
2503
|
+
},
|
|
2504
|
+
run({ args }) {
|
|
2505
|
+
return runWithJsonErrors(async () => {
|
|
2506
|
+
const { registerWikiSource } = await import("./stash-add");
|
|
2507
|
+
const result = await registerWikiSource({
|
|
2508
|
+
ref: args.ref.trim(),
|
|
2509
|
+
name: args.name,
|
|
2510
|
+
options: Object.keys(buildWebsiteOptions(args)).length > 0 ? buildWebsiteOptions(args) : undefined,
|
|
2511
|
+
trustThisInstall: args.trust,
|
|
2512
|
+
writable: args.writable,
|
|
2513
|
+
});
|
|
2514
|
+
output("wiki-register", result);
|
|
2515
|
+
});
|
|
2516
|
+
},
|
|
2517
|
+
});
|
|
2467
2518
|
const wikiListCommand = defineCommand({
|
|
2468
2519
|
meta: { name: "list", description: "List wikis with page/raw counts and last-modified timestamps" },
|
|
2469
2520
|
run() {
|
|
@@ -2549,12 +2600,9 @@ const wikiSearchCommand = defineCommand({
|
|
|
2549
2600
|
},
|
|
2550
2601
|
run({ args }) {
|
|
2551
2602
|
return runWithJsonErrors(async () => {
|
|
2552
|
-
const { searchInWiki } = await import("./wiki.js");
|
|
2603
|
+
const { resolveWikiSource, searchInWiki } = await import("./wiki.js");
|
|
2553
2604
|
const stashDir = resolveStashDir();
|
|
2554
|
-
|
|
2555
|
-
if (!fs.existsSync(wikiDir)) {
|
|
2556
|
-
throw new NotFoundError(`Wiki not found: ${args.name}`);
|
|
2557
|
-
}
|
|
2605
|
+
resolveWikiSource(stashDir, args.name);
|
|
2558
2606
|
const parsedLimit = args.limit ? Number(args.limit) : undefined;
|
|
2559
2607
|
const limit = typeof parsedLimit === "number" && Number.isFinite(parsedLimit) && parsedLimit > 0 ? parsedLimit : undefined;
|
|
2560
2608
|
const response = await searchInWiki({ stashDir, wikiName: args.name, query: args.query, limit });
|
|
@@ -2633,6 +2681,7 @@ const wikiCommand = defineCommand({
|
|
|
2633
2681
|
},
|
|
2634
2682
|
subCommands: {
|
|
2635
2683
|
create: wikiCreateCommand,
|
|
2684
|
+
register: wikiRegisterCommand,
|
|
2636
2685
|
list: wikiListCommand,
|
|
2637
2686
|
show: wikiShowCommand,
|
|
2638
2687
|
remove: wikiRemoveCommand,
|
|
@@ -2974,14 +3023,15 @@ ranking can learn from actual usage.
|
|
|
2974
3023
|
|
|
2975
3024
|
## Wikis
|
|
2976
3025
|
|
|
2977
|
-
Multi-wiki knowledge bases (Karpathy-style).
|
|
2978
|
-
\`<stashDir>/wikis/<name
|
|
2979
|
-
|
|
2980
|
-
|
|
3026
|
+
Multi-wiki knowledge bases (Karpathy-style). A stash-owned wiki lives at
|
|
3027
|
+
\`<stashDir>/wikis/<name>/\`; external directories or repos can also be registered
|
|
3028
|
+
as first-class wikis. akm owns lifecycle + raw-slug + lint + index regeneration
|
|
3029
|
+
for stash-owned wikis; page edits use your native Read/Write/Edit tools.
|
|
2981
3030
|
|
|
2982
3031
|
\`\`\`sh
|
|
2983
3032
|
akm wiki list # List wikis (name, pages, raws, last-modified)
|
|
2984
3033
|
akm wiki create research # Scaffold a new wiki
|
|
3034
|
+
akm wiki register ics-docs ~/code/ics-documentation # Register an external wiki
|
|
2985
3035
|
akm wiki show research # Path, description, counts, last 3 log entries
|
|
2986
3036
|
akm wiki pages research # Page refs + descriptions (excludes schema/index/log/raw)
|
|
2987
3037
|
akm wiki search research "attention" # Scoped search (equivalent to --type wiki --wiki research)
|
package/dist/local-search.js
CHANGED
|
@@ -393,7 +393,7 @@ async function tryVecScores(db, query, k, config) {
|
|
|
393
393
|
}
|
|
394
394
|
// ── Substring fallback (no index) ───────────────────────────────────────────
|
|
395
395
|
async function substringSearch(query, searchType, limit, stashDir, sources, config) {
|
|
396
|
-
const assets = await indexAssets(stashDir, searchType);
|
|
396
|
+
const assets = await indexAssets(stashDir, searchType, sources);
|
|
397
397
|
const matched = assets.filter((asset) => !query || buildSearchText(asset.entry).includes(query));
|
|
398
398
|
if (!query) {
|
|
399
399
|
const sorted = matched.sort(compareAssets);
|
|
@@ -568,7 +568,12 @@ function readFileSize(filePath) {
|
|
|
568
568
|
return undefined;
|
|
569
569
|
}
|
|
570
570
|
}
|
|
571
|
-
async function indexAssets(stashDir, type) {
|
|
571
|
+
async function indexAssets(stashDir, type, sources) {
|
|
572
|
+
const resolvedStashDir = realpathOrResolve(stashDir);
|
|
573
|
+
const source = sources?.find((entry) => realpathOrResolve(entry.path) === resolvedStashDir);
|
|
574
|
+
if (source?.wikiName) {
|
|
575
|
+
return indexWikiRootAssets(stashDir, source.wikiName, type);
|
|
576
|
+
}
|
|
572
577
|
const assets = [];
|
|
573
578
|
const filterType = type === "any" ? undefined : type;
|
|
574
579
|
const fileContexts = walkStashFlat(stashDir);
|
|
@@ -626,6 +631,29 @@ async function indexAssets(stashDir, type) {
|
|
|
626
631
|
}
|
|
627
632
|
return assets;
|
|
628
633
|
}
|
|
634
|
+
async function indexWikiRootAssets(wikiRoot, wikiName, type) {
|
|
635
|
+
if (type !== "any" && type !== "wiki")
|
|
636
|
+
return [];
|
|
637
|
+
const assets = [];
|
|
638
|
+
for (const ctx of walkStashFlat(wikiRoot)) {
|
|
639
|
+
if (ctx.ext !== ".md")
|
|
640
|
+
continue;
|
|
641
|
+
if (!shouldIndexStashFile(wikiRoot, ctx.absPath, { treatStashRootAsWikiRoot: true }))
|
|
642
|
+
continue;
|
|
643
|
+
const relNoExt = ctx.relPath.replace(/\.md$/, "");
|
|
644
|
+
assets.push({
|
|
645
|
+
entry: {
|
|
646
|
+
name: `${wikiName}/${relNoExt}`,
|
|
647
|
+
type: "wiki",
|
|
648
|
+
filename: ctx.fileName,
|
|
649
|
+
description: ctx.frontmatter()?.description,
|
|
650
|
+
source: "frontmatter",
|
|
651
|
+
},
|
|
652
|
+
path: ctx.absPath,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
return assets;
|
|
656
|
+
}
|
|
629
657
|
function compareAssets(a, b) {
|
|
630
658
|
if (a.entry.type !== b.entry.type)
|
|
631
659
|
return a.entry.type.localeCompare(b.entry.type);
|
|
@@ -659,3 +687,11 @@ function deduplicateAssetsByPath(assets) {
|
|
|
659
687
|
return true;
|
|
660
688
|
});
|
|
661
689
|
}
|
|
690
|
+
function realpathOrResolve(targetPath) {
|
|
691
|
+
try {
|
|
692
|
+
return fs.realpathSync(targetPath);
|
|
693
|
+
}
|
|
694
|
+
catch {
|
|
695
|
+
return path.resolve(targetPath);
|
|
696
|
+
}
|
|
697
|
+
}
|
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.
|
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-rc3",
|
|
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": [
|