skillwiki 0.2.0-beta.2 → 0.2.0-beta.20
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 +1174 -142
- 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 +90 -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,16 @@ 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
|
|
57
75
|
};
|
|
58
76
|
|
|
59
77
|
// ../shared/src/json-output.ts
|
|
@@ -93,10 +111,10 @@ var TypedKnowledgeSchema = z.object({
|
|
|
93
111
|
});
|
|
94
112
|
var sha256Hex = z.string().regex(/^[0-9a-f]{64}$/);
|
|
95
113
|
var RawSourceSchema = z.object({
|
|
96
|
-
title: z.string().min(1),
|
|
97
|
-
source_url: z.string().
|
|
114
|
+
title: z.string().min(1).optional(),
|
|
115
|
+
source_url: z.string().nullable(),
|
|
98
116
|
ingested: isoDate,
|
|
99
|
-
ingested_by: z.enum(["wiki-ingest", "proj-work", "manual"]),
|
|
117
|
+
ingested_by: z.enum(["wiki-ingest", "proj-work", "manual"]).optional(),
|
|
100
118
|
sha256: sha256Hex,
|
|
101
119
|
project: wikilink.optional(),
|
|
102
120
|
work_item: wikilink.optional(),
|
|
@@ -231,7 +249,7 @@ async function runHash(input) {
|
|
|
231
249
|
const sha256 = createHash("sha256").update(bodyBytes).digest("hex");
|
|
232
250
|
return {
|
|
233
251
|
exitCode: ExitCode.OK,
|
|
234
|
-
result: ok({ path: input.file, sha256, byte_count: bodyBytes.byteLength })
|
|
252
|
+
result: ok({ path: input.file, sha256, byte_count: bodyBytes.byteLength, humanHint: sha256 })
|
|
235
253
|
};
|
|
236
254
|
}
|
|
237
255
|
|
|
@@ -261,7 +279,7 @@ function runFetchGuardSync(input) {
|
|
|
261
279
|
result: err("HOST_BLOCKED", { sanitized_url: sanitized, host: u.hostname })
|
|
262
280
|
};
|
|
263
281
|
}
|
|
264
|
-
return { exitCode: ExitCode.OK, result: ok({ allowed: true, sanitized_url: sanitized }) };
|
|
282
|
+
return { exitCode: ExitCode.OK, result: ok({ allowed: true, sanitized_url: sanitized, humanHint: `ALLOWED: ${sanitized}` }) };
|
|
265
283
|
}
|
|
266
284
|
function sanitizeUrl(u) {
|
|
267
285
|
const clone = new URL(u.toString());
|
|
@@ -301,17 +319,18 @@ async function runValidate(input) {
|
|
|
301
319
|
}
|
|
302
320
|
const det = detectSchema(fm.data);
|
|
303
321
|
if (!det.schema) {
|
|
304
|
-
return { exitCode: ExitCode.SCHEMA_NOT_DETECTED, result: ok({ schema: null, valid: false, errors: [] }) };
|
|
322
|
+
return { exitCode: ExitCode.SCHEMA_NOT_DETECTED, result: ok({ schema: null, valid: false, errors: [], humanHint: "schema not detected" }) };
|
|
305
323
|
}
|
|
306
324
|
const parsed = SCHEMAS[det.schema].safeParse(fm.data);
|
|
307
325
|
if (!parsed.success) {
|
|
308
326
|
const errors = parsed.error.issues.map((i) => ({ path: i.path.join("."), message: i.message }));
|
|
309
327
|
return {
|
|
310
328
|
exitCode: ExitCode.INVALID_FRONTMATTER,
|
|
311
|
-
result: ok({ schema: det.schema, valid: false, errors })
|
|
329
|
+
result: ok({ schema: det.schema, valid: false, errors, humanHint: `INVALID (${det.schema})
|
|
330
|
+
${errors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}` })
|
|
312
331
|
};
|
|
313
332
|
}
|
|
314
|
-
return { exitCode: ExitCode.OK, result: ok({ schema: det.schema, valid: true, errors: [] }) };
|
|
333
|
+
return { exitCode: ExitCode.OK, result: ok({ schema: det.schema, valid: true, errors: [], humanHint: `VALID (${det.schema})` }) };
|
|
315
334
|
}
|
|
316
335
|
|
|
317
336
|
// src/commands/graph.ts
|
|
@@ -397,7 +416,8 @@ async function runGraphBuild(input) {
|
|
|
397
416
|
}
|
|
398
417
|
return {
|
|
399
418
|
exitCode: ExitCode.OK,
|
|
400
|
-
result: ok({ out_path: input.out, node_count: scan.data.typedKnowledge.length, edge_count }
|
|
419
|
+
result: ok({ out_path: input.out, node_count: scan.data.typedKnowledge.length, edge_count, humanHint: `nodes: ${scan.data.typedKnowledge.length}, edges: ${edge_count}
|
|
420
|
+
written: ${input.out}` })
|
|
401
421
|
};
|
|
402
422
|
}
|
|
403
423
|
function computeAdamicAdar(adj) {
|
|
@@ -472,22 +492,19 @@ async function runOverlap(input) {
|
|
|
472
492
|
}
|
|
473
493
|
return { id, members, score };
|
|
474
494
|
});
|
|
475
|
-
|
|
495
|
+
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");
|
|
496
|
+
return { exitCode: ExitCode.OK, result: ok({ clusters, humanHint }) };
|
|
476
497
|
}
|
|
477
498
|
|
|
478
499
|
// src/utils/wiki-path.ts
|
|
479
500
|
import { join as join2 } from "path";
|
|
480
501
|
|
|
481
502
|
// src/utils/dotenv.ts
|
|
482
|
-
import { readFile as readFile4 } from "fs/promises";
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
text = await readFile4(path, "utf8");
|
|
488
|
-
} catch {
|
|
489
|
-
return {};
|
|
490
|
-
}
|
|
503
|
+
import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
504
|
+
import { dirname as dirname2 } from "path";
|
|
505
|
+
var CONFIG_KEYS = ["WIKI_PATH", "WIKI_LANG"];
|
|
506
|
+
var _whitelist = new Set(CONFIG_KEYS);
|
|
507
|
+
function parseDotenvText(text) {
|
|
491
508
|
const out = {};
|
|
492
509
|
for (const rawLine of text.split(/\r?\n/)) {
|
|
493
510
|
const line = rawLine.trim();
|
|
@@ -496,12 +513,65 @@ async function parseDotenvFile(path) {
|
|
|
496
513
|
if (eq <= 0) continue;
|
|
497
514
|
const key = line.slice(0, eq).trim();
|
|
498
515
|
const value = line.slice(eq + 1).trim();
|
|
499
|
-
if (!
|
|
516
|
+
if (!_whitelist.has(key)) continue;
|
|
500
517
|
if (value.length === 0) continue;
|
|
501
518
|
out[key] = value;
|
|
502
519
|
}
|
|
503
520
|
return out;
|
|
504
521
|
}
|
|
522
|
+
async function parseDotenvFile(path) {
|
|
523
|
+
let text;
|
|
524
|
+
try {
|
|
525
|
+
text = await readFile4(path, "utf8");
|
|
526
|
+
} catch {
|
|
527
|
+
return {};
|
|
528
|
+
}
|
|
529
|
+
return parseDotenvText(text);
|
|
530
|
+
}
|
|
531
|
+
async function writeDotenv(filePath, entries, originalContent) {
|
|
532
|
+
const lines = originalContent !== void 0 ? updateLines(originalContent, entries) : freshLines(entries);
|
|
533
|
+
await mkdir2(dirname2(filePath), { recursive: true });
|
|
534
|
+
await writeFile2(filePath, lines.join("\n") + "\n", "utf8");
|
|
535
|
+
}
|
|
536
|
+
function freshLines(entries) {
|
|
537
|
+
const out = [];
|
|
538
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
539
|
+
if (value !== void 0) out.push(`${key}=${value}`);
|
|
540
|
+
}
|
|
541
|
+
return out;
|
|
542
|
+
}
|
|
543
|
+
function updateLines(originalContent, entries) {
|
|
544
|
+
let rawLines = originalContent.split(/\r?\n/);
|
|
545
|
+
if (rawLines.length > 0 && rawLines[rawLines.length - 1] === "") {
|
|
546
|
+
rawLines = rawLines.slice(0, -1);
|
|
547
|
+
}
|
|
548
|
+
const keysToWrite = new Set(Object.keys(entries));
|
|
549
|
+
const out = [];
|
|
550
|
+
for (const line of rawLines) {
|
|
551
|
+
const trimmed = line.trim();
|
|
552
|
+
if (trimmed.length === 0 || trimmed.startsWith("#")) {
|
|
553
|
+
out.push(line);
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
const eq = trimmed.indexOf("=");
|
|
557
|
+
if (eq <= 0) {
|
|
558
|
+
out.push(line);
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
const key = trimmed.slice(0, eq).trim();
|
|
562
|
+
if (keysToWrite.has(key)) {
|
|
563
|
+
out.push(`${key}=${entries[key]}`);
|
|
564
|
+
keysToWrite.delete(key);
|
|
565
|
+
} else {
|
|
566
|
+
out.push(line);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
for (const key of keysToWrite) {
|
|
570
|
+
const value = entries[key];
|
|
571
|
+
if (value !== void 0) out.push(`${key}=${value}`);
|
|
572
|
+
}
|
|
573
|
+
return out;
|
|
574
|
+
}
|
|
505
575
|
|
|
506
576
|
// src/utils/wiki-path.ts
|
|
507
577
|
async function resolveInitTimePath(input) {
|
|
@@ -569,7 +639,10 @@ async function runOrphans(input) {
|
|
|
569
639
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
570
640
|
const slugToPath = {};
|
|
571
641
|
for (const p of scan.data.typedKnowledge) {
|
|
572
|
-
|
|
642
|
+
const rel = p.relPath.replace(/\.md$/, "");
|
|
643
|
+
slugToPath[rel] = p.relPath;
|
|
644
|
+
const filename = rel.split("/").pop();
|
|
645
|
+
if (!(filename in slugToPath)) slugToPath[filename] = p.relPath;
|
|
573
646
|
}
|
|
574
647
|
const adj = {};
|
|
575
648
|
for (const p of scan.data.typedKnowledge) adj[p.relPath] = /* @__PURE__ */ new Set();
|
|
@@ -578,7 +651,7 @@ async function runOrphans(input) {
|
|
|
578
651
|
const split = splitFrontmatter(text);
|
|
579
652
|
const body = split.ok ? split.data.body : text;
|
|
580
653
|
for (const slug of extractBodyWikilinks(body)) {
|
|
581
|
-
const tgt = slugToPath[slug.split("/").pop()];
|
|
654
|
+
const tgt = slugToPath[slug] ?? slugToPath[slug.split("/").pop()];
|
|
582
655
|
if (tgt) {
|
|
583
656
|
adj[p.relPath].add(tgt);
|
|
584
657
|
adj[tgt].add(p.relPath);
|
|
@@ -609,7 +682,11 @@ async function runOrphans(input) {
|
|
|
609
682
|
}
|
|
610
683
|
}
|
|
611
684
|
}
|
|
612
|
-
|
|
685
|
+
const hintLines = [];
|
|
686
|
+
if (orphans.length > 0) hintLines.push(`orphans: ${orphans.length}`, ...orphans.map((o) => ` ${o}`));
|
|
687
|
+
if (bridges.length > 0) hintLines.push(`bridges: ${bridges.length}`, ...bridges.map((b) => ` ${b.path}`));
|
|
688
|
+
if (hintLines.length === 0) hintLines.push("no orphans or bridges");
|
|
689
|
+
return { exitCode: ExitCode.OK, result: ok({ orphans, bridges, humanHint: hintLines.join("\n") }) };
|
|
613
690
|
}
|
|
614
691
|
function simulateRemoval(adj, removed) {
|
|
615
692
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -630,20 +707,92 @@ function simulateRemoval(adj, removed) {
|
|
|
630
707
|
|
|
631
708
|
// src/commands/audit.ts
|
|
632
709
|
import { readFile as readFile5, stat as stat2 } from "fs/promises";
|
|
633
|
-
import { dirname as
|
|
710
|
+
import { dirname as dirname3, resolve, join as join3 } from "path";
|
|
634
711
|
|
|
635
712
|
// src/parsers/citations.ts
|
|
636
713
|
var FENCE2 = /```[\s\S]*?```/g;
|
|
714
|
+
var MARKER_RE = /\^\[(raw\/[^\]]+)\]/g;
|
|
715
|
+
function stripFences(body) {
|
|
716
|
+
return body.replace(FENCE2, "");
|
|
717
|
+
}
|
|
637
718
|
function extractCitationMarkers(body) {
|
|
638
|
-
const stripped = body
|
|
719
|
+
const stripped = stripFences(body);
|
|
639
720
|
const out = [];
|
|
640
|
-
const re = /\^\[(raw\/[^\]]+)\]/g;
|
|
641
721
|
let m;
|
|
642
|
-
while ((m =
|
|
722
|
+
while ((m = MARKER_RE.exec(stripped)) !== null) {
|
|
643
723
|
out.push({ marker: m[0], target: m[1] });
|
|
644
724
|
}
|
|
645
725
|
return out;
|
|
646
726
|
}
|
|
727
|
+
function hasSourcesFooter(body) {
|
|
728
|
+
return /^## Sources\s*$/m.test(stripFences(body));
|
|
729
|
+
}
|
|
730
|
+
function isLegacyCitationStyle(body) {
|
|
731
|
+
const markers = extractCitationMarkers(body);
|
|
732
|
+
if (markers.length === 0) return false;
|
|
733
|
+
if (!hasSourcesFooter(body)) return true;
|
|
734
|
+
const lines = stripFences(body).split("\n");
|
|
735
|
+
let inSources = false;
|
|
736
|
+
for (const line of lines) {
|
|
737
|
+
if (/^## Sources\b/.test(line.trim())) {
|
|
738
|
+
inSources = true;
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
if (inSources) continue;
|
|
742
|
+
const markerOnly = line.replace(MARKER_RE, "").trim();
|
|
743
|
+
if (markerOnly.length === 0 && /\^\[raw\//.test(line)) return true;
|
|
744
|
+
const lastMarkerIdx = line.lastIndexOf("^[raw/");
|
|
745
|
+
if (lastMarkerIdx >= 0) {
|
|
746
|
+
const afterLast = line.slice(lastMarkerIdx).replace(MARKER_RE, "").trim();
|
|
747
|
+
if (afterLast.length > 0) return true;
|
|
748
|
+
const beforeFirst = line.slice(0, line.indexOf("^[raw/")).trim();
|
|
749
|
+
if (beforeFirst.length > 0 && !/[.!?]\s*$/.test(beforeFirst)) return true;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
function hasOrphanedCitations(body) {
|
|
755
|
+
const stripped = stripFences(body);
|
|
756
|
+
const lines = stripped.split("\n");
|
|
757
|
+
let inSources = false;
|
|
758
|
+
let sourcesEnded = false;
|
|
759
|
+
let sourcesStartLine = -1;
|
|
760
|
+
let lastNonBlankInSources = -1;
|
|
761
|
+
for (let i = 0; i < lines.length; i++) {
|
|
762
|
+
const line = lines[i];
|
|
763
|
+
const trimmed = line.trim();
|
|
764
|
+
if (/^## Sources\b/.test(trimmed)) {
|
|
765
|
+
inSources = true;
|
|
766
|
+
sourcesStartLine = i;
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
if (!inSources || sourcesEnded) continue;
|
|
770
|
+
if (trimmed.length === 0) {
|
|
771
|
+
if (lastNonBlankInSources >= 0) {
|
|
772
|
+
sourcesEnded = true;
|
|
773
|
+
}
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
const isListItem = /^\s*[-*]\s+/.test(line);
|
|
777
|
+
const hasMarker = /\^\[raw\//.test(line);
|
|
778
|
+
if (isListItem && hasMarker) {
|
|
779
|
+
lastNonBlankInSources = i;
|
|
780
|
+
} else if (hasMarker && !isListItem) {
|
|
781
|
+
return true;
|
|
782
|
+
} else {
|
|
783
|
+
sourcesEnded = true;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
if (sourcesStartLine === -1) return false;
|
|
787
|
+
if (sourcesEnded) {
|
|
788
|
+
for (let i = lastNonBlankInSources + 1; i < lines.length; i++) {
|
|
789
|
+
if (/\^\[raw\//.test(lines[i])) {
|
|
790
|
+
return true;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
647
796
|
|
|
648
797
|
// src/commands/audit.ts
|
|
649
798
|
async function runAudit(input) {
|
|
@@ -657,7 +806,7 @@ async function runAudit(input) {
|
|
|
657
806
|
if (!fm.ok) return { exitCode: ExitCode.INVALID_FRONTMATTER, result: fm };
|
|
658
807
|
const split = splitFrontmatter(text);
|
|
659
808
|
const body = split.ok ? split.data.body : text;
|
|
660
|
-
const vault = await findVaultRoot(
|
|
809
|
+
const vault = await findVaultRoot(dirname3(resolve(input.file)));
|
|
661
810
|
if (!vault) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID") };
|
|
662
811
|
const markers = extractCitationMarkers(body);
|
|
663
812
|
const resolved = await Promise.all(markers.map(async (m) => {
|
|
@@ -672,13 +821,36 @@ async function runAudit(input) {
|
|
|
672
821
|
const referenced = new Set(resolved.map((m) => m.target));
|
|
673
822
|
const unused_sources = sources.filter((s) => !referenced.has(s));
|
|
674
823
|
const missing_from_sources = [...referenced].filter((t) => !sources.includes(t));
|
|
824
|
+
const broken = resolved.filter((m) => !m.resolved);
|
|
825
|
+
const footerMatch = body.match(/\n## Sources\n([\s\S]*)$/);
|
|
826
|
+
let footer_consistency;
|
|
827
|
+
if (footerMatch) {
|
|
828
|
+
const footerTargets = /* @__PURE__ */ new Set();
|
|
829
|
+
const footerRe = /\^\[(raw\/[^\]]+)\]/g;
|
|
830
|
+
let mm;
|
|
831
|
+
while ((mm = footerRe.exec(footerMatch[1])) !== null) footerTargets.add(mm[1]);
|
|
832
|
+
const bodyTargets = new Set(resolved.map((m) => m.target));
|
|
833
|
+
const missing_from_footer = [...bodyTargets].filter((t) => !footerTargets.has(t));
|
|
834
|
+
const extra_in_footer = [...footerTargets].filter((t) => !bodyTargets.has(t));
|
|
835
|
+
footer_consistency = { missing_from_footer, extra_in_footer };
|
|
836
|
+
}
|
|
837
|
+
const hintLines = [];
|
|
838
|
+
hintLines.push(`markers: ${resolved.length}, broken: ${broken.length}`);
|
|
839
|
+
if (unused_sources.length > 0) hintLines.push(`unused_sources: ${unused_sources.length}`);
|
|
840
|
+
if (missing_from_sources.length > 0) hintLines.push(`missing_from_sources: ${missing_from_sources.length}`);
|
|
841
|
+
if (footer_consistency) {
|
|
842
|
+
if (footer_consistency.missing_from_footer.length > 0) hintLines.push(`missing_from_footer: ${footer_consistency.missing_from_footer.length}`);
|
|
843
|
+
if (footer_consistency.extra_in_footer.length > 0) hintLines.push(`extra_in_footer: ${footer_consistency.extra_in_footer.length}`);
|
|
844
|
+
}
|
|
845
|
+
if (broken.length === 0 && unused_sources.length === 0 && missing_from_sources.length === 0) hintLines.push("OK");
|
|
846
|
+
const humanHint = hintLines.join("\n");
|
|
675
847
|
if (resolved.some((m) => !m.resolved)) {
|
|
676
|
-
return { exitCode: ExitCode.UNRESOLVED_MARKERS, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources } }) };
|
|
848
|
+
return { exitCode: ExitCode.UNRESOLVED_MARKERS, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, footer_consistency, humanHint }) };
|
|
677
849
|
}
|
|
678
850
|
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 } }) };
|
|
851
|
+
return { exitCode: ExitCode.SOURCES_INCONSISTENT, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, footer_consistency, humanHint }) };
|
|
680
852
|
}
|
|
681
|
-
return { exitCode: ExitCode.OK, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources } }) };
|
|
853
|
+
return { exitCode: ExitCode.OK, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, footer_consistency, humanHint }) };
|
|
682
854
|
}
|
|
683
855
|
async function findVaultRoot(start) {
|
|
684
856
|
let cur = start;
|
|
@@ -688,7 +860,7 @@ async function findVaultRoot(start) {
|
|
|
688
860
|
return cur;
|
|
689
861
|
} catch {
|
|
690
862
|
}
|
|
691
|
-
const parent =
|
|
863
|
+
const parent = dirname3(cur);
|
|
692
864
|
if (parent === cur) return null;
|
|
693
865
|
cur = parent;
|
|
694
866
|
}
|
|
@@ -700,10 +872,10 @@ import { readdir as readdir2, stat as stat4 } from "fs/promises";
|
|
|
700
872
|
import { join as join4 } from "path";
|
|
701
873
|
|
|
702
874
|
// src/utils/install-fs.ts
|
|
703
|
-
import { copyFile, mkdir as
|
|
704
|
-
import { dirname as
|
|
875
|
+
import { copyFile, mkdir as mkdir3, rename, writeFile as writeFile3, stat as stat3 } from "fs/promises";
|
|
876
|
+
import { dirname as dirname4 } from "path";
|
|
705
877
|
async function atomicCopyWithBackup(src, dst) {
|
|
706
|
-
await
|
|
878
|
+
await mkdir3(dirname4(dst), { recursive: true });
|
|
707
879
|
let backupPath = null;
|
|
708
880
|
try {
|
|
709
881
|
await stat3(dst);
|
|
@@ -721,9 +893,9 @@ async function atomicCopyWithBackup(src, dst) {
|
|
|
721
893
|
return ok({ copied: true, backupPath });
|
|
722
894
|
}
|
|
723
895
|
async function writeManifest(path, m) {
|
|
724
|
-
await
|
|
896
|
+
await mkdir3(dirname4(path), { recursive: true });
|
|
725
897
|
const enriched = { installed_at: (/* @__PURE__ */ new Date()).toISOString(), ...m };
|
|
726
|
-
await
|
|
898
|
+
await writeFile3(path, JSON.stringify(enriched, null, 2));
|
|
727
899
|
}
|
|
728
900
|
|
|
729
901
|
// src/commands/install.ts
|
|
@@ -758,7 +930,12 @@ async function runInstall(input) {
|
|
|
758
930
|
}
|
|
759
931
|
const manifest_path = join4(input.target, "wiki-manifest.json");
|
|
760
932
|
if (!input.dryRun) await writeManifest(manifest_path, { installed, backed_up });
|
|
761
|
-
|
|
933
|
+
const hintLines = [
|
|
934
|
+
`installed: ${installed.length}`,
|
|
935
|
+
input.dryRun ? "(dry run)" : `backed up: ${backed_up.length}`,
|
|
936
|
+
`manifest: ${manifest_path}`
|
|
937
|
+
];
|
|
938
|
+
return { exitCode: ExitCode.OK, result: ok({ installed, backed_up, manifest_path, humanHint: hintLines.join("\n") }) };
|
|
762
939
|
}
|
|
763
940
|
|
|
764
941
|
// src/commands/path.ts
|
|
@@ -770,7 +947,7 @@ async function runPath(input) {
|
|
|
770
947
|
home: input.home,
|
|
771
948
|
explain: input.explain
|
|
772
949
|
});
|
|
773
|
-
return { exitCode: ExitCode.OK, result: ok({ path: r2.path, source: r2.source, ...r2.chain ? { chain: r2.chain } : {} }) };
|
|
950
|
+
return { exitCode: ExitCode.OK, result: ok({ path: r2.path, source: r2.source, ...r2.chain ? { chain: r2.chain } : {}, humanHint: `${r2.path} (via ${r2.source})` }) };
|
|
774
951
|
}
|
|
775
952
|
const r = await resolveRuntimePath({
|
|
776
953
|
flag: input.flag,
|
|
@@ -779,7 +956,7 @@ async function runPath(input) {
|
|
|
779
956
|
explain: input.explain
|
|
780
957
|
});
|
|
781
958
|
if (!r.ok) return { exitCode: ExitCode.NO_VAULT_CONFIGURED, result: r };
|
|
782
|
-
return { exitCode: ExitCode.OK, result: ok({ path: r.data.path, source: r.data.source, ...r.data.chain ? { chain: r.data.chain } : {} }) };
|
|
959
|
+
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
960
|
}
|
|
784
961
|
|
|
785
962
|
// src/utils/lang.ts
|
|
@@ -833,14 +1010,42 @@ async function runLang(input) {
|
|
|
833
1010
|
value: resolved.value,
|
|
834
1011
|
source: resolved.source,
|
|
835
1012
|
canonical: resolved.canonical,
|
|
836
|
-
...chain ? { chain } : {}
|
|
1013
|
+
...chain ? { chain } : {},
|
|
1014
|
+
humanHint: `${resolved.value} (via ${resolved.source})`
|
|
837
1015
|
})
|
|
838
1016
|
};
|
|
839
1017
|
}
|
|
840
1018
|
|
|
841
1019
|
// src/commands/init.ts
|
|
842
|
-
import { mkdir as
|
|
843
|
-
import { join as join7
|
|
1020
|
+
import { mkdir as mkdir4, readFile as readFile6, readdir as readdir3, writeFile as writeFile4 } from "fs/promises";
|
|
1021
|
+
import { join as join7 } from "path";
|
|
1022
|
+
|
|
1023
|
+
// src/parsers/taxonomy.ts
|
|
1024
|
+
import yaml2 from "js-yaml";
|
|
1025
|
+
var FENCE_RE = /^##\s+Tag Taxonomy\s*$[\s\S]*?```yaml\s*\n([\s\S]*?)\n```/m;
|
|
1026
|
+
function extractTaxonomy(schemaText) {
|
|
1027
|
+
const m = schemaText.match(FENCE_RE);
|
|
1028
|
+
if (!m) return err("NO_TAXONOMY_BLOCK", { message: "No fenced YAML taxonomy block found in SCHEMA.md" });
|
|
1029
|
+
let parsed;
|
|
1030
|
+
try {
|
|
1031
|
+
parsed = yaml2.load(m[1], { schema: yaml2.JSON_SCHEMA });
|
|
1032
|
+
} catch (e) {
|
|
1033
|
+
return err("INVALID_FRONTMATTER", { message: e.message });
|
|
1034
|
+
}
|
|
1035
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
1036
|
+
return err("INVALID_FRONTMATTER", { message: "taxonomy block is not an object" });
|
|
1037
|
+
}
|
|
1038
|
+
const tax = parsed.taxonomy;
|
|
1039
|
+
if (!Array.isArray(tax)) {
|
|
1040
|
+
return err("INVALID_FRONTMATTER", { message: "taxonomy key missing or not an array" });
|
|
1041
|
+
}
|
|
1042
|
+
if (!tax.every((x) => typeof x === "string")) {
|
|
1043
|
+
return err("INVALID_FRONTMATTER", { message: "taxonomy must be a list of strings" });
|
|
1044
|
+
}
|
|
1045
|
+
return ok(tax);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// src/commands/init.ts
|
|
844
1049
|
var DEFAULT_TAXONOMY = [
|
|
845
1050
|
"research",
|
|
846
1051
|
"comparison",
|
|
@@ -865,25 +1070,59 @@ var VAULT_DIRS = [
|
|
|
865
1070
|
"meta",
|
|
866
1071
|
"projects"
|
|
867
1072
|
];
|
|
1073
|
+
function extractDomainFromSchema(text) {
|
|
1074
|
+
const m = text.match(/^##\s+Domain\s*\n([\s\S]*?)(?=\n\n|\n##|\s*$)/m);
|
|
1075
|
+
if (!m) return "";
|
|
1076
|
+
const d = m[1].trim();
|
|
1077
|
+
return d.startsWith("##") ? "" : d;
|
|
1078
|
+
}
|
|
1079
|
+
async function discoverTagsFromPages(target, knownSlugs) {
|
|
1080
|
+
const knownSet = new Set(knownSlugs);
|
|
1081
|
+
const discovered = /* @__PURE__ */ new Set();
|
|
1082
|
+
for (const dir of ["entities", "concepts", "comparisons", "queries"]) {
|
|
1083
|
+
let entries;
|
|
1084
|
+
try {
|
|
1085
|
+
entries = (await readdir3(join7(target, dir), { withFileTypes: true })).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name);
|
|
1086
|
+
} catch {
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
for (const file of entries) {
|
|
1090
|
+
try {
|
|
1091
|
+
const text = await readFile6(join7(target, dir, file), "utf8");
|
|
1092
|
+
const fm = extractFrontmatter(text);
|
|
1093
|
+
if (!fm.ok || !fm.data.tags || !Array.isArray(fm.data.tags)) continue;
|
|
1094
|
+
for (const t of fm.data.tags) {
|
|
1095
|
+
if (typeof t === "string" && !knownSet.has(t)) discovered.add(t);
|
|
1096
|
+
}
|
|
1097
|
+
} catch {
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return [...discovered].sort();
|
|
1102
|
+
}
|
|
868
1103
|
async function runInit(input) {
|
|
869
1104
|
const pathRes = await resolveInitTimePath({ flag: input.flag, envValue: input.envValue, home: input.home });
|
|
870
1105
|
const target = pathRes.path;
|
|
871
1106
|
const langRes = await resolveLang({ flag: input.lang, envValue: void 0, home: input.home });
|
|
872
1107
|
const canonicalLang = langRes.canonical;
|
|
873
|
-
let
|
|
1108
|
+
let oldSchemaText;
|
|
874
1109
|
try {
|
|
875
|
-
await
|
|
876
|
-
hasSchema = true;
|
|
1110
|
+
oldSchemaText = await readFile6(join7(target, "SCHEMA.md"), "utf8");
|
|
877
1111
|
} catch {
|
|
878
1112
|
}
|
|
879
|
-
if (
|
|
1113
|
+
if (oldSchemaText && !input.force) {
|
|
880
1114
|
return {
|
|
881
1115
|
exitCode: ExitCode.INIT_TARGET_NOT_EMPTY,
|
|
882
1116
|
result: err("INIT_TARGET_NOT_EMPTY", { target })
|
|
883
1117
|
};
|
|
884
1118
|
}
|
|
885
1119
|
const envPath = join7(input.home, ".skillwiki", ".env");
|
|
886
|
-
|
|
1120
|
+
let existingEnvRaw = "";
|
|
1121
|
+
try {
|
|
1122
|
+
existingEnvRaw = await readFile6(envPath, "utf8");
|
|
1123
|
+
} catch {
|
|
1124
|
+
}
|
|
1125
|
+
const existingEnv = parseDotenvText(existingEnvRaw);
|
|
887
1126
|
const swDotenvHadPath = existingEnv.WIKI_PATH !== void 0;
|
|
888
1127
|
if (existingEnv.WIKI_PATH !== void 0 && existingEnv.WIKI_PATH !== target && !input.force) {
|
|
889
1128
|
return {
|
|
@@ -899,73 +1138,125 @@ async function runInit(input) {
|
|
|
899
1138
|
}
|
|
900
1139
|
const created = [];
|
|
901
1140
|
try {
|
|
902
|
-
await
|
|
1141
|
+
await mkdir4(target, { recursive: true });
|
|
903
1142
|
for (const d of VAULT_DIRS) {
|
|
904
|
-
await
|
|
1143
|
+
await mkdir4(join7(target, d), { recursive: true });
|
|
905
1144
|
created.push(d + "/");
|
|
906
1145
|
}
|
|
907
1146
|
} catch (e) {
|
|
908
1147
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
|
|
909
1148
|
}
|
|
910
1149
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
911
|
-
|
|
912
|
-
|
|
1150
|
+
let taxonomy = input.taxonomy && input.taxonomy.length > 0 ? input.taxonomy : DEFAULT_TAXONOMY;
|
|
1151
|
+
let domain = input.domain;
|
|
1152
|
+
let oldTaxonomy = [];
|
|
1153
|
+
if (oldSchemaText) {
|
|
1154
|
+
if (!domain) {
|
|
1155
|
+
const oldDomain = extractDomainFromSchema(oldSchemaText);
|
|
1156
|
+
if (oldDomain) domain = oldDomain;
|
|
1157
|
+
}
|
|
1158
|
+
const oldTax = extractTaxonomy(oldSchemaText);
|
|
1159
|
+
if (oldTax.ok) oldTaxonomy = oldTax.data;
|
|
1160
|
+
}
|
|
1161
|
+
const taxonomySet = new Set(taxonomy);
|
|
1162
|
+
for (const t of oldTaxonomy) {
|
|
1163
|
+
if (!taxonomySet.has(t)) {
|
|
1164
|
+
taxonomy.push(t);
|
|
1165
|
+
taxonomySet.add(t);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
const discovered = await discoverTagsFromPages(target, taxonomy);
|
|
1169
|
+
const discovered_tags = discovered.length;
|
|
1170
|
+
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
1171
|
try {
|
|
914
1172
|
const schemaTpl = await readFile6(join7(input.templates, "SCHEMA.md"), "utf8");
|
|
915
|
-
const schema = schemaTpl.replace("{{DOMAIN}}",
|
|
916
|
-
await
|
|
1173
|
+
const schema = schemaTpl.replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang).replace("{{TAXONOMY_YAML}}", fullTaxonomyYaml);
|
|
1174
|
+
await writeFile4(join7(target, "SCHEMA.md"), schema, "utf8");
|
|
917
1175
|
created.push("SCHEMA.md");
|
|
918
1176
|
} catch (e) {
|
|
919
1177
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: "SCHEMA.md", message: String(e) }) };
|
|
920
1178
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1179
|
+
const preserved = [];
|
|
1180
|
+
async function writeOrPreserve(fileName, render) {
|
|
1181
|
+
try {
|
|
1182
|
+
const existing = await readFile6(join7(target, fileName), "utf8");
|
|
1183
|
+
if (existing.split("\n").length > 10) {
|
|
1184
|
+
preserved.push(fileName);
|
|
1185
|
+
return void 0;
|
|
1186
|
+
}
|
|
1187
|
+
} catch {
|
|
1188
|
+
}
|
|
1189
|
+
try {
|
|
1190
|
+
await writeFile4(join7(target, fileName), await render(), "utf8");
|
|
1191
|
+
created.push(fileName);
|
|
1192
|
+
return void 0;
|
|
1193
|
+
} catch (e) {
|
|
1194
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: fileName, message: String(e) }) };
|
|
1195
|
+
}
|
|
936
1196
|
}
|
|
937
|
-
|
|
938
|
-
await
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
return {
|
|
1197
|
+
const err1 = await writeOrPreserve("index.md", async () => {
|
|
1198
|
+
const tpl = await readFile6(join7(input.templates, "index.md"), "utf8");
|
|
1199
|
+
return tpl.replace("{{INIT_DATE}}", today);
|
|
1200
|
+
});
|
|
1201
|
+
if (err1) return err1;
|
|
1202
|
+
const err22 = await writeOrPreserve("log.md", async () => {
|
|
1203
|
+
const tpl = await readFile6(join7(input.templates, "log.md"), "utf8");
|
|
1204
|
+
return tpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang);
|
|
1205
|
+
});
|
|
1206
|
+
if (err22) return err22;
|
|
1207
|
+
const isTempPath = target.startsWith("/tmp/") || target === "/tmp" || target.startsWith("/var/tmp/") || target === "/var/tmp" || target.startsWith("/private/tmp/");
|
|
1208
|
+
const skipEnv = !!input.noEnv || isTempPath;
|
|
1209
|
+
let envWritten = "";
|
|
1210
|
+
if (!skipEnv) {
|
|
1211
|
+
try {
|
|
1212
|
+
await writeDotenv(envPath, { WIKI_PATH: target, WIKI_LANG: canonicalLang }, existingEnvRaw);
|
|
1213
|
+
envWritten = envPath;
|
|
1214
|
+
} catch (e) {
|
|
1215
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: envPath, message: String(e) }) };
|
|
1216
|
+
}
|
|
945
1217
|
}
|
|
946
1218
|
const importedFromHermes = pathRes.source === "hermes-dotenv" && !swDotenvHadPath;
|
|
1219
|
+
const humanHint = [
|
|
1220
|
+
`vault: ${target}`,
|
|
1221
|
+
`domain: ${domain}`,
|
|
1222
|
+
`lang: ${canonicalLang}`,
|
|
1223
|
+
`created: ${created.length}, preserved: ${preserved.length}`,
|
|
1224
|
+
`discovered tags: ${discovered_tags}`,
|
|
1225
|
+
skipEnv ? "env: skipped" : `env: ${envWritten}`
|
|
1226
|
+
].join("\n");
|
|
947
1227
|
return {
|
|
948
1228
|
exitCode: ExitCode.OK,
|
|
949
1229
|
result: ok({
|
|
950
1230
|
vault: target,
|
|
951
|
-
domain
|
|
1231
|
+
domain,
|
|
952
1232
|
taxonomy,
|
|
953
1233
|
lang: canonicalLang,
|
|
954
1234
|
created,
|
|
955
|
-
|
|
956
|
-
|
|
1235
|
+
preserved,
|
|
1236
|
+
env_written: envWritten,
|
|
1237
|
+
env_skipped: skipEnv,
|
|
1238
|
+
imported_from_hermes: importedFromHermes,
|
|
1239
|
+
discovered_tags,
|
|
1240
|
+
humanHint
|
|
957
1241
|
})
|
|
958
1242
|
};
|
|
959
1243
|
}
|
|
960
1244
|
|
|
1245
|
+
// src/utils/slug.ts
|
|
1246
|
+
function buildSlugMap(pages) {
|
|
1247
|
+
const map = /* @__PURE__ */ new Map();
|
|
1248
|
+
for (const p of pages) {
|
|
1249
|
+
const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
|
|
1250
|
+
map.set(slug.toLowerCase(), slug);
|
|
1251
|
+
}
|
|
1252
|
+
return map;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
961
1255
|
// src/commands/links.ts
|
|
962
1256
|
async function runLinks(input) {
|
|
963
1257
|
const scan = await scanVault(input.vault);
|
|
964
1258
|
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
|
-
}
|
|
1259
|
+
const slugs = buildSlugMap(scan.data.typedKnowledge);
|
|
969
1260
|
const broken = [];
|
|
970
1261
|
for (const p of scan.data.typedKnowledge) {
|
|
971
1262
|
const text = await readPage(p);
|
|
@@ -974,48 +1265,22 @@ async function runLinks(input) {
|
|
|
974
1265
|
const lines = body.split("\n");
|
|
975
1266
|
for (const slug of extractBodyWikilinks(body)) {
|
|
976
1267
|
const tail = slug.split("/").pop();
|
|
977
|
-
if (!slugs.has(tail)) {
|
|
1268
|
+
if (!slugs.has(tail.toLowerCase())) {
|
|
978
1269
|
const line = lines.findIndex((l) => l.includes(`[[${slug}`));
|
|
979
1270
|
broken.push({ page: p.relPath, slug, line: line >= 0 ? line + 1 : 0 });
|
|
980
1271
|
}
|
|
981
1272
|
}
|
|
982
1273
|
}
|
|
983
1274
|
if (broken.length > 0) {
|
|
984
|
-
return { exitCode: ExitCode.BROKEN_WIKILINKS, result: ok({ broken
|
|
1275
|
+
return { exitCode: ExitCode.BROKEN_WIKILINKS, result: ok({ broken, humanHint: `broken: ${broken.length}
|
|
1276
|
+
${broken.map((b) => ` ${b.page}:[[${b.slug}]] (line ${b.line})`).join("\n")}` }) };
|
|
985
1277
|
}
|
|
986
|
-
return { exitCode: ExitCode.OK, result: ok({ broken }) };
|
|
1278
|
+
return { exitCode: ExitCode.OK, result: ok({ broken, humanHint: "no broken wikilinks" }) };
|
|
987
1279
|
}
|
|
988
1280
|
|
|
989
1281
|
// src/commands/tag-audit.ts
|
|
990
1282
|
import { readFile as readFile7 } from "fs/promises";
|
|
991
1283
|
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
1284
|
async function runTagAudit(input) {
|
|
1020
1285
|
const scan = await scanVault(input.vault);
|
|
1021
1286
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
@@ -1037,9 +1302,9 @@ async function runTagAudit(input) {
|
|
|
1037
1302
|
}
|
|
1038
1303
|
}
|
|
1039
1304
|
if (violations.length > 0) {
|
|
1040
|
-
return { exitCode: ExitCode.TAG_NOT_IN_TAXONOMY, result: ok({ violations, taxonomy: tax.data }) };
|
|
1305
|
+
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
1306
|
}
|
|
1042
|
-
return { exitCode: ExitCode.OK, result: ok({ violations, taxonomy: tax.data }) };
|
|
1307
|
+
return { exitCode: ExitCode.OK, result: ok({ violations, taxonomy: tax.data, humanHint: "all tags valid" }) };
|
|
1043
1308
|
}
|
|
1044
1309
|
|
|
1045
1310
|
// src/commands/index-check.ts
|
|
@@ -1053,7 +1318,11 @@ async function runIndexCheck(input) {
|
|
|
1053
1318
|
indexText = await readFile8(join9(input.vault, "index.md"), "utf8");
|
|
1054
1319
|
} catch {
|
|
1055
1320
|
}
|
|
1056
|
-
const
|
|
1321
|
+
const indexSlugsLower = /* @__PURE__ */ new Map();
|
|
1322
|
+
for (const s of extractBodyWikilinks(indexText)) {
|
|
1323
|
+
const tail = s.split("/").pop();
|
|
1324
|
+
indexSlugsLower.set(tail.toLowerCase(), tail);
|
|
1325
|
+
}
|
|
1057
1326
|
const fileSlugs = /* @__PURE__ */ new Map();
|
|
1058
1327
|
for (const p of scan.data.typedKnowledge) {
|
|
1059
1328
|
const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
|
|
@@ -1061,16 +1330,21 @@ async function runIndexCheck(input) {
|
|
|
1061
1330
|
}
|
|
1062
1331
|
const missing_from_index = [];
|
|
1063
1332
|
for (const [slug, relPath] of fileSlugs.entries()) {
|
|
1064
|
-
if (!
|
|
1333
|
+
if (!indexSlugsLower.has(slug.toLowerCase())) missing_from_index.push(relPath);
|
|
1065
1334
|
}
|
|
1335
|
+
const fileSlugsLower = new Set([...fileSlugs.keys()].map((s) => s.toLowerCase()));
|
|
1066
1336
|
const ghost_entries = [];
|
|
1067
|
-
for (const
|
|
1068
|
-
if (!
|
|
1337
|
+
for (const [lower, orig] of indexSlugsLower) {
|
|
1338
|
+
if (!fileSlugsLower.has(lower)) ghost_entries.push(orig);
|
|
1069
1339
|
}
|
|
1340
|
+
const hintLines = [];
|
|
1341
|
+
if (missing_from_index.length > 0) hintLines.push(`missing from index: ${missing_from_index.length}`, ...missing_from_index.map((p) => ` ${p}`));
|
|
1342
|
+
if (ghost_entries.length > 0) hintLines.push(`ghost entries: ${ghost_entries.length}`, ...ghost_entries.map((g) => ` ${g}`));
|
|
1343
|
+
if (hintLines.length === 0) hintLines.push("index OK");
|
|
1070
1344
|
if (missing_from_index.length > 0 || ghost_entries.length > 0) {
|
|
1071
|
-
return { exitCode: ExitCode.INDEX_INCOMPLETE, result: ok({ missing_from_index, ghost_entries }) };
|
|
1345
|
+
return { exitCode: ExitCode.INDEX_INCOMPLETE, result: ok({ missing_from_index, ghost_entries, humanHint: hintLines.join("\n") }) };
|
|
1072
1346
|
}
|
|
1073
|
-
return { exitCode: ExitCode.OK, result: ok({ missing_from_index, ghost_entries }) };
|
|
1347
|
+
return { exitCode: ExitCode.OK, result: ok({ missing_from_index, ghost_entries, humanHint: hintLines.join("\n") }) };
|
|
1074
1348
|
}
|
|
1075
1349
|
|
|
1076
1350
|
// src/commands/stale.ts
|
|
@@ -1110,8 +1384,8 @@ async function runStale(input) {
|
|
|
1110
1384
|
stale.push({ page: p.relPath, page_updated: updated, newest_source_ingested: newest, gap_days: gap });
|
|
1111
1385
|
}
|
|
1112
1386
|
}
|
|
1113
|
-
if (stale.length > 0) return { exitCode: ExitCode.STALE_PAGE, result: ok({ stale }) };
|
|
1114
|
-
return { exitCode: ExitCode.OK, result: ok({ stale }) };
|
|
1387
|
+
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") }) };
|
|
1388
|
+
return { exitCode: ExitCode.OK, result: ok({ stale, humanHint: "no stale pages" }) };
|
|
1115
1389
|
}
|
|
1116
1390
|
|
|
1117
1391
|
// src/commands/pagesize.ts
|
|
@@ -1126,17 +1400,17 @@ async function runPagesize(input) {
|
|
|
1126
1400
|
const count = body.split("\n").length;
|
|
1127
1401
|
if (count > input.lines) oversized.push({ page: p.relPath, lines: count });
|
|
1128
1402
|
}
|
|
1129
|
-
if (oversized.length > 0) return { exitCode: ExitCode.PAGE_TOO_LARGE, result: ok({ oversized }) };
|
|
1130
|
-
return { exitCode: ExitCode.OK, result: ok({ oversized }) };
|
|
1403
|
+
if (oversized.length > 0) return { exitCode: ExitCode.PAGE_TOO_LARGE, result: ok({ oversized, humanHint: oversized.map((p) => `${p.page}: ${p.lines} lines`).join("\n") }) };
|
|
1404
|
+
return { exitCode: ExitCode.OK, result: ok({ oversized, humanHint: "all pages within size limit" }) };
|
|
1131
1405
|
}
|
|
1132
1406
|
|
|
1133
1407
|
// src/commands/log-rotate.ts
|
|
1134
|
-
import { readFile as readFile10, rename as rename2, writeFile as
|
|
1408
|
+
import { readFile as readFile10, rename as rename2, writeFile as writeFile5, stat as stat5 } from "fs/promises";
|
|
1135
1409
|
import { join as join11 } from "path";
|
|
1136
1410
|
var ENTRY_RE = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
|
|
1137
1411
|
async function runLogRotate(input) {
|
|
1138
1412
|
try {
|
|
1139
|
-
await
|
|
1413
|
+
await stat5(join11(input.vault, "SCHEMA.md"));
|
|
1140
1414
|
} catch {
|
|
1141
1415
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
1142
1416
|
}
|
|
@@ -1150,12 +1424,12 @@ async function runLogRotate(input) {
|
|
|
1150
1424
|
const matches = [...logText.matchAll(ENTRY_RE)];
|
|
1151
1425
|
const entries = matches.length;
|
|
1152
1426
|
if (entries < input.threshold) {
|
|
1153
|
-
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: false }) };
|
|
1427
|
+
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: false, humanHint: `${entries}/${input.threshold} entries \u2014 no rotation needed` }) };
|
|
1154
1428
|
}
|
|
1155
1429
|
if (!input.apply) {
|
|
1156
1430
|
return {
|
|
1157
1431
|
exitCode: ExitCode.LOG_ROTATE_NEEDED,
|
|
1158
|
-
result: ok({ entries, threshold: input.threshold, rotated: false })
|
|
1432
|
+
result: ok({ entries, threshold: input.threshold, rotated: false, humanHint: `${entries}/${input.threshold} entries \u2014 rotation needed (use --apply)` })
|
|
1159
1433
|
};
|
|
1160
1434
|
}
|
|
1161
1435
|
const newestYear = matches[matches.length - 1][1];
|
|
@@ -1172,17 +1446,88 @@ Chronological action log. Newest entries last. Skill writes append entries; lint
|
|
|
1172
1446
|
|
|
1173
1447
|
- Previous log moved to ${rotatedName}
|
|
1174
1448
|
`;
|
|
1175
|
-
await
|
|
1449
|
+
await writeFile5(logPath, fresh, "utf8");
|
|
1176
1450
|
} catch (e) {
|
|
1177
1451
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
|
|
1178
1452
|
}
|
|
1179
|
-
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: true, rotated_to: rotatedName }) };
|
|
1453
|
+
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: true, rotated_to: rotatedName, humanHint: `rotated ${entries} entries to ${rotatedName}` }) };
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// src/commands/topic-map-check.ts
|
|
1457
|
+
var DEFAULT_THRESHOLD = 200;
|
|
1458
|
+
async function runTopicMapCheck(input) {
|
|
1459
|
+
const threshold = input.threshold ?? DEFAULT_THRESHOLD;
|
|
1460
|
+
const scan = await scanVault(input.vault);
|
|
1461
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1462
|
+
const page_count = scan.data.typedKnowledge.length;
|
|
1463
|
+
const recommended = page_count >= threshold;
|
|
1464
|
+
return {
|
|
1465
|
+
exitCode: ExitCode.OK,
|
|
1466
|
+
result: ok({
|
|
1467
|
+
recommended,
|
|
1468
|
+
page_count,
|
|
1469
|
+
threshold,
|
|
1470
|
+
humanHint: recommended ? `topic map recommended (${page_count} pages >= ${threshold} threshold)` : `topic map not needed (${page_count} pages < ${threshold} threshold)`
|
|
1471
|
+
})
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// src/commands/index-link-format.ts
|
|
1476
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
1477
|
+
import { join as join12 } from "path";
|
|
1478
|
+
var MD_LINK_RE = /\[[^\[\]]+\]\([^)]+\.md\)/;
|
|
1479
|
+
async function runIndexLinkFormat(input) {
|
|
1480
|
+
let text = "";
|
|
1481
|
+
try {
|
|
1482
|
+
text = await readFile11(join12(input.vault, "index.md"), "utf8");
|
|
1483
|
+
} catch {
|
|
1484
|
+
}
|
|
1485
|
+
const markdown_links = [];
|
|
1486
|
+
for (const [i, line] of text.split("\n").entries()) {
|
|
1487
|
+
if (MD_LINK_RE.test(line)) markdown_links.push({ line: i + 1, text: line.trim() });
|
|
1488
|
+
}
|
|
1489
|
+
const humanHint = markdown_links.length === 0 ? "all index links use wikilink format" : `markdown links found: ${markdown_links.length}
|
|
1490
|
+
${markdown_links.map((l) => ` line ${l.line}: ${l.text}`).join("\n")}`;
|
|
1491
|
+
return { exitCode: ExitCode.OK, result: ok({ markdown_links, humanHint }) };
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// src/commands/dedup.ts
|
|
1495
|
+
async function runDedup(input) {
|
|
1496
|
+
const scan = await scanVault(input.vault);
|
|
1497
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1498
|
+
const hashMap = /* @__PURE__ */ new Map();
|
|
1499
|
+
let totalFiles = 0;
|
|
1500
|
+
for (const raw of scan.data.raw) {
|
|
1501
|
+
const fm = extractFrontmatter(await readPage(raw));
|
|
1502
|
+
if (!fm.ok) continue;
|
|
1503
|
+
const sha = typeof fm.data.sha256 === "string" ? fm.data.sha256 : null;
|
|
1504
|
+
if (!sha || sha.length !== 64) continue;
|
|
1505
|
+
totalFiles++;
|
|
1506
|
+
const existing = hashMap.get(sha);
|
|
1507
|
+
if (existing) existing.push(raw.relPath);
|
|
1508
|
+
else hashMap.set(sha, [raw.relPath]);
|
|
1509
|
+
}
|
|
1510
|
+
const duplicates = [...hashMap.entries()].filter(([, files]) => files.length > 1).map(([sha256, files]) => ({ sha256, files }));
|
|
1511
|
+
const exitCode = duplicates.length > 0 ? ExitCode.RAW_DEDUP_DETECTED : ExitCode.OK;
|
|
1512
|
+
const hintLines = [`scanned: ${totalFiles} raw files`];
|
|
1513
|
+
if (duplicates.length > 0) {
|
|
1514
|
+
hintLines.push(`duplicates: ${duplicates.length}`);
|
|
1515
|
+
for (const d of duplicates) hintLines.push(` ${d.sha256.slice(0, 12)}... \u2192 ${d.files.join(", ")}`);
|
|
1516
|
+
} else {
|
|
1517
|
+
hintLines.push("0 duplicates");
|
|
1518
|
+
}
|
|
1519
|
+
return {
|
|
1520
|
+
exitCode,
|
|
1521
|
+
result: ok({ scanned: totalFiles, duplicates, humanHint: hintLines.join("\n") })
|
|
1522
|
+
};
|
|
1180
1523
|
}
|
|
1181
1524
|
|
|
1182
1525
|
// src/commands/lint.ts
|
|
1183
|
-
var
|
|
1184
|
-
var
|
|
1185
|
-
var
|
|
1526
|
+
var STRUCT_MIN_BODY_LINES = 60;
|
|
1527
|
+
var STRUCT_MIN_SECTIONS = 3;
|
|
1528
|
+
var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_dedup", "tag_not_in_taxonomy"];
|
|
1529
|
+
var WARNING_ORDER = ["index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "contested", "orphans", "legacy_citation_style", "orphaned_citations"];
|
|
1530
|
+
var INFO_ORDER = ["bridges", "low_confidence_single_source", "page_structure", "topic_map_recommended"];
|
|
1186
1531
|
async function runLint(input) {
|
|
1187
1532
|
const buckets = {};
|
|
1188
1533
|
const links = await runLinks({ vault: input.vault });
|
|
@@ -1202,6 +1547,10 @@ async function runLint(input) {
|
|
|
1202
1547
|
ghost_entries: idx.result.data.ghost_entries
|
|
1203
1548
|
}];
|
|
1204
1549
|
}
|
|
1550
|
+
const linkFmt = await runIndexLinkFormat({ vault: input.vault });
|
|
1551
|
+
if (linkFmt.result.ok && linkFmt.result.data.markdown_links.length > 0) {
|
|
1552
|
+
buckets.index_link_format = linkFmt.result.data.markdown_links;
|
|
1553
|
+
}
|
|
1205
1554
|
const stale = await runStale({ vault: input.vault, days: input.days });
|
|
1206
1555
|
if (stale.result.ok && stale.result.data.stale.length > 0) buckets.stale_page = stale.result.data.stale;
|
|
1207
1556
|
const pagesize = await runPagesize({ vault: input.vault, lines: input.lines });
|
|
@@ -1215,6 +1564,42 @@ async function runLint(input) {
|
|
|
1215
1564
|
if (orphans.result.data.orphans.length > 0) buckets.orphans = orphans.result.data.orphans;
|
|
1216
1565
|
if (orphans.result.data.bridges.length > 0) buckets.bridges = orphans.result.data.bridges;
|
|
1217
1566
|
}
|
|
1567
|
+
const topicMap = await runTopicMapCheck({ vault: input.vault });
|
|
1568
|
+
if (topicMap.result.ok && topicMap.result.data.recommended) {
|
|
1569
|
+
buckets.topic_map_recommended = [{ page_count: topicMap.result.data.page_count, threshold: topicMap.result.data.threshold }];
|
|
1570
|
+
}
|
|
1571
|
+
const dedup = await runDedup({ vault: input.vault });
|
|
1572
|
+
if (dedup.result.ok && dedup.result.data.duplicates.length > 0) buckets.raw_dedup = dedup.result.data.duplicates;
|
|
1573
|
+
const scan = await scanVault(input.vault);
|
|
1574
|
+
if (scan.ok) {
|
|
1575
|
+
const legacyPages = [];
|
|
1576
|
+
const orphanedPages = [];
|
|
1577
|
+
const structFlags = [];
|
|
1578
|
+
for (const page of scan.data.typedKnowledge) {
|
|
1579
|
+
const text = await readPage(page);
|
|
1580
|
+
const split = splitFrontmatter(text);
|
|
1581
|
+
if (!split.ok) continue;
|
|
1582
|
+
const body = split.data.body;
|
|
1583
|
+
if (isLegacyCitationStyle(body)) legacyPages.push(page.relPath);
|
|
1584
|
+
if (hasOrphanedCitations(body)) orphanedPages.push(page.relPath);
|
|
1585
|
+
const bodyLines = body.split("\n").filter((l) => l.trim().length > 0).length;
|
|
1586
|
+
if (bodyLines < STRUCT_MIN_BODY_LINES) {
|
|
1587
|
+
const hasOverview = /^## Overview/m.test(body);
|
|
1588
|
+
const hasRelated = /^## (Related|Relationships)/m.test(body);
|
|
1589
|
+
const sectionCount = (body.match(/^## /gm) ?? []).length;
|
|
1590
|
+
if (!hasOverview || !hasRelated || sectionCount < STRUCT_MIN_SECTIONS) {
|
|
1591
|
+
const reasons = [];
|
|
1592
|
+
if (!hasOverview) reasons.push("no Overview");
|
|
1593
|
+
if (!hasRelated) reasons.push("no Related or Relationships");
|
|
1594
|
+
if (sectionCount < STRUCT_MIN_SECTIONS) reasons.push(`only ${sectionCount} sections`);
|
|
1595
|
+
structFlags.push(`${page.relPath}: ${bodyLines} lines, ${reasons.join(", ")}`);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
if (legacyPages.length > 0) buckets.legacy_citation_style = legacyPages;
|
|
1600
|
+
if (orphanedPages.length > 0) buckets.orphaned_citations = orphanedPages;
|
|
1601
|
+
if (structFlags.length > 0) buckets.page_structure = structFlags;
|
|
1602
|
+
}
|
|
1218
1603
|
const errorOut = ERROR_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
1219
1604
|
const warningOut = WARNING_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
1220
1605
|
const infoOut = INFO_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
@@ -1226,19 +1611,629 @@ async function runLint(input) {
|
|
|
1226
1611
|
let exitCode = ExitCode.OK;
|
|
1227
1612
|
if (summary.errors > 0) exitCode = ExitCode.LINT_HAS_ERRORS;
|
|
1228
1613
|
else if (summary.warnings > 0 || summary.info > 0) exitCode = ExitCode.LINT_HAS_WARNINGS;
|
|
1614
|
+
const hintLines = [];
|
|
1615
|
+
if (summary.errors > 0) hintLines.push(`errors: ${summary.errors}`);
|
|
1616
|
+
if (summary.warnings > 0) hintLines.push(`warnings: ${summary.warnings}`);
|
|
1617
|
+
if (summary.info > 0) hintLines.push(`info: ${summary.info}`);
|
|
1618
|
+
const allBuckets = [...errorOut, ...warningOut, ...infoOut];
|
|
1619
|
+
for (const b of allBuckets) {
|
|
1620
|
+
hintLines.push(` ${b.kind}: ${b.items.length}`);
|
|
1621
|
+
}
|
|
1622
|
+
if (hintLines.length === 0) hintLines.push("0 errors, 0 warnings, 0 info");
|
|
1229
1623
|
return {
|
|
1230
1624
|
exitCode,
|
|
1231
1625
|
result: ok({
|
|
1232
1626
|
vault: { path: input.vault, source: input.source ?? "resolved" },
|
|
1233
1627
|
summary,
|
|
1234
|
-
by_severity: { error: errorOut, warning: warningOut, info: infoOut }
|
|
1628
|
+
by_severity: { error: errorOut, warning: warningOut, info: infoOut },
|
|
1629
|
+
humanHint: hintLines.join("\n")
|
|
1630
|
+
})
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
// src/commands/config.ts
|
|
1635
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
1636
|
+
import { existsSync } from "fs";
|
|
1637
|
+
import { join as join13 } from "path";
|
|
1638
|
+
function validateKey(key) {
|
|
1639
|
+
return CONFIG_KEYS.includes(key);
|
|
1640
|
+
}
|
|
1641
|
+
function configPath(home) {
|
|
1642
|
+
return join13(home, ".skillwiki", ".env");
|
|
1643
|
+
}
|
|
1644
|
+
async function runConfigGet(input) {
|
|
1645
|
+
if (!validateKey(input.key)) {
|
|
1646
|
+
return { exitCode: ExitCode.INVALID_CONFIG_KEY, result: err("INVALID_CONFIG_KEY", { key: input.key }) };
|
|
1647
|
+
}
|
|
1648
|
+
const map = await parseDotenvFile(configPath(input.home));
|
|
1649
|
+
const value = map[input.key] ?? "";
|
|
1650
|
+
return { exitCode: ExitCode.OK, result: ok({ key: input.key, value, humanHint: value }) };
|
|
1651
|
+
}
|
|
1652
|
+
async function runConfigSet(input) {
|
|
1653
|
+
if (!validateKey(input.key)) {
|
|
1654
|
+
return { exitCode: ExitCode.INVALID_CONFIG_KEY, result: err("INVALID_CONFIG_KEY", { key: input.key }) };
|
|
1655
|
+
}
|
|
1656
|
+
const filePath = configPath(input.home);
|
|
1657
|
+
try {
|
|
1658
|
+
let originalContent;
|
|
1659
|
+
try {
|
|
1660
|
+
originalContent = await readFile12(filePath, "utf8");
|
|
1661
|
+
} catch {
|
|
1662
|
+
}
|
|
1663
|
+
const existing = originalContent !== void 0 ? parseDotenvText(originalContent) : {};
|
|
1664
|
+
const merged = { ...existing, [input.key]: input.value };
|
|
1665
|
+
await writeDotenv(filePath, merged, originalContent);
|
|
1666
|
+
return { exitCode: ExitCode.OK, result: ok({ key: input.key, value: input.value, written: true, humanHint: `${input.key}=${input.value}` }) };
|
|
1667
|
+
} catch (e) {
|
|
1668
|
+
return { exitCode: ExitCode.CONFIG_WRITE_FAILED, result: err("CONFIG_WRITE_FAILED", { key: input.key, error: String(e) }) };
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
async function runConfigList(input) {
|
|
1672
|
+
const map = await parseDotenvFile(configPath(input.home));
|
|
1673
|
+
const entries = Object.entries(map).map(([key, value]) => ({ key, value: value ?? "" }));
|
|
1674
|
+
return { exitCode: ExitCode.OK, result: ok({ entries, humanHint: entries.map((e) => `${e.key}=${e.value}`).join("\n") }) };
|
|
1675
|
+
}
|
|
1676
|
+
async function runConfigPath(input) {
|
|
1677
|
+
const filePath = configPath(input.home);
|
|
1678
|
+
return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync(filePath), humanHint: filePath }) };
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// src/commands/doctor.ts
|
|
1682
|
+
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
|
|
1683
|
+
import { join as join15 } from "path";
|
|
1684
|
+
import { execSync } from "child_process";
|
|
1685
|
+
|
|
1686
|
+
// src/utils/auto-update.ts
|
|
1687
|
+
import { readFileSync, writeFileSync, existsSync as existsSync2, mkdirSync } from "fs";
|
|
1688
|
+
import { join as join14, dirname as dirname6 } from "path";
|
|
1689
|
+
import { spawn } from "child_process";
|
|
1690
|
+
|
|
1691
|
+
// src/utils/update-consts.ts
|
|
1692
|
+
var CACHE_FILENAME = ".update-cache.json";
|
|
1693
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1694
|
+
var ENV_DISABLE_KEY = "NO_UPDATE_NOTIFIER";
|
|
1695
|
+
var CLI_DISABLE_FLAG = "--no-update-notifier";
|
|
1696
|
+
|
|
1697
|
+
// src/utils/auto-update.ts
|
|
1698
|
+
function cachePath(home) {
|
|
1699
|
+
return join14(home, ".skillwiki", CACHE_FILENAME);
|
|
1700
|
+
}
|
|
1701
|
+
function readCacheRaw(home) {
|
|
1702
|
+
try {
|
|
1703
|
+
const raw = readFileSync(cachePath(home), "utf8");
|
|
1704
|
+
return JSON.parse(raw);
|
|
1705
|
+
} catch {
|
|
1706
|
+
return null;
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
function readCache(home) {
|
|
1710
|
+
const cache = readCacheRaw(home);
|
|
1711
|
+
if (!cache) return { cache: null, hasUpdate: false, isStale: true };
|
|
1712
|
+
const isStale = Date.now() - cache.lastCheck >= CHECK_INTERVAL_MS;
|
|
1713
|
+
const hasUpdate = !!cache.latestVersion && semverGt(cache.latestVersion, cache.currentVersion);
|
|
1714
|
+
return { cache, hasUpdate, isStale };
|
|
1715
|
+
}
|
|
1716
|
+
function writeCache(home, cache) {
|
|
1717
|
+
const p = cachePath(home);
|
|
1718
|
+
mkdirSync(dirname6(p), { recursive: true });
|
|
1719
|
+
writeFileSync(p, JSON.stringify(cache, null, 2));
|
|
1720
|
+
}
|
|
1721
|
+
function latestFromCache(home, currentVersion) {
|
|
1722
|
+
const { cache } = readCache(home);
|
|
1723
|
+
if (!cache || !cache.latestVersion) return { hasUpdate: false, latest: null };
|
|
1724
|
+
return {
|
|
1725
|
+
hasUpdate: semverGt(cache.latestVersion, currentVersion),
|
|
1726
|
+
latest: cache.latestVersion
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
function isDisabled() {
|
|
1730
|
+
return !!(process.env[ENV_DISABLE_KEY] || process.env.NODE_ENV === "test" || process.argv.includes(CLI_DISABLE_FLAG));
|
|
1731
|
+
}
|
|
1732
|
+
function triggerAutoUpdate(home, currentVersion) {
|
|
1733
|
+
if (isDisabled()) return;
|
|
1734
|
+
const { isStale } = readCache(home);
|
|
1735
|
+
if (!isStale) return;
|
|
1736
|
+
const bgScript = new URL("../auto-update-bg.js", import.meta.url).pathname;
|
|
1737
|
+
if (!existsSync2(bgScript)) return;
|
|
1738
|
+
const child = spawn(process.execPath, [bgScript, home, currentVersion], {
|
|
1739
|
+
detached: true,
|
|
1740
|
+
stdio: "ignore"
|
|
1741
|
+
});
|
|
1742
|
+
child.on("error", () => {
|
|
1743
|
+
});
|
|
1744
|
+
child.unref();
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// src/commands/doctor.ts
|
|
1748
|
+
function check(status, id, label, detail) {
|
|
1749
|
+
return { id, label, status, detail };
|
|
1750
|
+
}
|
|
1751
|
+
function checkNodeVersion() {
|
|
1752
|
+
const major = parseInt(process.version.slice(1).split(".")[0], 10);
|
|
1753
|
+
if (major >= 20) {
|
|
1754
|
+
return check("pass", "node_version", "Node.js version", `v${major} >= 20`);
|
|
1755
|
+
}
|
|
1756
|
+
return check("error", "node_version", "Node.js version", `Node.js v${major} is below minimum v20`);
|
|
1757
|
+
}
|
|
1758
|
+
function checkCliOnPath(argv) {
|
|
1759
|
+
if (argv.length >= 2 && argv[1].endsWith("cli.js")) {
|
|
1760
|
+
return check("warn", "cli_on_path", "skillwiki on PATH", "Running via node cli.js (dev mode) \u2014 PATH check skipped");
|
|
1761
|
+
}
|
|
1762
|
+
if (argv.length >= 2 && argv[1] === "skillwiki") {
|
|
1763
|
+
return check("pass", "cli_on_path", "skillwiki on PATH", "Running as skillwiki \u2014 already on PATH");
|
|
1764
|
+
}
|
|
1765
|
+
try {
|
|
1766
|
+
execSync("which skillwiki 2>/dev/null", { encoding: "utf8" }).trim();
|
|
1767
|
+
return check("pass", "cli_on_path", "skillwiki on PATH", "skillwiki found on PATH");
|
|
1768
|
+
} catch {
|
|
1769
|
+
return check("warn", "cli_on_path", "skillwiki on PATH", "skillwiki not found on PATH");
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
async function checkConfigFile(home) {
|
|
1773
|
+
const cfgPath = configPath(home);
|
|
1774
|
+
if (!existsSync3(cfgPath)) {
|
|
1775
|
+
return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
|
|
1776
|
+
}
|
|
1777
|
+
try {
|
|
1778
|
+
const map = await parseDotenvFile(cfgPath);
|
|
1779
|
+
const keys = Object.keys(map);
|
|
1780
|
+
return check("pass", "config_file", "Config file exists", `Found with keys: ${keys.length > 0 ? keys.join(", ") : "(none set)"}`);
|
|
1781
|
+
} catch (e) {
|
|
1782
|
+
return check("warn", "config_file", "Config file exists", `Failed to parse ${cfgPath}: ${String(e)}`);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
function checkWikiPathExists(resolvedPath) {
|
|
1786
|
+
if (resolvedPath === void 0) {
|
|
1787
|
+
return check("error", "wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
1788
|
+
}
|
|
1789
|
+
if (existsSync3(resolvedPath) && statSync(resolvedPath).isDirectory()) {
|
|
1790
|
+
return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
|
|
1791
|
+
}
|
|
1792
|
+
return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
|
|
1793
|
+
}
|
|
1794
|
+
function checkVaultStructure(resolvedPath) {
|
|
1795
|
+
if (resolvedPath === void 0) {
|
|
1796
|
+
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
1797
|
+
}
|
|
1798
|
+
if (!existsSync3(resolvedPath)) {
|
|
1799
|
+
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
|
|
1800
|
+
}
|
|
1801
|
+
const missing = [];
|
|
1802
|
+
if (!existsSync3(join15(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
|
|
1803
|
+
for (const dir of ["raw", "entities", "concepts", "meta"]) {
|
|
1804
|
+
if (!existsSync3(join15(resolvedPath, dir))) missing.push(dir + "/");
|
|
1805
|
+
}
|
|
1806
|
+
if (missing.length === 0) {
|
|
1807
|
+
return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
|
|
1808
|
+
}
|
|
1809
|
+
return check("error", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")}`);
|
|
1810
|
+
}
|
|
1811
|
+
function checkSkillsInstalled(home) {
|
|
1812
|
+
const skillsDir = join15(home, ".claude", "skills");
|
|
1813
|
+
if (!existsSync3(skillsDir)) {
|
|
1814
|
+
return check("warn", "skills_installed", "Skills installed", `${skillsDir} not found`);
|
|
1815
|
+
}
|
|
1816
|
+
const found = findSkillMd(skillsDir);
|
|
1817
|
+
if (found.length > 0) {
|
|
1818
|
+
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found`);
|
|
1819
|
+
}
|
|
1820
|
+
return check("warn", "skills_installed", "Skills installed", "No SKILL.md files found in ~/.claude/skills/");
|
|
1821
|
+
}
|
|
1822
|
+
function checkNpmUpdate(home, currentVersion) {
|
|
1823
|
+
const { hasUpdate, latest } = latestFromCache(home, currentVersion);
|
|
1824
|
+
if (!latest) {
|
|
1825
|
+
return check("pass", "npm_update", "npm CLI version", `v${currentVersion} (no cache yet)`);
|
|
1826
|
+
}
|
|
1827
|
+
if (hasUpdate) {
|
|
1828
|
+
return check("warn", "npm_update", "npm CLI version", `v${currentVersion} \u2014 update available: v${latest}. Run \`skillwiki update\`.`);
|
|
1829
|
+
}
|
|
1830
|
+
return check("pass", "npm_update", "npm CLI version", `v${currentVersion} (latest: v${latest})`);
|
|
1831
|
+
}
|
|
1832
|
+
function checkPluginVersionDrift(home, currentVersion) {
|
|
1833
|
+
const pluginJsonPath = join15(home, ".claude", "plugins", "cache", "llm-wiki", "plugin.json");
|
|
1834
|
+
if (!existsSync3(pluginJsonPath)) {
|
|
1835
|
+
return check("pass", "plugin_version_drift", "Plugin/CLI version", "Plugin cache not found \u2014 plugin not installed");
|
|
1836
|
+
}
|
|
1837
|
+
try {
|
|
1838
|
+
const content = readFileSync2(pluginJsonPath, { encoding: "utf8" });
|
|
1839
|
+
const pluginData = JSON.parse(content);
|
|
1840
|
+
const pluginVersion = pluginData.version;
|
|
1841
|
+
if (!pluginVersion) {
|
|
1842
|
+
return check("pass", "plugin_version_drift", "Plugin/CLI version", "Plugin version not found in cache");
|
|
1843
|
+
}
|
|
1844
|
+
if (pluginVersion === currentVersion) {
|
|
1845
|
+
return check("pass", "plugin_version_drift", "Plugin/CLI version", `Both at v${currentVersion}`);
|
|
1846
|
+
}
|
|
1847
|
+
const updateCmd = semverGt(pluginVersion, currentVersion) ? "npm install -g skillwiki@beta" : "claude plugin update skillwiki@llm-wiki";
|
|
1848
|
+
return check(
|
|
1849
|
+
"warn",
|
|
1850
|
+
"plugin_version_drift",
|
|
1851
|
+
"Plugin/CLI version",
|
|
1852
|
+
`Plugin v${pluginVersion} \u2260 CLI v${currentVersion} \u2014 run \`${updateCmd}\``
|
|
1853
|
+
);
|
|
1854
|
+
} catch {
|
|
1855
|
+
return check("pass", "plugin_version_drift", "Plugin/CLI version", "Could not read plugin cache");
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
function findSkillMd(dir) {
|
|
1859
|
+
const results = [];
|
|
1860
|
+
let entries;
|
|
1861
|
+
try {
|
|
1862
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
1863
|
+
} catch {
|
|
1864
|
+
return results;
|
|
1865
|
+
}
|
|
1866
|
+
for (const entry of entries) {
|
|
1867
|
+
if (entry.isFile() && entry.name === "SKILL.md") {
|
|
1868
|
+
results.push(join15(dir, entry.name));
|
|
1869
|
+
} else if (entry.isDirectory()) {
|
|
1870
|
+
results.push(...findSkillMd(join15(dir, entry.name)));
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
return results;
|
|
1874
|
+
}
|
|
1875
|
+
async function runDoctor(input) {
|
|
1876
|
+
const checks = [];
|
|
1877
|
+
checks.push(checkNodeVersion());
|
|
1878
|
+
checks.push(checkCliOnPath(input.argv));
|
|
1879
|
+
checks.push(await checkConfigFile(input.home));
|
|
1880
|
+
const resolved = await resolveRuntimePath({ flag: void 0, envValue: input.envValue, home: input.home });
|
|
1881
|
+
if (resolved.ok) {
|
|
1882
|
+
checks.push(check("pass", "wiki_path_set", "WIKI_PATH configured", `Resolved via ${resolved.data.source}: ${resolved.data.path}`));
|
|
1883
|
+
} else {
|
|
1884
|
+
checks.push(check("error", "wiki_path_set", "WIKI_PATH configured", "No vault configured. Run `skillwiki init` or pass --vault."));
|
|
1885
|
+
}
|
|
1886
|
+
const resolvedPath = resolved.ok ? resolved.data.path : void 0;
|
|
1887
|
+
checks.push(checkWikiPathExists(resolvedPath));
|
|
1888
|
+
checks.push(checkVaultStructure(resolvedPath));
|
|
1889
|
+
checks.push(checkSkillsInstalled(input.home));
|
|
1890
|
+
checks.push(checkNpmUpdate(input.home, input.currentVersion));
|
|
1891
|
+
checks.push(checkPluginVersionDrift(input.home, input.currentVersion));
|
|
1892
|
+
const summary = {
|
|
1893
|
+
pass: checks.filter((c) => c.status === "pass").length,
|
|
1894
|
+
warn: checks.filter((c) => c.status === "warn").length,
|
|
1895
|
+
error: checks.filter((c) => c.status === "error").length
|
|
1896
|
+
};
|
|
1897
|
+
const exitCode = summary.error > 0 ? ExitCode.DOCTOR_HAS_ERRORS : summary.warn > 0 ? ExitCode.DOCTOR_HAS_WARNINGS : ExitCode.OK;
|
|
1898
|
+
const statusIcon = { pass: "\u2713", warn: "\u26A0", error: "\u2717" };
|
|
1899
|
+
const lines = checks.map((c) => {
|
|
1900
|
+
const icon = statusIcon[c.status];
|
|
1901
|
+
const padded = c.label.padEnd(24);
|
|
1902
|
+
return ` ${icon} ${padded} ${c.detail}`;
|
|
1903
|
+
});
|
|
1904
|
+
lines.push("");
|
|
1905
|
+
lines.push(`${summary.pass} pass \xB7 ${summary.warn} warn \xB7 ${summary.error} error`);
|
|
1906
|
+
const humanHint = lines.join("\n");
|
|
1907
|
+
return { exitCode, result: ok({ checks, summary, humanHint }) };
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
// src/commands/archive.ts
|
|
1911
|
+
import { rename as rename3, mkdir as mkdir5, readFile as readFile13, writeFile as writeFile6 } from "fs/promises";
|
|
1912
|
+
import { join as join16, dirname as dirname7 } from "path";
|
|
1913
|
+
async function runArchive(input) {
|
|
1914
|
+
const scan = await scanVault(input.vault);
|
|
1915
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1916
|
+
let relPath;
|
|
1917
|
+
if (input.page.includes("/")) {
|
|
1918
|
+
relPath = scan.data.typedKnowledge.find((p) => p.relPath === input.page)?.relPath;
|
|
1919
|
+
} else {
|
|
1920
|
+
relPath = scan.data.typedKnowledge.find((p) => p.relPath.replace(/\.md$/, "").split("/").pop() === input.page)?.relPath;
|
|
1921
|
+
}
|
|
1922
|
+
if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
|
|
1923
|
+
if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
|
|
1924
|
+
const archivePath = join16("_archive", relPath);
|
|
1925
|
+
await mkdir5(dirname7(join16(input.vault, archivePath)), { recursive: true });
|
|
1926
|
+
let indexUpdated = false;
|
|
1927
|
+
const indexPath = join16(input.vault, "index.md");
|
|
1928
|
+
try {
|
|
1929
|
+
const idx = await readFile13(indexPath, "utf8");
|
|
1930
|
+
const slug = relPath.replace(/\.md$/, "").split("/").pop();
|
|
1931
|
+
const originalLines = idx.split("\n");
|
|
1932
|
+
const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
|
|
1933
|
+
if (filtered.length !== originalLines.length) {
|
|
1934
|
+
await writeFile6(indexPath, filtered.join("\n"), "utf8");
|
|
1935
|
+
indexUpdated = true;
|
|
1936
|
+
}
|
|
1937
|
+
} catch (e) {
|
|
1938
|
+
if (e?.code !== "ENOENT") throw e;
|
|
1939
|
+
}
|
|
1940
|
+
await rename3(join16(input.vault, relPath), join16(input.vault, archivePath));
|
|
1941
|
+
return { exitCode: ExitCode.OK, result: ok({ archived_from: relPath, archived_to: archivePath, index_updated: indexUpdated, humanHint: `${relPath} -> ${archivePath}${indexUpdated ? " (index updated)" : ""}` }) };
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
// src/commands/drift.ts
|
|
1945
|
+
import { createHash as createHash2 } from "crypto";
|
|
1946
|
+
|
|
1947
|
+
// src/utils/fetch.ts
|
|
1948
|
+
async function controlledFetch(url, opts) {
|
|
1949
|
+
let current = url;
|
|
1950
|
+
for (let hop = 0; hop <= opts.maxRedirects; hop++) {
|
|
1951
|
+
const guard = runFetchGuardSync({ url: current });
|
|
1952
|
+
if (!guard.result.ok) return guard.result;
|
|
1953
|
+
const ctrl = new AbortController();
|
|
1954
|
+
const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs);
|
|
1955
|
+
let res;
|
|
1956
|
+
try {
|
|
1957
|
+
res = await fetch(current, { redirect: "manual", signal: ctrl.signal });
|
|
1958
|
+
} catch (e) {
|
|
1959
|
+
clearTimeout(timer);
|
|
1960
|
+
if (e?.name === "AbortError") return err("FETCH_TIMEOUT", { url: current });
|
|
1961
|
+
return err("FETCH_FAILED", { message: String(e) });
|
|
1962
|
+
}
|
|
1963
|
+
clearTimeout(timer);
|
|
1964
|
+
if (res.status >= 300 && res.status < 400) {
|
|
1965
|
+
const loc = res.headers.get("location");
|
|
1966
|
+
if (!loc) return err("FETCH_FAILED", { reason: "redirect without Location" });
|
|
1967
|
+
current = new URL(loc, current).toString();
|
|
1968
|
+
continue;
|
|
1969
|
+
}
|
|
1970
|
+
const declared = Number(res.headers.get("content-length") ?? "0");
|
|
1971
|
+
if (declared > opts.maxBytes) return err("FETCH_TOO_LARGE", { declared, limit: opts.maxBytes });
|
|
1972
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
1973
|
+
if (buf.byteLength > opts.maxBytes) return err("FETCH_TOO_LARGE", { actual: buf.byteLength, limit: opts.maxBytes });
|
|
1974
|
+
return ok({ url: current, status: res.status, body: new TextDecoder().decode(buf), bytes: buf.byteLength });
|
|
1975
|
+
}
|
|
1976
|
+
return err("FETCH_FAILED", { reason: "too many redirects", limit: opts.maxRedirects });
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
// src/commands/drift.ts
|
|
1980
|
+
var FETCH_OPTS = { timeoutMs: 1e4, maxBytes: 5e6, maxRedirects: 5 };
|
|
1981
|
+
async function runDrift(input) {
|
|
1982
|
+
const doFetch = input.fetchFn ?? controlledFetch;
|
|
1983
|
+
const scan = await scanVault(input.vault);
|
|
1984
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1985
|
+
const results = [];
|
|
1986
|
+
for (const raw of scan.data.raw) {
|
|
1987
|
+
const fm = extractFrontmatter(await readPage(raw));
|
|
1988
|
+
if (!fm.ok) continue;
|
|
1989
|
+
const sourceUrl = typeof fm.data.source_url === "string" ? fm.data.source_url : null;
|
|
1990
|
+
const storedHash = typeof fm.data.sha256 === "string" ? fm.data.sha256 : null;
|
|
1991
|
+
if (!sourceUrl || !storedHash) continue;
|
|
1992
|
+
const resp = await doFetch(sourceUrl, FETCH_OPTS);
|
|
1993
|
+
if (!resp.ok) {
|
|
1994
|
+
results.push({
|
|
1995
|
+
raw_path: raw.relPath,
|
|
1996
|
+
source_url: sourceUrl,
|
|
1997
|
+
stored_sha256: storedHash,
|
|
1998
|
+
current_sha256: null,
|
|
1999
|
+
status: "fetch_failed",
|
|
2000
|
+
fetch_error: resp.error
|
|
2001
|
+
});
|
|
2002
|
+
continue;
|
|
2003
|
+
}
|
|
2004
|
+
const currentHash = createHash2("sha256").update(Buffer.from(resp.data.body, "utf8")).digest("hex");
|
|
2005
|
+
const drifted2 = currentHash !== storedHash;
|
|
2006
|
+
results.push({
|
|
2007
|
+
raw_path: raw.relPath,
|
|
2008
|
+
source_url: sourceUrl,
|
|
2009
|
+
stored_sha256: storedHash,
|
|
2010
|
+
current_sha256: currentHash,
|
|
2011
|
+
status: drifted2 ? "drifted" : "unchanged"
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
2014
|
+
const drifted = results.filter((r) => r.status === "drifted");
|
|
2015
|
+
const fetchFailed = results.filter((r) => r.status === "fetch_failed");
|
|
2016
|
+
const unchanged = results.filter((r) => r.status === "unchanged").length;
|
|
2017
|
+
const exitCode = drifted.length > 0 ? ExitCode.DRIFT_DETECTED : ExitCode.OK;
|
|
2018
|
+
const hintLines = [`scanned: ${results.length}, unchanged: ${unchanged}`];
|
|
2019
|
+
if (drifted.length > 0) hintLines.push(`drifted: ${drifted.length}`, ...drifted.map((d) => ` ${d.raw_path}`));
|
|
2020
|
+
if (fetchFailed.length > 0) hintLines.push(`fetch_failed: ${fetchFailed.length}`, ...fetchFailed.map((f) => ` ${f.raw_path}: ${f.fetch_error}`));
|
|
2021
|
+
return {
|
|
2022
|
+
exitCode,
|
|
2023
|
+
result: ok({ scanned: results.length, drifted, fetch_failed: fetchFailed, unchanged, humanHint: hintLines.join("\n") })
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
// src/commands/migrate-citations.ts
|
|
2028
|
+
import { writeFile as writeFile7 } from "fs/promises";
|
|
2029
|
+
var MARKER_RE2 = /\^\[(raw\/[^\]]+)\]/g;
|
|
2030
|
+
function moveMarkersToParagraphEnd(body) {
|
|
2031
|
+
const lines = body.split("\n");
|
|
2032
|
+
const result = [];
|
|
2033
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2034
|
+
const line = lines[i];
|
|
2035
|
+
if (line.trimStart().startsWith("```")) {
|
|
2036
|
+
result.push(line);
|
|
2037
|
+
if (!line.trimEnd().endsWith("```") || line.trim() === "```") {
|
|
2038
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
2039
|
+
result.push(lines[j]);
|
|
2040
|
+
if (lines[j].trimStart().startsWith("```")) {
|
|
2041
|
+
i = j;
|
|
2042
|
+
break;
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
continue;
|
|
2047
|
+
}
|
|
2048
|
+
if (/^## Sources\b/.test(line.trim())) {
|
|
2049
|
+
result.push(line);
|
|
2050
|
+
continue;
|
|
2051
|
+
}
|
|
2052
|
+
const markers = [...line.matchAll(MARKER_RE2)];
|
|
2053
|
+
if (markers.length === 0) {
|
|
2054
|
+
result.push(line);
|
|
2055
|
+
continue;
|
|
2056
|
+
}
|
|
2057
|
+
const proseOnly = line.replace(MARKER_RE2, "").trim();
|
|
2058
|
+
if (proseOnly.length === 0) {
|
|
2059
|
+
const markerStr = " " + markers.map((m) => m[0]).join(" ");
|
|
2060
|
+
let merged = false;
|
|
2061
|
+
for (let k = result.length - 1; k >= 0; k--) {
|
|
2062
|
+
if (result[k].trim().length > 0) {
|
|
2063
|
+
result[k] = result[k].trimEnd() + markerStr;
|
|
2064
|
+
merged = true;
|
|
2065
|
+
break;
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
if (!merged) result.push(line);
|
|
2069
|
+
continue;
|
|
2070
|
+
}
|
|
2071
|
+
const lastMarkerIdx = line.lastIndexOf("^[raw/");
|
|
2072
|
+
const afterLast = line.slice(lastMarkerIdx).replace(MARKER_RE2, "").trim();
|
|
2073
|
+
const firstMarkerIdx = line.indexOf("^[raw/");
|
|
2074
|
+
const beforeFirst = line.slice(0, firstMarkerIdx).trim();
|
|
2075
|
+
const alreadyAtEnd = afterLast.length === 0 && (beforeFirst.length === 0 || /[.!?]\s*$/.test(beforeFirst));
|
|
2076
|
+
if (alreadyAtEnd) {
|
|
2077
|
+
result.push(line);
|
|
2078
|
+
continue;
|
|
2079
|
+
}
|
|
2080
|
+
let cleaned = line.replace(/\s*\^\[raw\/[^\]]+\]\s*/g, " ").trimEnd();
|
|
2081
|
+
cleaned = cleaned.replace(/ +/g, " ").trimEnd();
|
|
2082
|
+
const markerStrings = markers.map((m) => m[0]);
|
|
2083
|
+
if (cleaned.length > 0 && /[.!?]$/.test(cleaned)) {
|
|
2084
|
+
cleaned += " " + markerStrings.join(" ");
|
|
2085
|
+
} else if (cleaned.length > 0) {
|
|
2086
|
+
cleaned += ". " + markerStrings.join(" ");
|
|
2087
|
+
} else {
|
|
2088
|
+
cleaned = markerStrings.join(" ");
|
|
2089
|
+
}
|
|
2090
|
+
result.push(cleaned);
|
|
2091
|
+
}
|
|
2092
|
+
return result.join("\n");
|
|
2093
|
+
}
|
|
2094
|
+
function buildSourcesFooter(targets) {
|
|
2095
|
+
return "\n## Sources\n" + targets.map((t) => `- ^[${t}]`).join("\n") + "\n";
|
|
2096
|
+
}
|
|
2097
|
+
function reorderSourcesFm(rawFm, targets) {
|
|
2098
|
+
const sourcesLineRe = /^sources:\s*\[([^\]]*)\]\s*$/m;
|
|
2099
|
+
const match = rawFm.match(sourcesLineRe);
|
|
2100
|
+
if (!match) return rawFm;
|
|
2101
|
+
const existing = match[1].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter((s) => s.length > 0);
|
|
2102
|
+
const targetSet = new Set(targets);
|
|
2103
|
+
const reordered = [
|
|
2104
|
+
...targets,
|
|
2105
|
+
...existing.filter((s) => !targetSet.has(s))
|
|
2106
|
+
];
|
|
2107
|
+
const newLine = `sources: [${reordered.join(", ")}]`;
|
|
2108
|
+
return rawFm.replace(sourcesLineRe, newLine);
|
|
2109
|
+
}
|
|
2110
|
+
function removeExistingFooter(body) {
|
|
2111
|
+
const footerRe = /\n## Sources\n[\s\S]*$/;
|
|
2112
|
+
return body.replace(footerRe, "");
|
|
2113
|
+
}
|
|
2114
|
+
async function runMigrateCitations(input) {
|
|
2115
|
+
const scan = await scanVault(input.vault);
|
|
2116
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
2117
|
+
const migrated = [];
|
|
2118
|
+
const skipped = [];
|
|
2119
|
+
let unchanged = 0;
|
|
2120
|
+
for (const page of scan.data.typedKnowledge) {
|
|
2121
|
+
const text = await readPage(page);
|
|
2122
|
+
const split = splitFrontmatter(text);
|
|
2123
|
+
if (!split.ok) continue;
|
|
2124
|
+
const { rawFrontmatter, body } = split.data;
|
|
2125
|
+
const markers = extractCitationMarkers(body);
|
|
2126
|
+
if (markers.length === 0) {
|
|
2127
|
+
unchanged++;
|
|
2128
|
+
continue;
|
|
2129
|
+
}
|
|
2130
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2131
|
+
const uniqueTargets = [];
|
|
2132
|
+
for (const m of markers) {
|
|
2133
|
+
if (!seen.has(m.target)) {
|
|
2134
|
+
seen.add(m.target);
|
|
2135
|
+
uniqueTargets.push(m.target);
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
const bodyWithoutFooter = removeExistingFooter(body);
|
|
2139
|
+
const migratedBody = moveMarkersToParagraphEnd(bodyWithoutFooter);
|
|
2140
|
+
const newFooter = buildSourcesFooter(uniqueTargets);
|
|
2141
|
+
const newFm = reorderSourcesFm(rawFrontmatter, uniqueTargets);
|
|
2142
|
+
const newText = `---
|
|
2143
|
+
${newFm}
|
|
2144
|
+
---
|
|
2145
|
+
${migratedBody}${newFooter}`;
|
|
2146
|
+
if (newText === text) {
|
|
2147
|
+
skipped.push(page.relPath);
|
|
2148
|
+
continue;
|
|
2149
|
+
}
|
|
2150
|
+
if (!input.dryRun) {
|
|
2151
|
+
await writeFile7(page.absPath, newText, "utf8");
|
|
2152
|
+
}
|
|
2153
|
+
migrated.push(page.relPath);
|
|
2154
|
+
}
|
|
2155
|
+
const exitCode = migrated.length > 0 ? ExitCode.MIGRATION_APPLIED : ExitCode.OK;
|
|
2156
|
+
const hintLines = [`scanned: ${migrated.length + skipped.length + unchanged}`];
|
|
2157
|
+
if (migrated.length > 0) hintLines.push(`migrated: ${migrated.length}`);
|
|
2158
|
+
if (skipped.length > 0) hintLines.push(`skipped (already clean): ${skipped.length}`);
|
|
2159
|
+
if (unchanged > 0) hintLines.push(`unchanged (no markers): ${unchanged}`);
|
|
2160
|
+
return {
|
|
2161
|
+
exitCode,
|
|
2162
|
+
result: ok({
|
|
2163
|
+
scanned: migrated.length + skipped.length + unchanged,
|
|
2164
|
+
migrated,
|
|
2165
|
+
skipped,
|
|
2166
|
+
unchanged,
|
|
2167
|
+
humanHint: hintLines.join("\n")
|
|
2168
|
+
})
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
// src/commands/update.ts
|
|
2173
|
+
import { execSync as execSync2 } from "child_process";
|
|
2174
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2175
|
+
async function runUpdate(input) {
|
|
2176
|
+
const pkg2 = JSON.parse(
|
|
2177
|
+
readFileSync3(new URL("../../package.json", import.meta.url), "utf8")
|
|
2178
|
+
);
|
|
2179
|
+
const currentVersion = pkg2.version;
|
|
2180
|
+
const tag = input.distTag ?? "beta";
|
|
2181
|
+
let latest;
|
|
2182
|
+
try {
|
|
2183
|
+
latest = execSync2(`npm view skillwiki@${tag} version`, {
|
|
2184
|
+
encoding: "utf8",
|
|
2185
|
+
timeout: 15e3
|
|
2186
|
+
}).trim();
|
|
2187
|
+
} catch (e) {
|
|
2188
|
+
return {
|
|
2189
|
+
exitCode: ExitCode.PREFLIGHT_FAILED,
|
|
2190
|
+
result: err("PREFLIGHT_FAILED", { message: `Failed to query npm registry: ${String(e)}` })
|
|
2191
|
+
};
|
|
2192
|
+
}
|
|
2193
|
+
const cache = {
|
|
2194
|
+
lastCheck: Date.now(),
|
|
2195
|
+
latestVersion: latest,
|
|
2196
|
+
currentVersion
|
|
2197
|
+
};
|
|
2198
|
+
if (latest === currentVersion) {
|
|
2199
|
+
writeCache(input.home, cache);
|
|
2200
|
+
return {
|
|
2201
|
+
exitCode: ExitCode.OK,
|
|
2202
|
+
result: ok({
|
|
2203
|
+
previousVersion: currentVersion,
|
|
2204
|
+
newVersion: null,
|
|
2205
|
+
wasAlreadyLatest: true,
|
|
2206
|
+
humanHint: `Already on latest ${tag}: v${currentVersion}`
|
|
2207
|
+
})
|
|
2208
|
+
};
|
|
2209
|
+
}
|
|
2210
|
+
try {
|
|
2211
|
+
execSync2(`npm install -g skillwiki@${tag}`, {
|
|
2212
|
+
stdio: "pipe",
|
|
2213
|
+
timeout: 6e4
|
|
2214
|
+
});
|
|
2215
|
+
} catch (e) {
|
|
2216
|
+
return {
|
|
2217
|
+
exitCode: ExitCode.PREFLIGHT_FAILED,
|
|
2218
|
+
result: err("PREFLIGHT_FAILED", { message: `npm install failed: ${String(e)}` })
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
writeCache(input.home, { ...cache, updateAppliedAt: Date.now() });
|
|
2222
|
+
return {
|
|
2223
|
+
exitCode: ExitCode.OK,
|
|
2224
|
+
result: ok({
|
|
2225
|
+
previousVersion: currentVersion,
|
|
2226
|
+
newVersion: latest,
|
|
2227
|
+
wasAlreadyLatest: false,
|
|
2228
|
+
humanHint: `Updated skillwiki ${currentVersion} \u2192 ${latest}`
|
|
1235
2229
|
})
|
|
1236
2230
|
};
|
|
1237
2231
|
}
|
|
1238
2232
|
|
|
1239
2233
|
// src/cli.ts
|
|
2234
|
+
var pkg = JSON.parse(readFileSync4(new URL("../package.json", import.meta.url), "utf8"));
|
|
1240
2235
|
var program = new Command();
|
|
1241
|
-
program.name("skillwiki").description("Deterministic helpers for CodeWiki skills").version(
|
|
2236
|
+
program.name("skillwiki").description("Deterministic helpers for CodeWiki skills").version(pkg.version);
|
|
1242
2237
|
program.option("--human", "render terminal-readable output instead of JSON");
|
|
1243
2238
|
function emit(r) {
|
|
1244
2239
|
if (program.opts().human) printHuman(r.result);
|
|
@@ -1257,7 +2252,7 @@ program.command("orphans [vault]").action(async (vault) => emit(await runOrphans
|
|
|
1257
2252
|
})));
|
|
1258
2253
|
program.command("audit <file>").action(async (file) => emit(await runAudit({ file })));
|
|
1259
2254
|
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("
|
|
2255
|
+
const skillsRoot = opts.skillsRoot ?? new URL("../skills/", import.meta.url).pathname;
|
|
1261
2256
|
emit(await runInstall({ skillsRoot, target: opts.target, dryRun: !!opts.dryRun }));
|
|
1262
2257
|
});
|
|
1263
2258
|
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) => {
|
|
@@ -1279,7 +2274,7 @@ program.command("lang").option("--lang <code>", "explicit language override").op
|
|
|
1279
2274
|
explain: !!opts.explain
|
|
1280
2275
|
}));
|
|
1281
2276
|
});
|
|
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) => {
|
|
2277
|
+
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").action(async (opts) => {
|
|
1283
2278
|
const templates = new URL("../templates/", import.meta.url).pathname;
|
|
1284
2279
|
const taxonomy = typeof opts.taxonomy === "string" ? opts.taxonomy.split(",").map((s) => s.trim()).filter((s) => s.length > 0) : void 0;
|
|
1285
2280
|
emit(await runInit({
|
|
@@ -1290,7 +2285,8 @@ program.command("init").option("--target <dir>", "explicit target directory").re
|
|
|
1290
2285
|
domain: opts.domain,
|
|
1291
2286
|
taxonomy,
|
|
1292
2287
|
lang: opts.lang,
|
|
1293
|
-
force: !!opts.force
|
|
2288
|
+
force: !!opts.force,
|
|
2289
|
+
noEnv: opts.env === false
|
|
1294
2290
|
}));
|
|
1295
2291
|
});
|
|
1296
2292
|
async function resolveVaultArg(arg) {
|
|
@@ -1344,6 +2340,42 @@ program.command("lint [vault]").option("--days <n>", "stale threshold", (s) => p
|
|
|
1344
2340
|
logThreshold: opts.logThreshold
|
|
1345
2341
|
}));
|
|
1346
2342
|
});
|
|
2343
|
+
var configCmd = program.command("config").description("manage skillwiki configuration");
|
|
2344
|
+
configCmd.command("get <key>").description("print the value of a config key").action(async (key) => emit(await runConfigGet({ key, home: process.env.HOME ?? "" })));
|
|
2345
|
+
configCmd.command("set <key> <value>").description("set a config key value").action(async (key, value) => emit(await runConfigSet({ key, value, home: process.env.HOME ?? "" })));
|
|
2346
|
+
configCmd.command("list").description("list all config key=value pairs").action(async () => emit(await runConfigList({ home: process.env.HOME ?? "" })));
|
|
2347
|
+
configCmd.command("path").description("print the config file path").action(async () => emit(await runConfigPath({ home: process.env.HOME ?? "" })));
|
|
2348
|
+
program.command("doctor").description("diagnose skillwiki setup issues").action(async () => emit(await runDoctor({
|
|
2349
|
+
home: process.env.HOME ?? "",
|
|
2350
|
+
envValue: process.env.WIKI_PATH,
|
|
2351
|
+
argv: process.argv,
|
|
2352
|
+
currentVersion: pkg.version
|
|
2353
|
+
})));
|
|
2354
|
+
program.command("archive <page> [vault]").description("archive a typed-knowledge page").action(async (page, vault) => {
|
|
2355
|
+
const v = await resolveVaultArg(vault);
|
|
2356
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2357
|
+
else emit(await runArchive({ vault: v.vault, page }));
|
|
2358
|
+
});
|
|
2359
|
+
program.command("drift [vault]").description("detect content drift in raw sources").action(async (vault) => {
|
|
2360
|
+
const v = await resolveVaultArg(vault);
|
|
2361
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2362
|
+
else emit(await runDrift({ vault: v.vault }));
|
|
2363
|
+
});
|
|
2364
|
+
program.command("dedup [vault]").description("detect duplicate raw sources by sha256").action(async (vault) => {
|
|
2365
|
+
const v = await resolveVaultArg(vault);
|
|
2366
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2367
|
+
else emit(await runDedup({ vault: v.vault }));
|
|
2368
|
+
});
|
|
2369
|
+
program.command("migrate-citations [vault]").description("migrate ^[raw/...] markers to paragraph-end citations").option("--dry-run", "preview changes without writing", false).action(async (vault, opts) => {
|
|
2370
|
+
const v = await resolveVaultArg(vault);
|
|
2371
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2372
|
+
else emit(await runMigrateCitations({ vault: v.vault, dryRun: !!opts.dryRun }));
|
|
2373
|
+
});
|
|
2374
|
+
program.command("update").description("update skillwiki CLI to the latest version").option("--tag <tag>", "npm dist-tag", "beta").action(async (opts) => emit(await runUpdate({
|
|
2375
|
+
home: process.env.HOME ?? "",
|
|
2376
|
+
distTag: opts.tag
|
|
2377
|
+
})));
|
|
2378
|
+
triggerAutoUpdate(process.env.HOME ?? "", pkg.version);
|
|
1347
2379
|
program.parseAsync(process.argv).catch((e) => {
|
|
1348
2380
|
process.stdout.write(JSON.stringify({ ok: false, error: "INTERNAL", detail: { message: String(e) } }) + "\n");
|
|
1349
2381
|
process.exit(1);
|