skillwiki 0.2.0-beta.8 → 0.2.1-beta.1
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 +948 -105
- package/package.json +4 -2
- package/skills/.claude-plugin/plugin.json +2 -1
- package/skills/bin/skillwiki +5 -0
- package/skills/package.json +2 -1
- package/skills/proj-decide/SKILL.md +1 -1
- package/skills/proj-distill/SKILL.md +1 -1
- package/skills/proj-work/SKILL.md +1 -1
- package/skills/using-skillwiki/SKILL.md +57 -0
- package/skills/wiki-archive/SKILL.md +2 -1
- package/skills/wiki-audit/SKILL.md +1 -1
- package/skills/wiki-crystallize/SKILL.md +1 -1
- package/skills/wiki-ingest/SKILL.md +3 -3
- package/skills/wiki-lint/SKILL.md +1 -1
- package/skills/wiki-query/SKILL.md +2 -2
- package/templates/SCHEMA.md +2 -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
|
|
@@ -66,7 +69,11 @@ var ExitCode = {
|
|
|
66
69
|
DOCTOR_HAS_ERRORS: 29,
|
|
67
70
|
ARCHIVE_TARGET_NOT_FOUND: 30,
|
|
68
71
|
ARCHIVE_ALREADY_ARCHIVED: 31,
|
|
69
|
-
DRIFT_DETECTED: 32
|
|
72
|
+
DRIFT_DETECTED: 32,
|
|
73
|
+
RAW_DEDUP_DETECTED: 33,
|
|
74
|
+
MIGRATION_APPLIED: 34,
|
|
75
|
+
UNKNOWN_WIKI_PROFILE: 35,
|
|
76
|
+
DEDUP_APPLIED: 36
|
|
70
77
|
};
|
|
71
78
|
|
|
72
79
|
// ../shared/src/json-output.ts
|
|
@@ -106,10 +113,10 @@ var TypedKnowledgeSchema = z.object({
|
|
|
106
113
|
});
|
|
107
114
|
var sha256Hex = z.string().regex(/^[0-9a-f]{64}$/);
|
|
108
115
|
var RawSourceSchema = z.object({
|
|
109
|
-
title: z.string().min(1),
|
|
110
|
-
source_url: z.string().
|
|
116
|
+
title: z.string().min(1).optional(),
|
|
117
|
+
source_url: z.string().nullable(),
|
|
111
118
|
ingested: isoDate,
|
|
112
|
-
ingested_by: z.enum(["wiki-ingest", "proj-work", "manual"]),
|
|
119
|
+
ingested_by: z.enum(["wiki-ingest", "proj-work", "manual"]).optional(),
|
|
113
120
|
sha256: sha256Hex,
|
|
114
121
|
project: wikilink.optional(),
|
|
115
122
|
work_item: wikilink.optional(),
|
|
@@ -499,6 +506,16 @@ import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir2 } from
|
|
|
499
506
|
import { dirname as dirname2 } from "path";
|
|
500
507
|
var CONFIG_KEYS = ["WIKI_PATH", "WIKI_LANG"];
|
|
501
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
|
+
}
|
|
502
519
|
function parseDotenvText(text) {
|
|
503
520
|
const out = {};
|
|
504
521
|
for (const rawLine of text.split(/\r?\n/)) {
|
|
@@ -508,7 +525,7 @@ function parseDotenvText(text) {
|
|
|
508
525
|
if (eq <= 0) continue;
|
|
509
526
|
const key = line.slice(0, eq).trim();
|
|
510
527
|
const value = line.slice(eq + 1).trim();
|
|
511
|
-
if (!_whitelist.has(key)) continue;
|
|
528
|
+
if (!_whitelist.has(key) && !isValidWikiProfileKey(key)) continue;
|
|
512
529
|
if (value.length === 0) continue;
|
|
513
530
|
out[key] = value;
|
|
514
531
|
}
|
|
@@ -593,6 +610,14 @@ async function resolveInitTimePath(input) {
|
|
|
593
610
|
return { path: hermes.WIKI_PATH, source: "hermes-dotenv", ...input.explain ? { chain } : {} };
|
|
594
611
|
}
|
|
595
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 });
|
|
596
621
|
const fallback = join2(input.home, "wiki");
|
|
597
622
|
if (input.explain) chain.push({ source: "default", matched: true, value: fallback });
|
|
598
623
|
return { path: fallback, source: "default", ...input.explain ? { chain } : {} };
|
|
@@ -604,15 +629,73 @@ async function resolveRuntimePath(input) {
|
|
|
604
629
|
return ok({ path: input.flag, source: "flag", ...input.explain ? { chain } : {} });
|
|
605
630
|
}
|
|
606
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 });
|
|
607
670
|
if (input.envValue !== void 0 && input.envValue.length > 0) {
|
|
608
671
|
if (input.explain) chain.push({ source: "env", matched: true, value: input.envValue });
|
|
609
672
|
return ok({ path: input.envValue, source: "env", ...input.explain ? { chain } : {} });
|
|
610
673
|
}
|
|
611
674
|
if (input.explain) chain.push({ source: "env", matched: false });
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
if (
|
|
615
|
-
|
|
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 } : {} });
|
|
616
699
|
}
|
|
617
700
|
if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: false });
|
|
618
701
|
return err("NO_VAULT_CONFIGURED", {
|
|
@@ -626,15 +709,21 @@ async function runOrphans(input) {
|
|
|
626
709
|
if (input.vault) {
|
|
627
710
|
vault = input.vault;
|
|
628
711
|
} else {
|
|
629
|
-
const r = await resolveRuntimePath({ flag: void 0, envValue: input.envValue, home: input.home ?? "" });
|
|
630
|
-
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
|
+
}
|
|
631
717
|
vault = r.data.path;
|
|
632
718
|
}
|
|
633
719
|
const scan = await scanVault(vault);
|
|
634
720
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
635
721
|
const slugToPath = {};
|
|
636
722
|
for (const p of scan.data.typedKnowledge) {
|
|
637
|
-
|
|
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;
|
|
638
727
|
}
|
|
639
728
|
const adj = {};
|
|
640
729
|
for (const p of scan.data.typedKnowledge) adj[p.relPath] = /* @__PURE__ */ new Set();
|
|
@@ -643,7 +732,7 @@ async function runOrphans(input) {
|
|
|
643
732
|
const split = splitFrontmatter(text);
|
|
644
733
|
const body = split.ok ? split.data.body : text;
|
|
645
734
|
for (const slug of extractBodyWikilinks(body)) {
|
|
646
|
-
const tgt = slugToPath[slug.split("/").pop()];
|
|
735
|
+
const tgt = slugToPath[slug] ?? slugToPath[slug.split("/").pop()];
|
|
647
736
|
if (tgt) {
|
|
648
737
|
adj[p.relPath].add(tgt);
|
|
649
738
|
adj[tgt].add(p.relPath);
|
|
@@ -703,16 +792,92 @@ import { dirname as dirname3, resolve, join as join3 } from "path";
|
|
|
703
792
|
|
|
704
793
|
// src/parsers/citations.ts
|
|
705
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
|
+
}
|
|
706
803
|
function extractCitationMarkers(body) {
|
|
707
|
-
const stripped = body
|
|
804
|
+
const stripped = stripFences(body);
|
|
708
805
|
const out = [];
|
|
709
|
-
const re = /\^\[(raw\/[^\]]+)\]/g;
|
|
710
806
|
let m;
|
|
711
|
-
while ((m =
|
|
807
|
+
while ((m = MARKER_RE.exec(stripped)) !== null) {
|
|
712
808
|
out.push({ marker: m[0], target: m[1] });
|
|
713
809
|
}
|
|
714
810
|
return out;
|
|
715
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
|
+
}
|
|
716
881
|
|
|
717
882
|
// src/commands/audit.ts
|
|
718
883
|
async function runAudit(input) {
|
|
@@ -737,24 +902,40 @@ async function runAudit(input) {
|
|
|
737
902
|
return { ...m, resolved: false };
|
|
738
903
|
}
|
|
739
904
|
}));
|
|
740
|
-
const sources = fm.data.sources ?? [];
|
|
905
|
+
const sources = (fm.data.sources ?? []).map((s) => s.replace(/^\^\[/, "").replace(/\]$/, ""));
|
|
741
906
|
const referenced = new Set(resolved.map((m) => m.target));
|
|
742
907
|
const unused_sources = sources.filter((s) => !referenced.has(s));
|
|
743
908
|
const missing_from_sources = [...referenced].filter((t) => !sources.includes(t));
|
|
744
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
|
+
}
|
|
745
922
|
const hintLines = [];
|
|
746
923
|
hintLines.push(`markers: ${resolved.length}, broken: ${broken.length}`);
|
|
747
924
|
if (unused_sources.length > 0) hintLines.push(`unused_sources: ${unused_sources.length}`);
|
|
748
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
|
+
}
|
|
749
930
|
if (broken.length === 0 && unused_sources.length === 0 && missing_from_sources.length === 0) hintLines.push("OK");
|
|
750
931
|
const humanHint = hintLines.join("\n");
|
|
751
932
|
if (resolved.some((m) => !m.resolved)) {
|
|
752
|
-
return { exitCode: ExitCode.UNRESOLVED_MARKERS, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, humanHint }) };
|
|
933
|
+
return { exitCode: ExitCode.UNRESOLVED_MARKERS, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, footer_consistency, humanHint }) };
|
|
753
934
|
}
|
|
754
935
|
if (unused_sources.length > 0 || missing_from_sources.length > 0) {
|
|
755
|
-
return { exitCode: ExitCode.SOURCES_INCONSISTENT, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, humanHint }) };
|
|
936
|
+
return { exitCode: ExitCode.SOURCES_INCONSISTENT, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, footer_consistency, humanHint }) };
|
|
756
937
|
}
|
|
757
|
-
return { exitCode: ExitCode.OK, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, humanHint }) };
|
|
938
|
+
return { exitCode: ExitCode.OK, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, footer_consistency, humanHint }) };
|
|
758
939
|
}
|
|
759
940
|
async function findVaultRoot(start) {
|
|
760
941
|
let cur = start;
|
|
@@ -832,6 +1013,20 @@ async function runInstall(input) {
|
|
|
832
1013
|
installed.push(dst);
|
|
833
1014
|
if (r.data.backupPath) backed_up.push(r.data.backupPath);
|
|
834
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
|
+
}
|
|
835
1030
|
const manifest_path = join4(input.target, "wiki-manifest.json");
|
|
836
1031
|
if (!input.dryRun) await writeManifest(manifest_path, { installed, backed_up });
|
|
837
1032
|
const hintLines = [
|
|
@@ -857,9 +1052,13 @@ async function runPath(input) {
|
|
|
857
1052
|
flag: input.flag,
|
|
858
1053
|
envValue: input.envValue,
|
|
859
1054
|
home: input.home,
|
|
1055
|
+
wiki: input.wiki,
|
|
860
1056
|
explain: input.explain
|
|
861
1057
|
});
|
|
862
|
-
if (!r.ok)
|
|
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
|
+
}
|
|
863
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})` }) };
|
|
864
1063
|
}
|
|
865
1064
|
|
|
@@ -972,8 +1171,10 @@ var VAULT_DIRS = [
|
|
|
972
1171
|
"comparisons",
|
|
973
1172
|
"queries",
|
|
974
1173
|
"meta",
|
|
975
|
-
"projects"
|
|
1174
|
+
"projects",
|
|
1175
|
+
".obsidian"
|
|
976
1176
|
];
|
|
1177
|
+
var ATTACHMENT_FOLDER = "raw/assets";
|
|
977
1178
|
function extractDomainFromSchema(text) {
|
|
978
1179
|
const m = text.match(/^##\s+Domain\s*\n([\s\S]*?)(?=\n\n|\n##|\s*$)/m);
|
|
979
1180
|
if (!m) return "";
|
|
@@ -1028,13 +1229,13 @@ async function runInit(input) {
|
|
|
1028
1229
|
}
|
|
1029
1230
|
const existingEnv = parseDotenvText(existingEnvRaw);
|
|
1030
1231
|
const swDotenvHadPath = existingEnv.WIKI_PATH !== void 0;
|
|
1031
|
-
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) {
|
|
1032
1233
|
return {
|
|
1033
1234
|
exitCode: ExitCode.ENV_WRITE_CONFLICT,
|
|
1034
1235
|
result: err("ENV_WRITE_CONFLICT", { key: "WIKI_PATH", existing: existingEnv.WIKI_PATH, attempted: target })
|
|
1035
1236
|
};
|
|
1036
1237
|
}
|
|
1037
|
-
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) {
|
|
1038
1239
|
return {
|
|
1039
1240
|
exitCode: ExitCode.ENV_WRITE_CONFLICT,
|
|
1040
1241
|
result: err("ENV_WRITE_CONFLICT", { key: "WIKI_LANG", existing: existingEnv.WIKI_LANG, attempted: canonicalLang })
|
|
@@ -1103,17 +1304,29 @@ async function runInit(input) {
|
|
|
1103
1304
|
return tpl.replace("{{INIT_DATE}}", today);
|
|
1104
1305
|
});
|
|
1105
1306
|
if (err1) return err1;
|
|
1106
|
-
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 () => {
|
|
1107
1312
|
const tpl = await readFile6(join7(input.templates, "log.md"), "utf8");
|
|
1108
1313
|
return tpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang);
|
|
1109
1314
|
});
|
|
1110
|
-
if (
|
|
1111
|
-
const
|
|
1112
|
-
const skipEnv = !!input.noEnv || isTempPath;
|
|
1315
|
+
if (err22) return err22;
|
|
1316
|
+
const skipEnv = !!input.noEnv;
|
|
1113
1317
|
let envWritten = "";
|
|
1114
1318
|
if (!skipEnv) {
|
|
1115
1319
|
try {
|
|
1116
|
-
|
|
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);
|
|
1117
1330
|
envWritten = envPath;
|
|
1118
1331
|
} catch (e) {
|
|
1119
1332
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: envPath, message: String(e) }) };
|
|
@@ -1160,7 +1373,8 @@ function buildSlugMap(pages) {
|
|
|
1160
1373
|
async function runLinks(input) {
|
|
1161
1374
|
const scan = await scanVault(input.vault);
|
|
1162
1375
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1163
|
-
const
|
|
1376
|
+
const allPages = [...scan.data.typedKnowledge, ...scan.data.raw, ...scan.data.workItems, ...scan.data.compound];
|
|
1377
|
+
const slugs = buildSlugMap(allPages);
|
|
1164
1378
|
const broken = [];
|
|
1165
1379
|
for (const p of scan.data.typedKnowledge) {
|
|
1166
1380
|
const text = await readPage(p);
|
|
@@ -1168,7 +1382,7 @@ async function runLinks(input) {
|
|
|
1168
1382
|
const body = split.ok ? split.data.body : text;
|
|
1169
1383
|
const lines = body.split("\n");
|
|
1170
1384
|
for (const slug of extractBodyWikilinks(body)) {
|
|
1171
|
-
const tail = slug.split("/").pop();
|
|
1385
|
+
const tail = slug.split("/").pop().replace(/\.md$/, "");
|
|
1172
1386
|
if (!slugs.has(tail.toLowerCase())) {
|
|
1173
1387
|
const line = lines.findIndex((l) => l.includes(`[[${slug}`));
|
|
1174
1388
|
broken.push({ page: p.relPath, slug, line: line >= 0 ? line + 1 : 0 });
|
|
@@ -1395,10 +1609,102 @@ ${markdown_links.map((l) => ` line ${l.line}: ${l.text}`).join("\n")}`;
|
|
|
1395
1609
|
return { exitCode: ExitCode.OK, result: ok({ markdown_links, humanHint }) };
|
|
1396
1610
|
}
|
|
1397
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
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1398
1691
|
// src/commands/lint.ts
|
|
1399
|
-
var
|
|
1400
|
-
var
|
|
1401
|
-
|
|
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"];
|
|
1402
1708
|
async function runLint(input) {
|
|
1403
1709
|
const buckets = {};
|
|
1404
1710
|
const links = await runLinks({ vault: input.vault });
|
|
@@ -1439,6 +1745,56 @@ async function runLint(input) {
|
|
|
1439
1745
|
if (topicMap.result.ok && topicMap.result.data.recommended) {
|
|
1440
1746
|
buckets.topic_map_recommended = [{ page_count: topicMap.result.data.page_count, threshold: topicMap.result.data.threshold }];
|
|
1441
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
|
+
}
|
|
1442
1798
|
const errorOut = ERROR_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
1443
1799
|
const warningOut = WARNING_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
1444
1800
|
const infoOut = INFO_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
@@ -1473,12 +1829,12 @@ async function runLint(input) {
|
|
|
1473
1829
|
// src/commands/config.ts
|
|
1474
1830
|
import { readFile as readFile12 } from "fs/promises";
|
|
1475
1831
|
import { existsSync } from "fs";
|
|
1476
|
-
import { join as
|
|
1832
|
+
import { join as join14 } from "path";
|
|
1477
1833
|
function validateKey(key) {
|
|
1478
|
-
return CONFIG_KEYS.includes(key);
|
|
1834
|
+
return CONFIG_KEYS.includes(key) || isValidWikiProfileKey(key);
|
|
1479
1835
|
}
|
|
1480
1836
|
function configPath(home) {
|
|
1481
|
-
return
|
|
1837
|
+
return join14(home, ".skillwiki", ".env");
|
|
1482
1838
|
}
|
|
1483
1839
|
async function runConfigGet(input) {
|
|
1484
1840
|
if (!validateKey(input.key)) {
|
|
@@ -1510,7 +1866,21 @@ async function runConfigSet(input) {
|
|
|
1510
1866
|
async function runConfigList(input) {
|
|
1511
1867
|
const map = await parseDotenvFile(configPath(input.home));
|
|
1512
1868
|
const entries = Object.entries(map).map(([key, value]) => ({ key, value: value ?? "" }));
|
|
1513
|
-
|
|
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 }) };
|
|
1514
1884
|
}
|
|
1515
1885
|
async function runConfigPath(input) {
|
|
1516
1886
|
const filePath = configPath(input.home);
|
|
@@ -1518,9 +1888,93 @@ async function runConfigPath(input) {
|
|
|
1518
1888
|
}
|
|
1519
1889
|
|
|
1520
1890
|
// src/commands/doctor.ts
|
|
1521
|
-
import { existsSync as
|
|
1522
|
-
import { join as
|
|
1891
|
+
import { existsSync as existsSync3, readdirSync, statSync } from "fs";
|
|
1892
|
+
import { join as join17 } from "path";
|
|
1523
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/utils/plugin-registry.ts
|
|
1957
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
1958
|
+
import { join as join16 } from "path";
|
|
1959
|
+
var REGISTRY_PATH = join16(".claude", "plugins", "installed_plugins.json");
|
|
1960
|
+
var PLUGIN_KEY = "skillwiki@llm-wiki";
|
|
1961
|
+
function readInstalledPlugins(home) {
|
|
1962
|
+
try {
|
|
1963
|
+
const raw = readFileSync3(join16(home, REGISTRY_PATH), "utf8");
|
|
1964
|
+
return JSON.parse(raw);
|
|
1965
|
+
} catch {
|
|
1966
|
+
return null;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
function findPlugin(home, key = PLUGIN_KEY) {
|
|
1970
|
+
const registry = readInstalledPlugins(home);
|
|
1971
|
+
if (!registry?.plugins) return null;
|
|
1972
|
+
const entries = registry.plugins[key];
|
|
1973
|
+
if (!entries || entries.length === 0) return null;
|
|
1974
|
+
return entries[0];
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
// src/commands/doctor.ts
|
|
1524
1978
|
function check(status, id, label, detail) {
|
|
1525
1979
|
return { id, label, status, detail };
|
|
1526
1980
|
}
|
|
@@ -1547,7 +2001,7 @@ function checkCliOnPath(argv) {
|
|
|
1547
2001
|
}
|
|
1548
2002
|
async function checkConfigFile(home) {
|
|
1549
2003
|
const cfgPath = configPath(home);
|
|
1550
|
-
if (!
|
|
2004
|
+
if (!existsSync3(cfgPath)) {
|
|
1551
2005
|
return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
|
|
1552
2006
|
}
|
|
1553
2007
|
try {
|
|
@@ -1562,7 +2016,7 @@ function checkWikiPathExists(resolvedPath) {
|
|
|
1562
2016
|
if (resolvedPath === void 0) {
|
|
1563
2017
|
return check("error", "wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
1564
2018
|
}
|
|
1565
|
-
if (
|
|
2019
|
+
if (existsSync3(resolvedPath) && statSync(resolvedPath).isDirectory()) {
|
|
1566
2020
|
return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
|
|
1567
2021
|
}
|
|
1568
2022
|
return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
|
|
@@ -1571,29 +2025,90 @@ function checkVaultStructure(resolvedPath) {
|
|
|
1571
2025
|
if (resolvedPath === void 0) {
|
|
1572
2026
|
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
1573
2027
|
}
|
|
1574
|
-
if (!
|
|
2028
|
+
if (!existsSync3(resolvedPath)) {
|
|
1575
2029
|
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
|
|
1576
2030
|
}
|
|
1577
2031
|
const missing = [];
|
|
1578
|
-
if (!
|
|
2032
|
+
if (!existsSync3(join17(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
|
|
1579
2033
|
for (const dir of ["raw", "entities", "concepts", "meta"]) {
|
|
1580
|
-
if (!
|
|
2034
|
+
if (!existsSync3(join17(resolvedPath, dir))) missing.push(dir + "/");
|
|
1581
2035
|
}
|
|
1582
2036
|
if (missing.length === 0) {
|
|
1583
2037
|
return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
|
|
1584
2038
|
}
|
|
1585
|
-
return check("
|
|
2039
|
+
return check("warn", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to add CodeWiki structure`);
|
|
1586
2040
|
}
|
|
1587
2041
|
function checkSkillsInstalled(home) {
|
|
1588
|
-
const
|
|
1589
|
-
if (
|
|
1590
|
-
|
|
2042
|
+
const plugin = findPlugin(home);
|
|
2043
|
+
if (plugin) {
|
|
2044
|
+
const found = findSkillMd(plugin.installPath);
|
|
2045
|
+
if (found.length > 0) {
|
|
2046
|
+
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (plugin v${plugin.version})`);
|
|
2047
|
+
}
|
|
1591
2048
|
}
|
|
1592
|
-
const
|
|
1593
|
-
if (
|
|
1594
|
-
|
|
2049
|
+
const skillsDir = join17(home, ".claude", "skills");
|
|
2050
|
+
if (existsSync3(skillsDir)) {
|
|
2051
|
+
const found = findSkillMd(skillsDir);
|
|
2052
|
+
if (found.length > 0) {
|
|
2053
|
+
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (CLI install)`);
|
|
2054
|
+
}
|
|
1595
2055
|
}
|
|
1596
|
-
return check("warn", "skills_installed", "Skills installed", "No SKILL.md files found
|
|
2056
|
+
return check("warn", "skills_installed", "Skills installed", "No SKILL.md files found");
|
|
2057
|
+
}
|
|
2058
|
+
function checkNpmUpdate(home, currentVersion) {
|
|
2059
|
+
const { hasUpdate, latest } = latestFromCache(home, currentVersion);
|
|
2060
|
+
if (!latest) {
|
|
2061
|
+
return check("pass", "npm_update", "npm CLI version", `v${currentVersion} (no cache yet)`);
|
|
2062
|
+
}
|
|
2063
|
+
if (hasUpdate) {
|
|
2064
|
+
return check("warn", "npm_update", "npm CLI version", `v${currentVersion} \u2014 update available: v${latest}. Run \`skillwiki update\`.`);
|
|
2065
|
+
}
|
|
2066
|
+
return check("pass", "npm_update", "npm CLI version", `v${currentVersion} (latest: v${latest})`);
|
|
2067
|
+
}
|
|
2068
|
+
function checkPluginVersionDrift(home, currentVersion) {
|
|
2069
|
+
const plugin = findPlugin(home);
|
|
2070
|
+
if (!plugin) {
|
|
2071
|
+
return check("pass", "plugin_version_drift", "Plugin/CLI version", "Plugin not installed \u2014 CLI only");
|
|
2072
|
+
}
|
|
2073
|
+
const pluginVersion = plugin.version;
|
|
2074
|
+
if (pluginVersion === currentVersion) {
|
|
2075
|
+
return check("pass", "plugin_version_drift", "Plugin/CLI version", `Both at v${currentVersion}`);
|
|
2076
|
+
}
|
|
2077
|
+
const updateCmd = semverGt(pluginVersion, currentVersion) ? "npm install -g skillwiki@beta" : "claude plugin update skillwiki@llm-wiki";
|
|
2078
|
+
return check(
|
|
2079
|
+
"warn",
|
|
2080
|
+
"plugin_version_drift",
|
|
2081
|
+
"Plugin/CLI version",
|
|
2082
|
+
`Plugin v${pluginVersion} \u2260 CLI v${currentVersion} \u2014 run \`${updateCmd}\``
|
|
2083
|
+
);
|
|
2084
|
+
}
|
|
2085
|
+
async function checkProfiles(home) {
|
|
2086
|
+
const map = await parseDotenvFile(configPath(home));
|
|
2087
|
+
const profiles = [];
|
|
2088
|
+
for (const key of Object.keys(map)) {
|
|
2089
|
+
if (key.startsWith("WIKI_") && key.endsWith("_PATH") && key !== "WIKI_PATH") {
|
|
2090
|
+
const name = key.slice(5, -5).toLowerCase().replace(/_/g, "-");
|
|
2091
|
+
profiles.push(name);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
if (profiles.length === 0) {
|
|
2095
|
+
return check("pass", "wiki_profiles", "Wiki profiles", "No named profiles configured");
|
|
2096
|
+
}
|
|
2097
|
+
const defaultProfile = map["WIKI_DEFAULT"] ?? "(none)";
|
|
2098
|
+
return check(
|
|
2099
|
+
"pass",
|
|
2100
|
+
"wiki_profiles",
|
|
2101
|
+
"Wiki profiles",
|
|
2102
|
+
`${profiles.length} profile(s): ${profiles.join(", ")}; default: ${defaultProfile}`
|
|
2103
|
+
);
|
|
2104
|
+
}
|
|
2105
|
+
async function checkProjectLocalOverride(cwd) {
|
|
2106
|
+
const dir = cwd ?? process.cwd();
|
|
2107
|
+
const envPath = join17(dir, ".skillwiki", ".env");
|
|
2108
|
+
if (existsSync3(envPath)) {
|
|
2109
|
+
return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
|
|
2110
|
+
}
|
|
2111
|
+
return check("pass", "project_local", "Project-local config", "None");
|
|
1597
2112
|
}
|
|
1598
2113
|
function findSkillMd(dir) {
|
|
1599
2114
|
const results = [];
|
|
@@ -1605,9 +2120,9 @@ function findSkillMd(dir) {
|
|
|
1605
2120
|
}
|
|
1606
2121
|
for (const entry of entries) {
|
|
1607
2122
|
if (entry.isFile() && entry.name === "SKILL.md") {
|
|
1608
|
-
results.push(
|
|
2123
|
+
results.push(join17(dir, entry.name));
|
|
1609
2124
|
} else if (entry.isDirectory()) {
|
|
1610
|
-
results.push(...findSkillMd(
|
|
2125
|
+
results.push(...findSkillMd(join17(dir, entry.name)));
|
|
1611
2126
|
}
|
|
1612
2127
|
}
|
|
1613
2128
|
return results;
|
|
@@ -1617,6 +2132,8 @@ async function runDoctor(input) {
|
|
|
1617
2132
|
checks.push(checkNodeVersion());
|
|
1618
2133
|
checks.push(checkCliOnPath(input.argv));
|
|
1619
2134
|
checks.push(await checkConfigFile(input.home));
|
|
2135
|
+
checks.push(await checkProfiles(input.home));
|
|
2136
|
+
checks.push(await checkProjectLocalOverride(input.cwd));
|
|
1620
2137
|
const resolved = await resolveRuntimePath({ flag: void 0, envValue: input.envValue, home: input.home });
|
|
1621
2138
|
if (resolved.ok) {
|
|
1622
2139
|
checks.push(check("pass", "wiki_path_set", "WIKI_PATH configured", `Resolved via ${resolved.data.source}: ${resolved.data.path}`));
|
|
@@ -1627,6 +2144,8 @@ async function runDoctor(input) {
|
|
|
1627
2144
|
checks.push(checkWikiPathExists(resolvedPath));
|
|
1628
2145
|
checks.push(checkVaultStructure(resolvedPath));
|
|
1629
2146
|
checks.push(checkSkillsInstalled(input.home));
|
|
2147
|
+
checks.push(checkNpmUpdate(input.home, input.currentVersion));
|
|
2148
|
+
checks.push(checkPluginVersionDrift(input.home, input.currentVersion));
|
|
1630
2149
|
const summary = {
|
|
1631
2150
|
pass: checks.filter((c) => c.status === "pass").length,
|
|
1632
2151
|
warn: checks.filter((c) => c.status === "warn").length,
|
|
@@ -1647,7 +2166,7 @@ async function runDoctor(input) {
|
|
|
1647
2166
|
|
|
1648
2167
|
// src/commands/archive.ts
|
|
1649
2168
|
import { rename as rename3, mkdir as mkdir5, readFile as readFile13, writeFile as writeFile6 } from "fs/promises";
|
|
1650
|
-
import { join as
|
|
2169
|
+
import { join as join18, dirname as dirname7 } from "path";
|
|
1651
2170
|
async function runArchive(input) {
|
|
1652
2171
|
const scan = await scanVault(input.vault);
|
|
1653
2172
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
@@ -1659,10 +2178,10 @@ async function runArchive(input) {
|
|
|
1659
2178
|
}
|
|
1660
2179
|
if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
|
|
1661
2180
|
if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
|
|
1662
|
-
const archivePath =
|
|
1663
|
-
await mkdir5(
|
|
2181
|
+
const archivePath = join18("_archive", relPath);
|
|
2182
|
+
await mkdir5(dirname7(join18(input.vault, archivePath)), { recursive: true });
|
|
1664
2183
|
let indexUpdated = false;
|
|
1665
|
-
const indexPath =
|
|
2184
|
+
const indexPath = join18(input.vault, "index.md");
|
|
1666
2185
|
try {
|
|
1667
2186
|
const idx = await readFile13(indexPath, "utf8");
|
|
1668
2187
|
const slug = relPath.replace(/\.md$/, "").split("/").pop();
|
|
@@ -1675,12 +2194,13 @@ async function runArchive(input) {
|
|
|
1675
2194
|
} catch (e) {
|
|
1676
2195
|
if (e?.code !== "ENOENT") throw e;
|
|
1677
2196
|
}
|
|
1678
|
-
await rename3(
|
|
2197
|
+
await rename3(join18(input.vault, relPath), join18(input.vault, archivePath));
|
|
1679
2198
|
return { exitCode: ExitCode.OK, result: ok({ archived_from: relPath, archived_to: archivePath, index_updated: indexUpdated, humanHint: `${relPath} -> ${archivePath}${indexUpdated ? " (index updated)" : ""}` }) };
|
|
1680
2199
|
}
|
|
1681
2200
|
|
|
1682
2201
|
// src/commands/drift.ts
|
|
1683
2202
|
import { createHash as createHash2 } from "crypto";
|
|
2203
|
+
import { writeFile as writeFile7 } from "fs/promises";
|
|
1684
2204
|
|
|
1685
2205
|
// src/utils/fetch.ts
|
|
1686
2206
|
async function controlledFetch(url, opts) {
|
|
@@ -1722,11 +2242,15 @@ async function runDrift(input) {
|
|
|
1722
2242
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1723
2243
|
const results = [];
|
|
1724
2244
|
for (const raw of scan.data.raw) {
|
|
1725
|
-
const
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
const
|
|
1729
|
-
|
|
2245
|
+
const text = await readPage(raw);
|
|
2246
|
+
const split = splitFrontmatter(text);
|
|
2247
|
+
if (!split.ok) continue;
|
|
2248
|
+
const { rawFrontmatter, body } = split.data;
|
|
2249
|
+
const sourceUrlMatch = rawFrontmatter.match(/^source_url:\s*(.+)$/m);
|
|
2250
|
+
const storedHashMatch = rawFrontmatter.match(/^sha256:\s*([a-f0-9]+)$/m);
|
|
2251
|
+
if (!sourceUrlMatch || !storedHashMatch) continue;
|
|
2252
|
+
const sourceUrl = sourceUrlMatch[1].trim();
|
|
2253
|
+
const storedHash = storedHashMatch[1];
|
|
1730
2254
|
const resp = await doFetch(sourceUrl, FETCH_OPTS);
|
|
1731
2255
|
if (!resp.ok) {
|
|
1732
2256
|
results.push({
|
|
@@ -1741,29 +2265,318 @@ async function runDrift(input) {
|
|
|
1741
2265
|
}
|
|
1742
2266
|
const currentHash = createHash2("sha256").update(Buffer.from(resp.data.body, "utf8")).digest("hex");
|
|
1743
2267
|
const drifted2 = currentHash !== storedHash;
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
2268
|
+
if (drifted2 && input.apply) {
|
|
2269
|
+
const newFm = rawFrontmatter.replace(/^sha256:\s*[a-f0-9]+$/m, `sha256: ${currentHash}`);
|
|
2270
|
+
const newText = `---
|
|
2271
|
+
${newFm}
|
|
2272
|
+
---
|
|
2273
|
+
${body}`;
|
|
2274
|
+
await writeFile7(raw.absPath, newText, "utf8");
|
|
2275
|
+
results.push({
|
|
2276
|
+
raw_path: raw.relPath,
|
|
2277
|
+
source_url: sourceUrl,
|
|
2278
|
+
stored_sha256: storedHash,
|
|
2279
|
+
current_sha256: currentHash,
|
|
2280
|
+
status: "updated"
|
|
2281
|
+
});
|
|
2282
|
+
} else {
|
|
2283
|
+
results.push({
|
|
2284
|
+
raw_path: raw.relPath,
|
|
2285
|
+
source_url: sourceUrl,
|
|
2286
|
+
stored_sha256: storedHash,
|
|
2287
|
+
current_sha256: currentHash,
|
|
2288
|
+
status: drifted2 ? "drifted" : "unchanged"
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
1751
2291
|
}
|
|
1752
2292
|
const drifted = results.filter((r) => r.status === "drifted");
|
|
1753
2293
|
const fetchFailed = results.filter((r) => r.status === "fetch_failed");
|
|
2294
|
+
const updated = results.filter((r) => r.status === "updated");
|
|
1754
2295
|
const unchanged = results.filter((r) => r.status === "unchanged").length;
|
|
1755
2296
|
const exitCode = drifted.length > 0 ? ExitCode.DRIFT_DETECTED : ExitCode.OK;
|
|
1756
2297
|
const hintLines = [`scanned: ${results.length}, unchanged: ${unchanged}`];
|
|
1757
2298
|
if (drifted.length > 0) hintLines.push(`drifted: ${drifted.length}`, ...drifted.map((d) => ` ${d.raw_path}`));
|
|
1758
2299
|
if (fetchFailed.length > 0) hintLines.push(`fetch_failed: ${fetchFailed.length}`, ...fetchFailed.map((f) => ` ${f.raw_path}: ${f.fetch_error}`));
|
|
2300
|
+
if (updated.length > 0) hintLines.push(`updated: ${updated.length}`, ...updated.map((u) => ` ${u.raw_path}`));
|
|
1759
2301
|
return {
|
|
1760
2302
|
exitCode,
|
|
1761
|
-
result: ok({ scanned: results.length, drifted, fetch_failed: fetchFailed, unchanged, humanHint: hintLines.join("\n") })
|
|
2303
|
+
result: ok({ scanned: results.length, drifted, fetch_failed: fetchFailed, updated, unchanged, humanHint: hintLines.join("\n") })
|
|
2304
|
+
};
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
// src/commands/migrate-citations.ts
|
|
2308
|
+
import { writeFile as writeFile8 } from "fs/promises";
|
|
2309
|
+
var MARKER_RE2 = /\^\[(raw\/[^\]]+)\]/g;
|
|
2310
|
+
function moveMarkersToParagraphEnd(body) {
|
|
2311
|
+
const lines = body.split("\n");
|
|
2312
|
+
const result = [];
|
|
2313
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2314
|
+
const line = lines[i];
|
|
2315
|
+
if (line.trimStart().startsWith("```")) {
|
|
2316
|
+
result.push(line);
|
|
2317
|
+
if (!line.trimEnd().endsWith("```") || line.trim() === "```") {
|
|
2318
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
2319
|
+
result.push(lines[j]);
|
|
2320
|
+
if (lines[j].trimStart().startsWith("```")) {
|
|
2321
|
+
i = j;
|
|
2322
|
+
break;
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
continue;
|
|
2327
|
+
}
|
|
2328
|
+
if (/^## Sources\b/.test(line.trim())) {
|
|
2329
|
+
result.push(line);
|
|
2330
|
+
continue;
|
|
2331
|
+
}
|
|
2332
|
+
const markers = [...line.matchAll(MARKER_RE2)];
|
|
2333
|
+
if (markers.length === 0) {
|
|
2334
|
+
result.push(line);
|
|
2335
|
+
continue;
|
|
2336
|
+
}
|
|
2337
|
+
const proseOnly = line.replace(MARKER_RE2, "").trim();
|
|
2338
|
+
if (proseOnly.length === 0) {
|
|
2339
|
+
const markerStr = " " + markers.map((m) => m[0]).join(" ");
|
|
2340
|
+
let merged = false;
|
|
2341
|
+
for (let k = result.length - 1; k >= 0; k--) {
|
|
2342
|
+
if (result[k].trim().length > 0) {
|
|
2343
|
+
result[k] = result[k].trimEnd() + markerStr;
|
|
2344
|
+
merged = true;
|
|
2345
|
+
break;
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
if (!merged) result.push(line);
|
|
2349
|
+
continue;
|
|
2350
|
+
}
|
|
2351
|
+
const lastMarkerIdx = line.lastIndexOf("^[raw/");
|
|
2352
|
+
const afterLast = line.slice(lastMarkerIdx).replace(MARKER_RE2, "").trim();
|
|
2353
|
+
const firstMarkerIdx = line.indexOf("^[raw/");
|
|
2354
|
+
const beforeFirst = line.slice(0, firstMarkerIdx).trim();
|
|
2355
|
+
const alreadyAtEnd = afterLast.length === 0 && (beforeFirst.length === 0 || /[.!?]\s*$/.test(beforeFirst));
|
|
2356
|
+
if (alreadyAtEnd) {
|
|
2357
|
+
result.push(line);
|
|
2358
|
+
continue;
|
|
2359
|
+
}
|
|
2360
|
+
let cleaned = line.replace(/\s*\^\[raw\/[^\]]+\]\s*/g, " ").trimEnd();
|
|
2361
|
+
cleaned = cleaned.replace(/ +/g, " ").trimEnd();
|
|
2362
|
+
const markerStrings = markers.map((m) => m[0]);
|
|
2363
|
+
if (cleaned.length > 0 && /[.!?]$/.test(cleaned)) {
|
|
2364
|
+
cleaned += " " + markerStrings.join(" ");
|
|
2365
|
+
} else if (cleaned.length > 0) {
|
|
2366
|
+
cleaned += ". " + markerStrings.join(" ");
|
|
2367
|
+
} else {
|
|
2368
|
+
cleaned = markerStrings.join(" ");
|
|
2369
|
+
}
|
|
2370
|
+
result.push(cleaned);
|
|
2371
|
+
}
|
|
2372
|
+
return result.join("\n");
|
|
2373
|
+
}
|
|
2374
|
+
function buildSourcesFooter(targets) {
|
|
2375
|
+
return "\n## Sources\n" + targets.map((t) => `- ^[${t}]`).join("\n") + "\n";
|
|
2376
|
+
}
|
|
2377
|
+
function reorderSourcesFm(rawFm, targets) {
|
|
2378
|
+
const sourcesLineRe = /^sources:\s*\[([^\]]*)\]\s*$/m;
|
|
2379
|
+
const match = rawFm.match(sourcesLineRe);
|
|
2380
|
+
if (!match) return rawFm;
|
|
2381
|
+
const existing = match[1].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter((s) => s.length > 0);
|
|
2382
|
+
const targetSet = new Set(targets);
|
|
2383
|
+
const reordered = [
|
|
2384
|
+
...targets,
|
|
2385
|
+
...existing.filter((s) => !targetSet.has(s))
|
|
2386
|
+
];
|
|
2387
|
+
const newLine = `sources: [${reordered.join(", ")}]`;
|
|
2388
|
+
return rawFm.replace(sourcesLineRe, newLine);
|
|
2389
|
+
}
|
|
2390
|
+
function removeExistingFooter(body) {
|
|
2391
|
+
const footerRe = /\n## Sources\n[\s\S]*$/;
|
|
2392
|
+
return body.replace(footerRe, "");
|
|
2393
|
+
}
|
|
2394
|
+
async function runMigrateCitations(input) {
|
|
2395
|
+
const scan = await scanVault(input.vault);
|
|
2396
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
2397
|
+
const migrated = [];
|
|
2398
|
+
const skipped = [];
|
|
2399
|
+
let unchanged = 0;
|
|
2400
|
+
for (const page of scan.data.typedKnowledge) {
|
|
2401
|
+
const text = await readPage(page);
|
|
2402
|
+
const split = splitFrontmatter(text);
|
|
2403
|
+
if (!split.ok) continue;
|
|
2404
|
+
const { rawFrontmatter, body } = split.data;
|
|
2405
|
+
const markers = extractCitationMarkers(body);
|
|
2406
|
+
if (markers.length === 0) {
|
|
2407
|
+
unchanged++;
|
|
2408
|
+
continue;
|
|
2409
|
+
}
|
|
2410
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2411
|
+
const uniqueTargets = [];
|
|
2412
|
+
for (const m of markers) {
|
|
2413
|
+
if (!seen.has(m.target)) {
|
|
2414
|
+
seen.add(m.target);
|
|
2415
|
+
uniqueTargets.push(m.target);
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
const bodyWithoutFooter = removeExistingFooter(body);
|
|
2419
|
+
const migratedBody = moveMarkersToParagraphEnd(bodyWithoutFooter);
|
|
2420
|
+
const newFooter = buildSourcesFooter(uniqueTargets);
|
|
2421
|
+
const newFm = reorderSourcesFm(rawFrontmatter, uniqueTargets);
|
|
2422
|
+
const newText = `---
|
|
2423
|
+
${newFm}
|
|
2424
|
+
---
|
|
2425
|
+
${migratedBody}${newFooter}`;
|
|
2426
|
+
if (newText === text) {
|
|
2427
|
+
skipped.push(page.relPath);
|
|
2428
|
+
continue;
|
|
2429
|
+
}
|
|
2430
|
+
if (!input.dryRun) {
|
|
2431
|
+
await writeFile8(page.absPath, newText, "utf8");
|
|
2432
|
+
}
|
|
2433
|
+
migrated.push(page.relPath);
|
|
2434
|
+
}
|
|
2435
|
+
const exitCode = migrated.length > 0 ? ExitCode.MIGRATION_APPLIED : ExitCode.OK;
|
|
2436
|
+
const hintLines = [`scanned: ${migrated.length + skipped.length + unchanged}`];
|
|
2437
|
+
if (migrated.length > 0) hintLines.push(`migrated: ${migrated.length}`);
|
|
2438
|
+
if (skipped.length > 0) hintLines.push(`skipped (already clean): ${skipped.length}`);
|
|
2439
|
+
if (unchanged > 0) hintLines.push(`unchanged (no markers): ${unchanged}`);
|
|
2440
|
+
return {
|
|
2441
|
+
exitCode,
|
|
2442
|
+
result: ok({
|
|
2443
|
+
scanned: migrated.length + skipped.length + unchanged,
|
|
2444
|
+
migrated,
|
|
2445
|
+
skipped,
|
|
2446
|
+
unchanged,
|
|
2447
|
+
humanHint: hintLines.join("\n")
|
|
2448
|
+
})
|
|
2449
|
+
};
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
// src/commands/frontmatter-fix.ts
|
|
2453
|
+
import { writeFile as writeFile9 } from "fs/promises";
|
|
2454
|
+
function isoToday() {
|
|
2455
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2456
|
+
}
|
|
2457
|
+
function fixFrontmatter(rawFm) {
|
|
2458
|
+
const additions = [];
|
|
2459
|
+
if (!/^created:/m.test(rawFm)) additions.push(`created: ${isoToday()}`);
|
|
2460
|
+
if (!/^updated:/m.test(rawFm)) additions.push(`updated: ${isoToday()}`);
|
|
2461
|
+
if (!/^tags:/m.test(rawFm)) additions.push("tags: []");
|
|
2462
|
+
if (!/^sources:/m.test(rawFm)) additions.push("sources: []");
|
|
2463
|
+
if (!/^provenance:/m.test(rawFm)) additions.push("provenance: research");
|
|
2464
|
+
if (additions.length === 0) return rawFm;
|
|
2465
|
+
return rawFm.trimEnd() + "\n" + additions.join("\n") + "\n";
|
|
2466
|
+
}
|
|
2467
|
+
function removeOrphanTagsLines(body) {
|
|
2468
|
+
return body.split("\n").filter((line) => !/^tags:\s*\[/.test(line.trim())).join("\n");
|
|
2469
|
+
}
|
|
2470
|
+
async function runFrontmatterFix(input) {
|
|
2471
|
+
const scan = await scanVault(input.vault);
|
|
2472
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
2473
|
+
const fixed = [];
|
|
2474
|
+
const skipped = [];
|
|
2475
|
+
let unchanged = 0;
|
|
2476
|
+
for (const page of scan.data.typedKnowledge) {
|
|
2477
|
+
const text = await readPage(page);
|
|
2478
|
+
const split = splitFrontmatter(text);
|
|
2479
|
+
if (!split.ok) {
|
|
2480
|
+
skipped.push(page.relPath);
|
|
2481
|
+
continue;
|
|
2482
|
+
}
|
|
2483
|
+
const { rawFrontmatter, body } = split.data;
|
|
2484
|
+
const newFm = fixFrontmatter(rawFrontmatter);
|
|
2485
|
+
const newBody = removeOrphanTagsLines(body);
|
|
2486
|
+
const newText = `---
|
|
2487
|
+
${newFm}
|
|
2488
|
+
---
|
|
2489
|
+
${newBody}`;
|
|
2490
|
+
if (newText === text) {
|
|
2491
|
+
unchanged++;
|
|
2492
|
+
continue;
|
|
2493
|
+
}
|
|
2494
|
+
if (!input.dryRun) {
|
|
2495
|
+
await writeFile9(page.absPath, newText, "utf8");
|
|
2496
|
+
}
|
|
2497
|
+
fixed.push(page.relPath);
|
|
2498
|
+
}
|
|
2499
|
+
const exitCode = fixed.length > 0 ? ExitCode.MIGRATION_APPLIED : ExitCode.OK;
|
|
2500
|
+
const hintLines = [`scanned: ${fixed.length + skipped.length + unchanged}`];
|
|
2501
|
+
if (fixed.length > 0) hintLines.push(`fixed: ${fixed.length}`);
|
|
2502
|
+
if (skipped.length > 0) hintLines.push(`skipped (parse error): ${skipped.length}`);
|
|
2503
|
+
if (unchanged > 0) hintLines.push(`unchanged: ${unchanged}`);
|
|
2504
|
+
if (input.dryRun && fixed.length > 0) hintLines.push("(dry run \u2014 no files written)");
|
|
2505
|
+
return {
|
|
2506
|
+
exitCode,
|
|
2507
|
+
result: ok({
|
|
2508
|
+
scanned: fixed.length + skipped.length + unchanged,
|
|
2509
|
+
fixed,
|
|
2510
|
+
skipped,
|
|
2511
|
+
unchanged,
|
|
2512
|
+
humanHint: hintLines.join("\n")
|
|
2513
|
+
})
|
|
2514
|
+
};
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
// src/commands/update.ts
|
|
2518
|
+
import { execSync as execSync2 } from "child_process";
|
|
2519
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
2520
|
+
async function runUpdate(input) {
|
|
2521
|
+
const pkg2 = JSON.parse(
|
|
2522
|
+
readFileSync4(new URL("../../package.json", import.meta.url), "utf8")
|
|
2523
|
+
);
|
|
2524
|
+
const currentVersion = pkg2.version;
|
|
2525
|
+
const tag = input.distTag ?? "beta";
|
|
2526
|
+
let latest;
|
|
2527
|
+
try {
|
|
2528
|
+
latest = execSync2(`npm view skillwiki@${tag} version`, {
|
|
2529
|
+
encoding: "utf8",
|
|
2530
|
+
timeout: 15e3
|
|
2531
|
+
}).trim();
|
|
2532
|
+
} catch (e) {
|
|
2533
|
+
return {
|
|
2534
|
+
exitCode: ExitCode.PREFLIGHT_FAILED,
|
|
2535
|
+
result: err("PREFLIGHT_FAILED", { message: `Failed to query npm registry: ${String(e)}` })
|
|
2536
|
+
};
|
|
2537
|
+
}
|
|
2538
|
+
const cache = {
|
|
2539
|
+
lastCheck: Date.now(),
|
|
2540
|
+
latestVersion: latest,
|
|
2541
|
+
currentVersion
|
|
2542
|
+
};
|
|
2543
|
+
if (latest === currentVersion) {
|
|
2544
|
+
writeCache(input.home, cache);
|
|
2545
|
+
return {
|
|
2546
|
+
exitCode: ExitCode.OK,
|
|
2547
|
+
result: ok({
|
|
2548
|
+
previousVersion: currentVersion,
|
|
2549
|
+
newVersion: null,
|
|
2550
|
+
wasAlreadyLatest: true,
|
|
2551
|
+
humanHint: `Already on latest ${tag}: v${currentVersion}`
|
|
2552
|
+
})
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
2555
|
+
try {
|
|
2556
|
+
execSync2(`npm install -g skillwiki@${tag}`, {
|
|
2557
|
+
stdio: "pipe",
|
|
2558
|
+
timeout: 6e4
|
|
2559
|
+
});
|
|
2560
|
+
} catch (e) {
|
|
2561
|
+
return {
|
|
2562
|
+
exitCode: ExitCode.PREFLIGHT_FAILED,
|
|
2563
|
+
result: err("PREFLIGHT_FAILED", { message: `npm install failed: ${String(e)}` })
|
|
2564
|
+
};
|
|
2565
|
+
}
|
|
2566
|
+
writeCache(input.home, { ...cache, updateAppliedAt: Date.now() });
|
|
2567
|
+
return {
|
|
2568
|
+
exitCode: ExitCode.OK,
|
|
2569
|
+
result: ok({
|
|
2570
|
+
previousVersion: currentVersion,
|
|
2571
|
+
newVersion: latest,
|
|
2572
|
+
wasAlreadyLatest: false,
|
|
2573
|
+
humanHint: `Updated skillwiki ${currentVersion} \u2192 ${latest}`
|
|
2574
|
+
})
|
|
1762
2575
|
};
|
|
1763
2576
|
}
|
|
1764
2577
|
|
|
1765
2578
|
// src/cli.ts
|
|
1766
|
-
var pkg = JSON.parse(
|
|
2579
|
+
var pkg = JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8"));
|
|
1767
2580
|
var program = new Command();
|
|
1768
2581
|
program.name("skillwiki").description("Deterministic helpers for CodeWiki skills").version(pkg.version);
|
|
1769
2582
|
program.option("--human", "render terminal-readable output instead of JSON");
|
|
@@ -1775,19 +2588,20 @@ function emit(r) {
|
|
|
1775
2588
|
program.command("hash <file>").action(async (file) => emit(await runHash({ file })));
|
|
1776
2589
|
program.command("fetch-guard <url>").action(async (url) => emit(await runFetchGuard({ url })));
|
|
1777
2590
|
program.command("validate <file>").action(async (file) => emit(await runValidate({ file })));
|
|
1778
|
-
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 })));
|
|
2591
|
+
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 })));
|
|
1779
2592
|
program.command("overlap <vault>").action(async (vault) => emit(await runOverlap({ vault })));
|
|
1780
|
-
program.command("orphans [vault]").action(async (vault) => emit(await runOrphans({
|
|
2593
|
+
program.command("orphans [vault]").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => emit(await runOrphans({
|
|
1781
2594
|
vault,
|
|
1782
2595
|
envValue: process.env.WIKI_PATH,
|
|
1783
|
-
home: process.env.HOME ?? ""
|
|
2596
|
+
home: process.env.HOME ?? "",
|
|
2597
|
+
wiki: opts.wiki
|
|
1784
2598
|
})));
|
|
1785
2599
|
program.command("audit <file>").action(async (file) => emit(await runAudit({ file })));
|
|
1786
2600
|
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) => {
|
|
1787
2601
|
const skillsRoot = opts.skillsRoot ?? new URL("../skills/", import.meta.url).pathname;
|
|
1788
2602
|
emit(await runInstall({ skillsRoot, target: opts.target, dryRun: !!opts.dryRun }));
|
|
1789
2603
|
});
|
|
1790
|
-
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) => {
|
|
2604
|
+
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) => {
|
|
1791
2605
|
const initTime = !!opts.initTime;
|
|
1792
2606
|
const flag = initTime ? opts.target : opts.vault;
|
|
1793
2607
|
emit(await runPath({
|
|
@@ -1795,6 +2609,7 @@ program.command("path").option("--vault <dir>", "explicit vault override (runtim
|
|
|
1795
2609
|
envValue: process.env.WIKI_PATH,
|
|
1796
2610
|
home: process.env.HOME ?? "",
|
|
1797
2611
|
initTime,
|
|
2612
|
+
wiki: opts.wiki,
|
|
1798
2613
|
explain: !!opts.explain
|
|
1799
2614
|
}));
|
|
1800
2615
|
});
|
|
@@ -1806,7 +2621,7 @@ program.command("lang").option("--lang <code>", "explicit language override").op
|
|
|
1806
2621
|
explain: !!opts.explain
|
|
1807
2622
|
}));
|
|
1808
2623
|
});
|
|
1809
|
-
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) => {
|
|
2624
|
+
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) => {
|
|
1810
2625
|
const templates = new URL("../templates/", import.meta.url).pathname;
|
|
1811
2626
|
const taxonomy = typeof opts.taxonomy === "string" ? opts.taxonomy.split(",").map((s) => s.trim()).filter((s) => s.length > 0) : void 0;
|
|
1812
2627
|
emit(await runInit({
|
|
@@ -1818,51 +2633,57 @@ program.command("init").option("--target <dir>", "explicit target directory").re
|
|
|
1818
2633
|
taxonomy,
|
|
1819
2634
|
lang: opts.lang,
|
|
1820
2635
|
force: !!opts.force,
|
|
1821
|
-
noEnv: opts.env === false
|
|
2636
|
+
noEnv: opts.env === false,
|
|
2637
|
+
profile: opts.profile
|
|
1822
2638
|
}));
|
|
1823
2639
|
});
|
|
1824
|
-
async function resolveVaultArg(arg) {
|
|
2640
|
+
async function resolveVaultArg(arg, wiki) {
|
|
1825
2641
|
if (arg) return { ok: true, vault: arg };
|
|
1826
2642
|
const r = await resolveRuntimePath({
|
|
1827
2643
|
flag: void 0,
|
|
1828
2644
|
envValue: process.env.WIKI_PATH,
|
|
1829
|
-
|
|
2645
|
+
wikiEnv: process.env.WIKI,
|
|
2646
|
+
home: process.env.HOME ?? "",
|
|
2647
|
+
wiki
|
|
1830
2648
|
});
|
|
1831
|
-
if (!r.ok)
|
|
2649
|
+
if (!r.ok) {
|
|
2650
|
+
const exitCode = r.error === "UNKNOWN_WIKI_PROFILE" ? 35 : 25;
|
|
2651
|
+
return { ok: false, exitCode, payload: r };
|
|
2652
|
+
}
|
|
1832
2653
|
return { ok: true, vault: r.data.path };
|
|
1833
2654
|
}
|
|
1834
|
-
program.command("links [vault]").action(async (vault) => {
|
|
1835
|
-
const v = await resolveVaultArg(vault);
|
|
2655
|
+
program.command("links [vault]").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2656
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1836
2657
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1837
2658
|
else emit(await runLinks({ vault: v.vault }));
|
|
1838
2659
|
});
|
|
1839
|
-
program.command("tag-audit [vault]").action(async (vault) => {
|
|
1840
|
-
const v = await resolveVaultArg(vault);
|
|
2660
|
+
program.command("tag-audit [vault]").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2661
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1841
2662
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1842
2663
|
else emit(await runTagAudit({ vault: v.vault }));
|
|
1843
2664
|
});
|
|
1844
|
-
program.command("index-check [vault]").action(async (vault) => {
|
|
1845
|
-
const v = await resolveVaultArg(vault);
|
|
2665
|
+
program.command("index-check [vault]").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2666
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1846
2667
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1847
2668
|
else emit(await runIndexCheck({ vault: v.vault }));
|
|
1848
2669
|
});
|
|
1849
|
-
program.command("stale [vault]").option("--days <n>", "staleness threshold in days", (s) => parseInt(s, 10), 90).action(async (vault, opts) => {
|
|
1850
|
-
const v = await resolveVaultArg(vault);
|
|
2670
|
+
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) => {
|
|
2671
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1851
2672
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1852
2673
|
else emit(await runStale({ vault: v.vault, days: opts.days }));
|
|
1853
2674
|
});
|
|
1854
|
-
program.command("pagesize [vault]").option("--lines <n>", "max body lines", (s) => parseInt(s, 10), 200).action(async (vault, opts) => {
|
|
1855
|
-
const v = await resolveVaultArg(vault);
|
|
2675
|
+
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) => {
|
|
2676
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1856
2677
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1857
2678
|
else emit(await runPagesize({ vault: v.vault, lines: opts.lines }));
|
|
1858
2679
|
});
|
|
1859
|
-
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) => {
|
|
1860
|
-
const v = await resolveVaultArg(vault);
|
|
2680
|
+
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) => {
|
|
2681
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1861
2682
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1862
2683
|
else emit(await runLogRotate({ vault: v.vault, threshold: opts.threshold, apply: !!opts.apply }));
|
|
1863
2684
|
});
|
|
1864
|
-
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) => {
|
|
1865
|
-
const v = await resolveVaultArg(vault);
|
|
2685
|
+
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) => {
|
|
2686
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1866
2687
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1867
2688
|
else emit(await runLint({
|
|
1868
2689
|
vault: v.vault,
|
|
@@ -1875,23 +2696,45 @@ program.command("lint [vault]").option("--days <n>", "stale threshold", (s) => p
|
|
|
1875
2696
|
var configCmd = program.command("config").description("manage skillwiki configuration");
|
|
1876
2697
|
configCmd.command("get <key>").description("print the value of a config key").action(async (key) => emit(await runConfigGet({ key, home: process.env.HOME ?? "" })));
|
|
1877
2698
|
configCmd.command("set <key> <value>").description("set a config key value").action(async (key, value) => emit(await runConfigSet({ key, value, home: process.env.HOME ?? "" })));
|
|
1878
|
-
configCmd.command("list").description("list all config key=value pairs").action(async () => emit(await runConfigList({ home: process.env.HOME ?? "" })));
|
|
2699
|
+
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 })));
|
|
1879
2700
|
configCmd.command("path").description("print the config file path").action(async () => emit(await runConfigPath({ home: process.env.HOME ?? "" })));
|
|
1880
2701
|
program.command("doctor").description("diagnose skillwiki setup issues").action(async () => emit(await runDoctor({
|
|
1881
2702
|
home: process.env.HOME ?? "",
|
|
1882
2703
|
envValue: process.env.WIKI_PATH,
|
|
1883
|
-
argv: process.argv
|
|
2704
|
+
argv: process.argv,
|
|
2705
|
+
currentVersion: pkg.version,
|
|
2706
|
+
cwd: process.cwd()
|
|
1884
2707
|
})));
|
|
1885
|
-
program.command("archive <page> [vault]").description("archive a typed-knowledge page").action(async (page, vault) => {
|
|
1886
|
-
const v = await resolveVaultArg(vault);
|
|
2708
|
+
program.command("archive <page> [vault]").description("archive a typed-knowledge page").option("--wiki <name>", "wiki profile name").action(async (page, vault, opts) => {
|
|
2709
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1887
2710
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1888
2711
|
else emit(await runArchive({ vault: v.vault, page }));
|
|
1889
2712
|
});
|
|
1890
|
-
program.command("drift [vault]").description("detect content drift in raw sources").action(async (vault) => {
|
|
1891
|
-
const v = await resolveVaultArg(vault);
|
|
2713
|
+
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) => {
|
|
2714
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2715
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2716
|
+
else emit(await runDrift({ vault: v.vault, apply: opts.apply }));
|
|
2717
|
+
});
|
|
2718
|
+
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) => {
|
|
2719
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2720
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2721
|
+
else emit(await runDedup({ vault: v.vault, apply: opts.apply }));
|
|
2722
|
+
});
|
|
2723
|
+
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) => {
|
|
2724
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1892
2725
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1893
|
-
else emit(await
|
|
2726
|
+
else emit(await runMigrateCitations({ vault: v.vault, dryRun: !!opts.dryRun }));
|
|
1894
2727
|
});
|
|
2728
|
+
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) => {
|
|
2729
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2730
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2731
|
+
else emit(await runFrontmatterFix({ vault: v.vault, dryRun: !!opts.dryRun }));
|
|
2732
|
+
});
|
|
2733
|
+
program.command("update").description("update skillwiki CLI to the latest version").option("--tag <tag>", "npm dist-tag", "beta").action(async (opts) => emit(await runUpdate({
|
|
2734
|
+
home: process.env.HOME ?? "",
|
|
2735
|
+
distTag: opts.tag
|
|
2736
|
+
})));
|
|
2737
|
+
triggerAutoUpdate(process.env.HOME ?? "", pkg.version);
|
|
1895
2738
|
program.parseAsync(process.argv).catch((e) => {
|
|
1896
2739
|
process.stdout.write(JSON.stringify({ ok: false, error: "INTERNAL", detail: { message: String(e) } }) + "\n");
|
|
1897
2740
|
process.exit(1);
|