skillwiki 0.2.0-beta.8 → 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 +925 -100
- 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 +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,72 @@ 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, readFileSync as readFileSync3, statSync } from "fs";
|
|
1892
|
+
import { join as join16 } 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/commands/doctor.ts
|
|
1524
1957
|
function check(status, id, label, detail) {
|
|
1525
1958
|
return { id, label, status, detail };
|
|
1526
1959
|
}
|
|
@@ -1547,7 +1980,7 @@ function checkCliOnPath(argv) {
|
|
|
1547
1980
|
}
|
|
1548
1981
|
async function checkConfigFile(home) {
|
|
1549
1982
|
const cfgPath = configPath(home);
|
|
1550
|
-
if (!
|
|
1983
|
+
if (!existsSync3(cfgPath)) {
|
|
1551
1984
|
return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
|
|
1552
1985
|
}
|
|
1553
1986
|
try {
|
|
@@ -1562,7 +1995,7 @@ function checkWikiPathExists(resolvedPath) {
|
|
|
1562
1995
|
if (resolvedPath === void 0) {
|
|
1563
1996
|
return check("error", "wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
1564
1997
|
}
|
|
1565
|
-
if (
|
|
1998
|
+
if (existsSync3(resolvedPath) && statSync(resolvedPath).isDirectory()) {
|
|
1566
1999
|
return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
|
|
1567
2000
|
}
|
|
1568
2001
|
return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
|
|
@@ -1571,22 +2004,22 @@ function checkVaultStructure(resolvedPath) {
|
|
|
1571
2004
|
if (resolvedPath === void 0) {
|
|
1572
2005
|
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
1573
2006
|
}
|
|
1574
|
-
if (!
|
|
2007
|
+
if (!existsSync3(resolvedPath)) {
|
|
1575
2008
|
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
|
|
1576
2009
|
}
|
|
1577
2010
|
const missing = [];
|
|
1578
|
-
if (!
|
|
2011
|
+
if (!existsSync3(join16(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
|
|
1579
2012
|
for (const dir of ["raw", "entities", "concepts", "meta"]) {
|
|
1580
|
-
if (!
|
|
2013
|
+
if (!existsSync3(join16(resolvedPath, dir))) missing.push(dir + "/");
|
|
1581
2014
|
}
|
|
1582
2015
|
if (missing.length === 0) {
|
|
1583
2016
|
return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
|
|
1584
2017
|
}
|
|
1585
|
-
return check("
|
|
2018
|
+
return check("warn", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to add CodeWiki structure`);
|
|
1586
2019
|
}
|
|
1587
2020
|
function checkSkillsInstalled(home) {
|
|
1588
|
-
const skillsDir =
|
|
1589
|
-
if (!
|
|
2021
|
+
const skillsDir = join16(home, ".claude", "skills");
|
|
2022
|
+
if (!existsSync3(skillsDir)) {
|
|
1590
2023
|
return check("warn", "skills_installed", "Skills installed", `${skillsDir} not found`);
|
|
1591
2024
|
}
|
|
1592
2025
|
const found = findSkillMd(skillsDir);
|
|
@@ -1595,6 +2028,70 @@ function checkSkillsInstalled(home) {
|
|
|
1595
2028
|
}
|
|
1596
2029
|
return check("warn", "skills_installed", "Skills installed", "No SKILL.md files found in ~/.claude/skills/");
|
|
1597
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
|
+
}
|
|
1598
2095
|
function findSkillMd(dir) {
|
|
1599
2096
|
const results = [];
|
|
1600
2097
|
let entries;
|
|
@@ -1605,9 +2102,9 @@ function findSkillMd(dir) {
|
|
|
1605
2102
|
}
|
|
1606
2103
|
for (const entry of entries) {
|
|
1607
2104
|
if (entry.isFile() && entry.name === "SKILL.md") {
|
|
1608
|
-
results.push(
|
|
2105
|
+
results.push(join16(dir, entry.name));
|
|
1609
2106
|
} else if (entry.isDirectory()) {
|
|
1610
|
-
results.push(...findSkillMd(
|
|
2107
|
+
results.push(...findSkillMd(join16(dir, entry.name)));
|
|
1611
2108
|
}
|
|
1612
2109
|
}
|
|
1613
2110
|
return results;
|
|
@@ -1617,6 +2114,8 @@ async function runDoctor(input) {
|
|
|
1617
2114
|
checks.push(checkNodeVersion());
|
|
1618
2115
|
checks.push(checkCliOnPath(input.argv));
|
|
1619
2116
|
checks.push(await checkConfigFile(input.home));
|
|
2117
|
+
checks.push(await checkProfiles(input.home));
|
|
2118
|
+
checks.push(await checkProjectLocalOverride(input.cwd));
|
|
1620
2119
|
const resolved = await resolveRuntimePath({ flag: void 0, envValue: input.envValue, home: input.home });
|
|
1621
2120
|
if (resolved.ok) {
|
|
1622
2121
|
checks.push(check("pass", "wiki_path_set", "WIKI_PATH configured", `Resolved via ${resolved.data.source}: ${resolved.data.path}`));
|
|
@@ -1627,6 +2126,8 @@ async function runDoctor(input) {
|
|
|
1627
2126
|
checks.push(checkWikiPathExists(resolvedPath));
|
|
1628
2127
|
checks.push(checkVaultStructure(resolvedPath));
|
|
1629
2128
|
checks.push(checkSkillsInstalled(input.home));
|
|
2129
|
+
checks.push(checkNpmUpdate(input.home, input.currentVersion));
|
|
2130
|
+
checks.push(checkPluginVersionDrift(input.home, input.currentVersion));
|
|
1630
2131
|
const summary = {
|
|
1631
2132
|
pass: checks.filter((c) => c.status === "pass").length,
|
|
1632
2133
|
warn: checks.filter((c) => c.status === "warn").length,
|
|
@@ -1647,7 +2148,7 @@ async function runDoctor(input) {
|
|
|
1647
2148
|
|
|
1648
2149
|
// src/commands/archive.ts
|
|
1649
2150
|
import { rename as rename3, mkdir as mkdir5, readFile as readFile13, writeFile as writeFile6 } from "fs/promises";
|
|
1650
|
-
import { join as
|
|
2151
|
+
import { join as join17, dirname as dirname7 } from "path";
|
|
1651
2152
|
async function runArchive(input) {
|
|
1652
2153
|
const scan = await scanVault(input.vault);
|
|
1653
2154
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
@@ -1659,10 +2160,10 @@ async function runArchive(input) {
|
|
|
1659
2160
|
}
|
|
1660
2161
|
if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
|
|
1661
2162
|
if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
|
|
1662
|
-
const archivePath =
|
|
1663
|
-
await mkdir5(
|
|
2163
|
+
const archivePath = join17("_archive", relPath);
|
|
2164
|
+
await mkdir5(dirname7(join17(input.vault, archivePath)), { recursive: true });
|
|
1664
2165
|
let indexUpdated = false;
|
|
1665
|
-
const indexPath =
|
|
2166
|
+
const indexPath = join17(input.vault, "index.md");
|
|
1666
2167
|
try {
|
|
1667
2168
|
const idx = await readFile13(indexPath, "utf8");
|
|
1668
2169
|
const slug = relPath.replace(/\.md$/, "").split("/").pop();
|
|
@@ -1675,12 +2176,13 @@ async function runArchive(input) {
|
|
|
1675
2176
|
} catch (e) {
|
|
1676
2177
|
if (e?.code !== "ENOENT") throw e;
|
|
1677
2178
|
}
|
|
1678
|
-
await rename3(
|
|
2179
|
+
await rename3(join17(input.vault, relPath), join17(input.vault, archivePath));
|
|
1679
2180
|
return { exitCode: ExitCode.OK, result: ok({ archived_from: relPath, archived_to: archivePath, index_updated: indexUpdated, humanHint: `${relPath} -> ${archivePath}${indexUpdated ? " (index updated)" : ""}` }) };
|
|
1680
2181
|
}
|
|
1681
2182
|
|
|
1682
2183
|
// src/commands/drift.ts
|
|
1683
2184
|
import { createHash as createHash2 } from "crypto";
|
|
2185
|
+
import { writeFile as writeFile7 } from "fs/promises";
|
|
1684
2186
|
|
|
1685
2187
|
// src/utils/fetch.ts
|
|
1686
2188
|
async function controlledFetch(url, opts) {
|
|
@@ -1722,11 +2224,15 @@ async function runDrift(input) {
|
|
|
1722
2224
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1723
2225
|
const results = [];
|
|
1724
2226
|
for (const raw of scan.data.raw) {
|
|
1725
|
-
const
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
const
|
|
1729
|
-
|
|
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];
|
|
1730
2236
|
const resp = await doFetch(sourceUrl, FETCH_OPTS);
|
|
1731
2237
|
if (!resp.ok) {
|
|
1732
2238
|
results.push({
|
|
@@ -1741,29 +2247,318 @@ async function runDrift(input) {
|
|
|
1741
2247
|
}
|
|
1742
2248
|
const currentHash = createHash2("sha256").update(Buffer.from(resp.data.body, "utf8")).digest("hex");
|
|
1743
2249
|
const drifted2 = currentHash !== storedHash;
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
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
|
+
}
|
|
1751
2273
|
}
|
|
1752
2274
|
const drifted = results.filter((r) => r.status === "drifted");
|
|
1753
2275
|
const fetchFailed = results.filter((r) => r.status === "fetch_failed");
|
|
2276
|
+
const updated = results.filter((r) => r.status === "updated");
|
|
1754
2277
|
const unchanged = results.filter((r) => r.status === "unchanged").length;
|
|
1755
2278
|
const exitCode = drifted.length > 0 ? ExitCode.DRIFT_DETECTED : ExitCode.OK;
|
|
1756
2279
|
const hintLines = [`scanned: ${results.length}, unchanged: ${unchanged}`];
|
|
1757
2280
|
if (drifted.length > 0) hintLines.push(`drifted: ${drifted.length}`, ...drifted.map((d) => ` ${d.raw_path}`));
|
|
1758
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}`));
|
|
1759
2283
|
return {
|
|
1760
2284
|
exitCode,
|
|
1761
|
-
result: ok({ scanned: results.length, drifted, fetch_failed: fetchFailed, unchanged, humanHint: hintLines.join("\n") })
|
|
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
|
+
})
|
|
1762
2557
|
};
|
|
1763
2558
|
}
|
|
1764
2559
|
|
|
1765
2560
|
// src/cli.ts
|
|
1766
|
-
var pkg = JSON.parse(
|
|
2561
|
+
var pkg = JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8"));
|
|
1767
2562
|
var program = new Command();
|
|
1768
2563
|
program.name("skillwiki").description("Deterministic helpers for CodeWiki skills").version(pkg.version);
|
|
1769
2564
|
program.option("--human", "render terminal-readable output instead of JSON");
|
|
@@ -1775,19 +2570,20 @@ function emit(r) {
|
|
|
1775
2570
|
program.command("hash <file>").action(async (file) => emit(await runHash({ file })));
|
|
1776
2571
|
program.command("fetch-guard <url>").action(async (url) => emit(await runFetchGuard({ url })));
|
|
1777
2572
|
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 })));
|
|
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 })));
|
|
1779
2574
|
program.command("overlap <vault>").action(async (vault) => emit(await runOverlap({ vault })));
|
|
1780
|
-
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({
|
|
1781
2576
|
vault,
|
|
1782
2577
|
envValue: process.env.WIKI_PATH,
|
|
1783
|
-
home: process.env.HOME ?? ""
|
|
2578
|
+
home: process.env.HOME ?? "",
|
|
2579
|
+
wiki: opts.wiki
|
|
1784
2580
|
})));
|
|
1785
2581
|
program.command("audit <file>").action(async (file) => emit(await runAudit({ file })));
|
|
1786
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) => {
|
|
1787
2583
|
const skillsRoot = opts.skillsRoot ?? new URL("../skills/", import.meta.url).pathname;
|
|
1788
2584
|
emit(await runInstall({ skillsRoot, target: opts.target, dryRun: !!opts.dryRun }));
|
|
1789
2585
|
});
|
|
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) => {
|
|
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) => {
|
|
1791
2587
|
const initTime = !!opts.initTime;
|
|
1792
2588
|
const flag = initTime ? opts.target : opts.vault;
|
|
1793
2589
|
emit(await runPath({
|
|
@@ -1795,6 +2591,7 @@ program.command("path").option("--vault <dir>", "explicit vault override (runtim
|
|
|
1795
2591
|
envValue: process.env.WIKI_PATH,
|
|
1796
2592
|
home: process.env.HOME ?? "",
|
|
1797
2593
|
initTime,
|
|
2594
|
+
wiki: opts.wiki,
|
|
1798
2595
|
explain: !!opts.explain
|
|
1799
2596
|
}));
|
|
1800
2597
|
});
|
|
@@ -1806,7 +2603,7 @@ program.command("lang").option("--lang <code>", "explicit language override").op
|
|
|
1806
2603
|
explain: !!opts.explain
|
|
1807
2604
|
}));
|
|
1808
2605
|
});
|
|
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) => {
|
|
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) => {
|
|
1810
2607
|
const templates = new URL("../templates/", import.meta.url).pathname;
|
|
1811
2608
|
const taxonomy = typeof opts.taxonomy === "string" ? opts.taxonomy.split(",").map((s) => s.trim()).filter((s) => s.length > 0) : void 0;
|
|
1812
2609
|
emit(await runInit({
|
|
@@ -1818,51 +2615,57 @@ program.command("init").option("--target <dir>", "explicit target directory").re
|
|
|
1818
2615
|
taxonomy,
|
|
1819
2616
|
lang: opts.lang,
|
|
1820
2617
|
force: !!opts.force,
|
|
1821
|
-
noEnv: opts.env === false
|
|
2618
|
+
noEnv: opts.env === false,
|
|
2619
|
+
profile: opts.profile
|
|
1822
2620
|
}));
|
|
1823
2621
|
});
|
|
1824
|
-
async function resolveVaultArg(arg) {
|
|
2622
|
+
async function resolveVaultArg(arg, wiki) {
|
|
1825
2623
|
if (arg) return { ok: true, vault: arg };
|
|
1826
2624
|
const r = await resolveRuntimePath({
|
|
1827
2625
|
flag: void 0,
|
|
1828
2626
|
envValue: process.env.WIKI_PATH,
|
|
1829
|
-
|
|
2627
|
+
wikiEnv: process.env.WIKI,
|
|
2628
|
+
home: process.env.HOME ?? "",
|
|
2629
|
+
wiki
|
|
1830
2630
|
});
|
|
1831
|
-
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
|
+
}
|
|
1832
2635
|
return { ok: true, vault: r.data.path };
|
|
1833
2636
|
}
|
|
1834
|
-
program.command("links [vault]").action(async (vault) => {
|
|
1835
|
-
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);
|
|
1836
2639
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1837
2640
|
else emit(await runLinks({ vault: v.vault }));
|
|
1838
2641
|
});
|
|
1839
|
-
program.command("tag-audit [vault]").action(async (vault) => {
|
|
1840
|
-
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);
|
|
1841
2644
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1842
2645
|
else emit(await runTagAudit({ vault: v.vault }));
|
|
1843
2646
|
});
|
|
1844
|
-
program.command("index-check [vault]").action(async (vault) => {
|
|
1845
|
-
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);
|
|
1846
2649
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1847
2650
|
else emit(await runIndexCheck({ vault: v.vault }));
|
|
1848
2651
|
});
|
|
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);
|
|
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);
|
|
1851
2654
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1852
2655
|
else emit(await runStale({ vault: v.vault, days: opts.days }));
|
|
1853
2656
|
});
|
|
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);
|
|
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);
|
|
1856
2659
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1857
2660
|
else emit(await runPagesize({ vault: v.vault, lines: opts.lines }));
|
|
1858
2661
|
});
|
|
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);
|
|
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);
|
|
1861
2664
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1862
2665
|
else emit(await runLogRotate({ vault: v.vault, threshold: opts.threshold, apply: !!opts.apply }));
|
|
1863
2666
|
});
|
|
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);
|
|
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);
|
|
1866
2669
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1867
2670
|
else emit(await runLint({
|
|
1868
2671
|
vault: v.vault,
|
|
@@ -1875,23 +2678,45 @@ program.command("lint [vault]").option("--days <n>", "stale threshold", (s) => p
|
|
|
1875
2678
|
var configCmd = program.command("config").description("manage skillwiki configuration");
|
|
1876
2679
|
configCmd.command("get <key>").description("print the value of a config key").action(async (key) => emit(await runConfigGet({ key, home: process.env.HOME ?? "" })));
|
|
1877
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 ?? "" })));
|
|
1878
|
-
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 })));
|
|
1879
2682
|
configCmd.command("path").description("print the config file path").action(async () => emit(await runConfigPath({ home: process.env.HOME ?? "" })));
|
|
1880
2683
|
program.command("doctor").description("diagnose skillwiki setup issues").action(async () => emit(await runDoctor({
|
|
1881
2684
|
home: process.env.HOME ?? "",
|
|
1882
2685
|
envValue: process.env.WIKI_PATH,
|
|
1883
|
-
argv: process.argv
|
|
2686
|
+
argv: process.argv,
|
|
2687
|
+
currentVersion: pkg.version,
|
|
2688
|
+
cwd: process.cwd()
|
|
1884
2689
|
})));
|
|
1885
|
-
program.command("archive <page> [vault]").description("archive a typed-knowledge page").action(async (page, vault) => {
|
|
1886
|
-
const v = await resolveVaultArg(vault);
|
|
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);
|
|
1887
2692
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1888
2693
|
else emit(await runArchive({ vault: v.vault, page }));
|
|
1889
2694
|
});
|
|
1890
|
-
program.command("drift [vault]").description("detect content drift in raw sources").action(async (vault) => {
|
|
1891
|
-
const v = await resolveVaultArg(vault);
|
|
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);
|
|
1892
2707
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1893
|
-
else emit(await
|
|
2708
|
+
else emit(await runMigrateCitations({ vault: v.vault, dryRun: !!opts.dryRun }));
|
|
1894
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
|
|
2718
|
+
})));
|
|
2719
|
+
triggerAutoUpdate(process.env.HOME ?? "", pkg.version);
|
|
1895
2720
|
program.parseAsync(process.argv).catch((e) => {
|
|
1896
2721
|
process.stdout.write(JSON.stringify({ ok: false, error: "INTERNAL", detail: { message: String(e) } }) + "\n");
|
|
1897
2722
|
process.exit(1);
|