skillwiki 0.2.0-beta.3 → 0.2.0-beta.31
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 +1514 -172
- package/package.json +19 -5
- package/skills/.claude-plugin/plugin.json +3 -2
- package/skills/bin/skillwiki +5 -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 +9 -2
- package/skills/proj-decide/SKILL.md +1 -1
- package/skills/proj-distill/SKILL.md +26 -4
- package/skills/proj-work/SKILL.md +20 -1
- package/skills/using-skillwiki/SKILL.md +122 -0
- package/skills/wiki-adapter-prd/SKILL.md +87 -0
- package/skills/wiki-archive/SKILL.md +43 -0
- package/skills/wiki-audit/SKILL.md +1 -1
- package/skills/wiki-crystallize/SKILL.md +1 -1
- package/skills/wiki-ingest/SKILL.md +15 -4
- package/skills/wiki-lint/SKILL.md +1 -1
- package/skills/wiki-query/SKILL.md +2 -2
- package/skills/wiki-reingest/SKILL.md +54 -0
- package/templates/SCHEMA.md +18 -1
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
semverGt
|
|
4
|
+
} from "./chunk-XM5IYZX7.js";
|
|
2
5
|
|
|
3
6
|
// src/cli.ts
|
|
7
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
4
8
|
import { Command } from "commander";
|
|
5
9
|
|
|
6
10
|
// src/utils/output.ts
|
|
@@ -9,9 +13,14 @@ function printJson(r) {
|
|
|
9
13
|
}
|
|
10
14
|
function printHuman(r) {
|
|
11
15
|
if (r.ok) {
|
|
12
|
-
|
|
16
|
+
if (typeof r.data === "object" && r.data !== null && "humanHint" in r.data) {
|
|
17
|
+
process.stdout.write(`${r.data.humanHint}
|
|
18
|
+
`);
|
|
19
|
+
} else {
|
|
20
|
+
process.stdout.write(`OK
|
|
13
21
|
${formatData(r.data)}
|
|
14
22
|
`);
|
|
23
|
+
}
|
|
15
24
|
} else {
|
|
16
25
|
process.stdout.write(`ERR ${r.error}
|
|
17
26
|
${r.detail !== void 0 ? formatData(r.detail) + "\n" : ""}`);
|
|
@@ -53,7 +62,18 @@ var ExitCode = {
|
|
|
53
62
|
LINT_HAS_WARNINGS: 22,
|
|
54
63
|
LINT_HAS_ERRORS: 23,
|
|
55
64
|
ENV_WRITE_CONFLICT: 24,
|
|
56
|
-
NO_VAULT_CONFIGURED: 25
|
|
65
|
+
NO_VAULT_CONFIGURED: 25,
|
|
66
|
+
INVALID_CONFIG_KEY: 26,
|
|
67
|
+
CONFIG_WRITE_FAILED: 27,
|
|
68
|
+
DOCTOR_HAS_WARNINGS: 28,
|
|
69
|
+
DOCTOR_HAS_ERRORS: 29,
|
|
70
|
+
ARCHIVE_TARGET_NOT_FOUND: 30,
|
|
71
|
+
ARCHIVE_ALREADY_ARCHIVED: 31,
|
|
72
|
+
DRIFT_DETECTED: 32,
|
|
73
|
+
RAW_DEDUP_DETECTED: 33,
|
|
74
|
+
MIGRATION_APPLIED: 34,
|
|
75
|
+
UNKNOWN_WIKI_PROFILE: 35,
|
|
76
|
+
DEDUP_APPLIED: 36
|
|
57
77
|
};
|
|
58
78
|
|
|
59
79
|
// ../shared/src/json-output.ts
|
|
@@ -93,10 +113,10 @@ var TypedKnowledgeSchema = z.object({
|
|
|
93
113
|
});
|
|
94
114
|
var sha256Hex = z.string().regex(/^[0-9a-f]{64}$/);
|
|
95
115
|
var RawSourceSchema = z.object({
|
|
96
|
-
title: z.string().min(1),
|
|
97
|
-
source_url: z.string().
|
|
116
|
+
title: z.string().min(1).optional(),
|
|
117
|
+
source_url: z.string().nullable(),
|
|
98
118
|
ingested: isoDate,
|
|
99
|
-
ingested_by: z.enum(["wiki-ingest", "proj-work", "manual"]),
|
|
119
|
+
ingested_by: z.enum(["wiki-ingest", "proj-work", "manual"]).optional(),
|
|
100
120
|
sha256: sha256Hex,
|
|
101
121
|
project: wikilink.optional(),
|
|
102
122
|
work_item: wikilink.optional(),
|
|
@@ -231,7 +251,7 @@ async function runHash(input) {
|
|
|
231
251
|
const sha256 = createHash("sha256").update(bodyBytes).digest("hex");
|
|
232
252
|
return {
|
|
233
253
|
exitCode: ExitCode.OK,
|
|
234
|
-
result: ok({ path: input.file, sha256, byte_count: bodyBytes.byteLength })
|
|
254
|
+
result: ok({ path: input.file, sha256, byte_count: bodyBytes.byteLength, humanHint: sha256 })
|
|
235
255
|
};
|
|
236
256
|
}
|
|
237
257
|
|
|
@@ -261,7 +281,7 @@ function runFetchGuardSync(input) {
|
|
|
261
281
|
result: err("HOST_BLOCKED", { sanitized_url: sanitized, host: u.hostname })
|
|
262
282
|
};
|
|
263
283
|
}
|
|
264
|
-
return { exitCode: ExitCode.OK, result: ok({ allowed: true, sanitized_url: sanitized }) };
|
|
284
|
+
return { exitCode: ExitCode.OK, result: ok({ allowed: true, sanitized_url: sanitized, humanHint: `ALLOWED: ${sanitized}` }) };
|
|
265
285
|
}
|
|
266
286
|
function sanitizeUrl(u) {
|
|
267
287
|
const clone = new URL(u.toString());
|
|
@@ -301,17 +321,18 @@ async function runValidate(input) {
|
|
|
301
321
|
}
|
|
302
322
|
const det = detectSchema(fm.data);
|
|
303
323
|
if (!det.schema) {
|
|
304
|
-
return { exitCode: ExitCode.SCHEMA_NOT_DETECTED, result: ok({ schema: null, valid: false, errors: [] }) };
|
|
324
|
+
return { exitCode: ExitCode.SCHEMA_NOT_DETECTED, result: ok({ schema: null, valid: false, errors: [], humanHint: "schema not detected" }) };
|
|
305
325
|
}
|
|
306
326
|
const parsed = SCHEMAS[det.schema].safeParse(fm.data);
|
|
307
327
|
if (!parsed.success) {
|
|
308
328
|
const errors = parsed.error.issues.map((i) => ({ path: i.path.join("."), message: i.message }));
|
|
309
329
|
return {
|
|
310
330
|
exitCode: ExitCode.INVALID_FRONTMATTER,
|
|
311
|
-
result: ok({ schema: det.schema, valid: false, errors })
|
|
331
|
+
result: ok({ schema: det.schema, valid: false, errors, humanHint: `INVALID (${det.schema})
|
|
332
|
+
${errors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}` })
|
|
312
333
|
};
|
|
313
334
|
}
|
|
314
|
-
return { exitCode: ExitCode.OK, result: ok({ schema: det.schema, valid: true, errors: [] }) };
|
|
335
|
+
return { exitCode: ExitCode.OK, result: ok({ schema: det.schema, valid: true, errors: [], humanHint: `VALID (${det.schema})` }) };
|
|
315
336
|
}
|
|
316
337
|
|
|
317
338
|
// src/commands/graph.ts
|
|
@@ -397,7 +418,8 @@ async function runGraphBuild(input) {
|
|
|
397
418
|
}
|
|
398
419
|
return {
|
|
399
420
|
exitCode: ExitCode.OK,
|
|
400
|
-
result: ok({ out_path: input.out, node_count: scan.data.typedKnowledge.length, edge_count }
|
|
421
|
+
result: ok({ out_path: input.out, node_count: scan.data.typedKnowledge.length, edge_count, humanHint: `nodes: ${scan.data.typedKnowledge.length}, edges: ${edge_count}
|
|
422
|
+
written: ${input.out}` })
|
|
401
423
|
};
|
|
402
424
|
}
|
|
403
425
|
function computeAdamicAdar(adj) {
|
|
@@ -472,22 +494,29 @@ async function runOverlap(input) {
|
|
|
472
494
|
}
|
|
473
495
|
return { id, members, score };
|
|
474
496
|
});
|
|
475
|
-
|
|
497
|
+
const humanHint = clusters.length === 0 ? "no overlap clusters found" : clusters.map((c) => `cluster (${c.members.length} pages, score ${c.score}): ${c.members.join(", ")}`).join("\n");
|
|
498
|
+
return { exitCode: ExitCode.OK, result: ok({ clusters, humanHint }) };
|
|
476
499
|
}
|
|
477
500
|
|
|
478
501
|
// src/utils/wiki-path.ts
|
|
479
502
|
import { join as join2 } from "path";
|
|
480
503
|
|
|
481
504
|
// src/utils/dotenv.ts
|
|
482
|
-
import { readFile as readFile4 } from "fs/promises";
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
505
|
+
import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
506
|
+
import { dirname as dirname2 } from "path";
|
|
507
|
+
var CONFIG_KEYS = ["WIKI_PATH", "WIKI_LANG"];
|
|
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
|
+
}
|
|
519
|
+
function parseDotenvText(text) {
|
|
491
520
|
const out = {};
|
|
492
521
|
for (const rawLine of text.split(/\r?\n/)) {
|
|
493
522
|
const line = rawLine.trim();
|
|
@@ -496,12 +525,65 @@ async function parseDotenvFile(path) {
|
|
|
496
525
|
if (eq <= 0) continue;
|
|
497
526
|
const key = line.slice(0, eq).trim();
|
|
498
527
|
const value = line.slice(eq + 1).trim();
|
|
499
|
-
if (!
|
|
528
|
+
if (!_whitelist.has(key) && !isValidWikiProfileKey(key)) continue;
|
|
500
529
|
if (value.length === 0) continue;
|
|
501
530
|
out[key] = value;
|
|
502
531
|
}
|
|
503
532
|
return out;
|
|
504
533
|
}
|
|
534
|
+
async function parseDotenvFile(path) {
|
|
535
|
+
let text;
|
|
536
|
+
try {
|
|
537
|
+
text = await readFile4(path, "utf8");
|
|
538
|
+
} catch {
|
|
539
|
+
return {};
|
|
540
|
+
}
|
|
541
|
+
return parseDotenvText(text);
|
|
542
|
+
}
|
|
543
|
+
async function writeDotenv(filePath, entries, originalContent) {
|
|
544
|
+
const lines = originalContent !== void 0 ? updateLines(originalContent, entries) : freshLines(entries);
|
|
545
|
+
await mkdir2(dirname2(filePath), { recursive: true });
|
|
546
|
+
await writeFile2(filePath, lines.join("\n") + "\n", "utf8");
|
|
547
|
+
}
|
|
548
|
+
function freshLines(entries) {
|
|
549
|
+
const out = [];
|
|
550
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
551
|
+
if (value !== void 0) out.push(`${key}=${value}`);
|
|
552
|
+
}
|
|
553
|
+
return out;
|
|
554
|
+
}
|
|
555
|
+
function updateLines(originalContent, entries) {
|
|
556
|
+
let rawLines = originalContent.split(/\r?\n/);
|
|
557
|
+
if (rawLines.length > 0 && rawLines[rawLines.length - 1] === "") {
|
|
558
|
+
rawLines = rawLines.slice(0, -1);
|
|
559
|
+
}
|
|
560
|
+
const keysToWrite = new Set(Object.keys(entries));
|
|
561
|
+
const out = [];
|
|
562
|
+
for (const line of rawLines) {
|
|
563
|
+
const trimmed = line.trim();
|
|
564
|
+
if (trimmed.length === 0 || trimmed.startsWith("#")) {
|
|
565
|
+
out.push(line);
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
const eq = trimmed.indexOf("=");
|
|
569
|
+
if (eq <= 0) {
|
|
570
|
+
out.push(line);
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
const key = trimmed.slice(0, eq).trim();
|
|
574
|
+
if (keysToWrite.has(key)) {
|
|
575
|
+
out.push(`${key}=${entries[key]}`);
|
|
576
|
+
keysToWrite.delete(key);
|
|
577
|
+
} else {
|
|
578
|
+
out.push(line);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
for (const key of keysToWrite) {
|
|
582
|
+
const value = entries[key];
|
|
583
|
+
if (value !== void 0) out.push(`${key}=${value}`);
|
|
584
|
+
}
|
|
585
|
+
return out;
|
|
586
|
+
}
|
|
505
587
|
|
|
506
588
|
// src/utils/wiki-path.ts
|
|
507
589
|
async function resolveInitTimePath(input) {
|
|
@@ -528,6 +610,14 @@ async function resolveInitTimePath(input) {
|
|
|
528
610
|
return { path: hermes.WIKI_PATH, source: "hermes-dotenv", ...input.explain ? { chain } : {} };
|
|
529
611
|
}
|
|
530
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 });
|
|
531
621
|
const fallback = join2(input.home, "wiki");
|
|
532
622
|
if (input.explain) chain.push({ source: "default", matched: true, value: fallback });
|
|
533
623
|
return { path: fallback, source: "default", ...input.explain ? { chain } : {} };
|
|
@@ -539,15 +629,73 @@ async function resolveRuntimePath(input) {
|
|
|
539
629
|
return ok({ path: input.flag, source: "flag", ...input.explain ? { chain } : {} });
|
|
540
630
|
}
|
|
541
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 });
|
|
542
670
|
if (input.envValue !== void 0 && input.envValue.length > 0) {
|
|
543
671
|
if (input.explain) chain.push({ source: "env", matched: true, value: input.envValue });
|
|
544
672
|
return ok({ path: input.envValue, source: "env", ...input.explain ? { chain } : {} });
|
|
545
673
|
}
|
|
546
674
|
if (input.explain) chain.push({ source: "env", matched: false });
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
if (
|
|
550
|
-
|
|
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 } : {} });
|
|
551
699
|
}
|
|
552
700
|
if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: false });
|
|
553
701
|
return err("NO_VAULT_CONFIGURED", {
|
|
@@ -561,15 +709,21 @@ async function runOrphans(input) {
|
|
|
561
709
|
if (input.vault) {
|
|
562
710
|
vault = input.vault;
|
|
563
711
|
} else {
|
|
564
|
-
const r = await resolveRuntimePath({ flag: void 0, envValue: input.envValue, home: input.home ?? "" });
|
|
565
|
-
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
|
+
}
|
|
566
717
|
vault = r.data.path;
|
|
567
718
|
}
|
|
568
719
|
const scan = await scanVault(vault);
|
|
569
720
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
570
721
|
const slugToPath = {};
|
|
571
722
|
for (const p of scan.data.typedKnowledge) {
|
|
572
|
-
|
|
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;
|
|
573
727
|
}
|
|
574
728
|
const adj = {};
|
|
575
729
|
for (const p of scan.data.typedKnowledge) adj[p.relPath] = /* @__PURE__ */ new Set();
|
|
@@ -578,7 +732,7 @@ async function runOrphans(input) {
|
|
|
578
732
|
const split = splitFrontmatter(text);
|
|
579
733
|
const body = split.ok ? split.data.body : text;
|
|
580
734
|
for (const slug of extractBodyWikilinks(body)) {
|
|
581
|
-
const tgt = slugToPath[slug.split("/").pop()];
|
|
735
|
+
const tgt = slugToPath[slug] ?? slugToPath[slug.split("/").pop()];
|
|
582
736
|
if (tgt) {
|
|
583
737
|
adj[p.relPath].add(tgt);
|
|
584
738
|
adj[tgt].add(p.relPath);
|
|
@@ -609,7 +763,11 @@ async function runOrphans(input) {
|
|
|
609
763
|
}
|
|
610
764
|
}
|
|
611
765
|
}
|
|
612
|
-
|
|
766
|
+
const hintLines = [];
|
|
767
|
+
if (orphans.length > 0) hintLines.push(`orphans: ${orphans.length}`, ...orphans.map((o) => ` ${o}`));
|
|
768
|
+
if (bridges.length > 0) hintLines.push(`bridges: ${bridges.length}`, ...bridges.map((b) => ` ${b.path}`));
|
|
769
|
+
if (hintLines.length === 0) hintLines.push("no orphans or bridges");
|
|
770
|
+
return { exitCode: ExitCode.OK, result: ok({ orphans, bridges, humanHint: hintLines.join("\n") }) };
|
|
613
771
|
}
|
|
614
772
|
function simulateRemoval(adj, removed) {
|
|
615
773
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -630,20 +788,96 @@ function simulateRemoval(adj, removed) {
|
|
|
630
788
|
|
|
631
789
|
// src/commands/audit.ts
|
|
632
790
|
import { readFile as readFile5, stat as stat2 } from "fs/promises";
|
|
633
|
-
import { dirname as
|
|
791
|
+
import { dirname as dirname3, resolve, join as join3 } from "path";
|
|
634
792
|
|
|
635
793
|
// src/parsers/citations.ts
|
|
636
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
|
+
}
|
|
637
803
|
function extractCitationMarkers(body) {
|
|
638
|
-
const stripped = body
|
|
804
|
+
const stripped = stripFences(body);
|
|
639
805
|
const out = [];
|
|
640
|
-
const re = /\^\[(raw\/[^\]]+)\]/g;
|
|
641
806
|
let m;
|
|
642
|
-
while ((m =
|
|
807
|
+
while ((m = MARKER_RE.exec(stripped)) !== null) {
|
|
643
808
|
out.push({ marker: m[0], target: m[1] });
|
|
644
809
|
}
|
|
645
810
|
return out;
|
|
646
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
|
+
}
|
|
647
881
|
|
|
648
882
|
// src/commands/audit.ts
|
|
649
883
|
async function runAudit(input) {
|
|
@@ -657,7 +891,7 @@ async function runAudit(input) {
|
|
|
657
891
|
if (!fm.ok) return { exitCode: ExitCode.INVALID_FRONTMATTER, result: fm };
|
|
658
892
|
const split = splitFrontmatter(text);
|
|
659
893
|
const body = split.ok ? split.data.body : text;
|
|
660
|
-
const vault = await findVaultRoot(
|
|
894
|
+
const vault = await findVaultRoot(dirname3(resolve(input.file)));
|
|
661
895
|
if (!vault) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID") };
|
|
662
896
|
const markers = extractCitationMarkers(body);
|
|
663
897
|
const resolved = await Promise.all(markers.map(async (m) => {
|
|
@@ -668,17 +902,40 @@ async function runAudit(input) {
|
|
|
668
902
|
return { ...m, resolved: false };
|
|
669
903
|
}
|
|
670
904
|
}));
|
|
671
|
-
const sources = fm.data.sources ?? [];
|
|
905
|
+
const sources = (fm.data.sources ?? []).map((s) => s.replace(/^\^\[/, "").replace(/\]$/, ""));
|
|
672
906
|
const referenced = new Set(resolved.map((m) => m.target));
|
|
673
907
|
const unused_sources = sources.filter((s) => !referenced.has(s));
|
|
674
908
|
const missing_from_sources = [...referenced].filter((t) => !sources.includes(t));
|
|
909
|
+
const broken = resolved.filter((m) => !m.resolved);
|
|
910
|
+
const footerMatch = body.match(/\n## Sources\n([\s\S]*)$/);
|
|
911
|
+
let footer_consistency;
|
|
912
|
+
if (footerMatch) {
|
|
913
|
+
const footerTargets = /* @__PURE__ */ new Set();
|
|
914
|
+
const footerRe = /\^\[(raw\/[^\]]+)\]/g;
|
|
915
|
+
let mm;
|
|
916
|
+
while ((mm = footerRe.exec(footerMatch[1])) !== null) footerTargets.add(mm[1]);
|
|
917
|
+
const bodyTargets = new Set(resolved.map((m) => m.target));
|
|
918
|
+
const missing_from_footer = [...bodyTargets].filter((t) => !footerTargets.has(t));
|
|
919
|
+
const extra_in_footer = [...footerTargets].filter((t) => !bodyTargets.has(t));
|
|
920
|
+
footer_consistency = { missing_from_footer, extra_in_footer };
|
|
921
|
+
}
|
|
922
|
+
const hintLines = [];
|
|
923
|
+
hintLines.push(`markers: ${resolved.length}, broken: ${broken.length}`);
|
|
924
|
+
if (unused_sources.length > 0) hintLines.push(`unused_sources: ${unused_sources.length}`);
|
|
925
|
+
if (missing_from_sources.length > 0) hintLines.push(`missing_from_sources: ${missing_from_sources.length}`);
|
|
926
|
+
if (footer_consistency) {
|
|
927
|
+
if (footer_consistency.missing_from_footer.length > 0) hintLines.push(`missing_from_footer: ${footer_consistency.missing_from_footer.length}`);
|
|
928
|
+
if (footer_consistency.extra_in_footer.length > 0) hintLines.push(`extra_in_footer: ${footer_consistency.extra_in_footer.length}`);
|
|
929
|
+
}
|
|
930
|
+
if (broken.length === 0 && unused_sources.length === 0 && missing_from_sources.length === 0) hintLines.push("OK");
|
|
931
|
+
const humanHint = hintLines.join("\n");
|
|
675
932
|
if (resolved.some((m) => !m.resolved)) {
|
|
676
|
-
return { exitCode: ExitCode.UNRESOLVED_MARKERS, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources } }) };
|
|
933
|
+
return { exitCode: ExitCode.UNRESOLVED_MARKERS, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, footer_consistency, humanHint }) };
|
|
677
934
|
}
|
|
678
935
|
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 } }) };
|
|
936
|
+
return { exitCode: ExitCode.SOURCES_INCONSISTENT, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, footer_consistency, humanHint }) };
|
|
680
937
|
}
|
|
681
|
-
return { exitCode: ExitCode.OK, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources } }) };
|
|
938
|
+
return { exitCode: ExitCode.OK, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, footer_consistency, humanHint }) };
|
|
682
939
|
}
|
|
683
940
|
async function findVaultRoot(start) {
|
|
684
941
|
let cur = start;
|
|
@@ -688,7 +945,7 @@ async function findVaultRoot(start) {
|
|
|
688
945
|
return cur;
|
|
689
946
|
} catch {
|
|
690
947
|
}
|
|
691
|
-
const parent =
|
|
948
|
+
const parent = dirname3(cur);
|
|
692
949
|
if (parent === cur) return null;
|
|
693
950
|
cur = parent;
|
|
694
951
|
}
|
|
@@ -700,10 +957,10 @@ import { readdir as readdir2, stat as stat4 } from "fs/promises";
|
|
|
700
957
|
import { join as join4 } from "path";
|
|
701
958
|
|
|
702
959
|
// src/utils/install-fs.ts
|
|
703
|
-
import { copyFile, mkdir as
|
|
704
|
-
import { dirname as
|
|
960
|
+
import { copyFile, mkdir as mkdir3, rename, writeFile as writeFile3, stat as stat3 } from "fs/promises";
|
|
961
|
+
import { dirname as dirname4 } from "path";
|
|
705
962
|
async function atomicCopyWithBackup(src, dst) {
|
|
706
|
-
await
|
|
963
|
+
await mkdir3(dirname4(dst), { recursive: true });
|
|
707
964
|
let backupPath = null;
|
|
708
965
|
try {
|
|
709
966
|
await stat3(dst);
|
|
@@ -721,9 +978,9 @@ async function atomicCopyWithBackup(src, dst) {
|
|
|
721
978
|
return ok({ copied: true, backupPath });
|
|
722
979
|
}
|
|
723
980
|
async function writeManifest(path, m) {
|
|
724
|
-
await
|
|
981
|
+
await mkdir3(dirname4(path), { recursive: true });
|
|
725
982
|
const enriched = { installed_at: (/* @__PURE__ */ new Date()).toISOString(), ...m };
|
|
726
|
-
await
|
|
983
|
+
await writeFile3(path, JSON.stringify(enriched, null, 2));
|
|
727
984
|
}
|
|
728
985
|
|
|
729
986
|
// src/commands/install.ts
|
|
@@ -756,9 +1013,28 @@ async function runInstall(input) {
|
|
|
756
1013
|
installed.push(dst);
|
|
757
1014
|
if (r.data.backupPath) backed_up.push(r.data.backupPath);
|
|
758
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
|
+
}
|
|
759
1030
|
const manifest_path = join4(input.target, "wiki-manifest.json");
|
|
760
1031
|
if (!input.dryRun) await writeManifest(manifest_path, { installed, backed_up });
|
|
761
|
-
|
|
1032
|
+
const hintLines = [
|
|
1033
|
+
`installed: ${installed.length}`,
|
|
1034
|
+
input.dryRun ? "(dry run)" : `backed up: ${backed_up.length}`,
|
|
1035
|
+
`manifest: ${manifest_path}`
|
|
1036
|
+
];
|
|
1037
|
+
return { exitCode: ExitCode.OK, result: ok({ installed, backed_up, manifest_path, humanHint: hintLines.join("\n") }) };
|
|
762
1038
|
}
|
|
763
1039
|
|
|
764
1040
|
// src/commands/path.ts
|
|
@@ -770,16 +1046,20 @@ async function runPath(input) {
|
|
|
770
1046
|
home: input.home,
|
|
771
1047
|
explain: input.explain
|
|
772
1048
|
});
|
|
773
|
-
return { exitCode: ExitCode.OK, result: ok({ path: r2.path, source: r2.source, ...r2.chain ? { chain: r2.chain } : {} }) };
|
|
1049
|
+
return { exitCode: ExitCode.OK, result: ok({ path: r2.path, source: r2.source, ...r2.chain ? { chain: r2.chain } : {}, humanHint: `${r2.path} (via ${r2.source})` }) };
|
|
774
1050
|
}
|
|
775
1051
|
const r = await resolveRuntimePath({
|
|
776
1052
|
flag: input.flag,
|
|
777
1053
|
envValue: input.envValue,
|
|
778
1054
|
home: input.home,
|
|
1055
|
+
wiki: input.wiki,
|
|
779
1056
|
explain: input.explain
|
|
780
1057
|
});
|
|
781
|
-
if (!r.ok)
|
|
782
|
-
|
|
1058
|
+
if (!r.ok) {
|
|
1059
|
+
const exitCode = r.error === "UNKNOWN_WIKI_PROFILE" ? ExitCode.UNKNOWN_WIKI_PROFILE : ExitCode.NO_VAULT_CONFIGURED;
|
|
1060
|
+
return { exitCode, result: r };
|
|
1061
|
+
}
|
|
1062
|
+
return { exitCode: ExitCode.OK, result: ok({ path: r.data.path, source: r.data.source, ...r.data.chain ? { chain: r.data.chain } : {}, humanHint: `${r.data.path} (via ${r.data.source})` }) };
|
|
783
1063
|
}
|
|
784
1064
|
|
|
785
1065
|
// src/utils/lang.ts
|
|
@@ -833,14 +1113,42 @@ async function runLang(input) {
|
|
|
833
1113
|
value: resolved.value,
|
|
834
1114
|
source: resolved.source,
|
|
835
1115
|
canonical: resolved.canonical,
|
|
836
|
-
...chain ? { chain } : {}
|
|
1116
|
+
...chain ? { chain } : {},
|
|
1117
|
+
humanHint: `${resolved.value} (via ${resolved.source})`
|
|
837
1118
|
})
|
|
838
1119
|
};
|
|
839
1120
|
}
|
|
840
1121
|
|
|
841
1122
|
// src/commands/init.ts
|
|
842
|
-
import { mkdir as
|
|
843
|
-
import { join as join7
|
|
1123
|
+
import { mkdir as mkdir4, readFile as readFile6, readdir as readdir3, writeFile as writeFile4 } from "fs/promises";
|
|
1124
|
+
import { join as join7 } from "path";
|
|
1125
|
+
|
|
1126
|
+
// src/parsers/taxonomy.ts
|
|
1127
|
+
import yaml2 from "js-yaml";
|
|
1128
|
+
var FENCE_RE = /^##\s+Tag Taxonomy\s*$[\s\S]*?```yaml\s*\n([\s\S]*?)\n```/m;
|
|
1129
|
+
function extractTaxonomy(schemaText) {
|
|
1130
|
+
const m = schemaText.match(FENCE_RE);
|
|
1131
|
+
if (!m) return err("NO_TAXONOMY_BLOCK", { message: "No fenced YAML taxonomy block found in SCHEMA.md" });
|
|
1132
|
+
let parsed;
|
|
1133
|
+
try {
|
|
1134
|
+
parsed = yaml2.load(m[1], { schema: yaml2.JSON_SCHEMA });
|
|
1135
|
+
} catch (e) {
|
|
1136
|
+
return err("INVALID_FRONTMATTER", { message: e.message });
|
|
1137
|
+
}
|
|
1138
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
1139
|
+
return err("INVALID_FRONTMATTER", { message: "taxonomy block is not an object" });
|
|
1140
|
+
}
|
|
1141
|
+
const tax = parsed.taxonomy;
|
|
1142
|
+
if (!Array.isArray(tax)) {
|
|
1143
|
+
return err("INVALID_FRONTMATTER", { message: "taxonomy key missing or not an array" });
|
|
1144
|
+
}
|
|
1145
|
+
if (!tax.every((x) => typeof x === "string")) {
|
|
1146
|
+
return err("INVALID_FRONTMATTER", { message: "taxonomy must be a list of strings" });
|
|
1147
|
+
}
|
|
1148
|
+
return ok(tax);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// src/commands/init.ts
|
|
844
1152
|
var DEFAULT_TAXONOMY = [
|
|
845
1153
|
"research",
|
|
846
1154
|
"comparison",
|
|
@@ -865,33 +1173,67 @@ var VAULT_DIRS = [
|
|
|
865
1173
|
"meta",
|
|
866
1174
|
"projects"
|
|
867
1175
|
];
|
|
1176
|
+
function extractDomainFromSchema(text) {
|
|
1177
|
+
const m = text.match(/^##\s+Domain\s*\n([\s\S]*?)(?=\n\n|\n##|\s*$)/m);
|
|
1178
|
+
if (!m) return "";
|
|
1179
|
+
const d = m[1].trim();
|
|
1180
|
+
return d.startsWith("##") ? "" : d;
|
|
1181
|
+
}
|
|
1182
|
+
async function discoverTagsFromPages(target, knownSlugs) {
|
|
1183
|
+
const knownSet = new Set(knownSlugs);
|
|
1184
|
+
const discovered = /* @__PURE__ */ new Set();
|
|
1185
|
+
for (const dir of ["entities", "concepts", "comparisons", "queries"]) {
|
|
1186
|
+
let entries;
|
|
1187
|
+
try {
|
|
1188
|
+
entries = (await readdir3(join7(target, dir), { withFileTypes: true })).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name);
|
|
1189
|
+
} catch {
|
|
1190
|
+
continue;
|
|
1191
|
+
}
|
|
1192
|
+
for (const file of entries) {
|
|
1193
|
+
try {
|
|
1194
|
+
const text = await readFile6(join7(target, dir, file), "utf8");
|
|
1195
|
+
const fm = extractFrontmatter(text);
|
|
1196
|
+
if (!fm.ok || !fm.data.tags || !Array.isArray(fm.data.tags)) continue;
|
|
1197
|
+
for (const t of fm.data.tags) {
|
|
1198
|
+
if (typeof t === "string" && !knownSet.has(t)) discovered.add(t);
|
|
1199
|
+
}
|
|
1200
|
+
} catch {
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
return [...discovered].sort();
|
|
1205
|
+
}
|
|
868
1206
|
async function runInit(input) {
|
|
869
1207
|
const pathRes = await resolveInitTimePath({ flag: input.flag, envValue: input.envValue, home: input.home });
|
|
870
1208
|
const target = pathRes.path;
|
|
871
1209
|
const langRes = await resolveLang({ flag: input.lang, envValue: void 0, home: input.home });
|
|
872
1210
|
const canonicalLang = langRes.canonical;
|
|
873
|
-
let
|
|
1211
|
+
let oldSchemaText;
|
|
874
1212
|
try {
|
|
875
|
-
await
|
|
876
|
-
hasSchema = true;
|
|
1213
|
+
oldSchemaText = await readFile6(join7(target, "SCHEMA.md"), "utf8");
|
|
877
1214
|
} catch {
|
|
878
1215
|
}
|
|
879
|
-
if (
|
|
1216
|
+
if (oldSchemaText && !input.force) {
|
|
880
1217
|
return {
|
|
881
1218
|
exitCode: ExitCode.INIT_TARGET_NOT_EMPTY,
|
|
882
1219
|
result: err("INIT_TARGET_NOT_EMPTY", { target })
|
|
883
1220
|
};
|
|
884
1221
|
}
|
|
885
1222
|
const envPath = join7(input.home, ".skillwiki", ".env");
|
|
886
|
-
|
|
1223
|
+
let existingEnvRaw = "";
|
|
1224
|
+
try {
|
|
1225
|
+
existingEnvRaw = await readFile6(envPath, "utf8");
|
|
1226
|
+
} catch {
|
|
1227
|
+
}
|
|
1228
|
+
const existingEnv = parseDotenvText(existingEnvRaw);
|
|
887
1229
|
const swDotenvHadPath = existingEnv.WIKI_PATH !== void 0;
|
|
888
|
-
if (existingEnv.WIKI_PATH !== void 0 && existingEnv.WIKI_PATH !== target && !input.force) {
|
|
1230
|
+
if (!input.profile && existingEnv.WIKI_PATH !== void 0 && existingEnv.WIKI_PATH !== target && !input.force) {
|
|
889
1231
|
return {
|
|
890
1232
|
exitCode: ExitCode.ENV_WRITE_CONFLICT,
|
|
891
1233
|
result: err("ENV_WRITE_CONFLICT", { key: "WIKI_PATH", existing: existingEnv.WIKI_PATH, attempted: target })
|
|
892
1234
|
};
|
|
893
1235
|
}
|
|
894
|
-
if (existingEnv.WIKI_LANG !== void 0 && existingEnv.WIKI_LANG !== canonicalLang && !input.force) {
|
|
1236
|
+
if (!input.profile && existingEnv.WIKI_LANG !== void 0 && existingEnv.WIKI_LANG !== canonicalLang && !input.force) {
|
|
895
1237
|
return {
|
|
896
1238
|
exitCode: ExitCode.ENV_WRITE_CONFLICT,
|
|
897
1239
|
result: err("ENV_WRITE_CONFLICT", { key: "WIKI_LANG", existing: existingEnv.WIKI_LANG, attempted: canonicalLang })
|
|
@@ -899,73 +1241,133 @@ async function runInit(input) {
|
|
|
899
1241
|
}
|
|
900
1242
|
const created = [];
|
|
901
1243
|
try {
|
|
902
|
-
await
|
|
1244
|
+
await mkdir4(target, { recursive: true });
|
|
903
1245
|
for (const d of VAULT_DIRS) {
|
|
904
|
-
await
|
|
1246
|
+
await mkdir4(join7(target, d), { recursive: true });
|
|
905
1247
|
created.push(d + "/");
|
|
906
1248
|
}
|
|
907
1249
|
} catch (e) {
|
|
908
1250
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
|
|
909
1251
|
}
|
|
910
1252
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
911
|
-
|
|
912
|
-
|
|
1253
|
+
let taxonomy = input.taxonomy && input.taxonomy.length > 0 ? input.taxonomy : DEFAULT_TAXONOMY;
|
|
1254
|
+
let domain = input.domain;
|
|
1255
|
+
let oldTaxonomy = [];
|
|
1256
|
+
if (oldSchemaText) {
|
|
1257
|
+
if (!domain) {
|
|
1258
|
+
const oldDomain = extractDomainFromSchema(oldSchemaText);
|
|
1259
|
+
if (oldDomain) domain = oldDomain;
|
|
1260
|
+
}
|
|
1261
|
+
const oldTax = extractTaxonomy(oldSchemaText);
|
|
1262
|
+
if (oldTax.ok) oldTaxonomy = oldTax.data;
|
|
1263
|
+
}
|
|
1264
|
+
const taxonomySet = new Set(taxonomy);
|
|
1265
|
+
for (const t of oldTaxonomy) {
|
|
1266
|
+
if (!taxonomySet.has(t)) {
|
|
1267
|
+
taxonomy.push(t);
|
|
1268
|
+
taxonomySet.add(t);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
const discovered = await discoverTagsFromPages(target, taxonomy);
|
|
1272
|
+
const discovered_tags = discovered.length;
|
|
1273
|
+
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
1274
|
try {
|
|
914
1275
|
const schemaTpl = await readFile6(join7(input.templates, "SCHEMA.md"), "utf8");
|
|
915
|
-
const schema = schemaTpl.replace("{{DOMAIN}}",
|
|
916
|
-
await
|
|
1276
|
+
const schema = schemaTpl.replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang).replace("{{TAXONOMY_YAML}}", fullTaxonomyYaml);
|
|
1277
|
+
await writeFile4(join7(target, "SCHEMA.md"), schema, "utf8");
|
|
917
1278
|
created.push("SCHEMA.md");
|
|
918
1279
|
} catch (e) {
|
|
919
1280
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: "SCHEMA.md", message: String(e) }) };
|
|
920
1281
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1282
|
+
const preserved = [];
|
|
1283
|
+
async function writeOrPreserve(fileName, render) {
|
|
1284
|
+
try {
|
|
1285
|
+
const existing = await readFile6(join7(target, fileName), "utf8");
|
|
1286
|
+
if (existing.split("\n").length > 10) {
|
|
1287
|
+
preserved.push(fileName);
|
|
1288
|
+
return void 0;
|
|
1289
|
+
}
|
|
1290
|
+
} catch {
|
|
1291
|
+
}
|
|
1292
|
+
try {
|
|
1293
|
+
await writeFile4(join7(target, fileName), await render(), "utf8");
|
|
1294
|
+
created.push(fileName);
|
|
1295
|
+
return void 0;
|
|
1296
|
+
} catch (e) {
|
|
1297
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: fileName, message: String(e) }) };
|
|
1298
|
+
}
|
|
936
1299
|
}
|
|
937
|
-
|
|
938
|
-
await
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
return {
|
|
1300
|
+
const err1 = await writeOrPreserve("index.md", async () => {
|
|
1301
|
+
const tpl = await readFile6(join7(input.templates, "index.md"), "utf8");
|
|
1302
|
+
return tpl.replace("{{INIT_DATE}}", today);
|
|
1303
|
+
});
|
|
1304
|
+
if (err1) return err1;
|
|
1305
|
+
const err22 = await writeOrPreserve("log.md", async () => {
|
|
1306
|
+
const tpl = await readFile6(join7(input.templates, "log.md"), "utf8");
|
|
1307
|
+
return tpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang);
|
|
1308
|
+
});
|
|
1309
|
+
if (err22) return err22;
|
|
1310
|
+
const skipEnv = !!input.noEnv;
|
|
1311
|
+
let envWritten = "";
|
|
1312
|
+
if (!skipEnv) {
|
|
1313
|
+
try {
|
|
1314
|
+
const envEntries = {};
|
|
1315
|
+
if (input.profile) {
|
|
1316
|
+
envEntries[profileKey(input.profile, "PATH")] = target;
|
|
1317
|
+
envEntries[profileKey(input.profile, "LANG")] = canonicalLang;
|
|
1318
|
+
envEntries["WIKI_DEFAULT"] = input.profile;
|
|
1319
|
+
} else {
|
|
1320
|
+
envEntries["WIKI_PATH"] = target;
|
|
1321
|
+
envEntries["WIKI_LANG"] = canonicalLang;
|
|
1322
|
+
}
|
|
1323
|
+
await writeDotenv(envPath, envEntries, existingEnvRaw);
|
|
1324
|
+
envWritten = envPath;
|
|
1325
|
+
} catch (e) {
|
|
1326
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: envPath, message: String(e) }) };
|
|
1327
|
+
}
|
|
945
1328
|
}
|
|
946
1329
|
const importedFromHermes = pathRes.source === "hermes-dotenv" && !swDotenvHadPath;
|
|
1330
|
+
const humanHint = [
|
|
1331
|
+
`vault: ${target}`,
|
|
1332
|
+
`domain: ${domain}`,
|
|
1333
|
+
`lang: ${canonicalLang}`,
|
|
1334
|
+
`created: ${created.length}, preserved: ${preserved.length}`,
|
|
1335
|
+
`discovered tags: ${discovered_tags}`,
|
|
1336
|
+
skipEnv ? "env: skipped" : `env: ${envWritten}`
|
|
1337
|
+
].join("\n");
|
|
947
1338
|
return {
|
|
948
1339
|
exitCode: ExitCode.OK,
|
|
949
1340
|
result: ok({
|
|
950
1341
|
vault: target,
|
|
951
|
-
domain
|
|
1342
|
+
domain,
|
|
952
1343
|
taxonomy,
|
|
953
1344
|
lang: canonicalLang,
|
|
954
1345
|
created,
|
|
955
|
-
|
|
956
|
-
|
|
1346
|
+
preserved,
|
|
1347
|
+
env_written: envWritten,
|
|
1348
|
+
env_skipped: skipEnv,
|
|
1349
|
+
imported_from_hermes: importedFromHermes,
|
|
1350
|
+
discovered_tags,
|
|
1351
|
+
humanHint
|
|
957
1352
|
})
|
|
958
1353
|
};
|
|
959
1354
|
}
|
|
960
1355
|
|
|
1356
|
+
// src/utils/slug.ts
|
|
1357
|
+
function buildSlugMap(pages) {
|
|
1358
|
+
const map = /* @__PURE__ */ new Map();
|
|
1359
|
+
for (const p of pages) {
|
|
1360
|
+
const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
|
|
1361
|
+
map.set(slug.toLowerCase(), slug);
|
|
1362
|
+
}
|
|
1363
|
+
return map;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
961
1366
|
// src/commands/links.ts
|
|
962
1367
|
async function runLinks(input) {
|
|
963
1368
|
const scan = await scanVault(input.vault);
|
|
964
1369
|
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
|
-
}
|
|
1370
|
+
const slugs = buildSlugMap(scan.data.typedKnowledge);
|
|
969
1371
|
const broken = [];
|
|
970
1372
|
for (const p of scan.data.typedKnowledge) {
|
|
971
1373
|
const text = await readPage(p);
|
|
@@ -974,48 +1376,22 @@ async function runLinks(input) {
|
|
|
974
1376
|
const lines = body.split("\n");
|
|
975
1377
|
for (const slug of extractBodyWikilinks(body)) {
|
|
976
1378
|
const tail = slug.split("/").pop();
|
|
977
|
-
if (!slugs.has(tail)) {
|
|
1379
|
+
if (!slugs.has(tail.toLowerCase())) {
|
|
978
1380
|
const line = lines.findIndex((l) => l.includes(`[[${slug}`));
|
|
979
1381
|
broken.push({ page: p.relPath, slug, line: line >= 0 ? line + 1 : 0 });
|
|
980
1382
|
}
|
|
981
1383
|
}
|
|
982
1384
|
}
|
|
983
1385
|
if (broken.length > 0) {
|
|
984
|
-
return { exitCode: ExitCode.BROKEN_WIKILINKS, result: ok({ broken
|
|
1386
|
+
return { exitCode: ExitCode.BROKEN_WIKILINKS, result: ok({ broken, humanHint: `broken: ${broken.length}
|
|
1387
|
+
${broken.map((b) => ` ${b.page}:[[${b.slug}]] (line ${b.line})`).join("\n")}` }) };
|
|
985
1388
|
}
|
|
986
|
-
return { exitCode: ExitCode.OK, result: ok({ broken }) };
|
|
1389
|
+
return { exitCode: ExitCode.OK, result: ok({ broken, humanHint: "no broken wikilinks" }) };
|
|
987
1390
|
}
|
|
988
1391
|
|
|
989
1392
|
// src/commands/tag-audit.ts
|
|
990
1393
|
import { readFile as readFile7 } from "fs/promises";
|
|
991
1394
|
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
1395
|
async function runTagAudit(input) {
|
|
1020
1396
|
const scan = await scanVault(input.vault);
|
|
1021
1397
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
@@ -1037,9 +1413,9 @@ async function runTagAudit(input) {
|
|
|
1037
1413
|
}
|
|
1038
1414
|
}
|
|
1039
1415
|
if (violations.length > 0) {
|
|
1040
|
-
return { exitCode: ExitCode.TAG_NOT_IN_TAXONOMY, result: ok({ violations, taxonomy: tax.data }) };
|
|
1416
|
+
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
1417
|
}
|
|
1042
|
-
return { exitCode: ExitCode.OK, result: ok({ violations, taxonomy: tax.data }) };
|
|
1418
|
+
return { exitCode: ExitCode.OK, result: ok({ violations, taxonomy: tax.data, humanHint: "all tags valid" }) };
|
|
1043
1419
|
}
|
|
1044
1420
|
|
|
1045
1421
|
// src/commands/index-check.ts
|
|
@@ -1053,7 +1429,11 @@ async function runIndexCheck(input) {
|
|
|
1053
1429
|
indexText = await readFile8(join9(input.vault, "index.md"), "utf8");
|
|
1054
1430
|
} catch {
|
|
1055
1431
|
}
|
|
1056
|
-
const
|
|
1432
|
+
const indexSlugsLower = /* @__PURE__ */ new Map();
|
|
1433
|
+
for (const s of extractBodyWikilinks(indexText)) {
|
|
1434
|
+
const tail = s.split("/").pop();
|
|
1435
|
+
indexSlugsLower.set(tail.toLowerCase(), tail);
|
|
1436
|
+
}
|
|
1057
1437
|
const fileSlugs = /* @__PURE__ */ new Map();
|
|
1058
1438
|
for (const p of scan.data.typedKnowledge) {
|
|
1059
1439
|
const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
|
|
@@ -1061,16 +1441,21 @@ async function runIndexCheck(input) {
|
|
|
1061
1441
|
}
|
|
1062
1442
|
const missing_from_index = [];
|
|
1063
1443
|
for (const [slug, relPath] of fileSlugs.entries()) {
|
|
1064
|
-
if (!
|
|
1444
|
+
if (!indexSlugsLower.has(slug.toLowerCase())) missing_from_index.push(relPath);
|
|
1065
1445
|
}
|
|
1446
|
+
const fileSlugsLower = new Set([...fileSlugs.keys()].map((s) => s.toLowerCase()));
|
|
1066
1447
|
const ghost_entries = [];
|
|
1067
|
-
for (const
|
|
1068
|
-
if (!
|
|
1448
|
+
for (const [lower, orig] of indexSlugsLower) {
|
|
1449
|
+
if (!fileSlugsLower.has(lower)) ghost_entries.push(orig);
|
|
1069
1450
|
}
|
|
1451
|
+
const hintLines = [];
|
|
1452
|
+
if (missing_from_index.length > 0) hintLines.push(`missing from index: ${missing_from_index.length}`, ...missing_from_index.map((p) => ` ${p}`));
|
|
1453
|
+
if (ghost_entries.length > 0) hintLines.push(`ghost entries: ${ghost_entries.length}`, ...ghost_entries.map((g) => ` ${g}`));
|
|
1454
|
+
if (hintLines.length === 0) hintLines.push("index OK");
|
|
1070
1455
|
if (missing_from_index.length > 0 || ghost_entries.length > 0) {
|
|
1071
|
-
return { exitCode: ExitCode.INDEX_INCOMPLETE, result: ok({ missing_from_index, ghost_entries }) };
|
|
1456
|
+
return { exitCode: ExitCode.INDEX_INCOMPLETE, result: ok({ missing_from_index, ghost_entries, humanHint: hintLines.join("\n") }) };
|
|
1072
1457
|
}
|
|
1073
|
-
return { exitCode: ExitCode.OK, result: ok({ missing_from_index, ghost_entries }) };
|
|
1458
|
+
return { exitCode: ExitCode.OK, result: ok({ missing_from_index, ghost_entries, humanHint: hintLines.join("\n") }) };
|
|
1074
1459
|
}
|
|
1075
1460
|
|
|
1076
1461
|
// src/commands/stale.ts
|
|
@@ -1110,8 +1495,8 @@ async function runStale(input) {
|
|
|
1110
1495
|
stale.push({ page: p.relPath, page_updated: updated, newest_source_ingested: newest, gap_days: gap });
|
|
1111
1496
|
}
|
|
1112
1497
|
}
|
|
1113
|
-
if (stale.length > 0) return { exitCode: ExitCode.STALE_PAGE, result: ok({ stale }) };
|
|
1114
|
-
return { exitCode: ExitCode.OK, result: ok({ stale }) };
|
|
1498
|
+
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") }) };
|
|
1499
|
+
return { exitCode: ExitCode.OK, result: ok({ stale, humanHint: "no stale pages" }) };
|
|
1115
1500
|
}
|
|
1116
1501
|
|
|
1117
1502
|
// src/commands/pagesize.ts
|
|
@@ -1126,17 +1511,17 @@ async function runPagesize(input) {
|
|
|
1126
1511
|
const count = body.split("\n").length;
|
|
1127
1512
|
if (count > input.lines) oversized.push({ page: p.relPath, lines: count });
|
|
1128
1513
|
}
|
|
1129
|
-
if (oversized.length > 0) return { exitCode: ExitCode.PAGE_TOO_LARGE, result: ok({ oversized }) };
|
|
1130
|
-
return { exitCode: ExitCode.OK, result: ok({ oversized }) };
|
|
1514
|
+
if (oversized.length > 0) return { exitCode: ExitCode.PAGE_TOO_LARGE, result: ok({ oversized, humanHint: oversized.map((p) => `${p.page}: ${p.lines} lines`).join("\n") }) };
|
|
1515
|
+
return { exitCode: ExitCode.OK, result: ok({ oversized, humanHint: "all pages within size limit" }) };
|
|
1131
1516
|
}
|
|
1132
1517
|
|
|
1133
1518
|
// src/commands/log-rotate.ts
|
|
1134
|
-
import { readFile as readFile10, rename as rename2, writeFile as
|
|
1519
|
+
import { readFile as readFile10, rename as rename2, writeFile as writeFile5, stat as stat5 } from "fs/promises";
|
|
1135
1520
|
import { join as join11 } from "path";
|
|
1136
1521
|
var ENTRY_RE = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
|
|
1137
1522
|
async function runLogRotate(input) {
|
|
1138
1523
|
try {
|
|
1139
|
-
await
|
|
1524
|
+
await stat5(join11(input.vault, "SCHEMA.md"));
|
|
1140
1525
|
} catch {
|
|
1141
1526
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
1142
1527
|
}
|
|
@@ -1150,12 +1535,12 @@ async function runLogRotate(input) {
|
|
|
1150
1535
|
const matches = [...logText.matchAll(ENTRY_RE)];
|
|
1151
1536
|
const entries = matches.length;
|
|
1152
1537
|
if (entries < input.threshold) {
|
|
1153
|
-
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: false }) };
|
|
1538
|
+
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: false, humanHint: `${entries}/${input.threshold} entries \u2014 no rotation needed` }) };
|
|
1154
1539
|
}
|
|
1155
1540
|
if (!input.apply) {
|
|
1156
1541
|
return {
|
|
1157
1542
|
exitCode: ExitCode.LOG_ROTATE_NEEDED,
|
|
1158
|
-
result: ok({ entries, threshold: input.threshold, rotated: false })
|
|
1543
|
+
result: ok({ entries, threshold: input.threshold, rotated: false, humanHint: `${entries}/${input.threshold} entries \u2014 rotation needed (use --apply)` })
|
|
1159
1544
|
};
|
|
1160
1545
|
}
|
|
1161
1546
|
const newestYear = matches[matches.length - 1][1];
|
|
@@ -1172,17 +1557,147 @@ Chronological action log. Newest entries last. Skill writes append entries; lint
|
|
|
1172
1557
|
|
|
1173
1558
|
- Previous log moved to ${rotatedName}
|
|
1174
1559
|
`;
|
|
1175
|
-
await
|
|
1560
|
+
await writeFile5(logPath, fresh, "utf8");
|
|
1176
1561
|
} catch (e) {
|
|
1177
1562
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
|
|
1178
1563
|
}
|
|
1179
|
-
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: true, rotated_to: rotatedName }) };
|
|
1564
|
+
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: true, rotated_to: rotatedName, humanHint: `rotated ${entries} entries to ${rotatedName}` }) };
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
// src/commands/topic-map-check.ts
|
|
1568
|
+
var DEFAULT_THRESHOLD = 200;
|
|
1569
|
+
async function runTopicMapCheck(input) {
|
|
1570
|
+
const threshold = input.threshold ?? DEFAULT_THRESHOLD;
|
|
1571
|
+
const scan = await scanVault(input.vault);
|
|
1572
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1573
|
+
const page_count = scan.data.typedKnowledge.length;
|
|
1574
|
+
const recommended = page_count >= threshold;
|
|
1575
|
+
return {
|
|
1576
|
+
exitCode: ExitCode.OK,
|
|
1577
|
+
result: ok({
|
|
1578
|
+
recommended,
|
|
1579
|
+
page_count,
|
|
1580
|
+
threshold,
|
|
1581
|
+
humanHint: recommended ? `topic map recommended (${page_count} pages >= ${threshold} threshold)` : `topic map not needed (${page_count} pages < ${threshold} threshold)`
|
|
1582
|
+
})
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// src/commands/index-link-format.ts
|
|
1587
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
1588
|
+
import { join as join12 } from "path";
|
|
1589
|
+
var MD_LINK_RE = /\[[^\[\]]+\]\([^)]+\.md\)/;
|
|
1590
|
+
async function runIndexLinkFormat(input) {
|
|
1591
|
+
let text = "";
|
|
1592
|
+
try {
|
|
1593
|
+
text = await readFile11(join12(input.vault, "index.md"), "utf8");
|
|
1594
|
+
} catch {
|
|
1595
|
+
}
|
|
1596
|
+
const markdown_links = [];
|
|
1597
|
+
for (const [i, line] of text.split("\n").entries()) {
|
|
1598
|
+
if (MD_LINK_RE.test(line)) markdown_links.push({ line: i + 1, text: line.trim() });
|
|
1599
|
+
}
|
|
1600
|
+
const humanHint = markdown_links.length === 0 ? "all index links use wikilink format" : `markdown links found: ${markdown_links.length}
|
|
1601
|
+
${markdown_links.map((l) => ` line ${l.line}: ${l.text}`).join("\n")}`;
|
|
1602
|
+
return { exitCode: ExitCode.OK, result: ok({ markdown_links, humanHint }) };
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// src/commands/dedup.ts
|
|
1606
|
+
import { readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
1607
|
+
import { join as join13 } from "path";
|
|
1608
|
+
async function runDedup(input) {
|
|
1609
|
+
const scan = await scanVault(input.vault);
|
|
1610
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1611
|
+
const hashMap = /* @__PURE__ */ new Map();
|
|
1612
|
+
let totalFiles = 0;
|
|
1613
|
+
for (const raw of scan.data.raw) {
|
|
1614
|
+
const fm = extractFrontmatter(await readPage(raw));
|
|
1615
|
+
if (!fm.ok) continue;
|
|
1616
|
+
const sha = typeof fm.data.sha256 === "string" ? fm.data.sha256 : null;
|
|
1617
|
+
if (!sha || sha.length !== 64) continue;
|
|
1618
|
+
totalFiles++;
|
|
1619
|
+
const existing = hashMap.get(sha);
|
|
1620
|
+
if (existing) existing.push(raw.relPath);
|
|
1621
|
+
else hashMap.set(sha, [raw.relPath]);
|
|
1622
|
+
}
|
|
1623
|
+
const duplicates = [...hashMap.entries()].filter(([, files]) => files.length > 1).map(([sha256, files]) => ({ sha256, files }));
|
|
1624
|
+
const rewired = [];
|
|
1625
|
+
const removed = [];
|
|
1626
|
+
if (input.apply && duplicates.length > 0) {
|
|
1627
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
1628
|
+
for (const group of duplicates) {
|
|
1629
|
+
const canonical = group.files[0];
|
|
1630
|
+
for (let i = 1; i < group.files.length; i++) {
|
|
1631
|
+
replacements.set(group.files[i], canonical);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
for (const page of scan.data.typedKnowledge) {
|
|
1635
|
+
const text = readFileSync(join13(input.vault, page.relPath), "utf-8");
|
|
1636
|
+
let updated = text;
|
|
1637
|
+
let changed = false;
|
|
1638
|
+
for (const [oldPath, newPath] of replacements) {
|
|
1639
|
+
const oldMarker = `^[${oldPath}]`;
|
|
1640
|
+
const newMarker = `^[${newPath}]`;
|
|
1641
|
+
if (updated.includes(oldMarker)) {
|
|
1642
|
+
updated = updated.replaceAll(oldMarker, newMarker);
|
|
1643
|
+
changed = true;
|
|
1644
|
+
}
|
|
1645
|
+
const oldFm = `- "^[${oldPath}]"`;
|
|
1646
|
+
const newFm = `- "^[${newPath}]"`;
|
|
1647
|
+
if (updated.includes(oldFm)) {
|
|
1648
|
+
updated = updated.replaceAll(oldFm, newFm);
|
|
1649
|
+
changed = true;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
if (changed) {
|
|
1653
|
+
writeFileSync(join13(input.vault, page.relPath), updated);
|
|
1654
|
+
rewired.push(page.relPath);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
for (const [oldPath] of replacements) {
|
|
1658
|
+
const fullPath = join13(input.vault, oldPath);
|
|
1659
|
+
try {
|
|
1660
|
+
unlinkSync(fullPath);
|
|
1661
|
+
removed.push(oldPath);
|
|
1662
|
+
} catch {
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
const exitCode = duplicates.length > 0 ? input.apply ? ExitCode.DEDUP_APPLIED : ExitCode.RAW_DEDUP_DETECTED : ExitCode.OK;
|
|
1667
|
+
const hintLines = [`scanned: ${totalFiles} raw files`];
|
|
1668
|
+
if (duplicates.length > 0) {
|
|
1669
|
+
hintLines.push(`duplicates: ${duplicates.length}`);
|
|
1670
|
+
for (const d of duplicates) hintLines.push(` ${d.sha256.slice(0, 12)}... \u2192 ${d.files.join(", ")}`);
|
|
1671
|
+
if (input.apply) {
|
|
1672
|
+
hintLines.push(`rewired: ${rewired.length} pages`);
|
|
1673
|
+
hintLines.push(`removed: ${removed.length} raw files`);
|
|
1674
|
+
}
|
|
1675
|
+
} else {
|
|
1676
|
+
hintLines.push("0 duplicates");
|
|
1677
|
+
}
|
|
1678
|
+
return {
|
|
1679
|
+
exitCode,
|
|
1680
|
+
result: ok({ scanned: totalFiles, duplicates, rewired, removed, humanHint: hintLines.join("\n") })
|
|
1681
|
+
};
|
|
1180
1682
|
}
|
|
1181
1683
|
|
|
1182
1684
|
// src/commands/lint.ts
|
|
1183
|
-
var
|
|
1184
|
-
var
|
|
1185
|
-
|
|
1685
|
+
var STRUCT_MIN_BODY_LINES = 60;
|
|
1686
|
+
var STRUCT_MIN_SECTIONS = 3;
|
|
1687
|
+
function hasDuplicateFrontmatter(body) {
|
|
1688
|
+
if (/^---\r?\n/.test(body)) return true;
|
|
1689
|
+
const lines = body.split(/\r?\n/);
|
|
1690
|
+
const limit = Math.min(lines.length, 20);
|
|
1691
|
+
let seenYamlKey = false;
|
|
1692
|
+
for (let i = 0; i < limit; i++) {
|
|
1693
|
+
if (/^\w[\w-]*:/.test(lines[i].trim())) seenYamlKey = true;
|
|
1694
|
+
if (seenYamlKey && lines[i].trim() === "---") return true;
|
|
1695
|
+
}
|
|
1696
|
+
return false;
|
|
1697
|
+
}
|
|
1698
|
+
var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_dedup", "tag_not_in_taxonomy"];
|
|
1699
|
+
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"];
|
|
1700
|
+
var INFO_ORDER = ["bridges", "page_structure", "topic_map_recommended", "frontmatter_wikilink"];
|
|
1186
1701
|
async function runLint(input) {
|
|
1187
1702
|
const buckets = {};
|
|
1188
1703
|
const links = await runLinks({ vault: input.vault });
|
|
@@ -1202,6 +1717,10 @@ async function runLint(input) {
|
|
|
1202
1717
|
ghost_entries: idx.result.data.ghost_entries
|
|
1203
1718
|
}];
|
|
1204
1719
|
}
|
|
1720
|
+
const linkFmt = await runIndexLinkFormat({ vault: input.vault });
|
|
1721
|
+
if (linkFmt.result.ok && linkFmt.result.data.markdown_links.length > 0) {
|
|
1722
|
+
buckets.index_link_format = linkFmt.result.data.markdown_links;
|
|
1723
|
+
}
|
|
1205
1724
|
const stale = await runStale({ vault: input.vault, days: input.days });
|
|
1206
1725
|
if (stale.result.ok && stale.result.data.stale.length > 0) buckets.stale_page = stale.result.data.stale;
|
|
1207
1726
|
const pagesize = await runPagesize({ vault: input.vault, lines: input.lines });
|
|
@@ -1215,6 +1734,59 @@ async function runLint(input) {
|
|
|
1215
1734
|
if (orphans.result.data.orphans.length > 0) buckets.orphans = orphans.result.data.orphans;
|
|
1216
1735
|
if (orphans.result.data.bridges.length > 0) buckets.bridges = orphans.result.data.bridges;
|
|
1217
1736
|
}
|
|
1737
|
+
const topicMap = await runTopicMapCheck({ vault: input.vault });
|
|
1738
|
+
if (topicMap.result.ok && topicMap.result.data.recommended) {
|
|
1739
|
+
buckets.topic_map_recommended = [{ page_count: topicMap.result.data.page_count, threshold: topicMap.result.data.threshold }];
|
|
1740
|
+
}
|
|
1741
|
+
const dedup = await runDedup({ vault: input.vault });
|
|
1742
|
+
if (dedup.result.ok && dedup.result.data.duplicates.length > 0) buckets.raw_dedup = dedup.result.data.duplicates;
|
|
1743
|
+
const scan = await scanVault(input.vault);
|
|
1744
|
+
const slugs = scan.ok ? buildSlugMap(scan.data.typedKnowledge) : /* @__PURE__ */ new Map();
|
|
1745
|
+
if (scan.ok) {
|
|
1746
|
+
const legacyPages = [];
|
|
1747
|
+
const orphanedPages = [];
|
|
1748
|
+
const structFlags = [];
|
|
1749
|
+
const dupFrontmatter = [];
|
|
1750
|
+
const noOverview = [];
|
|
1751
|
+
const fmWikilinkFlags = [];
|
|
1752
|
+
for (const page of scan.data.typedKnowledge) {
|
|
1753
|
+
const text = await readPage(page);
|
|
1754
|
+
const split = splitFrontmatter(text);
|
|
1755
|
+
if (!split.ok) continue;
|
|
1756
|
+
const body = split.data.body;
|
|
1757
|
+
const rawFm = split.data.rawFrontmatter;
|
|
1758
|
+
if (hasDuplicateFrontmatter(body)) dupFrontmatter.push(page.relPath);
|
|
1759
|
+
if (isLegacyCitationStyle(body)) legacyPages.push(page.relPath);
|
|
1760
|
+
if (hasOrphanedCitations(body)) orphanedPages.push(page.relPath);
|
|
1761
|
+
const fmLinks = rawFm.match(/\[\[([^\[\]|]+)(?:\|[^\[\]]*)?\]\]/g) ?? [];
|
|
1762
|
+
for (const link of fmLinks) {
|
|
1763
|
+
const target = link.replace(/^\[\[/, "").replace(/(?:\|[^\[\]]*)?\]\]$/, "").trim();
|
|
1764
|
+
const tail = target.split("/").pop();
|
|
1765
|
+
if (!slugs.has(tail.toLowerCase())) {
|
|
1766
|
+
fmWikilinkFlags.push(`${page.relPath}: [[${target}]] does not resolve`);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
const bodyLines = body.split("\n").filter((l) => l.trim().length > 0).length;
|
|
1770
|
+
const hasOverview = /^## Overview/m.test(body);
|
|
1771
|
+
if (!hasOverview) noOverview.push(page.relPath);
|
|
1772
|
+
if (bodyLines < STRUCT_MIN_BODY_LINES) {
|
|
1773
|
+
const hasRelated = /^## (Related|Relationships)/m.test(body);
|
|
1774
|
+
const sectionCount = (body.match(/^## /gm) ?? []).length;
|
|
1775
|
+
if (!hasRelated || sectionCount < STRUCT_MIN_SECTIONS) {
|
|
1776
|
+
const reasons = [];
|
|
1777
|
+
if (!hasRelated) reasons.push("no Related or Relationships");
|
|
1778
|
+
if (sectionCount < STRUCT_MIN_SECTIONS) reasons.push(`only ${sectionCount} sections`);
|
|
1779
|
+
structFlags.push(`${page.relPath}: ${bodyLines} lines, ${reasons.join(", ")}`);
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
if (legacyPages.length > 0) buckets.legacy_citation_style = legacyPages;
|
|
1784
|
+
if (orphanedPages.length > 0) buckets.orphaned_citations = orphanedPages;
|
|
1785
|
+
if (structFlags.length > 0) buckets.page_structure = structFlags;
|
|
1786
|
+
if (dupFrontmatter.length > 0) buckets.duplicate_frontmatter = dupFrontmatter;
|
|
1787
|
+
if (noOverview.length > 0) buckets.missing_overview = noOverview;
|
|
1788
|
+
if (fmWikilinkFlags.length > 0) buckets.frontmatter_wikilink = fmWikilinkFlags;
|
|
1789
|
+
}
|
|
1218
1790
|
const errorOut = ERROR_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
1219
1791
|
const warningOut = WARNING_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
1220
1792
|
const infoOut = INFO_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
@@ -1226,19 +1798,738 @@ async function runLint(input) {
|
|
|
1226
1798
|
let exitCode = ExitCode.OK;
|
|
1227
1799
|
if (summary.errors > 0) exitCode = ExitCode.LINT_HAS_ERRORS;
|
|
1228
1800
|
else if (summary.warnings > 0 || summary.info > 0) exitCode = ExitCode.LINT_HAS_WARNINGS;
|
|
1801
|
+
const hintLines = [];
|
|
1802
|
+
if (summary.errors > 0) hintLines.push(`errors: ${summary.errors}`);
|
|
1803
|
+
if (summary.warnings > 0) hintLines.push(`warnings: ${summary.warnings}`);
|
|
1804
|
+
if (summary.info > 0) hintLines.push(`info: ${summary.info}`);
|
|
1805
|
+
const allBuckets = [...errorOut, ...warningOut, ...infoOut];
|
|
1806
|
+
for (const b of allBuckets) {
|
|
1807
|
+
hintLines.push(` ${b.kind}: ${b.items.length}`);
|
|
1808
|
+
}
|
|
1809
|
+
if (hintLines.length === 0) hintLines.push("0 errors, 0 warnings, 0 info");
|
|
1229
1810
|
return {
|
|
1230
1811
|
exitCode,
|
|
1231
1812
|
result: ok({
|
|
1232
1813
|
vault: { path: input.vault, source: input.source ?? "resolved" },
|
|
1233
1814
|
summary,
|
|
1234
|
-
by_severity: { error: errorOut, warning: warningOut, info: infoOut }
|
|
1815
|
+
by_severity: { error: errorOut, warning: warningOut, info: infoOut },
|
|
1816
|
+
humanHint: hintLines.join("\n")
|
|
1817
|
+
})
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
// src/commands/config.ts
|
|
1822
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
1823
|
+
import { existsSync } from "fs";
|
|
1824
|
+
import { join as join14 } from "path";
|
|
1825
|
+
function validateKey(key) {
|
|
1826
|
+
return CONFIG_KEYS.includes(key) || isValidWikiProfileKey(key);
|
|
1827
|
+
}
|
|
1828
|
+
function configPath(home) {
|
|
1829
|
+
return join14(home, ".skillwiki", ".env");
|
|
1830
|
+
}
|
|
1831
|
+
async function runConfigGet(input) {
|
|
1832
|
+
if (!validateKey(input.key)) {
|
|
1833
|
+
return { exitCode: ExitCode.INVALID_CONFIG_KEY, result: err("INVALID_CONFIG_KEY", { key: input.key }) };
|
|
1834
|
+
}
|
|
1835
|
+
const map = await parseDotenvFile(configPath(input.home));
|
|
1836
|
+
const value = map[input.key] ?? "";
|
|
1837
|
+
return { exitCode: ExitCode.OK, result: ok({ key: input.key, value, humanHint: value }) };
|
|
1838
|
+
}
|
|
1839
|
+
async function runConfigSet(input) {
|
|
1840
|
+
if (!validateKey(input.key)) {
|
|
1841
|
+
return { exitCode: ExitCode.INVALID_CONFIG_KEY, result: err("INVALID_CONFIG_KEY", { key: input.key }) };
|
|
1842
|
+
}
|
|
1843
|
+
const filePath = configPath(input.home);
|
|
1844
|
+
try {
|
|
1845
|
+
let originalContent;
|
|
1846
|
+
try {
|
|
1847
|
+
originalContent = await readFile12(filePath, "utf8");
|
|
1848
|
+
} catch {
|
|
1849
|
+
}
|
|
1850
|
+
const existing = originalContent !== void 0 ? parseDotenvText(originalContent) : {};
|
|
1851
|
+
const merged = { ...existing, [input.key]: input.value };
|
|
1852
|
+
await writeDotenv(filePath, merged, originalContent);
|
|
1853
|
+
return { exitCode: ExitCode.OK, result: ok({ key: input.key, value: input.value, written: true, humanHint: `${input.key}=${input.value}` }) };
|
|
1854
|
+
} catch (e) {
|
|
1855
|
+
return { exitCode: ExitCode.CONFIG_WRITE_FAILED, result: err("CONFIG_WRITE_FAILED", { key: input.key, error: String(e) }) };
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
async function runConfigList(input) {
|
|
1859
|
+
const map = await parseDotenvFile(configPath(input.home));
|
|
1860
|
+
const entries = Object.entries(map).map(([key, value]) => ({ key, value: value ?? "" }));
|
|
1861
|
+
let profiles;
|
|
1862
|
+
if (input.profiles) {
|
|
1863
|
+
const defaultProfile = map["WIKI_DEFAULT"];
|
|
1864
|
+
profiles = [];
|
|
1865
|
+
for (const key of Object.keys(map)) {
|
|
1866
|
+
const m = key.match(/^WIKI_([A-Z][A-Z0-9_]{0,31})_PATH$/);
|
|
1867
|
+
if (m && key !== "WIKI_PATH") {
|
|
1868
|
+
const name = m[1].toLowerCase().replace(/_/g, "-");
|
|
1869
|
+
profiles.push({ name, path: map[key] ?? "", isDefault: name === defaultProfile });
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
profiles.sort((a, b) => a.name.localeCompare(b.name));
|
|
1873
|
+
}
|
|
1874
|
+
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");
|
|
1875
|
+
return { exitCode: ExitCode.OK, result: ok({ entries, profiles, humanHint: hint }) };
|
|
1876
|
+
}
|
|
1877
|
+
async function runConfigPath(input) {
|
|
1878
|
+
const filePath = configPath(input.home);
|
|
1879
|
+
return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync(filePath), humanHint: filePath }) };
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
// src/commands/doctor.ts
|
|
1883
|
+
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, statSync } from "fs";
|
|
1884
|
+
import { join as join16 } from "path";
|
|
1885
|
+
import { execSync } from "child_process";
|
|
1886
|
+
|
|
1887
|
+
// src/utils/auto-update.ts
|
|
1888
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync } from "fs";
|
|
1889
|
+
import { join as join15, dirname as dirname6 } from "path";
|
|
1890
|
+
import { spawn } from "child_process";
|
|
1891
|
+
|
|
1892
|
+
// src/utils/update-consts.ts
|
|
1893
|
+
var CACHE_FILENAME = ".update-cache.json";
|
|
1894
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1895
|
+
var ENV_DISABLE_KEY = "NO_UPDATE_NOTIFIER";
|
|
1896
|
+
var CLI_DISABLE_FLAG = "--no-update-notifier";
|
|
1897
|
+
|
|
1898
|
+
// src/utils/auto-update.ts
|
|
1899
|
+
function cachePath(home) {
|
|
1900
|
+
return join15(home, ".skillwiki", CACHE_FILENAME);
|
|
1901
|
+
}
|
|
1902
|
+
function readCacheRaw(home) {
|
|
1903
|
+
try {
|
|
1904
|
+
const raw = readFileSync2(cachePath(home), "utf8");
|
|
1905
|
+
return JSON.parse(raw);
|
|
1906
|
+
} catch {
|
|
1907
|
+
return null;
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
function readCache(home) {
|
|
1911
|
+
const cache = readCacheRaw(home);
|
|
1912
|
+
if (!cache) return { cache: null, hasUpdate: false, isStale: true };
|
|
1913
|
+
const isStale = Date.now() - cache.lastCheck >= CHECK_INTERVAL_MS;
|
|
1914
|
+
const hasUpdate = !!cache.latestVersion && semverGt(cache.latestVersion, cache.currentVersion);
|
|
1915
|
+
return { cache, hasUpdate, isStale };
|
|
1916
|
+
}
|
|
1917
|
+
function writeCache(home, cache) {
|
|
1918
|
+
const p = cachePath(home);
|
|
1919
|
+
mkdirSync(dirname6(p), { recursive: true });
|
|
1920
|
+
writeFileSync2(p, JSON.stringify(cache, null, 2));
|
|
1921
|
+
}
|
|
1922
|
+
function latestFromCache(home, currentVersion) {
|
|
1923
|
+
const { cache } = readCache(home);
|
|
1924
|
+
if (!cache || !cache.latestVersion) return { hasUpdate: false, latest: null };
|
|
1925
|
+
return {
|
|
1926
|
+
hasUpdate: semverGt(cache.latestVersion, currentVersion),
|
|
1927
|
+
latest: cache.latestVersion
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
function isDisabled() {
|
|
1931
|
+
return !!(process.env[ENV_DISABLE_KEY] || process.env.NODE_ENV === "test" || process.argv.includes(CLI_DISABLE_FLAG));
|
|
1932
|
+
}
|
|
1933
|
+
function triggerAutoUpdate(home, currentVersion) {
|
|
1934
|
+
if (isDisabled()) return;
|
|
1935
|
+
const { isStale } = readCache(home);
|
|
1936
|
+
if (!isStale) return;
|
|
1937
|
+
const bgScript = new URL("../auto-update-bg.js", import.meta.url).pathname;
|
|
1938
|
+
if (!existsSync2(bgScript)) return;
|
|
1939
|
+
const child = spawn(process.execPath, [bgScript, home, currentVersion], {
|
|
1940
|
+
detached: true,
|
|
1941
|
+
stdio: "ignore"
|
|
1942
|
+
});
|
|
1943
|
+
child.on("error", () => {
|
|
1944
|
+
});
|
|
1945
|
+
child.unref();
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
// src/commands/doctor.ts
|
|
1949
|
+
function check(status, id, label, detail) {
|
|
1950
|
+
return { id, label, status, detail };
|
|
1951
|
+
}
|
|
1952
|
+
function checkNodeVersion() {
|
|
1953
|
+
const major = parseInt(process.version.slice(1).split(".")[0], 10);
|
|
1954
|
+
if (major >= 20) {
|
|
1955
|
+
return check("pass", "node_version", "Node.js version", `v${major} >= 20`);
|
|
1956
|
+
}
|
|
1957
|
+
return check("error", "node_version", "Node.js version", `Node.js v${major} is below minimum v20`);
|
|
1958
|
+
}
|
|
1959
|
+
function checkCliOnPath(argv) {
|
|
1960
|
+
if (argv.length >= 2 && argv[1].endsWith("cli.js")) {
|
|
1961
|
+
return check("warn", "cli_on_path", "skillwiki on PATH", "Running via node cli.js (dev mode) \u2014 PATH check skipped");
|
|
1962
|
+
}
|
|
1963
|
+
if (argv.length >= 2 && argv[1] === "skillwiki") {
|
|
1964
|
+
return check("pass", "cli_on_path", "skillwiki on PATH", "Running as skillwiki \u2014 already on PATH");
|
|
1965
|
+
}
|
|
1966
|
+
try {
|
|
1967
|
+
execSync("which skillwiki 2>/dev/null", { encoding: "utf8" }).trim();
|
|
1968
|
+
return check("pass", "cli_on_path", "skillwiki on PATH", "skillwiki found on PATH");
|
|
1969
|
+
} catch {
|
|
1970
|
+
return check("warn", "cli_on_path", "skillwiki on PATH", "skillwiki not found on PATH");
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
async function checkConfigFile(home) {
|
|
1974
|
+
const cfgPath = configPath(home);
|
|
1975
|
+
if (!existsSync3(cfgPath)) {
|
|
1976
|
+
return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
|
|
1977
|
+
}
|
|
1978
|
+
try {
|
|
1979
|
+
const map = await parseDotenvFile(cfgPath);
|
|
1980
|
+
const keys = Object.keys(map);
|
|
1981
|
+
return check("pass", "config_file", "Config file exists", `Found with keys: ${keys.length > 0 ? keys.join(", ") : "(none set)"}`);
|
|
1982
|
+
} catch (e) {
|
|
1983
|
+
return check("warn", "config_file", "Config file exists", `Failed to parse ${cfgPath}: ${String(e)}`);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
function checkWikiPathExists(resolvedPath) {
|
|
1987
|
+
if (resolvedPath === void 0) {
|
|
1988
|
+
return check("error", "wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
1989
|
+
}
|
|
1990
|
+
if (existsSync3(resolvedPath) && statSync(resolvedPath).isDirectory()) {
|
|
1991
|
+
return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
|
|
1992
|
+
}
|
|
1993
|
+
return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
|
|
1994
|
+
}
|
|
1995
|
+
function checkVaultStructure(resolvedPath) {
|
|
1996
|
+
if (resolvedPath === void 0) {
|
|
1997
|
+
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
1998
|
+
}
|
|
1999
|
+
if (!existsSync3(resolvedPath)) {
|
|
2000
|
+
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
|
|
2001
|
+
}
|
|
2002
|
+
const missing = [];
|
|
2003
|
+
if (!existsSync3(join16(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
|
|
2004
|
+
for (const dir of ["raw", "entities", "concepts", "meta"]) {
|
|
2005
|
+
if (!existsSync3(join16(resolvedPath, dir))) missing.push(dir + "/");
|
|
2006
|
+
}
|
|
2007
|
+
if (missing.length === 0) {
|
|
2008
|
+
return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
|
|
2009
|
+
}
|
|
2010
|
+
return check("warn", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to add CodeWiki structure`);
|
|
2011
|
+
}
|
|
2012
|
+
function checkSkillsInstalled(home) {
|
|
2013
|
+
const skillsDir = join16(home, ".claude", "skills");
|
|
2014
|
+
if (!existsSync3(skillsDir)) {
|
|
2015
|
+
return check("warn", "skills_installed", "Skills installed", `${skillsDir} not found`);
|
|
2016
|
+
}
|
|
2017
|
+
const found = findSkillMd(skillsDir);
|
|
2018
|
+
if (found.length > 0) {
|
|
2019
|
+
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found`);
|
|
2020
|
+
}
|
|
2021
|
+
return check("warn", "skills_installed", "Skills installed", "No SKILL.md files found in ~/.claude/skills/");
|
|
2022
|
+
}
|
|
2023
|
+
function checkNpmUpdate(home, currentVersion) {
|
|
2024
|
+
const { hasUpdate, latest } = latestFromCache(home, currentVersion);
|
|
2025
|
+
if (!latest) {
|
|
2026
|
+
return check("pass", "npm_update", "npm CLI version", `v${currentVersion} (no cache yet)`);
|
|
2027
|
+
}
|
|
2028
|
+
if (hasUpdate) {
|
|
2029
|
+
return check("warn", "npm_update", "npm CLI version", `v${currentVersion} \u2014 update available: v${latest}. Run \`skillwiki update\`.`);
|
|
2030
|
+
}
|
|
2031
|
+
return check("pass", "npm_update", "npm CLI version", `v${currentVersion} (latest: v${latest})`);
|
|
2032
|
+
}
|
|
2033
|
+
function checkPluginVersionDrift(home, currentVersion) {
|
|
2034
|
+
const pluginJsonPath = join16(home, ".claude", "plugins", "cache", "llm-wiki", "plugin.json");
|
|
2035
|
+
if (!existsSync3(pluginJsonPath)) {
|
|
2036
|
+
return check("pass", "plugin_version_drift", "Plugin/CLI version", "Plugin cache not found \u2014 plugin not installed");
|
|
2037
|
+
}
|
|
2038
|
+
try {
|
|
2039
|
+
const content = readFileSync3(pluginJsonPath, { encoding: "utf8" });
|
|
2040
|
+
const pluginData = JSON.parse(content);
|
|
2041
|
+
const pluginVersion = pluginData.version;
|
|
2042
|
+
if (!pluginVersion) {
|
|
2043
|
+
return check("pass", "plugin_version_drift", "Plugin/CLI version", "Plugin version not found in cache");
|
|
2044
|
+
}
|
|
2045
|
+
if (pluginVersion === currentVersion) {
|
|
2046
|
+
return check("pass", "plugin_version_drift", "Plugin/CLI version", `Both at v${currentVersion}`);
|
|
2047
|
+
}
|
|
2048
|
+
const updateCmd = semverGt(pluginVersion, currentVersion) ? "npm install -g skillwiki@beta" : "claude plugin update skillwiki@llm-wiki";
|
|
2049
|
+
return check(
|
|
2050
|
+
"warn",
|
|
2051
|
+
"plugin_version_drift",
|
|
2052
|
+
"Plugin/CLI version",
|
|
2053
|
+
`Plugin v${pluginVersion} \u2260 CLI v${currentVersion} \u2014 run \`${updateCmd}\``
|
|
2054
|
+
);
|
|
2055
|
+
} catch {
|
|
2056
|
+
return check("pass", "plugin_version_drift", "Plugin/CLI version", "Could not read plugin cache");
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
async function checkProfiles(home) {
|
|
2060
|
+
const map = await parseDotenvFile(configPath(home));
|
|
2061
|
+
const profiles = [];
|
|
2062
|
+
for (const key of Object.keys(map)) {
|
|
2063
|
+
if (key.startsWith("WIKI_") && key.endsWith("_PATH") && key !== "WIKI_PATH") {
|
|
2064
|
+
const name = key.slice(5, -5).toLowerCase().replace(/_/g, "-");
|
|
2065
|
+
profiles.push(name);
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
if (profiles.length === 0) {
|
|
2069
|
+
return check("pass", "wiki_profiles", "Wiki profiles", "No named profiles configured");
|
|
2070
|
+
}
|
|
2071
|
+
const defaultProfile = map["WIKI_DEFAULT"] ?? "(none)";
|
|
2072
|
+
return check(
|
|
2073
|
+
"pass",
|
|
2074
|
+
"wiki_profiles",
|
|
2075
|
+
"Wiki profiles",
|
|
2076
|
+
`${profiles.length} profile(s): ${profiles.join(", ")}; default: ${defaultProfile}`
|
|
2077
|
+
);
|
|
2078
|
+
}
|
|
2079
|
+
async function checkProjectLocalOverride(cwd) {
|
|
2080
|
+
const dir = cwd ?? process.cwd();
|
|
2081
|
+
const envPath = join16(dir, ".skillwiki", ".env");
|
|
2082
|
+
if (existsSync3(envPath)) {
|
|
2083
|
+
return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
|
|
2084
|
+
}
|
|
2085
|
+
return check("pass", "project_local", "Project-local config", "None");
|
|
2086
|
+
}
|
|
2087
|
+
function findSkillMd(dir) {
|
|
2088
|
+
const results = [];
|
|
2089
|
+
let entries;
|
|
2090
|
+
try {
|
|
2091
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
2092
|
+
} catch {
|
|
2093
|
+
return results;
|
|
2094
|
+
}
|
|
2095
|
+
for (const entry of entries) {
|
|
2096
|
+
if (entry.isFile() && entry.name === "SKILL.md") {
|
|
2097
|
+
results.push(join16(dir, entry.name));
|
|
2098
|
+
} else if (entry.isDirectory()) {
|
|
2099
|
+
results.push(...findSkillMd(join16(dir, entry.name)));
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
return results;
|
|
2103
|
+
}
|
|
2104
|
+
async function runDoctor(input) {
|
|
2105
|
+
const checks = [];
|
|
2106
|
+
checks.push(checkNodeVersion());
|
|
2107
|
+
checks.push(checkCliOnPath(input.argv));
|
|
2108
|
+
checks.push(await checkConfigFile(input.home));
|
|
2109
|
+
checks.push(await checkProfiles(input.home));
|
|
2110
|
+
checks.push(await checkProjectLocalOverride(input.cwd));
|
|
2111
|
+
const resolved = await resolveRuntimePath({ flag: void 0, envValue: input.envValue, home: input.home });
|
|
2112
|
+
if (resolved.ok) {
|
|
2113
|
+
checks.push(check("pass", "wiki_path_set", "WIKI_PATH configured", `Resolved via ${resolved.data.source}: ${resolved.data.path}`));
|
|
2114
|
+
} else {
|
|
2115
|
+
checks.push(check("error", "wiki_path_set", "WIKI_PATH configured", "No vault configured. Run `skillwiki init` or pass --vault."));
|
|
2116
|
+
}
|
|
2117
|
+
const resolvedPath = resolved.ok ? resolved.data.path : void 0;
|
|
2118
|
+
checks.push(checkWikiPathExists(resolvedPath));
|
|
2119
|
+
checks.push(checkVaultStructure(resolvedPath));
|
|
2120
|
+
checks.push(checkSkillsInstalled(input.home));
|
|
2121
|
+
checks.push(checkNpmUpdate(input.home, input.currentVersion));
|
|
2122
|
+
checks.push(checkPluginVersionDrift(input.home, input.currentVersion));
|
|
2123
|
+
const summary = {
|
|
2124
|
+
pass: checks.filter((c) => c.status === "pass").length,
|
|
2125
|
+
warn: checks.filter((c) => c.status === "warn").length,
|
|
2126
|
+
error: checks.filter((c) => c.status === "error").length
|
|
2127
|
+
};
|
|
2128
|
+
const exitCode = summary.error > 0 ? ExitCode.DOCTOR_HAS_ERRORS : summary.warn > 0 ? ExitCode.DOCTOR_HAS_WARNINGS : ExitCode.OK;
|
|
2129
|
+
const statusIcon = { pass: "\u2713", warn: "\u26A0", error: "\u2717" };
|
|
2130
|
+
const lines = checks.map((c) => {
|
|
2131
|
+
const icon = statusIcon[c.status];
|
|
2132
|
+
const padded = c.label.padEnd(24);
|
|
2133
|
+
return ` ${icon} ${padded} ${c.detail}`;
|
|
2134
|
+
});
|
|
2135
|
+
lines.push("");
|
|
2136
|
+
lines.push(`${summary.pass} pass \xB7 ${summary.warn} warn \xB7 ${summary.error} error`);
|
|
2137
|
+
const humanHint = lines.join("\n");
|
|
2138
|
+
return { exitCode, result: ok({ checks, summary, humanHint }) };
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
// src/commands/archive.ts
|
|
2142
|
+
import { rename as rename3, mkdir as mkdir5, readFile as readFile13, writeFile as writeFile6 } from "fs/promises";
|
|
2143
|
+
import { join as join17, dirname as dirname7 } from "path";
|
|
2144
|
+
async function runArchive(input) {
|
|
2145
|
+
const scan = await scanVault(input.vault);
|
|
2146
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
2147
|
+
let relPath;
|
|
2148
|
+
if (input.page.includes("/")) {
|
|
2149
|
+
relPath = scan.data.typedKnowledge.find((p) => p.relPath === input.page)?.relPath;
|
|
2150
|
+
} else {
|
|
2151
|
+
relPath = scan.data.typedKnowledge.find((p) => p.relPath.replace(/\.md$/, "").split("/").pop() === input.page)?.relPath;
|
|
2152
|
+
}
|
|
2153
|
+
if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
|
|
2154
|
+
if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
|
|
2155
|
+
const archivePath = join17("_archive", relPath);
|
|
2156
|
+
await mkdir5(dirname7(join17(input.vault, archivePath)), { recursive: true });
|
|
2157
|
+
let indexUpdated = false;
|
|
2158
|
+
const indexPath = join17(input.vault, "index.md");
|
|
2159
|
+
try {
|
|
2160
|
+
const idx = await readFile13(indexPath, "utf8");
|
|
2161
|
+
const slug = relPath.replace(/\.md$/, "").split("/").pop();
|
|
2162
|
+
const originalLines = idx.split("\n");
|
|
2163
|
+
const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
|
|
2164
|
+
if (filtered.length !== originalLines.length) {
|
|
2165
|
+
await writeFile6(indexPath, filtered.join("\n"), "utf8");
|
|
2166
|
+
indexUpdated = true;
|
|
2167
|
+
}
|
|
2168
|
+
} catch (e) {
|
|
2169
|
+
if (e?.code !== "ENOENT") throw e;
|
|
2170
|
+
}
|
|
2171
|
+
await rename3(join17(input.vault, relPath), join17(input.vault, archivePath));
|
|
2172
|
+
return { exitCode: ExitCode.OK, result: ok({ archived_from: relPath, archived_to: archivePath, index_updated: indexUpdated, humanHint: `${relPath} -> ${archivePath}${indexUpdated ? " (index updated)" : ""}` }) };
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
// src/commands/drift.ts
|
|
2176
|
+
import { createHash as createHash2 } from "crypto";
|
|
2177
|
+
|
|
2178
|
+
// src/utils/fetch.ts
|
|
2179
|
+
async function controlledFetch(url, opts) {
|
|
2180
|
+
let current = url;
|
|
2181
|
+
for (let hop = 0; hop <= opts.maxRedirects; hop++) {
|
|
2182
|
+
const guard = runFetchGuardSync({ url: current });
|
|
2183
|
+
if (!guard.result.ok) return guard.result;
|
|
2184
|
+
const ctrl = new AbortController();
|
|
2185
|
+
const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs);
|
|
2186
|
+
let res;
|
|
2187
|
+
try {
|
|
2188
|
+
res = await fetch(current, { redirect: "manual", signal: ctrl.signal });
|
|
2189
|
+
} catch (e) {
|
|
2190
|
+
clearTimeout(timer);
|
|
2191
|
+
if (e?.name === "AbortError") return err("FETCH_TIMEOUT", { url: current });
|
|
2192
|
+
return err("FETCH_FAILED", { message: String(e) });
|
|
2193
|
+
}
|
|
2194
|
+
clearTimeout(timer);
|
|
2195
|
+
if (res.status >= 300 && res.status < 400) {
|
|
2196
|
+
const loc = res.headers.get("location");
|
|
2197
|
+
if (!loc) return err("FETCH_FAILED", { reason: "redirect without Location" });
|
|
2198
|
+
current = new URL(loc, current).toString();
|
|
2199
|
+
continue;
|
|
2200
|
+
}
|
|
2201
|
+
const declared = Number(res.headers.get("content-length") ?? "0");
|
|
2202
|
+
if (declared > opts.maxBytes) return err("FETCH_TOO_LARGE", { declared, limit: opts.maxBytes });
|
|
2203
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
2204
|
+
if (buf.byteLength > opts.maxBytes) return err("FETCH_TOO_LARGE", { actual: buf.byteLength, limit: opts.maxBytes });
|
|
2205
|
+
return ok({ url: current, status: res.status, body: new TextDecoder().decode(buf), bytes: buf.byteLength });
|
|
2206
|
+
}
|
|
2207
|
+
return err("FETCH_FAILED", { reason: "too many redirects", limit: opts.maxRedirects });
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
// src/commands/drift.ts
|
|
2211
|
+
var FETCH_OPTS = { timeoutMs: 1e4, maxBytes: 5e6, maxRedirects: 5 };
|
|
2212
|
+
async function runDrift(input) {
|
|
2213
|
+
const doFetch = input.fetchFn ?? controlledFetch;
|
|
2214
|
+
const scan = await scanVault(input.vault);
|
|
2215
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
2216
|
+
const results = [];
|
|
2217
|
+
for (const raw of scan.data.raw) {
|
|
2218
|
+
const fm = extractFrontmatter(await readPage(raw));
|
|
2219
|
+
if (!fm.ok) continue;
|
|
2220
|
+
const sourceUrl = typeof fm.data.source_url === "string" ? fm.data.source_url : null;
|
|
2221
|
+
const storedHash = typeof fm.data.sha256 === "string" ? fm.data.sha256 : null;
|
|
2222
|
+
if (!sourceUrl || !storedHash) continue;
|
|
2223
|
+
const resp = await doFetch(sourceUrl, FETCH_OPTS);
|
|
2224
|
+
if (!resp.ok) {
|
|
2225
|
+
results.push({
|
|
2226
|
+
raw_path: raw.relPath,
|
|
2227
|
+
source_url: sourceUrl,
|
|
2228
|
+
stored_sha256: storedHash,
|
|
2229
|
+
current_sha256: null,
|
|
2230
|
+
status: "fetch_failed",
|
|
2231
|
+
fetch_error: resp.error
|
|
2232
|
+
});
|
|
2233
|
+
continue;
|
|
2234
|
+
}
|
|
2235
|
+
const currentHash = createHash2("sha256").update(Buffer.from(resp.data.body, "utf8")).digest("hex");
|
|
2236
|
+
const drifted2 = currentHash !== storedHash;
|
|
2237
|
+
results.push({
|
|
2238
|
+
raw_path: raw.relPath,
|
|
2239
|
+
source_url: sourceUrl,
|
|
2240
|
+
stored_sha256: storedHash,
|
|
2241
|
+
current_sha256: currentHash,
|
|
2242
|
+
status: drifted2 ? "drifted" : "unchanged"
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
const drifted = results.filter((r) => r.status === "drifted");
|
|
2246
|
+
const fetchFailed = results.filter((r) => r.status === "fetch_failed");
|
|
2247
|
+
const unchanged = results.filter((r) => r.status === "unchanged").length;
|
|
2248
|
+
const exitCode = drifted.length > 0 ? ExitCode.DRIFT_DETECTED : ExitCode.OK;
|
|
2249
|
+
const hintLines = [`scanned: ${results.length}, unchanged: ${unchanged}`];
|
|
2250
|
+
if (drifted.length > 0) hintLines.push(`drifted: ${drifted.length}`, ...drifted.map((d) => ` ${d.raw_path}`));
|
|
2251
|
+
if (fetchFailed.length > 0) hintLines.push(`fetch_failed: ${fetchFailed.length}`, ...fetchFailed.map((f) => ` ${f.raw_path}: ${f.fetch_error}`));
|
|
2252
|
+
return {
|
|
2253
|
+
exitCode,
|
|
2254
|
+
result: ok({ scanned: results.length, drifted, fetch_failed: fetchFailed, unchanged, humanHint: hintLines.join("\n") })
|
|
2255
|
+
};
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
// src/commands/migrate-citations.ts
|
|
2259
|
+
import { writeFile as writeFile7 } from "fs/promises";
|
|
2260
|
+
var MARKER_RE2 = /\^\[(raw\/[^\]]+)\]/g;
|
|
2261
|
+
function moveMarkersToParagraphEnd(body) {
|
|
2262
|
+
const lines = body.split("\n");
|
|
2263
|
+
const result = [];
|
|
2264
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2265
|
+
const line = lines[i];
|
|
2266
|
+
if (line.trimStart().startsWith("```")) {
|
|
2267
|
+
result.push(line);
|
|
2268
|
+
if (!line.trimEnd().endsWith("```") || line.trim() === "```") {
|
|
2269
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
2270
|
+
result.push(lines[j]);
|
|
2271
|
+
if (lines[j].trimStart().startsWith("```")) {
|
|
2272
|
+
i = j;
|
|
2273
|
+
break;
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
continue;
|
|
2278
|
+
}
|
|
2279
|
+
if (/^## Sources\b/.test(line.trim())) {
|
|
2280
|
+
result.push(line);
|
|
2281
|
+
continue;
|
|
2282
|
+
}
|
|
2283
|
+
const markers = [...line.matchAll(MARKER_RE2)];
|
|
2284
|
+
if (markers.length === 0) {
|
|
2285
|
+
result.push(line);
|
|
2286
|
+
continue;
|
|
2287
|
+
}
|
|
2288
|
+
const proseOnly = line.replace(MARKER_RE2, "").trim();
|
|
2289
|
+
if (proseOnly.length === 0) {
|
|
2290
|
+
const markerStr = " " + markers.map((m) => m[0]).join(" ");
|
|
2291
|
+
let merged = false;
|
|
2292
|
+
for (let k = result.length - 1; k >= 0; k--) {
|
|
2293
|
+
if (result[k].trim().length > 0) {
|
|
2294
|
+
result[k] = result[k].trimEnd() + markerStr;
|
|
2295
|
+
merged = true;
|
|
2296
|
+
break;
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
if (!merged) result.push(line);
|
|
2300
|
+
continue;
|
|
2301
|
+
}
|
|
2302
|
+
const lastMarkerIdx = line.lastIndexOf("^[raw/");
|
|
2303
|
+
const afterLast = line.slice(lastMarkerIdx).replace(MARKER_RE2, "").trim();
|
|
2304
|
+
const firstMarkerIdx = line.indexOf("^[raw/");
|
|
2305
|
+
const beforeFirst = line.slice(0, firstMarkerIdx).trim();
|
|
2306
|
+
const alreadyAtEnd = afterLast.length === 0 && (beforeFirst.length === 0 || /[.!?]\s*$/.test(beforeFirst));
|
|
2307
|
+
if (alreadyAtEnd) {
|
|
2308
|
+
result.push(line);
|
|
2309
|
+
continue;
|
|
2310
|
+
}
|
|
2311
|
+
let cleaned = line.replace(/\s*\^\[raw\/[^\]]+\]\s*/g, " ").trimEnd();
|
|
2312
|
+
cleaned = cleaned.replace(/ +/g, " ").trimEnd();
|
|
2313
|
+
const markerStrings = markers.map((m) => m[0]);
|
|
2314
|
+
if (cleaned.length > 0 && /[.!?]$/.test(cleaned)) {
|
|
2315
|
+
cleaned += " " + markerStrings.join(" ");
|
|
2316
|
+
} else if (cleaned.length > 0) {
|
|
2317
|
+
cleaned += ". " + markerStrings.join(" ");
|
|
2318
|
+
} else {
|
|
2319
|
+
cleaned = markerStrings.join(" ");
|
|
2320
|
+
}
|
|
2321
|
+
result.push(cleaned);
|
|
2322
|
+
}
|
|
2323
|
+
return result.join("\n");
|
|
2324
|
+
}
|
|
2325
|
+
function buildSourcesFooter(targets) {
|
|
2326
|
+
return "\n## Sources\n" + targets.map((t) => `- ^[${t}]`).join("\n") + "\n";
|
|
2327
|
+
}
|
|
2328
|
+
function reorderSourcesFm(rawFm, targets) {
|
|
2329
|
+
const sourcesLineRe = /^sources:\s*\[([^\]]*)\]\s*$/m;
|
|
2330
|
+
const match = rawFm.match(sourcesLineRe);
|
|
2331
|
+
if (!match) return rawFm;
|
|
2332
|
+
const existing = match[1].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter((s) => s.length > 0);
|
|
2333
|
+
const targetSet = new Set(targets);
|
|
2334
|
+
const reordered = [
|
|
2335
|
+
...targets,
|
|
2336
|
+
...existing.filter((s) => !targetSet.has(s))
|
|
2337
|
+
];
|
|
2338
|
+
const newLine = `sources: [${reordered.join(", ")}]`;
|
|
2339
|
+
return rawFm.replace(sourcesLineRe, newLine);
|
|
2340
|
+
}
|
|
2341
|
+
function removeExistingFooter(body) {
|
|
2342
|
+
const footerRe = /\n## Sources\n[\s\S]*$/;
|
|
2343
|
+
return body.replace(footerRe, "");
|
|
2344
|
+
}
|
|
2345
|
+
async function runMigrateCitations(input) {
|
|
2346
|
+
const scan = await scanVault(input.vault);
|
|
2347
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
2348
|
+
const migrated = [];
|
|
2349
|
+
const skipped = [];
|
|
2350
|
+
let unchanged = 0;
|
|
2351
|
+
for (const page of scan.data.typedKnowledge) {
|
|
2352
|
+
const text = await readPage(page);
|
|
2353
|
+
const split = splitFrontmatter(text);
|
|
2354
|
+
if (!split.ok) continue;
|
|
2355
|
+
const { rawFrontmatter, body } = split.data;
|
|
2356
|
+
const markers = extractCitationMarkers(body);
|
|
2357
|
+
if (markers.length === 0) {
|
|
2358
|
+
unchanged++;
|
|
2359
|
+
continue;
|
|
2360
|
+
}
|
|
2361
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2362
|
+
const uniqueTargets = [];
|
|
2363
|
+
for (const m of markers) {
|
|
2364
|
+
if (!seen.has(m.target)) {
|
|
2365
|
+
seen.add(m.target);
|
|
2366
|
+
uniqueTargets.push(m.target);
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
const bodyWithoutFooter = removeExistingFooter(body);
|
|
2370
|
+
const migratedBody = moveMarkersToParagraphEnd(bodyWithoutFooter);
|
|
2371
|
+
const newFooter = buildSourcesFooter(uniqueTargets);
|
|
2372
|
+
const newFm = reorderSourcesFm(rawFrontmatter, uniqueTargets);
|
|
2373
|
+
const newText = `---
|
|
2374
|
+
${newFm}
|
|
2375
|
+
---
|
|
2376
|
+
${migratedBody}${newFooter}`;
|
|
2377
|
+
if (newText === text) {
|
|
2378
|
+
skipped.push(page.relPath);
|
|
2379
|
+
continue;
|
|
2380
|
+
}
|
|
2381
|
+
if (!input.dryRun) {
|
|
2382
|
+
await writeFile7(page.absPath, newText, "utf8");
|
|
2383
|
+
}
|
|
2384
|
+
migrated.push(page.relPath);
|
|
2385
|
+
}
|
|
2386
|
+
const exitCode = migrated.length > 0 ? ExitCode.MIGRATION_APPLIED : ExitCode.OK;
|
|
2387
|
+
const hintLines = [`scanned: ${migrated.length + skipped.length + unchanged}`];
|
|
2388
|
+
if (migrated.length > 0) hintLines.push(`migrated: ${migrated.length}`);
|
|
2389
|
+
if (skipped.length > 0) hintLines.push(`skipped (already clean): ${skipped.length}`);
|
|
2390
|
+
if (unchanged > 0) hintLines.push(`unchanged (no markers): ${unchanged}`);
|
|
2391
|
+
return {
|
|
2392
|
+
exitCode,
|
|
2393
|
+
result: ok({
|
|
2394
|
+
scanned: migrated.length + skipped.length + unchanged,
|
|
2395
|
+
migrated,
|
|
2396
|
+
skipped,
|
|
2397
|
+
unchanged,
|
|
2398
|
+
humanHint: hintLines.join("\n")
|
|
2399
|
+
})
|
|
2400
|
+
};
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
// src/commands/frontmatter-fix.ts
|
|
2404
|
+
import { writeFile as writeFile8 } from "fs/promises";
|
|
2405
|
+
function isoToday() {
|
|
2406
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2407
|
+
}
|
|
2408
|
+
function fixFrontmatter(rawFm) {
|
|
2409
|
+
const additions = [];
|
|
2410
|
+
if (!/^created:/m.test(rawFm)) additions.push(`created: ${isoToday()}`);
|
|
2411
|
+
if (!/^updated:/m.test(rawFm)) additions.push(`updated: ${isoToday()}`);
|
|
2412
|
+
if (!/^tags:/m.test(rawFm)) additions.push("tags: []");
|
|
2413
|
+
if (!/^sources:/m.test(rawFm)) additions.push("sources: []");
|
|
2414
|
+
if (!/^provenance:/m.test(rawFm)) additions.push("provenance: research");
|
|
2415
|
+
if (additions.length === 0) return rawFm;
|
|
2416
|
+
return rawFm.trimEnd() + "\n" + additions.join("\n") + "\n";
|
|
2417
|
+
}
|
|
2418
|
+
function removeOrphanTagsLines(body) {
|
|
2419
|
+
return body.split("\n").filter((line) => !/^tags:\s*\[/.test(line.trim())).join("\n");
|
|
2420
|
+
}
|
|
2421
|
+
async function runFrontmatterFix(input) {
|
|
2422
|
+
const scan = await scanVault(input.vault);
|
|
2423
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
2424
|
+
const fixed = [];
|
|
2425
|
+
const skipped = [];
|
|
2426
|
+
let unchanged = 0;
|
|
2427
|
+
for (const page of scan.data.typedKnowledge) {
|
|
2428
|
+
const text = await readPage(page);
|
|
2429
|
+
const split = splitFrontmatter(text);
|
|
2430
|
+
if (!split.ok) {
|
|
2431
|
+
skipped.push(page.relPath);
|
|
2432
|
+
continue;
|
|
2433
|
+
}
|
|
2434
|
+
const { rawFrontmatter, body } = split.data;
|
|
2435
|
+
const newFm = fixFrontmatter(rawFrontmatter);
|
|
2436
|
+
const newBody = removeOrphanTagsLines(body);
|
|
2437
|
+
const newText = `---
|
|
2438
|
+
${newFm}
|
|
2439
|
+
---
|
|
2440
|
+
${newBody}`;
|
|
2441
|
+
if (newText === text) {
|
|
2442
|
+
unchanged++;
|
|
2443
|
+
continue;
|
|
2444
|
+
}
|
|
2445
|
+
if (!input.dryRun) {
|
|
2446
|
+
await writeFile8(page.absPath, newText, "utf8");
|
|
2447
|
+
}
|
|
2448
|
+
fixed.push(page.relPath);
|
|
2449
|
+
}
|
|
2450
|
+
const exitCode = fixed.length > 0 ? ExitCode.MIGRATION_APPLIED : ExitCode.OK;
|
|
2451
|
+
const hintLines = [`scanned: ${fixed.length + skipped.length + unchanged}`];
|
|
2452
|
+
if (fixed.length > 0) hintLines.push(`fixed: ${fixed.length}`);
|
|
2453
|
+
if (skipped.length > 0) hintLines.push(`skipped (parse error): ${skipped.length}`);
|
|
2454
|
+
if (unchanged > 0) hintLines.push(`unchanged: ${unchanged}`);
|
|
2455
|
+
if (input.dryRun && fixed.length > 0) hintLines.push("(dry run \u2014 no files written)");
|
|
2456
|
+
return {
|
|
2457
|
+
exitCode,
|
|
2458
|
+
result: ok({
|
|
2459
|
+
scanned: fixed.length + skipped.length + unchanged,
|
|
2460
|
+
fixed,
|
|
2461
|
+
skipped,
|
|
2462
|
+
unchanged,
|
|
2463
|
+
humanHint: hintLines.join("\n")
|
|
2464
|
+
})
|
|
2465
|
+
};
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
// src/commands/update.ts
|
|
2469
|
+
import { execSync as execSync2 } from "child_process";
|
|
2470
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
2471
|
+
async function runUpdate(input) {
|
|
2472
|
+
const pkg2 = JSON.parse(
|
|
2473
|
+
readFileSync4(new URL("../../package.json", import.meta.url), "utf8")
|
|
2474
|
+
);
|
|
2475
|
+
const currentVersion = pkg2.version;
|
|
2476
|
+
const tag = input.distTag ?? "beta";
|
|
2477
|
+
let latest;
|
|
2478
|
+
try {
|
|
2479
|
+
latest = execSync2(`npm view skillwiki@${tag} version`, {
|
|
2480
|
+
encoding: "utf8",
|
|
2481
|
+
timeout: 15e3
|
|
2482
|
+
}).trim();
|
|
2483
|
+
} catch (e) {
|
|
2484
|
+
return {
|
|
2485
|
+
exitCode: ExitCode.PREFLIGHT_FAILED,
|
|
2486
|
+
result: err("PREFLIGHT_FAILED", { message: `Failed to query npm registry: ${String(e)}` })
|
|
2487
|
+
};
|
|
2488
|
+
}
|
|
2489
|
+
const cache = {
|
|
2490
|
+
lastCheck: Date.now(),
|
|
2491
|
+
latestVersion: latest,
|
|
2492
|
+
currentVersion
|
|
2493
|
+
};
|
|
2494
|
+
if (latest === currentVersion) {
|
|
2495
|
+
writeCache(input.home, cache);
|
|
2496
|
+
return {
|
|
2497
|
+
exitCode: ExitCode.OK,
|
|
2498
|
+
result: ok({
|
|
2499
|
+
previousVersion: currentVersion,
|
|
2500
|
+
newVersion: null,
|
|
2501
|
+
wasAlreadyLatest: true,
|
|
2502
|
+
humanHint: `Already on latest ${tag}: v${currentVersion}`
|
|
2503
|
+
})
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
try {
|
|
2507
|
+
execSync2(`npm install -g skillwiki@${tag}`, {
|
|
2508
|
+
stdio: "pipe",
|
|
2509
|
+
timeout: 6e4
|
|
2510
|
+
});
|
|
2511
|
+
} catch (e) {
|
|
2512
|
+
return {
|
|
2513
|
+
exitCode: ExitCode.PREFLIGHT_FAILED,
|
|
2514
|
+
result: err("PREFLIGHT_FAILED", { message: `npm install failed: ${String(e)}` })
|
|
2515
|
+
};
|
|
2516
|
+
}
|
|
2517
|
+
writeCache(input.home, { ...cache, updateAppliedAt: Date.now() });
|
|
2518
|
+
return {
|
|
2519
|
+
exitCode: ExitCode.OK,
|
|
2520
|
+
result: ok({
|
|
2521
|
+
previousVersion: currentVersion,
|
|
2522
|
+
newVersion: latest,
|
|
2523
|
+
wasAlreadyLatest: false,
|
|
2524
|
+
humanHint: `Updated skillwiki ${currentVersion} \u2192 ${latest}`
|
|
1235
2525
|
})
|
|
1236
2526
|
};
|
|
1237
2527
|
}
|
|
1238
2528
|
|
|
1239
2529
|
// src/cli.ts
|
|
2530
|
+
var pkg = JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8"));
|
|
1240
2531
|
var program = new Command();
|
|
1241
|
-
program.name("skillwiki").description("Deterministic helpers for CodeWiki skills").version(
|
|
2532
|
+
program.name("skillwiki").description("Deterministic helpers for CodeWiki skills").version(pkg.version);
|
|
1242
2533
|
program.option("--human", "render terminal-readable output instead of JSON");
|
|
1243
2534
|
function emit(r) {
|
|
1244
2535
|
if (program.opts().human) printHuman(r.result);
|
|
@@ -1248,19 +2539,20 @@ function emit(r) {
|
|
|
1248
2539
|
program.command("hash <file>").action(async (file) => emit(await runHash({ file })));
|
|
1249
2540
|
program.command("fetch-guard <url>").action(async (url) => emit(await runFetchGuard({ url })));
|
|
1250
2541
|
program.command("validate <file>").action(async (file) => emit(await runValidate({ file })));
|
|
1251
|
-
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 })));
|
|
2542
|
+
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 })));
|
|
1252
2543
|
program.command("overlap <vault>").action(async (vault) => emit(await runOverlap({ vault })));
|
|
1253
|
-
program.command("orphans [vault]").action(async (vault) => emit(await runOrphans({
|
|
2544
|
+
program.command("orphans [vault]").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => emit(await runOrphans({
|
|
1254
2545
|
vault,
|
|
1255
2546
|
envValue: process.env.WIKI_PATH,
|
|
1256
|
-
home: process.env.HOME ?? ""
|
|
2547
|
+
home: process.env.HOME ?? "",
|
|
2548
|
+
wiki: opts.wiki
|
|
1257
2549
|
})));
|
|
1258
2550
|
program.command("audit <file>").action(async (file) => emit(await runAudit({ file })));
|
|
1259
2551
|
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
2552
|
const skillsRoot = opts.skillsRoot ?? new URL("../skills/", import.meta.url).pathname;
|
|
1261
2553
|
emit(await runInstall({ skillsRoot, target: opts.target, dryRun: !!opts.dryRun }));
|
|
1262
2554
|
});
|
|
1263
|
-
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) => {
|
|
2555
|
+
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) => {
|
|
1264
2556
|
const initTime = !!opts.initTime;
|
|
1265
2557
|
const flag = initTime ? opts.target : opts.vault;
|
|
1266
2558
|
emit(await runPath({
|
|
@@ -1268,6 +2560,7 @@ program.command("path").option("--vault <dir>", "explicit vault override (runtim
|
|
|
1268
2560
|
envValue: process.env.WIKI_PATH,
|
|
1269
2561
|
home: process.env.HOME ?? "",
|
|
1270
2562
|
initTime,
|
|
2563
|
+
wiki: opts.wiki,
|
|
1271
2564
|
explain: !!opts.explain
|
|
1272
2565
|
}));
|
|
1273
2566
|
});
|
|
@@ -1279,7 +2572,7 @@ program.command("lang").option("--lang <code>", "explicit language override").op
|
|
|
1279
2572
|
explain: !!opts.explain
|
|
1280
2573
|
}));
|
|
1281
2574
|
});
|
|
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) => {
|
|
2575
|
+
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) => {
|
|
1283
2576
|
const templates = new URL("../templates/", import.meta.url).pathname;
|
|
1284
2577
|
const taxonomy = typeof opts.taxonomy === "string" ? opts.taxonomy.split(",").map((s) => s.trim()).filter((s) => s.length > 0) : void 0;
|
|
1285
2578
|
emit(await runInit({
|
|
@@ -1290,51 +2583,58 @@ program.command("init").option("--target <dir>", "explicit target directory").re
|
|
|
1290
2583
|
domain: opts.domain,
|
|
1291
2584
|
taxonomy,
|
|
1292
2585
|
lang: opts.lang,
|
|
1293
|
-
force: !!opts.force
|
|
2586
|
+
force: !!opts.force,
|
|
2587
|
+
noEnv: opts.env === false,
|
|
2588
|
+
profile: opts.profile
|
|
1294
2589
|
}));
|
|
1295
2590
|
});
|
|
1296
|
-
async function resolveVaultArg(arg) {
|
|
2591
|
+
async function resolveVaultArg(arg, wiki) {
|
|
1297
2592
|
if (arg) return { ok: true, vault: arg };
|
|
1298
2593
|
const r = await resolveRuntimePath({
|
|
1299
2594
|
flag: void 0,
|
|
1300
2595
|
envValue: process.env.WIKI_PATH,
|
|
1301
|
-
|
|
2596
|
+
wikiEnv: process.env.WIKI,
|
|
2597
|
+
home: process.env.HOME ?? "",
|
|
2598
|
+
wiki
|
|
1302
2599
|
});
|
|
1303
|
-
if (!r.ok)
|
|
2600
|
+
if (!r.ok) {
|
|
2601
|
+
const exitCode = r.error === "UNKNOWN_WIKI_PROFILE" ? 35 : 25;
|
|
2602
|
+
return { ok: false, exitCode, payload: r };
|
|
2603
|
+
}
|
|
1304
2604
|
return { ok: true, vault: r.data.path };
|
|
1305
2605
|
}
|
|
1306
|
-
program.command("links [vault]").action(async (vault) => {
|
|
1307
|
-
const v = await resolveVaultArg(vault);
|
|
2606
|
+
program.command("links [vault]").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2607
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1308
2608
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1309
2609
|
else emit(await runLinks({ vault: v.vault }));
|
|
1310
2610
|
});
|
|
1311
|
-
program.command("tag-audit [vault]").action(async (vault) => {
|
|
1312
|
-
const v = await resolveVaultArg(vault);
|
|
2611
|
+
program.command("tag-audit [vault]").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2612
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1313
2613
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1314
2614
|
else emit(await runTagAudit({ vault: v.vault }));
|
|
1315
2615
|
});
|
|
1316
|
-
program.command("index-check [vault]").action(async (vault) => {
|
|
1317
|
-
const v = await resolveVaultArg(vault);
|
|
2616
|
+
program.command("index-check [vault]").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2617
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1318
2618
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1319
2619
|
else emit(await runIndexCheck({ vault: v.vault }));
|
|
1320
2620
|
});
|
|
1321
|
-
program.command("stale [vault]").option("--days <n>", "staleness threshold in days", (s) => parseInt(s, 10), 90).action(async (vault, opts) => {
|
|
1322
|
-
const v = await resolveVaultArg(vault);
|
|
2621
|
+
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) => {
|
|
2622
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1323
2623
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1324
2624
|
else emit(await runStale({ vault: v.vault, days: opts.days }));
|
|
1325
2625
|
});
|
|
1326
|
-
program.command("pagesize [vault]").option("--lines <n>", "max body lines", (s) => parseInt(s, 10), 200).action(async (vault, opts) => {
|
|
1327
|
-
const v = await resolveVaultArg(vault);
|
|
2626
|
+
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) => {
|
|
2627
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1328
2628
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1329
2629
|
else emit(await runPagesize({ vault: v.vault, lines: opts.lines }));
|
|
1330
2630
|
});
|
|
1331
|
-
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) => {
|
|
1332
|
-
const v = await resolveVaultArg(vault);
|
|
2631
|
+
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) => {
|
|
2632
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1333
2633
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1334
2634
|
else emit(await runLogRotate({ vault: v.vault, threshold: opts.threshold, apply: !!opts.apply }));
|
|
1335
2635
|
});
|
|
1336
|
-
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) => {
|
|
1337
|
-
const v = await resolveVaultArg(vault);
|
|
2636
|
+
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) => {
|
|
2637
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
1338
2638
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1339
2639
|
else emit(await runLint({
|
|
1340
2640
|
vault: v.vault,
|
|
@@ -1344,6 +2644,48 @@ program.command("lint [vault]").option("--days <n>", "stale threshold", (s) => p
|
|
|
1344
2644
|
logThreshold: opts.logThreshold
|
|
1345
2645
|
}));
|
|
1346
2646
|
});
|
|
2647
|
+
var configCmd = program.command("config").description("manage skillwiki configuration");
|
|
2648
|
+
configCmd.command("get <key>").description("print the value of a config key").action(async (key) => emit(await runConfigGet({ key, home: process.env.HOME ?? "" })));
|
|
2649
|
+
configCmd.command("set <key> <value>").description("set a config key value").action(async (key, value) => emit(await runConfigSet({ key, value, home: process.env.HOME ?? "" })));
|
|
2650
|
+
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 })));
|
|
2651
|
+
configCmd.command("path").description("print the config file path").action(async () => emit(await runConfigPath({ home: process.env.HOME ?? "" })));
|
|
2652
|
+
program.command("doctor").description("diagnose skillwiki setup issues").action(async () => emit(await runDoctor({
|
|
2653
|
+
home: process.env.HOME ?? "",
|
|
2654
|
+
envValue: process.env.WIKI_PATH,
|
|
2655
|
+
argv: process.argv,
|
|
2656
|
+
currentVersion: pkg.version,
|
|
2657
|
+
cwd: process.cwd()
|
|
2658
|
+
})));
|
|
2659
|
+
program.command("archive <page> [vault]").description("archive a typed-knowledge page").option("--wiki <name>", "wiki profile name").action(async (page, vault, opts) => {
|
|
2660
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2661
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2662
|
+
else emit(await runArchive({ vault: v.vault, page }));
|
|
2663
|
+
});
|
|
2664
|
+
program.command("drift [vault]").description("detect content drift in raw sources").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
2665
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2666
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2667
|
+
else emit(await runDrift({ vault: v.vault }));
|
|
2668
|
+
});
|
|
2669
|
+
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) => {
|
|
2670
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2671
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2672
|
+
else emit(await runDedup({ vault: v.vault, apply: opts.apply }));
|
|
2673
|
+
});
|
|
2674
|
+
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) => {
|
|
2675
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2676
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2677
|
+
else emit(await runMigrateCitations({ vault: v.vault, dryRun: !!opts.dryRun }));
|
|
2678
|
+
});
|
|
2679
|
+
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) => {
|
|
2680
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
2681
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
2682
|
+
else emit(await runFrontmatterFix({ vault: v.vault, dryRun: !!opts.dryRun }));
|
|
2683
|
+
});
|
|
2684
|
+
program.command("update").description("update skillwiki CLI to the latest version").option("--tag <tag>", "npm dist-tag", "beta").action(async (opts) => emit(await runUpdate({
|
|
2685
|
+
home: process.env.HOME ?? "",
|
|
2686
|
+
distTag: opts.tag
|
|
2687
|
+
})));
|
|
2688
|
+
triggerAutoUpdate(process.env.HOME ?? "", pkg.version);
|
|
1347
2689
|
program.parseAsync(process.argv).catch((e) => {
|
|
1348
2690
|
process.stdout.write(JSON.stringify({ ok: false, error: "INTERNAL", detail: { message: String(e) } }) + "\n");
|
|
1349
2691
|
process.exit(1);
|