skillwiki 0.2.0-beta.1 → 0.2.0-beta.11
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/cli.js +721 -134
- package/package.json +19 -6
- package/skills/.claude-plugin/plugin.json +24 -0
- package/skills/README.md +10 -0
- package/skills/hooks/hooks.json +16 -0
- package/skills/hooks/run-hook.cmd +43 -0
- package/skills/hooks/session-start +29 -0
- package/skills/package.json +13 -0
- package/skills/proj-decide/SKILL.md +24 -0
- package/skills/proj-distill/SKILL.md +48 -0
- package/skills/proj-init/SKILL.md +29 -0
- package/skills/proj-work/SKILL.md +48 -0
- package/skills/using-skillwiki/SKILL.md +65 -0
- package/skills/wiki-adapter-prd/SKILL.md +87 -0
- package/skills/wiki-archive/SKILL.md +42 -0
- package/skills/wiki-audit/SKILL.md +33 -0
- package/skills/wiki-crystallize/SKILL.md +34 -0
- package/skills/wiki-ingest/SKILL.md +58 -0
- package/skills/wiki-init/SKILL.md +36 -0
- package/skills/wiki-lint/SKILL.md +33 -0
- package/skills/wiki-query/SKILL.md +40 -0
- package/skills/wiki-reingest/SKILL.md +54 -0
- package/templates/SCHEMA.md +16 -0
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
+
import { readFileSync } from "fs";
|
|
4
5
|
import { Command } from "commander";
|
|
5
6
|
|
|
6
7
|
// src/utils/output.ts
|
|
@@ -9,9 +10,14 @@ function printJson(r) {
|
|
|
9
10
|
}
|
|
10
11
|
function printHuman(r) {
|
|
11
12
|
if (r.ok) {
|
|
12
|
-
|
|
13
|
+
if (typeof r.data === "object" && r.data !== null && "humanHint" in r.data) {
|
|
14
|
+
process.stdout.write(`${r.data.humanHint}
|
|
15
|
+
`);
|
|
16
|
+
} else {
|
|
17
|
+
process.stdout.write(`OK
|
|
13
18
|
${formatData(r.data)}
|
|
14
19
|
`);
|
|
20
|
+
}
|
|
15
21
|
} else {
|
|
16
22
|
process.stdout.write(`ERR ${r.error}
|
|
17
23
|
${r.detail !== void 0 ? formatData(r.detail) + "\n" : ""}`);
|
|
@@ -53,7 +59,15 @@ var ExitCode = {
|
|
|
53
59
|
LINT_HAS_WARNINGS: 22,
|
|
54
60
|
LINT_HAS_ERRORS: 23,
|
|
55
61
|
ENV_WRITE_CONFLICT: 24,
|
|
56
|
-
NO_VAULT_CONFIGURED: 25
|
|
62
|
+
NO_VAULT_CONFIGURED: 25,
|
|
63
|
+
INVALID_CONFIG_KEY: 26,
|
|
64
|
+
CONFIG_WRITE_FAILED: 27,
|
|
65
|
+
DOCTOR_HAS_WARNINGS: 28,
|
|
66
|
+
DOCTOR_HAS_ERRORS: 29,
|
|
67
|
+
ARCHIVE_TARGET_NOT_FOUND: 30,
|
|
68
|
+
ARCHIVE_ALREADY_ARCHIVED: 31,
|
|
69
|
+
DRIFT_DETECTED: 32,
|
|
70
|
+
RAW_DEDUP_DETECTED: 33
|
|
57
71
|
};
|
|
58
72
|
|
|
59
73
|
// ../shared/src/json-output.ts
|
|
@@ -231,7 +245,7 @@ async function runHash(input) {
|
|
|
231
245
|
const sha256 = createHash("sha256").update(bodyBytes).digest("hex");
|
|
232
246
|
return {
|
|
233
247
|
exitCode: ExitCode.OK,
|
|
234
|
-
result: ok({ path: input.file, sha256, byte_count: bodyBytes.byteLength })
|
|
248
|
+
result: ok({ path: input.file, sha256, byte_count: bodyBytes.byteLength, humanHint: sha256 })
|
|
235
249
|
};
|
|
236
250
|
}
|
|
237
251
|
|
|
@@ -261,7 +275,7 @@ function runFetchGuardSync(input) {
|
|
|
261
275
|
result: err("HOST_BLOCKED", { sanitized_url: sanitized, host: u.hostname })
|
|
262
276
|
};
|
|
263
277
|
}
|
|
264
|
-
return { exitCode: ExitCode.OK, result: ok({ allowed: true, sanitized_url: sanitized }) };
|
|
278
|
+
return { exitCode: ExitCode.OK, result: ok({ allowed: true, sanitized_url: sanitized, humanHint: `ALLOWED: ${sanitized}` }) };
|
|
265
279
|
}
|
|
266
280
|
function sanitizeUrl(u) {
|
|
267
281
|
const clone = new URL(u.toString());
|
|
@@ -301,17 +315,18 @@ async function runValidate(input) {
|
|
|
301
315
|
}
|
|
302
316
|
const det = detectSchema(fm.data);
|
|
303
317
|
if (!det.schema) {
|
|
304
|
-
return { exitCode: ExitCode.SCHEMA_NOT_DETECTED, result: ok({ schema: null, valid: false, errors: [] }) };
|
|
318
|
+
return { exitCode: ExitCode.SCHEMA_NOT_DETECTED, result: ok({ schema: null, valid: false, errors: [], humanHint: "schema not detected" }) };
|
|
305
319
|
}
|
|
306
320
|
const parsed = SCHEMAS[det.schema].safeParse(fm.data);
|
|
307
321
|
if (!parsed.success) {
|
|
308
322
|
const errors = parsed.error.issues.map((i) => ({ path: i.path.join("."), message: i.message }));
|
|
309
323
|
return {
|
|
310
324
|
exitCode: ExitCode.INVALID_FRONTMATTER,
|
|
311
|
-
result: ok({ schema: det.schema, valid: false, errors })
|
|
325
|
+
result: ok({ schema: det.schema, valid: false, errors, humanHint: `INVALID (${det.schema})
|
|
326
|
+
${errors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}` })
|
|
312
327
|
};
|
|
313
328
|
}
|
|
314
|
-
return { exitCode: ExitCode.OK, result: ok({ schema: det.schema, valid: true, errors: [] }) };
|
|
329
|
+
return { exitCode: ExitCode.OK, result: ok({ schema: det.schema, valid: true, errors: [], humanHint: `VALID (${det.schema})` }) };
|
|
315
330
|
}
|
|
316
331
|
|
|
317
332
|
// src/commands/graph.ts
|
|
@@ -397,7 +412,8 @@ async function runGraphBuild(input) {
|
|
|
397
412
|
}
|
|
398
413
|
return {
|
|
399
414
|
exitCode: ExitCode.OK,
|
|
400
|
-
result: ok({ out_path: input.out, node_count: scan.data.typedKnowledge.length, edge_count }
|
|
415
|
+
result: ok({ out_path: input.out, node_count: scan.data.typedKnowledge.length, edge_count, humanHint: `nodes: ${scan.data.typedKnowledge.length}, edges: ${edge_count}
|
|
416
|
+
written: ${input.out}` })
|
|
401
417
|
};
|
|
402
418
|
}
|
|
403
419
|
function computeAdamicAdar(adj) {
|
|
@@ -472,22 +488,19 @@ async function runOverlap(input) {
|
|
|
472
488
|
}
|
|
473
489
|
return { id, members, score };
|
|
474
490
|
});
|
|
475
|
-
|
|
491
|
+
const humanHint = clusters.length === 0 ? "no overlap clusters found" : clusters.map((c) => `cluster (${c.members.length} pages, score ${c.score}): ${c.members.join(", ")}`).join("\n");
|
|
492
|
+
return { exitCode: ExitCode.OK, result: ok({ clusters, humanHint }) };
|
|
476
493
|
}
|
|
477
494
|
|
|
478
495
|
// src/utils/wiki-path.ts
|
|
479
496
|
import { join as join2 } from "path";
|
|
480
497
|
|
|
481
498
|
// src/utils/dotenv.ts
|
|
482
|
-
import { readFile as readFile4 } from "fs/promises";
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
text = await readFile4(path, "utf8");
|
|
488
|
-
} catch {
|
|
489
|
-
return {};
|
|
490
|
-
}
|
|
499
|
+
import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
500
|
+
import { dirname as dirname2 } from "path";
|
|
501
|
+
var CONFIG_KEYS = ["WIKI_PATH", "WIKI_LANG"];
|
|
502
|
+
var _whitelist = new Set(CONFIG_KEYS);
|
|
503
|
+
function parseDotenvText(text) {
|
|
491
504
|
const out = {};
|
|
492
505
|
for (const rawLine of text.split(/\r?\n/)) {
|
|
493
506
|
const line = rawLine.trim();
|
|
@@ -496,12 +509,65 @@ async function parseDotenvFile(path) {
|
|
|
496
509
|
if (eq <= 0) continue;
|
|
497
510
|
const key = line.slice(0, eq).trim();
|
|
498
511
|
const value = line.slice(eq + 1).trim();
|
|
499
|
-
if (!
|
|
512
|
+
if (!_whitelist.has(key)) continue;
|
|
500
513
|
if (value.length === 0) continue;
|
|
501
514
|
out[key] = value;
|
|
502
515
|
}
|
|
503
516
|
return out;
|
|
504
517
|
}
|
|
518
|
+
async function parseDotenvFile(path) {
|
|
519
|
+
let text;
|
|
520
|
+
try {
|
|
521
|
+
text = await readFile4(path, "utf8");
|
|
522
|
+
} catch {
|
|
523
|
+
return {};
|
|
524
|
+
}
|
|
525
|
+
return parseDotenvText(text);
|
|
526
|
+
}
|
|
527
|
+
async function writeDotenv(filePath, entries, originalContent) {
|
|
528
|
+
const lines = originalContent !== void 0 ? updateLines(originalContent, entries) : freshLines(entries);
|
|
529
|
+
await mkdir2(dirname2(filePath), { recursive: true });
|
|
530
|
+
await writeFile2(filePath, lines.join("\n") + "\n", "utf8");
|
|
531
|
+
}
|
|
532
|
+
function freshLines(entries) {
|
|
533
|
+
const out = [];
|
|
534
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
535
|
+
if (value !== void 0) out.push(`${key}=${value}`);
|
|
536
|
+
}
|
|
537
|
+
return out;
|
|
538
|
+
}
|
|
539
|
+
function updateLines(originalContent, entries) {
|
|
540
|
+
let rawLines = originalContent.split(/\r?\n/);
|
|
541
|
+
if (rawLines.length > 0 && rawLines[rawLines.length - 1] === "") {
|
|
542
|
+
rawLines = rawLines.slice(0, -1);
|
|
543
|
+
}
|
|
544
|
+
const keysToWrite = new Set(Object.keys(entries));
|
|
545
|
+
const out = [];
|
|
546
|
+
for (const line of rawLines) {
|
|
547
|
+
const trimmed = line.trim();
|
|
548
|
+
if (trimmed.length === 0 || trimmed.startsWith("#")) {
|
|
549
|
+
out.push(line);
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
const eq = trimmed.indexOf("=");
|
|
553
|
+
if (eq <= 0) {
|
|
554
|
+
out.push(line);
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
const key = trimmed.slice(0, eq).trim();
|
|
558
|
+
if (keysToWrite.has(key)) {
|
|
559
|
+
out.push(`${key}=${entries[key]}`);
|
|
560
|
+
keysToWrite.delete(key);
|
|
561
|
+
} else {
|
|
562
|
+
out.push(line);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
for (const key of keysToWrite) {
|
|
566
|
+
const value = entries[key];
|
|
567
|
+
if (value !== void 0) out.push(`${key}=${value}`);
|
|
568
|
+
}
|
|
569
|
+
return out;
|
|
570
|
+
}
|
|
505
571
|
|
|
506
572
|
// src/utils/wiki-path.ts
|
|
507
573
|
async function resolveInitTimePath(input) {
|
|
@@ -609,7 +675,11 @@ async function runOrphans(input) {
|
|
|
609
675
|
}
|
|
610
676
|
}
|
|
611
677
|
}
|
|
612
|
-
|
|
678
|
+
const hintLines = [];
|
|
679
|
+
if (orphans.length > 0) hintLines.push(`orphans: ${orphans.length}`, ...orphans.map((o) => ` ${o}`));
|
|
680
|
+
if (bridges.length > 0) hintLines.push(`bridges: ${bridges.length}`, ...bridges.map((b) => ` ${b.path}`));
|
|
681
|
+
if (hintLines.length === 0) hintLines.push("no orphans or bridges");
|
|
682
|
+
return { exitCode: ExitCode.OK, result: ok({ orphans, bridges, humanHint: hintLines.join("\n") }) };
|
|
613
683
|
}
|
|
614
684
|
function simulateRemoval(adj, removed) {
|
|
615
685
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -630,7 +700,7 @@ function simulateRemoval(adj, removed) {
|
|
|
630
700
|
|
|
631
701
|
// src/commands/audit.ts
|
|
632
702
|
import { readFile as readFile5, stat as stat2 } from "fs/promises";
|
|
633
|
-
import { dirname as
|
|
703
|
+
import { dirname as dirname3, resolve, join as join3 } from "path";
|
|
634
704
|
|
|
635
705
|
// src/parsers/citations.ts
|
|
636
706
|
var FENCE2 = /```[\s\S]*?```/g;
|
|
@@ -657,7 +727,7 @@ async function runAudit(input) {
|
|
|
657
727
|
if (!fm.ok) return { exitCode: ExitCode.INVALID_FRONTMATTER, result: fm };
|
|
658
728
|
const split = splitFrontmatter(text);
|
|
659
729
|
const body = split.ok ? split.data.body : text;
|
|
660
|
-
const vault = await findVaultRoot(
|
|
730
|
+
const vault = await findVaultRoot(dirname3(resolve(input.file)));
|
|
661
731
|
if (!vault) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID") };
|
|
662
732
|
const markers = extractCitationMarkers(body);
|
|
663
733
|
const resolved = await Promise.all(markers.map(async (m) => {
|
|
@@ -672,13 +742,20 @@ async function runAudit(input) {
|
|
|
672
742
|
const referenced = new Set(resolved.map((m) => m.target));
|
|
673
743
|
const unused_sources = sources.filter((s) => !referenced.has(s));
|
|
674
744
|
const missing_from_sources = [...referenced].filter((t) => !sources.includes(t));
|
|
745
|
+
const broken = resolved.filter((m) => !m.resolved);
|
|
746
|
+
const hintLines = [];
|
|
747
|
+
hintLines.push(`markers: ${resolved.length}, broken: ${broken.length}`);
|
|
748
|
+
if (unused_sources.length > 0) hintLines.push(`unused_sources: ${unused_sources.length}`);
|
|
749
|
+
if (missing_from_sources.length > 0) hintLines.push(`missing_from_sources: ${missing_from_sources.length}`);
|
|
750
|
+
if (broken.length === 0 && unused_sources.length === 0 && missing_from_sources.length === 0) hintLines.push("OK");
|
|
751
|
+
const humanHint = hintLines.join("\n");
|
|
675
752
|
if (resolved.some((m) => !m.resolved)) {
|
|
676
|
-
return { exitCode: ExitCode.UNRESOLVED_MARKERS, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources } }) };
|
|
753
|
+
return { exitCode: ExitCode.UNRESOLVED_MARKERS, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, humanHint }) };
|
|
677
754
|
}
|
|
678
755
|
if (unused_sources.length > 0 || missing_from_sources.length > 0) {
|
|
679
|
-
return { exitCode: ExitCode.SOURCES_INCONSISTENT, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources } }) };
|
|
756
|
+
return { exitCode: ExitCode.SOURCES_INCONSISTENT, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, humanHint }) };
|
|
680
757
|
}
|
|
681
|
-
return { exitCode: ExitCode.OK, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources } }) };
|
|
758
|
+
return { exitCode: ExitCode.OK, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, humanHint }) };
|
|
682
759
|
}
|
|
683
760
|
async function findVaultRoot(start) {
|
|
684
761
|
let cur = start;
|
|
@@ -688,7 +765,7 @@ async function findVaultRoot(start) {
|
|
|
688
765
|
return cur;
|
|
689
766
|
} catch {
|
|
690
767
|
}
|
|
691
|
-
const parent =
|
|
768
|
+
const parent = dirname3(cur);
|
|
692
769
|
if (parent === cur) return null;
|
|
693
770
|
cur = parent;
|
|
694
771
|
}
|
|
@@ -700,10 +777,10 @@ import { readdir as readdir2, stat as stat4 } from "fs/promises";
|
|
|
700
777
|
import { join as join4 } from "path";
|
|
701
778
|
|
|
702
779
|
// src/utils/install-fs.ts
|
|
703
|
-
import { copyFile, mkdir as
|
|
704
|
-
import { dirname as
|
|
780
|
+
import { copyFile, mkdir as mkdir3, rename, writeFile as writeFile3, stat as stat3 } from "fs/promises";
|
|
781
|
+
import { dirname as dirname4 } from "path";
|
|
705
782
|
async function atomicCopyWithBackup(src, dst) {
|
|
706
|
-
await
|
|
783
|
+
await mkdir3(dirname4(dst), { recursive: true });
|
|
707
784
|
let backupPath = null;
|
|
708
785
|
try {
|
|
709
786
|
await stat3(dst);
|
|
@@ -721,9 +798,9 @@ async function atomicCopyWithBackup(src, dst) {
|
|
|
721
798
|
return ok({ copied: true, backupPath });
|
|
722
799
|
}
|
|
723
800
|
async function writeManifest(path, m) {
|
|
724
|
-
await
|
|
801
|
+
await mkdir3(dirname4(path), { recursive: true });
|
|
725
802
|
const enriched = { installed_at: (/* @__PURE__ */ new Date()).toISOString(), ...m };
|
|
726
|
-
await
|
|
803
|
+
await writeFile3(path, JSON.stringify(enriched, null, 2));
|
|
727
804
|
}
|
|
728
805
|
|
|
729
806
|
// src/commands/install.ts
|
|
@@ -758,7 +835,12 @@ async function runInstall(input) {
|
|
|
758
835
|
}
|
|
759
836
|
const manifest_path = join4(input.target, "wiki-manifest.json");
|
|
760
837
|
if (!input.dryRun) await writeManifest(manifest_path, { installed, backed_up });
|
|
761
|
-
|
|
838
|
+
const hintLines = [
|
|
839
|
+
`installed: ${installed.length}`,
|
|
840
|
+
input.dryRun ? "(dry run)" : `backed up: ${backed_up.length}`,
|
|
841
|
+
`manifest: ${manifest_path}`
|
|
842
|
+
];
|
|
843
|
+
return { exitCode: ExitCode.OK, result: ok({ installed, backed_up, manifest_path, humanHint: hintLines.join("\n") }) };
|
|
762
844
|
}
|
|
763
845
|
|
|
764
846
|
// src/commands/path.ts
|
|
@@ -770,7 +852,7 @@ async function runPath(input) {
|
|
|
770
852
|
home: input.home,
|
|
771
853
|
explain: input.explain
|
|
772
854
|
});
|
|
773
|
-
return { exitCode: ExitCode.OK, result: ok({ path: r2.path, source: r2.source, ...r2.chain ? { chain: r2.chain } : {} }) };
|
|
855
|
+
return { exitCode: ExitCode.OK, result: ok({ path: r2.path, source: r2.source, ...r2.chain ? { chain: r2.chain } : {}, humanHint: `${r2.path} (via ${r2.source})` }) };
|
|
774
856
|
}
|
|
775
857
|
const r = await resolveRuntimePath({
|
|
776
858
|
flag: input.flag,
|
|
@@ -779,7 +861,7 @@ async function runPath(input) {
|
|
|
779
861
|
explain: input.explain
|
|
780
862
|
});
|
|
781
863
|
if (!r.ok) return { exitCode: ExitCode.NO_VAULT_CONFIGURED, result: r };
|
|
782
|
-
return { exitCode: ExitCode.OK, result: ok({ path: r.data.path, source: r.data.source, ...r.data.chain ? { chain: r.data.chain } : {} }) };
|
|
864
|
+
return { exitCode: ExitCode.OK, result: ok({ path: r.data.path, source: r.data.source, ...r.data.chain ? { chain: r.data.chain } : {}, humanHint: `${r.data.path} (via ${r.data.source})` }) };
|
|
783
865
|
}
|
|
784
866
|
|
|
785
867
|
// src/utils/lang.ts
|
|
@@ -833,14 +915,42 @@ async function runLang(input) {
|
|
|
833
915
|
value: resolved.value,
|
|
834
916
|
source: resolved.source,
|
|
835
917
|
canonical: resolved.canonical,
|
|
836
|
-
...chain ? { chain } : {}
|
|
918
|
+
...chain ? { chain } : {},
|
|
919
|
+
humanHint: `${resolved.value} (via ${resolved.source})`
|
|
837
920
|
})
|
|
838
921
|
};
|
|
839
922
|
}
|
|
840
923
|
|
|
841
924
|
// src/commands/init.ts
|
|
842
|
-
import { mkdir as
|
|
843
|
-
import { join as join7
|
|
925
|
+
import { mkdir as mkdir4, readFile as readFile6, readdir as readdir3, writeFile as writeFile4 } from "fs/promises";
|
|
926
|
+
import { join as join7 } from "path";
|
|
927
|
+
|
|
928
|
+
// src/parsers/taxonomy.ts
|
|
929
|
+
import yaml2 from "js-yaml";
|
|
930
|
+
var FENCE_RE = /^##\s+Tag Taxonomy\s*$[\s\S]*?```yaml\s*\n([\s\S]*?)\n```/m;
|
|
931
|
+
function extractTaxonomy(schemaText) {
|
|
932
|
+
const m = schemaText.match(FENCE_RE);
|
|
933
|
+
if (!m) return err("NO_TAXONOMY_BLOCK", { message: "No fenced YAML taxonomy block found in SCHEMA.md" });
|
|
934
|
+
let parsed;
|
|
935
|
+
try {
|
|
936
|
+
parsed = yaml2.load(m[1], { schema: yaml2.JSON_SCHEMA });
|
|
937
|
+
} catch (e) {
|
|
938
|
+
return err("INVALID_FRONTMATTER", { message: e.message });
|
|
939
|
+
}
|
|
940
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
941
|
+
return err("INVALID_FRONTMATTER", { message: "taxonomy block is not an object" });
|
|
942
|
+
}
|
|
943
|
+
const tax = parsed.taxonomy;
|
|
944
|
+
if (!Array.isArray(tax)) {
|
|
945
|
+
return err("INVALID_FRONTMATTER", { message: "taxonomy key missing or not an array" });
|
|
946
|
+
}
|
|
947
|
+
if (!tax.every((x) => typeof x === "string")) {
|
|
948
|
+
return err("INVALID_FRONTMATTER", { message: "taxonomy must be a list of strings" });
|
|
949
|
+
}
|
|
950
|
+
return ok(tax);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// src/commands/init.ts
|
|
844
954
|
var DEFAULT_TAXONOMY = [
|
|
845
955
|
"research",
|
|
846
956
|
"comparison",
|
|
@@ -865,25 +975,59 @@ var VAULT_DIRS = [
|
|
|
865
975
|
"meta",
|
|
866
976
|
"projects"
|
|
867
977
|
];
|
|
978
|
+
function extractDomainFromSchema(text) {
|
|
979
|
+
const m = text.match(/^##\s+Domain\s*\n([\s\S]*?)(?=\n\n|\n##|\s*$)/m);
|
|
980
|
+
if (!m) return "";
|
|
981
|
+
const d = m[1].trim();
|
|
982
|
+
return d.startsWith("##") ? "" : d;
|
|
983
|
+
}
|
|
984
|
+
async function discoverTagsFromPages(target, knownSlugs) {
|
|
985
|
+
const knownSet = new Set(knownSlugs);
|
|
986
|
+
const discovered = /* @__PURE__ */ new Set();
|
|
987
|
+
for (const dir of ["entities", "concepts", "comparisons", "queries"]) {
|
|
988
|
+
let entries;
|
|
989
|
+
try {
|
|
990
|
+
entries = (await readdir3(join7(target, dir), { withFileTypes: true })).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name);
|
|
991
|
+
} catch {
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
for (const file of entries) {
|
|
995
|
+
try {
|
|
996
|
+
const text = await readFile6(join7(target, dir, file), "utf8");
|
|
997
|
+
const fm = extractFrontmatter(text);
|
|
998
|
+
if (!fm.ok || !fm.data.tags || !Array.isArray(fm.data.tags)) continue;
|
|
999
|
+
for (const t of fm.data.tags) {
|
|
1000
|
+
if (typeof t === "string" && !knownSet.has(t)) discovered.add(t);
|
|
1001
|
+
}
|
|
1002
|
+
} catch {
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return [...discovered].sort();
|
|
1007
|
+
}
|
|
868
1008
|
async function runInit(input) {
|
|
869
1009
|
const pathRes = await resolveInitTimePath({ flag: input.flag, envValue: input.envValue, home: input.home });
|
|
870
1010
|
const target = pathRes.path;
|
|
871
1011
|
const langRes = await resolveLang({ flag: input.lang, envValue: void 0, home: input.home });
|
|
872
1012
|
const canonicalLang = langRes.canonical;
|
|
873
|
-
let
|
|
1013
|
+
let oldSchemaText;
|
|
874
1014
|
try {
|
|
875
|
-
await
|
|
876
|
-
hasSchema = true;
|
|
1015
|
+
oldSchemaText = await readFile6(join7(target, "SCHEMA.md"), "utf8");
|
|
877
1016
|
} catch {
|
|
878
1017
|
}
|
|
879
|
-
if (
|
|
1018
|
+
if (oldSchemaText && !input.force) {
|
|
880
1019
|
return {
|
|
881
1020
|
exitCode: ExitCode.INIT_TARGET_NOT_EMPTY,
|
|
882
1021
|
result: err("INIT_TARGET_NOT_EMPTY", { target })
|
|
883
1022
|
};
|
|
884
1023
|
}
|
|
885
1024
|
const envPath = join7(input.home, ".skillwiki", ".env");
|
|
886
|
-
|
|
1025
|
+
let existingEnvRaw = "";
|
|
1026
|
+
try {
|
|
1027
|
+
existingEnvRaw = await readFile6(envPath, "utf8");
|
|
1028
|
+
} catch {
|
|
1029
|
+
}
|
|
1030
|
+
const existingEnv = parseDotenvText(existingEnvRaw);
|
|
887
1031
|
const swDotenvHadPath = existingEnv.WIKI_PATH !== void 0;
|
|
888
1032
|
if (existingEnv.WIKI_PATH !== void 0 && existingEnv.WIKI_PATH !== target && !input.force) {
|
|
889
1033
|
return {
|
|
@@ -899,73 +1043,125 @@ async function runInit(input) {
|
|
|
899
1043
|
}
|
|
900
1044
|
const created = [];
|
|
901
1045
|
try {
|
|
902
|
-
await
|
|
1046
|
+
await mkdir4(target, { recursive: true });
|
|
903
1047
|
for (const d of VAULT_DIRS) {
|
|
904
|
-
await
|
|
1048
|
+
await mkdir4(join7(target, d), { recursive: true });
|
|
905
1049
|
created.push(d + "/");
|
|
906
1050
|
}
|
|
907
1051
|
} catch (e) {
|
|
908
1052
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
|
|
909
1053
|
}
|
|
910
1054
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
911
|
-
|
|
912
|
-
|
|
1055
|
+
let taxonomy = input.taxonomy && input.taxonomy.length > 0 ? input.taxonomy : DEFAULT_TAXONOMY;
|
|
1056
|
+
let domain = input.domain;
|
|
1057
|
+
let oldTaxonomy = [];
|
|
1058
|
+
if (oldSchemaText) {
|
|
1059
|
+
if (!domain) {
|
|
1060
|
+
const oldDomain = extractDomainFromSchema(oldSchemaText);
|
|
1061
|
+
if (oldDomain) domain = oldDomain;
|
|
1062
|
+
}
|
|
1063
|
+
const oldTax = extractTaxonomy(oldSchemaText);
|
|
1064
|
+
if (oldTax.ok) oldTaxonomy = oldTax.data;
|
|
1065
|
+
}
|
|
1066
|
+
const taxonomySet = new Set(taxonomy);
|
|
1067
|
+
for (const t of oldTaxonomy) {
|
|
1068
|
+
if (!taxonomySet.has(t)) {
|
|
1069
|
+
taxonomy.push(t);
|
|
1070
|
+
taxonomySet.add(t);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
const discovered = await discoverTagsFromPages(target, taxonomy);
|
|
1074
|
+
const discovered_tags = discovered.length;
|
|
1075
|
+
const fullTaxonomyYaml = discovered.length > 0 ? taxonomy.map((t) => ` - ${t}`).join("\n") + "\n # --- Discovered from existing pages ---\n" + discovered.map((t) => ` - ${t}`).join("\n") : taxonomy.map((t) => ` - ${t}`).join("\n");
|
|
913
1076
|
try {
|
|
914
1077
|
const schemaTpl = await readFile6(join7(input.templates, "SCHEMA.md"), "utf8");
|
|
915
|
-
const schema = schemaTpl.replace("{{DOMAIN}}",
|
|
916
|
-
await
|
|
1078
|
+
const schema = schemaTpl.replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang).replace("{{TAXONOMY_YAML}}", fullTaxonomyYaml);
|
|
1079
|
+
await writeFile4(join7(target, "SCHEMA.md"), schema, "utf8");
|
|
917
1080
|
created.push("SCHEMA.md");
|
|
918
1081
|
} catch (e) {
|
|
919
1082
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: "SCHEMA.md", message: String(e) }) };
|
|
920
1083
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1084
|
+
const preserved = [];
|
|
1085
|
+
async function writeOrPreserve(fileName, render) {
|
|
1086
|
+
try {
|
|
1087
|
+
const existing = await readFile6(join7(target, fileName), "utf8");
|
|
1088
|
+
if (existing.split("\n").length > 10) {
|
|
1089
|
+
preserved.push(fileName);
|
|
1090
|
+
return void 0;
|
|
1091
|
+
}
|
|
1092
|
+
} catch {
|
|
1093
|
+
}
|
|
1094
|
+
try {
|
|
1095
|
+
await writeFile4(join7(target, fileName), await render(), "utf8");
|
|
1096
|
+
created.push(fileName);
|
|
1097
|
+
return void 0;
|
|
1098
|
+
} catch (e) {
|
|
1099
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: fileName, message: String(e) }) };
|
|
1100
|
+
}
|
|
936
1101
|
}
|
|
937
|
-
|
|
938
|
-
await
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
return {
|
|
1102
|
+
const err1 = await writeOrPreserve("index.md", async () => {
|
|
1103
|
+
const tpl = await readFile6(join7(input.templates, "index.md"), "utf8");
|
|
1104
|
+
return tpl.replace("{{INIT_DATE}}", today);
|
|
1105
|
+
});
|
|
1106
|
+
if (err1) return err1;
|
|
1107
|
+
const err22 = await writeOrPreserve("log.md", async () => {
|
|
1108
|
+
const tpl = await readFile6(join7(input.templates, "log.md"), "utf8");
|
|
1109
|
+
return tpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang);
|
|
1110
|
+
});
|
|
1111
|
+
if (err22) return err22;
|
|
1112
|
+
const isTempPath = target.startsWith("/tmp/") || target === "/tmp" || target.startsWith("/var/tmp/") || target === "/var/tmp" || target.startsWith("/private/tmp/");
|
|
1113
|
+
const skipEnv = !!input.noEnv || isTempPath;
|
|
1114
|
+
let envWritten = "";
|
|
1115
|
+
if (!skipEnv) {
|
|
1116
|
+
try {
|
|
1117
|
+
await writeDotenv(envPath, { WIKI_PATH: target, WIKI_LANG: canonicalLang }, existingEnvRaw);
|
|
1118
|
+
envWritten = envPath;
|
|
1119
|
+
} catch (e) {
|
|
1120
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: envPath, message: String(e) }) };
|
|
1121
|
+
}
|
|
945
1122
|
}
|
|
946
1123
|
const importedFromHermes = pathRes.source === "hermes-dotenv" && !swDotenvHadPath;
|
|
1124
|
+
const humanHint = [
|
|
1125
|
+
`vault: ${target}`,
|
|
1126
|
+
`domain: ${domain}`,
|
|
1127
|
+
`lang: ${canonicalLang}`,
|
|
1128
|
+
`created: ${created.length}, preserved: ${preserved.length}`,
|
|
1129
|
+
`discovered tags: ${discovered_tags}`,
|
|
1130
|
+
skipEnv ? "env: skipped" : `env: ${envWritten}`
|
|
1131
|
+
].join("\n");
|
|
947
1132
|
return {
|
|
948
1133
|
exitCode: ExitCode.OK,
|
|
949
1134
|
result: ok({
|
|
950
1135
|
vault: target,
|
|
951
|
-
domain
|
|
1136
|
+
domain,
|
|
952
1137
|
taxonomy,
|
|
953
1138
|
lang: canonicalLang,
|
|
954
1139
|
created,
|
|
955
|
-
|
|
956
|
-
|
|
1140
|
+
preserved,
|
|
1141
|
+
env_written: envWritten,
|
|
1142
|
+
env_skipped: skipEnv,
|
|
1143
|
+
imported_from_hermes: importedFromHermes,
|
|
1144
|
+
discovered_tags,
|
|
1145
|
+
humanHint
|
|
957
1146
|
})
|
|
958
1147
|
};
|
|
959
1148
|
}
|
|
960
1149
|
|
|
1150
|
+
// src/utils/slug.ts
|
|
1151
|
+
function buildSlugMap(pages) {
|
|
1152
|
+
const map = /* @__PURE__ */ new Map();
|
|
1153
|
+
for (const p of pages) {
|
|
1154
|
+
const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
|
|
1155
|
+
map.set(slug.toLowerCase(), slug);
|
|
1156
|
+
}
|
|
1157
|
+
return map;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
961
1160
|
// src/commands/links.ts
|
|
962
1161
|
async function runLinks(input) {
|
|
963
1162
|
const scan = await scanVault(input.vault);
|
|
964
1163
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
965
|
-
const slugs =
|
|
966
|
-
for (const p of scan.data.typedKnowledge) {
|
|
967
|
-
slugs.add(p.relPath.replace(/\.md$/, "").split("/").pop());
|
|
968
|
-
}
|
|
1164
|
+
const slugs = buildSlugMap(scan.data.typedKnowledge);
|
|
969
1165
|
const broken = [];
|
|
970
1166
|
for (const p of scan.data.typedKnowledge) {
|
|
971
1167
|
const text = await readPage(p);
|
|
@@ -974,48 +1170,22 @@ async function runLinks(input) {
|
|
|
974
1170
|
const lines = body.split("\n");
|
|
975
1171
|
for (const slug of extractBodyWikilinks(body)) {
|
|
976
1172
|
const tail = slug.split("/").pop();
|
|
977
|
-
if (!slugs.has(tail)) {
|
|
1173
|
+
if (!slugs.has(tail.toLowerCase())) {
|
|
978
1174
|
const line = lines.findIndex((l) => l.includes(`[[${slug}`));
|
|
979
1175
|
broken.push({ page: p.relPath, slug, line: line >= 0 ? line + 1 : 0 });
|
|
980
1176
|
}
|
|
981
1177
|
}
|
|
982
1178
|
}
|
|
983
1179
|
if (broken.length > 0) {
|
|
984
|
-
return { exitCode: ExitCode.BROKEN_WIKILINKS, result: ok({ broken
|
|
1180
|
+
return { exitCode: ExitCode.BROKEN_WIKILINKS, result: ok({ broken, humanHint: `broken: ${broken.length}
|
|
1181
|
+
${broken.map((b) => ` ${b.page}:[[${b.slug}]] (line ${b.line})`).join("\n")}` }) };
|
|
985
1182
|
}
|
|
986
|
-
return { exitCode: ExitCode.OK, result: ok({ broken }) };
|
|
1183
|
+
return { exitCode: ExitCode.OK, result: ok({ broken, humanHint: "no broken wikilinks" }) };
|
|
987
1184
|
}
|
|
988
1185
|
|
|
989
1186
|
// src/commands/tag-audit.ts
|
|
990
1187
|
import { readFile as readFile7 } from "fs/promises";
|
|
991
1188
|
import { join as join8 } from "path";
|
|
992
|
-
|
|
993
|
-
// src/parsers/taxonomy.ts
|
|
994
|
-
import yaml2 from "js-yaml";
|
|
995
|
-
var FENCE_RE = /^##\s+Tag Taxonomy\s*$[\s\S]*?```yaml\s*\n([\s\S]*?)\n```/m;
|
|
996
|
-
function extractTaxonomy(schemaText) {
|
|
997
|
-
const m = schemaText.match(FENCE_RE);
|
|
998
|
-
if (!m) return ok([]);
|
|
999
|
-
let parsed;
|
|
1000
|
-
try {
|
|
1001
|
-
parsed = yaml2.load(m[1], { schema: yaml2.JSON_SCHEMA });
|
|
1002
|
-
} catch (e) {
|
|
1003
|
-
return err("INVALID_FRONTMATTER", { message: e.message });
|
|
1004
|
-
}
|
|
1005
|
-
if (parsed === null || typeof parsed !== "object") {
|
|
1006
|
-
return err("INVALID_FRONTMATTER", { message: "taxonomy block is not an object" });
|
|
1007
|
-
}
|
|
1008
|
-
const tax = parsed.taxonomy;
|
|
1009
|
-
if (!Array.isArray(tax)) {
|
|
1010
|
-
return err("INVALID_FRONTMATTER", { message: "taxonomy key missing or not an array" });
|
|
1011
|
-
}
|
|
1012
|
-
if (!tax.every((x) => typeof x === "string")) {
|
|
1013
|
-
return err("INVALID_FRONTMATTER", { message: "taxonomy must be a list of strings" });
|
|
1014
|
-
}
|
|
1015
|
-
return ok(tax);
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
// src/commands/tag-audit.ts
|
|
1019
1189
|
async function runTagAudit(input) {
|
|
1020
1190
|
const scan = await scanVault(input.vault);
|
|
1021
1191
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
@@ -1037,9 +1207,9 @@ async function runTagAudit(input) {
|
|
|
1037
1207
|
}
|
|
1038
1208
|
}
|
|
1039
1209
|
if (violations.length > 0) {
|
|
1040
|
-
return { exitCode: ExitCode.TAG_NOT_IN_TAXONOMY, result: ok({ violations, taxonomy: tax.data }) };
|
|
1210
|
+
return { exitCode: ExitCode.TAG_NOT_IN_TAXONOMY, result: ok({ violations, taxonomy: tax.data, humanHint: violations.map((v) => `${v.page}: "${v.tag}" not in taxonomy`).join("\n") }) };
|
|
1041
1211
|
}
|
|
1042
|
-
return { exitCode: ExitCode.OK, result: ok({ violations, taxonomy: tax.data }) };
|
|
1212
|
+
return { exitCode: ExitCode.OK, result: ok({ violations, taxonomy: tax.data, humanHint: "all tags valid" }) };
|
|
1043
1213
|
}
|
|
1044
1214
|
|
|
1045
1215
|
// src/commands/index-check.ts
|
|
@@ -1053,7 +1223,11 @@ async function runIndexCheck(input) {
|
|
|
1053
1223
|
indexText = await readFile8(join9(input.vault, "index.md"), "utf8");
|
|
1054
1224
|
} catch {
|
|
1055
1225
|
}
|
|
1056
|
-
const
|
|
1226
|
+
const indexSlugsLower = /* @__PURE__ */ new Map();
|
|
1227
|
+
for (const s of extractBodyWikilinks(indexText)) {
|
|
1228
|
+
const tail = s.split("/").pop();
|
|
1229
|
+
indexSlugsLower.set(tail.toLowerCase(), tail);
|
|
1230
|
+
}
|
|
1057
1231
|
const fileSlugs = /* @__PURE__ */ new Map();
|
|
1058
1232
|
for (const p of scan.data.typedKnowledge) {
|
|
1059
1233
|
const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
|
|
@@ -1061,16 +1235,21 @@ async function runIndexCheck(input) {
|
|
|
1061
1235
|
}
|
|
1062
1236
|
const missing_from_index = [];
|
|
1063
1237
|
for (const [slug, relPath] of fileSlugs.entries()) {
|
|
1064
|
-
if (!
|
|
1238
|
+
if (!indexSlugsLower.has(slug.toLowerCase())) missing_from_index.push(relPath);
|
|
1065
1239
|
}
|
|
1240
|
+
const fileSlugsLower = new Set([...fileSlugs.keys()].map((s) => s.toLowerCase()));
|
|
1066
1241
|
const ghost_entries = [];
|
|
1067
|
-
for (const
|
|
1068
|
-
if (!
|
|
1242
|
+
for (const [lower, orig] of indexSlugsLower) {
|
|
1243
|
+
if (!fileSlugsLower.has(lower)) ghost_entries.push(orig);
|
|
1069
1244
|
}
|
|
1245
|
+
const hintLines = [];
|
|
1246
|
+
if (missing_from_index.length > 0) hintLines.push(`missing from index: ${missing_from_index.length}`, ...missing_from_index.map((p) => ` ${p}`));
|
|
1247
|
+
if (ghost_entries.length > 0) hintLines.push(`ghost entries: ${ghost_entries.length}`, ...ghost_entries.map((g) => ` ${g}`));
|
|
1248
|
+
if (hintLines.length === 0) hintLines.push("index OK");
|
|
1070
1249
|
if (missing_from_index.length > 0 || ghost_entries.length > 0) {
|
|
1071
|
-
return { exitCode: ExitCode.INDEX_INCOMPLETE, result: ok({ missing_from_index, ghost_entries }) };
|
|
1250
|
+
return { exitCode: ExitCode.INDEX_INCOMPLETE, result: ok({ missing_from_index, ghost_entries, humanHint: hintLines.join("\n") }) };
|
|
1072
1251
|
}
|
|
1073
|
-
return { exitCode: ExitCode.OK, result: ok({ missing_from_index, ghost_entries }) };
|
|
1252
|
+
return { exitCode: ExitCode.OK, result: ok({ missing_from_index, ghost_entries, humanHint: hintLines.join("\n") }) };
|
|
1074
1253
|
}
|
|
1075
1254
|
|
|
1076
1255
|
// src/commands/stale.ts
|
|
@@ -1110,8 +1289,8 @@ async function runStale(input) {
|
|
|
1110
1289
|
stale.push({ page: p.relPath, page_updated: updated, newest_source_ingested: newest, gap_days: gap });
|
|
1111
1290
|
}
|
|
1112
1291
|
}
|
|
1113
|
-
if (stale.length > 0) return { exitCode: ExitCode.STALE_PAGE, result: ok({ stale }) };
|
|
1114
|
-
return { exitCode: ExitCode.OK, result: ok({ stale }) };
|
|
1292
|
+
if (stale.length > 0) return { exitCode: ExitCode.STALE_PAGE, result: ok({ stale, humanHint: stale.map((s) => `${s.page} (${s.gap_days}d stale)`).join("\n") }) };
|
|
1293
|
+
return { exitCode: ExitCode.OK, result: ok({ stale, humanHint: "no stale pages" }) };
|
|
1115
1294
|
}
|
|
1116
1295
|
|
|
1117
1296
|
// src/commands/pagesize.ts
|
|
@@ -1126,17 +1305,17 @@ async function runPagesize(input) {
|
|
|
1126
1305
|
const count = body.split("\n").length;
|
|
1127
1306
|
if (count > input.lines) oversized.push({ page: p.relPath, lines: count });
|
|
1128
1307
|
}
|
|
1129
|
-
if (oversized.length > 0) return { exitCode: ExitCode.PAGE_TOO_LARGE, result: ok({ oversized }) };
|
|
1130
|
-
return { exitCode: ExitCode.OK, result: ok({ oversized }) };
|
|
1308
|
+
if (oversized.length > 0) return { exitCode: ExitCode.PAGE_TOO_LARGE, result: ok({ oversized, humanHint: oversized.map((p) => `${p.page}: ${p.lines} lines`).join("\n") }) };
|
|
1309
|
+
return { exitCode: ExitCode.OK, result: ok({ oversized, humanHint: "all pages within size limit" }) };
|
|
1131
1310
|
}
|
|
1132
1311
|
|
|
1133
1312
|
// src/commands/log-rotate.ts
|
|
1134
|
-
import { readFile as readFile10, rename as rename2, writeFile as
|
|
1313
|
+
import { readFile as readFile10, rename as rename2, writeFile as writeFile5, stat as stat5 } from "fs/promises";
|
|
1135
1314
|
import { join as join11 } from "path";
|
|
1136
1315
|
var ENTRY_RE = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
|
|
1137
1316
|
async function runLogRotate(input) {
|
|
1138
1317
|
try {
|
|
1139
|
-
await
|
|
1318
|
+
await stat5(join11(input.vault, "SCHEMA.md"));
|
|
1140
1319
|
} catch {
|
|
1141
1320
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
1142
1321
|
}
|
|
@@ -1150,12 +1329,12 @@ async function runLogRotate(input) {
|
|
|
1150
1329
|
const matches = [...logText.matchAll(ENTRY_RE)];
|
|
1151
1330
|
const entries = matches.length;
|
|
1152
1331
|
if (entries < input.threshold) {
|
|
1153
|
-
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: false }) };
|
|
1332
|
+
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: false, humanHint: `${entries}/${input.threshold} entries \u2014 no rotation needed` }) };
|
|
1154
1333
|
}
|
|
1155
1334
|
if (!input.apply) {
|
|
1156
1335
|
return {
|
|
1157
1336
|
exitCode: ExitCode.LOG_ROTATE_NEEDED,
|
|
1158
|
-
result: ok({ entries, threshold: input.threshold, rotated: false })
|
|
1337
|
+
result: ok({ entries, threshold: input.threshold, rotated: false, humanHint: `${entries}/${input.threshold} entries \u2014 rotation needed (use --apply)` })
|
|
1159
1338
|
};
|
|
1160
1339
|
}
|
|
1161
1340
|
const newestYear = matches[matches.length - 1][1];
|
|
@@ -1172,17 +1351,86 @@ Chronological action log. Newest entries last. Skill writes append entries; lint
|
|
|
1172
1351
|
|
|
1173
1352
|
- Previous log moved to ${rotatedName}
|
|
1174
1353
|
`;
|
|
1175
|
-
await
|
|
1354
|
+
await writeFile5(logPath, fresh, "utf8");
|
|
1176
1355
|
} catch (e) {
|
|
1177
1356
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
|
|
1178
1357
|
}
|
|
1179
|
-
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: true, rotated_to: rotatedName }) };
|
|
1358
|
+
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: true, rotated_to: rotatedName, humanHint: `rotated ${entries} entries to ${rotatedName}` }) };
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// src/commands/topic-map-check.ts
|
|
1362
|
+
var DEFAULT_THRESHOLD = 200;
|
|
1363
|
+
async function runTopicMapCheck(input) {
|
|
1364
|
+
const threshold = input.threshold ?? DEFAULT_THRESHOLD;
|
|
1365
|
+
const scan = await scanVault(input.vault);
|
|
1366
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1367
|
+
const page_count = scan.data.typedKnowledge.length;
|
|
1368
|
+
const recommended = page_count >= threshold;
|
|
1369
|
+
return {
|
|
1370
|
+
exitCode: ExitCode.OK,
|
|
1371
|
+
result: ok({
|
|
1372
|
+
recommended,
|
|
1373
|
+
page_count,
|
|
1374
|
+
threshold,
|
|
1375
|
+
humanHint: recommended ? `topic map recommended (${page_count} pages >= ${threshold} threshold)` : `topic map not needed (${page_count} pages < ${threshold} threshold)`
|
|
1376
|
+
})
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// src/commands/index-link-format.ts
|
|
1381
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
1382
|
+
import { join as join12 } from "path";
|
|
1383
|
+
var MD_LINK_RE = /\[[^\[\]]+\]\([^)]+\.md\)/;
|
|
1384
|
+
async function runIndexLinkFormat(input) {
|
|
1385
|
+
let text = "";
|
|
1386
|
+
try {
|
|
1387
|
+
text = await readFile11(join12(input.vault, "index.md"), "utf8");
|
|
1388
|
+
} catch {
|
|
1389
|
+
}
|
|
1390
|
+
const markdown_links = [];
|
|
1391
|
+
for (const [i, line] of text.split("\n").entries()) {
|
|
1392
|
+
if (MD_LINK_RE.test(line)) markdown_links.push({ line: i + 1, text: line.trim() });
|
|
1393
|
+
}
|
|
1394
|
+
const humanHint = markdown_links.length === 0 ? "all index links use wikilink format" : `markdown links found: ${markdown_links.length}
|
|
1395
|
+
${markdown_links.map((l) => ` line ${l.line}: ${l.text}`).join("\n")}`;
|
|
1396
|
+
return { exitCode: ExitCode.OK, result: ok({ markdown_links, humanHint }) };
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// src/commands/dedup.ts
|
|
1400
|
+
async function runDedup(input) {
|
|
1401
|
+
const scan = await scanVault(input.vault);
|
|
1402
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1403
|
+
const hashMap = /* @__PURE__ */ new Map();
|
|
1404
|
+
let totalFiles = 0;
|
|
1405
|
+
for (const raw of scan.data.raw) {
|
|
1406
|
+
const fm = extractFrontmatter(await readPage(raw));
|
|
1407
|
+
if (!fm.ok) continue;
|
|
1408
|
+
const sha = typeof fm.data.sha256 === "string" ? fm.data.sha256 : null;
|
|
1409
|
+
if (!sha || sha.length !== 64) continue;
|
|
1410
|
+
totalFiles++;
|
|
1411
|
+
const existing = hashMap.get(sha);
|
|
1412
|
+
if (existing) existing.push(raw.relPath);
|
|
1413
|
+
else hashMap.set(sha, [raw.relPath]);
|
|
1414
|
+
}
|
|
1415
|
+
const duplicates = [...hashMap.entries()].filter(([, files]) => files.length > 1).map(([sha256, files]) => ({ sha256, files }));
|
|
1416
|
+
const exitCode = duplicates.length > 0 ? ExitCode.RAW_DEDUP_DETECTED : ExitCode.OK;
|
|
1417
|
+
const hintLines = [`scanned: ${totalFiles} raw files`];
|
|
1418
|
+
if (duplicates.length > 0) {
|
|
1419
|
+
hintLines.push(`duplicates: ${duplicates.length}`);
|
|
1420
|
+
for (const d of duplicates) hintLines.push(` ${d.sha256.slice(0, 12)}... \u2192 ${d.files.join(", ")}`);
|
|
1421
|
+
} else {
|
|
1422
|
+
hintLines.push("0 duplicates");
|
|
1423
|
+
}
|
|
1424
|
+
return {
|
|
1425
|
+
exitCode,
|
|
1426
|
+
result: ok({ scanned: totalFiles, duplicates, humanHint: hintLines.join("\n") })
|
|
1427
|
+
};
|
|
1180
1428
|
}
|
|
1181
1429
|
|
|
1182
1430
|
// src/commands/lint.ts
|
|
1183
|
-
var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "
|
|
1184
|
-
var WARNING_ORDER = ["index_incomplete", "stale_page", "page_too_large", "log_rotate_needed", "contested", "orphans"];
|
|
1185
|
-
var INFO_ORDER = ["bridges", "low_confidence_single_source"];
|
|
1431
|
+
var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_dedup", "tag_not_in_taxonomy"];
|
|
1432
|
+
var WARNING_ORDER = ["index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "contested", "orphans"];
|
|
1433
|
+
var INFO_ORDER = ["bridges", "low_confidence_single_source", "topic_map_recommended"];
|
|
1186
1434
|
async function runLint(input) {
|
|
1187
1435
|
const buckets = {};
|
|
1188
1436
|
const links = await runLinks({ vault: input.vault });
|
|
@@ -1202,6 +1450,10 @@ async function runLint(input) {
|
|
|
1202
1450
|
ghost_entries: idx.result.data.ghost_entries
|
|
1203
1451
|
}];
|
|
1204
1452
|
}
|
|
1453
|
+
const linkFmt = await runIndexLinkFormat({ vault: input.vault });
|
|
1454
|
+
if (linkFmt.result.ok && linkFmt.result.data.markdown_links.length > 0) {
|
|
1455
|
+
buckets.index_link_format = linkFmt.result.data.markdown_links;
|
|
1456
|
+
}
|
|
1205
1457
|
const stale = await runStale({ vault: input.vault, days: input.days });
|
|
1206
1458
|
if (stale.result.ok && stale.result.data.stale.length > 0) buckets.stale_page = stale.result.data.stale;
|
|
1207
1459
|
const pagesize = await runPagesize({ vault: input.vault, lines: input.lines });
|
|
@@ -1215,6 +1467,12 @@ async function runLint(input) {
|
|
|
1215
1467
|
if (orphans.result.data.orphans.length > 0) buckets.orphans = orphans.result.data.orphans;
|
|
1216
1468
|
if (orphans.result.data.bridges.length > 0) buckets.bridges = orphans.result.data.bridges;
|
|
1217
1469
|
}
|
|
1470
|
+
const topicMap = await runTopicMapCheck({ vault: input.vault });
|
|
1471
|
+
if (topicMap.result.ok && topicMap.result.data.recommended) {
|
|
1472
|
+
buckets.topic_map_recommended = [{ page_count: topicMap.result.data.page_count, threshold: topicMap.result.data.threshold }];
|
|
1473
|
+
}
|
|
1474
|
+
const dedup = await runDedup({ vault: input.vault });
|
|
1475
|
+
if (dedup.result.ok && dedup.result.data.duplicates.length > 0) buckets.raw_dedup = dedup.result.data.duplicates;
|
|
1218
1476
|
const errorOut = ERROR_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
1219
1477
|
const warningOut = WARNING_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
1220
1478
|
const infoOut = INFO_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
@@ -1226,19 +1484,322 @@ async function runLint(input) {
|
|
|
1226
1484
|
let exitCode = ExitCode.OK;
|
|
1227
1485
|
if (summary.errors > 0) exitCode = ExitCode.LINT_HAS_ERRORS;
|
|
1228
1486
|
else if (summary.warnings > 0 || summary.info > 0) exitCode = ExitCode.LINT_HAS_WARNINGS;
|
|
1487
|
+
const hintLines = [];
|
|
1488
|
+
if (summary.errors > 0) hintLines.push(`errors: ${summary.errors}`);
|
|
1489
|
+
if (summary.warnings > 0) hintLines.push(`warnings: ${summary.warnings}`);
|
|
1490
|
+
if (summary.info > 0) hintLines.push(`info: ${summary.info}`);
|
|
1491
|
+
const allBuckets = [...errorOut, ...warningOut, ...infoOut];
|
|
1492
|
+
for (const b of allBuckets) {
|
|
1493
|
+
hintLines.push(` ${b.kind}: ${b.items.length}`);
|
|
1494
|
+
}
|
|
1495
|
+
if (hintLines.length === 0) hintLines.push("0 errors, 0 warnings, 0 info");
|
|
1229
1496
|
return {
|
|
1230
1497
|
exitCode,
|
|
1231
1498
|
result: ok({
|
|
1232
1499
|
vault: { path: input.vault, source: input.source ?? "resolved" },
|
|
1233
1500
|
summary,
|
|
1234
|
-
by_severity: { error: errorOut, warning: warningOut, info: infoOut }
|
|
1501
|
+
by_severity: { error: errorOut, warning: warningOut, info: infoOut },
|
|
1502
|
+
humanHint: hintLines.join("\n")
|
|
1235
1503
|
})
|
|
1236
1504
|
};
|
|
1237
1505
|
}
|
|
1238
1506
|
|
|
1507
|
+
// src/commands/config.ts
|
|
1508
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
1509
|
+
import { existsSync } from "fs";
|
|
1510
|
+
import { join as join13 } from "path";
|
|
1511
|
+
function validateKey(key) {
|
|
1512
|
+
return CONFIG_KEYS.includes(key);
|
|
1513
|
+
}
|
|
1514
|
+
function configPath(home) {
|
|
1515
|
+
return join13(home, ".skillwiki", ".env");
|
|
1516
|
+
}
|
|
1517
|
+
async function runConfigGet(input) {
|
|
1518
|
+
if (!validateKey(input.key)) {
|
|
1519
|
+
return { exitCode: ExitCode.INVALID_CONFIG_KEY, result: err("INVALID_CONFIG_KEY", { key: input.key }) };
|
|
1520
|
+
}
|
|
1521
|
+
const map = await parseDotenvFile(configPath(input.home));
|
|
1522
|
+
const value = map[input.key] ?? "";
|
|
1523
|
+
return { exitCode: ExitCode.OK, result: ok({ key: input.key, value, humanHint: value }) };
|
|
1524
|
+
}
|
|
1525
|
+
async function runConfigSet(input) {
|
|
1526
|
+
if (!validateKey(input.key)) {
|
|
1527
|
+
return { exitCode: ExitCode.INVALID_CONFIG_KEY, result: err("INVALID_CONFIG_KEY", { key: input.key }) };
|
|
1528
|
+
}
|
|
1529
|
+
const filePath = configPath(input.home);
|
|
1530
|
+
try {
|
|
1531
|
+
let originalContent;
|
|
1532
|
+
try {
|
|
1533
|
+
originalContent = await readFile12(filePath, "utf8");
|
|
1534
|
+
} catch {
|
|
1535
|
+
}
|
|
1536
|
+
const existing = originalContent !== void 0 ? parseDotenvText(originalContent) : {};
|
|
1537
|
+
const merged = { ...existing, [input.key]: input.value };
|
|
1538
|
+
await writeDotenv(filePath, merged, originalContent);
|
|
1539
|
+
return { exitCode: ExitCode.OK, result: ok({ key: input.key, value: input.value, written: true, humanHint: `${input.key}=${input.value}` }) };
|
|
1540
|
+
} catch (e) {
|
|
1541
|
+
return { exitCode: ExitCode.CONFIG_WRITE_FAILED, result: err("CONFIG_WRITE_FAILED", { key: input.key, error: String(e) }) };
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
async function runConfigList(input) {
|
|
1545
|
+
const map = await parseDotenvFile(configPath(input.home));
|
|
1546
|
+
const entries = Object.entries(map).map(([key, value]) => ({ key, value: value ?? "" }));
|
|
1547
|
+
return { exitCode: ExitCode.OK, result: ok({ entries, humanHint: entries.map((e) => `${e.key}=${e.value}`).join("\n") }) };
|
|
1548
|
+
}
|
|
1549
|
+
async function runConfigPath(input) {
|
|
1550
|
+
const filePath = configPath(input.home);
|
|
1551
|
+
return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync(filePath), humanHint: filePath }) };
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// src/commands/doctor.ts
|
|
1555
|
+
import { existsSync as existsSync2, readdirSync, statSync } from "fs";
|
|
1556
|
+
import { join as join14 } from "path";
|
|
1557
|
+
import { execSync } from "child_process";
|
|
1558
|
+
function check(status, id, label, detail) {
|
|
1559
|
+
return { id, label, status, detail };
|
|
1560
|
+
}
|
|
1561
|
+
function checkNodeVersion() {
|
|
1562
|
+
const major = parseInt(process.version.slice(1).split(".")[0], 10);
|
|
1563
|
+
if (major >= 20) {
|
|
1564
|
+
return check("pass", "node_version", "Node.js version", `v${major} >= 20`);
|
|
1565
|
+
}
|
|
1566
|
+
return check("error", "node_version", "Node.js version", `Node.js v${major} is below minimum v20`);
|
|
1567
|
+
}
|
|
1568
|
+
function checkCliOnPath(argv) {
|
|
1569
|
+
if (argv.length >= 2 && argv[1].endsWith("cli.js")) {
|
|
1570
|
+
return check("warn", "cli_on_path", "skillwiki on PATH", "Running via node cli.js (dev mode) \u2014 PATH check skipped");
|
|
1571
|
+
}
|
|
1572
|
+
if (argv.length >= 2 && argv[1] === "skillwiki") {
|
|
1573
|
+
return check("pass", "cli_on_path", "skillwiki on PATH", "Running as skillwiki \u2014 already on PATH");
|
|
1574
|
+
}
|
|
1575
|
+
try {
|
|
1576
|
+
execSync("which skillwiki 2>/dev/null", { encoding: "utf8" }).trim();
|
|
1577
|
+
return check("pass", "cli_on_path", "skillwiki on PATH", "skillwiki found on PATH");
|
|
1578
|
+
} catch {
|
|
1579
|
+
return check("warn", "cli_on_path", "skillwiki on PATH", "skillwiki not found on PATH");
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
async function checkConfigFile(home) {
|
|
1583
|
+
const cfgPath = configPath(home);
|
|
1584
|
+
if (!existsSync2(cfgPath)) {
|
|
1585
|
+
return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
|
|
1586
|
+
}
|
|
1587
|
+
try {
|
|
1588
|
+
const map = await parseDotenvFile(cfgPath);
|
|
1589
|
+
const keys = Object.keys(map);
|
|
1590
|
+
return check("pass", "config_file", "Config file exists", `Found with keys: ${keys.length > 0 ? keys.join(", ") : "(none set)"}`);
|
|
1591
|
+
} catch (e) {
|
|
1592
|
+
return check("warn", "config_file", "Config file exists", `Failed to parse ${cfgPath}: ${String(e)}`);
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
function checkWikiPathExists(resolvedPath) {
|
|
1596
|
+
if (resolvedPath === void 0) {
|
|
1597
|
+
return check("error", "wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
1598
|
+
}
|
|
1599
|
+
if (existsSync2(resolvedPath) && statSync(resolvedPath).isDirectory()) {
|
|
1600
|
+
return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
|
|
1601
|
+
}
|
|
1602
|
+
return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
|
|
1603
|
+
}
|
|
1604
|
+
function checkVaultStructure(resolvedPath) {
|
|
1605
|
+
if (resolvedPath === void 0) {
|
|
1606
|
+
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
1607
|
+
}
|
|
1608
|
+
if (!existsSync2(resolvedPath)) {
|
|
1609
|
+
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
|
|
1610
|
+
}
|
|
1611
|
+
const missing = [];
|
|
1612
|
+
if (!existsSync2(join14(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
|
|
1613
|
+
for (const dir of ["raw", "entities", "concepts", "meta"]) {
|
|
1614
|
+
if (!existsSync2(join14(resolvedPath, dir))) missing.push(dir + "/");
|
|
1615
|
+
}
|
|
1616
|
+
if (missing.length === 0) {
|
|
1617
|
+
return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
|
|
1618
|
+
}
|
|
1619
|
+
return check("error", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")}`);
|
|
1620
|
+
}
|
|
1621
|
+
function checkSkillsInstalled(home) {
|
|
1622
|
+
const skillsDir = join14(home, ".claude", "skills");
|
|
1623
|
+
if (!existsSync2(skillsDir)) {
|
|
1624
|
+
return check("warn", "skills_installed", "Skills installed", `${skillsDir} not found`);
|
|
1625
|
+
}
|
|
1626
|
+
const found = findSkillMd(skillsDir);
|
|
1627
|
+
if (found.length > 0) {
|
|
1628
|
+
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found`);
|
|
1629
|
+
}
|
|
1630
|
+
return check("warn", "skills_installed", "Skills installed", "No SKILL.md files found in ~/.claude/skills/");
|
|
1631
|
+
}
|
|
1632
|
+
function findSkillMd(dir) {
|
|
1633
|
+
const results = [];
|
|
1634
|
+
let entries;
|
|
1635
|
+
try {
|
|
1636
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
1637
|
+
} catch {
|
|
1638
|
+
return results;
|
|
1639
|
+
}
|
|
1640
|
+
for (const entry of entries) {
|
|
1641
|
+
if (entry.isFile() && entry.name === "SKILL.md") {
|
|
1642
|
+
results.push(join14(dir, entry.name));
|
|
1643
|
+
} else if (entry.isDirectory()) {
|
|
1644
|
+
results.push(...findSkillMd(join14(dir, entry.name)));
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
return results;
|
|
1648
|
+
}
|
|
1649
|
+
async function runDoctor(input) {
|
|
1650
|
+
const checks = [];
|
|
1651
|
+
checks.push(checkNodeVersion());
|
|
1652
|
+
checks.push(checkCliOnPath(input.argv));
|
|
1653
|
+
checks.push(await checkConfigFile(input.home));
|
|
1654
|
+
const resolved = await resolveRuntimePath({ flag: void 0, envValue: input.envValue, home: input.home });
|
|
1655
|
+
if (resolved.ok) {
|
|
1656
|
+
checks.push(check("pass", "wiki_path_set", "WIKI_PATH configured", `Resolved via ${resolved.data.source}: ${resolved.data.path}`));
|
|
1657
|
+
} else {
|
|
1658
|
+
checks.push(check("error", "wiki_path_set", "WIKI_PATH configured", "No vault configured. Run `skillwiki init` or pass --vault."));
|
|
1659
|
+
}
|
|
1660
|
+
const resolvedPath = resolved.ok ? resolved.data.path : void 0;
|
|
1661
|
+
checks.push(checkWikiPathExists(resolvedPath));
|
|
1662
|
+
checks.push(checkVaultStructure(resolvedPath));
|
|
1663
|
+
checks.push(checkSkillsInstalled(input.home));
|
|
1664
|
+
const summary = {
|
|
1665
|
+
pass: checks.filter((c) => c.status === "pass").length,
|
|
1666
|
+
warn: checks.filter((c) => c.status === "warn").length,
|
|
1667
|
+
error: checks.filter((c) => c.status === "error").length
|
|
1668
|
+
};
|
|
1669
|
+
const exitCode = summary.error > 0 ? ExitCode.DOCTOR_HAS_ERRORS : summary.warn > 0 ? ExitCode.DOCTOR_HAS_WARNINGS : ExitCode.OK;
|
|
1670
|
+
const statusIcon = { pass: "\u2713", warn: "\u26A0", error: "\u2717" };
|
|
1671
|
+
const lines = checks.map((c) => {
|
|
1672
|
+
const icon = statusIcon[c.status];
|
|
1673
|
+
const padded = c.label.padEnd(24);
|
|
1674
|
+
return ` ${icon} ${padded} ${c.detail}`;
|
|
1675
|
+
});
|
|
1676
|
+
lines.push("");
|
|
1677
|
+
lines.push(`${summary.pass} pass \xB7 ${summary.warn} warn \xB7 ${summary.error} error`);
|
|
1678
|
+
const humanHint = lines.join("\n");
|
|
1679
|
+
return { exitCode, result: ok({ checks, summary, humanHint }) };
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// src/commands/archive.ts
|
|
1683
|
+
import { rename as rename3, mkdir as mkdir5, readFile as readFile13, writeFile as writeFile6 } from "fs/promises";
|
|
1684
|
+
import { join as join15, dirname as dirname6 } from "path";
|
|
1685
|
+
async function runArchive(input) {
|
|
1686
|
+
const scan = await scanVault(input.vault);
|
|
1687
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1688
|
+
let relPath;
|
|
1689
|
+
if (input.page.includes("/")) {
|
|
1690
|
+
relPath = scan.data.typedKnowledge.find((p) => p.relPath === input.page)?.relPath;
|
|
1691
|
+
} else {
|
|
1692
|
+
relPath = scan.data.typedKnowledge.find((p) => p.relPath.replace(/\.md$/, "").split("/").pop() === input.page)?.relPath;
|
|
1693
|
+
}
|
|
1694
|
+
if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
|
|
1695
|
+
if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
|
|
1696
|
+
const archivePath = join15("_archive", relPath);
|
|
1697
|
+
await mkdir5(dirname6(join15(input.vault, archivePath)), { recursive: true });
|
|
1698
|
+
let indexUpdated = false;
|
|
1699
|
+
const indexPath = join15(input.vault, "index.md");
|
|
1700
|
+
try {
|
|
1701
|
+
const idx = await readFile13(indexPath, "utf8");
|
|
1702
|
+
const slug = relPath.replace(/\.md$/, "").split("/").pop();
|
|
1703
|
+
const originalLines = idx.split("\n");
|
|
1704
|
+
const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
|
|
1705
|
+
if (filtered.length !== originalLines.length) {
|
|
1706
|
+
await writeFile6(indexPath, filtered.join("\n"), "utf8");
|
|
1707
|
+
indexUpdated = true;
|
|
1708
|
+
}
|
|
1709
|
+
} catch (e) {
|
|
1710
|
+
if (e?.code !== "ENOENT") throw e;
|
|
1711
|
+
}
|
|
1712
|
+
await rename3(join15(input.vault, relPath), join15(input.vault, archivePath));
|
|
1713
|
+
return { exitCode: ExitCode.OK, result: ok({ archived_from: relPath, archived_to: archivePath, index_updated: indexUpdated, humanHint: `${relPath} -> ${archivePath}${indexUpdated ? " (index updated)" : ""}` }) };
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// src/commands/drift.ts
|
|
1717
|
+
import { createHash as createHash2 } from "crypto";
|
|
1718
|
+
|
|
1719
|
+
// src/utils/fetch.ts
|
|
1720
|
+
async function controlledFetch(url, opts) {
|
|
1721
|
+
let current = url;
|
|
1722
|
+
for (let hop = 0; hop <= opts.maxRedirects; hop++) {
|
|
1723
|
+
const guard = runFetchGuardSync({ url: current });
|
|
1724
|
+
if (!guard.result.ok) return guard.result;
|
|
1725
|
+
const ctrl = new AbortController();
|
|
1726
|
+
const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs);
|
|
1727
|
+
let res;
|
|
1728
|
+
try {
|
|
1729
|
+
res = await fetch(current, { redirect: "manual", signal: ctrl.signal });
|
|
1730
|
+
} catch (e) {
|
|
1731
|
+
clearTimeout(timer);
|
|
1732
|
+
if (e?.name === "AbortError") return err("FETCH_TIMEOUT", { url: current });
|
|
1733
|
+
return err("FETCH_FAILED", { message: String(e) });
|
|
1734
|
+
}
|
|
1735
|
+
clearTimeout(timer);
|
|
1736
|
+
if (res.status >= 300 && res.status < 400) {
|
|
1737
|
+
const loc = res.headers.get("location");
|
|
1738
|
+
if (!loc) return err("FETCH_FAILED", { reason: "redirect without Location" });
|
|
1739
|
+
current = new URL(loc, current).toString();
|
|
1740
|
+
continue;
|
|
1741
|
+
}
|
|
1742
|
+
const declared = Number(res.headers.get("content-length") ?? "0");
|
|
1743
|
+
if (declared > opts.maxBytes) return err("FETCH_TOO_LARGE", { declared, limit: opts.maxBytes });
|
|
1744
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
1745
|
+
if (buf.byteLength > opts.maxBytes) return err("FETCH_TOO_LARGE", { actual: buf.byteLength, limit: opts.maxBytes });
|
|
1746
|
+
return ok({ url: current, status: res.status, body: new TextDecoder().decode(buf), bytes: buf.byteLength });
|
|
1747
|
+
}
|
|
1748
|
+
return err("FETCH_FAILED", { reason: "too many redirects", limit: opts.maxRedirects });
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
// src/commands/drift.ts
|
|
1752
|
+
var FETCH_OPTS = { timeoutMs: 1e4, maxBytes: 5e6, maxRedirects: 5 };
|
|
1753
|
+
async function runDrift(input) {
|
|
1754
|
+
const doFetch = input.fetchFn ?? controlledFetch;
|
|
1755
|
+
const scan = await scanVault(input.vault);
|
|
1756
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1757
|
+
const results = [];
|
|
1758
|
+
for (const raw of scan.data.raw) {
|
|
1759
|
+
const fm = extractFrontmatter(await readPage(raw));
|
|
1760
|
+
if (!fm.ok) continue;
|
|
1761
|
+
const sourceUrl = typeof fm.data.source_url === "string" ? fm.data.source_url : null;
|
|
1762
|
+
const storedHash = typeof fm.data.sha256 === "string" ? fm.data.sha256 : null;
|
|
1763
|
+
if (!sourceUrl || !storedHash) continue;
|
|
1764
|
+
const resp = await doFetch(sourceUrl, FETCH_OPTS);
|
|
1765
|
+
if (!resp.ok) {
|
|
1766
|
+
results.push({
|
|
1767
|
+
raw_path: raw.relPath,
|
|
1768
|
+
source_url: sourceUrl,
|
|
1769
|
+
stored_sha256: storedHash,
|
|
1770
|
+
current_sha256: null,
|
|
1771
|
+
status: "fetch_failed",
|
|
1772
|
+
fetch_error: resp.error
|
|
1773
|
+
});
|
|
1774
|
+
continue;
|
|
1775
|
+
}
|
|
1776
|
+
const currentHash = createHash2("sha256").update(Buffer.from(resp.data.body, "utf8")).digest("hex");
|
|
1777
|
+
const drifted2 = currentHash !== storedHash;
|
|
1778
|
+
results.push({
|
|
1779
|
+
raw_path: raw.relPath,
|
|
1780
|
+
source_url: sourceUrl,
|
|
1781
|
+
stored_sha256: storedHash,
|
|
1782
|
+
current_sha256: currentHash,
|
|
1783
|
+
status: drifted2 ? "drifted" : "unchanged"
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
const drifted = results.filter((r) => r.status === "drifted");
|
|
1787
|
+
const fetchFailed = results.filter((r) => r.status === "fetch_failed");
|
|
1788
|
+
const unchanged = results.filter((r) => r.status === "unchanged").length;
|
|
1789
|
+
const exitCode = drifted.length > 0 ? ExitCode.DRIFT_DETECTED : ExitCode.OK;
|
|
1790
|
+
const hintLines = [`scanned: ${results.length}, unchanged: ${unchanged}`];
|
|
1791
|
+
if (drifted.length > 0) hintLines.push(`drifted: ${drifted.length}`, ...drifted.map((d) => ` ${d.raw_path}`));
|
|
1792
|
+
if (fetchFailed.length > 0) hintLines.push(`fetch_failed: ${fetchFailed.length}`, ...fetchFailed.map((f) => ` ${f.raw_path}: ${f.fetch_error}`));
|
|
1793
|
+
return {
|
|
1794
|
+
exitCode,
|
|
1795
|
+
result: ok({ scanned: results.length, drifted, fetch_failed: fetchFailed, unchanged, humanHint: hintLines.join("\n") })
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1239
1799
|
// src/cli.ts
|
|
1800
|
+
var pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
1240
1801
|
var program = new Command();
|
|
1241
|
-
program.name("skillwiki").description("Deterministic helpers for CodeWiki skills").version(
|
|
1802
|
+
program.name("skillwiki").description("Deterministic helpers for CodeWiki skills").version(pkg.version);
|
|
1242
1803
|
program.option("--human", "render terminal-readable output instead of JSON");
|
|
1243
1804
|
function emit(r) {
|
|
1244
1805
|
if (program.opts().human) printHuman(r.result);
|
|
@@ -1257,7 +1818,7 @@ program.command("orphans [vault]").action(async (vault) => emit(await runOrphans
|
|
|
1257
1818
|
})));
|
|
1258
1819
|
program.command("audit <file>").action(async (file) => emit(await runAudit({ file })));
|
|
1259
1820
|
program.command("install").option("--target <dir>", "target install directory", `${process.env.HOME ?? ""}/.claude/skills/`).option("--dry-run", "preview only", false).option("--skills-root <dir>", "source skills directory (defaults to packaged)").action(async (opts) => {
|
|
1260
|
-
const skillsRoot = opts.skillsRoot ?? new URL("
|
|
1821
|
+
const skillsRoot = opts.skillsRoot ?? new URL("../skills/", import.meta.url).pathname;
|
|
1261
1822
|
emit(await runInstall({ skillsRoot, target: opts.target, dryRun: !!opts.dryRun }));
|
|
1262
1823
|
});
|
|
1263
1824
|
program.command("path").option("--vault <dir>", "explicit vault override (runtime)").option("--target <dir>", "explicit target override (init-time)").option("--init-time", "use init-time chain instead of runtime", false).option("--explain", "include resolution chain in output", false).action(async (opts) => {
|
|
@@ -1279,7 +1840,7 @@ program.command("lang").option("--lang <code>", "explicit language override").op
|
|
|
1279
1840
|
explain: !!opts.explain
|
|
1280
1841
|
}));
|
|
1281
1842
|
});
|
|
1282
|
-
program.command("init").option("--target <dir>", "explicit target directory").requiredOption("--domain <text>", "knowledge domain seed").option("--taxonomy <csv>", "comma-separated tag list").option("--lang <code>", "output language (BCP 47 or alias)").option("--force", "override existing target / env conflict", false).action(async (opts) => {
|
|
1843
|
+
program.command("init").option("--target <dir>", "explicit target directory").requiredOption("--domain <text>", "knowledge domain seed").option("--taxonomy <csv>", "comma-separated tag list").option("--lang <code>", "output language (BCP 47 or alias)").option("--force", "override existing target / env conflict", false).option("--no-env", "skip writing ~/.skillwiki/.env").action(async (opts) => {
|
|
1283
1844
|
const templates = new URL("../templates/", import.meta.url).pathname;
|
|
1284
1845
|
const taxonomy = typeof opts.taxonomy === "string" ? opts.taxonomy.split(",").map((s) => s.trim()).filter((s) => s.length > 0) : void 0;
|
|
1285
1846
|
emit(await runInit({
|
|
@@ -1290,7 +1851,8 @@ program.command("init").option("--target <dir>", "explicit target directory").re
|
|
|
1290
1851
|
domain: opts.domain,
|
|
1291
1852
|
taxonomy,
|
|
1292
1853
|
lang: opts.lang,
|
|
1293
|
-
force: !!opts.force
|
|
1854
|
+
force: !!opts.force,
|
|
1855
|
+
noEnv: opts.env === false
|
|
1294
1856
|
}));
|
|
1295
1857
|
});
|
|
1296
1858
|
async function resolveVaultArg(arg) {
|
|
@@ -1344,6 +1906,31 @@ program.command("lint [vault]").option("--days <n>", "stale threshold", (s) => p
|
|
|
1344
1906
|
logThreshold: opts.logThreshold
|
|
1345
1907
|
}));
|
|
1346
1908
|
});
|
|
1909
|
+
var configCmd = program.command("config").description("manage skillwiki configuration");
|
|
1910
|
+
configCmd.command("get <key>").description("print the value of a config key").action(async (key) => emit(await runConfigGet({ key, home: process.env.HOME ?? "" })));
|
|
1911
|
+
configCmd.command("set <key> <value>").description("set a config key value").action(async (key, value) => emit(await runConfigSet({ key, value, home: process.env.HOME ?? "" })));
|
|
1912
|
+
configCmd.command("list").description("list all config key=value pairs").action(async () => emit(await runConfigList({ home: process.env.HOME ?? "" })));
|
|
1913
|
+
configCmd.command("path").description("print the config file path").action(async () => emit(await runConfigPath({ home: process.env.HOME ?? "" })));
|
|
1914
|
+
program.command("doctor").description("diagnose skillwiki setup issues").action(async () => emit(await runDoctor({
|
|
1915
|
+
home: process.env.HOME ?? "",
|
|
1916
|
+
envValue: process.env.WIKI_PATH,
|
|
1917
|
+
argv: process.argv
|
|
1918
|
+
})));
|
|
1919
|
+
program.command("archive <page> [vault]").description("archive a typed-knowledge page").action(async (page, vault) => {
|
|
1920
|
+
const v = await resolveVaultArg(vault);
|
|
1921
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1922
|
+
else emit(await runArchive({ vault: v.vault, page }));
|
|
1923
|
+
});
|
|
1924
|
+
program.command("drift [vault]").description("detect content drift in raw sources").action(async (vault) => {
|
|
1925
|
+
const v = await resolveVaultArg(vault);
|
|
1926
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1927
|
+
else emit(await runDrift({ vault: v.vault }));
|
|
1928
|
+
});
|
|
1929
|
+
program.command("dedup [vault]").description("detect duplicate raw sources by sha256").action(async (vault) => {
|
|
1930
|
+
const v = await resolveVaultArg(vault);
|
|
1931
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1932
|
+
else emit(await runDedup({ vault: v.vault }));
|
|
1933
|
+
});
|
|
1347
1934
|
program.parseAsync(process.argv).catch((e) => {
|
|
1348
1935
|
process.stdout.write(JSON.stringify({ ok: false, error: "INTERNAL", detail: { message: String(e) } }) + "\n");
|
|
1349
1936
|
process.exit(1);
|