skillwiki 0.2.0-beta.7 → 0.2.0
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 +1151 -106
- package/package.json +3 -2
- package/skills/.claude-plugin/plugin.json +1 -1
- package/skills/bin/skillwiki +5 -0
- package/skills/package.json +1 -1
- package/skills/proj-decide/SKILL.md +1 -1
- package/skills/proj-distill/SKILL.md +26 -4
- package/skills/proj-work/SKILL.md +20 -1
- package/skills/using-skillwiki/SKILL.md +66 -1
- package/skills/wiki-adapter-prd/SKILL.md +87 -0
- package/skills/wiki-archive/SKILL.md +43 -0
- package/skills/wiki-audit/SKILL.md +1 -1
- package/skills/wiki-crystallize/SKILL.md +1 -1
- package/skills/wiki-ingest/SKILL.md +15 -4
- package/skills/wiki-lint/SKILL.md +1 -1
- package/skills/wiki-query/SKILL.md +2 -2
- package/skills/wiki-reingest/SKILL.md +54 -0
- package/templates/SCHEMA.md +18 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
semverGt
|
|
4
|
+
} from "./chunk-XM5IYZX7.js";
|
|
2
5
|
|
|
3
6
|
// src/cli.ts
|
|
4
|
-
import { readFileSync } from "fs";
|
|
7
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
5
8
|
import { Command } from "commander";
|
|
6
9
|
|
|
7
10
|
// src/utils/output.ts
|
|
@@ -63,7 +66,14 @@ var ExitCode = {
|
|
|
63
66
|
INVALID_CONFIG_KEY: 26,
|
|
64
67
|
CONFIG_WRITE_FAILED: 27,
|
|
65
68
|
DOCTOR_HAS_WARNINGS: 28,
|
|
66
|
-
DOCTOR_HAS_ERRORS: 29
|
|
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,
|
|
76
|
+
DEDUP_APPLIED: 36
|
|
67
77
|
};
|
|
68
78
|
|
|
69
79
|
// ../shared/src/json-output.ts
|
|
@@ -103,10 +113,10 @@ var TypedKnowledgeSchema = z.object({
|
|
|
103
113
|
});
|
|
104
114
|
var sha256Hex = z.string().regex(/^[0-9a-f]{64}$/);
|
|
105
115
|
var RawSourceSchema = z.object({
|
|
106
|
-
title: z.string().min(1),
|
|
107
|
-
source_url: z.string().
|
|
116
|
+
title: z.string().min(1).optional(),
|
|
117
|
+
source_url: z.string().nullable(),
|
|
108
118
|
ingested: isoDate,
|
|
109
|
-
ingested_by: z.enum(["wiki-ingest", "proj-work", "manual"]),
|
|
119
|
+
ingested_by: z.enum(["wiki-ingest", "proj-work", "manual"]).optional(),
|
|
110
120
|
sha256: sha256Hex,
|
|
111
121
|
project: wikilink.optional(),
|
|
112
122
|
work_item: wikilink.optional(),
|
|
@@ -241,7 +251,7 @@ async function runHash(input) {
|
|
|
241
251
|
const sha256 = createHash("sha256").update(bodyBytes).digest("hex");
|
|
242
252
|
return {
|
|
243
253
|
exitCode: ExitCode.OK,
|
|
244
|
-
result: ok({ path: input.file, sha256, byte_count: bodyBytes.byteLength })
|
|
254
|
+
result: ok({ path: input.file, sha256, byte_count: bodyBytes.byteLength, humanHint: sha256 })
|
|
245
255
|
};
|
|
246
256
|
}
|
|
247
257
|
|
|
@@ -271,7 +281,7 @@ function runFetchGuardSync(input) {
|
|
|
271
281
|
result: err("HOST_BLOCKED", { sanitized_url: sanitized, host: u.hostname })
|
|
272
282
|
};
|
|
273
283
|
}
|
|
274
|
-
return { exitCode: ExitCode.OK, result: ok({ allowed: true, sanitized_url: sanitized }) };
|
|
284
|
+
return { exitCode: ExitCode.OK, result: ok({ allowed: true, sanitized_url: sanitized, humanHint: `ALLOWED: ${sanitized}` }) };
|
|
275
285
|
}
|
|
276
286
|
function sanitizeUrl(u) {
|
|
277
287
|
const clone = new URL(u.toString());
|
|
@@ -311,17 +321,18 @@ async function runValidate(input) {
|
|
|
311
321
|
}
|
|
312
322
|
const det = detectSchema(fm.data);
|
|
313
323
|
if (!det.schema) {
|
|
314
|
-
return { exitCode: ExitCode.SCHEMA_NOT_DETECTED, result: ok({ schema: null, valid: false, errors: [] }) };
|
|
324
|
+
return { exitCode: ExitCode.SCHEMA_NOT_DETECTED, result: ok({ schema: null, valid: false, errors: [], humanHint: "schema not detected" }) };
|
|
315
325
|
}
|
|
316
326
|
const parsed = SCHEMAS[det.schema].safeParse(fm.data);
|
|
317
327
|
if (!parsed.success) {
|
|
318
328
|
const errors = parsed.error.issues.map((i) => ({ path: i.path.join("."), message: i.message }));
|
|
319
329
|
return {
|
|
320
330
|
exitCode: ExitCode.INVALID_FRONTMATTER,
|
|
321
|
-
result: ok({ schema: det.schema, valid: false, errors })
|
|
331
|
+
result: ok({ schema: det.schema, valid: false, errors, humanHint: `INVALID (${det.schema})
|
|
332
|
+
${errors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}` })
|
|
322
333
|
};
|
|
323
334
|
}
|
|
324
|
-
return { exitCode: ExitCode.OK, result: ok({ schema: det.schema, valid: true, errors: [] }) };
|
|
335
|
+
return { exitCode: ExitCode.OK, result: ok({ schema: det.schema, valid: true, errors: [], humanHint: `VALID (${det.schema})` }) };
|
|
325
336
|
}
|
|
326
337
|
|
|
327
338
|
// src/commands/graph.ts
|
|
@@ -407,7 +418,8 @@ async function runGraphBuild(input) {
|
|
|
407
418
|
}
|
|
408
419
|
return {
|
|
409
420
|
exitCode: ExitCode.OK,
|
|
410
|
-
result: ok({ out_path: input.out, node_count: scan.data.typedKnowledge.length, edge_count }
|
|
421
|
+
result: ok({ out_path: input.out, node_count: scan.data.typedKnowledge.length, edge_count, humanHint: `nodes: ${scan.data.typedKnowledge.length}, edges: ${edge_count}
|
|
422
|
+
written: ${input.out}` })
|
|
411
423
|
};
|
|
412
424
|
}
|
|
413
425
|
function computeAdamicAdar(adj) {
|
|
@@ -482,7 +494,8 @@ async function runOverlap(input) {
|
|
|
482
494
|
}
|
|
483
495
|
return { id, members, score };
|
|
484
496
|
});
|
|
485
|
-
|
|
497
|
+
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");
|
|
498
|
+
return { exitCode: ExitCode.OK, result: ok({ clusters, humanHint }) };
|
|
486
499
|
}
|
|
487
500
|
|
|
488
501
|
// src/utils/wiki-path.ts
|
|
@@ -493,6 +506,16 @@ import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir2 } from
|
|
|
493
506
|
import { dirname as dirname2 } from "path";
|
|
494
507
|
var CONFIG_KEYS = ["WIKI_PATH", "WIKI_LANG"];
|
|
495
508
|
var _whitelist = new Set(CONFIG_KEYS);
|
|
509
|
+
var PROFILE_PATH_RE = /^WIKI_([A-Z][A-Z0-9_]{0,31})_PATH$/;
|
|
510
|
+
var PROFILE_LANG_RE = /^WIKI_([A-Z][A-Z0-9_]{0,31})_LANG$/;
|
|
511
|
+
var PROFILE_DEFAULT_RE = /^WIKI_DEFAULT$/;
|
|
512
|
+
function isValidWikiProfileKey(key) {
|
|
513
|
+
if (key === "WIKI_PATH" || key === "WIKI_LANG") return false;
|
|
514
|
+
return PROFILE_PATH_RE.test(key) || PROFILE_LANG_RE.test(key) || PROFILE_DEFAULT_RE.test(key);
|
|
515
|
+
}
|
|
516
|
+
function profileKey(name, suffix) {
|
|
517
|
+
return `WIKI_${name.toUpperCase().replace(/-/g, "_").replace(/[^A-Z0-9_]/g, "")}_${suffix}`;
|
|
518
|
+
}
|
|
496
519
|
function parseDotenvText(text) {
|
|
497
520
|
const out = {};
|
|
498
521
|
for (const rawLine of text.split(/\r?\n/)) {
|
|
@@ -502,7 +525,7 @@ function parseDotenvText(text) {
|
|
|
502
525
|
if (eq <= 0) continue;
|
|
503
526
|
const key = line.slice(0, eq).trim();
|
|
504
527
|
const value = line.slice(eq + 1).trim();
|
|
505
|
-
if (!_whitelist.has(key)) continue;
|
|
528
|
+
if (!_whitelist.has(key) && !isValidWikiProfileKey(key)) continue;
|
|
506
529
|
if (value.length === 0) continue;
|
|
507
530
|
out[key] = value;
|
|
508
531
|
}
|
|
@@ -587,6 +610,14 @@ async function resolveInitTimePath(input) {
|
|
|
587
610
|
return { path: hermes.WIKI_PATH, source: "hermes-dotenv", ...input.explain ? { chain } : {} };
|
|
588
611
|
}
|
|
589
612
|
if (input.explain) chain.push({ source: "hermes-dotenv", matched: false });
|
|
613
|
+
if (input.cwd) {
|
|
614
|
+
const projCfg = await parseDotenvFile(join2(input.cwd, ".skillwiki", ".env"));
|
|
615
|
+
if (projCfg.WIKI_PATH !== void 0) {
|
|
616
|
+
if (input.explain) chain.push({ source: "project-dotenv", matched: true, value: projCfg.WIKI_PATH });
|
|
617
|
+
return { path: projCfg.WIKI_PATH, source: "project-dotenv", ...input.explain ? { chain } : {} };
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (input.explain) chain.push({ source: "project-dotenv", matched: false });
|
|
590
621
|
const fallback = join2(input.home, "wiki");
|
|
591
622
|
if (input.explain) chain.push({ source: "default", matched: true, value: fallback });
|
|
592
623
|
return { path: fallback, source: "default", ...input.explain ? { chain } : {} };
|
|
@@ -598,15 +629,73 @@ async function resolveRuntimePath(input) {
|
|
|
598
629
|
return ok({ path: input.flag, source: "flag", ...input.explain ? { chain } : {} });
|
|
599
630
|
}
|
|
600
631
|
if (input.explain) chain.push({ source: "flag", matched: false });
|
|
632
|
+
const swGlobal = await parseDotenvFile(join2(input.home, ".skillwiki", ".env"));
|
|
633
|
+
const wikiName = input.wiki;
|
|
634
|
+
if (wikiName !== void 0 && wikiName.length > 0) {
|
|
635
|
+
if (wikiName.toLowerCase() === "default") {
|
|
636
|
+
const path2 = swGlobal.WIKI_PATH;
|
|
637
|
+
if (path2 !== void 0) {
|
|
638
|
+
if (input.explain) chain.push({ source: "wiki-profile", matched: true, value: path2 });
|
|
639
|
+
return ok({ path: path2, source: "skillwiki-dotenv", ...input.explain ? { chain } : {} });
|
|
640
|
+
}
|
|
641
|
+
if (input.explain) chain.push({ source: "wiki-profile", matched: false });
|
|
642
|
+
return err("UNKNOWN_WIKI_PROFILE", {
|
|
643
|
+
message: `Wiki profile "default" not found. Set it with: skillwiki config set wiki.path <dir>`
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
const key = profileKey(wikiName, "PATH");
|
|
647
|
+
const path = swGlobal[key];
|
|
648
|
+
if (path !== void 0) {
|
|
649
|
+
if (input.explain) chain.push({ source: "wiki-profile", matched: true, value: path });
|
|
650
|
+
return ok({ path, source: "wiki-profile", ...input.explain ? { chain } : {} });
|
|
651
|
+
}
|
|
652
|
+
if (input.explain) chain.push({ source: "wiki-profile", matched: false });
|
|
653
|
+
return err("UNKNOWN_WIKI_PROFILE", {
|
|
654
|
+
message: `Wiki profile "${wikiName}" not found. Set it with: skillwiki config set wiki.${wikiName}.path <dir>`
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
if (input.wikiEnv !== void 0 && input.wikiEnv.length > 0) {
|
|
658
|
+
const key = profileKey(input.wikiEnv, "PATH");
|
|
659
|
+
const path = swGlobal[key];
|
|
660
|
+
if (path !== void 0) {
|
|
661
|
+
if (input.explain) chain.push({ source: "wiki-profile", matched: true, value: path });
|
|
662
|
+
return ok({ path, source: "wiki-profile", ...input.explain ? { chain } : {} });
|
|
663
|
+
}
|
|
664
|
+
if (input.explain) chain.push({ source: "wiki-profile", matched: false });
|
|
665
|
+
return err("UNKNOWN_WIKI_PROFILE", {
|
|
666
|
+
message: `Wiki profile "${input.wikiEnv}" not found (from $WIKI env). Set it with: skillwiki config set wiki.${input.wikiEnv}.path <dir>`
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
if (input.explain) chain.push({ source: "wiki-profile", matched: false });
|
|
601
670
|
if (input.envValue !== void 0 && input.envValue.length > 0) {
|
|
602
671
|
if (input.explain) chain.push({ source: "env", matched: true, value: input.envValue });
|
|
603
672
|
return ok({ path: input.envValue, source: "env", ...input.explain ? { chain } : {} });
|
|
604
673
|
}
|
|
605
674
|
if (input.explain) chain.push({ source: "env", matched: false });
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
if (
|
|
609
|
-
|
|
675
|
+
if (input.cwd) {
|
|
676
|
+
const projCfg = await parseDotenvFile(join2(input.cwd, ".skillwiki", ".env"));
|
|
677
|
+
if (projCfg.WIKI_PATH !== void 0) {
|
|
678
|
+
if (input.explain) chain.push({ source: "project-dotenv", matched: true, value: projCfg.WIKI_PATH });
|
|
679
|
+
return ok({ path: projCfg.WIKI_PATH, source: "project-dotenv", ...input.explain ? { chain } : {} });
|
|
680
|
+
}
|
|
681
|
+
if (input.explain) chain.push({ source: "project-dotenv", matched: false });
|
|
682
|
+
}
|
|
683
|
+
const defaultProfile = swGlobal["WIKI_DEFAULT"];
|
|
684
|
+
if (defaultProfile !== void 0) {
|
|
685
|
+
const key = profileKey(defaultProfile, "PATH");
|
|
686
|
+
const path = swGlobal[key];
|
|
687
|
+
if (path !== void 0) {
|
|
688
|
+
if (input.explain) chain.push({ source: "wiki-default", matched: true, value: path });
|
|
689
|
+
return ok({ path, source: "wiki-default", ...input.explain ? { chain } : {} });
|
|
690
|
+
}
|
|
691
|
+
if (input.explain) chain.push({ source: "wiki-default", matched: false });
|
|
692
|
+
return err("UNKNOWN_WIKI_PROFILE", {
|
|
693
|
+
message: `Default wiki profile "${defaultProfile}" not found. Set it with: skillwiki config set wiki.${defaultProfile}.path <dir>`
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
if (swGlobal.WIKI_PATH !== void 0) {
|
|
697
|
+
if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: true, value: swGlobal.WIKI_PATH });
|
|
698
|
+
return ok({ path: swGlobal.WIKI_PATH, source: "skillwiki-dotenv", ...input.explain ? { chain } : {} });
|
|
610
699
|
}
|
|
611
700
|
if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: false });
|
|
612
701
|
return err("NO_VAULT_CONFIGURED", {
|
|
@@ -620,15 +709,21 @@ async function runOrphans(input) {
|
|
|
620
709
|
if (input.vault) {
|
|
621
710
|
vault = input.vault;
|
|
622
711
|
} else {
|
|
623
|
-
const r = await resolveRuntimePath({ flag: void 0, envValue: input.envValue, home: input.home ?? "" });
|
|
624
|
-
if (!r.ok)
|
|
712
|
+
const r = await resolveRuntimePath({ flag: void 0, envValue: input.envValue, home: input.home ?? "", wiki: input.wiki });
|
|
713
|
+
if (!r.ok) {
|
|
714
|
+
const exitCode = r.error === "UNKNOWN_WIKI_PROFILE" ? ExitCode.UNKNOWN_WIKI_PROFILE : ExitCode.NO_VAULT_CONFIGURED;
|
|
715
|
+
return { exitCode, result: r };
|
|
716
|
+
}
|
|
625
717
|
vault = r.data.path;
|
|
626
718
|
}
|
|
627
719
|
const scan = await scanVault(vault);
|
|
628
720
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
629
721
|
const slugToPath = {};
|
|
630
722
|
for (const p of scan.data.typedKnowledge) {
|
|
631
|
-
|
|
723
|
+
const rel = p.relPath.replace(/\.md$/, "");
|
|
724
|
+
slugToPath[rel] = p.relPath;
|
|
725
|
+
const filename = rel.split("/").pop();
|
|
726
|
+
if (!(filename in slugToPath)) slugToPath[filename] = p.relPath;
|
|
632
727
|
}
|
|
633
728
|
const adj = {};
|
|
634
729
|
for (const p of scan.data.typedKnowledge) adj[p.relPath] = /* @__PURE__ */ new Set();
|
|
@@ -637,7 +732,7 @@ async function runOrphans(input) {
|
|
|
637
732
|
const split = splitFrontmatter(text);
|
|
638
733
|
const body = split.ok ? split.data.body : text;
|
|
639
734
|
for (const slug of extractBodyWikilinks(body)) {
|
|
640
|
-
const tgt = slugToPath[slug.split("/").pop()];
|
|
735
|
+
const tgt = slugToPath[slug] ?? slugToPath[slug.split("/").pop()];
|
|
641
736
|
if (tgt) {
|
|
642
737
|
adj[p.relPath].add(tgt);
|
|
643
738
|
adj[tgt].add(p.relPath);
|
|
@@ -668,7 +763,11 @@ async function runOrphans(input) {
|
|
|
668
763
|
}
|
|
669
764
|
}
|
|
670
765
|
}
|
|
671
|
-
|
|
766
|
+
const hintLines = [];
|
|
767
|
+
if (orphans.length > 0) hintLines.push(`orphans: ${orphans.length}`, ...orphans.map((o) => ` ${o}`));
|
|
768
|
+
if (bridges.length > 0) hintLines.push(`bridges: ${bridges.length}`, ...bridges.map((b) => ` ${b.path}`));
|
|
769
|
+
if (hintLines.length === 0) hintLines.push("no orphans or bridges");
|
|
770
|
+
return { exitCode: ExitCode.OK, result: ok({ orphans, bridges, humanHint: hintLines.join("\n") }) };
|
|
672
771
|
}
|
|
673
772
|
function simulateRemoval(adj, removed) {
|
|
674
773
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -693,16 +792,92 @@ import { dirname as dirname3, resolve, join as join3 } from "path";
|
|
|
693
792
|
|
|
694
793
|
// src/parsers/citations.ts
|
|
695
794
|
var FENCE2 = /```[\s\S]*?```/g;
|
|
795
|
+
var INLINE_CODE = /`[^`\n]+`/g;
|
|
796
|
+
var MARKER_RE = /\^\[(raw\/[^\]]+)\]/g;
|
|
797
|
+
function stripFences(body) {
|
|
798
|
+
return body.replace(FENCE2, "").replace(INLINE_CODE, "");
|
|
799
|
+
}
|
|
800
|
+
function stripFencedBlocks(body) {
|
|
801
|
+
return body.replace(FENCE2, "");
|
|
802
|
+
}
|
|
696
803
|
function extractCitationMarkers(body) {
|
|
697
|
-
const stripped = body
|
|
804
|
+
const stripped = stripFences(body);
|
|
698
805
|
const out = [];
|
|
699
|
-
const re = /\^\[(raw\/[^\]]+)\]/g;
|
|
700
806
|
let m;
|
|
701
|
-
while ((m =
|
|
807
|
+
while ((m = MARKER_RE.exec(stripped)) !== null) {
|
|
702
808
|
out.push({ marker: m[0], target: m[1] });
|
|
703
809
|
}
|
|
704
810
|
return out;
|
|
705
811
|
}
|
|
812
|
+
function hasSourcesFooter(body) {
|
|
813
|
+
return /^## Sources\s*$/m.test(stripFencedBlocks(body));
|
|
814
|
+
}
|
|
815
|
+
function isLegacyCitationStyle(body) {
|
|
816
|
+
const markers = extractCitationMarkers(body);
|
|
817
|
+
if (markers.length === 0) return false;
|
|
818
|
+
if (!hasSourcesFooter(body)) return true;
|
|
819
|
+
const lines = stripFences(body).split("\n");
|
|
820
|
+
let inSources = false;
|
|
821
|
+
for (const line of lines) {
|
|
822
|
+
if (/^## Sources\b/.test(line.trim())) {
|
|
823
|
+
inSources = true;
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
if (inSources) continue;
|
|
827
|
+
const markerOnly = line.replace(MARKER_RE, "").trim();
|
|
828
|
+
if (markerOnly.length === 0 && /\^\[raw\//.test(line)) return true;
|
|
829
|
+
const lastMarkerIdx = line.lastIndexOf("^[raw/");
|
|
830
|
+
if (lastMarkerIdx >= 0) {
|
|
831
|
+
const afterLast = line.slice(lastMarkerIdx).replace(MARKER_RE, "").trim();
|
|
832
|
+
if (afterLast.length > 0) return true;
|
|
833
|
+
const beforeFirst = line.slice(0, line.indexOf("^[raw/")).trim();
|
|
834
|
+
if (beforeFirst.length > 0 && !/[.!?]\s*$/.test(beforeFirst)) return true;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return false;
|
|
838
|
+
}
|
|
839
|
+
function hasOrphanedCitations(body) {
|
|
840
|
+
const stripped = stripFences(body);
|
|
841
|
+
const lines = stripped.split("\n");
|
|
842
|
+
let inSources = false;
|
|
843
|
+
let sourcesEnded = false;
|
|
844
|
+
let sourcesStartLine = -1;
|
|
845
|
+
let lastNonBlankInSources = -1;
|
|
846
|
+
for (let i = 0; i < lines.length; i++) {
|
|
847
|
+
const line = lines[i];
|
|
848
|
+
const trimmed = line.trim();
|
|
849
|
+
if (/^## Sources\b/.test(trimmed)) {
|
|
850
|
+
inSources = true;
|
|
851
|
+
sourcesStartLine = i;
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
if (!inSources || sourcesEnded) continue;
|
|
855
|
+
if (trimmed.length === 0) {
|
|
856
|
+
if (lastNonBlankInSources >= 0) {
|
|
857
|
+
sourcesEnded = true;
|
|
858
|
+
}
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
const isListItem = /^\s*[-*]\s+/.test(line);
|
|
862
|
+
const hasMarker = /\^\[raw\//.test(line);
|
|
863
|
+
if (isListItem && hasMarker) {
|
|
864
|
+
lastNonBlankInSources = i;
|
|
865
|
+
} else if (hasMarker && !isListItem) {
|
|
866
|
+
return true;
|
|
867
|
+
} else {
|
|
868
|
+
sourcesEnded = true;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
if (sourcesStartLine === -1) return false;
|
|
872
|
+
if (sourcesEnded) {
|
|
873
|
+
for (let i = lastNonBlankInSources + 1; i < lines.length; i++) {
|
|
874
|
+
if (/\^\[raw\//.test(lines[i])) {
|
|
875
|
+
return true;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
706
881
|
|
|
707
882
|
// src/commands/audit.ts
|
|
708
883
|
async function runAudit(input) {
|
|
@@ -727,17 +902,40 @@ async function runAudit(input) {
|
|
|
727
902
|
return { ...m, resolved: false };
|
|
728
903
|
}
|
|
729
904
|
}));
|
|
730
|
-
const sources = fm.data.sources ?? [];
|
|
905
|
+
const sources = (fm.data.sources ?? []).map((s) => s.replace(/^\^\[/, "").replace(/\]$/, ""));
|
|
731
906
|
const referenced = new Set(resolved.map((m) => m.target));
|
|
732
907
|
const unused_sources = sources.filter((s) => !referenced.has(s));
|
|
733
908
|
const missing_from_sources = [...referenced].filter((t) => !sources.includes(t));
|
|
909
|
+
const broken = resolved.filter((m) => !m.resolved);
|
|
910
|
+
const footerMatch = body.match(/\n## Sources\n([\s\S]*)$/);
|
|
911
|
+
let footer_consistency;
|
|
912
|
+
if (footerMatch) {
|
|
913
|
+
const footerTargets = /* @__PURE__ */ new Set();
|
|
914
|
+
const footerRe = /\^\[(raw\/[^\]]+)\]/g;
|
|
915
|
+
let mm;
|
|
916
|
+
while ((mm = footerRe.exec(footerMatch[1])) !== null) footerTargets.add(mm[1]);
|
|
917
|
+
const bodyTargets = new Set(resolved.map((m) => m.target));
|
|
918
|
+
const missing_from_footer = [...bodyTargets].filter((t) => !footerTargets.has(t));
|
|
919
|
+
const extra_in_footer = [...footerTargets].filter((t) => !bodyTargets.has(t));
|
|
920
|
+
footer_consistency = { missing_from_footer, extra_in_footer };
|
|
921
|
+
}
|
|
922
|
+
const hintLines = [];
|
|
923
|
+
hintLines.push(`markers: ${resolved.length}, broken: ${broken.length}`);
|
|
924
|
+
if (unused_sources.length > 0) hintLines.push(`unused_sources: ${unused_sources.length}`);
|
|
925
|
+
if (missing_from_sources.length > 0) hintLines.push(`missing_from_sources: ${missing_from_sources.length}`);
|
|
926
|
+
if (footer_consistency) {
|
|
927
|
+
if (footer_consistency.missing_from_footer.length > 0) hintLines.push(`missing_from_footer: ${footer_consistency.missing_from_footer.length}`);
|
|
928
|
+
if (footer_consistency.extra_in_footer.length > 0) hintLines.push(`extra_in_footer: ${footer_consistency.extra_in_footer.length}`);
|
|
929
|
+
}
|
|
930
|
+
if (broken.length === 0 && unused_sources.length === 0 && missing_from_sources.length === 0) hintLines.push("OK");
|
|
931
|
+
const humanHint = hintLines.join("\n");
|
|
734
932
|
if (resolved.some((m) => !m.resolved)) {
|
|
735
|
-
return { exitCode: ExitCode.UNRESOLVED_MARKERS, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources } }) };
|
|
933
|
+
return { exitCode: ExitCode.UNRESOLVED_MARKERS, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, footer_consistency, humanHint }) };
|
|
736
934
|
}
|
|
737
935
|
if (unused_sources.length > 0 || missing_from_sources.length > 0) {
|
|
738
|
-
return { exitCode: ExitCode.SOURCES_INCONSISTENT, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources } }) };
|
|
936
|
+
return { exitCode: ExitCode.SOURCES_INCONSISTENT, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, footer_consistency, humanHint }) };
|
|
739
937
|
}
|
|
740
|
-
return { exitCode: ExitCode.OK, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources } }) };
|
|
938
|
+
return { exitCode: ExitCode.OK, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, footer_consistency, humanHint }) };
|
|
741
939
|
}
|
|
742
940
|
async function findVaultRoot(start) {
|
|
743
941
|
let cur = start;
|
|
@@ -815,9 +1013,28 @@ async function runInstall(input) {
|
|
|
815
1013
|
installed.push(dst);
|
|
816
1014
|
if (r.data.backupPath) backed_up.push(r.data.backupPath);
|
|
817
1015
|
}
|
|
1016
|
+
const binSrc = join4(input.skillsRoot, "bin", "skillwiki");
|
|
1017
|
+
try {
|
|
1018
|
+
await stat4(binSrc);
|
|
1019
|
+
const binDst = join4(input.target, "bin", "skillwiki");
|
|
1020
|
+
if (!input.dryRun) {
|
|
1021
|
+
const r = await atomicCopyWithBackup(binSrc, binDst);
|
|
1022
|
+
if (!r.ok) return { exitCode: ExitCode.ATOMIC_COPY_FAILED, result: r };
|
|
1023
|
+
installed.push(binDst);
|
|
1024
|
+
if (r.data.backupPath) backed_up.push(r.data.backupPath);
|
|
1025
|
+
} else {
|
|
1026
|
+
installed.push(binDst);
|
|
1027
|
+
}
|
|
1028
|
+
} catch {
|
|
1029
|
+
}
|
|
818
1030
|
const manifest_path = join4(input.target, "wiki-manifest.json");
|
|
819
1031
|
if (!input.dryRun) await writeManifest(manifest_path, { installed, backed_up });
|
|
820
|
-
|
|
1032
|
+
const hintLines = [
|
|
1033
|
+
`installed: ${installed.length}`,
|
|
1034
|
+
input.dryRun ? "(dry run)" : `backed up: ${backed_up.length}`,
|
|
1035
|
+
`manifest: ${manifest_path}`
|
|
1036
|
+
];
|
|
1037
|
+
return { exitCode: ExitCode.OK, result: ok({ installed, backed_up, manifest_path, humanHint: hintLines.join("\n") }) };
|
|
821
1038
|
}
|
|
822
1039
|
|
|
823
1040
|
// src/commands/path.ts
|
|
@@ -829,16 +1046,20 @@ async function runPath(input) {
|
|
|
829
1046
|
home: input.home,
|
|
830
1047
|
explain: input.explain
|
|
831
1048
|
});
|
|
832
|
-
return { exitCode: ExitCode.OK, result: ok({ path: r2.path, source: r2.source, ...r2.chain ? { chain: r2.chain } : {} }) };
|
|
1049
|
+
return { exitCode: ExitCode.OK, result: ok({ path: r2.path, source: r2.source, ...r2.chain ? { chain: r2.chain } : {}, humanHint: `${r2.path} (via ${r2.source})` }) };
|
|
833
1050
|
}
|
|
834
1051
|
const r = await resolveRuntimePath({
|
|
835
1052
|
flag: input.flag,
|
|
836
1053
|
envValue: input.envValue,
|
|
837
1054
|
home: input.home,
|
|
1055
|
+
wiki: input.wiki,
|
|
838
1056
|
explain: input.explain
|
|
839
1057
|
});
|
|
840
|
-
if (!r.ok)
|
|
841
|
-
|
|
1058
|
+
if (!r.ok) {
|
|
1059
|
+
const exitCode = r.error === "UNKNOWN_WIKI_PROFILE" ? ExitCode.UNKNOWN_WIKI_PROFILE : ExitCode.NO_VAULT_CONFIGURED;
|
|
1060
|
+
return { exitCode, result: r };
|
|
1061
|
+
}
|
|
1062
|
+
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})` }) };
|
|
842
1063
|
}
|
|
843
1064
|
|
|
844
1065
|
// src/utils/lang.ts
|
|
@@ -892,7 +1113,8 @@ async function runLang(input) {
|
|
|
892
1113
|
value: resolved.value,
|
|
893
1114
|
source: resolved.source,
|
|
894
1115
|
canonical: resolved.canonical,
|
|
895
|
-
...chain ? { chain } : {}
|
|
1116
|
+
...chain ? { chain } : {},
|
|
1117
|
+
humanHint: `${resolved.value} (via ${resolved.source})`
|
|
896
1118
|
})
|
|
897
1119
|
};
|
|
898
1120
|
}
|
|
@@ -949,8 +1171,10 @@ var VAULT_DIRS = [
|
|
|
949
1171
|
"comparisons",
|
|
950
1172
|
"queries",
|
|
951
1173
|
"meta",
|
|
952
|
-
"projects"
|
|
1174
|
+
"projects",
|
|
1175
|
+
".obsidian"
|
|
953
1176
|
];
|
|
1177
|
+
var ATTACHMENT_FOLDER = "raw/assets";
|
|
954
1178
|
function extractDomainFromSchema(text) {
|
|
955
1179
|
const m = text.match(/^##\s+Domain\s*\n([\s\S]*?)(?=\n\n|\n##|\s*$)/m);
|
|
956
1180
|
if (!m) return "";
|
|
@@ -1005,13 +1229,13 @@ async function runInit(input) {
|
|
|
1005
1229
|
}
|
|
1006
1230
|
const existingEnv = parseDotenvText(existingEnvRaw);
|
|
1007
1231
|
const swDotenvHadPath = existingEnv.WIKI_PATH !== void 0;
|
|
1008
|
-
if (existingEnv.WIKI_PATH !== void 0 && existingEnv.WIKI_PATH !== target && !input.force) {
|
|
1232
|
+
if (!input.profile && existingEnv.WIKI_PATH !== void 0 && existingEnv.WIKI_PATH !== target && !input.force) {
|
|
1009
1233
|
return {
|
|
1010
1234
|
exitCode: ExitCode.ENV_WRITE_CONFLICT,
|
|
1011
1235
|
result: err("ENV_WRITE_CONFLICT", { key: "WIKI_PATH", existing: existingEnv.WIKI_PATH, attempted: target })
|
|
1012
1236
|
};
|
|
1013
1237
|
}
|
|
1014
|
-
if (existingEnv.WIKI_LANG !== void 0 && existingEnv.WIKI_LANG !== canonicalLang && !input.force) {
|
|
1238
|
+
if (!input.profile && existingEnv.WIKI_LANG !== void 0 && existingEnv.WIKI_LANG !== canonicalLang && !input.force) {
|
|
1015
1239
|
return {
|
|
1016
1240
|
exitCode: ExitCode.ENV_WRITE_CONFLICT,
|
|
1017
1241
|
result: err("ENV_WRITE_CONFLICT", { key: "WIKI_LANG", existing: existingEnv.WIKI_LANG, attempted: canonicalLang })
|
|
@@ -1080,23 +1304,43 @@ async function runInit(input) {
|
|
|
1080
1304
|
return tpl.replace("{{INIT_DATE}}", today);
|
|
1081
1305
|
});
|
|
1082
1306
|
if (err1) return err1;
|
|
1083
|
-
const
|
|
1307
|
+
const errObsidian = await writeOrPreserve(".obsidian/app.json", async () => {
|
|
1308
|
+
return JSON.stringify({ attachmentFolderPath: ATTACHMENT_FOLDER }, null, 2) + "\n";
|
|
1309
|
+
});
|
|
1310
|
+
if (errObsidian) return errObsidian;
|
|
1311
|
+
const err22 = await writeOrPreserve("log.md", async () => {
|
|
1084
1312
|
const tpl = await readFile6(join7(input.templates, "log.md"), "utf8");
|
|
1085
1313
|
return tpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang);
|
|
1086
1314
|
});
|
|
1087
|
-
if (
|
|
1088
|
-
const
|
|
1089
|
-
const skipEnv = !!input.noEnv || isTempPath;
|
|
1315
|
+
if (err22) return err22;
|
|
1316
|
+
const skipEnv = !!input.noEnv;
|
|
1090
1317
|
let envWritten = "";
|
|
1091
1318
|
if (!skipEnv) {
|
|
1092
1319
|
try {
|
|
1093
|
-
|
|
1320
|
+
const envEntries = {};
|
|
1321
|
+
if (input.profile) {
|
|
1322
|
+
envEntries[profileKey(input.profile, "PATH")] = target;
|
|
1323
|
+
envEntries[profileKey(input.profile, "LANG")] = canonicalLang;
|
|
1324
|
+
envEntries["WIKI_DEFAULT"] = input.profile;
|
|
1325
|
+
} else {
|
|
1326
|
+
envEntries["WIKI_PATH"] = target;
|
|
1327
|
+
envEntries["WIKI_LANG"] = canonicalLang;
|
|
1328
|
+
}
|
|
1329
|
+
await writeDotenv(envPath, envEntries, existingEnvRaw);
|
|
1094
1330
|
envWritten = envPath;
|
|
1095
1331
|
} catch (e) {
|
|
1096
1332
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: envPath, message: String(e) }) };
|
|
1097
1333
|
}
|
|
1098
1334
|
}
|
|
1099
1335
|
const importedFromHermes = pathRes.source === "hermes-dotenv" && !swDotenvHadPath;
|
|
1336
|
+
const humanHint = [
|
|
1337
|
+
`vault: ${target}`,
|
|
1338
|
+
`domain: ${domain}`,
|
|
1339
|
+
`lang: ${canonicalLang}`,
|
|
1340
|
+
`created: ${created.length}, preserved: ${preserved.length}`,
|
|
1341
|
+
`discovered tags: ${discovered_tags}`,
|
|
1342
|
+
skipEnv ? "env: skipped" : `env: ${envWritten}`
|
|
1343
|
+
].join("\n");
|
|
1100
1344
|
return {
|
|
1101
1345
|
exitCode: ExitCode.OK,
|
|
1102
1346
|
result: ok({
|
|
@@ -1109,7 +1353,8 @@ async function runInit(input) {
|
|
|
1109
1353
|
env_written: envWritten,
|
|
1110
1354
|
env_skipped: skipEnv,
|
|
1111
1355
|
imported_from_hermes: importedFromHermes,
|
|
1112
|
-
discovered_tags
|
|
1356
|
+
discovered_tags,
|
|
1357
|
+
humanHint
|
|
1113
1358
|
})
|
|
1114
1359
|
};
|
|
1115
1360
|
}
|
|
@@ -1128,7 +1373,8 @@ function buildSlugMap(pages) {
|
|
|
1128
1373
|
async function runLinks(input) {
|
|
1129
1374
|
const scan = await scanVault(input.vault);
|
|
1130
1375
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1131
|
-
const
|
|
1376
|
+
const allPages = [...scan.data.typedKnowledge, ...scan.data.raw, ...scan.data.workItems, ...scan.data.compound];
|
|
1377
|
+
const slugs = buildSlugMap(allPages);
|
|
1132
1378
|
const broken = [];
|
|
1133
1379
|
for (const p of scan.data.typedKnowledge) {
|
|
1134
1380
|
const text = await readPage(p);
|
|
@@ -1136,7 +1382,7 @@ async function runLinks(input) {
|
|
|
1136
1382
|
const body = split.ok ? split.data.body : text;
|
|
1137
1383
|
const lines = body.split("\n");
|
|
1138
1384
|
for (const slug of extractBodyWikilinks(body)) {
|
|
1139
|
-
const tail = slug.split("/").pop();
|
|
1385
|
+
const tail = slug.split("/").pop().replace(/\.md$/, "");
|
|
1140
1386
|
if (!slugs.has(tail.toLowerCase())) {
|
|
1141
1387
|
const line = lines.findIndex((l) => l.includes(`[[${slug}`));
|
|
1142
1388
|
broken.push({ page: p.relPath, slug, line: line >= 0 ? line + 1 : 0 });
|
|
@@ -1144,9 +1390,10 @@ async function runLinks(input) {
|
|
|
1144
1390
|
}
|
|
1145
1391
|
}
|
|
1146
1392
|
if (broken.length > 0) {
|
|
1147
|
-
return { exitCode: ExitCode.BROKEN_WIKILINKS, result: ok({ broken
|
|
1393
|
+
return { exitCode: ExitCode.BROKEN_WIKILINKS, result: ok({ broken, humanHint: `broken: ${broken.length}
|
|
1394
|
+
${broken.map((b) => ` ${b.page}:[[${b.slug}]] (line ${b.line})`).join("\n")}` }) };
|
|
1148
1395
|
}
|
|
1149
|
-
return { exitCode: ExitCode.OK, result: ok({ broken }) };
|
|
1396
|
+
return { exitCode: ExitCode.OK, result: ok({ broken, humanHint: "no broken wikilinks" }) };
|
|
1150
1397
|
}
|
|
1151
1398
|
|
|
1152
1399
|
// src/commands/tag-audit.ts
|
|
@@ -1173,9 +1420,9 @@ async function runTagAudit(input) {
|
|
|
1173
1420
|
}
|
|
1174
1421
|
}
|
|
1175
1422
|
if (violations.length > 0) {
|
|
1176
|
-
return { exitCode: ExitCode.TAG_NOT_IN_TAXONOMY, result: ok({ violations, taxonomy: tax.data }) };
|
|
1423
|
+
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") }) };
|
|
1177
1424
|
}
|
|
1178
|
-
return { exitCode: ExitCode.OK, result: ok({ violations, taxonomy: tax.data }) };
|
|
1425
|
+
return { exitCode: ExitCode.OK, result: ok({ violations, taxonomy: tax.data, humanHint: "all tags valid" }) };
|
|
1179
1426
|
}
|
|
1180
1427
|
|
|
1181
1428
|
// src/commands/index-check.ts
|
|
@@ -1208,10 +1455,14 @@ async function runIndexCheck(input) {
|
|
|
1208
1455
|
for (const [lower, orig] of indexSlugsLower) {
|
|
1209
1456
|
if (!fileSlugsLower.has(lower)) ghost_entries.push(orig);
|
|
1210
1457
|
}
|
|
1458
|
+
const hintLines = [];
|
|
1459
|
+
if (missing_from_index.length > 0) hintLines.push(`missing from index: ${missing_from_index.length}`, ...missing_from_index.map((p) => ` ${p}`));
|
|
1460
|
+
if (ghost_entries.length > 0) hintLines.push(`ghost entries: ${ghost_entries.length}`, ...ghost_entries.map((g) => ` ${g}`));
|
|
1461
|
+
if (hintLines.length === 0) hintLines.push("index OK");
|
|
1211
1462
|
if (missing_from_index.length > 0 || ghost_entries.length > 0) {
|
|
1212
|
-
return { exitCode: ExitCode.INDEX_INCOMPLETE, result: ok({ missing_from_index, ghost_entries }) };
|
|
1463
|
+
return { exitCode: ExitCode.INDEX_INCOMPLETE, result: ok({ missing_from_index, ghost_entries, humanHint: hintLines.join("\n") }) };
|
|
1213
1464
|
}
|
|
1214
|
-
return { exitCode: ExitCode.OK, result: ok({ missing_from_index, ghost_entries }) };
|
|
1465
|
+
return { exitCode: ExitCode.OK, result: ok({ missing_from_index, ghost_entries, humanHint: hintLines.join("\n") }) };
|
|
1215
1466
|
}
|
|
1216
1467
|
|
|
1217
1468
|
// src/commands/stale.ts
|
|
@@ -1251,8 +1502,8 @@ async function runStale(input) {
|
|
|
1251
1502
|
stale.push({ page: p.relPath, page_updated: updated, newest_source_ingested: newest, gap_days: gap });
|
|
1252
1503
|
}
|
|
1253
1504
|
}
|
|
1254
|
-
if (stale.length > 0) return { exitCode: ExitCode.STALE_PAGE, result: ok({ stale }) };
|
|
1255
|
-
return { exitCode: ExitCode.OK, result: ok({ stale }) };
|
|
1505
|
+
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") }) };
|
|
1506
|
+
return { exitCode: ExitCode.OK, result: ok({ stale, humanHint: "no stale pages" }) };
|
|
1256
1507
|
}
|
|
1257
1508
|
|
|
1258
1509
|
// src/commands/pagesize.ts
|
|
@@ -1267,8 +1518,8 @@ async function runPagesize(input) {
|
|
|
1267
1518
|
const count = body.split("\n").length;
|
|
1268
1519
|
if (count > input.lines) oversized.push({ page: p.relPath, lines: count });
|
|
1269
1520
|
}
|
|
1270
|
-
if (oversized.length > 0) return { exitCode: ExitCode.PAGE_TOO_LARGE, result: ok({ oversized }) };
|
|
1271
|
-
return { exitCode: ExitCode.OK, result: ok({ oversized }) };
|
|
1521
|
+
if (oversized.length > 0) return { exitCode: ExitCode.PAGE_TOO_LARGE, result: ok({ oversized, humanHint: oversized.map((p) => `${p.page}: ${p.lines} lines`).join("\n") }) };
|
|
1522
|
+
return { exitCode: ExitCode.OK, result: ok({ oversized, humanHint: "all pages within size limit" }) };
|
|
1272
1523
|
}
|
|
1273
1524
|
|
|
1274
1525
|
// src/commands/log-rotate.ts
|
|
@@ -1291,12 +1542,12 @@ async function runLogRotate(input) {
|
|
|
1291
1542
|
const matches = [...logText.matchAll(ENTRY_RE)];
|
|
1292
1543
|
const entries = matches.length;
|
|
1293
1544
|
if (entries < input.threshold) {
|
|
1294
|
-
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: false }) };
|
|
1545
|
+
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: false, humanHint: `${entries}/${input.threshold} entries \u2014 no rotation needed` }) };
|
|
1295
1546
|
}
|
|
1296
1547
|
if (!input.apply) {
|
|
1297
1548
|
return {
|
|
1298
1549
|
exitCode: ExitCode.LOG_ROTATE_NEEDED,
|
|
1299
|
-
result: ok({ entries, threshold: input.threshold, rotated: false })
|
|
1550
|
+
result: ok({ entries, threshold: input.threshold, rotated: false, humanHint: `${entries}/${input.threshold} entries \u2014 rotation needed (use --apply)` })
|
|
1300
1551
|
};
|
|
1301
1552
|
}
|
|
1302
1553
|
const newestYear = matches[matches.length - 1][1];
|
|
@@ -1317,13 +1568,143 @@ Chronological action log. Newest entries last. Skill writes append entries; lint
|
|
|
1317
1568
|
} catch (e) {
|
|
1318
1569
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
|
|
1319
1570
|
}
|
|
1320
|
-
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: true, rotated_to: rotatedName }) };
|
|
1571
|
+
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: true, rotated_to: rotatedName, humanHint: `rotated ${entries} entries to ${rotatedName}` }) };
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// src/commands/topic-map-check.ts
|
|
1575
|
+
var DEFAULT_THRESHOLD = 200;
|
|
1576
|
+
async function runTopicMapCheck(input) {
|
|
1577
|
+
const threshold = input.threshold ?? DEFAULT_THRESHOLD;
|
|
1578
|
+
const scan = await scanVault(input.vault);
|
|
1579
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1580
|
+
const page_count = scan.data.typedKnowledge.length;
|
|
1581
|
+
const recommended = page_count >= threshold;
|
|
1582
|
+
return {
|
|
1583
|
+
exitCode: ExitCode.OK,
|
|
1584
|
+
result: ok({
|
|
1585
|
+
recommended,
|
|
1586
|
+
page_count,
|
|
1587
|
+
threshold,
|
|
1588
|
+
humanHint: recommended ? `topic map recommended (${page_count} pages >= ${threshold} threshold)` : `topic map not needed (${page_count} pages < ${threshold} threshold)`
|
|
1589
|
+
})
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// src/commands/index-link-format.ts
|
|
1594
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
1595
|
+
import { join as join12 } from "path";
|
|
1596
|
+
var MD_LINK_RE = /\[[^\[\]]+\]\([^)]+\.md\)/;
|
|
1597
|
+
async function runIndexLinkFormat(input) {
|
|
1598
|
+
let text = "";
|
|
1599
|
+
try {
|
|
1600
|
+
text = await readFile11(join12(input.vault, "index.md"), "utf8");
|
|
1601
|
+
} catch {
|
|
1602
|
+
}
|
|
1603
|
+
const markdown_links = [];
|
|
1604
|
+
for (const [i, line] of text.split("\n").entries()) {
|
|
1605
|
+
if (MD_LINK_RE.test(line)) markdown_links.push({ line: i + 1, text: line.trim() });
|
|
1606
|
+
}
|
|
1607
|
+
const humanHint = markdown_links.length === 0 ? "all index links use wikilink format" : `markdown links found: ${markdown_links.length}
|
|
1608
|
+
${markdown_links.map((l) => ` line ${l.line}: ${l.text}`).join("\n")}`;
|
|
1609
|
+
return { exitCode: ExitCode.OK, result: ok({ markdown_links, humanHint }) };
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// src/commands/dedup.ts
|
|
1613
|
+
import { readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
1614
|
+
import { join as join13 } from "path";
|
|
1615
|
+
async function runDedup(input) {
|
|
1616
|
+
const scan = await scanVault(input.vault);
|
|
1617
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1618
|
+
const hashMap = /* @__PURE__ */ new Map();
|
|
1619
|
+
let totalFiles = 0;
|
|
1620
|
+
for (const raw of scan.data.raw) {
|
|
1621
|
+
const fm = extractFrontmatter(await readPage(raw));
|
|
1622
|
+
if (!fm.ok) continue;
|
|
1623
|
+
const sha = typeof fm.data.sha256 === "string" ? fm.data.sha256 : null;
|
|
1624
|
+
if (!sha || sha.length !== 64) continue;
|
|
1625
|
+
totalFiles++;
|
|
1626
|
+
const existing = hashMap.get(sha);
|
|
1627
|
+
if (existing) existing.push(raw.relPath);
|
|
1628
|
+
else hashMap.set(sha, [raw.relPath]);
|
|
1629
|
+
}
|
|
1630
|
+
const duplicates = [...hashMap.entries()].filter(([, files]) => files.length > 1).map(([sha256, files]) => ({ sha256, files }));
|
|
1631
|
+
const rewired = [];
|
|
1632
|
+
const removed = [];
|
|
1633
|
+
if (input.apply && duplicates.length > 0) {
|
|
1634
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
1635
|
+
for (const group of duplicates) {
|
|
1636
|
+
const canonical = group.files[0];
|
|
1637
|
+
for (let i = 1; i < group.files.length; i++) {
|
|
1638
|
+
replacements.set(group.files[i], canonical);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
for (const page of scan.data.typedKnowledge) {
|
|
1642
|
+
const text = readFileSync(join13(input.vault, page.relPath), "utf-8");
|
|
1643
|
+
let updated = text;
|
|
1644
|
+
let changed = false;
|
|
1645
|
+
for (const [oldPath, newPath] of replacements) {
|
|
1646
|
+
const oldMarker = `^[${oldPath}]`;
|
|
1647
|
+
const newMarker = `^[${newPath}]`;
|
|
1648
|
+
if (updated.includes(oldMarker)) {
|
|
1649
|
+
updated = updated.replaceAll(oldMarker, newMarker);
|
|
1650
|
+
changed = true;
|
|
1651
|
+
}
|
|
1652
|
+
const oldFm = `- "^[${oldPath}]"`;
|
|
1653
|
+
const newFm = `- "^[${newPath}]"`;
|
|
1654
|
+
if (updated.includes(oldFm)) {
|
|
1655
|
+
updated = updated.replaceAll(oldFm, newFm);
|
|
1656
|
+
changed = true;
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
if (changed) {
|
|
1660
|
+
writeFileSync(join13(input.vault, page.relPath), updated);
|
|
1661
|
+
rewired.push(page.relPath);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
for (const [oldPath] of replacements) {
|
|
1665
|
+
const fullPath = join13(input.vault, oldPath);
|
|
1666
|
+
try {
|
|
1667
|
+
unlinkSync(fullPath);
|
|
1668
|
+
removed.push(oldPath);
|
|
1669
|
+
} catch {
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
const exitCode = duplicates.length > 0 ? input.apply ? ExitCode.DEDUP_APPLIED : ExitCode.RAW_DEDUP_DETECTED : ExitCode.OK;
|
|
1674
|
+
const hintLines = [`scanned: ${totalFiles} raw files`];
|
|
1675
|
+
if (duplicates.length > 0) {
|
|
1676
|
+
hintLines.push(`duplicates: ${duplicates.length}`);
|
|
1677
|
+
for (const d of duplicates) hintLines.push(` ${d.sha256.slice(0, 12)}... \u2192 ${d.files.join(", ")}`);
|
|
1678
|
+
if (input.apply) {
|
|
1679
|
+
hintLines.push(`rewired: ${rewired.length} pages`);
|
|
1680
|
+
hintLines.push(`removed: ${removed.length} raw files`);
|
|
1681
|
+
}
|
|
1682
|
+
} else {
|
|
1683
|
+
hintLines.push("0 duplicates");
|
|
1684
|
+
}
|
|
1685
|
+
return {
|
|
1686
|
+
exitCode,
|
|
1687
|
+
result: ok({ scanned: totalFiles, duplicates, rewired, removed, humanHint: hintLines.join("\n") })
|
|
1688
|
+
};
|
|
1321
1689
|
}
|
|
1322
1690
|
|
|
1323
1691
|
// src/commands/lint.ts
|
|
1324
|
-
var
|
|
1325
|
-
var
|
|
1326
|
-
|
|
1692
|
+
var STRUCT_MIN_BODY_LINES = 60;
|
|
1693
|
+
var STRUCT_MIN_SECTIONS = 3;
|
|
1694
|
+
function hasDuplicateFrontmatter(body) {
|
|
1695
|
+
if (/^---\r?\n/.test(body)) return true;
|
|
1696
|
+
const lines = body.split(/\r?\n/);
|
|
1697
|
+
const limit = Math.min(lines.length, 20);
|
|
1698
|
+
let seenYamlKey = false;
|
|
1699
|
+
for (let i = 0; i < limit; i++) {
|
|
1700
|
+
if (/^\w[\w-]*:/.test(lines[i].trim())) seenYamlKey = true;
|
|
1701
|
+
if (seenYamlKey && lines[i].trim() === "---") return true;
|
|
1702
|
+
}
|
|
1703
|
+
return false;
|
|
1704
|
+
}
|
|
1705
|
+
var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_dedup", "tag_not_in_taxonomy"];
|
|
1706
|
+
var WARNING_ORDER = ["index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "orphans", "legacy_citation_style", "orphaned_citations", "duplicate_frontmatter", "missing_overview"];
|
|
1707
|
+
var INFO_ORDER = ["bridges", "page_structure", "topic_map_recommended", "frontmatter_wikilink"];
|
|
1327
1708
|
async function runLint(input) {
|
|
1328
1709
|
const buckets = {};
|
|
1329
1710
|
const links = await runLinks({ vault: input.vault });
|
|
@@ -1343,6 +1724,10 @@ async function runLint(input) {
|
|
|
1343
1724
|
ghost_entries: idx.result.data.ghost_entries
|
|
1344
1725
|
}];
|
|
1345
1726
|
}
|
|
1727
|
+
const linkFmt = await runIndexLinkFormat({ vault: input.vault });
|
|
1728
|
+
if (linkFmt.result.ok && linkFmt.result.data.markdown_links.length > 0) {
|
|
1729
|
+
buckets.index_link_format = linkFmt.result.data.markdown_links;
|
|
1730
|
+
}
|
|
1346
1731
|
const stale = await runStale({ vault: input.vault, days: input.days });
|
|
1347
1732
|
if (stale.result.ok && stale.result.data.stale.length > 0) buckets.stale_page = stale.result.data.stale;
|
|
1348
1733
|
const pagesize = await runPagesize({ vault: input.vault, lines: input.lines });
|
|
@@ -1356,6 +1741,60 @@ async function runLint(input) {
|
|
|
1356
1741
|
if (orphans.result.data.orphans.length > 0) buckets.orphans = orphans.result.data.orphans;
|
|
1357
1742
|
if (orphans.result.data.bridges.length > 0) buckets.bridges = orphans.result.data.bridges;
|
|
1358
1743
|
}
|
|
1744
|
+
const topicMap = await runTopicMapCheck({ vault: input.vault });
|
|
1745
|
+
if (topicMap.result.ok && topicMap.result.data.recommended) {
|
|
1746
|
+
buckets.topic_map_recommended = [{ page_count: topicMap.result.data.page_count, threshold: topicMap.result.data.threshold }];
|
|
1747
|
+
}
|
|
1748
|
+
const dedup = await runDedup({ vault: input.vault });
|
|
1749
|
+
if (dedup.result.ok && dedup.result.data.duplicates.length > 0) buckets.raw_dedup = dedup.result.data.duplicates;
|
|
1750
|
+
const scan = await scanVault(input.vault);
|
|
1751
|
+
const allPages = scan.ok ? [...scan.data.typedKnowledge, ...scan.data.raw, ...scan.data.workItems, ...scan.data.compound] : [];
|
|
1752
|
+
const slugs = scan.ok ? buildSlugMap(allPages) : /* @__PURE__ */ new Map();
|
|
1753
|
+
if (scan.ok) {
|
|
1754
|
+
const legacyPages = [];
|
|
1755
|
+
const orphanedPages = [];
|
|
1756
|
+
const structFlags = [];
|
|
1757
|
+
const dupFrontmatter = [];
|
|
1758
|
+
const noOverview = [];
|
|
1759
|
+
const fmWikilinkFlags = [];
|
|
1760
|
+
for (const page of scan.data.typedKnowledge) {
|
|
1761
|
+
const text = await readPage(page);
|
|
1762
|
+
const split = splitFrontmatter(text);
|
|
1763
|
+
if (!split.ok) continue;
|
|
1764
|
+
const body = split.data.body;
|
|
1765
|
+
const rawFm = split.data.rawFrontmatter;
|
|
1766
|
+
if (hasDuplicateFrontmatter(body)) dupFrontmatter.push(page.relPath);
|
|
1767
|
+
if (isLegacyCitationStyle(body)) legacyPages.push(page.relPath);
|
|
1768
|
+
if (hasOrphanedCitations(body)) orphanedPages.push(page.relPath);
|
|
1769
|
+
const fmLinks = rawFm.match(/\[\[([^\[\]|]+)(?:\|[^\[\]]*)?\]\]/g) ?? [];
|
|
1770
|
+
for (const link of fmLinks) {
|
|
1771
|
+
const target = link.replace(/^\[\[/, "").replace(/(?:\|[^\[\]]*)?\]\]$/, "").trim();
|
|
1772
|
+
const tail = target.split("/").pop().replace(/\.md$/, "");
|
|
1773
|
+
if (!slugs.has(tail.toLowerCase())) {
|
|
1774
|
+
fmWikilinkFlags.push(`${page.relPath}: [[${target}]] does not resolve`);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
const bodyLines = body.split("\n").filter((l) => l.trim().length > 0).length;
|
|
1778
|
+
const hasOverview = /^## Overview/m.test(body);
|
|
1779
|
+
if (!hasOverview) noOverview.push(page.relPath);
|
|
1780
|
+
if (bodyLines < STRUCT_MIN_BODY_LINES) {
|
|
1781
|
+
const hasRelated = /^## (Related|Relationships)/m.test(body);
|
|
1782
|
+
const sectionCount = (body.match(/^## /gm) ?? []).length;
|
|
1783
|
+
if (!hasRelated || sectionCount < STRUCT_MIN_SECTIONS) {
|
|
1784
|
+
const reasons = [];
|
|
1785
|
+
if (!hasRelated) reasons.push("no Related or Relationships");
|
|
1786
|
+
if (sectionCount < STRUCT_MIN_SECTIONS) reasons.push(`only ${sectionCount} sections`);
|
|
1787
|
+
structFlags.push(`${page.relPath}: ${bodyLines} lines, ${reasons.join(", ")}`);
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
if (legacyPages.length > 0) buckets.legacy_citation_style = legacyPages;
|
|
1792
|
+
if (orphanedPages.length > 0) buckets.orphaned_citations = orphanedPages;
|
|
1793
|
+
if (structFlags.length > 0) buckets.page_structure = structFlags;
|
|
1794
|
+
if (dupFrontmatter.length > 0) buckets.duplicate_frontmatter = dupFrontmatter;
|
|
1795
|
+
if (noOverview.length > 0) buckets.missing_overview = noOverview;
|
|
1796
|
+
if (fmWikilinkFlags.length > 0) buckets.frontmatter_wikilink = fmWikilinkFlags;
|
|
1797
|
+
}
|
|
1359
1798
|
const errorOut = ERROR_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
1360
1799
|
const warningOut = WARNING_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
1361
1800
|
const infoOut = INFO_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
@@ -1367,25 +1806,35 @@ async function runLint(input) {
|
|
|
1367
1806
|
let exitCode = ExitCode.OK;
|
|
1368
1807
|
if (summary.errors > 0) exitCode = ExitCode.LINT_HAS_ERRORS;
|
|
1369
1808
|
else if (summary.warnings > 0 || summary.info > 0) exitCode = ExitCode.LINT_HAS_WARNINGS;
|
|
1809
|
+
const hintLines = [];
|
|
1810
|
+
if (summary.errors > 0) hintLines.push(`errors: ${summary.errors}`);
|
|
1811
|
+
if (summary.warnings > 0) hintLines.push(`warnings: ${summary.warnings}`);
|
|
1812
|
+
if (summary.info > 0) hintLines.push(`info: ${summary.info}`);
|
|
1813
|
+
const allBuckets = [...errorOut, ...warningOut, ...infoOut];
|
|
1814
|
+
for (const b of allBuckets) {
|
|
1815
|
+
hintLines.push(` ${b.kind}: ${b.items.length}`);
|
|
1816
|
+
}
|
|
1817
|
+
if (hintLines.length === 0) hintLines.push("0 errors, 0 warnings, 0 info");
|
|
1370
1818
|
return {
|
|
1371
1819
|
exitCode,
|
|
1372
1820
|
result: ok({
|
|
1373
1821
|
vault: { path: input.vault, source: input.source ?? "resolved" },
|
|
1374
1822
|
summary,
|
|
1375
|
-
by_severity: { error: errorOut, warning: warningOut, info: infoOut }
|
|
1823
|
+
by_severity: { error: errorOut, warning: warningOut, info: infoOut },
|
|
1824
|
+
humanHint: hintLines.join("\n")
|
|
1376
1825
|
})
|
|
1377
1826
|
};
|
|
1378
1827
|
}
|
|
1379
1828
|
|
|
1380
1829
|
// src/commands/config.ts
|
|
1381
|
-
import { readFile as
|
|
1830
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
1382
1831
|
import { existsSync } from "fs";
|
|
1383
|
-
import { join as
|
|
1832
|
+
import { join as join14 } from "path";
|
|
1384
1833
|
function validateKey(key) {
|
|
1385
|
-
return CONFIG_KEYS.includes(key);
|
|
1834
|
+
return CONFIG_KEYS.includes(key) || isValidWikiProfileKey(key);
|
|
1386
1835
|
}
|
|
1387
1836
|
function configPath(home) {
|
|
1388
|
-
return
|
|
1837
|
+
return join14(home, ".skillwiki", ".env");
|
|
1389
1838
|
}
|
|
1390
1839
|
async function runConfigGet(input) {
|
|
1391
1840
|
if (!validateKey(input.key)) {
|
|
@@ -1403,7 +1852,7 @@ async function runConfigSet(input) {
|
|
|
1403
1852
|
try {
|
|
1404
1853
|
let originalContent;
|
|
1405
1854
|
try {
|
|
1406
|
-
originalContent = await
|
|
1855
|
+
originalContent = await readFile12(filePath, "utf8");
|
|
1407
1856
|
} catch {
|
|
1408
1857
|
}
|
|
1409
1858
|
const existing = originalContent !== void 0 ? parseDotenvText(originalContent) : {};
|
|
@@ -1417,7 +1866,21 @@ async function runConfigSet(input) {
|
|
|
1417
1866
|
async function runConfigList(input) {
|
|
1418
1867
|
const map = await parseDotenvFile(configPath(input.home));
|
|
1419
1868
|
const entries = Object.entries(map).map(([key, value]) => ({ key, value: value ?? "" }));
|
|
1420
|
-
|
|
1869
|
+
let profiles;
|
|
1870
|
+
if (input.profiles) {
|
|
1871
|
+
const defaultProfile = map["WIKI_DEFAULT"];
|
|
1872
|
+
profiles = [];
|
|
1873
|
+
for (const key of Object.keys(map)) {
|
|
1874
|
+
const m = key.match(/^WIKI_([A-Z][A-Z0-9_]{0,31})_PATH$/);
|
|
1875
|
+
if (m && key !== "WIKI_PATH") {
|
|
1876
|
+
const name = m[1].toLowerCase().replace(/_/g, "-");
|
|
1877
|
+
profiles.push({ name, path: map[key] ?? "", isDefault: name === defaultProfile });
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
profiles.sort((a, b) => a.name.localeCompare(b.name));
|
|
1881
|
+
}
|
|
1882
|
+
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");
|
|
1883
|
+
return { exitCode: ExitCode.OK, result: ok({ entries, profiles, humanHint: hint }) };
|
|
1421
1884
|
}
|
|
1422
1885
|
async function runConfigPath(input) {
|
|
1423
1886
|
const filePath = configPath(input.home);
|
|
@@ -1425,9 +1888,72 @@ async function runConfigPath(input) {
|
|
|
1425
1888
|
}
|
|
1426
1889
|
|
|
1427
1890
|
// src/commands/doctor.ts
|
|
1428
|
-
import { existsSync as
|
|
1429
|
-
import { join as
|
|
1891
|
+
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, statSync } from "fs";
|
|
1892
|
+
import { join as join16 } from "path";
|
|
1430
1893
|
import { execSync } from "child_process";
|
|
1894
|
+
|
|
1895
|
+
// src/utils/auto-update.ts
|
|
1896
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync } from "fs";
|
|
1897
|
+
import { join as join15, dirname as dirname6 } from "path";
|
|
1898
|
+
import { spawn } from "child_process";
|
|
1899
|
+
|
|
1900
|
+
// src/utils/update-consts.ts
|
|
1901
|
+
var CACHE_FILENAME = ".update-cache.json";
|
|
1902
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1903
|
+
var ENV_DISABLE_KEY = "NO_UPDATE_NOTIFIER";
|
|
1904
|
+
var CLI_DISABLE_FLAG = "--no-update-notifier";
|
|
1905
|
+
|
|
1906
|
+
// src/utils/auto-update.ts
|
|
1907
|
+
function cachePath(home) {
|
|
1908
|
+
return join15(home, ".skillwiki", CACHE_FILENAME);
|
|
1909
|
+
}
|
|
1910
|
+
function readCacheRaw(home) {
|
|
1911
|
+
try {
|
|
1912
|
+
const raw = readFileSync2(cachePath(home), "utf8");
|
|
1913
|
+
return JSON.parse(raw);
|
|
1914
|
+
} catch {
|
|
1915
|
+
return null;
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
function readCache(home) {
|
|
1919
|
+
const cache = readCacheRaw(home);
|
|
1920
|
+
if (!cache) return { cache: null, hasUpdate: false, isStale: true };
|
|
1921
|
+
const isStale = Date.now() - cache.lastCheck >= CHECK_INTERVAL_MS;
|
|
1922
|
+
const hasUpdate = !!cache.latestVersion && semverGt(cache.latestVersion, cache.currentVersion);
|
|
1923
|
+
return { cache, hasUpdate, isStale };
|
|
1924
|
+
}
|
|
1925
|
+
function writeCache(home, cache) {
|
|
1926
|
+
const p = cachePath(home);
|
|
1927
|
+
mkdirSync(dirname6(p), { recursive: true });
|
|
1928
|
+
writeFileSync2(p, JSON.stringify(cache, null, 2));
|
|
1929
|
+
}
|
|
1930
|
+
function latestFromCache(home, currentVersion) {
|
|
1931
|
+
const { cache } = readCache(home);
|
|
1932
|
+
if (!cache || !cache.latestVersion) return { hasUpdate: false, latest: null };
|
|
1933
|
+
return {
|
|
1934
|
+
hasUpdate: semverGt(cache.latestVersion, currentVersion),
|
|
1935
|
+
latest: cache.latestVersion
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
function isDisabled() {
|
|
1939
|
+
return !!(process.env[ENV_DISABLE_KEY] || process.env.NODE_ENV === "test" || process.argv.includes(CLI_DISABLE_FLAG));
|
|
1940
|
+
}
|
|
1941
|
+
function triggerAutoUpdate(home, currentVersion) {
|
|
1942
|
+
if (isDisabled()) return;
|
|
1943
|
+
const { isStale } = readCache(home);
|
|
1944
|
+
if (!isStale) return;
|
|
1945
|
+
const bgScript = new URL("../auto-update-bg.js", import.meta.url).pathname;
|
|
1946
|
+
if (!existsSync2(bgScript)) return;
|
|
1947
|
+
const child = spawn(process.execPath, [bgScript, home, currentVersion], {
|
|
1948
|
+
detached: true,
|
|
1949
|
+
stdio: "ignore"
|
|
1950
|
+
});
|
|
1951
|
+
child.on("error", () => {
|
|
1952
|
+
});
|
|
1953
|
+
child.unref();
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
// src/commands/doctor.ts
|
|
1431
1957
|
function check(status, id, label, detail) {
|
|
1432
1958
|
return { id, label, status, detail };
|
|
1433
1959
|
}
|
|
@@ -1454,7 +1980,7 @@ function checkCliOnPath(argv) {
|
|
|
1454
1980
|
}
|
|
1455
1981
|
async function checkConfigFile(home) {
|
|
1456
1982
|
const cfgPath = configPath(home);
|
|
1457
|
-
if (!
|
|
1983
|
+
if (!existsSync3(cfgPath)) {
|
|
1458
1984
|
return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
|
|
1459
1985
|
}
|
|
1460
1986
|
try {
|
|
@@ -1469,7 +1995,7 @@ function checkWikiPathExists(resolvedPath) {
|
|
|
1469
1995
|
if (resolvedPath === void 0) {
|
|
1470
1996
|
return check("error", "wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
1471
1997
|
}
|
|
1472
|
-
if (
|
|
1998
|
+
if (existsSync3(resolvedPath) && statSync(resolvedPath).isDirectory()) {
|
|
1473
1999
|
return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
|
|
1474
2000
|
}
|
|
1475
2001
|
return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
|
|
@@ -1478,22 +2004,22 @@ function checkVaultStructure(resolvedPath) {
|
|
|
1478
2004
|
if (resolvedPath === void 0) {
|
|
1479
2005
|
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
1480
2006
|
}
|
|
1481
|
-
if (!
|
|
2007
|
+
if (!existsSync3(resolvedPath)) {
|
|
1482
2008
|
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
|
|
1483
2009
|
}
|
|
1484
2010
|
const missing = [];
|
|
1485
|
-
if (!
|
|
2011
|
+
if (!existsSync3(join16(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
|
|
1486
2012
|
for (const dir of ["raw", "entities", "concepts", "meta"]) {
|
|
1487
|
-
if (!
|
|
2013
|
+
if (!existsSync3(join16(resolvedPath, dir))) missing.push(dir + "/");
|
|
1488
2014
|
}
|
|
1489
2015
|
if (missing.length === 0) {
|
|
1490
2016
|
return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
|
|
1491
2017
|
}
|
|
1492
|
-
return check("
|
|
2018
|
+
return check("warn", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to add CodeWiki structure`);
|
|
1493
2019
|
}
|
|
1494
2020
|
function checkSkillsInstalled(home) {
|
|
1495
|
-
const skillsDir =
|
|
1496
|
-
if (!
|
|
2021
|
+
const skillsDir = join16(home, ".claude", "skills");
|
|
2022
|
+
if (!existsSync3(skillsDir)) {
|
|
1497
2023
|
return check("warn", "skills_installed", "Skills installed", `${skillsDir} not found`);
|
|
1498
2024
|
}
|
|
1499
2025
|
const found = findSkillMd(skillsDir);
|
|
@@ -1502,6 +2028,70 @@ function checkSkillsInstalled(home) {
|
|
|
1502
2028
|
}
|
|
1503
2029
|
return check("warn", "skills_installed", "Skills installed", "No SKILL.md files found in ~/.claude/skills/");
|
|
1504
2030
|
}
|
|
2031
|
+
function checkNpmUpdate(home, currentVersion) {
|
|
2032
|
+
const { hasUpdate, latest } = latestFromCache(home, currentVersion);
|
|
2033
|
+
if (!latest) {
|
|
2034
|
+
return check("pass", "npm_update", "npm CLI version", `v${currentVersion} (no cache yet)`);
|
|
2035
|
+
}
|
|
2036
|
+
if (hasUpdate) {
|
|
2037
|
+
return check("warn", "npm_update", "npm CLI version", `v${currentVersion} \u2014 update available: v${latest}. Run \`skillwiki update\`.`);
|
|
2038
|
+
}
|
|
2039
|
+
return check("pass", "npm_update", "npm CLI version", `v${currentVersion} (latest: v${latest})`);
|
|
2040
|
+
}
|
|
2041
|
+
function checkPluginVersionDrift(home, currentVersion) {
|
|
2042
|
+
const pluginJsonPath = join16(home, ".claude", "plugins", "cache", "llm-wiki", "plugin.json");
|
|
2043
|
+
if (!existsSync3(pluginJsonPath)) {
|
|
2044
|
+
return check("pass", "plugin_version_drift", "Plugin/CLI version", "Plugin cache not found \u2014 plugin not installed");
|
|
2045
|
+
}
|
|
2046
|
+
try {
|
|
2047
|
+
const content = readFileSync3(pluginJsonPath, { encoding: "utf8" });
|
|
2048
|
+
const pluginData = JSON.parse(content);
|
|
2049
|
+
const pluginVersion = pluginData.version;
|
|
2050
|
+
if (!pluginVersion) {
|
|
2051
|
+
return check("pass", "plugin_version_drift", "Plugin/CLI version", "Plugin version not found in cache");
|
|
2052
|
+
}
|
|
2053
|
+
if (pluginVersion === currentVersion) {
|
|
2054
|
+
return check("pass", "plugin_version_drift", "Plugin/CLI version", `Both at v${currentVersion}`);
|
|
2055
|
+
}
|
|
2056
|
+
const updateCmd = semverGt(pluginVersion, currentVersion) ? "npm install -g skillwiki@beta" : "claude plugin update skillwiki@llm-wiki";
|
|
2057
|
+
return check(
|
|
2058
|
+
"warn",
|
|
2059
|
+
"plugin_version_drift",
|
|
2060
|
+
"Plugin/CLI version",
|
|
2061
|
+
`Plugin v${pluginVersion} \u2260 CLI v${currentVersion} \u2014 run \`${updateCmd}\``
|
|
2062
|
+
);
|
|
2063
|
+
} catch {
|
|
2064
|
+
return check("pass", "plugin_version_drift", "Plugin/CLI version", "Could not read plugin cache");
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
async function checkProfiles(home) {
|
|
2068
|
+
const map = await parseDotenvFile(configPath(home));
|
|
2069
|
+
const profiles = [];
|
|
2070
|
+
for (const key of Object.keys(map)) {
|
|
2071
|
+
if (key.startsWith("WIKI_") && key.endsWith("_PATH") && key !== "WIKI_PATH") {
|
|
2072
|
+
const name = key.slice(5, -5).toLowerCase().replace(/_/g, "-");
|
|
2073
|
+
profiles.push(name);
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
if (profiles.length === 0) {
|
|
2077
|
+
return check("pass", "wiki_profiles", "Wiki profiles", "No named profiles configured");
|
|
2078
|
+
}
|
|
2079
|
+
const defaultProfile = map["WIKI_DEFAULT"] ?? "(none)";
|
|
2080
|
+
return check(
|
|
2081
|
+
"pass",
|
|
2082
|
+
"wiki_profiles",
|
|
2083
|
+
"Wiki profiles",
|
|
2084
|
+
`${profiles.length} profile(s): ${profiles.join(", ")}; default: ${defaultProfile}`
|
|
2085
|
+
);
|
|
2086
|
+
}
|
|
2087
|
+
async function checkProjectLocalOverride(cwd) {
|
|
2088
|
+
const dir = cwd ?? process.cwd();
|
|
2089
|
+
const envPath = join16(dir, ".skillwiki", ".env");
|
|
2090
|
+
if (existsSync3(envPath)) {
|
|
2091
|
+
return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
|
|
2092
|
+
}
|
|
2093
|
+
return check("pass", "project_local", "Project-local config", "None");
|
|
2094
|
+
}
|
|
1505
2095
|
function findSkillMd(dir) {
|
|
1506
2096
|
const results = [];
|
|
1507
2097
|
let entries;
|
|
@@ -1512,9 +2102,9 @@ function findSkillMd(dir) {
|
|
|
1512
2102
|
}
|
|
1513
2103
|
for (const entry of entries) {
|
|
1514
2104
|
if (entry.isFile() && entry.name === "SKILL.md") {
|
|
1515
|
-
results.push(
|
|
2105
|
+
results.push(join16(dir, entry.name));
|
|
1516
2106
|
} else if (entry.isDirectory()) {
|
|
1517
|
-
results.push(...findSkillMd(
|
|
2107
|
+
results.push(...findSkillMd(join16(dir, entry.name)));
|
|
1518
2108
|
}
|
|
1519
2109
|
}
|
|
1520
2110
|
return results;
|
|
@@ -1524,6 +2114,8 @@ async function runDoctor(input) {
|
|
|
1524
2114
|
checks.push(checkNodeVersion());
|
|
1525
2115
|
checks.push(checkCliOnPath(input.argv));
|
|
1526
2116
|
checks.push(await checkConfigFile(input.home));
|
|
2117
|
+
checks.push(await checkProfiles(input.home));
|
|
2118
|
+
checks.push(await checkProjectLocalOverride(input.cwd));
|
|
1527
2119
|
const resolved = await resolveRuntimePath({ flag: void 0, envValue: input.envValue, home: input.home });
|
|
1528
2120
|
if (resolved.ok) {
|
|
1529
2121
|
checks.push(check("pass", "wiki_path_set", "WIKI_PATH configured", `Resolved via ${resolved.data.source}: ${resolved.data.path}`));
|
|
@@ -1534,6 +2126,8 @@ async function runDoctor(input) {
|
|
|
1534
2126
|
checks.push(checkWikiPathExists(resolvedPath));
|
|
1535
2127
|
checks.push(checkVaultStructure(resolvedPath));
|
|
1536
2128
|
checks.push(checkSkillsInstalled(input.home));
|
|
2129
|
+
checks.push(checkNpmUpdate(input.home, input.currentVersion));
|
|
2130
|
+
checks.push(checkPluginVersionDrift(input.home, input.currentVersion));
|
|
1537
2131
|
const summary = {
|
|
1538
2132
|
pass: checks.filter((c) => c.status === "pass").length,
|
|
1539
2133
|
warn: checks.filter((c) => c.status === "warn").length,
|
|
@@ -1552,8 +2146,419 @@ async function runDoctor(input) {
|
|
|
1552
2146
|
return { exitCode, result: ok({ checks, summary, humanHint }) };
|
|
1553
2147
|
}
|
|
1554
2148
|
|
|
2149
|
+
// src/commands/archive.ts
|
|
2150
|
+
import { rename as rename3, mkdir as mkdir5, readFile as readFile13, writeFile as writeFile6 } from "fs/promises";
|
|
2151
|
+
import { join as join17, dirname as dirname7 } from "path";
|
|
2152
|
+
async function runArchive(input) {
|
|
2153
|
+
const scan = await scanVault(input.vault);
|
|
2154
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
2155
|
+
let relPath;
|
|
2156
|
+
if (input.page.includes("/")) {
|
|
2157
|
+
relPath = scan.data.typedKnowledge.find((p) => p.relPath === input.page)?.relPath;
|
|
2158
|
+
} else {
|
|
2159
|
+
relPath = scan.data.typedKnowledge.find((p) => p.relPath.replace(/\.md$/, "").split("/").pop() === input.page)?.relPath;
|
|
2160
|
+
}
|
|
2161
|
+
if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
|
|
2162
|
+
if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
|
|
2163
|
+
const archivePath = join17("_archive", relPath);
|
|
2164
|
+
await mkdir5(dirname7(join17(input.vault, archivePath)), { recursive: true });
|
|
2165
|
+
let indexUpdated = false;
|
|
2166
|
+
const indexPath = join17(input.vault, "index.md");
|
|
2167
|
+
try {
|
|
2168
|
+
const idx = await readFile13(indexPath, "utf8");
|
|
2169
|
+
const slug = relPath.replace(/\.md$/, "").split("/").pop();
|
|
2170
|
+
const originalLines = idx.split("\n");
|
|
2171
|
+
const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
|
|
2172
|
+
if (filtered.length !== originalLines.length) {
|
|
2173
|
+
await writeFile6(indexPath, filtered.join("\n"), "utf8");
|
|
2174
|
+
indexUpdated = true;
|
|
2175
|
+
}
|
|
2176
|
+
} catch (e) {
|
|
2177
|
+
if (e?.code !== "ENOENT") throw e;
|
|
2178
|
+
}
|
|
2179
|
+
await rename3(join17(input.vault, relPath), join17(input.vault, archivePath));
|
|
2180
|
+
return { exitCode: ExitCode.OK, result: ok({ archived_from: relPath, archived_to: archivePath, index_updated: indexUpdated, humanHint: `${relPath} -> ${archivePath}${indexUpdated ? " (index updated)" : ""}` }) };
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
// src/commands/drift.ts
|
|
2184
|
+
import { createHash as createHash2 } from "crypto";
|
|
2185
|
+
import { writeFile as writeFile7 } from "fs/promises";
|
|
2186
|
+
|
|
2187
|
+
// src/utils/fetch.ts
|
|
2188
|
+
async function controlledFetch(url, opts) {
|
|
2189
|
+
let current = url;
|
|
2190
|
+
for (let hop = 0; hop <= opts.maxRedirects; hop++) {
|
|
2191
|
+
const guard = runFetchGuardSync({ url: current });
|
|
2192
|
+
if (!guard.result.ok) return guard.result;
|
|
2193
|
+
const ctrl = new AbortController();
|
|
2194
|
+
const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs);
|
|
2195
|
+
let res;
|
|
2196
|
+
try {
|
|
2197
|
+
res = await fetch(current, { redirect: "manual", signal: ctrl.signal });
|
|
2198
|
+
} catch (e) {
|
|
2199
|
+
clearTimeout(timer);
|
|
2200
|
+
if (e?.name === "AbortError") return err("FETCH_TIMEOUT", { url: current });
|
|
2201
|
+
return err("FETCH_FAILED", { message: String(e) });
|
|
2202
|
+
}
|
|
2203
|
+
clearTimeout(timer);
|
|
2204
|
+
if (res.status >= 300 && res.status < 400) {
|
|
2205
|
+
const loc = res.headers.get("location");
|
|
2206
|
+
if (!loc) return err("FETCH_FAILED", { reason: "redirect without Location" });
|
|
2207
|
+
current = new URL(loc, current).toString();
|
|
2208
|
+
continue;
|
|
2209
|
+
}
|
|
2210
|
+
const declared = Number(res.headers.get("content-length") ?? "0");
|
|
2211
|
+
if (declared > opts.maxBytes) return err("FETCH_TOO_LARGE", { declared, limit: opts.maxBytes });
|
|
2212
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
2213
|
+
if (buf.byteLength > opts.maxBytes) return err("FETCH_TOO_LARGE", { actual: buf.byteLength, limit: opts.maxBytes });
|
|
2214
|
+
return ok({ url: current, status: res.status, body: new TextDecoder().decode(buf), bytes: buf.byteLength });
|
|
2215
|
+
}
|
|
2216
|
+
return err("FETCH_FAILED", { reason: "too many redirects", limit: opts.maxRedirects });
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
// src/commands/drift.ts
|
|
2220
|
+
var FETCH_OPTS = { timeoutMs: 1e4, maxBytes: 5e6, maxRedirects: 5 };
|
|
2221
|
+
async function runDrift(input) {
|
|
2222
|
+
const doFetch = input.fetchFn ?? controlledFetch;
|
|
2223
|
+
const scan = await scanVault(input.vault);
|
|
2224
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
2225
|
+
const results = [];
|
|
2226
|
+
for (const raw of scan.data.raw) {
|
|
2227
|
+
const text = await readPage(raw);
|
|
2228
|
+
const split = splitFrontmatter(text);
|
|
2229
|
+
if (!split.ok) continue;
|
|
2230
|
+
const { rawFrontmatter, body } = split.data;
|
|
2231
|
+
const sourceUrlMatch = rawFrontmatter.match(/^source_url:\s*(.+)$/m);
|
|
2232
|
+
const storedHashMatch = rawFrontmatter.match(/^sha256:\s*([a-f0-9]+)$/m);
|
|
2233
|
+
if (!sourceUrlMatch || !storedHashMatch) continue;
|
|
2234
|
+
const sourceUrl = sourceUrlMatch[1].trim();
|
|
2235
|
+
const storedHash = storedHashMatch[1];
|
|
2236
|
+
const resp = await doFetch(sourceUrl, FETCH_OPTS);
|
|
2237
|
+
if (!resp.ok) {
|
|
2238
|
+
results.push({
|
|
2239
|
+
raw_path: raw.relPath,
|
|
2240
|
+
source_url: sourceUrl,
|
|
2241
|
+
stored_sha256: storedHash,
|
|
2242
|
+
current_sha256: null,
|
|
2243
|
+
status: "fetch_failed",
|
|
2244
|
+
fetch_error: resp.error
|
|
2245
|
+
});
|
|
2246
|
+
continue;
|
|
2247
|
+
}
|
|
2248
|
+
const currentHash = createHash2("sha256").update(Buffer.from(resp.data.body, "utf8")).digest("hex");
|
|
2249
|
+
const drifted2 = currentHash !== storedHash;
|
|
2250
|
+
if (drifted2 && input.apply) {
|
|
2251
|
+
const newFm = rawFrontmatter.replace(/^sha256:\s*[a-f0-9]+$/m, `sha256: ${currentHash}`);
|
|
2252
|
+
const newText = `---
|
|
2253
|
+
${newFm}
|
|
2254
|
+
---
|
|
2255
|
+
${body}`;
|
|
2256
|
+
await writeFile7(raw.absPath, newText, "utf8");
|
|
2257
|
+
results.push({
|
|
2258
|
+
raw_path: raw.relPath,
|
|
2259
|
+
source_url: sourceUrl,
|
|
2260
|
+
stored_sha256: storedHash,
|
|
2261
|
+
current_sha256: currentHash,
|
|
2262
|
+
status: "updated"
|
|
2263
|
+
});
|
|
2264
|
+
} else {
|
|
2265
|
+
results.push({
|
|
2266
|
+
raw_path: raw.relPath,
|
|
2267
|
+
source_url: sourceUrl,
|
|
2268
|
+
stored_sha256: storedHash,
|
|
2269
|
+
current_sha256: currentHash,
|
|
2270
|
+
status: drifted2 ? "drifted" : "unchanged"
|
|
2271
|
+
});
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
const drifted = results.filter((r) => r.status === "drifted");
|
|
2275
|
+
const fetchFailed = results.filter((r) => r.status === "fetch_failed");
|
|
2276
|
+
const updated = results.filter((r) => r.status === "updated");
|
|
2277
|
+
const unchanged = results.filter((r) => r.status === "unchanged").length;
|
|
2278
|
+
const exitCode = drifted.length > 0 ? ExitCode.DRIFT_DETECTED : ExitCode.OK;
|
|
2279
|
+
const hintLines = [`scanned: ${results.length}, unchanged: ${unchanged}`];
|
|
2280
|
+
if (drifted.length > 0) hintLines.push(`drifted: ${drifted.length}`, ...drifted.map((d) => ` ${d.raw_path}`));
|
|
2281
|
+
if (fetchFailed.length > 0) hintLines.push(`fetch_failed: ${fetchFailed.length}`, ...fetchFailed.map((f) => ` ${f.raw_path}: ${f.fetch_error}`));
|
|
2282
|
+
if (updated.length > 0) hintLines.push(`updated: ${updated.length}`, ...updated.map((u) => ` ${u.raw_path}`));
|
|
2283
|
+
return {
|
|
2284
|
+
exitCode,
|
|
2285
|
+
result: ok({ scanned: results.length, drifted, fetch_failed: fetchFailed, updated, unchanged, humanHint: hintLines.join("\n") })
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
// src/commands/migrate-citations.ts
|
|
2290
|
+
import { writeFile as writeFile8 } from "fs/promises";
|
|
2291
|
+
var MARKER_RE2 = /\^\[(raw\/[^\]]+)\]/g;
|
|
2292
|
+
function moveMarkersToParagraphEnd(body) {
|
|
2293
|
+
const lines = body.split("\n");
|
|
2294
|
+
const result = [];
|
|
2295
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2296
|
+
const line = lines[i];
|
|
2297
|
+
if (line.trimStart().startsWith("```")) {
|
|
2298
|
+
result.push(line);
|
|
2299
|
+
if (!line.trimEnd().endsWith("```") || line.trim() === "```") {
|
|
2300
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
2301
|
+
result.push(lines[j]);
|
|
2302
|
+
if (lines[j].trimStart().startsWith("```")) {
|
|
2303
|
+
i = j;
|
|
2304
|
+
break;
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
continue;
|
|
2309
|
+
}
|
|
2310
|
+
if (/^## Sources\b/.test(line.trim())) {
|
|
2311
|
+
result.push(line);
|
|
2312
|
+
continue;
|
|
2313
|
+
}
|
|
2314
|
+
const markers = [...line.matchAll(MARKER_RE2)];
|
|
2315
|
+
if (markers.length === 0) {
|
|
2316
|
+
result.push(line);
|
|
2317
|
+
continue;
|
|
2318
|
+
}
|
|
2319
|
+
const proseOnly = line.replace(MARKER_RE2, "").trim();
|
|
2320
|
+
if (proseOnly.length === 0) {
|
|
2321
|
+
const markerStr = " " + markers.map((m) => m[0]).join(" ");
|
|
2322
|
+
let merged = false;
|
|
2323
|
+
for (let k = result.length - 1; k >= 0; k--) {
|
|
2324
|
+
if (result[k].trim().length > 0) {
|
|
2325
|
+
result[k] = result[k].trimEnd() + markerStr;
|
|
2326
|
+
merged = true;
|
|
2327
|
+
break;
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
if (!merged) result.push(line);
|
|
2331
|
+
continue;
|
|
2332
|
+
}
|
|
2333
|
+
const lastMarkerIdx = line.lastIndexOf("^[raw/");
|
|
2334
|
+
const afterLast = line.slice(lastMarkerIdx).replace(MARKER_RE2, "").trim();
|
|
2335
|
+
const firstMarkerIdx = line.indexOf("^[raw/");
|
|
2336
|
+
const beforeFirst = line.slice(0, firstMarkerIdx).trim();
|
|
2337
|
+
const alreadyAtEnd = afterLast.length === 0 && (beforeFirst.length === 0 || /[.!?]\s*$/.test(beforeFirst));
|
|
2338
|
+
if (alreadyAtEnd) {
|
|
2339
|
+
result.push(line);
|
|
2340
|
+
continue;
|
|
2341
|
+
}
|
|
2342
|
+
let cleaned = line.replace(/\s*\^\[raw\/[^\]]+\]\s*/g, " ").trimEnd();
|
|
2343
|
+
cleaned = cleaned.replace(/ +/g, " ").trimEnd();
|
|
2344
|
+
const markerStrings = markers.map((m) => m[0]);
|
|
2345
|
+
if (cleaned.length > 0 && /[.!?]$/.test(cleaned)) {
|
|
2346
|
+
cleaned += " " + markerStrings.join(" ");
|
|
2347
|
+
} else if (cleaned.length > 0) {
|
|
2348
|
+
cleaned += ". " + markerStrings.join(" ");
|
|
2349
|
+
} else {
|
|
2350
|
+
cleaned = markerStrings.join(" ");
|
|
2351
|
+
}
|
|
2352
|
+
result.push(cleaned);
|
|
2353
|
+
}
|
|
2354
|
+
return result.join("\n");
|
|
2355
|
+
}
|
|
2356
|
+
function buildSourcesFooter(targets) {
|
|
2357
|
+
return "\n## Sources\n" + targets.map((t) => `- ^[${t}]`).join("\n") + "\n";
|
|
2358
|
+
}
|
|
2359
|
+
function reorderSourcesFm(rawFm, targets) {
|
|
2360
|
+
const sourcesLineRe = /^sources:\s*\[([^\]]*)\]\s*$/m;
|
|
2361
|
+
const match = rawFm.match(sourcesLineRe);
|
|
2362
|
+
if (!match) return rawFm;
|
|
2363
|
+
const existing = match[1].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter((s) => s.length > 0);
|
|
2364
|
+
const targetSet = new Set(targets);
|
|
2365
|
+
const reordered = [
|
|
2366
|
+
...targets,
|
|
2367
|
+
...existing.filter((s) => !targetSet.has(s))
|
|
2368
|
+
];
|
|
2369
|
+
const newLine = `sources: [${reordered.join(", ")}]`;
|
|
2370
|
+
return rawFm.replace(sourcesLineRe, newLine);
|
|
2371
|
+
}
|
|
2372
|
+
function removeExistingFooter(body) {
|
|
2373
|
+
const footerRe = /\n## Sources\n[\s\S]*$/;
|
|
2374
|
+
return body.replace(footerRe, "");
|
|
2375
|
+
}
|
|
2376
|
+
async function runMigrateCitations(input) {
|
|
2377
|
+
const scan = await scanVault(input.vault);
|
|
2378
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
2379
|
+
const migrated = [];
|
|
2380
|
+
const skipped = [];
|
|
2381
|
+
let unchanged = 0;
|
|
2382
|
+
for (const page of scan.data.typedKnowledge) {
|
|
2383
|
+
const text = await readPage(page);
|
|
2384
|
+
const split = splitFrontmatter(text);
|
|
2385
|
+
if (!split.ok) continue;
|
|
2386
|
+
const { rawFrontmatter, body } = split.data;
|
|
2387
|
+
const markers = extractCitationMarkers(body);
|
|
2388
|
+
if (markers.length === 0) {
|
|
2389
|
+
unchanged++;
|
|
2390
|
+
continue;
|
|
2391
|
+
}
|
|
2392
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2393
|
+
const uniqueTargets = [];
|
|
2394
|
+
for (const m of markers) {
|
|
2395
|
+
if (!seen.has(m.target)) {
|
|
2396
|
+
seen.add(m.target);
|
|
2397
|
+
uniqueTargets.push(m.target);
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
const bodyWithoutFooter = removeExistingFooter(body);
|
|
2401
|
+
const migratedBody = moveMarkersToParagraphEnd(bodyWithoutFooter);
|
|
2402
|
+
const newFooter = buildSourcesFooter(uniqueTargets);
|
|
2403
|
+
const newFm = reorderSourcesFm(rawFrontmatter, uniqueTargets);
|
|
2404
|
+
const newText = `---
|
|
2405
|
+
${newFm}
|
|
2406
|
+
---
|
|
2407
|
+
${migratedBody}${newFooter}`;
|
|
2408
|
+
if (newText === text) {
|
|
2409
|
+
skipped.push(page.relPath);
|
|
2410
|
+
continue;
|
|
2411
|
+
}
|
|
2412
|
+
if (!input.dryRun) {
|
|
2413
|
+
await writeFile8(page.absPath, newText, "utf8");
|
|
2414
|
+
}
|
|
2415
|
+
migrated.push(page.relPath);
|
|
2416
|
+
}
|
|
2417
|
+
const exitCode = migrated.length > 0 ? ExitCode.MIGRATION_APPLIED : ExitCode.OK;
|
|
2418
|
+
const hintLines = [`scanned: ${migrated.length + skipped.length + unchanged}`];
|
|
2419
|
+
if (migrated.length > 0) hintLines.push(`migrated: ${migrated.length}`);
|
|
2420
|
+
if (skipped.length > 0) hintLines.push(`skipped (already clean): ${skipped.length}`);
|
|
2421
|
+
if (unchanged > 0) hintLines.push(`unchanged (no markers): ${unchanged}`);
|
|
2422
|
+
return {
|
|
2423
|
+
exitCode,
|
|
2424
|
+
result: ok({
|
|
2425
|
+
scanned: migrated.length + skipped.length + unchanged,
|
|
2426
|
+
migrated,
|
|
2427
|
+
skipped,
|
|
2428
|
+
unchanged,
|
|
2429
|
+
humanHint: hintLines.join("\n")
|
|
2430
|
+
})
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
// src/commands/frontmatter-fix.ts
|
|
2435
|
+
import { writeFile as writeFile9 } from "fs/promises";
|
|
2436
|
+
function isoToday() {
|
|
2437
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2438
|
+
}
|
|
2439
|
+
function fixFrontmatter(rawFm) {
|
|
2440
|
+
const additions = [];
|
|
2441
|
+
if (!/^created:/m.test(rawFm)) additions.push(`created: ${isoToday()}`);
|
|
2442
|
+
if (!/^updated:/m.test(rawFm)) additions.push(`updated: ${isoToday()}`);
|
|
2443
|
+
if (!/^tags:/m.test(rawFm)) additions.push("tags: []");
|
|
2444
|
+
if (!/^sources:/m.test(rawFm)) additions.push("sources: []");
|
|
2445
|
+
if (!/^provenance:/m.test(rawFm)) additions.push("provenance: research");
|
|
2446
|
+
if (additions.length === 0) return rawFm;
|
|
2447
|
+
return rawFm.trimEnd() + "\n" + additions.join("\n") + "\n";
|
|
2448
|
+
}
|
|
2449
|
+
function removeOrphanTagsLines(body) {
|
|
2450
|
+
return body.split("\n").filter((line) => !/^tags:\s*\[/.test(line.trim())).join("\n");
|
|
2451
|
+
}
|
|
2452
|
+
async function runFrontmatterFix(input) {
|
|
2453
|
+
const scan = await scanVault(input.vault);
|
|
2454
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
2455
|
+
const fixed = [];
|
|
2456
|
+
const skipped = [];
|
|
2457
|
+
let unchanged = 0;
|
|
2458
|
+
for (const page of scan.data.typedKnowledge) {
|
|
2459
|
+
const text = await readPage(page);
|
|
2460
|
+
const split = splitFrontmatter(text);
|
|
2461
|
+
if (!split.ok) {
|
|
2462
|
+
skipped.push(page.relPath);
|
|
2463
|
+
continue;
|
|
2464
|
+
}
|
|
2465
|
+
const { rawFrontmatter, body } = split.data;
|
|
2466
|
+
const newFm = fixFrontmatter(rawFrontmatter);
|
|
2467
|
+
const newBody = removeOrphanTagsLines(body);
|
|
2468
|
+
const newText = `---
|
|
2469
|
+
${newFm}
|
|
2470
|
+
---
|
|
2471
|
+
${newBody}`;
|
|
2472
|
+
if (newText === text) {
|
|
2473
|
+
unchanged++;
|
|
2474
|
+
continue;
|
|
2475
|
+
}
|
|
2476
|
+
if (!input.dryRun) {
|
|
2477
|
+
await writeFile9(page.absPath, newText, "utf8");
|
|
2478
|
+
}
|
|
2479
|
+
fixed.push(page.relPath);
|
|
2480
|
+
}
|
|
2481
|
+
const exitCode = fixed.length > 0 ? ExitCode.MIGRATION_APPLIED : ExitCode.OK;
|
|
2482
|
+
const hintLines = [`scanned: ${fixed.length + skipped.length + unchanged}`];
|
|
2483
|
+
if (fixed.length > 0) hintLines.push(`fixed: ${fixed.length}`);
|
|
2484
|
+
if (skipped.length > 0) hintLines.push(`skipped (parse error): ${skipped.length}`);
|
|
2485
|
+
if (unchanged > 0) hintLines.push(`unchanged: ${unchanged}`);
|
|
2486
|
+
if (input.dryRun && fixed.length > 0) hintLines.push("(dry run \u2014 no files written)");
|
|
2487
|
+
return {
|
|
2488
|
+
exitCode,
|
|
2489
|
+
result: ok({
|
|
2490
|
+
scanned: fixed.length + skipped.length + unchanged,
|
|
2491
|
+
fixed,
|
|
2492
|
+
skipped,
|
|
2493
|
+
unchanged,
|
|
2494
|
+
humanHint: hintLines.join("\n")
|
|
2495
|
+
})
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
// src/commands/update.ts
|
|
2500
|
+
import { execSync as execSync2 } from "child_process";
|
|
2501
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
2502
|
+
async function runUpdate(input) {
|
|
2503
|
+
const pkg2 = JSON.parse(
|
|
2504
|
+
readFileSync4(new URL("../../package.json", import.meta.url), "utf8")
|
|
2505
|
+
);
|
|
2506
|
+
const currentVersion = pkg2.version;
|
|
2507
|
+
const tag = input.distTag ?? "beta";
|
|
2508
|
+
let latest;
|
|
2509
|
+
try {
|
|
2510
|
+
latest = execSync2(`npm view skillwiki@${tag} version`, {
|
|
2511
|
+
encoding: "utf8",
|
|
2512
|
+
timeout: 15e3
|
|
2513
|
+
}).trim();
|
|
2514
|
+
} catch (e) {
|
|
2515
|
+
return {
|
|
2516
|
+
exitCode: ExitCode.PREFLIGHT_FAILED,
|
|
2517
|
+
result: err("PREFLIGHT_FAILED", { message: `Failed to query npm registry: ${String(e)}` })
|
|
2518
|
+
};
|
|
2519
|
+
}
|
|
2520
|
+
const cache = {
|
|
2521
|
+
lastCheck: Date.now(),
|
|
2522
|
+
latestVersion: latest,
|
|
2523
|
+
currentVersion
|
|
2524
|
+
};
|
|
2525
|
+
if (latest === currentVersion) {
|
|
2526
|
+
writeCache(input.home, cache);
|
|
2527
|
+
return {
|
|
2528
|
+
exitCode: ExitCode.OK,
|
|
2529
|
+
result: ok({
|
|
2530
|
+
previousVersion: currentVersion,
|
|
2531
|
+
newVersion: null,
|
|
2532
|
+
wasAlreadyLatest: true,
|
|
2533
|
+
humanHint: `Already on latest ${tag}: v${currentVersion}`
|
|
2534
|
+
})
|
|
2535
|
+
};
|
|
2536
|
+
}
|
|
2537
|
+
try {
|
|
2538
|
+
execSync2(`npm install -g skillwiki@${tag}`, {
|
|
2539
|
+
stdio: "pipe",
|
|
2540
|
+
timeout: 6e4
|
|
2541
|
+
});
|
|
2542
|
+
} catch (e) {
|
|
2543
|
+
return {
|
|
2544
|
+
exitCode: ExitCode.PREFLIGHT_FAILED,
|
|
2545
|
+
result: err("PREFLIGHT_FAILED", { message: `npm install failed: ${String(e)}` })
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
writeCache(input.home, { ...cache, updateAppliedAt: Date.now() });
|
|
2549
|
+
return {
|
|
2550
|
+
exitCode: ExitCode.OK,
|
|
2551
|
+
result: ok({
|
|
2552
|
+
previousVersion: currentVersion,
|
|
2553
|
+
newVersion: latest,
|
|
2554
|
+
wasAlreadyLatest: false,
|
|
2555
|
+
humanHint: `Updated skillwiki ${currentVersion} \u2192 ${latest}`
|
|
2556
|
+
})
|
|
2557
|
+
};
|
|
2558
|
+
}
|
|
2559
|
+
|
|
1555
2560
|
// src/cli.ts
|
|
1556
|
-
var pkg = JSON.parse(
|
|
2561
|
+
var pkg = JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8"));
|
|
1557
2562
|
var program = new Command();
|
|
1558
2563
|
program.name("skillwiki").description("Deterministic helpers for CodeWiki skills").version(pkg.version);
|
|
1559
2564
|
program.option("--human", "render terminal-readable output instead of JSON");
|
|
@@ -1565,19 +2570,20 @@ function emit(r) {
|
|
|
1565
2570
|
program.command("hash <file>").action(async (file) => emit(await runHash({ file })));
|
|
1566
2571
|
program.command("fetch-guard <url>").action(async (url) => emit(await runFetchGuard({ url })));
|
|
1567
2572
|
program.command("validate <file>").action(async (file) => emit(await runValidate({ file })));
|
|
1568
|
-
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 })));
|
|
2573
|
+
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 })));
|
|
1569
2574
|
program.command("overlap <vault>").action(async (vault) => emit(await runOverlap({ vault })));
|
|
1570
|
-
program.command("orphans [vault]").action(async (vault) => emit(await runOrphans({
|
|
2575
|
+
program.command("orphans [vault]").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => emit(await runOrphans({
|
|
1571
2576
|
vault,
|
|
1572
2577
|
envValue: process.env.WIKI_PATH,
|
|
1573
|
-
home: process.env.HOME ?? ""
|
|
2578
|
+
home: process.env.HOME ?? "",
|
|
2579
|
+
wiki: opts.wiki
|
|
1574
2580
|
})));
|
|
1575
2581
|
program.command("audit <file>").action(async (file) => emit(await runAudit({ file })));
|
|
1576
2582
|
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) => {
|
|
1577
2583
|
const skillsRoot = opts.skillsRoot ?? new URL("../skills/", import.meta.url).pathname;
|
|
1578
2584
|
emit(await runInstall({ skillsRoot, target: opts.target, dryRun: !!opts.dryRun }));
|
|
1579
2585
|
});
|
|
1580
|
-
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) => {
|
|
2586
|
+
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) => {
|
|
1581
2587
|
const initTime = !!opts.initTime;
|
|
1582
2588
|
const flag = initTime ? opts.target : opts.vault;
|
|
1583
2589
|
emit(await runPath({
|
|
@@ -1585,6 +2591,7 @@ program.command("path").option("--vault <dir>", "explicit vault override (runtim
|
|
|
1585
2591
|
envValue: process.env.WIKI_PATH,
|
|
1586
2592
|
home: process.env.HOME ?? "",
|
|
1587
2593
|
initTime,
|
|
2594
|
+
wiki: opts.wiki,
|
|
1588
2595
|
explain: !!opts.explain
|
|
1589
2596
|
}));
|
|
1590
2597
|
});
|
|
@@ -1596,7 +2603,7 @@ program.command("lang").option("--lang <code>", "explicit language override").op
|
|
|
1596
2603
|
explain: !!opts.explain
|
|
1597
2604
|
}));
|
|
1598
2605
|
});
|
|
1599
|
-
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) => {
|
|
2606
|
+
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) => {
|
|
1600
2607
|
const templates = new URL("../templates/", import.meta.url).pathname;
|
|
1601
2608
|
const taxonomy = typeof opts.taxonomy === "string" ? opts.taxonomy.split(",").map((s) => s.trim()).filter((s) => s.length > 0) : void 0;
|
|
1602
2609
|
emit(await runInit({
|
|
@@ -1608,51 +2615,57 @@ program.command("init").option("--target <dir>", "explicit target directory").re
|
|
|
1608
2615
|
taxonomy,
|
|
1609
2616
|
lang: opts.lang,
|
|
1610
2617
|
force: !!opts.force,
|
|
1611
|
-
noEnv: opts.env === false
|
|
2618
|
+
noEnv: opts.env === false,
|
|
2619
|
+
profile: opts.profile
|
|
1612
2620
|
}));
|
|
1613
2621
|
});
|
|
1614
|
-
async function resolveVaultArg(arg) {
|
|
2622
|
+
async function resolveVaultArg(arg, wiki) {
|
|
1615
2623
|
if (arg) return { ok: true, vault: arg };
|
|
1616
2624
|
const r = await resolveRuntimePath({
|
|
1617
2625
|
flag: void 0,
|
|
1618
2626
|
envValue: process.env.WIKI_PATH,
|
|
1619
|
-
|
|
2627
|
+
wikiEnv: process.env.WIKI,
|
|
2628
|
+
home: process.env.HOME ?? "",
|
|
2629
|
+
wiki
|
|
1620
2630
|
});
|
|
1621
|
-
if (!r.ok)
|
|
2631
|
+
if (!r.ok) {
|
|
2632
|
+
const exitCode = r.error === "UNKNOWN_WIKI_PROFILE" ? 35 : 25;
|
|
2633
|
+
return { ok: false, exitCode, payload: r };
|
|
2634
|
+
}
|
|
1622
2635
|
return { ok: true, vault: r.data.path };
|
|
1623
2636
|
}
|
|
1624
|
-
program.command("links [vault]").action(async (vault) => {
|
|
1625
|
-
const v = await resolveVaultArg(vault);
|
|
2637
|
+
program.command("links [vault]").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2638
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1626
2639
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1627
2640
|
else emit(await runLinks({ vault: v.vault }));
|
|
1628
2641
|
});
|
|
1629
|
-
program.command("tag-audit [vault]").action(async (vault) => {
|
|
1630
|
-
const v = await resolveVaultArg(vault);
|
|
2642
|
+
program.command("tag-audit [vault]").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2643
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1631
2644
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1632
2645
|
else emit(await runTagAudit({ vault: v.vault }));
|
|
1633
2646
|
});
|
|
1634
|
-
program.command("index-check [vault]").action(async (vault) => {
|
|
1635
|
-
const v = await resolveVaultArg(vault);
|
|
2647
|
+
program.command("index-check [vault]").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2648
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1636
2649
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1637
2650
|
else emit(await runIndexCheck({ vault: v.vault }));
|
|
1638
2651
|
});
|
|
1639
|
-
program.command("stale [vault]").option("--days <n>", "staleness threshold in days", (s) => parseInt(s, 10), 90).action(async (vault, opts) => {
|
|
1640
|
-
const v = await resolveVaultArg(vault);
|
|
2652
|
+
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) => {
|
|
2653
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1641
2654
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1642
2655
|
else emit(await runStale({ vault: v.vault, days: opts.days }));
|
|
1643
2656
|
});
|
|
1644
|
-
program.command("pagesize [vault]").option("--lines <n>", "max body lines", (s) => parseInt(s, 10), 200).action(async (vault, opts) => {
|
|
1645
|
-
const v = await resolveVaultArg(vault);
|
|
2657
|
+
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) => {
|
|
2658
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1646
2659
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1647
2660
|
else emit(await runPagesize({ vault: v.vault, lines: opts.lines }));
|
|
1648
2661
|
});
|
|
1649
|
-
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) => {
|
|
1650
|
-
const v = await resolveVaultArg(vault);
|
|
2662
|
+
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) => {
|
|
2663
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1651
2664
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1652
2665
|
else emit(await runLogRotate({ vault: v.vault, threshold: opts.threshold, apply: !!opts.apply }));
|
|
1653
2666
|
});
|
|
1654
|
-
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) => {
|
|
1655
|
-
const v = await resolveVaultArg(vault);
|
|
2667
|
+
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) => {
|
|
2668
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1656
2669
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1657
2670
|
else emit(await runLint({
|
|
1658
2671
|
vault: v.vault,
|
|
@@ -1665,13 +2678,45 @@ program.command("lint [vault]").option("--days <n>", "stale threshold", (s) => p
|
|
|
1665
2678
|
var configCmd = program.command("config").description("manage skillwiki configuration");
|
|
1666
2679
|
configCmd.command("get <key>").description("print the value of a config key").action(async (key) => emit(await runConfigGet({ key, home: process.env.HOME ?? "" })));
|
|
1667
2680
|
configCmd.command("set <key> <value>").description("set a config key value").action(async (key, value) => emit(await runConfigSet({ key, value, home: process.env.HOME ?? "" })));
|
|
1668
|
-
configCmd.command("list").description("list all config key=value pairs").action(async () => emit(await runConfigList({ home: process.env.HOME ?? "" })));
|
|
2681
|
+
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 })));
|
|
1669
2682
|
configCmd.command("path").description("print the config file path").action(async () => emit(await runConfigPath({ home: process.env.HOME ?? "" })));
|
|
1670
2683
|
program.command("doctor").description("diagnose skillwiki setup issues").action(async () => emit(await runDoctor({
|
|
1671
2684
|
home: process.env.HOME ?? "",
|
|
1672
2685
|
envValue: process.env.WIKI_PATH,
|
|
1673
|
-
argv: process.argv
|
|
2686
|
+
argv: process.argv,
|
|
2687
|
+
currentVersion: pkg.version,
|
|
2688
|
+
cwd: process.cwd()
|
|
2689
|
+
})));
|
|
2690
|
+
program.command("archive <page> [vault]").description("archive a typed-knowledge page").option("--wiki <name>", "wiki profile name").action(async (page, vault, opts) => {
|
|
2691
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2692
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2693
|
+
else emit(await runArchive({ vault: v.vault, page }));
|
|
2694
|
+
});
|
|
2695
|
+
program.command("drift [vault]").description("detect content drift in raw sources").option("--apply", "update sha256 in drifted sources").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2696
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2697
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2698
|
+
else emit(await runDrift({ vault: v.vault, apply: opts.apply }));
|
|
2699
|
+
});
|
|
2700
|
+
program.command("dedup [vault]").description("detect duplicate raw sources by sha256").option("--apply", "rewire citations and remove duplicate raw files", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2701
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2702
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2703
|
+
else emit(await runDedup({ vault: v.vault, apply: opts.apply }));
|
|
2704
|
+
});
|
|
2705
|
+
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) => {
|
|
2706
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2707
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2708
|
+
else emit(await runMigrateCitations({ vault: v.vault, dryRun: !!opts.dryRun }));
|
|
2709
|
+
});
|
|
2710
|
+
program.command("frontmatter-fix [vault]").description("fix common frontmatter issues on typed-knowledge pages").option("--dry-run", "preview changes without writing", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2711
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2712
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2713
|
+
else emit(await runFrontmatterFix({ vault: v.vault, dryRun: !!opts.dryRun }));
|
|
2714
|
+
});
|
|
2715
|
+
program.command("update").description("update skillwiki CLI to the latest version").option("--tag <tag>", "npm dist-tag", "beta").action(async (opts) => emit(await runUpdate({
|
|
2716
|
+
home: process.env.HOME ?? "",
|
|
2717
|
+
distTag: opts.tag
|
|
1674
2718
|
})));
|
|
2719
|
+
triggerAutoUpdate(process.env.HOME ?? "", pkg.version);
|
|
1675
2720
|
program.parseAsync(process.argv).catch((e) => {
|
|
1676
2721
|
process.stdout.write(JSON.stringify({ ok: false, error: "INTERNAL", detail: { message: String(e) } }) + "\n");
|
|
1677
2722
|
process.exit(1);
|