skillwiki 0.2.0-beta.2 → 0.2.0-beta.21
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/auto-update-bg.js +43 -0
- package/dist/chunk-XM5IYZX7.js +45 -0
- package/dist/cli.js +1350 -172
- package/package.json +18 -5
- package/skills/.claude-plugin/plugin.json +24 -0
- package/skills/README.md +10 -0
- package/skills/hooks/hooks.json +16 -0
- package/skills/hooks/run-hook.cmd +43 -0
- package/skills/hooks/session-start +29 -0
- package/skills/package.json +13 -0
- package/skills/proj-decide/SKILL.md +24 -0
- package/skills/proj-distill/SKILL.md +48 -0
- package/skills/proj-init/SKILL.md +29 -0
- package/skills/proj-work/SKILL.md +48 -0
- package/skills/using-skillwiki/SKILL.md +102 -0
- package/skills/wiki-adapter-prd/SKILL.md +87 -0
- package/skills/wiki-archive/SKILL.md +42 -0
- package/skills/wiki-audit/SKILL.md +33 -0
- package/skills/wiki-crystallize/SKILL.md +34 -0
- package/skills/wiki-ingest/SKILL.md +58 -0
- package/skills/wiki-init/SKILL.md +36 -0
- package/skills/wiki-lint/SKILL.md +33 -0
- package/skills/wiki-query/SKILL.md +40 -0
- package/skills/wiki-reingest/SKILL.md +54 -0
- package/templates/SCHEMA.md +18 -1
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
semverGt
|
|
4
|
+
} from "./chunk-XM5IYZX7.js";
|
|
2
5
|
|
|
3
6
|
// src/cli.ts
|
|
7
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
4
8
|
import { Command } from "commander";
|
|
5
9
|
|
|
6
10
|
// src/utils/output.ts
|
|
@@ -9,9 +13,14 @@ function printJson(r) {
|
|
|
9
13
|
}
|
|
10
14
|
function printHuman(r) {
|
|
11
15
|
if (r.ok) {
|
|
12
|
-
|
|
16
|
+
if (typeof r.data === "object" && r.data !== null && "humanHint" in r.data) {
|
|
17
|
+
process.stdout.write(`${r.data.humanHint}
|
|
18
|
+
`);
|
|
19
|
+
} else {
|
|
20
|
+
process.stdout.write(`OK
|
|
13
21
|
${formatData(r.data)}
|
|
14
22
|
`);
|
|
23
|
+
}
|
|
15
24
|
} else {
|
|
16
25
|
process.stdout.write(`ERR ${r.error}
|
|
17
26
|
${r.detail !== void 0 ? formatData(r.detail) + "\n" : ""}`);
|
|
@@ -53,7 +62,17 @@ var ExitCode = {
|
|
|
53
62
|
LINT_HAS_WARNINGS: 22,
|
|
54
63
|
LINT_HAS_ERRORS: 23,
|
|
55
64
|
ENV_WRITE_CONFLICT: 24,
|
|
56
|
-
NO_VAULT_CONFIGURED: 25
|
|
65
|
+
NO_VAULT_CONFIGURED: 25,
|
|
66
|
+
INVALID_CONFIG_KEY: 26,
|
|
67
|
+
CONFIG_WRITE_FAILED: 27,
|
|
68
|
+
DOCTOR_HAS_WARNINGS: 28,
|
|
69
|
+
DOCTOR_HAS_ERRORS: 29,
|
|
70
|
+
ARCHIVE_TARGET_NOT_FOUND: 30,
|
|
71
|
+
ARCHIVE_ALREADY_ARCHIVED: 31,
|
|
72
|
+
DRIFT_DETECTED: 32,
|
|
73
|
+
RAW_DEDUP_DETECTED: 33,
|
|
74
|
+
MIGRATION_APPLIED: 34,
|
|
75
|
+
UNKNOWN_WIKI_PROFILE: 35
|
|
57
76
|
};
|
|
58
77
|
|
|
59
78
|
// ../shared/src/json-output.ts
|
|
@@ -93,10 +112,10 @@ var TypedKnowledgeSchema = z.object({
|
|
|
93
112
|
});
|
|
94
113
|
var sha256Hex = z.string().regex(/^[0-9a-f]{64}$/);
|
|
95
114
|
var RawSourceSchema = z.object({
|
|
96
|
-
title: z.string().min(1),
|
|
97
|
-
source_url: z.string().
|
|
115
|
+
title: z.string().min(1).optional(),
|
|
116
|
+
source_url: z.string().nullable(),
|
|
98
117
|
ingested: isoDate,
|
|
99
|
-
ingested_by: z.enum(["wiki-ingest", "proj-work", "manual"]),
|
|
118
|
+
ingested_by: z.enum(["wiki-ingest", "proj-work", "manual"]).optional(),
|
|
100
119
|
sha256: sha256Hex,
|
|
101
120
|
project: wikilink.optional(),
|
|
102
121
|
work_item: wikilink.optional(),
|
|
@@ -231,7 +250,7 @@ async function runHash(input) {
|
|
|
231
250
|
const sha256 = createHash("sha256").update(bodyBytes).digest("hex");
|
|
232
251
|
return {
|
|
233
252
|
exitCode: ExitCode.OK,
|
|
234
|
-
result: ok({ path: input.file, sha256, byte_count: bodyBytes.byteLength })
|
|
253
|
+
result: ok({ path: input.file, sha256, byte_count: bodyBytes.byteLength, humanHint: sha256 })
|
|
235
254
|
};
|
|
236
255
|
}
|
|
237
256
|
|
|
@@ -261,7 +280,7 @@ function runFetchGuardSync(input) {
|
|
|
261
280
|
result: err("HOST_BLOCKED", { sanitized_url: sanitized, host: u.hostname })
|
|
262
281
|
};
|
|
263
282
|
}
|
|
264
|
-
return { exitCode: ExitCode.OK, result: ok({ allowed: true, sanitized_url: sanitized }) };
|
|
283
|
+
return { exitCode: ExitCode.OK, result: ok({ allowed: true, sanitized_url: sanitized, humanHint: `ALLOWED: ${sanitized}` }) };
|
|
265
284
|
}
|
|
266
285
|
function sanitizeUrl(u) {
|
|
267
286
|
const clone = new URL(u.toString());
|
|
@@ -301,17 +320,18 @@ async function runValidate(input) {
|
|
|
301
320
|
}
|
|
302
321
|
const det = detectSchema(fm.data);
|
|
303
322
|
if (!det.schema) {
|
|
304
|
-
return { exitCode: ExitCode.SCHEMA_NOT_DETECTED, result: ok({ schema: null, valid: false, errors: [] }) };
|
|
323
|
+
return { exitCode: ExitCode.SCHEMA_NOT_DETECTED, result: ok({ schema: null, valid: false, errors: [], humanHint: "schema not detected" }) };
|
|
305
324
|
}
|
|
306
325
|
const parsed = SCHEMAS[det.schema].safeParse(fm.data);
|
|
307
326
|
if (!parsed.success) {
|
|
308
327
|
const errors = parsed.error.issues.map((i) => ({ path: i.path.join("."), message: i.message }));
|
|
309
328
|
return {
|
|
310
329
|
exitCode: ExitCode.INVALID_FRONTMATTER,
|
|
311
|
-
result: ok({ schema: det.schema, valid: false, errors })
|
|
330
|
+
result: ok({ schema: det.schema, valid: false, errors, humanHint: `INVALID (${det.schema})
|
|
331
|
+
${errors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}` })
|
|
312
332
|
};
|
|
313
333
|
}
|
|
314
|
-
return { exitCode: ExitCode.OK, result: ok({ schema: det.schema, valid: true, errors: [] }) };
|
|
334
|
+
return { exitCode: ExitCode.OK, result: ok({ schema: det.schema, valid: true, errors: [], humanHint: `VALID (${det.schema})` }) };
|
|
315
335
|
}
|
|
316
336
|
|
|
317
337
|
// src/commands/graph.ts
|
|
@@ -397,7 +417,8 @@ async function runGraphBuild(input) {
|
|
|
397
417
|
}
|
|
398
418
|
return {
|
|
399
419
|
exitCode: ExitCode.OK,
|
|
400
|
-
result: ok({ out_path: input.out, node_count: scan.data.typedKnowledge.length, edge_count }
|
|
420
|
+
result: ok({ out_path: input.out, node_count: scan.data.typedKnowledge.length, edge_count, humanHint: `nodes: ${scan.data.typedKnowledge.length}, edges: ${edge_count}
|
|
421
|
+
written: ${input.out}` })
|
|
401
422
|
};
|
|
402
423
|
}
|
|
403
424
|
function computeAdamicAdar(adj) {
|
|
@@ -472,22 +493,29 @@ async function runOverlap(input) {
|
|
|
472
493
|
}
|
|
473
494
|
return { id, members, score };
|
|
474
495
|
});
|
|
475
|
-
|
|
496
|
+
const humanHint = clusters.length === 0 ? "no overlap clusters found" : clusters.map((c) => `cluster (${c.members.length} pages, score ${c.score}): ${c.members.join(", ")}`).join("\n");
|
|
497
|
+
return { exitCode: ExitCode.OK, result: ok({ clusters, humanHint }) };
|
|
476
498
|
}
|
|
477
499
|
|
|
478
500
|
// src/utils/wiki-path.ts
|
|
479
501
|
import { join as join2 } from "path";
|
|
480
502
|
|
|
481
503
|
// src/utils/dotenv.ts
|
|
482
|
-
import { readFile as readFile4 } from "fs/promises";
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
504
|
+
import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
505
|
+
import { dirname as dirname2 } from "path";
|
|
506
|
+
var CONFIG_KEYS = ["WIKI_PATH", "WIKI_LANG"];
|
|
507
|
+
var _whitelist = new Set(CONFIG_KEYS);
|
|
508
|
+
var PROFILE_PATH_RE = /^WIKI_([A-Z][A-Z0-9_]{0,31})_PATH$/;
|
|
509
|
+
var PROFILE_LANG_RE = /^WIKI_([A-Z][A-Z0-9_]{0,31})_LANG$/;
|
|
510
|
+
var PROFILE_DEFAULT_RE = /^WIKI_DEFAULT$/;
|
|
511
|
+
function isValidWikiProfileKey(key) {
|
|
512
|
+
if (key === "WIKI_PATH" || key === "WIKI_LANG") return false;
|
|
513
|
+
return PROFILE_PATH_RE.test(key) || PROFILE_LANG_RE.test(key) || PROFILE_DEFAULT_RE.test(key);
|
|
514
|
+
}
|
|
515
|
+
function profileKey(name, suffix) {
|
|
516
|
+
return `WIKI_${name.toUpperCase().replace(/-/g, "_").replace(/[^A-Z0-9_]/g, "")}_${suffix}`;
|
|
517
|
+
}
|
|
518
|
+
function parseDotenvText(text) {
|
|
491
519
|
const out = {};
|
|
492
520
|
for (const rawLine of text.split(/\r?\n/)) {
|
|
493
521
|
const line = rawLine.trim();
|
|
@@ -496,12 +524,65 @@ async function parseDotenvFile(path) {
|
|
|
496
524
|
if (eq <= 0) continue;
|
|
497
525
|
const key = line.slice(0, eq).trim();
|
|
498
526
|
const value = line.slice(eq + 1).trim();
|
|
499
|
-
if (!
|
|
527
|
+
if (!_whitelist.has(key) && !isValidWikiProfileKey(key)) continue;
|
|
500
528
|
if (value.length === 0) continue;
|
|
501
529
|
out[key] = value;
|
|
502
530
|
}
|
|
503
531
|
return out;
|
|
504
532
|
}
|
|
533
|
+
async function parseDotenvFile(path) {
|
|
534
|
+
let text;
|
|
535
|
+
try {
|
|
536
|
+
text = await readFile4(path, "utf8");
|
|
537
|
+
} catch {
|
|
538
|
+
return {};
|
|
539
|
+
}
|
|
540
|
+
return parseDotenvText(text);
|
|
541
|
+
}
|
|
542
|
+
async function writeDotenv(filePath, entries, originalContent) {
|
|
543
|
+
const lines = originalContent !== void 0 ? updateLines(originalContent, entries) : freshLines(entries);
|
|
544
|
+
await mkdir2(dirname2(filePath), { recursive: true });
|
|
545
|
+
await writeFile2(filePath, lines.join("\n") + "\n", "utf8");
|
|
546
|
+
}
|
|
547
|
+
function freshLines(entries) {
|
|
548
|
+
const out = [];
|
|
549
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
550
|
+
if (value !== void 0) out.push(`${key}=${value}`);
|
|
551
|
+
}
|
|
552
|
+
return out;
|
|
553
|
+
}
|
|
554
|
+
function updateLines(originalContent, entries) {
|
|
555
|
+
let rawLines = originalContent.split(/\r?\n/);
|
|
556
|
+
if (rawLines.length > 0 && rawLines[rawLines.length - 1] === "") {
|
|
557
|
+
rawLines = rawLines.slice(0, -1);
|
|
558
|
+
}
|
|
559
|
+
const keysToWrite = new Set(Object.keys(entries));
|
|
560
|
+
const out = [];
|
|
561
|
+
for (const line of rawLines) {
|
|
562
|
+
const trimmed = line.trim();
|
|
563
|
+
if (trimmed.length === 0 || trimmed.startsWith("#")) {
|
|
564
|
+
out.push(line);
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
const eq = trimmed.indexOf("=");
|
|
568
|
+
if (eq <= 0) {
|
|
569
|
+
out.push(line);
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
const key = trimmed.slice(0, eq).trim();
|
|
573
|
+
if (keysToWrite.has(key)) {
|
|
574
|
+
out.push(`${key}=${entries[key]}`);
|
|
575
|
+
keysToWrite.delete(key);
|
|
576
|
+
} else {
|
|
577
|
+
out.push(line);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
for (const key of keysToWrite) {
|
|
581
|
+
const value = entries[key];
|
|
582
|
+
if (value !== void 0) out.push(`${key}=${value}`);
|
|
583
|
+
}
|
|
584
|
+
return out;
|
|
585
|
+
}
|
|
505
586
|
|
|
506
587
|
// src/utils/wiki-path.ts
|
|
507
588
|
async function resolveInitTimePath(input) {
|
|
@@ -528,6 +609,14 @@ async function resolveInitTimePath(input) {
|
|
|
528
609
|
return { path: hermes.WIKI_PATH, source: "hermes-dotenv", ...input.explain ? { chain } : {} };
|
|
529
610
|
}
|
|
530
611
|
if (input.explain) chain.push({ source: "hermes-dotenv", matched: false });
|
|
612
|
+
if (input.cwd) {
|
|
613
|
+
const projCfg = await parseDotenvFile(join2(input.cwd, ".skillwiki", ".env"));
|
|
614
|
+
if (projCfg.WIKI_PATH !== void 0) {
|
|
615
|
+
if (input.explain) chain.push({ source: "project-dotenv", matched: true, value: projCfg.WIKI_PATH });
|
|
616
|
+
return { path: projCfg.WIKI_PATH, source: "project-dotenv", ...input.explain ? { chain } : {} };
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
if (input.explain) chain.push({ source: "project-dotenv", matched: false });
|
|
531
620
|
const fallback = join2(input.home, "wiki");
|
|
532
621
|
if (input.explain) chain.push({ source: "default", matched: true, value: fallback });
|
|
533
622
|
return { path: fallback, source: "default", ...input.explain ? { chain } : {} };
|
|
@@ -539,15 +628,73 @@ async function resolveRuntimePath(input) {
|
|
|
539
628
|
return ok({ path: input.flag, source: "flag", ...input.explain ? { chain } : {} });
|
|
540
629
|
}
|
|
541
630
|
if (input.explain) chain.push({ source: "flag", matched: false });
|
|
631
|
+
const swGlobal = await parseDotenvFile(join2(input.home, ".skillwiki", ".env"));
|
|
632
|
+
const wikiName = input.wiki;
|
|
633
|
+
if (wikiName !== void 0 && wikiName.length > 0) {
|
|
634
|
+
if (wikiName.toLowerCase() === "default") {
|
|
635
|
+
const path2 = swGlobal.WIKI_PATH;
|
|
636
|
+
if (path2 !== void 0) {
|
|
637
|
+
if (input.explain) chain.push({ source: "wiki-profile", matched: true, value: path2 });
|
|
638
|
+
return ok({ path: path2, source: "skillwiki-dotenv", ...input.explain ? { chain } : {} });
|
|
639
|
+
}
|
|
640
|
+
if (input.explain) chain.push({ source: "wiki-profile", matched: false });
|
|
641
|
+
return err("UNKNOWN_WIKI_PROFILE", {
|
|
642
|
+
message: `Wiki profile "default" not found. Set it with: skillwiki config set wiki.path <dir>`
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
const key = profileKey(wikiName, "PATH");
|
|
646
|
+
const path = swGlobal[key];
|
|
647
|
+
if (path !== void 0) {
|
|
648
|
+
if (input.explain) chain.push({ source: "wiki-profile", matched: true, value: path });
|
|
649
|
+
return ok({ path, source: "wiki-profile", ...input.explain ? { chain } : {} });
|
|
650
|
+
}
|
|
651
|
+
if (input.explain) chain.push({ source: "wiki-profile", matched: false });
|
|
652
|
+
return err("UNKNOWN_WIKI_PROFILE", {
|
|
653
|
+
message: `Wiki profile "${wikiName}" not found. Set it with: skillwiki config set wiki.${wikiName}.path <dir>`
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
if (input.wikiEnv !== void 0 && input.wikiEnv.length > 0) {
|
|
657
|
+
const key = profileKey(input.wikiEnv, "PATH");
|
|
658
|
+
const path = swGlobal[key];
|
|
659
|
+
if (path !== void 0) {
|
|
660
|
+
if (input.explain) chain.push({ source: "wiki-profile", matched: true, value: path });
|
|
661
|
+
return ok({ path, source: "wiki-profile", ...input.explain ? { chain } : {} });
|
|
662
|
+
}
|
|
663
|
+
if (input.explain) chain.push({ source: "wiki-profile", matched: false });
|
|
664
|
+
return err("UNKNOWN_WIKI_PROFILE", {
|
|
665
|
+
message: `Wiki profile "${input.wikiEnv}" not found (from $WIKI env). Set it with: skillwiki config set wiki.${input.wikiEnv}.path <dir>`
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
if (input.explain) chain.push({ source: "wiki-profile", matched: false });
|
|
542
669
|
if (input.envValue !== void 0 && input.envValue.length > 0) {
|
|
543
670
|
if (input.explain) chain.push({ source: "env", matched: true, value: input.envValue });
|
|
544
671
|
return ok({ path: input.envValue, source: "env", ...input.explain ? { chain } : {} });
|
|
545
672
|
}
|
|
546
673
|
if (input.explain) chain.push({ source: "env", matched: false });
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
if (
|
|
550
|
-
|
|
674
|
+
if (input.cwd) {
|
|
675
|
+
const projCfg = await parseDotenvFile(join2(input.cwd, ".skillwiki", ".env"));
|
|
676
|
+
if (projCfg.WIKI_PATH !== void 0) {
|
|
677
|
+
if (input.explain) chain.push({ source: "project-dotenv", matched: true, value: projCfg.WIKI_PATH });
|
|
678
|
+
return ok({ path: projCfg.WIKI_PATH, source: "project-dotenv", ...input.explain ? { chain } : {} });
|
|
679
|
+
}
|
|
680
|
+
if (input.explain) chain.push({ source: "project-dotenv", matched: false });
|
|
681
|
+
}
|
|
682
|
+
const defaultProfile = swGlobal["WIKI_DEFAULT"];
|
|
683
|
+
if (defaultProfile !== void 0) {
|
|
684
|
+
const key = profileKey(defaultProfile, "PATH");
|
|
685
|
+
const path = swGlobal[key];
|
|
686
|
+
if (path !== void 0) {
|
|
687
|
+
if (input.explain) chain.push({ source: "wiki-default", matched: true, value: path });
|
|
688
|
+
return ok({ path, source: "wiki-default", ...input.explain ? { chain } : {} });
|
|
689
|
+
}
|
|
690
|
+
if (input.explain) chain.push({ source: "wiki-default", matched: false });
|
|
691
|
+
return err("UNKNOWN_WIKI_PROFILE", {
|
|
692
|
+
message: `Default wiki profile "${defaultProfile}" not found. Set it with: skillwiki config set wiki.${defaultProfile}.path <dir>`
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
if (swGlobal.WIKI_PATH !== void 0) {
|
|
696
|
+
if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: true, value: swGlobal.WIKI_PATH });
|
|
697
|
+
return ok({ path: swGlobal.WIKI_PATH, source: "skillwiki-dotenv", ...input.explain ? { chain } : {} });
|
|
551
698
|
}
|
|
552
699
|
if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: false });
|
|
553
700
|
return err("NO_VAULT_CONFIGURED", {
|
|
@@ -561,15 +708,21 @@ async function runOrphans(input) {
|
|
|
561
708
|
if (input.vault) {
|
|
562
709
|
vault = input.vault;
|
|
563
710
|
} else {
|
|
564
|
-
const r = await resolveRuntimePath({ flag: void 0, envValue: input.envValue, home: input.home ?? "" });
|
|
565
|
-
if (!r.ok)
|
|
711
|
+
const r = await resolveRuntimePath({ flag: void 0, envValue: input.envValue, home: input.home ?? "", wiki: input.wiki });
|
|
712
|
+
if (!r.ok) {
|
|
713
|
+
const exitCode = r.error === "UNKNOWN_WIKI_PROFILE" ? ExitCode.UNKNOWN_WIKI_PROFILE : ExitCode.NO_VAULT_CONFIGURED;
|
|
714
|
+
return { exitCode, result: r };
|
|
715
|
+
}
|
|
566
716
|
vault = r.data.path;
|
|
567
717
|
}
|
|
568
718
|
const scan = await scanVault(vault);
|
|
569
719
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
570
720
|
const slugToPath = {};
|
|
571
721
|
for (const p of scan.data.typedKnowledge) {
|
|
572
|
-
|
|
722
|
+
const rel = p.relPath.replace(/\.md$/, "");
|
|
723
|
+
slugToPath[rel] = p.relPath;
|
|
724
|
+
const filename = rel.split("/").pop();
|
|
725
|
+
if (!(filename in slugToPath)) slugToPath[filename] = p.relPath;
|
|
573
726
|
}
|
|
574
727
|
const adj = {};
|
|
575
728
|
for (const p of scan.data.typedKnowledge) adj[p.relPath] = /* @__PURE__ */ new Set();
|
|
@@ -578,7 +731,7 @@ async function runOrphans(input) {
|
|
|
578
731
|
const split = splitFrontmatter(text);
|
|
579
732
|
const body = split.ok ? split.data.body : text;
|
|
580
733
|
for (const slug of extractBodyWikilinks(body)) {
|
|
581
|
-
const tgt = slugToPath[slug.split("/").pop()];
|
|
734
|
+
const tgt = slugToPath[slug] ?? slugToPath[slug.split("/").pop()];
|
|
582
735
|
if (tgt) {
|
|
583
736
|
adj[p.relPath].add(tgt);
|
|
584
737
|
adj[tgt].add(p.relPath);
|
|
@@ -609,7 +762,11 @@ async function runOrphans(input) {
|
|
|
609
762
|
}
|
|
610
763
|
}
|
|
611
764
|
}
|
|
612
|
-
|
|
765
|
+
const hintLines = [];
|
|
766
|
+
if (orphans.length > 0) hintLines.push(`orphans: ${orphans.length}`, ...orphans.map((o) => ` ${o}`));
|
|
767
|
+
if (bridges.length > 0) hintLines.push(`bridges: ${bridges.length}`, ...bridges.map((b) => ` ${b.path}`));
|
|
768
|
+
if (hintLines.length === 0) hintLines.push("no orphans or bridges");
|
|
769
|
+
return { exitCode: ExitCode.OK, result: ok({ orphans, bridges, humanHint: hintLines.join("\n") }) };
|
|
613
770
|
}
|
|
614
771
|
function simulateRemoval(adj, removed) {
|
|
615
772
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -630,20 +787,92 @@ function simulateRemoval(adj, removed) {
|
|
|
630
787
|
|
|
631
788
|
// src/commands/audit.ts
|
|
632
789
|
import { readFile as readFile5, stat as stat2 } from "fs/promises";
|
|
633
|
-
import { dirname as
|
|
790
|
+
import { dirname as dirname3, resolve, join as join3 } from "path";
|
|
634
791
|
|
|
635
792
|
// src/parsers/citations.ts
|
|
636
793
|
var FENCE2 = /```[\s\S]*?```/g;
|
|
794
|
+
var MARKER_RE = /\^\[(raw\/[^\]]+)\]/g;
|
|
795
|
+
function stripFences(body) {
|
|
796
|
+
return body.replace(FENCE2, "");
|
|
797
|
+
}
|
|
637
798
|
function extractCitationMarkers(body) {
|
|
638
|
-
const stripped = body
|
|
799
|
+
const stripped = stripFences(body);
|
|
639
800
|
const out = [];
|
|
640
|
-
const re = /\^\[(raw\/[^\]]+)\]/g;
|
|
641
801
|
let m;
|
|
642
|
-
while ((m =
|
|
802
|
+
while ((m = MARKER_RE.exec(stripped)) !== null) {
|
|
643
803
|
out.push({ marker: m[0], target: m[1] });
|
|
644
804
|
}
|
|
645
805
|
return out;
|
|
646
806
|
}
|
|
807
|
+
function hasSourcesFooter(body) {
|
|
808
|
+
return /^## Sources\s*$/m.test(stripFences(body));
|
|
809
|
+
}
|
|
810
|
+
function isLegacyCitationStyle(body) {
|
|
811
|
+
const markers = extractCitationMarkers(body);
|
|
812
|
+
if (markers.length === 0) return false;
|
|
813
|
+
if (!hasSourcesFooter(body)) return true;
|
|
814
|
+
const lines = stripFences(body).split("\n");
|
|
815
|
+
let inSources = false;
|
|
816
|
+
for (const line of lines) {
|
|
817
|
+
if (/^## Sources\b/.test(line.trim())) {
|
|
818
|
+
inSources = true;
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
if (inSources) continue;
|
|
822
|
+
const markerOnly = line.replace(MARKER_RE, "").trim();
|
|
823
|
+
if (markerOnly.length === 0 && /\^\[raw\//.test(line)) return true;
|
|
824
|
+
const lastMarkerIdx = line.lastIndexOf("^[raw/");
|
|
825
|
+
if (lastMarkerIdx >= 0) {
|
|
826
|
+
const afterLast = line.slice(lastMarkerIdx).replace(MARKER_RE, "").trim();
|
|
827
|
+
if (afterLast.length > 0) return true;
|
|
828
|
+
const beforeFirst = line.slice(0, line.indexOf("^[raw/")).trim();
|
|
829
|
+
if (beforeFirst.length > 0 && !/[.!?]\s*$/.test(beforeFirst)) return true;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return false;
|
|
833
|
+
}
|
|
834
|
+
function hasOrphanedCitations(body) {
|
|
835
|
+
const stripped = stripFences(body);
|
|
836
|
+
const lines = stripped.split("\n");
|
|
837
|
+
let inSources = false;
|
|
838
|
+
let sourcesEnded = false;
|
|
839
|
+
let sourcesStartLine = -1;
|
|
840
|
+
let lastNonBlankInSources = -1;
|
|
841
|
+
for (let i = 0; i < lines.length; i++) {
|
|
842
|
+
const line = lines[i];
|
|
843
|
+
const trimmed = line.trim();
|
|
844
|
+
if (/^## Sources\b/.test(trimmed)) {
|
|
845
|
+
inSources = true;
|
|
846
|
+
sourcesStartLine = i;
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
if (!inSources || sourcesEnded) continue;
|
|
850
|
+
if (trimmed.length === 0) {
|
|
851
|
+
if (lastNonBlankInSources >= 0) {
|
|
852
|
+
sourcesEnded = true;
|
|
853
|
+
}
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
const isListItem = /^\s*[-*]\s+/.test(line);
|
|
857
|
+
const hasMarker = /\^\[raw\//.test(line);
|
|
858
|
+
if (isListItem && hasMarker) {
|
|
859
|
+
lastNonBlankInSources = i;
|
|
860
|
+
} else if (hasMarker && !isListItem) {
|
|
861
|
+
return true;
|
|
862
|
+
} else {
|
|
863
|
+
sourcesEnded = true;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
if (sourcesStartLine === -1) return false;
|
|
867
|
+
if (sourcesEnded) {
|
|
868
|
+
for (let i = lastNonBlankInSources + 1; i < lines.length; i++) {
|
|
869
|
+
if (/\^\[raw\//.test(lines[i])) {
|
|
870
|
+
return true;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
647
876
|
|
|
648
877
|
// src/commands/audit.ts
|
|
649
878
|
async function runAudit(input) {
|
|
@@ -657,7 +886,7 @@ async function runAudit(input) {
|
|
|
657
886
|
if (!fm.ok) return { exitCode: ExitCode.INVALID_FRONTMATTER, result: fm };
|
|
658
887
|
const split = splitFrontmatter(text);
|
|
659
888
|
const body = split.ok ? split.data.body : text;
|
|
660
|
-
const vault = await findVaultRoot(
|
|
889
|
+
const vault = await findVaultRoot(dirname3(resolve(input.file)));
|
|
661
890
|
if (!vault) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID") };
|
|
662
891
|
const markers = extractCitationMarkers(body);
|
|
663
892
|
const resolved = await Promise.all(markers.map(async (m) => {
|
|
@@ -672,13 +901,36 @@ async function runAudit(input) {
|
|
|
672
901
|
const referenced = new Set(resolved.map((m) => m.target));
|
|
673
902
|
const unused_sources = sources.filter((s) => !referenced.has(s));
|
|
674
903
|
const missing_from_sources = [...referenced].filter((t) => !sources.includes(t));
|
|
904
|
+
const broken = resolved.filter((m) => !m.resolved);
|
|
905
|
+
const footerMatch = body.match(/\n## Sources\n([\s\S]*)$/);
|
|
906
|
+
let footer_consistency;
|
|
907
|
+
if (footerMatch) {
|
|
908
|
+
const footerTargets = /* @__PURE__ */ new Set();
|
|
909
|
+
const footerRe = /\^\[(raw\/[^\]]+)\]/g;
|
|
910
|
+
let mm;
|
|
911
|
+
while ((mm = footerRe.exec(footerMatch[1])) !== null) footerTargets.add(mm[1]);
|
|
912
|
+
const bodyTargets = new Set(resolved.map((m) => m.target));
|
|
913
|
+
const missing_from_footer = [...bodyTargets].filter((t) => !footerTargets.has(t));
|
|
914
|
+
const extra_in_footer = [...footerTargets].filter((t) => !bodyTargets.has(t));
|
|
915
|
+
footer_consistency = { missing_from_footer, extra_in_footer };
|
|
916
|
+
}
|
|
917
|
+
const hintLines = [];
|
|
918
|
+
hintLines.push(`markers: ${resolved.length}, broken: ${broken.length}`);
|
|
919
|
+
if (unused_sources.length > 0) hintLines.push(`unused_sources: ${unused_sources.length}`);
|
|
920
|
+
if (missing_from_sources.length > 0) hintLines.push(`missing_from_sources: ${missing_from_sources.length}`);
|
|
921
|
+
if (footer_consistency) {
|
|
922
|
+
if (footer_consistency.missing_from_footer.length > 0) hintLines.push(`missing_from_footer: ${footer_consistency.missing_from_footer.length}`);
|
|
923
|
+
if (footer_consistency.extra_in_footer.length > 0) hintLines.push(`extra_in_footer: ${footer_consistency.extra_in_footer.length}`);
|
|
924
|
+
}
|
|
925
|
+
if (broken.length === 0 && unused_sources.length === 0 && missing_from_sources.length === 0) hintLines.push("OK");
|
|
926
|
+
const humanHint = hintLines.join("\n");
|
|
675
927
|
if (resolved.some((m) => !m.resolved)) {
|
|
676
|
-
return { exitCode: ExitCode.UNRESOLVED_MARKERS, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources } }) };
|
|
928
|
+
return { exitCode: ExitCode.UNRESOLVED_MARKERS, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, footer_consistency, humanHint }) };
|
|
677
929
|
}
|
|
678
930
|
if (unused_sources.length > 0 || missing_from_sources.length > 0) {
|
|
679
|
-
return { exitCode: ExitCode.SOURCES_INCONSISTENT, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources } }) };
|
|
931
|
+
return { exitCode: ExitCode.SOURCES_INCONSISTENT, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, footer_consistency, humanHint }) };
|
|
680
932
|
}
|
|
681
|
-
return { exitCode: ExitCode.OK, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources } }) };
|
|
933
|
+
return { exitCode: ExitCode.OK, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, footer_consistency, humanHint }) };
|
|
682
934
|
}
|
|
683
935
|
async function findVaultRoot(start) {
|
|
684
936
|
let cur = start;
|
|
@@ -688,7 +940,7 @@ async function findVaultRoot(start) {
|
|
|
688
940
|
return cur;
|
|
689
941
|
} catch {
|
|
690
942
|
}
|
|
691
|
-
const parent =
|
|
943
|
+
const parent = dirname3(cur);
|
|
692
944
|
if (parent === cur) return null;
|
|
693
945
|
cur = parent;
|
|
694
946
|
}
|
|
@@ -700,10 +952,10 @@ import { readdir as readdir2, stat as stat4 } from "fs/promises";
|
|
|
700
952
|
import { join as join4 } from "path";
|
|
701
953
|
|
|
702
954
|
// src/utils/install-fs.ts
|
|
703
|
-
import { copyFile, mkdir as
|
|
704
|
-
import { dirname as
|
|
955
|
+
import { copyFile, mkdir as mkdir3, rename, writeFile as writeFile3, stat as stat3 } from "fs/promises";
|
|
956
|
+
import { dirname as dirname4 } from "path";
|
|
705
957
|
async function atomicCopyWithBackup(src, dst) {
|
|
706
|
-
await
|
|
958
|
+
await mkdir3(dirname4(dst), { recursive: true });
|
|
707
959
|
let backupPath = null;
|
|
708
960
|
try {
|
|
709
961
|
await stat3(dst);
|
|
@@ -721,9 +973,9 @@ async function atomicCopyWithBackup(src, dst) {
|
|
|
721
973
|
return ok({ copied: true, backupPath });
|
|
722
974
|
}
|
|
723
975
|
async function writeManifest(path, m) {
|
|
724
|
-
await
|
|
976
|
+
await mkdir3(dirname4(path), { recursive: true });
|
|
725
977
|
const enriched = { installed_at: (/* @__PURE__ */ new Date()).toISOString(), ...m };
|
|
726
|
-
await
|
|
978
|
+
await writeFile3(path, JSON.stringify(enriched, null, 2));
|
|
727
979
|
}
|
|
728
980
|
|
|
729
981
|
// src/commands/install.ts
|
|
@@ -758,7 +1010,12 @@ async function runInstall(input) {
|
|
|
758
1010
|
}
|
|
759
1011
|
const manifest_path = join4(input.target, "wiki-manifest.json");
|
|
760
1012
|
if (!input.dryRun) await writeManifest(manifest_path, { installed, backed_up });
|
|
761
|
-
|
|
1013
|
+
const hintLines = [
|
|
1014
|
+
`installed: ${installed.length}`,
|
|
1015
|
+
input.dryRun ? "(dry run)" : `backed up: ${backed_up.length}`,
|
|
1016
|
+
`manifest: ${manifest_path}`
|
|
1017
|
+
];
|
|
1018
|
+
return { exitCode: ExitCode.OK, result: ok({ installed, backed_up, manifest_path, humanHint: hintLines.join("\n") }) };
|
|
762
1019
|
}
|
|
763
1020
|
|
|
764
1021
|
// src/commands/path.ts
|
|
@@ -770,16 +1027,20 @@ async function runPath(input) {
|
|
|
770
1027
|
home: input.home,
|
|
771
1028
|
explain: input.explain
|
|
772
1029
|
});
|
|
773
|
-
return { exitCode: ExitCode.OK, result: ok({ path: r2.path, source: r2.source, ...r2.chain ? { chain: r2.chain } : {} }) };
|
|
1030
|
+
return { exitCode: ExitCode.OK, result: ok({ path: r2.path, source: r2.source, ...r2.chain ? { chain: r2.chain } : {}, humanHint: `${r2.path} (via ${r2.source})` }) };
|
|
774
1031
|
}
|
|
775
1032
|
const r = await resolveRuntimePath({
|
|
776
1033
|
flag: input.flag,
|
|
777
1034
|
envValue: input.envValue,
|
|
778
1035
|
home: input.home,
|
|
1036
|
+
wiki: input.wiki,
|
|
779
1037
|
explain: input.explain
|
|
780
1038
|
});
|
|
781
|
-
if (!r.ok)
|
|
782
|
-
|
|
1039
|
+
if (!r.ok) {
|
|
1040
|
+
const exitCode = r.error === "UNKNOWN_WIKI_PROFILE" ? ExitCode.UNKNOWN_WIKI_PROFILE : ExitCode.NO_VAULT_CONFIGURED;
|
|
1041
|
+
return { exitCode, result: r };
|
|
1042
|
+
}
|
|
1043
|
+
return { exitCode: ExitCode.OK, result: ok({ path: r.data.path, source: r.data.source, ...r.data.chain ? { chain: r.data.chain } : {}, humanHint: `${r.data.path} (via ${r.data.source})` }) };
|
|
783
1044
|
}
|
|
784
1045
|
|
|
785
1046
|
// src/utils/lang.ts
|
|
@@ -833,14 +1094,42 @@ async function runLang(input) {
|
|
|
833
1094
|
value: resolved.value,
|
|
834
1095
|
source: resolved.source,
|
|
835
1096
|
canonical: resolved.canonical,
|
|
836
|
-
...chain ? { chain } : {}
|
|
1097
|
+
...chain ? { chain } : {},
|
|
1098
|
+
humanHint: `${resolved.value} (via ${resolved.source})`
|
|
837
1099
|
})
|
|
838
1100
|
};
|
|
839
1101
|
}
|
|
840
1102
|
|
|
841
1103
|
// src/commands/init.ts
|
|
842
|
-
import { mkdir as
|
|
843
|
-
import { join as join7
|
|
1104
|
+
import { mkdir as mkdir4, readFile as readFile6, readdir as readdir3, writeFile as writeFile4 } from "fs/promises";
|
|
1105
|
+
import { join as join7 } from "path";
|
|
1106
|
+
|
|
1107
|
+
// src/parsers/taxonomy.ts
|
|
1108
|
+
import yaml2 from "js-yaml";
|
|
1109
|
+
var FENCE_RE = /^##\s+Tag Taxonomy\s*$[\s\S]*?```yaml\s*\n([\s\S]*?)\n```/m;
|
|
1110
|
+
function extractTaxonomy(schemaText) {
|
|
1111
|
+
const m = schemaText.match(FENCE_RE);
|
|
1112
|
+
if (!m) return err("NO_TAXONOMY_BLOCK", { message: "No fenced YAML taxonomy block found in SCHEMA.md" });
|
|
1113
|
+
let parsed;
|
|
1114
|
+
try {
|
|
1115
|
+
parsed = yaml2.load(m[1], { schema: yaml2.JSON_SCHEMA });
|
|
1116
|
+
} catch (e) {
|
|
1117
|
+
return err("INVALID_FRONTMATTER", { message: e.message });
|
|
1118
|
+
}
|
|
1119
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
1120
|
+
return err("INVALID_FRONTMATTER", { message: "taxonomy block is not an object" });
|
|
1121
|
+
}
|
|
1122
|
+
const tax = parsed.taxonomy;
|
|
1123
|
+
if (!Array.isArray(tax)) {
|
|
1124
|
+
return err("INVALID_FRONTMATTER", { message: "taxonomy key missing or not an array" });
|
|
1125
|
+
}
|
|
1126
|
+
if (!tax.every((x) => typeof x === "string")) {
|
|
1127
|
+
return err("INVALID_FRONTMATTER", { message: "taxonomy must be a list of strings" });
|
|
1128
|
+
}
|
|
1129
|
+
return ok(tax);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// src/commands/init.ts
|
|
844
1133
|
var DEFAULT_TAXONOMY = [
|
|
845
1134
|
"research",
|
|
846
1135
|
"comparison",
|
|
@@ -865,33 +1154,67 @@ var VAULT_DIRS = [
|
|
|
865
1154
|
"meta",
|
|
866
1155
|
"projects"
|
|
867
1156
|
];
|
|
1157
|
+
function extractDomainFromSchema(text) {
|
|
1158
|
+
const m = text.match(/^##\s+Domain\s*\n([\s\S]*?)(?=\n\n|\n##|\s*$)/m);
|
|
1159
|
+
if (!m) return "";
|
|
1160
|
+
const d = m[1].trim();
|
|
1161
|
+
return d.startsWith("##") ? "" : d;
|
|
1162
|
+
}
|
|
1163
|
+
async function discoverTagsFromPages(target, knownSlugs) {
|
|
1164
|
+
const knownSet = new Set(knownSlugs);
|
|
1165
|
+
const discovered = /* @__PURE__ */ new Set();
|
|
1166
|
+
for (const dir of ["entities", "concepts", "comparisons", "queries"]) {
|
|
1167
|
+
let entries;
|
|
1168
|
+
try {
|
|
1169
|
+
entries = (await readdir3(join7(target, dir), { withFileTypes: true })).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name);
|
|
1170
|
+
} catch {
|
|
1171
|
+
continue;
|
|
1172
|
+
}
|
|
1173
|
+
for (const file of entries) {
|
|
1174
|
+
try {
|
|
1175
|
+
const text = await readFile6(join7(target, dir, file), "utf8");
|
|
1176
|
+
const fm = extractFrontmatter(text);
|
|
1177
|
+
if (!fm.ok || !fm.data.tags || !Array.isArray(fm.data.tags)) continue;
|
|
1178
|
+
for (const t of fm.data.tags) {
|
|
1179
|
+
if (typeof t === "string" && !knownSet.has(t)) discovered.add(t);
|
|
1180
|
+
}
|
|
1181
|
+
} catch {
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
return [...discovered].sort();
|
|
1186
|
+
}
|
|
868
1187
|
async function runInit(input) {
|
|
869
1188
|
const pathRes = await resolveInitTimePath({ flag: input.flag, envValue: input.envValue, home: input.home });
|
|
870
1189
|
const target = pathRes.path;
|
|
871
1190
|
const langRes = await resolveLang({ flag: input.lang, envValue: void 0, home: input.home });
|
|
872
1191
|
const canonicalLang = langRes.canonical;
|
|
873
|
-
let
|
|
1192
|
+
let oldSchemaText;
|
|
874
1193
|
try {
|
|
875
|
-
await
|
|
876
|
-
hasSchema = true;
|
|
1194
|
+
oldSchemaText = await readFile6(join7(target, "SCHEMA.md"), "utf8");
|
|
877
1195
|
} catch {
|
|
878
1196
|
}
|
|
879
|
-
if (
|
|
1197
|
+
if (oldSchemaText && !input.force) {
|
|
880
1198
|
return {
|
|
881
1199
|
exitCode: ExitCode.INIT_TARGET_NOT_EMPTY,
|
|
882
1200
|
result: err("INIT_TARGET_NOT_EMPTY", { target })
|
|
883
1201
|
};
|
|
884
1202
|
}
|
|
885
1203
|
const envPath = join7(input.home, ".skillwiki", ".env");
|
|
886
|
-
|
|
1204
|
+
let existingEnvRaw = "";
|
|
1205
|
+
try {
|
|
1206
|
+
existingEnvRaw = await readFile6(envPath, "utf8");
|
|
1207
|
+
} catch {
|
|
1208
|
+
}
|
|
1209
|
+
const existingEnv = parseDotenvText(existingEnvRaw);
|
|
887
1210
|
const swDotenvHadPath = existingEnv.WIKI_PATH !== void 0;
|
|
888
|
-
if (existingEnv.WIKI_PATH !== void 0 && existingEnv.WIKI_PATH !== target && !input.force) {
|
|
1211
|
+
if (!input.profile && existingEnv.WIKI_PATH !== void 0 && existingEnv.WIKI_PATH !== target && !input.force) {
|
|
889
1212
|
return {
|
|
890
1213
|
exitCode: ExitCode.ENV_WRITE_CONFLICT,
|
|
891
1214
|
result: err("ENV_WRITE_CONFLICT", { key: "WIKI_PATH", existing: existingEnv.WIKI_PATH, attempted: target })
|
|
892
1215
|
};
|
|
893
1216
|
}
|
|
894
|
-
if (existingEnv.WIKI_LANG !== void 0 && existingEnv.WIKI_LANG !== canonicalLang && !input.force) {
|
|
1217
|
+
if (!input.profile && existingEnv.WIKI_LANG !== void 0 && existingEnv.WIKI_LANG !== canonicalLang && !input.force) {
|
|
895
1218
|
return {
|
|
896
1219
|
exitCode: ExitCode.ENV_WRITE_CONFLICT,
|
|
897
1220
|
result: err("ENV_WRITE_CONFLICT", { key: "WIKI_LANG", existing: existingEnv.WIKI_LANG, attempted: canonicalLang })
|
|
@@ -899,73 +1222,134 @@ async function runInit(input) {
|
|
|
899
1222
|
}
|
|
900
1223
|
const created = [];
|
|
901
1224
|
try {
|
|
902
|
-
await
|
|
1225
|
+
await mkdir4(target, { recursive: true });
|
|
903
1226
|
for (const d of VAULT_DIRS) {
|
|
904
|
-
await
|
|
1227
|
+
await mkdir4(join7(target, d), { recursive: true });
|
|
905
1228
|
created.push(d + "/");
|
|
906
1229
|
}
|
|
907
1230
|
} catch (e) {
|
|
908
1231
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
|
|
909
1232
|
}
|
|
910
1233
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
911
|
-
|
|
912
|
-
|
|
1234
|
+
let taxonomy = input.taxonomy && input.taxonomy.length > 0 ? input.taxonomy : DEFAULT_TAXONOMY;
|
|
1235
|
+
let domain = input.domain;
|
|
1236
|
+
let oldTaxonomy = [];
|
|
1237
|
+
if (oldSchemaText) {
|
|
1238
|
+
if (!domain) {
|
|
1239
|
+
const oldDomain = extractDomainFromSchema(oldSchemaText);
|
|
1240
|
+
if (oldDomain) domain = oldDomain;
|
|
1241
|
+
}
|
|
1242
|
+
const oldTax = extractTaxonomy(oldSchemaText);
|
|
1243
|
+
if (oldTax.ok) oldTaxonomy = oldTax.data;
|
|
1244
|
+
}
|
|
1245
|
+
const taxonomySet = new Set(taxonomy);
|
|
1246
|
+
for (const t of oldTaxonomy) {
|
|
1247
|
+
if (!taxonomySet.has(t)) {
|
|
1248
|
+
taxonomy.push(t);
|
|
1249
|
+
taxonomySet.add(t);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
const discovered = await discoverTagsFromPages(target, taxonomy);
|
|
1253
|
+
const discovered_tags = discovered.length;
|
|
1254
|
+
const fullTaxonomyYaml = discovered.length > 0 ? taxonomy.map((t) => ` - ${t}`).join("\n") + "\n # --- Discovered from existing pages ---\n" + discovered.map((t) => ` - ${t}`).join("\n") : taxonomy.map((t) => ` - ${t}`).join("\n");
|
|
913
1255
|
try {
|
|
914
1256
|
const schemaTpl = await readFile6(join7(input.templates, "SCHEMA.md"), "utf8");
|
|
915
|
-
const schema = schemaTpl.replace("{{DOMAIN}}",
|
|
916
|
-
await
|
|
1257
|
+
const schema = schemaTpl.replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang).replace("{{TAXONOMY_YAML}}", fullTaxonomyYaml);
|
|
1258
|
+
await writeFile4(join7(target, "SCHEMA.md"), schema, "utf8");
|
|
917
1259
|
created.push("SCHEMA.md");
|
|
918
1260
|
} catch (e) {
|
|
919
1261
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: "SCHEMA.md", message: String(e) }) };
|
|
920
1262
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1263
|
+
const preserved = [];
|
|
1264
|
+
async function writeOrPreserve(fileName, render) {
|
|
1265
|
+
try {
|
|
1266
|
+
const existing = await readFile6(join7(target, fileName), "utf8");
|
|
1267
|
+
if (existing.split("\n").length > 10) {
|
|
1268
|
+
preserved.push(fileName);
|
|
1269
|
+
return void 0;
|
|
1270
|
+
}
|
|
1271
|
+
} catch {
|
|
1272
|
+
}
|
|
1273
|
+
try {
|
|
1274
|
+
await writeFile4(join7(target, fileName), await render(), "utf8");
|
|
1275
|
+
created.push(fileName);
|
|
1276
|
+
return void 0;
|
|
1277
|
+
} catch (e) {
|
|
1278
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: fileName, message: String(e) }) };
|
|
1279
|
+
}
|
|
936
1280
|
}
|
|
937
|
-
|
|
938
|
-
await
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
return {
|
|
1281
|
+
const err1 = await writeOrPreserve("index.md", async () => {
|
|
1282
|
+
const tpl = await readFile6(join7(input.templates, "index.md"), "utf8");
|
|
1283
|
+
return tpl.replace("{{INIT_DATE}}", today);
|
|
1284
|
+
});
|
|
1285
|
+
if (err1) return err1;
|
|
1286
|
+
const err22 = await writeOrPreserve("log.md", async () => {
|
|
1287
|
+
const tpl = await readFile6(join7(input.templates, "log.md"), "utf8");
|
|
1288
|
+
return tpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang);
|
|
1289
|
+
});
|
|
1290
|
+
if (err22) return err22;
|
|
1291
|
+
const isTempPath = target.startsWith("/tmp/") || target === "/tmp" || target.startsWith("/var/tmp/") || target === "/var/tmp" || target.startsWith("/private/tmp/");
|
|
1292
|
+
const skipEnv = !!input.noEnv || isTempPath;
|
|
1293
|
+
let envWritten = "";
|
|
1294
|
+
if (!skipEnv) {
|
|
1295
|
+
try {
|
|
1296
|
+
const envEntries = {};
|
|
1297
|
+
if (input.profile) {
|
|
1298
|
+
envEntries[profileKey(input.profile, "PATH")] = target;
|
|
1299
|
+
envEntries[profileKey(input.profile, "LANG")] = canonicalLang;
|
|
1300
|
+
envEntries["WIKI_DEFAULT"] = input.profile;
|
|
1301
|
+
} else {
|
|
1302
|
+
envEntries["WIKI_PATH"] = target;
|
|
1303
|
+
envEntries["WIKI_LANG"] = canonicalLang;
|
|
1304
|
+
}
|
|
1305
|
+
await writeDotenv(envPath, envEntries, existingEnvRaw);
|
|
1306
|
+
envWritten = envPath;
|
|
1307
|
+
} catch (e) {
|
|
1308
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: envPath, message: String(e) }) };
|
|
1309
|
+
}
|
|
945
1310
|
}
|
|
946
1311
|
const importedFromHermes = pathRes.source === "hermes-dotenv" && !swDotenvHadPath;
|
|
1312
|
+
const humanHint = [
|
|
1313
|
+
`vault: ${target}`,
|
|
1314
|
+
`domain: ${domain}`,
|
|
1315
|
+
`lang: ${canonicalLang}`,
|
|
1316
|
+
`created: ${created.length}, preserved: ${preserved.length}`,
|
|
1317
|
+
`discovered tags: ${discovered_tags}`,
|
|
1318
|
+
skipEnv ? "env: skipped" : `env: ${envWritten}`
|
|
1319
|
+
].join("\n");
|
|
947
1320
|
return {
|
|
948
1321
|
exitCode: ExitCode.OK,
|
|
949
1322
|
result: ok({
|
|
950
1323
|
vault: target,
|
|
951
|
-
domain
|
|
1324
|
+
domain,
|
|
952
1325
|
taxonomy,
|
|
953
1326
|
lang: canonicalLang,
|
|
954
1327
|
created,
|
|
955
|
-
|
|
956
|
-
|
|
1328
|
+
preserved,
|
|
1329
|
+
env_written: envWritten,
|
|
1330
|
+
env_skipped: skipEnv,
|
|
1331
|
+
imported_from_hermes: importedFromHermes,
|
|
1332
|
+
discovered_tags,
|
|
1333
|
+
humanHint
|
|
957
1334
|
})
|
|
958
1335
|
};
|
|
959
1336
|
}
|
|
960
1337
|
|
|
1338
|
+
// src/utils/slug.ts
|
|
1339
|
+
function buildSlugMap(pages) {
|
|
1340
|
+
const map = /* @__PURE__ */ new Map();
|
|
1341
|
+
for (const p of pages) {
|
|
1342
|
+
const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
|
|
1343
|
+
map.set(slug.toLowerCase(), slug);
|
|
1344
|
+
}
|
|
1345
|
+
return map;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
961
1348
|
// src/commands/links.ts
|
|
962
1349
|
async function runLinks(input) {
|
|
963
1350
|
const scan = await scanVault(input.vault);
|
|
964
1351
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
965
|
-
const slugs =
|
|
966
|
-
for (const p of scan.data.typedKnowledge) {
|
|
967
|
-
slugs.add(p.relPath.replace(/\.md$/, "").split("/").pop());
|
|
968
|
-
}
|
|
1352
|
+
const slugs = buildSlugMap(scan.data.typedKnowledge);
|
|
969
1353
|
const broken = [];
|
|
970
1354
|
for (const p of scan.data.typedKnowledge) {
|
|
971
1355
|
const text = await readPage(p);
|
|
@@ -974,48 +1358,22 @@ async function runLinks(input) {
|
|
|
974
1358
|
const lines = body.split("\n");
|
|
975
1359
|
for (const slug of extractBodyWikilinks(body)) {
|
|
976
1360
|
const tail = slug.split("/").pop();
|
|
977
|
-
if (!slugs.has(tail)) {
|
|
1361
|
+
if (!slugs.has(tail.toLowerCase())) {
|
|
978
1362
|
const line = lines.findIndex((l) => l.includes(`[[${slug}`));
|
|
979
1363
|
broken.push({ page: p.relPath, slug, line: line >= 0 ? line + 1 : 0 });
|
|
980
1364
|
}
|
|
981
1365
|
}
|
|
982
1366
|
}
|
|
983
1367
|
if (broken.length > 0) {
|
|
984
|
-
return { exitCode: ExitCode.BROKEN_WIKILINKS, result: ok({ broken
|
|
1368
|
+
return { exitCode: ExitCode.BROKEN_WIKILINKS, result: ok({ broken, humanHint: `broken: ${broken.length}
|
|
1369
|
+
${broken.map((b) => ` ${b.page}:[[${b.slug}]] (line ${b.line})`).join("\n")}` }) };
|
|
985
1370
|
}
|
|
986
|
-
return { exitCode: ExitCode.OK, result: ok({ broken }) };
|
|
1371
|
+
return { exitCode: ExitCode.OK, result: ok({ broken, humanHint: "no broken wikilinks" }) };
|
|
987
1372
|
}
|
|
988
1373
|
|
|
989
1374
|
// src/commands/tag-audit.ts
|
|
990
1375
|
import { readFile as readFile7 } from "fs/promises";
|
|
991
1376
|
import { join as join8 } from "path";
|
|
992
|
-
|
|
993
|
-
// src/parsers/taxonomy.ts
|
|
994
|
-
import yaml2 from "js-yaml";
|
|
995
|
-
var FENCE_RE = /^##\s+Tag Taxonomy\s*$[\s\S]*?```yaml\s*\n([\s\S]*?)\n```/m;
|
|
996
|
-
function extractTaxonomy(schemaText) {
|
|
997
|
-
const m = schemaText.match(FENCE_RE);
|
|
998
|
-
if (!m) return ok([]);
|
|
999
|
-
let parsed;
|
|
1000
|
-
try {
|
|
1001
|
-
parsed = yaml2.load(m[1], { schema: yaml2.JSON_SCHEMA });
|
|
1002
|
-
} catch (e) {
|
|
1003
|
-
return err("INVALID_FRONTMATTER", { message: e.message });
|
|
1004
|
-
}
|
|
1005
|
-
if (parsed === null || typeof parsed !== "object") {
|
|
1006
|
-
return err("INVALID_FRONTMATTER", { message: "taxonomy block is not an object" });
|
|
1007
|
-
}
|
|
1008
|
-
const tax = parsed.taxonomy;
|
|
1009
|
-
if (!Array.isArray(tax)) {
|
|
1010
|
-
return err("INVALID_FRONTMATTER", { message: "taxonomy key missing or not an array" });
|
|
1011
|
-
}
|
|
1012
|
-
if (!tax.every((x) => typeof x === "string")) {
|
|
1013
|
-
return err("INVALID_FRONTMATTER", { message: "taxonomy must be a list of strings" });
|
|
1014
|
-
}
|
|
1015
|
-
return ok(tax);
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
// src/commands/tag-audit.ts
|
|
1019
1377
|
async function runTagAudit(input) {
|
|
1020
1378
|
const scan = await scanVault(input.vault);
|
|
1021
1379
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
@@ -1037,9 +1395,9 @@ async function runTagAudit(input) {
|
|
|
1037
1395
|
}
|
|
1038
1396
|
}
|
|
1039
1397
|
if (violations.length > 0) {
|
|
1040
|
-
return { exitCode: ExitCode.TAG_NOT_IN_TAXONOMY, result: ok({ violations, taxonomy: tax.data }) };
|
|
1398
|
+
return { exitCode: ExitCode.TAG_NOT_IN_TAXONOMY, result: ok({ violations, taxonomy: tax.data, humanHint: violations.map((v) => `${v.page}: "${v.tag}" not in taxonomy`).join("\n") }) };
|
|
1041
1399
|
}
|
|
1042
|
-
return { exitCode: ExitCode.OK, result: ok({ violations, taxonomy: tax.data }) };
|
|
1400
|
+
return { exitCode: ExitCode.OK, result: ok({ violations, taxonomy: tax.data, humanHint: "all tags valid" }) };
|
|
1043
1401
|
}
|
|
1044
1402
|
|
|
1045
1403
|
// src/commands/index-check.ts
|
|
@@ -1053,7 +1411,11 @@ async function runIndexCheck(input) {
|
|
|
1053
1411
|
indexText = await readFile8(join9(input.vault, "index.md"), "utf8");
|
|
1054
1412
|
} catch {
|
|
1055
1413
|
}
|
|
1056
|
-
const
|
|
1414
|
+
const indexSlugsLower = /* @__PURE__ */ new Map();
|
|
1415
|
+
for (const s of extractBodyWikilinks(indexText)) {
|
|
1416
|
+
const tail = s.split("/").pop();
|
|
1417
|
+
indexSlugsLower.set(tail.toLowerCase(), tail);
|
|
1418
|
+
}
|
|
1057
1419
|
const fileSlugs = /* @__PURE__ */ new Map();
|
|
1058
1420
|
for (const p of scan.data.typedKnowledge) {
|
|
1059
1421
|
const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
|
|
@@ -1061,16 +1423,21 @@ async function runIndexCheck(input) {
|
|
|
1061
1423
|
}
|
|
1062
1424
|
const missing_from_index = [];
|
|
1063
1425
|
for (const [slug, relPath] of fileSlugs.entries()) {
|
|
1064
|
-
if (!
|
|
1426
|
+
if (!indexSlugsLower.has(slug.toLowerCase())) missing_from_index.push(relPath);
|
|
1065
1427
|
}
|
|
1428
|
+
const fileSlugsLower = new Set([...fileSlugs.keys()].map((s) => s.toLowerCase()));
|
|
1066
1429
|
const ghost_entries = [];
|
|
1067
|
-
for (const
|
|
1068
|
-
if (!
|
|
1430
|
+
for (const [lower, orig] of indexSlugsLower) {
|
|
1431
|
+
if (!fileSlugsLower.has(lower)) ghost_entries.push(orig);
|
|
1069
1432
|
}
|
|
1433
|
+
const hintLines = [];
|
|
1434
|
+
if (missing_from_index.length > 0) hintLines.push(`missing from index: ${missing_from_index.length}`, ...missing_from_index.map((p) => ` ${p}`));
|
|
1435
|
+
if (ghost_entries.length > 0) hintLines.push(`ghost entries: ${ghost_entries.length}`, ...ghost_entries.map((g) => ` ${g}`));
|
|
1436
|
+
if (hintLines.length === 0) hintLines.push("index OK");
|
|
1070
1437
|
if (missing_from_index.length > 0 || ghost_entries.length > 0) {
|
|
1071
|
-
return { exitCode: ExitCode.INDEX_INCOMPLETE, result: ok({ missing_from_index, ghost_entries }) };
|
|
1438
|
+
return { exitCode: ExitCode.INDEX_INCOMPLETE, result: ok({ missing_from_index, ghost_entries, humanHint: hintLines.join("\n") }) };
|
|
1072
1439
|
}
|
|
1073
|
-
return { exitCode: ExitCode.OK, result: ok({ missing_from_index, ghost_entries }) };
|
|
1440
|
+
return { exitCode: ExitCode.OK, result: ok({ missing_from_index, ghost_entries, humanHint: hintLines.join("\n") }) };
|
|
1074
1441
|
}
|
|
1075
1442
|
|
|
1076
1443
|
// src/commands/stale.ts
|
|
@@ -1110,8 +1477,8 @@ async function runStale(input) {
|
|
|
1110
1477
|
stale.push({ page: p.relPath, page_updated: updated, newest_source_ingested: newest, gap_days: gap });
|
|
1111
1478
|
}
|
|
1112
1479
|
}
|
|
1113
|
-
if (stale.length > 0) return { exitCode: ExitCode.STALE_PAGE, result: ok({ stale }) };
|
|
1114
|
-
return { exitCode: ExitCode.OK, result: ok({ stale }) };
|
|
1480
|
+
if (stale.length > 0) return { exitCode: ExitCode.STALE_PAGE, result: ok({ stale, humanHint: stale.map((s) => `${s.page} (${s.gap_days}d stale)`).join("\n") }) };
|
|
1481
|
+
return { exitCode: ExitCode.OK, result: ok({ stale, humanHint: "no stale pages" }) };
|
|
1115
1482
|
}
|
|
1116
1483
|
|
|
1117
1484
|
// src/commands/pagesize.ts
|
|
@@ -1126,17 +1493,17 @@ async function runPagesize(input) {
|
|
|
1126
1493
|
const count = body.split("\n").length;
|
|
1127
1494
|
if (count > input.lines) oversized.push({ page: p.relPath, lines: count });
|
|
1128
1495
|
}
|
|
1129
|
-
if (oversized.length > 0) return { exitCode: ExitCode.PAGE_TOO_LARGE, result: ok({ oversized }) };
|
|
1130
|
-
return { exitCode: ExitCode.OK, result: ok({ oversized }) };
|
|
1496
|
+
if (oversized.length > 0) return { exitCode: ExitCode.PAGE_TOO_LARGE, result: ok({ oversized, humanHint: oversized.map((p) => `${p.page}: ${p.lines} lines`).join("\n") }) };
|
|
1497
|
+
return { exitCode: ExitCode.OK, result: ok({ oversized, humanHint: "all pages within size limit" }) };
|
|
1131
1498
|
}
|
|
1132
1499
|
|
|
1133
1500
|
// src/commands/log-rotate.ts
|
|
1134
|
-
import { readFile as readFile10, rename as rename2, writeFile as
|
|
1501
|
+
import { readFile as readFile10, rename as rename2, writeFile as writeFile5, stat as stat5 } from "fs/promises";
|
|
1135
1502
|
import { join as join11 } from "path";
|
|
1136
1503
|
var ENTRY_RE = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
|
|
1137
1504
|
async function runLogRotate(input) {
|
|
1138
1505
|
try {
|
|
1139
|
-
await
|
|
1506
|
+
await stat5(join11(input.vault, "SCHEMA.md"));
|
|
1140
1507
|
} catch {
|
|
1141
1508
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
1142
1509
|
}
|
|
@@ -1150,12 +1517,12 @@ async function runLogRotate(input) {
|
|
|
1150
1517
|
const matches = [...logText.matchAll(ENTRY_RE)];
|
|
1151
1518
|
const entries = matches.length;
|
|
1152
1519
|
if (entries < input.threshold) {
|
|
1153
|
-
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: false }) };
|
|
1520
|
+
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: false, humanHint: `${entries}/${input.threshold} entries \u2014 no rotation needed` }) };
|
|
1154
1521
|
}
|
|
1155
1522
|
if (!input.apply) {
|
|
1156
1523
|
return {
|
|
1157
1524
|
exitCode: ExitCode.LOG_ROTATE_NEEDED,
|
|
1158
|
-
result: ok({ entries, threshold: input.threshold, rotated: false })
|
|
1525
|
+
result: ok({ entries, threshold: input.threshold, rotated: false, humanHint: `${entries}/${input.threshold} entries \u2014 rotation needed (use --apply)` })
|
|
1159
1526
|
};
|
|
1160
1527
|
}
|
|
1161
1528
|
const newestYear = matches[matches.length - 1][1];
|
|
@@ -1172,17 +1539,88 @@ Chronological action log. Newest entries last. Skill writes append entries; lint
|
|
|
1172
1539
|
|
|
1173
1540
|
- Previous log moved to ${rotatedName}
|
|
1174
1541
|
`;
|
|
1175
|
-
await
|
|
1542
|
+
await writeFile5(logPath, fresh, "utf8");
|
|
1176
1543
|
} catch (e) {
|
|
1177
1544
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
|
|
1178
1545
|
}
|
|
1179
|
-
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: true, rotated_to: rotatedName }) };
|
|
1546
|
+
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: true, rotated_to: rotatedName, humanHint: `rotated ${entries} entries to ${rotatedName}` }) };
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// src/commands/topic-map-check.ts
|
|
1550
|
+
var DEFAULT_THRESHOLD = 200;
|
|
1551
|
+
async function runTopicMapCheck(input) {
|
|
1552
|
+
const threshold = input.threshold ?? DEFAULT_THRESHOLD;
|
|
1553
|
+
const scan = await scanVault(input.vault);
|
|
1554
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1555
|
+
const page_count = scan.data.typedKnowledge.length;
|
|
1556
|
+
const recommended = page_count >= threshold;
|
|
1557
|
+
return {
|
|
1558
|
+
exitCode: ExitCode.OK,
|
|
1559
|
+
result: ok({
|
|
1560
|
+
recommended,
|
|
1561
|
+
page_count,
|
|
1562
|
+
threshold,
|
|
1563
|
+
humanHint: recommended ? `topic map recommended (${page_count} pages >= ${threshold} threshold)` : `topic map not needed (${page_count} pages < ${threshold} threshold)`
|
|
1564
|
+
})
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// src/commands/index-link-format.ts
|
|
1569
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
1570
|
+
import { join as join12 } from "path";
|
|
1571
|
+
var MD_LINK_RE = /\[[^\[\]]+\]\([^)]+\.md\)/;
|
|
1572
|
+
async function runIndexLinkFormat(input) {
|
|
1573
|
+
let text = "";
|
|
1574
|
+
try {
|
|
1575
|
+
text = await readFile11(join12(input.vault, "index.md"), "utf8");
|
|
1576
|
+
} catch {
|
|
1577
|
+
}
|
|
1578
|
+
const markdown_links = [];
|
|
1579
|
+
for (const [i, line] of text.split("\n").entries()) {
|
|
1580
|
+
if (MD_LINK_RE.test(line)) markdown_links.push({ line: i + 1, text: line.trim() });
|
|
1581
|
+
}
|
|
1582
|
+
const humanHint = markdown_links.length === 0 ? "all index links use wikilink format" : `markdown links found: ${markdown_links.length}
|
|
1583
|
+
${markdown_links.map((l) => ` line ${l.line}: ${l.text}`).join("\n")}`;
|
|
1584
|
+
return { exitCode: ExitCode.OK, result: ok({ markdown_links, humanHint }) };
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// src/commands/dedup.ts
|
|
1588
|
+
async function runDedup(input) {
|
|
1589
|
+
const scan = await scanVault(input.vault);
|
|
1590
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1591
|
+
const hashMap = /* @__PURE__ */ new Map();
|
|
1592
|
+
let totalFiles = 0;
|
|
1593
|
+
for (const raw of scan.data.raw) {
|
|
1594
|
+
const fm = extractFrontmatter(await readPage(raw));
|
|
1595
|
+
if (!fm.ok) continue;
|
|
1596
|
+
const sha = typeof fm.data.sha256 === "string" ? fm.data.sha256 : null;
|
|
1597
|
+
if (!sha || sha.length !== 64) continue;
|
|
1598
|
+
totalFiles++;
|
|
1599
|
+
const existing = hashMap.get(sha);
|
|
1600
|
+
if (existing) existing.push(raw.relPath);
|
|
1601
|
+
else hashMap.set(sha, [raw.relPath]);
|
|
1602
|
+
}
|
|
1603
|
+
const duplicates = [...hashMap.entries()].filter(([, files]) => files.length > 1).map(([sha256, files]) => ({ sha256, files }));
|
|
1604
|
+
const exitCode = duplicates.length > 0 ? ExitCode.RAW_DEDUP_DETECTED : ExitCode.OK;
|
|
1605
|
+
const hintLines = [`scanned: ${totalFiles} raw files`];
|
|
1606
|
+
if (duplicates.length > 0) {
|
|
1607
|
+
hintLines.push(`duplicates: ${duplicates.length}`);
|
|
1608
|
+
for (const d of duplicates) hintLines.push(` ${d.sha256.slice(0, 12)}... \u2192 ${d.files.join(", ")}`);
|
|
1609
|
+
} else {
|
|
1610
|
+
hintLines.push("0 duplicates");
|
|
1611
|
+
}
|
|
1612
|
+
return {
|
|
1613
|
+
exitCode,
|
|
1614
|
+
result: ok({ scanned: totalFiles, duplicates, humanHint: hintLines.join("\n") })
|
|
1615
|
+
};
|
|
1180
1616
|
}
|
|
1181
1617
|
|
|
1182
1618
|
// src/commands/lint.ts
|
|
1183
|
-
var
|
|
1184
|
-
var
|
|
1185
|
-
var
|
|
1619
|
+
var STRUCT_MIN_BODY_LINES = 60;
|
|
1620
|
+
var STRUCT_MIN_SECTIONS = 3;
|
|
1621
|
+
var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_dedup", "tag_not_in_taxonomy"];
|
|
1622
|
+
var WARNING_ORDER = ["index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "contested", "orphans", "legacy_citation_style", "orphaned_citations"];
|
|
1623
|
+
var INFO_ORDER = ["bridges", "low_confidence_single_source", "page_structure", "topic_map_recommended"];
|
|
1186
1624
|
async function runLint(input) {
|
|
1187
1625
|
const buckets = {};
|
|
1188
1626
|
const links = await runLinks({ vault: input.vault });
|
|
@@ -1202,6 +1640,10 @@ async function runLint(input) {
|
|
|
1202
1640
|
ghost_entries: idx.result.data.ghost_entries
|
|
1203
1641
|
}];
|
|
1204
1642
|
}
|
|
1643
|
+
const linkFmt = await runIndexLinkFormat({ vault: input.vault });
|
|
1644
|
+
if (linkFmt.result.ok && linkFmt.result.data.markdown_links.length > 0) {
|
|
1645
|
+
buckets.index_link_format = linkFmt.result.data.markdown_links;
|
|
1646
|
+
}
|
|
1205
1647
|
const stale = await runStale({ vault: input.vault, days: input.days });
|
|
1206
1648
|
if (stale.result.ok && stale.result.data.stale.length > 0) buckets.stale_page = stale.result.data.stale;
|
|
1207
1649
|
const pagesize = await runPagesize({ vault: input.vault, lines: input.lines });
|
|
@@ -1215,6 +1657,42 @@ async function runLint(input) {
|
|
|
1215
1657
|
if (orphans.result.data.orphans.length > 0) buckets.orphans = orphans.result.data.orphans;
|
|
1216
1658
|
if (orphans.result.data.bridges.length > 0) buckets.bridges = orphans.result.data.bridges;
|
|
1217
1659
|
}
|
|
1660
|
+
const topicMap = await runTopicMapCheck({ vault: input.vault });
|
|
1661
|
+
if (topicMap.result.ok && topicMap.result.data.recommended) {
|
|
1662
|
+
buckets.topic_map_recommended = [{ page_count: topicMap.result.data.page_count, threshold: topicMap.result.data.threshold }];
|
|
1663
|
+
}
|
|
1664
|
+
const dedup = await runDedup({ vault: input.vault });
|
|
1665
|
+
if (dedup.result.ok && dedup.result.data.duplicates.length > 0) buckets.raw_dedup = dedup.result.data.duplicates;
|
|
1666
|
+
const scan = await scanVault(input.vault);
|
|
1667
|
+
if (scan.ok) {
|
|
1668
|
+
const legacyPages = [];
|
|
1669
|
+
const orphanedPages = [];
|
|
1670
|
+
const structFlags = [];
|
|
1671
|
+
for (const page of scan.data.typedKnowledge) {
|
|
1672
|
+
const text = await readPage(page);
|
|
1673
|
+
const split = splitFrontmatter(text);
|
|
1674
|
+
if (!split.ok) continue;
|
|
1675
|
+
const body = split.data.body;
|
|
1676
|
+
if (isLegacyCitationStyle(body)) legacyPages.push(page.relPath);
|
|
1677
|
+
if (hasOrphanedCitations(body)) orphanedPages.push(page.relPath);
|
|
1678
|
+
const bodyLines = body.split("\n").filter((l) => l.trim().length > 0).length;
|
|
1679
|
+
if (bodyLines < STRUCT_MIN_BODY_LINES) {
|
|
1680
|
+
const hasOverview = /^## Overview/m.test(body);
|
|
1681
|
+
const hasRelated = /^## (Related|Relationships)/m.test(body);
|
|
1682
|
+
const sectionCount = (body.match(/^## /gm) ?? []).length;
|
|
1683
|
+
if (!hasOverview || !hasRelated || sectionCount < STRUCT_MIN_SECTIONS) {
|
|
1684
|
+
const reasons = [];
|
|
1685
|
+
if (!hasOverview) reasons.push("no Overview");
|
|
1686
|
+
if (!hasRelated) reasons.push("no Related or Relationships");
|
|
1687
|
+
if (sectionCount < STRUCT_MIN_SECTIONS) reasons.push(`only ${sectionCount} sections`);
|
|
1688
|
+
structFlags.push(`${page.relPath}: ${bodyLines} lines, ${reasons.join(", ")}`);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
if (legacyPages.length > 0) buckets.legacy_citation_style = legacyPages;
|
|
1693
|
+
if (orphanedPages.length > 0) buckets.orphaned_citations = orphanedPages;
|
|
1694
|
+
if (structFlags.length > 0) buckets.page_structure = structFlags;
|
|
1695
|
+
}
|
|
1218
1696
|
const errorOut = ERROR_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
1219
1697
|
const warningOut = WARNING_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
1220
1698
|
const infoOut = INFO_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
@@ -1226,19 +1704,673 @@ async function runLint(input) {
|
|
|
1226
1704
|
let exitCode = ExitCode.OK;
|
|
1227
1705
|
if (summary.errors > 0) exitCode = ExitCode.LINT_HAS_ERRORS;
|
|
1228
1706
|
else if (summary.warnings > 0 || summary.info > 0) exitCode = ExitCode.LINT_HAS_WARNINGS;
|
|
1707
|
+
const hintLines = [];
|
|
1708
|
+
if (summary.errors > 0) hintLines.push(`errors: ${summary.errors}`);
|
|
1709
|
+
if (summary.warnings > 0) hintLines.push(`warnings: ${summary.warnings}`);
|
|
1710
|
+
if (summary.info > 0) hintLines.push(`info: ${summary.info}`);
|
|
1711
|
+
const allBuckets = [...errorOut, ...warningOut, ...infoOut];
|
|
1712
|
+
for (const b of allBuckets) {
|
|
1713
|
+
hintLines.push(` ${b.kind}: ${b.items.length}`);
|
|
1714
|
+
}
|
|
1715
|
+
if (hintLines.length === 0) hintLines.push("0 errors, 0 warnings, 0 info");
|
|
1229
1716
|
return {
|
|
1230
1717
|
exitCode,
|
|
1231
1718
|
result: ok({
|
|
1232
1719
|
vault: { path: input.vault, source: input.source ?? "resolved" },
|
|
1233
1720
|
summary,
|
|
1234
|
-
by_severity: { error: errorOut, warning: warningOut, info: infoOut }
|
|
1721
|
+
by_severity: { error: errorOut, warning: warningOut, info: infoOut },
|
|
1722
|
+
humanHint: hintLines.join("\n")
|
|
1723
|
+
})
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
// src/commands/config.ts
|
|
1728
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
1729
|
+
import { existsSync } from "fs";
|
|
1730
|
+
import { join as join13 } from "path";
|
|
1731
|
+
function validateKey(key) {
|
|
1732
|
+
return CONFIG_KEYS.includes(key) || isValidWikiProfileKey(key);
|
|
1733
|
+
}
|
|
1734
|
+
function configPath(home) {
|
|
1735
|
+
return join13(home, ".skillwiki", ".env");
|
|
1736
|
+
}
|
|
1737
|
+
async function runConfigGet(input) {
|
|
1738
|
+
if (!validateKey(input.key)) {
|
|
1739
|
+
return { exitCode: ExitCode.INVALID_CONFIG_KEY, result: err("INVALID_CONFIG_KEY", { key: input.key }) };
|
|
1740
|
+
}
|
|
1741
|
+
const map = await parseDotenvFile(configPath(input.home));
|
|
1742
|
+
const value = map[input.key] ?? "";
|
|
1743
|
+
return { exitCode: ExitCode.OK, result: ok({ key: input.key, value, humanHint: value }) };
|
|
1744
|
+
}
|
|
1745
|
+
async function runConfigSet(input) {
|
|
1746
|
+
if (!validateKey(input.key)) {
|
|
1747
|
+
return { exitCode: ExitCode.INVALID_CONFIG_KEY, result: err("INVALID_CONFIG_KEY", { key: input.key }) };
|
|
1748
|
+
}
|
|
1749
|
+
const filePath = configPath(input.home);
|
|
1750
|
+
try {
|
|
1751
|
+
let originalContent;
|
|
1752
|
+
try {
|
|
1753
|
+
originalContent = await readFile12(filePath, "utf8");
|
|
1754
|
+
} catch {
|
|
1755
|
+
}
|
|
1756
|
+
const existing = originalContent !== void 0 ? parseDotenvText(originalContent) : {};
|
|
1757
|
+
const merged = { ...existing, [input.key]: input.value };
|
|
1758
|
+
await writeDotenv(filePath, merged, originalContent);
|
|
1759
|
+
return { exitCode: ExitCode.OK, result: ok({ key: input.key, value: input.value, written: true, humanHint: `${input.key}=${input.value}` }) };
|
|
1760
|
+
} catch (e) {
|
|
1761
|
+
return { exitCode: ExitCode.CONFIG_WRITE_FAILED, result: err("CONFIG_WRITE_FAILED", { key: input.key, error: String(e) }) };
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
async function runConfigList(input) {
|
|
1765
|
+
const map = await parseDotenvFile(configPath(input.home));
|
|
1766
|
+
const entries = Object.entries(map).map(([key, value]) => ({ key, value: value ?? "" }));
|
|
1767
|
+
let profiles;
|
|
1768
|
+
if (input.profiles) {
|
|
1769
|
+
const defaultProfile = map["WIKI_DEFAULT"];
|
|
1770
|
+
profiles = [];
|
|
1771
|
+
for (const key of Object.keys(map)) {
|
|
1772
|
+
const m = key.match(/^WIKI_([A-Z][A-Z0-9_]{0,31})_PATH$/);
|
|
1773
|
+
if (m && key !== "WIKI_PATH") {
|
|
1774
|
+
const name = m[1].toLowerCase().replace(/_/g, "-");
|
|
1775
|
+
profiles.push({ name, path: map[key] ?? "", isDefault: name === defaultProfile });
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
profiles.sort((a, b) => a.name.localeCompare(b.name));
|
|
1779
|
+
}
|
|
1780
|
+
const hint = profiles ? profiles.map((p) => `${p.isDefault ? "* " : " "}${p.name} \u2192 ${p.path}`).join("\n") || "(no profiles)" : entries.map((e) => `${e.key}=${e.value}`).join("\n");
|
|
1781
|
+
return { exitCode: ExitCode.OK, result: ok({ entries, profiles, humanHint: hint }) };
|
|
1782
|
+
}
|
|
1783
|
+
async function runConfigPath(input) {
|
|
1784
|
+
const filePath = configPath(input.home);
|
|
1785
|
+
return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync(filePath), humanHint: filePath }) };
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
// src/commands/doctor.ts
|
|
1789
|
+
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
|
|
1790
|
+
import { join as join15 } from "path";
|
|
1791
|
+
import { execSync } from "child_process";
|
|
1792
|
+
|
|
1793
|
+
// src/utils/auto-update.ts
|
|
1794
|
+
import { readFileSync, writeFileSync, existsSync as existsSync2, mkdirSync } from "fs";
|
|
1795
|
+
import { join as join14, dirname as dirname6 } from "path";
|
|
1796
|
+
import { spawn } from "child_process";
|
|
1797
|
+
|
|
1798
|
+
// src/utils/update-consts.ts
|
|
1799
|
+
var CACHE_FILENAME = ".update-cache.json";
|
|
1800
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1801
|
+
var ENV_DISABLE_KEY = "NO_UPDATE_NOTIFIER";
|
|
1802
|
+
var CLI_DISABLE_FLAG = "--no-update-notifier";
|
|
1803
|
+
|
|
1804
|
+
// src/utils/auto-update.ts
|
|
1805
|
+
function cachePath(home) {
|
|
1806
|
+
return join14(home, ".skillwiki", CACHE_FILENAME);
|
|
1807
|
+
}
|
|
1808
|
+
function readCacheRaw(home) {
|
|
1809
|
+
try {
|
|
1810
|
+
const raw = readFileSync(cachePath(home), "utf8");
|
|
1811
|
+
return JSON.parse(raw);
|
|
1812
|
+
} catch {
|
|
1813
|
+
return null;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
function readCache(home) {
|
|
1817
|
+
const cache = readCacheRaw(home);
|
|
1818
|
+
if (!cache) return { cache: null, hasUpdate: false, isStale: true };
|
|
1819
|
+
const isStale = Date.now() - cache.lastCheck >= CHECK_INTERVAL_MS;
|
|
1820
|
+
const hasUpdate = !!cache.latestVersion && semverGt(cache.latestVersion, cache.currentVersion);
|
|
1821
|
+
return { cache, hasUpdate, isStale };
|
|
1822
|
+
}
|
|
1823
|
+
function writeCache(home, cache) {
|
|
1824
|
+
const p = cachePath(home);
|
|
1825
|
+
mkdirSync(dirname6(p), { recursive: true });
|
|
1826
|
+
writeFileSync(p, JSON.stringify(cache, null, 2));
|
|
1827
|
+
}
|
|
1828
|
+
function latestFromCache(home, currentVersion) {
|
|
1829
|
+
const { cache } = readCache(home);
|
|
1830
|
+
if (!cache || !cache.latestVersion) return { hasUpdate: false, latest: null };
|
|
1831
|
+
return {
|
|
1832
|
+
hasUpdate: semverGt(cache.latestVersion, currentVersion),
|
|
1833
|
+
latest: cache.latestVersion
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
function isDisabled() {
|
|
1837
|
+
return !!(process.env[ENV_DISABLE_KEY] || process.env.NODE_ENV === "test" || process.argv.includes(CLI_DISABLE_FLAG));
|
|
1838
|
+
}
|
|
1839
|
+
function triggerAutoUpdate(home, currentVersion) {
|
|
1840
|
+
if (isDisabled()) return;
|
|
1841
|
+
const { isStale } = readCache(home);
|
|
1842
|
+
if (!isStale) return;
|
|
1843
|
+
const bgScript = new URL("../auto-update-bg.js", import.meta.url).pathname;
|
|
1844
|
+
if (!existsSync2(bgScript)) return;
|
|
1845
|
+
const child = spawn(process.execPath, [bgScript, home, currentVersion], {
|
|
1846
|
+
detached: true,
|
|
1847
|
+
stdio: "ignore"
|
|
1848
|
+
});
|
|
1849
|
+
child.on("error", () => {
|
|
1850
|
+
});
|
|
1851
|
+
child.unref();
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
// src/commands/doctor.ts
|
|
1855
|
+
function check(status, id, label, detail) {
|
|
1856
|
+
return { id, label, status, detail };
|
|
1857
|
+
}
|
|
1858
|
+
function checkNodeVersion() {
|
|
1859
|
+
const major = parseInt(process.version.slice(1).split(".")[0], 10);
|
|
1860
|
+
if (major >= 20) {
|
|
1861
|
+
return check("pass", "node_version", "Node.js version", `v${major} >= 20`);
|
|
1862
|
+
}
|
|
1863
|
+
return check("error", "node_version", "Node.js version", `Node.js v${major} is below minimum v20`);
|
|
1864
|
+
}
|
|
1865
|
+
function checkCliOnPath(argv) {
|
|
1866
|
+
if (argv.length >= 2 && argv[1].endsWith("cli.js")) {
|
|
1867
|
+
return check("warn", "cli_on_path", "skillwiki on PATH", "Running via node cli.js (dev mode) \u2014 PATH check skipped");
|
|
1868
|
+
}
|
|
1869
|
+
if (argv.length >= 2 && argv[1] === "skillwiki") {
|
|
1870
|
+
return check("pass", "cli_on_path", "skillwiki on PATH", "Running as skillwiki \u2014 already on PATH");
|
|
1871
|
+
}
|
|
1872
|
+
try {
|
|
1873
|
+
execSync("which skillwiki 2>/dev/null", { encoding: "utf8" }).trim();
|
|
1874
|
+
return check("pass", "cli_on_path", "skillwiki on PATH", "skillwiki found on PATH");
|
|
1875
|
+
} catch {
|
|
1876
|
+
return check("warn", "cli_on_path", "skillwiki on PATH", "skillwiki not found on PATH");
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
async function checkConfigFile(home) {
|
|
1880
|
+
const cfgPath = configPath(home);
|
|
1881
|
+
if (!existsSync3(cfgPath)) {
|
|
1882
|
+
return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
|
|
1883
|
+
}
|
|
1884
|
+
try {
|
|
1885
|
+
const map = await parseDotenvFile(cfgPath);
|
|
1886
|
+
const keys = Object.keys(map);
|
|
1887
|
+
return check("pass", "config_file", "Config file exists", `Found with keys: ${keys.length > 0 ? keys.join(", ") : "(none set)"}`);
|
|
1888
|
+
} catch (e) {
|
|
1889
|
+
return check("warn", "config_file", "Config file exists", `Failed to parse ${cfgPath}: ${String(e)}`);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
function checkWikiPathExists(resolvedPath) {
|
|
1893
|
+
if (resolvedPath === void 0) {
|
|
1894
|
+
return check("error", "wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
1895
|
+
}
|
|
1896
|
+
if (existsSync3(resolvedPath) && statSync(resolvedPath).isDirectory()) {
|
|
1897
|
+
return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
|
|
1898
|
+
}
|
|
1899
|
+
return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
|
|
1900
|
+
}
|
|
1901
|
+
function checkVaultStructure(resolvedPath) {
|
|
1902
|
+
if (resolvedPath === void 0) {
|
|
1903
|
+
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
1904
|
+
}
|
|
1905
|
+
if (!existsSync3(resolvedPath)) {
|
|
1906
|
+
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
|
|
1907
|
+
}
|
|
1908
|
+
const missing = [];
|
|
1909
|
+
if (!existsSync3(join15(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
|
|
1910
|
+
for (const dir of ["raw", "entities", "concepts", "meta"]) {
|
|
1911
|
+
if (!existsSync3(join15(resolvedPath, dir))) missing.push(dir + "/");
|
|
1912
|
+
}
|
|
1913
|
+
if (missing.length === 0) {
|
|
1914
|
+
return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
|
|
1915
|
+
}
|
|
1916
|
+
return check("error", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")}`);
|
|
1917
|
+
}
|
|
1918
|
+
function checkSkillsInstalled(home) {
|
|
1919
|
+
const skillsDir = join15(home, ".claude", "skills");
|
|
1920
|
+
if (!existsSync3(skillsDir)) {
|
|
1921
|
+
return check("warn", "skills_installed", "Skills installed", `${skillsDir} not found`);
|
|
1922
|
+
}
|
|
1923
|
+
const found = findSkillMd(skillsDir);
|
|
1924
|
+
if (found.length > 0) {
|
|
1925
|
+
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found`);
|
|
1926
|
+
}
|
|
1927
|
+
return check("warn", "skills_installed", "Skills installed", "No SKILL.md files found in ~/.claude/skills/");
|
|
1928
|
+
}
|
|
1929
|
+
function checkNpmUpdate(home, currentVersion) {
|
|
1930
|
+
const { hasUpdate, latest } = latestFromCache(home, currentVersion);
|
|
1931
|
+
if (!latest) {
|
|
1932
|
+
return check("pass", "npm_update", "npm CLI version", `v${currentVersion} (no cache yet)`);
|
|
1933
|
+
}
|
|
1934
|
+
if (hasUpdate) {
|
|
1935
|
+
return check("warn", "npm_update", "npm CLI version", `v${currentVersion} \u2014 update available: v${latest}. Run \`skillwiki update\`.`);
|
|
1936
|
+
}
|
|
1937
|
+
return check("pass", "npm_update", "npm CLI version", `v${currentVersion} (latest: v${latest})`);
|
|
1938
|
+
}
|
|
1939
|
+
function checkPluginVersionDrift(home, currentVersion) {
|
|
1940
|
+
const pluginJsonPath = join15(home, ".claude", "plugins", "cache", "llm-wiki", "plugin.json");
|
|
1941
|
+
if (!existsSync3(pluginJsonPath)) {
|
|
1942
|
+
return check("pass", "plugin_version_drift", "Plugin/CLI version", "Plugin cache not found \u2014 plugin not installed");
|
|
1943
|
+
}
|
|
1944
|
+
try {
|
|
1945
|
+
const content = readFileSync2(pluginJsonPath, { encoding: "utf8" });
|
|
1946
|
+
const pluginData = JSON.parse(content);
|
|
1947
|
+
const pluginVersion = pluginData.version;
|
|
1948
|
+
if (!pluginVersion) {
|
|
1949
|
+
return check("pass", "plugin_version_drift", "Plugin/CLI version", "Plugin version not found in cache");
|
|
1950
|
+
}
|
|
1951
|
+
if (pluginVersion === currentVersion) {
|
|
1952
|
+
return check("pass", "plugin_version_drift", "Plugin/CLI version", `Both at v${currentVersion}`);
|
|
1953
|
+
}
|
|
1954
|
+
const updateCmd = semverGt(pluginVersion, currentVersion) ? "npm install -g skillwiki@beta" : "claude plugin update skillwiki@llm-wiki";
|
|
1955
|
+
return check(
|
|
1956
|
+
"warn",
|
|
1957
|
+
"plugin_version_drift",
|
|
1958
|
+
"Plugin/CLI version",
|
|
1959
|
+
`Plugin v${pluginVersion} \u2260 CLI v${currentVersion} \u2014 run \`${updateCmd}\``
|
|
1960
|
+
);
|
|
1961
|
+
} catch {
|
|
1962
|
+
return check("pass", "plugin_version_drift", "Plugin/CLI version", "Could not read plugin cache");
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
async function checkProfiles(home) {
|
|
1966
|
+
const map = await parseDotenvFile(configPath(home));
|
|
1967
|
+
const profiles = [];
|
|
1968
|
+
for (const key of Object.keys(map)) {
|
|
1969
|
+
if (key.startsWith("WIKI_") && key.endsWith("_PATH") && key !== "WIKI_PATH") {
|
|
1970
|
+
const name = key.slice(5, -5).toLowerCase().replace(/_/g, "-");
|
|
1971
|
+
profiles.push(name);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
if (profiles.length === 0) {
|
|
1975
|
+
return check("pass", "wiki_profiles", "Wiki profiles", "No named profiles configured");
|
|
1976
|
+
}
|
|
1977
|
+
const defaultProfile = map["WIKI_DEFAULT"] ?? "(none)";
|
|
1978
|
+
return check(
|
|
1979
|
+
"pass",
|
|
1980
|
+
"wiki_profiles",
|
|
1981
|
+
"Wiki profiles",
|
|
1982
|
+
`${profiles.length} profile(s): ${profiles.join(", ")}; default: ${defaultProfile}`
|
|
1983
|
+
);
|
|
1984
|
+
}
|
|
1985
|
+
async function checkProjectLocalOverride(cwd) {
|
|
1986
|
+
const dir = cwd ?? process.cwd();
|
|
1987
|
+
const envPath = join15(dir, ".skillwiki", ".env");
|
|
1988
|
+
if (existsSync3(envPath)) {
|
|
1989
|
+
return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
|
|
1990
|
+
}
|
|
1991
|
+
return check("pass", "project_local", "Project-local config", "None");
|
|
1992
|
+
}
|
|
1993
|
+
function findSkillMd(dir) {
|
|
1994
|
+
const results = [];
|
|
1995
|
+
let entries;
|
|
1996
|
+
try {
|
|
1997
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
1998
|
+
} catch {
|
|
1999
|
+
return results;
|
|
2000
|
+
}
|
|
2001
|
+
for (const entry of entries) {
|
|
2002
|
+
if (entry.isFile() && entry.name === "SKILL.md") {
|
|
2003
|
+
results.push(join15(dir, entry.name));
|
|
2004
|
+
} else if (entry.isDirectory()) {
|
|
2005
|
+
results.push(...findSkillMd(join15(dir, entry.name)));
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
return results;
|
|
2009
|
+
}
|
|
2010
|
+
async function runDoctor(input) {
|
|
2011
|
+
const checks = [];
|
|
2012
|
+
checks.push(checkNodeVersion());
|
|
2013
|
+
checks.push(checkCliOnPath(input.argv));
|
|
2014
|
+
checks.push(await checkConfigFile(input.home));
|
|
2015
|
+
checks.push(await checkProfiles(input.home));
|
|
2016
|
+
checks.push(await checkProjectLocalOverride(input.cwd));
|
|
2017
|
+
const resolved = await resolveRuntimePath({ flag: void 0, envValue: input.envValue, home: input.home });
|
|
2018
|
+
if (resolved.ok) {
|
|
2019
|
+
checks.push(check("pass", "wiki_path_set", "WIKI_PATH configured", `Resolved via ${resolved.data.source}: ${resolved.data.path}`));
|
|
2020
|
+
} else {
|
|
2021
|
+
checks.push(check("error", "wiki_path_set", "WIKI_PATH configured", "No vault configured. Run `skillwiki init` or pass --vault."));
|
|
2022
|
+
}
|
|
2023
|
+
const resolvedPath = resolved.ok ? resolved.data.path : void 0;
|
|
2024
|
+
checks.push(checkWikiPathExists(resolvedPath));
|
|
2025
|
+
checks.push(checkVaultStructure(resolvedPath));
|
|
2026
|
+
checks.push(checkSkillsInstalled(input.home));
|
|
2027
|
+
checks.push(checkNpmUpdate(input.home, input.currentVersion));
|
|
2028
|
+
checks.push(checkPluginVersionDrift(input.home, input.currentVersion));
|
|
2029
|
+
const summary = {
|
|
2030
|
+
pass: checks.filter((c) => c.status === "pass").length,
|
|
2031
|
+
warn: checks.filter((c) => c.status === "warn").length,
|
|
2032
|
+
error: checks.filter((c) => c.status === "error").length
|
|
2033
|
+
};
|
|
2034
|
+
const exitCode = summary.error > 0 ? ExitCode.DOCTOR_HAS_ERRORS : summary.warn > 0 ? ExitCode.DOCTOR_HAS_WARNINGS : ExitCode.OK;
|
|
2035
|
+
const statusIcon = { pass: "\u2713", warn: "\u26A0", error: "\u2717" };
|
|
2036
|
+
const lines = checks.map((c) => {
|
|
2037
|
+
const icon = statusIcon[c.status];
|
|
2038
|
+
const padded = c.label.padEnd(24);
|
|
2039
|
+
return ` ${icon} ${padded} ${c.detail}`;
|
|
2040
|
+
});
|
|
2041
|
+
lines.push("");
|
|
2042
|
+
lines.push(`${summary.pass} pass \xB7 ${summary.warn} warn \xB7 ${summary.error} error`);
|
|
2043
|
+
const humanHint = lines.join("\n");
|
|
2044
|
+
return { exitCode, result: ok({ checks, summary, humanHint }) };
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
// src/commands/archive.ts
|
|
2048
|
+
import { rename as rename3, mkdir as mkdir5, readFile as readFile13, writeFile as writeFile6 } from "fs/promises";
|
|
2049
|
+
import { join as join16, dirname as dirname7 } from "path";
|
|
2050
|
+
async function runArchive(input) {
|
|
2051
|
+
const scan = await scanVault(input.vault);
|
|
2052
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
2053
|
+
let relPath;
|
|
2054
|
+
if (input.page.includes("/")) {
|
|
2055
|
+
relPath = scan.data.typedKnowledge.find((p) => p.relPath === input.page)?.relPath;
|
|
2056
|
+
} else {
|
|
2057
|
+
relPath = scan.data.typedKnowledge.find((p) => p.relPath.replace(/\.md$/, "").split("/").pop() === input.page)?.relPath;
|
|
2058
|
+
}
|
|
2059
|
+
if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
|
|
2060
|
+
if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
|
|
2061
|
+
const archivePath = join16("_archive", relPath);
|
|
2062
|
+
await mkdir5(dirname7(join16(input.vault, archivePath)), { recursive: true });
|
|
2063
|
+
let indexUpdated = false;
|
|
2064
|
+
const indexPath = join16(input.vault, "index.md");
|
|
2065
|
+
try {
|
|
2066
|
+
const idx = await readFile13(indexPath, "utf8");
|
|
2067
|
+
const slug = relPath.replace(/\.md$/, "").split("/").pop();
|
|
2068
|
+
const originalLines = idx.split("\n");
|
|
2069
|
+
const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
|
|
2070
|
+
if (filtered.length !== originalLines.length) {
|
|
2071
|
+
await writeFile6(indexPath, filtered.join("\n"), "utf8");
|
|
2072
|
+
indexUpdated = true;
|
|
2073
|
+
}
|
|
2074
|
+
} catch (e) {
|
|
2075
|
+
if (e?.code !== "ENOENT") throw e;
|
|
2076
|
+
}
|
|
2077
|
+
await rename3(join16(input.vault, relPath), join16(input.vault, archivePath));
|
|
2078
|
+
return { exitCode: ExitCode.OK, result: ok({ archived_from: relPath, archived_to: archivePath, index_updated: indexUpdated, humanHint: `${relPath} -> ${archivePath}${indexUpdated ? " (index updated)" : ""}` }) };
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
// src/commands/drift.ts
|
|
2082
|
+
import { createHash as createHash2 } from "crypto";
|
|
2083
|
+
|
|
2084
|
+
// src/utils/fetch.ts
|
|
2085
|
+
async function controlledFetch(url, opts) {
|
|
2086
|
+
let current = url;
|
|
2087
|
+
for (let hop = 0; hop <= opts.maxRedirects; hop++) {
|
|
2088
|
+
const guard = runFetchGuardSync({ url: current });
|
|
2089
|
+
if (!guard.result.ok) return guard.result;
|
|
2090
|
+
const ctrl = new AbortController();
|
|
2091
|
+
const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs);
|
|
2092
|
+
let res;
|
|
2093
|
+
try {
|
|
2094
|
+
res = await fetch(current, { redirect: "manual", signal: ctrl.signal });
|
|
2095
|
+
} catch (e) {
|
|
2096
|
+
clearTimeout(timer);
|
|
2097
|
+
if (e?.name === "AbortError") return err("FETCH_TIMEOUT", { url: current });
|
|
2098
|
+
return err("FETCH_FAILED", { message: String(e) });
|
|
2099
|
+
}
|
|
2100
|
+
clearTimeout(timer);
|
|
2101
|
+
if (res.status >= 300 && res.status < 400) {
|
|
2102
|
+
const loc = res.headers.get("location");
|
|
2103
|
+
if (!loc) return err("FETCH_FAILED", { reason: "redirect without Location" });
|
|
2104
|
+
current = new URL(loc, current).toString();
|
|
2105
|
+
continue;
|
|
2106
|
+
}
|
|
2107
|
+
const declared = Number(res.headers.get("content-length") ?? "0");
|
|
2108
|
+
if (declared > opts.maxBytes) return err("FETCH_TOO_LARGE", { declared, limit: opts.maxBytes });
|
|
2109
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
2110
|
+
if (buf.byteLength > opts.maxBytes) return err("FETCH_TOO_LARGE", { actual: buf.byteLength, limit: opts.maxBytes });
|
|
2111
|
+
return ok({ url: current, status: res.status, body: new TextDecoder().decode(buf), bytes: buf.byteLength });
|
|
2112
|
+
}
|
|
2113
|
+
return err("FETCH_FAILED", { reason: "too many redirects", limit: opts.maxRedirects });
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
// src/commands/drift.ts
|
|
2117
|
+
var FETCH_OPTS = { timeoutMs: 1e4, maxBytes: 5e6, maxRedirects: 5 };
|
|
2118
|
+
async function runDrift(input) {
|
|
2119
|
+
const doFetch = input.fetchFn ?? controlledFetch;
|
|
2120
|
+
const scan = await scanVault(input.vault);
|
|
2121
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
2122
|
+
const results = [];
|
|
2123
|
+
for (const raw of scan.data.raw) {
|
|
2124
|
+
const fm = extractFrontmatter(await readPage(raw));
|
|
2125
|
+
if (!fm.ok) continue;
|
|
2126
|
+
const sourceUrl = typeof fm.data.source_url === "string" ? fm.data.source_url : null;
|
|
2127
|
+
const storedHash = typeof fm.data.sha256 === "string" ? fm.data.sha256 : null;
|
|
2128
|
+
if (!sourceUrl || !storedHash) continue;
|
|
2129
|
+
const resp = await doFetch(sourceUrl, FETCH_OPTS);
|
|
2130
|
+
if (!resp.ok) {
|
|
2131
|
+
results.push({
|
|
2132
|
+
raw_path: raw.relPath,
|
|
2133
|
+
source_url: sourceUrl,
|
|
2134
|
+
stored_sha256: storedHash,
|
|
2135
|
+
current_sha256: null,
|
|
2136
|
+
status: "fetch_failed",
|
|
2137
|
+
fetch_error: resp.error
|
|
2138
|
+
});
|
|
2139
|
+
continue;
|
|
2140
|
+
}
|
|
2141
|
+
const currentHash = createHash2("sha256").update(Buffer.from(resp.data.body, "utf8")).digest("hex");
|
|
2142
|
+
const drifted2 = currentHash !== storedHash;
|
|
2143
|
+
results.push({
|
|
2144
|
+
raw_path: raw.relPath,
|
|
2145
|
+
source_url: sourceUrl,
|
|
2146
|
+
stored_sha256: storedHash,
|
|
2147
|
+
current_sha256: currentHash,
|
|
2148
|
+
status: drifted2 ? "drifted" : "unchanged"
|
|
2149
|
+
});
|
|
2150
|
+
}
|
|
2151
|
+
const drifted = results.filter((r) => r.status === "drifted");
|
|
2152
|
+
const fetchFailed = results.filter((r) => r.status === "fetch_failed");
|
|
2153
|
+
const unchanged = results.filter((r) => r.status === "unchanged").length;
|
|
2154
|
+
const exitCode = drifted.length > 0 ? ExitCode.DRIFT_DETECTED : ExitCode.OK;
|
|
2155
|
+
const hintLines = [`scanned: ${results.length}, unchanged: ${unchanged}`];
|
|
2156
|
+
if (drifted.length > 0) hintLines.push(`drifted: ${drifted.length}`, ...drifted.map((d) => ` ${d.raw_path}`));
|
|
2157
|
+
if (fetchFailed.length > 0) hintLines.push(`fetch_failed: ${fetchFailed.length}`, ...fetchFailed.map((f) => ` ${f.raw_path}: ${f.fetch_error}`));
|
|
2158
|
+
return {
|
|
2159
|
+
exitCode,
|
|
2160
|
+
result: ok({ scanned: results.length, drifted, fetch_failed: fetchFailed, unchanged, humanHint: hintLines.join("\n") })
|
|
2161
|
+
};
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
// src/commands/migrate-citations.ts
|
|
2165
|
+
import { writeFile as writeFile7 } from "fs/promises";
|
|
2166
|
+
var MARKER_RE2 = /\^\[(raw\/[^\]]+)\]/g;
|
|
2167
|
+
function moveMarkersToParagraphEnd(body) {
|
|
2168
|
+
const lines = body.split("\n");
|
|
2169
|
+
const result = [];
|
|
2170
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2171
|
+
const line = lines[i];
|
|
2172
|
+
if (line.trimStart().startsWith("```")) {
|
|
2173
|
+
result.push(line);
|
|
2174
|
+
if (!line.trimEnd().endsWith("```") || line.trim() === "```") {
|
|
2175
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
2176
|
+
result.push(lines[j]);
|
|
2177
|
+
if (lines[j].trimStart().startsWith("```")) {
|
|
2178
|
+
i = j;
|
|
2179
|
+
break;
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
continue;
|
|
2184
|
+
}
|
|
2185
|
+
if (/^## Sources\b/.test(line.trim())) {
|
|
2186
|
+
result.push(line);
|
|
2187
|
+
continue;
|
|
2188
|
+
}
|
|
2189
|
+
const markers = [...line.matchAll(MARKER_RE2)];
|
|
2190
|
+
if (markers.length === 0) {
|
|
2191
|
+
result.push(line);
|
|
2192
|
+
continue;
|
|
2193
|
+
}
|
|
2194
|
+
const proseOnly = line.replace(MARKER_RE2, "").trim();
|
|
2195
|
+
if (proseOnly.length === 0) {
|
|
2196
|
+
const markerStr = " " + markers.map((m) => m[0]).join(" ");
|
|
2197
|
+
let merged = false;
|
|
2198
|
+
for (let k = result.length - 1; k >= 0; k--) {
|
|
2199
|
+
if (result[k].trim().length > 0) {
|
|
2200
|
+
result[k] = result[k].trimEnd() + markerStr;
|
|
2201
|
+
merged = true;
|
|
2202
|
+
break;
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
if (!merged) result.push(line);
|
|
2206
|
+
continue;
|
|
2207
|
+
}
|
|
2208
|
+
const lastMarkerIdx = line.lastIndexOf("^[raw/");
|
|
2209
|
+
const afterLast = line.slice(lastMarkerIdx).replace(MARKER_RE2, "").trim();
|
|
2210
|
+
const firstMarkerIdx = line.indexOf("^[raw/");
|
|
2211
|
+
const beforeFirst = line.slice(0, firstMarkerIdx).trim();
|
|
2212
|
+
const alreadyAtEnd = afterLast.length === 0 && (beforeFirst.length === 0 || /[.!?]\s*$/.test(beforeFirst));
|
|
2213
|
+
if (alreadyAtEnd) {
|
|
2214
|
+
result.push(line);
|
|
2215
|
+
continue;
|
|
2216
|
+
}
|
|
2217
|
+
let cleaned = line.replace(/\s*\^\[raw\/[^\]]+\]\s*/g, " ").trimEnd();
|
|
2218
|
+
cleaned = cleaned.replace(/ +/g, " ").trimEnd();
|
|
2219
|
+
const markerStrings = markers.map((m) => m[0]);
|
|
2220
|
+
if (cleaned.length > 0 && /[.!?]$/.test(cleaned)) {
|
|
2221
|
+
cleaned += " " + markerStrings.join(" ");
|
|
2222
|
+
} else if (cleaned.length > 0) {
|
|
2223
|
+
cleaned += ". " + markerStrings.join(" ");
|
|
2224
|
+
} else {
|
|
2225
|
+
cleaned = markerStrings.join(" ");
|
|
2226
|
+
}
|
|
2227
|
+
result.push(cleaned);
|
|
2228
|
+
}
|
|
2229
|
+
return result.join("\n");
|
|
2230
|
+
}
|
|
2231
|
+
function buildSourcesFooter(targets) {
|
|
2232
|
+
return "\n## Sources\n" + targets.map((t) => `- ^[${t}]`).join("\n") + "\n";
|
|
2233
|
+
}
|
|
2234
|
+
function reorderSourcesFm(rawFm, targets) {
|
|
2235
|
+
const sourcesLineRe = /^sources:\s*\[([^\]]*)\]\s*$/m;
|
|
2236
|
+
const match = rawFm.match(sourcesLineRe);
|
|
2237
|
+
if (!match) return rawFm;
|
|
2238
|
+
const existing = match[1].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter((s) => s.length > 0);
|
|
2239
|
+
const targetSet = new Set(targets);
|
|
2240
|
+
const reordered = [
|
|
2241
|
+
...targets,
|
|
2242
|
+
...existing.filter((s) => !targetSet.has(s))
|
|
2243
|
+
];
|
|
2244
|
+
const newLine = `sources: [${reordered.join(", ")}]`;
|
|
2245
|
+
return rawFm.replace(sourcesLineRe, newLine);
|
|
2246
|
+
}
|
|
2247
|
+
function removeExistingFooter(body) {
|
|
2248
|
+
const footerRe = /\n## Sources\n[\s\S]*$/;
|
|
2249
|
+
return body.replace(footerRe, "");
|
|
2250
|
+
}
|
|
2251
|
+
async function runMigrateCitations(input) {
|
|
2252
|
+
const scan = await scanVault(input.vault);
|
|
2253
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
2254
|
+
const migrated = [];
|
|
2255
|
+
const skipped = [];
|
|
2256
|
+
let unchanged = 0;
|
|
2257
|
+
for (const page of scan.data.typedKnowledge) {
|
|
2258
|
+
const text = await readPage(page);
|
|
2259
|
+
const split = splitFrontmatter(text);
|
|
2260
|
+
if (!split.ok) continue;
|
|
2261
|
+
const { rawFrontmatter, body } = split.data;
|
|
2262
|
+
const markers = extractCitationMarkers(body);
|
|
2263
|
+
if (markers.length === 0) {
|
|
2264
|
+
unchanged++;
|
|
2265
|
+
continue;
|
|
2266
|
+
}
|
|
2267
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2268
|
+
const uniqueTargets = [];
|
|
2269
|
+
for (const m of markers) {
|
|
2270
|
+
if (!seen.has(m.target)) {
|
|
2271
|
+
seen.add(m.target);
|
|
2272
|
+
uniqueTargets.push(m.target);
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
const bodyWithoutFooter = removeExistingFooter(body);
|
|
2276
|
+
const migratedBody = moveMarkersToParagraphEnd(bodyWithoutFooter);
|
|
2277
|
+
const newFooter = buildSourcesFooter(uniqueTargets);
|
|
2278
|
+
const newFm = reorderSourcesFm(rawFrontmatter, uniqueTargets);
|
|
2279
|
+
const newText = `---
|
|
2280
|
+
${newFm}
|
|
2281
|
+
---
|
|
2282
|
+
${migratedBody}${newFooter}`;
|
|
2283
|
+
if (newText === text) {
|
|
2284
|
+
skipped.push(page.relPath);
|
|
2285
|
+
continue;
|
|
2286
|
+
}
|
|
2287
|
+
if (!input.dryRun) {
|
|
2288
|
+
await writeFile7(page.absPath, newText, "utf8");
|
|
2289
|
+
}
|
|
2290
|
+
migrated.push(page.relPath);
|
|
2291
|
+
}
|
|
2292
|
+
const exitCode = migrated.length > 0 ? ExitCode.MIGRATION_APPLIED : ExitCode.OK;
|
|
2293
|
+
const hintLines = [`scanned: ${migrated.length + skipped.length + unchanged}`];
|
|
2294
|
+
if (migrated.length > 0) hintLines.push(`migrated: ${migrated.length}`);
|
|
2295
|
+
if (skipped.length > 0) hintLines.push(`skipped (already clean): ${skipped.length}`);
|
|
2296
|
+
if (unchanged > 0) hintLines.push(`unchanged (no markers): ${unchanged}`);
|
|
2297
|
+
return {
|
|
2298
|
+
exitCode,
|
|
2299
|
+
result: ok({
|
|
2300
|
+
scanned: migrated.length + skipped.length + unchanged,
|
|
2301
|
+
migrated,
|
|
2302
|
+
skipped,
|
|
2303
|
+
unchanged,
|
|
2304
|
+
humanHint: hintLines.join("\n")
|
|
2305
|
+
})
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
// src/commands/update.ts
|
|
2310
|
+
import { execSync as execSync2 } from "child_process";
|
|
2311
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2312
|
+
async function runUpdate(input) {
|
|
2313
|
+
const pkg2 = JSON.parse(
|
|
2314
|
+
readFileSync3(new URL("../../package.json", import.meta.url), "utf8")
|
|
2315
|
+
);
|
|
2316
|
+
const currentVersion = pkg2.version;
|
|
2317
|
+
const tag = input.distTag ?? "beta";
|
|
2318
|
+
let latest;
|
|
2319
|
+
try {
|
|
2320
|
+
latest = execSync2(`npm view skillwiki@${tag} version`, {
|
|
2321
|
+
encoding: "utf8",
|
|
2322
|
+
timeout: 15e3
|
|
2323
|
+
}).trim();
|
|
2324
|
+
} catch (e) {
|
|
2325
|
+
return {
|
|
2326
|
+
exitCode: ExitCode.PREFLIGHT_FAILED,
|
|
2327
|
+
result: err("PREFLIGHT_FAILED", { message: `Failed to query npm registry: ${String(e)}` })
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
const cache = {
|
|
2331
|
+
lastCheck: Date.now(),
|
|
2332
|
+
latestVersion: latest,
|
|
2333
|
+
currentVersion
|
|
2334
|
+
};
|
|
2335
|
+
if (latest === currentVersion) {
|
|
2336
|
+
writeCache(input.home, cache);
|
|
2337
|
+
return {
|
|
2338
|
+
exitCode: ExitCode.OK,
|
|
2339
|
+
result: ok({
|
|
2340
|
+
previousVersion: currentVersion,
|
|
2341
|
+
newVersion: null,
|
|
2342
|
+
wasAlreadyLatest: true,
|
|
2343
|
+
humanHint: `Already on latest ${tag}: v${currentVersion}`
|
|
2344
|
+
})
|
|
2345
|
+
};
|
|
2346
|
+
}
|
|
2347
|
+
try {
|
|
2348
|
+
execSync2(`npm install -g skillwiki@${tag}`, {
|
|
2349
|
+
stdio: "pipe",
|
|
2350
|
+
timeout: 6e4
|
|
2351
|
+
});
|
|
2352
|
+
} catch (e) {
|
|
2353
|
+
return {
|
|
2354
|
+
exitCode: ExitCode.PREFLIGHT_FAILED,
|
|
2355
|
+
result: err("PREFLIGHT_FAILED", { message: `npm install failed: ${String(e)}` })
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
writeCache(input.home, { ...cache, updateAppliedAt: Date.now() });
|
|
2359
|
+
return {
|
|
2360
|
+
exitCode: ExitCode.OK,
|
|
2361
|
+
result: ok({
|
|
2362
|
+
previousVersion: currentVersion,
|
|
2363
|
+
newVersion: latest,
|
|
2364
|
+
wasAlreadyLatest: false,
|
|
2365
|
+
humanHint: `Updated skillwiki ${currentVersion} \u2192 ${latest}`
|
|
1235
2366
|
})
|
|
1236
2367
|
};
|
|
1237
2368
|
}
|
|
1238
2369
|
|
|
1239
2370
|
// src/cli.ts
|
|
2371
|
+
var pkg = JSON.parse(readFileSync4(new URL("../package.json", import.meta.url), "utf8"));
|
|
1240
2372
|
var program = new Command();
|
|
1241
|
-
program.name("skillwiki").description("Deterministic helpers for CodeWiki skills").version(
|
|
2373
|
+
program.name("skillwiki").description("Deterministic helpers for CodeWiki skills").version(pkg.version);
|
|
1242
2374
|
program.option("--human", "render terminal-readable output instead of JSON");
|
|
1243
2375
|
function emit(r) {
|
|
1244
2376
|
if (program.opts().human) printHuman(r.result);
|
|
@@ -1248,19 +2380,20 @@ function emit(r) {
|
|
|
1248
2380
|
program.command("hash <file>").action(async (file) => emit(await runHash({ file })));
|
|
1249
2381
|
program.command("fetch-guard <url>").action(async (url) => emit(await runFetchGuard({ url })));
|
|
1250
2382
|
program.command("validate <file>").action(async (file) => emit(await runValidate({ file })));
|
|
1251
|
-
program.command("graph").description("graph subcommands").command("build <vault>").option("--out <path>", "graph output path", ".skillwiki/graph.json").action(async (vault, opts) => emit(await runGraphBuild({ vault, out: opts.out })));
|
|
2383
|
+
program.command("graph").description("graph subcommands").command("build <vault>").option("--out <path>", "graph output path", ".skillwiki/graph.json").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => emit(await runGraphBuild({ vault, out: opts.out })));
|
|
1252
2384
|
program.command("overlap <vault>").action(async (vault) => emit(await runOverlap({ vault })));
|
|
1253
|
-
program.command("orphans [vault]").action(async (vault) => emit(await runOrphans({
|
|
2385
|
+
program.command("orphans [vault]").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => emit(await runOrphans({
|
|
1254
2386
|
vault,
|
|
1255
2387
|
envValue: process.env.WIKI_PATH,
|
|
1256
|
-
home: process.env.HOME ?? ""
|
|
2388
|
+
home: process.env.HOME ?? "",
|
|
2389
|
+
wiki: opts.wiki
|
|
1257
2390
|
})));
|
|
1258
2391
|
program.command("audit <file>").action(async (file) => emit(await runAudit({ file })));
|
|
1259
2392
|
program.command("install").option("--target <dir>", "target install directory", `${process.env.HOME ?? ""}/.claude/skills/`).option("--dry-run", "preview only", false).option("--skills-root <dir>", "source skills directory (defaults to packaged)").action(async (opts) => {
|
|
1260
|
-
const skillsRoot = opts.skillsRoot ?? new URL("
|
|
2393
|
+
const skillsRoot = opts.skillsRoot ?? new URL("../skills/", import.meta.url).pathname;
|
|
1261
2394
|
emit(await runInstall({ skillsRoot, target: opts.target, dryRun: !!opts.dryRun }));
|
|
1262
2395
|
});
|
|
1263
|
-
program.command("path").option("--vault <dir>", "explicit vault override (runtime)").option("--target <dir>", "explicit target override (init-time)").option("--init-time", "use init-time chain instead of runtime", false).option("--explain", "include resolution chain in output", false).action(async (opts) => {
|
|
2396
|
+
program.command("path").option("--vault <dir>", "explicit vault override (runtime)").option("--target <dir>", "explicit target override (init-time)").option("--wiki <name>", "wiki profile name").option("--init-time", "use init-time chain instead of runtime", false).option("--explain", "include resolution chain in output", false).action(async (opts) => {
|
|
1264
2397
|
const initTime = !!opts.initTime;
|
|
1265
2398
|
const flag = initTime ? opts.target : opts.vault;
|
|
1266
2399
|
emit(await runPath({
|
|
@@ -1268,6 +2401,7 @@ program.command("path").option("--vault <dir>", "explicit vault override (runtim
|
|
|
1268
2401
|
envValue: process.env.WIKI_PATH,
|
|
1269
2402
|
home: process.env.HOME ?? "",
|
|
1270
2403
|
initTime,
|
|
2404
|
+
wiki: opts.wiki,
|
|
1271
2405
|
explain: !!opts.explain
|
|
1272
2406
|
}));
|
|
1273
2407
|
});
|
|
@@ -1279,7 +2413,7 @@ program.command("lang").option("--lang <code>", "explicit language override").op
|
|
|
1279
2413
|
explain: !!opts.explain
|
|
1280
2414
|
}));
|
|
1281
2415
|
});
|
|
1282
|
-
program.command("init").option("--target <dir>", "explicit target directory").requiredOption("--domain <text>", "knowledge domain seed").option("--taxonomy <csv>", "comma-separated tag list").option("--lang <code>", "output language (BCP 47 or alias)").option("--force", "override existing target / env conflict", false).action(async (opts) => {
|
|
2416
|
+
program.command("init").option("--target <dir>", "explicit target directory").requiredOption("--domain <text>", "knowledge domain seed").option("--taxonomy <csv>", "comma-separated tag list").option("--lang <code>", "output language (BCP 47 or alias)").option("--force", "override existing target / env conflict", false).option("--no-env", "skip writing ~/.skillwiki/.env").option("--profile <name>", "write as named wiki profile instead of WIKI_PATH").action(async (opts) => {
|
|
1283
2417
|
const templates = new URL("../templates/", import.meta.url).pathname;
|
|
1284
2418
|
const taxonomy = typeof opts.taxonomy === "string" ? opts.taxonomy.split(",").map((s) => s.trim()).filter((s) => s.length > 0) : void 0;
|
|
1285
2419
|
emit(await runInit({
|
|
@@ -1290,51 +2424,58 @@ program.command("init").option("--target <dir>", "explicit target directory").re
|
|
|
1290
2424
|
domain: opts.domain,
|
|
1291
2425
|
taxonomy,
|
|
1292
2426
|
lang: opts.lang,
|
|
1293
|
-
force: !!opts.force
|
|
2427
|
+
force: !!opts.force,
|
|
2428
|
+
noEnv: opts.env === false,
|
|
2429
|
+
profile: opts.profile
|
|
1294
2430
|
}));
|
|
1295
2431
|
});
|
|
1296
|
-
async function resolveVaultArg(arg) {
|
|
2432
|
+
async function resolveVaultArg(arg, wiki) {
|
|
1297
2433
|
if (arg) return { ok: true, vault: arg };
|
|
1298
2434
|
const r = await resolveRuntimePath({
|
|
1299
2435
|
flag: void 0,
|
|
1300
2436
|
envValue: process.env.WIKI_PATH,
|
|
1301
|
-
|
|
2437
|
+
wikiEnv: process.env.WIKI,
|
|
2438
|
+
home: process.env.HOME ?? "",
|
|
2439
|
+
wiki
|
|
1302
2440
|
});
|
|
1303
|
-
if (!r.ok)
|
|
2441
|
+
if (!r.ok) {
|
|
2442
|
+
const exitCode = r.error === "UNKNOWN_WIKI_PROFILE" ? 35 : 25;
|
|
2443
|
+
return { ok: false, exitCode, payload: r };
|
|
2444
|
+
}
|
|
1304
2445
|
return { ok: true, vault: r.data.path };
|
|
1305
2446
|
}
|
|
1306
|
-
program.command("links [vault]").action(async (vault) => {
|
|
1307
|
-
const v = await resolveVaultArg(vault);
|
|
2447
|
+
program.command("links [vault]").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2448
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1308
2449
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1309
2450
|
else emit(await runLinks({ vault: v.vault }));
|
|
1310
2451
|
});
|
|
1311
|
-
program.command("tag-audit [vault]").action(async (vault) => {
|
|
1312
|
-
const v = await resolveVaultArg(vault);
|
|
2452
|
+
program.command("tag-audit [vault]").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2453
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1313
2454
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1314
2455
|
else emit(await runTagAudit({ vault: v.vault }));
|
|
1315
2456
|
});
|
|
1316
|
-
program.command("index-check [vault]").action(async (vault) => {
|
|
1317
|
-
const v = await resolveVaultArg(vault);
|
|
2457
|
+
program.command("index-check [vault]").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2458
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1318
2459
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1319
2460
|
else emit(await runIndexCheck({ vault: v.vault }));
|
|
1320
2461
|
});
|
|
1321
|
-
program.command("stale [vault]").option("--days <n>", "staleness threshold in days", (s) => parseInt(s, 10), 90).action(async (vault, opts) => {
|
|
1322
|
-
const v = await resolveVaultArg(vault);
|
|
2462
|
+
program.command("stale [vault]").option("--days <n>", "staleness threshold in days", (s) => parseInt(s, 10), 90).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2463
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1323
2464
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1324
2465
|
else emit(await runStale({ vault: v.vault, days: opts.days }));
|
|
1325
2466
|
});
|
|
1326
|
-
program.command("pagesize [vault]").option("--lines <n>", "max body lines", (s) => parseInt(s, 10), 200).action(async (vault, opts) => {
|
|
1327
|
-
const v = await resolveVaultArg(vault);
|
|
2467
|
+
program.command("pagesize [vault]").option("--lines <n>", "max body lines", (s) => parseInt(s, 10), 200).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2468
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1328
2469
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1329
2470
|
else emit(await runPagesize({ vault: v.vault, lines: opts.lines }));
|
|
1330
2471
|
});
|
|
1331
|
-
program.command("log-rotate [vault]").option("--threshold <n>", "entry count threshold", (s) => parseInt(s, 10), 500).option("--apply", "actually rotate", false).action(async (vault, opts) => {
|
|
1332
|
-
const v = await resolveVaultArg(vault);
|
|
2472
|
+
program.command("log-rotate [vault]").option("--threshold <n>", "entry count threshold", (s) => parseInt(s, 10), 500).option("--apply", "actually rotate", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2473
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1333
2474
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1334
2475
|
else emit(await runLogRotate({ vault: v.vault, threshold: opts.threshold, apply: !!opts.apply }));
|
|
1335
2476
|
});
|
|
1336
|
-
program.command("lint [vault]").option("--days <n>", "stale threshold", (s) => parseInt(s, 10), 90).option("--lines <n>", "pagesize threshold", (s) => parseInt(s, 10), 200).option("--log-threshold <n>", "log rotation threshold", (s) => parseInt(s, 10), 500).action(async (vault, opts) => {
|
|
1337
|
-
const v = await resolveVaultArg(vault);
|
|
2477
|
+
program.command("lint [vault]").option("--days <n>", "stale threshold", (s) => parseInt(s, 10), 90).option("--lines <n>", "pagesize threshold", (s) => parseInt(s, 10), 200).option("--log-threshold <n>", "log rotation threshold", (s) => parseInt(s, 10), 500).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2478
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1338
2479
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1339
2480
|
else emit(await runLint({
|
|
1340
2481
|
vault: v.vault,
|
|
@@ -1344,6 +2485,43 @@ program.command("lint [vault]").option("--days <n>", "stale threshold", (s) => p
|
|
|
1344
2485
|
logThreshold: opts.logThreshold
|
|
1345
2486
|
}));
|
|
1346
2487
|
});
|
|
2488
|
+
var configCmd = program.command("config").description("manage skillwiki configuration");
|
|
2489
|
+
configCmd.command("get <key>").description("print the value of a config key").action(async (key) => emit(await runConfigGet({ key, home: process.env.HOME ?? "" })));
|
|
2490
|
+
configCmd.command("set <key> <value>").description("set a config key value").action(async (key, value) => emit(await runConfigSet({ key, value, home: process.env.HOME ?? "" })));
|
|
2491
|
+
configCmd.command("list").option("--profiles", "show wiki profiles summary", false).description("list all config key=value pairs").action(async (opts) => emit(await runConfigList({ home: process.env.HOME ?? "", profiles: !!opts.profiles })));
|
|
2492
|
+
configCmd.command("path").description("print the config file path").action(async () => emit(await runConfigPath({ home: process.env.HOME ?? "" })));
|
|
2493
|
+
program.command("doctor").description("diagnose skillwiki setup issues").action(async () => emit(await runDoctor({
|
|
2494
|
+
home: process.env.HOME ?? "",
|
|
2495
|
+
envValue: process.env.WIKI_PATH,
|
|
2496
|
+
argv: process.argv,
|
|
2497
|
+
currentVersion: pkg.version,
|
|
2498
|
+
cwd: process.cwd()
|
|
2499
|
+
})));
|
|
2500
|
+
program.command("archive <page> [vault]").description("archive a typed-knowledge page").option("--wiki <name>", "wiki profile name").action(async (page, vault, opts) => {
|
|
2501
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2502
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2503
|
+
else emit(await runArchive({ vault: v.vault, page }));
|
|
2504
|
+
});
|
|
2505
|
+
program.command("drift [vault]").description("detect content drift in raw sources").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2506
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2507
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2508
|
+
else emit(await runDrift({ vault: v.vault }));
|
|
2509
|
+
});
|
|
2510
|
+
program.command("dedup [vault]").description("detect duplicate raw sources by sha256").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2511
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2512
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2513
|
+
else emit(await runDedup({ vault: v.vault }));
|
|
2514
|
+
});
|
|
2515
|
+
program.command("migrate-citations [vault]").description("migrate ^[raw/...] markers to paragraph-end citations").option("--dry-run", "preview changes without writing", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2516
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2517
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2518
|
+
else emit(await runMigrateCitations({ vault: v.vault, dryRun: !!opts.dryRun }));
|
|
2519
|
+
});
|
|
2520
|
+
program.command("update").description("update skillwiki CLI to the latest version").option("--tag <tag>", "npm dist-tag", "beta").action(async (opts) => emit(await runUpdate({
|
|
2521
|
+
home: process.env.HOME ?? "",
|
|
2522
|
+
distTag: opts.tag
|
|
2523
|
+
})));
|
|
2524
|
+
triggerAutoUpdate(process.env.HOME ?? "", pkg.version);
|
|
1347
2525
|
program.parseAsync(process.argv).catch((e) => {
|
|
1348
2526
|
process.stdout.write(JSON.stringify({ ok: false, error: "INTERNAL", detail: { message: String(e) } }) + "\n");
|
|
1349
2527
|
process.exit(1);
|