skillwiki 0.8.0 → 0.8.1-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +831 -316
- package/package.json +1 -1
- package/skills/.claude-plugin/plugin.json +1 -1
- package/skills/.codex-plugin/plugin.json +2 -2
- package/skills/README.md +3 -1
- package/skills/hooks/session-start +1 -1
- package/skills/package.json +2 -1
- package/skills/skills/proj-decide/SKILL.md +25 -0
- package/skills/skills/proj-distill/SKILL.md +55 -0
- package/skills/skills/proj-init/SKILL.md +30 -0
- package/skills/skills/proj-work/SKILL.md +69 -0
- package/skills/skills/using-skillwiki/SKILL.md +157 -0
- package/skills/skills/wiki-adapter-prd/SKILL.md +88 -0
- package/skills/skills/wiki-add-task/SKILL.md +102 -0
- package/skills/skills/wiki-archive/SKILL.md +46 -0
- package/skills/skills/wiki-audit/SKILL.md +34 -0
- package/skills/skills/wiki-canvas/SKILL.md +57 -0
- package/skills/skills/wiki-crystallize/SKILL.md +29 -0
- package/skills/skills/wiki-gate-plan-mode/SKILL.md +80 -0
- package/skills/skills/wiki-ingest/SKILL.md +55 -0
- package/skills/skills/wiki-init/SKILL.md +37 -0
- package/skills/skills/wiki-lint/SKILL.md +25 -0
- package/skills/skills/wiki-query/SKILL.md +36 -0
- package/skills/skills/wiki-reingest/SKILL.md +55 -0
- package/skills/skills/wiki-sync/SKILL.md +240 -0
- package/skills/using-skillwiki/SKILL.md +46 -8
- package/skills/wiki-query/SKILL.md +1 -1
package/dist/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
|
|
10
10
|
// src/cli.ts
|
|
11
11
|
import { readFileSync as readFileSync12 } from "fs";
|
|
12
|
-
import { join as
|
|
12
|
+
import { join as join44 } from "path";
|
|
13
13
|
import { Command as Command2 } from "commander";
|
|
14
14
|
|
|
15
15
|
// ../shared/src/exit-codes.ts
|
|
@@ -62,7 +62,8 @@ var ExitCode = {
|
|
|
62
62
|
BACKUP_RESTORE_CONFLICTS: 45,
|
|
63
63
|
USAGE: 46,
|
|
64
64
|
BODY_TRUNCATION_GUARD: 47,
|
|
65
|
-
SYNC_LOCK_HELD: 48
|
|
65
|
+
SYNC_LOCK_HELD: 48,
|
|
66
|
+
LOG_APPEND_LOCK_HELD: 49
|
|
66
67
|
};
|
|
67
68
|
|
|
68
69
|
// ../shared/src/json-output.ts
|
|
@@ -499,6 +500,7 @@ import { dirname } from "path";
|
|
|
499
500
|
import { readFile as readFile3, readdir, stat } from "fs/promises";
|
|
500
501
|
import { join as join3, relative as relative2, sep as sep2 } from "path";
|
|
501
502
|
var TYPED_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
|
|
503
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
502
504
|
async function scanVault(root) {
|
|
503
505
|
try {
|
|
504
506
|
await stat(join3(root, "SCHEMA.md"));
|
|
@@ -509,6 +511,7 @@ async function scanVault(root) {
|
|
|
509
511
|
const rels = all.map((p) => ({ absPath: p, relPath: relative2(root, p).split(sep2).join("/") }));
|
|
510
512
|
return ok({
|
|
511
513
|
root,
|
|
514
|
+
allMarkdown: rels,
|
|
512
515
|
typedKnowledge: rels.filter((p) => TYPED_DIRS.some((d) => p.relPath.startsWith(d + "/"))),
|
|
513
516
|
raw: rels.filter((p) => p.relPath.startsWith("raw/")),
|
|
514
517
|
workItems: rels.filter((p) => /^projects\/[^/]+\/work\/[^/]+\/(spec|plan|log)\.md$/.test(p.relPath)),
|
|
@@ -520,8 +523,10 @@ async function walk(dir) {
|
|
|
520
523
|
const out = [];
|
|
521
524
|
for (const e of entries) {
|
|
522
525
|
const p = join3(dir, e.name);
|
|
523
|
-
if (e.isDirectory())
|
|
524
|
-
|
|
526
|
+
if (e.isDirectory()) {
|
|
527
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
528
|
+
out.push(...await walk(p));
|
|
529
|
+
} else if (e.isFile() && e.name.endsWith(".md")) out.push(p);
|
|
525
530
|
}
|
|
526
531
|
return out;
|
|
527
532
|
}
|
|
@@ -547,23 +552,137 @@ function extractBodyWikilinks(body) {
|
|
|
547
552
|
return out;
|
|
548
553
|
}
|
|
549
554
|
|
|
550
|
-
// src/
|
|
551
|
-
async function
|
|
552
|
-
const scan = await scanVault(input.vault);
|
|
553
|
-
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
555
|
+
// src/utils/community.ts
|
|
556
|
+
async function buildWikilinkAdjacency(typedKnowledge) {
|
|
554
557
|
const adjacency = {};
|
|
555
558
|
const slugToPath = {};
|
|
556
|
-
for (const p of
|
|
559
|
+
for (const p of typedKnowledge) {
|
|
557
560
|
const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
|
|
558
561
|
slugToPath[slug] = p.relPath;
|
|
559
562
|
}
|
|
560
|
-
for (const p of
|
|
563
|
+
for (const p of typedKnowledge) {
|
|
561
564
|
const text = await readPage(p);
|
|
562
565
|
const split = splitFrontmatter(text);
|
|
563
566
|
const body = split.ok ? split.data.body : text;
|
|
564
567
|
const links = extractBodyWikilinks(body);
|
|
565
568
|
adjacency[p.relPath] = links.map((slug) => slugToPath[slug.split("/").pop()]).filter((x) => Boolean(x));
|
|
566
569
|
}
|
|
570
|
+
return adjacency;
|
|
571
|
+
}
|
|
572
|
+
function toUndirectedWeighted(adj) {
|
|
573
|
+
const g = /* @__PURE__ */ new Map();
|
|
574
|
+
const ensure = (n) => {
|
|
575
|
+
let m = g.get(n);
|
|
576
|
+
if (!m) {
|
|
577
|
+
m = /* @__PURE__ */ new Map();
|
|
578
|
+
g.set(n, m);
|
|
579
|
+
}
|
|
580
|
+
return m;
|
|
581
|
+
};
|
|
582
|
+
for (const node of Object.keys(adj)) ensure(node);
|
|
583
|
+
for (const [a, nbrs] of Object.entries(adj)) {
|
|
584
|
+
for (const b of nbrs) {
|
|
585
|
+
if (a === b) continue;
|
|
586
|
+
ensure(a).set(b, 1);
|
|
587
|
+
ensure(b).set(a, 1);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return g;
|
|
591
|
+
}
|
|
592
|
+
function louvain(g) {
|
|
593
|
+
const nodes = [...g.keys()].sort();
|
|
594
|
+
const comm = /* @__PURE__ */ new Map();
|
|
595
|
+
nodes.forEach((n, i) => comm.set(n, i));
|
|
596
|
+
const k = /* @__PURE__ */ new Map();
|
|
597
|
+
let m2 = 0;
|
|
598
|
+
for (const n of nodes) {
|
|
599
|
+
let deg = 0;
|
|
600
|
+
for (const w of g.get(n).values()) deg += w;
|
|
601
|
+
k.set(n, deg);
|
|
602
|
+
m2 += deg;
|
|
603
|
+
}
|
|
604
|
+
if (m2 === 0) return comm;
|
|
605
|
+
const sumTot = /* @__PURE__ */ new Map();
|
|
606
|
+
for (const n of nodes) {
|
|
607
|
+
const c = comm.get(n);
|
|
608
|
+
sumTot.set(c, (sumTot.get(c) ?? 0) + k.get(n));
|
|
609
|
+
}
|
|
610
|
+
let improved = true;
|
|
611
|
+
while (improved) {
|
|
612
|
+
improved = false;
|
|
613
|
+
for (const n of nodes) {
|
|
614
|
+
const cur = comm.get(n);
|
|
615
|
+
const kn = k.get(n);
|
|
616
|
+
sumTot.set(cur, sumTot.get(cur) - kn);
|
|
617
|
+
const wToComm = /* @__PURE__ */ new Map();
|
|
618
|
+
for (const [nb, w] of g.get(n)) {
|
|
619
|
+
if (nb === n) continue;
|
|
620
|
+
const c = comm.get(nb);
|
|
621
|
+
wToComm.set(c, (wToComm.get(c) ?? 0) + w);
|
|
622
|
+
}
|
|
623
|
+
const gainFor = (c) => (wToComm.get(c) ?? 0) - (sumTot.get(c) ?? 0) * kn / m2;
|
|
624
|
+
const curGain = gainFor(cur);
|
|
625
|
+
let bestComm = cur;
|
|
626
|
+
let bestDelta = 0;
|
|
627
|
+
for (const c of wToComm.keys()) {
|
|
628
|
+
const delta = gainFor(c) - curGain;
|
|
629
|
+
if (delta > bestDelta) {
|
|
630
|
+
bestDelta = delta;
|
|
631
|
+
bestComm = c;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
comm.set(n, bestComm);
|
|
635
|
+
sumTot.set(bestComm, (sumTot.get(bestComm) ?? 0) + kn);
|
|
636
|
+
if (bestComm !== cur) improved = true;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return comm;
|
|
640
|
+
}
|
|
641
|
+
function communityCohesion(members, g) {
|
|
642
|
+
const n = members.length;
|
|
643
|
+
if (n < 2) return 1;
|
|
644
|
+
const set = new Set(members);
|
|
645
|
+
let internal = 0;
|
|
646
|
+
for (const a of members) {
|
|
647
|
+
for (const [b, w] of g.get(a) ?? /* @__PURE__ */ new Map()) {
|
|
648
|
+
if (a < b && set.has(b)) internal += w;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return internal / (n * (n - 1) / 2);
|
|
652
|
+
}
|
|
653
|
+
function findSparseCommunities(adj, opts = {}) {
|
|
654
|
+
const minSize = opts.minSize ?? 3;
|
|
655
|
+
const maxCohesion = opts.maxCohesion ?? 0.15;
|
|
656
|
+
const g = toUndirectedWeighted(adj);
|
|
657
|
+
const comm = louvain(g);
|
|
658
|
+
const groups = /* @__PURE__ */ new Map();
|
|
659
|
+
for (const [node, c] of comm) {
|
|
660
|
+
const arr = groups.get(c);
|
|
661
|
+
if (arr) arr.push(node);
|
|
662
|
+
else groups.set(c, [node]);
|
|
663
|
+
}
|
|
664
|
+
const out = [];
|
|
665
|
+
for (const members of groups.values()) {
|
|
666
|
+
if (members.length < minSize) continue;
|
|
667
|
+
const cohesion = communityCohesion(members, g);
|
|
668
|
+
if (cohesion < maxCohesion) {
|
|
669
|
+
out.push({
|
|
670
|
+
members: [...members].sort(),
|
|
671
|
+
size: members.length,
|
|
672
|
+
cohesion: Math.round(cohesion * 1e3) / 1e3,
|
|
673
|
+
action: members.length <= 5 ? "merge into adjacent community" : "split into smaller topics"
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
out.sort((a, b) => a.cohesion - b.cohesion);
|
|
678
|
+
return out;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/commands/graph.ts
|
|
682
|
+
async function runGraphBuild(input) {
|
|
683
|
+
const scan = await scanVault(input.vault);
|
|
684
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
685
|
+
const adjacency = await buildWikilinkAdjacency(scan.data.typedKnowledge);
|
|
567
686
|
const adamicAdar = computeAdamicAdar(adjacency);
|
|
568
687
|
const edge_count = Object.values(adjacency).reduce((acc, arr) => acc + arr.length, 0);
|
|
569
688
|
try {
|
|
@@ -2362,10 +2481,124 @@ Chronological action log. Newest entries last. Skill writes append entries; lint
|
|
|
2362
2481
|
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: true, rotated_to: rotatedName, humanHint: `rotated ${entries} entries to ${rotatedName}` }) };
|
|
2363
2482
|
}
|
|
2364
2483
|
|
|
2484
|
+
// src/commands/log-append.ts
|
|
2485
|
+
import { readFile as readFile13, rename as rename4, writeFile as writeFile8, stat as stat6 } from "fs/promises";
|
|
2486
|
+
import { join as join17 } from "path";
|
|
2487
|
+
|
|
2488
|
+
// src/utils/log-lock.ts
|
|
2489
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2490
|
+
import { join as join16 } from "path";
|
|
2491
|
+
function logLockPath(vault) {
|
|
2492
|
+
return join16(vault, ".skillwiki", "log-append.lock");
|
|
2493
|
+
}
|
|
2494
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2495
|
+
async function acquireLogLock(vault, opts = {}) {
|
|
2496
|
+
const retryMs = opts.retryMs ?? 2e3;
|
|
2497
|
+
const pollMs = opts.pollMs ?? 50;
|
|
2498
|
+
const staleMs = opts.staleMs ?? 1e4;
|
|
2499
|
+
const path = logLockPath(vault);
|
|
2500
|
+
const dir = join16(vault, ".skillwiki");
|
|
2501
|
+
if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
|
|
2502
|
+
const deadline = Date.now() + retryMs;
|
|
2503
|
+
const content = JSON.stringify({ pid: process.pid, acquired: (/* @__PURE__ */ new Date()).toISOString() }) + "\n";
|
|
2504
|
+
for (; ; ) {
|
|
2505
|
+
try {
|
|
2506
|
+
writeFileSync2(path, content, { flag: "wx" });
|
|
2507
|
+
return { ok: true };
|
|
2508
|
+
} catch (e) {
|
|
2509
|
+
const err3 = e;
|
|
2510
|
+
if (err3.code !== "EEXIST") throw err3;
|
|
2511
|
+
}
|
|
2512
|
+
try {
|
|
2513
|
+
const age = Date.now() - statSync2(path).mtimeMs;
|
|
2514
|
+
if (age > staleMs) {
|
|
2515
|
+
unlinkSync2(path);
|
|
2516
|
+
continue;
|
|
2517
|
+
}
|
|
2518
|
+
} catch {
|
|
2519
|
+
continue;
|
|
2520
|
+
}
|
|
2521
|
+
if (Date.now() >= deadline) return { ok: false };
|
|
2522
|
+
await sleep(pollMs);
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
function releaseLogLock(vault) {
|
|
2526
|
+
try {
|
|
2527
|
+
unlinkSync2(logLockPath(vault));
|
|
2528
|
+
} catch {
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
// src/commands/log-append.ts
|
|
2533
|
+
var ENTRY_RE2 = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
|
|
2534
|
+
async function runLogAppend(input) {
|
|
2535
|
+
try {
|
|
2536
|
+
await stat6(join17(input.vault, "SCHEMA.md"));
|
|
2537
|
+
} catch {
|
|
2538
|
+
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
2539
|
+
}
|
|
2540
|
+
const content = (input.content ?? "").trim();
|
|
2541
|
+
if (content.length === 0) {
|
|
2542
|
+
return { exitCode: ExitCode.USAGE, result: err("USAGE", { message: "--content must be a non-empty log entry" }) };
|
|
2543
|
+
}
|
|
2544
|
+
const acquired = await acquireLogLock(input.vault);
|
|
2545
|
+
if (!acquired.ok) {
|
|
2546
|
+
return { exitCode: ExitCode.LOG_APPEND_LOCK_HELD, result: err("LOG_APPEND_LOCK_HELD", { vault: input.vault }) };
|
|
2547
|
+
}
|
|
2548
|
+
const logPath = join17(input.vault, "log.md");
|
|
2549
|
+
try {
|
|
2550
|
+
let logText;
|
|
2551
|
+
try {
|
|
2552
|
+
logText = await readFile13(logPath, "utf8");
|
|
2553
|
+
} catch {
|
|
2554
|
+
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
|
|
2555
|
+
}
|
|
2556
|
+
const entriesBefore = [...logText.matchAll(ENTRY_RE2)].length;
|
|
2557
|
+
const body = logText.replace(/\s+$/, "");
|
|
2558
|
+
const next = `${body}
|
|
2559
|
+
|
|
2560
|
+
${content}
|
|
2561
|
+
`;
|
|
2562
|
+
try {
|
|
2563
|
+
const tmp = logPath + ".tmp";
|
|
2564
|
+
await writeFile8(tmp, next, "utf8");
|
|
2565
|
+
await rename4(tmp, logPath);
|
|
2566
|
+
} catch (e) {
|
|
2567
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
|
|
2568
|
+
}
|
|
2569
|
+
appendLastOp(input.vault, {
|
|
2570
|
+
operation: "log-append",
|
|
2571
|
+
summary: `appended log entry (${entriesBefore}->${entriesBefore + 1})`,
|
|
2572
|
+
files: ["log.md"],
|
|
2573
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2574
|
+
});
|
|
2575
|
+
const entriesAfter = entriesBefore + 1;
|
|
2576
|
+
return {
|
|
2577
|
+
exitCode: ExitCode.OK,
|
|
2578
|
+
result: ok({ entries_before: entriesBefore, entries_after: entriesAfter, appended: true, humanHint: `appended log entry (${entriesBefore}->${entriesAfter})` })
|
|
2579
|
+
};
|
|
2580
|
+
} finally {
|
|
2581
|
+
releaseLogLock(input.vault);
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2365
2585
|
// src/commands/lint.ts
|
|
2366
|
-
import { existsSync as
|
|
2367
|
-
import { readFile as
|
|
2368
|
-
import { join as
|
|
2586
|
+
import { existsSync as existsSync5 } from "fs";
|
|
2587
|
+
import { readFile as readFile17 } from "fs/promises";
|
|
2588
|
+
import { join as join22 } from "path";
|
|
2589
|
+
|
|
2590
|
+
// src/commands/sparse-community.ts
|
|
2591
|
+
async function runSparseCommunity(input) {
|
|
2592
|
+
const scan = await scanVault(input.vault);
|
|
2593
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
2594
|
+
const adjacency = await buildWikilinkAdjacency(scan.data.typedKnowledge);
|
|
2595
|
+
const communities = findSparseCommunities(adjacency, {
|
|
2596
|
+
minSize: input.minSize,
|
|
2597
|
+
maxCohesion: input.maxCohesion
|
|
2598
|
+
});
|
|
2599
|
+
const humanHint = communities.length === 0 ? "no sparse communities" : communities.map((c) => ` cohesion ${c.cohesion} (${c.size} pages): ${c.action}`).join("\n");
|
|
2600
|
+
return { exitCode: ExitCode.OK, result: ok({ communities, humanHint }) };
|
|
2601
|
+
}
|
|
2369
2602
|
|
|
2370
2603
|
// src/commands/topic-map-check.ts
|
|
2371
2604
|
var DEFAULT_THRESHOLD = 200;
|
|
@@ -2387,13 +2620,13 @@ async function runTopicMapCheck(input) {
|
|
|
2387
2620
|
}
|
|
2388
2621
|
|
|
2389
2622
|
// src/commands/index-link-format.ts
|
|
2390
|
-
import { readFile as
|
|
2391
|
-
import { join as
|
|
2623
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
2624
|
+
import { join as join18 } from "path";
|
|
2392
2625
|
var MD_LINK_RE = /\[[^\[\]]+\]\([^)]+\.md\)/;
|
|
2393
2626
|
async function runIndexLinkFormat(input) {
|
|
2394
2627
|
let text = "";
|
|
2395
2628
|
try {
|
|
2396
|
-
text = await
|
|
2629
|
+
text = await readFile14(join18(input.vault, "index.md"), "utf8");
|
|
2397
2630
|
} catch {
|
|
2398
2631
|
}
|
|
2399
2632
|
const markdown_links = [];
|
|
@@ -2406,8 +2639,8 @@ ${markdown_links.map((l) => ` line ${l.line}: ${l.text}`).join("\n")}`;
|
|
|
2406
2639
|
}
|
|
2407
2640
|
|
|
2408
2641
|
// src/commands/dedup.ts
|
|
2409
|
-
import { readFileSync as readFileSync3, writeFileSync as
|
|
2410
|
-
import { join as
|
|
2642
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3 } from "fs";
|
|
2643
|
+
import { join as join19 } from "path";
|
|
2411
2644
|
async function runDedup(input) {
|
|
2412
2645
|
const scan = await scanVault(input.vault);
|
|
2413
2646
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
@@ -2435,7 +2668,7 @@ async function runDedup(input) {
|
|
|
2435
2668
|
}
|
|
2436
2669
|
}
|
|
2437
2670
|
for (const page of scan.data.typedKnowledge) {
|
|
2438
|
-
const text = readFileSync3(
|
|
2671
|
+
const text = readFileSync3(join19(input.vault, page.relPath), "utf-8");
|
|
2439
2672
|
let updated = text;
|
|
2440
2673
|
let changed = false;
|
|
2441
2674
|
for (const [oldPath, newPath] of replacements) {
|
|
@@ -2453,14 +2686,14 @@ async function runDedup(input) {
|
|
|
2453
2686
|
}
|
|
2454
2687
|
}
|
|
2455
2688
|
if (changed) {
|
|
2456
|
-
|
|
2689
|
+
writeFileSync3(join19(input.vault, page.relPath), updated);
|
|
2457
2690
|
rewired.push(page.relPath);
|
|
2458
2691
|
}
|
|
2459
2692
|
}
|
|
2460
2693
|
for (const [oldPath] of replacements) {
|
|
2461
|
-
const fullPath =
|
|
2694
|
+
const fullPath = join19(input.vault, oldPath);
|
|
2462
2695
|
try {
|
|
2463
|
-
|
|
2696
|
+
unlinkSync3(fullPath);
|
|
2464
2697
|
removed.push(oldPath);
|
|
2465
2698
|
} catch {
|
|
2466
2699
|
}
|
|
@@ -2493,9 +2726,9 @@ async function runDedup(input) {
|
|
|
2493
2726
|
}
|
|
2494
2727
|
|
|
2495
2728
|
// src/utils/safe-write.ts
|
|
2496
|
-
import { open, readFile as
|
|
2729
|
+
import { open, readFile as readFile15, rename as rename5, unlink as unlink2, writeFile as writeFile9 } from "fs/promises";
|
|
2497
2730
|
import { randomBytes } from "crypto";
|
|
2498
|
-
import { dirname as dirname7, basename, join as
|
|
2731
|
+
import { dirname as dirname7, basename, join as join20 } from "path";
|
|
2499
2732
|
var DEFAULT_MIN_BODY_RATIO = 0.5;
|
|
2500
2733
|
var DEFAULT_MIN_OLD_BODY_BYTES = 200;
|
|
2501
2734
|
function bodyBytes(text) {
|
|
@@ -2505,7 +2738,7 @@ function bodyBytes(text) {
|
|
|
2505
2738
|
}
|
|
2506
2739
|
async function readIfExists(absPath) {
|
|
2507
2740
|
try {
|
|
2508
|
-
return await
|
|
2741
|
+
return await readFile15(absPath, "utf8");
|
|
2509
2742
|
} catch (e) {
|
|
2510
2743
|
if (e.code === "ENOENT") return null;
|
|
2511
2744
|
throw e;
|
|
@@ -2544,7 +2777,7 @@ async function safeWritePage(absPath, newContent, opts = {}) {
|
|
|
2544
2777
|
}
|
|
2545
2778
|
const dir = dirname7(absPath);
|
|
2546
2779
|
const tmpName = `.${basename(absPath)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
2547
|
-
const tmpPath =
|
|
2780
|
+
const tmpPath = join20(dir, tmpName);
|
|
2548
2781
|
try {
|
|
2549
2782
|
const handle = await open(tmpPath, "w");
|
|
2550
2783
|
try {
|
|
@@ -2556,7 +2789,7 @@ async function safeWritePage(absPath, newContent, opts = {}) {
|
|
|
2556
2789
|
} finally {
|
|
2557
2790
|
await handle.close();
|
|
2558
2791
|
}
|
|
2559
|
-
await
|
|
2792
|
+
await rename5(tmpPath, absPath);
|
|
2560
2793
|
return ok({ isNew, oldBodyBytes, newBodyBytes, bodyRatio, guardSkippedSmall });
|
|
2561
2794
|
} catch (e) {
|
|
2562
2795
|
try {
|
|
@@ -2608,28 +2841,100 @@ async function runRawBodyDedup(vault) {
|
|
|
2608
2841
|
}
|
|
2609
2842
|
|
|
2610
2843
|
// src/commands/path-too-long.ts
|
|
2844
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2845
|
+
import { mkdir as mkdir8, readFile as readFile16, rename as rename6, unlink as unlink3 } from "fs/promises";
|
|
2846
|
+
import { dirname as dirname8, join as join21, posix, resolve as resolve4 } from "path";
|
|
2611
2847
|
var MAX_PATH_LENGTH = 240;
|
|
2848
|
+
var WINDOWS_ABSOLUTE_PATH_LIMIT = 259;
|
|
2612
2849
|
async function runPathTooLong(input) {
|
|
2613
2850
|
const scan = await scanVault(input.vault);
|
|
2614
2851
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
2615
|
-
const
|
|
2616
|
-
const violations = [];
|
|
2617
|
-
for (const page of allPages) {
|
|
2618
|
-
if (page.relPath.length > MAX_PATH_LENGTH) {
|
|
2619
|
-
violations.push({ relPath: page.relPath, length: page.relPath.length });
|
|
2620
|
-
}
|
|
2621
|
-
}
|
|
2852
|
+
const violations = findPathTooLongViolations(scan.data.allMarkdown, MAX_PATH_LENGTH);
|
|
2622
2853
|
if (violations.length > 0) {
|
|
2623
2854
|
return {
|
|
2624
2855
|
exitCode: ExitCode.LINT_HAS_ERRORS,
|
|
2625
2856
|
result: ok({
|
|
2626
2857
|
violations,
|
|
2627
|
-
humanHint: violations.map((v) => `${v.relPath}: ${v.length} chars (max ${MAX_PATH_LENGTH})`).join("\n")
|
|
2858
|
+
humanHint: violations.map((v) => `${v.relPath}: ${v.length} chars (max ${MAX_PATH_LENGTH}) -> ${v.suggestedRelPath}`).join("\n")
|
|
2628
2859
|
})
|
|
2629
2860
|
};
|
|
2630
2861
|
}
|
|
2631
2862
|
return { exitCode: ExitCode.OK, result: ok({ violations, humanHint: "all paths within length limit" }) };
|
|
2632
2863
|
}
|
|
2864
|
+
async function fixPathTooLong(input) {
|
|
2865
|
+
const scan = await scanVault(input.vault);
|
|
2866
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
2867
|
+
const maxFixLength = maxFixPathLength(input.vault);
|
|
2868
|
+
const violations = findPathTooLongViolations(scan.data.allMarkdown, maxFixLength);
|
|
2869
|
+
const fixed = [];
|
|
2870
|
+
const unresolved = [];
|
|
2871
|
+
for (const violation of violations) {
|
|
2872
|
+
const target = await resolveFixTarget(input.vault, violation.relPath, violation.suggestedRelPath, maxFixLength);
|
|
2873
|
+
if (!target || target.relPath === violation.relPath || target.relPath.length > maxFixLength) {
|
|
2874
|
+
unresolved.push(violation.relPath);
|
|
2875
|
+
continue;
|
|
2876
|
+
}
|
|
2877
|
+
try {
|
|
2878
|
+
if (target.mode === "dedupe") {
|
|
2879
|
+
await unlink3(join21(input.vault, violation.relPath));
|
|
2880
|
+
} else {
|
|
2881
|
+
await mkdir8(dirname8(join21(input.vault, target.relPath)), { recursive: true });
|
|
2882
|
+
await rename6(join21(input.vault, violation.relPath), join21(input.vault, target.relPath));
|
|
2883
|
+
}
|
|
2884
|
+
fixed.push({ from: violation.relPath, to: target.relPath });
|
|
2885
|
+
} catch {
|
|
2886
|
+
unresolved.push(violation.relPath);
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
const rewired = [];
|
|
2890
|
+
if (fixed.length > 0) {
|
|
2891
|
+
const afterScan = await scanVault(input.vault);
|
|
2892
|
+
if (afterScan.ok) {
|
|
2893
|
+
for (const page of afterScan.data.allMarkdown) {
|
|
2894
|
+
if (!shouldRewriteReferences(page.relPath)) continue;
|
|
2895
|
+
try {
|
|
2896
|
+
const original = await readFile16(page.absPath, "utf8");
|
|
2897
|
+
let updated = original;
|
|
2898
|
+
for (const fix of fixed) {
|
|
2899
|
+
updated = replacePathReferences(updated, fix.from, fix.to);
|
|
2900
|
+
}
|
|
2901
|
+
if (updated !== original) {
|
|
2902
|
+
const write = await safeWritePage(page.absPath, updated);
|
|
2903
|
+
if (write.ok) rewired.push(page.relPath);
|
|
2904
|
+
else unresolved.push(`${page.relPath} (rewire)`);
|
|
2905
|
+
}
|
|
2906
|
+
} catch {
|
|
2907
|
+
unresolved.push(`${page.relPath} (rewire)`);
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
const hintLines = [
|
|
2913
|
+
`fixed: ${fixed.length}`,
|
|
2914
|
+
`rewired: ${rewired.length}`,
|
|
2915
|
+
`unresolved: ${unresolved.length}`
|
|
2916
|
+
];
|
|
2917
|
+
for (const f of fixed) hintLines.push(` ${f.from} -> ${f.to}`);
|
|
2918
|
+
for (const u of unresolved) hintLines.push(` unresolved: ${u}`);
|
|
2919
|
+
return {
|
|
2920
|
+
exitCode: unresolved.length > 0 ? ExitCode.LINT_HAS_ERRORS : ExitCode.OK,
|
|
2921
|
+
result: ok({ fixed, unresolved, rewired, humanHint: hintLines.join("\n") })
|
|
2922
|
+
};
|
|
2923
|
+
}
|
|
2924
|
+
function findPathTooLongViolations(pages, maxLength) {
|
|
2925
|
+
return pages.filter((page) => page.relPath.length > maxLength).map((page) => ({
|
|
2926
|
+
relPath: page.relPath,
|
|
2927
|
+
length: page.relPath.length,
|
|
2928
|
+
suggestedRelPath: truncateFilename(page.relPath, maxLength)
|
|
2929
|
+
}));
|
|
2930
|
+
}
|
|
2931
|
+
function maxFixPathLength(vault) {
|
|
2932
|
+
if (process.platform !== "win32") return MAX_PATH_LENGTH;
|
|
2933
|
+
const root = resolve4(vault);
|
|
2934
|
+
const separatorBudget = root.endsWith("\\") || root.endsWith("/") ? 0 : 1;
|
|
2935
|
+
const absoluteSafeRelLength = WINDOWS_ABSOLUTE_PATH_LIMIT - root.length - separatorBudget;
|
|
2936
|
+
return Math.max(1, Math.min(MAX_PATH_LENGTH, absoluteSafeRelLength));
|
|
2937
|
+
}
|
|
2633
2938
|
function truncateFilename(relPath, maxLength = MAX_PATH_LENGTH) {
|
|
2634
2939
|
if (relPath.length <= maxLength) return relPath;
|
|
2635
2940
|
const lastSlash = relPath.lastIndexOf("/");
|
|
@@ -2643,15 +2948,62 @@ function truncateFilename(relPath, maxLength = MAX_PATH_LENGTH) {
|
|
|
2643
2948
|
const maxPrefixLen = maxLength - dirPrefix.length - suffix.length;
|
|
2644
2949
|
if (maxPrefixLen <= 0) {
|
|
2645
2950
|
const fallback = dirPrefix + hash + ext;
|
|
2646
|
-
|
|
2647
|
-
const dirBudget = maxLength - suffix.length;
|
|
2648
|
-
return dirPrefix.slice(0, Math.max(0, dirBudget)) + suffix;
|
|
2649
|
-
}
|
|
2650
|
-
return fallback;
|
|
2951
|
+
return fallback.length <= maxLength ? fallback : relPath;
|
|
2651
2952
|
}
|
|
2652
2953
|
const prefix = base.slice(0, maxPrefixLen).replace(/[-_\s]+$/, "");
|
|
2653
2954
|
return dirPrefix + prefix + suffix;
|
|
2654
2955
|
}
|
|
2956
|
+
async function resolveFixTarget(vault, original, preferred, maxLength) {
|
|
2957
|
+
for (const candidate of candidateRelPaths(preferred, maxLength)) {
|
|
2958
|
+
if (candidate === original || candidate.length > maxLength) continue;
|
|
2959
|
+
const candidatePath = join21(vault, candidate);
|
|
2960
|
+
if (!existsSync4(candidatePath)) return { relPath: candidate, mode: "rename" };
|
|
2961
|
+
if (await hasSameContent(join21(vault, original), candidatePath)) {
|
|
2962
|
+
return { relPath: candidate, mode: "dedupe" };
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
return null;
|
|
2966
|
+
}
|
|
2967
|
+
function candidateRelPaths(preferred, maxLength) {
|
|
2968
|
+
const candidates = [preferred];
|
|
2969
|
+
if (preferred.length > maxLength) return candidates;
|
|
2970
|
+
const dir = posix.dirname(preferred) === "." ? "" : posix.dirname(preferred);
|
|
2971
|
+
const filename = posix.basename(preferred);
|
|
2972
|
+
const ext = filename.endsWith(".md") ? ".md" : "";
|
|
2973
|
+
const base = ext ? filename.slice(0, -3) : filename;
|
|
2974
|
+
const dirPrefix = dir ? `${dir}/` : "";
|
|
2975
|
+
for (let i = 2; i < 100; i++) {
|
|
2976
|
+
const suffix = `-${i}${ext}`;
|
|
2977
|
+
const prefixBudget = maxLength - dirPrefix.length - suffix.length;
|
|
2978
|
+
if (prefixBudget <= 0) break;
|
|
2979
|
+
candidates.push(`${dirPrefix}${base.slice(0, prefixBudget).replace(/[-_\s]+$/, "")}${suffix}`);
|
|
2980
|
+
}
|
|
2981
|
+
return candidates;
|
|
2982
|
+
}
|
|
2983
|
+
async function hasSameContent(a, b) {
|
|
2984
|
+
try {
|
|
2985
|
+
const [left, right] = await Promise.all([readFile16(a), readFile16(b)]);
|
|
2986
|
+
return left.equals(right);
|
|
2987
|
+
} catch {
|
|
2988
|
+
return false;
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
function shouldRewriteReferences(relPath) {
|
|
2992
|
+
if (relPath.startsWith("raw/")) return false;
|
|
2993
|
+
if (relPath.startsWith("_archive/")) return false;
|
|
2994
|
+
return true;
|
|
2995
|
+
}
|
|
2996
|
+
function replacePathReferences(content, oldRelPath, newRelPath) {
|
|
2997
|
+
let updated = content.replaceAll(oldRelPath, newRelPath);
|
|
2998
|
+
const oldStem = posix.basename(oldRelPath).replace(/\.md$/, "");
|
|
2999
|
+
const newStem = posix.basename(newRelPath).replace(/\.md$/, "");
|
|
3000
|
+
if (oldStem !== newStem) {
|
|
3001
|
+
const oldStemEscaped = oldStem.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3002
|
+
const stemWikilinkRe = new RegExp(`\\[\\[${oldStemEscaped}(\\|[^\\]]*)?\\]\\]`, "g");
|
|
3003
|
+
updated = updated.replace(stemWikilinkRe, (_match, alias) => `[[${newStem}${alias ?? ""}]]`);
|
|
3004
|
+
}
|
|
3005
|
+
return updated;
|
|
3006
|
+
}
|
|
2655
3007
|
function computeShortHash(input) {
|
|
2656
3008
|
let hash = 2166136261;
|
|
2657
3009
|
for (let i = 0; i < input.length; i++) {
|
|
@@ -2689,11 +3041,12 @@ function buildCliSurface() {
|
|
|
2689
3041
|
program2.command("claim").option("--project <slug>").option("--slug <slug>").option("--wiki <name>");
|
|
2690
3042
|
program2.command("pagesize").option("--lines <n>").option("--wiki <name>");
|
|
2691
3043
|
program2.command("log-rotate").option("--threshold <n>").option("--apply").option("--wiki <name>");
|
|
3044
|
+
program2.command("log-append").requiredOption("--content <text>").option("--wiki <name>");
|
|
2692
3045
|
program2.command("lint").option("--days <n>").option("--lines <n>").option("--log-threshold <n>").option("--fix").option("--only <bucket>").option("--wiki <name>");
|
|
2693
3046
|
program2.command("config");
|
|
2694
3047
|
program2.command("doctor");
|
|
2695
3048
|
program2.command("status").option("--wiki <name>");
|
|
2696
|
-
program2.command("archive").option("--wiki <name>");
|
|
3049
|
+
program2.command("archive").option("--wiki <name>").option("--cascade").option("--apply");
|
|
2697
3050
|
program2.command("drift").option("--apply").option("--new <date>").option("--wiki <name>");
|
|
2698
3051
|
program2.command("dedup").option("--apply").option("--wiki <name>");
|
|
2699
3052
|
program2.command("migrate-citations").option("--dry-run").option("--wiki <name>");
|
|
@@ -2726,6 +3079,9 @@ function buildCliSurface() {
|
|
|
2726
3079
|
syncCmd2.command("status").option("--wiki <name>");
|
|
2727
3080
|
syncCmd2.command("push").option("--wiki <name>");
|
|
2728
3081
|
syncCmd2.command("pull").option("--wiki <name>");
|
|
3082
|
+
syncCmd2.command("lock").option("--summary <text>").option("--ttl-minutes <n>").option("--force").option("--wiki <name>");
|
|
3083
|
+
syncCmd2.command("unlock").option("--force").option("--wiki <name>");
|
|
3084
|
+
syncCmd2.command("peers").option("--wiki <name>");
|
|
2729
3085
|
const backupCmd2 = program2.commands.find((c) => c.name() === "backup");
|
|
2730
3086
|
backupCmd2.command("sync").option("--dry-run").option("--bucket <name>").option("--endpoint <url>").option("--region <region>").option("--prune").option("--wiki <name>");
|
|
2731
3087
|
backupCmd2.command("restore").option("--bucket <name>").option("--endpoint <url>").option("--region <region>").option("--target <dir>").option("--wiki <name>");
|
|
@@ -2835,8 +3191,16 @@ function extractSourceEntries(rawFm) {
|
|
|
2835
3191
|
}
|
|
2836
3192
|
var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_dedup", "broken_sources", "tag_not_in_taxonomy", "path_too_long"];
|
|
2837
3193
|
var WARNING_ORDER = ["raw_body_duplicate", "raw_subdirectory_duplicate", "file_source_url", "index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "orphans", "compound_refs", "legacy_citation_style", "orphaned_citations", "duplicate_frontmatter", "work_item_health", "orphaned_project_pages", "missing_overview", "missing_diagram"];
|
|
2838
|
-
var INFO_ORDER = ["bridges", "page_structure", "topic_map_recommended", "frontmatter_wikilink", "wikilink_citation", "missing_tldr", "stale_sections", "cli_refs"];
|
|
3194
|
+
var INFO_ORDER = ["bridges", "sparse_community", "page_structure", "topic_map_recommended", "frontmatter_wikilink", "wikilink_citation", "missing_tldr", "stale_sections", "cli_refs"];
|
|
3195
|
+
var KNOWN_BUCKETS = [...ERROR_ORDER, ...WARNING_ORDER, ...INFO_ORDER];
|
|
2839
3196
|
async function runLint(input) {
|
|
3197
|
+
if (input.only && !KNOWN_BUCKETS.includes(input.only)) {
|
|
3198
|
+
return {
|
|
3199
|
+
exitCode: ExitCode.USAGE,
|
|
3200
|
+
result: { ok: false, error: "UNKNOWN_BUCKET", detail: `Unknown bucket "${input.only}". Valid: ${KNOWN_BUCKETS.join(", ")}` }
|
|
3201
|
+
};
|
|
3202
|
+
}
|
|
3203
|
+
const shouldFix = (bucket) => !!input.fix && (!input.only || input.only === bucket);
|
|
2840
3204
|
const buckets = {};
|
|
2841
3205
|
const fixed = [];
|
|
2842
3206
|
const unresolved = [];
|
|
@@ -2878,6 +3242,10 @@ async function runLint(input) {
|
|
|
2878
3242
|
if (orphans.result.data.orphans.length > 0) buckets.orphans = orphans.result.data.orphans;
|
|
2879
3243
|
if (orphans.result.data.bridges.length > 0) buckets.bridges = orphans.result.data.bridges;
|
|
2880
3244
|
}
|
|
3245
|
+
const sparse = await runSparseCommunity({ vault: input.vault });
|
|
3246
|
+
if (sparse.result.ok && sparse.result.data.communities.length > 0) {
|
|
3247
|
+
buckets.sparse_community = sparse.result.data.communities;
|
|
3248
|
+
}
|
|
2881
3249
|
const topicMap = await runTopicMapCheck({ vault: input.vault });
|
|
2882
3250
|
if (topicMap.result.ok && topicMap.result.data.recommended) {
|
|
2883
3251
|
buckets.topic_map_recommended = [{ page_count: topicMap.result.data.page_count, threshold: topicMap.result.data.threshold }];
|
|
@@ -2956,7 +3324,7 @@ async function runLint(input) {
|
|
|
2956
3324
|
let rawPath = entry.replace(/^"/, "").replace(/"$/, "").replace(/^'/, "").replace(/'$/, "");
|
|
2957
3325
|
rawPath = rawPath.replace(/^\^\[/, "").replace(/\]$/, "");
|
|
2958
3326
|
if (!rawPath.startsWith("raw/") && !rawPath.startsWith("_archive/raw/")) continue;
|
|
2959
|
-
if (!
|
|
3327
|
+
if (!existsSync5(join22(input.vault, rawPath)) && !existsSync5(join22(input.vault, rawPath + ".md")) && !rawPath.startsWith("_archive/") && !existsSync5(join22(input.vault, "_archive", rawPath)) && !existsSync5(join22(input.vault, "_archive", rawPath + ".md"))) {
|
|
2960
3328
|
brokenSourceFlags.push(`${page.relPath}: ${rawPath}`);
|
|
2961
3329
|
}
|
|
2962
3330
|
}
|
|
@@ -3047,11 +3415,11 @@ async function runLint(input) {
|
|
|
3047
3415
|
const slugMatch = String(entry).match(/\[\[([^\]]+)\]\]/);
|
|
3048
3416
|
if (!slugMatch) continue;
|
|
3049
3417
|
const slug = slugMatch[1];
|
|
3050
|
-
const knowledgePath =
|
|
3051
|
-
if (!
|
|
3418
|
+
const knowledgePath = join22(input.vault, "projects", slug, "knowledge.md");
|
|
3419
|
+
if (!existsSync5(knowledgePath)) continue;
|
|
3052
3420
|
const pageRef = page.relPath.replace(/\.md$/, "");
|
|
3053
3421
|
try {
|
|
3054
|
-
const knowledgeContent = await
|
|
3422
|
+
const knowledgeContent = await readFile17(knowledgePath, "utf8");
|
|
3055
3423
|
if (!knowledgeContent.includes(`[[${pageRef}]]`)) {
|
|
3056
3424
|
orphanedProjectPages.push(`${page.relPath}: not in projects/${slug}/knowledge.md`);
|
|
3057
3425
|
}
|
|
@@ -3062,7 +3430,7 @@ async function runLint(input) {
|
|
|
3062
3430
|
if (orphanedProjectPages.length > 0) buckets.orphaned_project_pages = orphanedProjectPages;
|
|
3063
3431
|
const cliRefFlags = [];
|
|
3064
3432
|
const cliSurface = buildCliSurface();
|
|
3065
|
-
const allScanPages = [...scan.data.typedKnowledge
|
|
3433
|
+
const allScanPages = [...scan.data.typedKnowledge];
|
|
3066
3434
|
for (const page of allScanPages) {
|
|
3067
3435
|
const text = await readPage(page);
|
|
3068
3436
|
const violations = validateCliRefs(text, page.relPath, cliSurface);
|
|
@@ -3092,13 +3460,13 @@ async function runLint(input) {
|
|
|
3092
3460
|
}
|
|
3093
3461
|
}
|
|
3094
3462
|
if (staleSectionFlags.length > 0) buckets.stale_sections = staleSectionFlags;
|
|
3095
|
-
if (
|
|
3463
|
+
if (shouldFix("legacy_citation_style") && legacyPages.length > 0) {
|
|
3096
3464
|
const FENCE_RE2 = /```[\s\S]*?```/g;
|
|
3097
3465
|
const INLINE_MARKER = /\^\[raw\/[^\]]+\]/g;
|
|
3098
3466
|
for (const relPath of legacyPages) {
|
|
3099
3467
|
try {
|
|
3100
3468
|
const absPath = `${input.vault}/${relPath}`;
|
|
3101
|
-
const raw = await
|
|
3469
|
+
const raw = await readFile17(absPath, "utf8");
|
|
3102
3470
|
const split = splitFrontmatter(raw);
|
|
3103
3471
|
if (!split.ok) {
|
|
3104
3472
|
unresolved.push(relPath);
|
|
@@ -3193,11 +3561,11 @@ ${newBody}`;
|
|
|
3193
3561
|
else delete buckets.legacy_citation_style;
|
|
3194
3562
|
}
|
|
3195
3563
|
}
|
|
3196
|
-
if (
|
|
3564
|
+
if (shouldFix("missing_overview") && noOverview.length > 0) {
|
|
3197
3565
|
for (const relPath of noOverview) {
|
|
3198
3566
|
try {
|
|
3199
3567
|
const absPath = `${input.vault}/${relPath}`;
|
|
3200
|
-
const raw = await
|
|
3568
|
+
const raw = await readFile17(absPath, "utf8");
|
|
3201
3569
|
const split = splitFrontmatter(raw);
|
|
3202
3570
|
if (!split.ok) {
|
|
3203
3571
|
unresolved.push(relPath);
|
|
@@ -3234,11 +3602,11 @@ ${trimmedBody}`;
|
|
|
3234
3602
|
if (remaining.length > 0) buckets.missing_overview = remaining;
|
|
3235
3603
|
else delete buckets.missing_overview;
|
|
3236
3604
|
}
|
|
3237
|
-
if (
|
|
3605
|
+
if (shouldFix("missing_tldr") && missingTldrFlags.length > 0) {
|
|
3238
3606
|
for (const relPath of missingTldrFlags) {
|
|
3239
3607
|
try {
|
|
3240
3608
|
const absPath = `${input.vault}/${relPath}`;
|
|
3241
|
-
const raw = await
|
|
3609
|
+
const raw = await readFile17(absPath, "utf8");
|
|
3242
3610
|
const split = splitFrontmatter(raw);
|
|
3243
3611
|
if (!split.ok) {
|
|
3244
3612
|
unresolved.push(relPath);
|
|
@@ -3281,14 +3649,14 @@ ${lines.join("\n")}`;
|
|
|
3281
3649
|
if (remaining.length > 0) buckets.missing_tldr = remaining;
|
|
3282
3650
|
else delete buckets.missing_tldr;
|
|
3283
3651
|
}
|
|
3284
|
-
if (
|
|
3652
|
+
if (shouldFix("wikilink_citation") && wikilinkCitationFlags.length > 0) {
|
|
3285
3653
|
const WIKILINK_RE = /\[\[raw\/([^\]|]+)(?:\|[^\]]*)?\]\]/g;
|
|
3286
3654
|
const FENCE_RE2 = /```[\s\S]*?```/g;
|
|
3287
3655
|
const wikilinkFixed = [];
|
|
3288
3656
|
for (const relPath of wikilinkCitationFlags) {
|
|
3289
3657
|
try {
|
|
3290
3658
|
const absPath = `${input.vault}/${relPath}`;
|
|
3291
|
-
const raw = await
|
|
3659
|
+
const raw = await readFile17(absPath, "utf8");
|
|
3292
3660
|
const split = splitFrontmatter(raw);
|
|
3293
3661
|
if (!split.ok) {
|
|
3294
3662
|
unresolved.push(relPath);
|
|
@@ -3370,12 +3738,12 @@ ${newBody}`;
|
|
|
3370
3738
|
else delete buckets.wikilink_citation;
|
|
3371
3739
|
}
|
|
3372
3740
|
}
|
|
3373
|
-
if (
|
|
3741
|
+
if (shouldFix("file_source_url") && fileSourceUrlFlags.length > 0) {
|
|
3374
3742
|
const FILE_FIXED = [];
|
|
3375
3743
|
for (const relPath of fileSourceUrlFlags) {
|
|
3376
3744
|
try {
|
|
3377
3745
|
const absPath = `${input.vault}/${relPath}`;
|
|
3378
|
-
const raw = await
|
|
3746
|
+
const raw = await readFile17(absPath, "utf8");
|
|
3379
3747
|
const parts = raw.split("---", 3);
|
|
3380
3748
|
if (parts.length < 3) {
|
|
3381
3749
|
unresolved.push(relPath);
|
|
@@ -3410,36 +3778,11 @@ ${newBody}`;
|
|
|
3410
3778
|
}
|
|
3411
3779
|
}
|
|
3412
3780
|
const pathViolations = buckets.path_too_long;
|
|
3413
|
-
if (
|
|
3414
|
-
const
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
const newRelPath = truncateFilename(v.relPath);
|
|
3419
|
-
const newAbsPath = `${input.vault}/${newRelPath}`;
|
|
3420
|
-
await rename5(absPath, newAbsPath);
|
|
3421
|
-
const oldPathEscaped = v.relPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3422
|
-
for (const page of allPages) {
|
|
3423
|
-
if (page.relPath === v.relPath) continue;
|
|
3424
|
-
const content = await readFile15(page.absPath, "utf8");
|
|
3425
|
-
if (!content.includes(v.relPath)) continue;
|
|
3426
|
-
let updated = content;
|
|
3427
|
-
const citationRe = new RegExp(`\\^\\[${oldPathEscaped}\\]`, "g");
|
|
3428
|
-
updated = updated.replace(citationRe, `^[${newRelPath}]`);
|
|
3429
|
-
const wikilinkRe = new RegExp(`\\[\\[${oldPathEscaped}(\\|[^\\]]*)?\\]\\]`, "g");
|
|
3430
|
-
updated = updated.replace(wikilinkRe, (_m, alias) => `[[${newRelPath}${alias ?? ""}]]`);
|
|
3431
|
-
if (updated !== content) {
|
|
3432
|
-
const w = await safeWritePage(page.absPath, updated);
|
|
3433
|
-
if (!w.ok) {
|
|
3434
|
-
unresolved.push(`${page.relPath} (rewire)`);
|
|
3435
|
-
}
|
|
3436
|
-
}
|
|
3437
|
-
}
|
|
3438
|
-
pathFixed.push(v.relPath);
|
|
3439
|
-
} catch {
|
|
3440
|
-
unresolved.push(v.relPath);
|
|
3441
|
-
}
|
|
3442
|
-
}
|
|
3781
|
+
if (shouldFix("path_too_long") && pathViolations && pathViolations.length > 0) {
|
|
3782
|
+
const pathFix = await fixPathTooLong({ vault: input.vault });
|
|
3783
|
+
const pathFixed = pathFix.result.ok ? pathFix.result.data.fixed.map((f) => f.from) : [];
|
|
3784
|
+
if (pathFix.result.ok) unresolved.push(...pathFix.result.data.unresolved);
|
|
3785
|
+
else unresolved.push(...pathViolations.map((v) => v.relPath));
|
|
3443
3786
|
fixed.push(...pathFixed);
|
|
3444
3787
|
if (pathFixed.length > 0) {
|
|
3445
3788
|
const fixedSet = new Set(pathFixed);
|
|
@@ -3453,13 +3796,6 @@ ${newBody}`;
|
|
|
3453
3796
|
const warningOut = WARNING_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
3454
3797
|
const infoOut = INFO_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
3455
3798
|
if (input.only) {
|
|
3456
|
-
const allKnown = [...ERROR_ORDER, ...WARNING_ORDER, ...INFO_ORDER];
|
|
3457
|
-
if (!allKnown.includes(input.only)) {
|
|
3458
|
-
return {
|
|
3459
|
-
exitCode: ExitCode.USAGE,
|
|
3460
|
-
result: { ok: false, error: "UNKNOWN_BUCKET", detail: `Unknown bucket "${input.only}". Valid: ${allKnown.join(", ")}` }
|
|
3461
|
-
};
|
|
3462
|
-
}
|
|
3463
3799
|
const match = [...errorOut, ...warningOut, ...infoOut].filter((b) => b.kind === input.only);
|
|
3464
3800
|
const severity = ERROR_ORDER.includes(input.only) ? "error" : WARNING_ORDER.includes(input.only) ? "warning" : "info";
|
|
3465
3801
|
const filtered = severity === "error" ? { error: match, warning: [], info: [] } : severity === "warning" ? { error: [], warning: match, info: [] } : { error: [], warning: [], info: match };
|
|
@@ -3523,14 +3859,14 @@ ${match.length === 0 ? "0 violations" : match.map((b) => ` ${b.kind}: ${b.items
|
|
|
3523
3859
|
}
|
|
3524
3860
|
|
|
3525
3861
|
// src/commands/config.ts
|
|
3526
|
-
import { readFile as
|
|
3527
|
-
import { existsSync as
|
|
3528
|
-
import { join as
|
|
3862
|
+
import { readFile as readFile18 } from "fs/promises";
|
|
3863
|
+
import { existsSync as existsSync6 } from "fs";
|
|
3864
|
+
import { join as join23 } from "path";
|
|
3529
3865
|
function validateKey(key) {
|
|
3530
3866
|
return CONFIG_KEYS.includes(key) || isValidWikiProfileKey(key);
|
|
3531
3867
|
}
|
|
3532
3868
|
function configPath(home) {
|
|
3533
|
-
return
|
|
3869
|
+
return join23(home, ".skillwiki", ".env");
|
|
3534
3870
|
}
|
|
3535
3871
|
async function runConfigGet(input) {
|
|
3536
3872
|
if (!validateKey(input.key)) {
|
|
@@ -3548,7 +3884,7 @@ async function runConfigSet(input) {
|
|
|
3548
3884
|
try {
|
|
3549
3885
|
let originalContent;
|
|
3550
3886
|
try {
|
|
3551
|
-
originalContent = await
|
|
3887
|
+
originalContent = await readFile18(filePath, "utf8");
|
|
3552
3888
|
} catch {
|
|
3553
3889
|
}
|
|
3554
3890
|
const existing = originalContent !== void 0 ? parseDotenvText(originalContent) : {};
|
|
@@ -3580,18 +3916,18 @@ async function runConfigList(input) {
|
|
|
3580
3916
|
}
|
|
3581
3917
|
async function runConfigPath(input) {
|
|
3582
3918
|
const filePath = configPath(input.home);
|
|
3583
|
-
return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists:
|
|
3919
|
+
return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync6(filePath), humanHint: filePath }) };
|
|
3584
3920
|
}
|
|
3585
3921
|
|
|
3586
3922
|
// src/commands/doctor.ts
|
|
3587
|
-
import { existsSync as
|
|
3588
|
-
import { join as
|
|
3923
|
+
import { existsSync as existsSync9, lstatSync, readlinkSync, readdirSync, statSync as statSync3, readFileSync as readFileSync7 } from "fs";
|
|
3924
|
+
import { join as join27, resolve as resolve5 } from "path";
|
|
3589
3925
|
import { execSync as execSync2 } from "child_process";
|
|
3590
3926
|
import { platform as platform2 } from "os";
|
|
3591
3927
|
|
|
3592
3928
|
// src/utils/auto-update.ts
|
|
3593
|
-
import { readFileSync as readFileSync4, writeFileSync as
|
|
3594
|
-
import { join as
|
|
3929
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync7, mkdirSync as mkdirSync3 } from "fs";
|
|
3930
|
+
import { join as join24, dirname as dirname9 } from "path";
|
|
3595
3931
|
import { spawn } from "child_process";
|
|
3596
3932
|
|
|
3597
3933
|
// src/utils/update-consts.ts
|
|
@@ -3602,7 +3938,7 @@ var CLI_DISABLE_FLAG = "--no-update-notifier";
|
|
|
3602
3938
|
|
|
3603
3939
|
// src/utils/auto-update.ts
|
|
3604
3940
|
function cachePath(home) {
|
|
3605
|
-
return
|
|
3941
|
+
return join24(home, ".skillwiki", CACHE_FILENAME);
|
|
3606
3942
|
}
|
|
3607
3943
|
function readCacheRaw(home) {
|
|
3608
3944
|
try {
|
|
@@ -3621,8 +3957,8 @@ function readCache(home) {
|
|
|
3621
3957
|
}
|
|
3622
3958
|
function writeCache(home, cache) {
|
|
3623
3959
|
const p = cachePath(home);
|
|
3624
|
-
|
|
3625
|
-
|
|
3960
|
+
mkdirSync3(dirname9(p), { recursive: true });
|
|
3961
|
+
writeFileSync4(p, JSON.stringify(cache, null, 2));
|
|
3626
3962
|
}
|
|
3627
3963
|
function latestFromCache(home, currentVersion) {
|
|
3628
3964
|
const { cache } = readCache(home);
|
|
@@ -3640,7 +3976,7 @@ function triggerAutoUpdate(home, currentVersion) {
|
|
|
3640
3976
|
const { isStale: isStale2 } = readCache(home);
|
|
3641
3977
|
if (!isStale2) return;
|
|
3642
3978
|
const bgScript = new URL("../auto-update-bg.js", import.meta.url).pathname;
|
|
3643
|
-
if (!
|
|
3979
|
+
if (!existsSync7(bgScript)) return;
|
|
3644
3980
|
const child = spawn(process.execPath, [bgScript, home, currentVersion], {
|
|
3645
3981
|
detached: true,
|
|
3646
3982
|
stdio: "ignore"
|
|
@@ -3652,12 +3988,12 @@ function triggerAutoUpdate(home, currentVersion) {
|
|
|
3652
3988
|
|
|
3653
3989
|
// src/utils/plugin-registry.ts
|
|
3654
3990
|
import { readFileSync as readFileSync5 } from "fs";
|
|
3655
|
-
import { join as
|
|
3656
|
-
var REGISTRY_PATH =
|
|
3991
|
+
import { join as join25 } from "path";
|
|
3992
|
+
var REGISTRY_PATH = join25(".claude", "plugins", "installed_plugins.json");
|
|
3657
3993
|
var PLUGIN_KEY = "skillwiki@llm-wiki";
|
|
3658
3994
|
function readInstalledPlugins(home) {
|
|
3659
3995
|
try {
|
|
3660
|
-
const raw = readFileSync5(
|
|
3996
|
+
const raw = readFileSync5(join25(home, REGISTRY_PATH), "utf8");
|
|
3661
3997
|
return JSON.parse(raw);
|
|
3662
3998
|
} catch {
|
|
3663
3999
|
return null;
|
|
@@ -3674,8 +4010,8 @@ function findPlugin(home, key = PLUGIN_KEY) {
|
|
|
3674
4010
|
// src/utils/s3-mount-health.ts
|
|
3675
4011
|
import { execSync } from "child_process";
|
|
3676
4012
|
import { platform } from "os";
|
|
3677
|
-
import { readFileSync as readFileSync6, writeFileSync as
|
|
3678
|
-
import { join as
|
|
4013
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, unlinkSync as unlinkSync4, readFileSync as readFile19 } from "fs";
|
|
4014
|
+
import { join as join26 } from "path";
|
|
3679
4015
|
var OS = platform();
|
|
3680
4016
|
function findRcloneMountPid() {
|
|
3681
4017
|
try {
|
|
@@ -3833,39 +4169,70 @@ function detectFuseMount(vaultPath) {
|
|
|
3833
4169
|
return null;
|
|
3834
4170
|
}
|
|
3835
4171
|
function writeTest(dir) {
|
|
3836
|
-
const testFile =
|
|
4172
|
+
const testFile = join26(dir, `.doctor-write-test-${process.pid}.tmp`);
|
|
3837
4173
|
const payload = `skillwiki doctor write test \u2014 ${Date.now()} \u2014 ${Math.random().toString(36).slice(2)}`;
|
|
3838
4174
|
const start = Date.now();
|
|
3839
4175
|
try {
|
|
3840
|
-
|
|
4176
|
+
writeFileSync5(testFile, payload, "utf8");
|
|
3841
4177
|
} catch (e) {
|
|
3842
4178
|
return { success: false, writeMs: Date.now() - start, readMs: 0, size: 0, error: `write failed: ${e.message}` };
|
|
3843
4179
|
}
|
|
3844
4180
|
const writeMs = Date.now() - start;
|
|
3845
4181
|
const readStart = Date.now();
|
|
3846
4182
|
try {
|
|
3847
|
-
const back =
|
|
4183
|
+
const back = readFile19(testFile, "utf8");
|
|
3848
4184
|
const readMs = Date.now() - readStart;
|
|
3849
4185
|
if (back !== payload) {
|
|
3850
4186
|
try {
|
|
3851
|
-
|
|
4187
|
+
unlinkSync4(testFile);
|
|
3852
4188
|
} catch {
|
|
3853
4189
|
}
|
|
3854
4190
|
return { success: false, writeMs, readMs, size: Buffer.byteLength(payload, "utf8"), error: "content mismatch \u2014 wrote and read-back differ" };
|
|
3855
4191
|
}
|
|
3856
4192
|
} catch (e) {
|
|
3857
4193
|
try {
|
|
3858
|
-
|
|
4194
|
+
unlinkSync4(testFile);
|
|
3859
4195
|
} catch {
|
|
3860
4196
|
}
|
|
3861
4197
|
return { success: false, writeMs, readMs: Date.now() - readStart, size: 0, error: `read failed: ${e.message}` };
|
|
3862
4198
|
}
|
|
3863
4199
|
try {
|
|
3864
|
-
|
|
4200
|
+
unlinkSync4(testFile);
|
|
3865
4201
|
} catch {
|
|
3866
4202
|
}
|
|
3867
4203
|
return { success: true, writeMs, readMs: Date.now() - readStart, size: Buffer.byteLength(payload, "utf8") };
|
|
3868
4204
|
}
|
|
4205
|
+
var DURATION_UNIT_SECONDS = {
|
|
4206
|
+
ms: 1 / 1e3,
|
|
4207
|
+
s: 1,
|
|
4208
|
+
m: 60,
|
|
4209
|
+
h: 3600,
|
|
4210
|
+
d: 86400,
|
|
4211
|
+
w: 604800
|
|
4212
|
+
};
|
|
4213
|
+
function parseDurationSeconds(raw) {
|
|
4214
|
+
const input = raw.trim().toLowerCase();
|
|
4215
|
+
if (!input) return null;
|
|
4216
|
+
if (/^\d+(?:\.\d+)?$/.test(input)) {
|
|
4217
|
+
const num = parseFloat(input);
|
|
4218
|
+
return Number.isFinite(num) ? num : null;
|
|
4219
|
+
}
|
|
4220
|
+
const re = /(\d+(?:\.\d+)?)(ms|s|m|h|d|w)/g;
|
|
4221
|
+
let total = 0;
|
|
4222
|
+
let consumed = 0;
|
|
4223
|
+
for (const match of input.matchAll(re)) {
|
|
4224
|
+
const full = match[0];
|
|
4225
|
+
const value = parseFloat(match[1]);
|
|
4226
|
+
const unit = match[2];
|
|
4227
|
+
if (!Number.isFinite(value)) return null;
|
|
4228
|
+
const factor = DURATION_UNIT_SECONDS[unit];
|
|
4229
|
+
if (factor === void 0) return null;
|
|
4230
|
+
total += value * factor;
|
|
4231
|
+
consumed += full.length;
|
|
4232
|
+
}
|
|
4233
|
+
if (consumed !== input.length) return null;
|
|
4234
|
+
return total;
|
|
4235
|
+
}
|
|
3869
4236
|
var FLAG_THRESHOLDS = {
|
|
3870
4237
|
"--vfs-write-back": { min: 15, unit: "s", label: "VFS write-back window" },
|
|
3871
4238
|
"--vfs-write-wait": { min: 10, unit: "s", label: "VFS write-wait" },
|
|
@@ -3887,14 +4254,14 @@ function checkNodeVersion() {
|
|
|
3887
4254
|
function detectCliChannels(argv, home) {
|
|
3888
4255
|
const channels = [];
|
|
3889
4256
|
if (argv.length >= 2 && argv[1].endsWith("cli.js")) {
|
|
3890
|
-
const devPath =
|
|
4257
|
+
const devPath = resolve5(argv[1]);
|
|
3891
4258
|
channels.push({ name: "dev", path: devPath, isDevLink: true });
|
|
3892
4259
|
}
|
|
3893
4260
|
try {
|
|
3894
4261
|
const whichOut = execSync2("which skillwiki 2>/dev/null", { encoding: "utf8" }).trim();
|
|
3895
4262
|
if (whichOut) {
|
|
3896
4263
|
const isDev = isDevSymlink(whichOut);
|
|
3897
|
-
if (!channels.some((c) => c.path ===
|
|
4264
|
+
if (!channels.some((c) => c.path === resolve5(whichOut))) {
|
|
3898
4265
|
channels.push({ name: "npm", path: whichOut, isDevLink: isDev });
|
|
3899
4266
|
}
|
|
3900
4267
|
}
|
|
@@ -3902,13 +4269,13 @@ function detectCliChannels(argv, home) {
|
|
|
3902
4269
|
}
|
|
3903
4270
|
const plugin = findPlugin(home);
|
|
3904
4271
|
if (plugin) {
|
|
3905
|
-
const pluginBin =
|
|
3906
|
-
if (
|
|
4272
|
+
const pluginBin = join27(plugin.installPath, "bin", "skillwiki");
|
|
4273
|
+
if (existsSync9(pluginBin)) {
|
|
3907
4274
|
channels.push({ name: "plugin", path: pluginBin, isDevLink: false });
|
|
3908
4275
|
}
|
|
3909
4276
|
}
|
|
3910
|
-
const installBin =
|
|
3911
|
-
if (
|
|
4277
|
+
const installBin = join27(home, ".claude", "skills", "bin", "skillwiki");
|
|
4278
|
+
if (existsSync9(installBin)) {
|
|
3912
4279
|
channels.push({ name: "install", path: installBin, isDevLink: false });
|
|
3913
4280
|
}
|
|
3914
4281
|
return channels;
|
|
@@ -3917,7 +4284,7 @@ function isDevSymlink(binPath) {
|
|
|
3917
4284
|
try {
|
|
3918
4285
|
const st = lstatSync(binPath);
|
|
3919
4286
|
if (st.isSymbolicLink()) {
|
|
3920
|
-
const target =
|
|
4287
|
+
const target = resolve5(binPath, "..", readlinkSync(binPath));
|
|
3921
4288
|
return target.includes("packages/cli") || target.includes("packages\\cli");
|
|
3922
4289
|
}
|
|
3923
4290
|
} catch {
|
|
@@ -3960,7 +4327,7 @@ function checkCliChannels(argv, home) {
|
|
|
3960
4327
|
}
|
|
3961
4328
|
async function checkConfigFile(home) {
|
|
3962
4329
|
const cfgPath = configPath(home);
|
|
3963
|
-
if (!
|
|
4330
|
+
if (!existsSync9(cfgPath)) {
|
|
3964
4331
|
return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
|
|
3965
4332
|
}
|
|
3966
4333
|
try {
|
|
@@ -3975,7 +4342,7 @@ function checkWikiPathExists(resolvedPath) {
|
|
|
3975
4342
|
if (resolvedPath === void 0) {
|
|
3976
4343
|
return check("error", "wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
3977
4344
|
}
|
|
3978
|
-
if (
|
|
4345
|
+
if (existsSync9(resolvedPath) && statSync3(resolvedPath).isDirectory()) {
|
|
3979
4346
|
return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
|
|
3980
4347
|
}
|
|
3981
4348
|
return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
|
|
@@ -3984,13 +4351,13 @@ function checkVaultStructure(resolvedPath) {
|
|
|
3984
4351
|
if (resolvedPath === void 0) {
|
|
3985
4352
|
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
3986
4353
|
}
|
|
3987
|
-
if (!
|
|
4354
|
+
if (!existsSync9(resolvedPath)) {
|
|
3988
4355
|
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
|
|
3989
4356
|
}
|
|
3990
4357
|
const missing = [];
|
|
3991
|
-
if (!
|
|
4358
|
+
if (!existsSync9(join27(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
|
|
3992
4359
|
for (const dir of ["raw", "entities", "concepts", "meta"]) {
|
|
3993
|
-
if (!
|
|
4360
|
+
if (!existsSync9(join27(resolvedPath, dir))) missing.push(dir + "/");
|
|
3994
4361
|
}
|
|
3995
4362
|
if (missing.length === 0) {
|
|
3996
4363
|
return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
|
|
@@ -3998,23 +4365,23 @@ function checkVaultStructure(resolvedPath) {
|
|
|
3998
4365
|
return check("warn", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to add CodeWiki structure`);
|
|
3999
4366
|
}
|
|
4000
4367
|
function checkSkillsInstalled(home, cwd) {
|
|
4001
|
-
const srcDir = cwd ?
|
|
4002
|
-
if (srcDir &&
|
|
4003
|
-
const found =
|
|
4368
|
+
const srcDir = cwd ? join27(cwd, "packages", "skills") : void 0;
|
|
4369
|
+
if (srcDir && existsSync9(srcDir)) {
|
|
4370
|
+
const found = findInstalledSkillMd(srcDir);
|
|
4004
4371
|
if (found.length > 0) {
|
|
4005
4372
|
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (source)`);
|
|
4006
4373
|
}
|
|
4007
4374
|
}
|
|
4008
4375
|
const plugin = findPlugin(home);
|
|
4009
4376
|
if (plugin) {
|
|
4010
|
-
const found =
|
|
4377
|
+
const found = findInstalledSkillMd(plugin.installPath);
|
|
4011
4378
|
if (found.length > 0) {
|
|
4012
4379
|
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (plugin v${plugin.version})`);
|
|
4013
4380
|
}
|
|
4014
4381
|
}
|
|
4015
|
-
const skillsDir =
|
|
4016
|
-
if (
|
|
4017
|
-
const found =
|
|
4382
|
+
const skillsDir = join27(home, ".claude", "skills");
|
|
4383
|
+
if (existsSync9(skillsDir)) {
|
|
4384
|
+
const found = findInstalledSkillMd(skillsDir);
|
|
4018
4385
|
if (found.length > 0) {
|
|
4019
4386
|
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (CLI install)`);
|
|
4020
4387
|
}
|
|
@@ -4023,10 +4390,10 @@ function checkSkillsInstalled(home, cwd) {
|
|
|
4023
4390
|
}
|
|
4024
4391
|
function checkDuplicateSkills(home) {
|
|
4025
4392
|
const plugin = findPlugin(home);
|
|
4026
|
-
const skillsDir =
|
|
4393
|
+
const skillsDir = join27(home, ".claude", "skills");
|
|
4027
4394
|
const agentSkillDirs = [
|
|
4028
|
-
{ label: "~/.codex/skills/", path:
|
|
4029
|
-
{ label: "~/.agents/skills/", path:
|
|
4395
|
+
{ label: "~/.codex/skills/", path: join27(home, ".codex", "skills") },
|
|
4396
|
+
{ label: "~/.agents/skills/", path: join27(home, ".agents", "skills") }
|
|
4030
4397
|
];
|
|
4031
4398
|
if (!plugin) {
|
|
4032
4399
|
return check("pass", "skills_duplicate", "Skills not duplicated", "Single install channel");
|
|
@@ -4103,8 +4470,8 @@ async function checkProfiles(home) {
|
|
|
4103
4470
|
}
|
|
4104
4471
|
async function checkProjectLocalOverride(cwd) {
|
|
4105
4472
|
const dir = cwd ?? process.cwd();
|
|
4106
|
-
const envPath =
|
|
4107
|
-
if (
|
|
4473
|
+
const envPath = join27(dir, ".skillwiki", ".env");
|
|
4474
|
+
if (existsSync9(envPath)) {
|
|
4108
4475
|
return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
|
|
4109
4476
|
}
|
|
4110
4477
|
return check("pass", "project_local", "Project-local config", "None");
|
|
@@ -4113,7 +4480,7 @@ function checkVaultGitRemote(resolvedPath) {
|
|
|
4113
4480
|
if (resolvedPath === void 0) {
|
|
4114
4481
|
return check("error", "vault_git_remote", "Vault git remote", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
4115
4482
|
}
|
|
4116
|
-
if (!
|
|
4483
|
+
if (!existsSync9(join27(resolvedPath, ".git"))) {
|
|
4117
4484
|
return check("warn", "vault_git_remote", "Vault git remote", "Vault is not a git repository \u2014 sync features unavailable");
|
|
4118
4485
|
}
|
|
4119
4486
|
try {
|
|
@@ -4136,9 +4503,9 @@ function checkObsidianTemplates(resolvedPath) {
|
|
|
4136
4503
|
return check("error", "obsidian_templates", "Obsidian templates", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
4137
4504
|
}
|
|
4138
4505
|
const missing = [];
|
|
4139
|
-
if (!
|
|
4140
|
-
if (!
|
|
4141
|
-
if (!
|
|
4506
|
+
if (!existsSync9(join27(resolvedPath, "_Templates"))) missing.push("_Templates/");
|
|
4507
|
+
if (!existsSync9(join27(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
|
|
4508
|
+
if (!existsSync9(join27(resolvedPath, ".obsidian", "app.json"))) missing.push(".obsidian/app.json");
|
|
4142
4509
|
if (missing.length === 0) {
|
|
4143
4510
|
return check("pass", "obsidian_templates", "Obsidian templates", "Template folder and config present");
|
|
4144
4511
|
}
|
|
@@ -4148,8 +4515,8 @@ function checkDotStoreClean(resolvedPath) {
|
|
|
4148
4515
|
if (resolvedPath === void 0) {
|
|
4149
4516
|
return check("error", "dsstore_clean", "No .DS_Store in raw/", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
4150
4517
|
}
|
|
4151
|
-
const rawDir =
|
|
4152
|
-
if (!
|
|
4518
|
+
const rawDir = join27(resolvedPath, "raw");
|
|
4519
|
+
if (!existsSync9(rawDir)) {
|
|
4153
4520
|
return check("pass", "dsstore_clean", "No .DS_Store in raw/", "raw/ directory not found \u2014 check skipped");
|
|
4154
4521
|
}
|
|
4155
4522
|
const found = [];
|
|
@@ -4164,7 +4531,7 @@ function checkDotStoreClean(resolvedPath) {
|
|
|
4164
4531
|
if (entry.name === ".DS_Store") {
|
|
4165
4532
|
found.push(rel ? `${rel}/.DS_Store` : ".DS_Store");
|
|
4166
4533
|
} else if (entry.isDirectory()) {
|
|
4167
|
-
walk2(
|
|
4534
|
+
walk2(join27(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
|
|
4168
4535
|
}
|
|
4169
4536
|
}
|
|
4170
4537
|
})(rawDir, "");
|
|
@@ -4177,7 +4544,7 @@ function checkSyncLastPush(resolvedPath) {
|
|
|
4177
4544
|
if (resolvedPath === void 0) {
|
|
4178
4545
|
return check("error", "sync_last_push", "Vault sync recency", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
4179
4546
|
}
|
|
4180
|
-
if (!
|
|
4547
|
+
if (!existsSync9(join27(resolvedPath, ".git"))) {
|
|
4181
4548
|
return check("pass", "sync_last_push", "Vault sync recency", "No git repo \u2014 sync check skipped");
|
|
4182
4549
|
}
|
|
4183
4550
|
let timestamp;
|
|
@@ -4218,8 +4585,8 @@ function checkS3MountPerf(resolvedPath) {
|
|
|
4218
4585
|
return check("pass", "s3_mount_perf", "S3 mount performance", "local disk");
|
|
4219
4586
|
}
|
|
4220
4587
|
const mountPoint = fuse.mountPoint;
|
|
4221
|
-
const conceptsDir =
|
|
4222
|
-
if (!
|
|
4588
|
+
const conceptsDir = join27(resolvedPath, "concepts");
|
|
4589
|
+
if (!existsSync9(conceptsDir)) {
|
|
4223
4590
|
return check("pass", "s3_mount_perf", "S3 mount performance", `S3 FUSE mount (${mountPoint}), no concepts/ to benchmark`);
|
|
4224
4591
|
}
|
|
4225
4592
|
const start = Date.now();
|
|
@@ -4258,6 +4625,73 @@ function checkS3MountPerf(resolvedPath) {
|
|
|
4258
4625
|
`S3 FUSE mount, cache warm (rg scan: ${elapsed.toFixed(3)}s)`
|
|
4259
4626
|
);
|
|
4260
4627
|
}
|
|
4628
|
+
var MAX_DIR_CACHE_TIME_SECONDS = 15 * 60;
|
|
4629
|
+
function formatDurationForHumans(seconds) {
|
|
4630
|
+
if (!Number.isFinite(seconds)) return `${seconds}s`;
|
|
4631
|
+
if (seconds >= 3600) return `${(seconds / 3600).toFixed(1)}h`;
|
|
4632
|
+
if (seconds >= 60) return `${(seconds / 60).toFixed(1)}m`;
|
|
4633
|
+
if (seconds >= 1) return `${seconds.toFixed(1)}s`;
|
|
4634
|
+
return `${Math.round(seconds * 1e3)}ms`;
|
|
4635
|
+
}
|
|
4636
|
+
function checkS3MountFreshness(resolvedPath) {
|
|
4637
|
+
if (!resolvedPath) {
|
|
4638
|
+
return check("pass", "s3_mount_freshness", "S3 visibility freshness", "No vault path \u2014 check skipped");
|
|
4639
|
+
}
|
|
4640
|
+
const fuse = detectFuseMount(resolvedPath);
|
|
4641
|
+
if (!fuse) {
|
|
4642
|
+
return check("pass", "s3_mount_freshness", "S3 visibility freshness", "local disk \u2014 check skipped");
|
|
4643
|
+
}
|
|
4644
|
+
const pid = findRcloneMountPid();
|
|
4645
|
+
if (pid === null) {
|
|
4646
|
+
return check(
|
|
4647
|
+
"warn",
|
|
4648
|
+
"s3_mount_freshness",
|
|
4649
|
+
"S3 visibility freshness",
|
|
4650
|
+
`S3 FUSE mount (${fuse.mountPoint}) but no rclone process found \u2014 cannot audit --dir-cache-time`
|
|
4651
|
+
);
|
|
4652
|
+
}
|
|
4653
|
+
const flags = parseRcloneFlags(pid);
|
|
4654
|
+
if (flags.size === 0) {
|
|
4655
|
+
return check(
|
|
4656
|
+
"warn",
|
|
4657
|
+
"s3_mount_freshness",
|
|
4658
|
+
"S3 visibility freshness",
|
|
4659
|
+
`rclone PID ${pid} found but could not parse flags`
|
|
4660
|
+
);
|
|
4661
|
+
}
|
|
4662
|
+
const raw = flags.get("--dir-cache-time");
|
|
4663
|
+
if (!raw) {
|
|
4664
|
+
return check(
|
|
4665
|
+
"pass",
|
|
4666
|
+
"s3_mount_freshness",
|
|
4667
|
+
"S3 visibility freshness",
|
|
4668
|
+
"PID " + pid + ": --dir-cache-time not set (rclone default 5m, within <=15m SLA)"
|
|
4669
|
+
);
|
|
4670
|
+
}
|
|
4671
|
+
const seconds = parseDurationSeconds(raw);
|
|
4672
|
+
if (seconds === null) {
|
|
4673
|
+
return check(
|
|
4674
|
+
"warn",
|
|
4675
|
+
"s3_mount_freshness",
|
|
4676
|
+
"S3 visibility freshness",
|
|
4677
|
+
`PID ${pid}: could not parse --dir-cache-time=${raw}`
|
|
4678
|
+
);
|
|
4679
|
+
}
|
|
4680
|
+
if (seconds > MAX_DIR_CACHE_TIME_SECONDS) {
|
|
4681
|
+
return check(
|
|
4682
|
+
"warn",
|
|
4683
|
+
"s3_mount_freshness",
|
|
4684
|
+
"S3 visibility freshness",
|
|
4685
|
+
`PID ${pid}: --dir-cache-time=${raw} (${formatDurationForHumans(seconds)}) exceeds 15m SLA \u2014 external S3 changes may remain invisible`
|
|
4686
|
+
);
|
|
4687
|
+
}
|
|
4688
|
+
return check(
|
|
4689
|
+
"pass",
|
|
4690
|
+
"s3_mount_freshness",
|
|
4691
|
+
"S3 visibility freshness",
|
|
4692
|
+
`PID ${pid}: --dir-cache-time=${raw} (${formatDurationForHumans(seconds)}), within <=15m SLA`
|
|
4693
|
+
);
|
|
4694
|
+
}
|
|
4261
4695
|
function checkRcloneFlagAudit(resolvedPath) {
|
|
4262
4696
|
if (!resolvedPath) {
|
|
4263
4697
|
return check("pass", "rclone_flags", "rclone VFS flags", "No vault path \u2014 check skipped");
|
|
@@ -4281,11 +4715,8 @@ function checkRcloneFlagAudit(resolvedPath) {
|
|
|
4281
4715
|
warnings.push(`${flag} not set (default may be unsafe)`);
|
|
4282
4716
|
continue;
|
|
4283
4717
|
}
|
|
4284
|
-
const
|
|
4285
|
-
if (
|
|
4286
|
-
let inSeconds = value;
|
|
4287
|
-
if (raw.endsWith("h")) inSeconds = value * 3600;
|
|
4288
|
-
else if (raw.endsWith("m")) inSeconds = value * 60;
|
|
4718
|
+
const inSeconds = parseDurationSeconds(raw);
|
|
4719
|
+
if (inSeconds === null) continue;
|
|
4289
4720
|
const thresholdSec = threshold.unit === "h" ? threshold.min * 3600 : threshold.unit === "m" ? threshold.min * 60 : threshold.min;
|
|
4290
4721
|
if (inSeconds < thresholdSec) {
|
|
4291
4722
|
warnings.push(`${flag}=${raw} (recommended \u2265${threshold.min}${threshold.unit})`);
|
|
@@ -4337,8 +4768,8 @@ function checkWriteTest(resolvedPath) {
|
|
|
4337
4768
|
if (!fuse) {
|
|
4338
4769
|
return check("pass", "s3_write_test", "S3 write test", "local disk \u2014 check skipped");
|
|
4339
4770
|
}
|
|
4340
|
-
const conceptsDir =
|
|
4341
|
-
if (!
|
|
4771
|
+
const conceptsDir = join27(resolvedPath, "concepts");
|
|
4772
|
+
if (!existsSync9(conceptsDir)) {
|
|
4342
4773
|
return check("pass", "s3_write_test", "S3 write test", "no concepts/ dir to test \u2014 check skipped");
|
|
4343
4774
|
}
|
|
4344
4775
|
const result = writeTest(conceptsDir);
|
|
@@ -4424,7 +4855,7 @@ function checkVfsCacheHealth(resolvedPath) {
|
|
|
4424
4855
|
}
|
|
4425
4856
|
function readVaultSyncConfig(home) {
|
|
4426
4857
|
try {
|
|
4427
|
-
const content = readFileSync7(
|
|
4858
|
+
const content = readFileSync7(join27(home, ".skillwiki", ".env"), "utf8");
|
|
4428
4859
|
let installed = false;
|
|
4429
4860
|
let role;
|
|
4430
4861
|
for (const line of content.split(/\r?\n/)) {
|
|
@@ -4458,12 +4889,12 @@ function vaultSyncChecks(input) {
|
|
|
4458
4889
|
];
|
|
4459
4890
|
}
|
|
4460
4891
|
const isMac = os === "darwin";
|
|
4461
|
-
const logDir = input.logDir ?? (isMac ?
|
|
4462
|
-
const shareDir = input.shareDir ?? (isMac ?
|
|
4463
|
-
const filterPath = input.filterPath ??
|
|
4892
|
+
const logDir = input.logDir ?? (isMac ? join27(home, "Library", "Logs") : join27(home, ".local", "state", "vault-sync", "log"));
|
|
4893
|
+
const shareDir = input.shareDir ?? (isMac ? join27(home, "Library", "Application Support", "vault-sync", "bin") : join27(home, ".local", "share", "vault-sync", "bin"));
|
|
4894
|
+
const filterPath = input.filterPath ?? join27(home, ".config", "rclone", "wiki-push-filters.txt");
|
|
4464
4895
|
const snapshotPath = input.snapshotScriptPath ?? "/root/.hermes/scripts/wiki-snapshot-v3.sh";
|
|
4465
|
-
const pushScriptPath =
|
|
4466
|
-
const c1 =
|
|
4896
|
+
const pushScriptPath = join27(shareDir, "wiki-push.sh");
|
|
4897
|
+
const c1 = existsSync9(pushScriptPath) ? check("pass", "vault_sync_installed", "Vault sync installed", `Found: ${pushScriptPath}`) : check("error", "vault_sync_installed", "Vault sync installed", `Script not found at ${pushScriptPath} \u2014 run vault-sync-install`);
|
|
4467
4898
|
let c2;
|
|
4468
4899
|
try {
|
|
4469
4900
|
if (isMac) {
|
|
@@ -4514,7 +4945,7 @@ function vaultSyncChecks(input) {
|
|
|
4514
4945
|
"Scheduler check failed \u2014 run vault-sync-install"
|
|
4515
4946
|
);
|
|
4516
4947
|
}
|
|
4517
|
-
const logFile =
|
|
4948
|
+
const logFile = join27(logDir, "wiki-push.log");
|
|
4518
4949
|
let c3;
|
|
4519
4950
|
try {
|
|
4520
4951
|
const logContent = readFileSync7(logFile, "utf8");
|
|
@@ -4573,7 +5004,7 @@ function vaultSyncChecks(input) {
|
|
|
4573
5004
|
}
|
|
4574
5005
|
}
|
|
4575
5006
|
} catch {
|
|
4576
|
-
c3 =
|
|
5007
|
+
c3 = existsSync9(logDir) ? check(
|
|
4577
5008
|
"warn",
|
|
4578
5009
|
"vault_sync_last_push_age",
|
|
4579
5010
|
"Vault sync last push recency",
|
|
@@ -4585,7 +5016,7 @@ function vaultSyncChecks(input) {
|
|
|
4585
5016
|
`Log directory not found at ${logDir}`
|
|
4586
5017
|
);
|
|
4587
5018
|
}
|
|
4588
|
-
const fetchLogFile =
|
|
5019
|
+
const fetchLogFile = join27(logDir, "wiki-fetch.log");
|
|
4589
5020
|
let cFetch;
|
|
4590
5021
|
try {
|
|
4591
5022
|
const logContent = readFileSync7(fetchLogFile, "utf8");
|
|
@@ -4632,7 +5063,7 @@ function vaultSyncChecks(input) {
|
|
|
4632
5063
|
}
|
|
4633
5064
|
let c4;
|
|
4634
5065
|
try {
|
|
4635
|
-
if (!
|
|
5066
|
+
if (!existsSync9(filterPath)) {
|
|
4636
5067
|
c4 = check(
|
|
4637
5068
|
"error",
|
|
4638
5069
|
"vault_sync_filter_present",
|
|
@@ -4681,7 +5112,7 @@ function vaultSyncChecks(input) {
|
|
|
4681
5112
|
);
|
|
4682
5113
|
} else {
|
|
4683
5114
|
try {
|
|
4684
|
-
if (!
|
|
5115
|
+
if (!existsSync9(snapshotPath)) {
|
|
4685
5116
|
c5 = check(
|
|
4686
5117
|
"error",
|
|
4687
5118
|
"vault_sync_snapshot_guard",
|
|
@@ -4727,13 +5158,17 @@ function findSkillMd(dir) {
|
|
|
4727
5158
|
}
|
|
4728
5159
|
for (const entry of entries) {
|
|
4729
5160
|
if (entry.isFile() && entry.name === "SKILL.md") {
|
|
4730
|
-
results.push(
|
|
5161
|
+
results.push(join27(dir, entry.name));
|
|
4731
5162
|
} else if (entry.isDirectory()) {
|
|
4732
|
-
results.push(...findSkillMd(
|
|
5163
|
+
results.push(...findSkillMd(join27(dir, entry.name)));
|
|
4733
5164
|
}
|
|
4734
5165
|
}
|
|
4735
5166
|
return results;
|
|
4736
5167
|
}
|
|
5168
|
+
function findInstalledSkillMd(dir) {
|
|
5169
|
+
const directSkills = findSkillNames(dir).map((name) => join27(dir, name, "SKILL.md"));
|
|
5170
|
+
return directSkills.length > 0 ? directSkills : findSkillMd(dir);
|
|
5171
|
+
}
|
|
4737
5172
|
function findSkillNames(dir) {
|
|
4738
5173
|
const results = [];
|
|
4739
5174
|
let entries;
|
|
@@ -4743,12 +5178,61 @@ function findSkillNames(dir) {
|
|
|
4743
5178
|
return results;
|
|
4744
5179
|
}
|
|
4745
5180
|
for (const entry of entries) {
|
|
4746
|
-
if (entry.isDirectory() &&
|
|
5181
|
+
if (entry.isDirectory() && existsSync9(join27(dir, entry.name, "SKILL.md"))) {
|
|
4747
5182
|
results.push(entry.name);
|
|
4748
5183
|
}
|
|
4749
5184
|
}
|
|
4750
5185
|
return results;
|
|
4751
5186
|
}
|
|
5187
|
+
var METRIC_TYPES = ["entities", "concepts", "comparisons", "queries", "meta"];
|
|
5188
|
+
async function vaultMetrics(resolvedPath) {
|
|
5189
|
+
const ids = [
|
|
5190
|
+
["vault_metric_pages", "Vault pages by type"],
|
|
5191
|
+
["vault_metric_orphans", "Vault orphan rate"],
|
|
5192
|
+
["vault_metric_bridges", "Vault bridge count"],
|
|
5193
|
+
["vault_metric_cohesion", "Mean community cohesion"],
|
|
5194
|
+
["vault_metric_log_size", "Vault log size"]
|
|
5195
|
+
];
|
|
5196
|
+
const noVault = () => ids.map(([id, label]) => check("info", id, label, "no vault configured"));
|
|
5197
|
+
if (!resolvedPath) return noVault();
|
|
5198
|
+
const scan = await scanVault(resolvedPath);
|
|
5199
|
+
if (!scan.ok) return noVault();
|
|
5200
|
+
const tk = scan.data.typedKnowledge;
|
|
5201
|
+
const perType = METRIC_TYPES.map((d) => `${d} ${tk.filter((p) => p.relPath.startsWith(d + "/")).length}`).join(", ");
|
|
5202
|
+
const adj = await buildWikilinkAdjacency(tk);
|
|
5203
|
+
const g = toUndirectedWeighted(adj);
|
|
5204
|
+
const nodes = [...g.keys()];
|
|
5205
|
+
const total = nodes.length;
|
|
5206
|
+
const orphanCount = nodes.filter((n) => g.get(n).size === 0).length;
|
|
5207
|
+
const orphanRate = total > 0 ? Math.round(orphanCount / total * 1e3) / 10 : 0;
|
|
5208
|
+
const comm = louvain(g);
|
|
5209
|
+
const groups = /* @__PURE__ */ new Map();
|
|
5210
|
+
for (const [node, c] of comm) {
|
|
5211
|
+
const arr = groups.get(c);
|
|
5212
|
+
if (arr) arr.push(node);
|
|
5213
|
+
else groups.set(c, [node]);
|
|
5214
|
+
}
|
|
5215
|
+
const cohesions = [...groups.values()].filter((m) => m.length >= 2).map((m) => communityCohesion(m, g));
|
|
5216
|
+
const meanCohesion = cohesions.length > 0 ? Math.round(cohesions.reduce((a, b) => a + b, 0) / cohesions.length * 1e3) / 1e3 : 0;
|
|
5217
|
+
let bridges = 0;
|
|
5218
|
+
for (const n of nodes) {
|
|
5219
|
+
const nbrComms = /* @__PURE__ */ new Set();
|
|
5220
|
+
for (const nb of g.get(n).keys()) nbrComms.add(comm.get(nb));
|
|
5221
|
+
if (nbrComms.size >= 3) bridges++;
|
|
5222
|
+
}
|
|
5223
|
+
let logLines = 0;
|
|
5224
|
+
try {
|
|
5225
|
+
logLines = readFileSync7(join27(resolvedPath, "log.md"), "utf8").split("\n").length;
|
|
5226
|
+
} catch {
|
|
5227
|
+
}
|
|
5228
|
+
return [
|
|
5229
|
+
check("info", "vault_metric_pages", "Vault pages by type", `${total} typed (${perType})`),
|
|
5230
|
+
check("info", "vault_metric_orphans", "Vault orphan rate", `${orphanRate}% (${orphanCount}/${total} degree-0)`),
|
|
5231
|
+
check("info", "vault_metric_bridges", "Vault bridge count", `${bridges} page(s) link >= 3 communities`),
|
|
5232
|
+
check("info", "vault_metric_cohesion", "Mean community cohesion", `${meanCohesion} across ${cohesions.length} communities (size >= 2)`),
|
|
5233
|
+
check("info", "vault_metric_log_size", "Vault log size", `${logLines} lines`)
|
|
5234
|
+
];
|
|
5235
|
+
}
|
|
4752
5236
|
async function runDoctor(input) {
|
|
4753
5237
|
const checks = [];
|
|
4754
5238
|
const vsConfig = readVaultSyncConfig(input.home);
|
|
@@ -4771,6 +5255,7 @@ async function runDoctor(input) {
|
|
|
4771
5255
|
checks.push(checkSyncLastPush(resolvedPath));
|
|
4772
5256
|
checks.push(checkDotStoreClean(resolvedPath));
|
|
4773
5257
|
checks.push(checkS3MountPerf(resolvedPath));
|
|
5258
|
+
checks.push(checkS3MountFreshness(resolvedPath));
|
|
4774
5259
|
checks.push(checkRcloneFlagAudit(resolvedPath));
|
|
4775
5260
|
checks.push(checkRcloneVersion(resolvedPath, vsConfig.installed));
|
|
4776
5261
|
checks.push(checkWriteTest(resolvedPath));
|
|
@@ -4784,6 +5269,7 @@ async function runDoctor(input) {
|
|
|
4784
5269
|
vaultSyncInstalled: vsConfig.installed,
|
|
4785
5270
|
vaultSyncRole: vsConfig.role
|
|
4786
5271
|
}));
|
|
5272
|
+
checks.push(...await vaultMetrics(resolvedPath));
|
|
4787
5273
|
const summary = {
|
|
4788
5274
|
pass: checks.filter((c) => c.status === "pass").length,
|
|
4789
5275
|
info: checks.filter((c) => c.status === "info").length,
|
|
@@ -4807,8 +5293,8 @@ async function runDoctor(input) {
|
|
|
4807
5293
|
}
|
|
4808
5294
|
|
|
4809
5295
|
// src/commands/archive.ts
|
|
4810
|
-
import { rename as
|
|
4811
|
-
import { join as
|
|
5296
|
+
import { rename as rename7, mkdir as mkdir9, readFile as readFile20, writeFile as writeFile10 } from "fs/promises";
|
|
5297
|
+
import { join as join28, dirname as dirname10 } from "path";
|
|
4812
5298
|
function countWikilinks(body, slug) {
|
|
4813
5299
|
const escaped = slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4814
5300
|
const re = new RegExp(`\\[\\[${escaped}(?:[|#][^\\]]*)?\\]\\]`, "g");
|
|
@@ -4836,7 +5322,7 @@ async function runArchive(input) {
|
|
|
4836
5322
|
if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
|
|
4837
5323
|
if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
|
|
4838
5324
|
const slug = relPath.replace(/\.md$/, "").split("/").pop();
|
|
4839
|
-
const archivePath =
|
|
5325
|
+
const archivePath = join28("_archive", relPath).replace(/\\/g, "/");
|
|
4840
5326
|
let cascade;
|
|
4841
5327
|
if (input.cascade) {
|
|
4842
5328
|
const wikilinkRefs = [];
|
|
@@ -4860,7 +5346,7 @@ async function runArchive(input) {
|
|
|
4860
5346
|
const indexRefs = [];
|
|
4861
5347
|
if (!isRaw) {
|
|
4862
5348
|
try {
|
|
4863
|
-
const idx = await
|
|
5349
|
+
const idx = await readFile20(join28(input.vault, "index.md"), "utf8");
|
|
4864
5350
|
idx.split("\n").forEach((line, i) => {
|
|
4865
5351
|
if (line.includes(`[[${slug}]]`)) indexRefs.push({ line: i + 1, text: line });
|
|
4866
5352
|
});
|
|
@@ -4886,8 +5372,8 @@ async function runArchive(input) {
|
|
|
4886
5372
|
}
|
|
4887
5373
|
if (input.cascade && input.apply && cascade) {
|
|
4888
5374
|
for (const ref of cascade.source_array_refs) {
|
|
4889
|
-
const absPath =
|
|
4890
|
-
const text = await
|
|
5375
|
+
const absPath = join28(input.vault, ref.page);
|
|
5376
|
+
const text = await readFile20(absPath, "utf8");
|
|
4891
5377
|
const split = splitFrontmatter(text);
|
|
4892
5378
|
if (!split.ok) continue;
|
|
4893
5379
|
const before = split.data.rawFrontmatter;
|
|
@@ -4898,29 +5384,29 @@ async function runArchive(input) {
|
|
|
4898
5384
|
);
|
|
4899
5385
|
if (fmRewritten === before) continue;
|
|
4900
5386
|
if (!arraysEqual(ref.sources_after, ref.sources_before)) {
|
|
4901
|
-
await
|
|
5387
|
+
await writeFile10(absPath, `---
|
|
4902
5388
|
${fmRewritten}
|
|
4903
5389
|
---${split.data.body}`, "utf8");
|
|
4904
5390
|
}
|
|
4905
5391
|
}
|
|
4906
5392
|
}
|
|
4907
|
-
await
|
|
5393
|
+
await mkdir9(dirname10(join28(input.vault, archivePath)), { recursive: true });
|
|
4908
5394
|
let indexUpdated = false;
|
|
4909
5395
|
if (!isRaw) {
|
|
4910
|
-
const indexPath =
|
|
5396
|
+
const indexPath = join28(input.vault, "index.md");
|
|
4911
5397
|
try {
|
|
4912
|
-
const idx = await
|
|
5398
|
+
const idx = await readFile20(indexPath, "utf8");
|
|
4913
5399
|
const originalLines = idx.split("\n");
|
|
4914
5400
|
const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
|
|
4915
5401
|
if (filtered.length !== originalLines.length) {
|
|
4916
|
-
await
|
|
5402
|
+
await writeFile10(indexPath, filtered.join("\n"), "utf8");
|
|
4917
5403
|
indexUpdated = true;
|
|
4918
5404
|
}
|
|
4919
5405
|
} catch (e) {
|
|
4920
5406
|
if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
|
|
4921
5407
|
}
|
|
4922
5408
|
}
|
|
4923
|
-
await
|
|
5409
|
+
await rename7(join28(input.vault, relPath), join28(input.vault, archivePath));
|
|
4924
5410
|
appendLastOp(input.vault, {
|
|
4925
5411
|
operation: input.cascade ? "archive-cascade" : "archive",
|
|
4926
5412
|
summary: `moved ${relPath} to ${archivePath}${input.cascade ? ` (cascade: ${cascade?.source_array_refs.length ?? 0} source arrays updated)` : ""}`,
|
|
@@ -5307,14 +5793,14 @@ ${newBody}`;
|
|
|
5307
5793
|
// src/commands/update.ts
|
|
5308
5794
|
import { execSync as execSync3 } from "child_process";
|
|
5309
5795
|
import { readFileSync as readFileSync8 } from "fs";
|
|
5310
|
-
import { join as
|
|
5796
|
+
import { join as join29 } from "path";
|
|
5311
5797
|
function resolveGlobalSkillsRoot() {
|
|
5312
5798
|
try {
|
|
5313
5799
|
const globalRoot = execSync3("npm root -g", {
|
|
5314
5800
|
encoding: "utf8",
|
|
5315
5801
|
timeout: 5e3
|
|
5316
5802
|
}).trim();
|
|
5317
|
-
return
|
|
5803
|
+
return join29(globalRoot, "skillwiki", "skills");
|
|
5318
5804
|
} catch {
|
|
5319
5805
|
return null;
|
|
5320
5806
|
}
|
|
@@ -5340,7 +5826,7 @@ async function runUpdate(input) {
|
|
|
5340
5826
|
);
|
|
5341
5827
|
const currentVersion = pkg2.version;
|
|
5342
5828
|
const tag = input.distTag ?? "latest";
|
|
5343
|
-
const target =
|
|
5829
|
+
const target = join29(input.home, ".claude", "skills");
|
|
5344
5830
|
let latest;
|
|
5345
5831
|
try {
|
|
5346
5832
|
latest = execSync3(`npm view skillwiki@${tag} version`, {
|
|
@@ -5410,16 +5896,16 @@ async function runUpdate(input) {
|
|
|
5410
5896
|
|
|
5411
5897
|
// src/commands/self-update.ts
|
|
5412
5898
|
import { execSync as execSync4 } from "child_process";
|
|
5413
|
-
import { existsSync as
|
|
5414
|
-
import { join as
|
|
5899
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
|
|
5900
|
+
import { join as join30 } from "path";
|
|
5415
5901
|
var DEFAULT_SOURCE_ROOT_SUFFIX = "/Desktop/code/llm-wiki";
|
|
5416
5902
|
async function runSelfUpdate(input) {
|
|
5417
5903
|
const currentVersion = JSON.parse(
|
|
5418
5904
|
readFileSync9(new URL("../../package.json", import.meta.url), "utf8")
|
|
5419
5905
|
).version;
|
|
5420
5906
|
const sourceRoot = input.sourceRoot ?? `${input.home}${DEFAULT_SOURCE_ROOT_SUFFIX}`;
|
|
5421
|
-
const localPkgPath =
|
|
5422
|
-
const hasLocalSource =
|
|
5907
|
+
const localPkgPath = join30(sourceRoot, "packages", "cli", "package.json");
|
|
5908
|
+
const hasLocalSource = existsSync10(localPkgPath);
|
|
5423
5909
|
if (input.check) {
|
|
5424
5910
|
let availableVersion = null;
|
|
5425
5911
|
let source;
|
|
@@ -5550,10 +6036,10 @@ async function runSelfUpdate(input) {
|
|
|
5550
6036
|
}
|
|
5551
6037
|
|
|
5552
6038
|
// src/commands/transcripts.ts
|
|
5553
|
-
import { readdir as readdir5, stat as
|
|
5554
|
-
import { join as
|
|
6039
|
+
import { readdir as readdir5, stat as stat7, readFile as readFile21 } from "fs/promises";
|
|
6040
|
+
import { join as join31 } from "path";
|
|
5555
6041
|
async function runTranscripts(input) {
|
|
5556
|
-
const dir =
|
|
6042
|
+
const dir = join31(input.vault, "raw", "transcripts");
|
|
5557
6043
|
let entries;
|
|
5558
6044
|
try {
|
|
5559
6045
|
entries = await readdir5(dir, { withFileTypes: true });
|
|
@@ -5563,13 +6049,13 @@ async function runTranscripts(input) {
|
|
|
5563
6049
|
const transcripts = [];
|
|
5564
6050
|
for (const entry of entries) {
|
|
5565
6051
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
5566
|
-
const filePath =
|
|
5567
|
-
const content = await
|
|
6052
|
+
const filePath = join31(dir, entry.name);
|
|
6053
|
+
const content = await readFile21(filePath, "utf8");
|
|
5568
6054
|
const fm = extractFrontmatter(content);
|
|
5569
6055
|
if (!fm.ok) continue;
|
|
5570
6056
|
const ingested = typeof fm.data.ingested === "string" ? fm.data.ingested : "";
|
|
5571
6057
|
if (input.since && ingested && ingested < input.since) continue;
|
|
5572
|
-
const s = await
|
|
6058
|
+
const s = await stat7(filePath);
|
|
5573
6059
|
transcripts.push({
|
|
5574
6060
|
file: `raw/transcripts/${entry.name}`,
|
|
5575
6061
|
ingested,
|
|
@@ -5581,12 +6067,12 @@ async function runTranscripts(input) {
|
|
|
5581
6067
|
}
|
|
5582
6068
|
|
|
5583
6069
|
// src/commands/project-index.ts
|
|
5584
|
-
import { readdir as readdir6, readFile as
|
|
5585
|
-
import { join as
|
|
6070
|
+
import { readdir as readdir6, readFile as readFile22, writeFile as writeFile11, mkdir as mkdir10 } from "fs/promises";
|
|
6071
|
+
import { join as join32, dirname as dirname11 } from "path";
|
|
5586
6072
|
var LAYER2_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
|
|
5587
6073
|
async function runProjectIndex(input) {
|
|
5588
6074
|
const slug = input.slug;
|
|
5589
|
-
const projectDir =
|
|
6075
|
+
const projectDir = join32(input.vault, "projects", slug);
|
|
5590
6076
|
try {
|
|
5591
6077
|
await readdir6(projectDir);
|
|
5592
6078
|
} catch {
|
|
@@ -5597,15 +6083,15 @@ async function runProjectIndex(input) {
|
|
|
5597
6083
|
}
|
|
5598
6084
|
const wikilinkPattern = `[[${slug}]]`;
|
|
5599
6085
|
const entries = [];
|
|
5600
|
-
const compoundDir =
|
|
6086
|
+
const compoundDir = join32(input.vault, "projects", slug, "compound");
|
|
5601
6087
|
try {
|
|
5602
6088
|
const compoundFiles = await readdir6(compoundDir, { withFileTypes: true });
|
|
5603
6089
|
for (const entry of compoundFiles) {
|
|
5604
6090
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
5605
|
-
const filePath =
|
|
6091
|
+
const filePath = join32(compoundDir, entry.name);
|
|
5606
6092
|
let text;
|
|
5607
6093
|
try {
|
|
5608
|
-
text = await
|
|
6094
|
+
text = await readFile22(filePath, "utf8");
|
|
5609
6095
|
} catch {
|
|
5610
6096
|
continue;
|
|
5611
6097
|
}
|
|
@@ -5622,16 +6108,16 @@ async function runProjectIndex(input) {
|
|
|
5622
6108
|
for (const dir of LAYER2_DIRS) {
|
|
5623
6109
|
let files;
|
|
5624
6110
|
try {
|
|
5625
|
-
files = await readdir6(
|
|
6111
|
+
files = await readdir6(join32(input.vault, dir), { withFileTypes: true });
|
|
5626
6112
|
} catch {
|
|
5627
6113
|
continue;
|
|
5628
6114
|
}
|
|
5629
6115
|
for (const entry of files) {
|
|
5630
6116
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
5631
|
-
const filePath =
|
|
6117
|
+
const filePath = join32(input.vault, dir, entry.name);
|
|
5632
6118
|
let text;
|
|
5633
6119
|
try {
|
|
5634
|
-
text = await
|
|
6120
|
+
text = await readFile22(filePath, "utf8");
|
|
5635
6121
|
} catch {
|
|
5636
6122
|
continue;
|
|
5637
6123
|
}
|
|
@@ -5652,11 +6138,11 @@ async function runProjectIndex(input) {
|
|
|
5652
6138
|
const tb = typeOrder[b.type] ?? 99;
|
|
5653
6139
|
return ta !== tb ? ta - tb : a.title.localeCompare(b.title);
|
|
5654
6140
|
});
|
|
5655
|
-
const indexPath =
|
|
6141
|
+
const indexPath = join32(projectDir, "knowledge.md");
|
|
5656
6142
|
let existing = false;
|
|
5657
6143
|
let stale = false;
|
|
5658
6144
|
try {
|
|
5659
|
-
const existingText = await
|
|
6145
|
+
const existingText = await readFile22(indexPath, "utf8");
|
|
5660
6146
|
existing = true;
|
|
5661
6147
|
const existingEntries = existingText.split("\n").filter((l) => l.startsWith("- [["));
|
|
5662
6148
|
const existingPages = new Set(existingEntries.map((l) => {
|
|
@@ -5696,8 +6182,8 @@ Autogenerated by \`skillwiki project-index\` on ${today}.
|
|
|
5696
6182
|
}
|
|
5697
6183
|
if (input.apply) {
|
|
5698
6184
|
try {
|
|
5699
|
-
await
|
|
5700
|
-
await
|
|
6185
|
+
await mkdir10(dirname11(indexPath), { recursive: true });
|
|
6186
|
+
await writeFile11(indexPath, body, "utf8");
|
|
5701
6187
|
} catch (e) {
|
|
5702
6188
|
return {
|
|
5703
6189
|
exitCode: ExitCode.WRITE_FAILED,
|
|
@@ -5725,10 +6211,10 @@ ${entries.map((e) => ` ${e.type}: [[${e.page.replace(/\.md$/, "")}]] \u2014 ${e
|
|
|
5725
6211
|
}
|
|
5726
6212
|
|
|
5727
6213
|
// src/commands/compound.ts
|
|
5728
|
-
import { writeFile as
|
|
5729
|
-
import { join as
|
|
5730
|
-
import { existsSync as
|
|
5731
|
-
import { readFile as
|
|
6214
|
+
import { writeFile as writeFile12, mkdir as mkdir11, readdir as readdir7, unlink as unlink4 } from "fs/promises";
|
|
6215
|
+
import { join as join33 } from "path";
|
|
6216
|
+
import { existsSync as existsSync11 } from "fs";
|
|
6217
|
+
import { readFile as readFile23 } from "fs/promises";
|
|
5732
6218
|
var RETRO_HEADING_RE = /^## \[(\d{4}-\d{2}-\d{2})(?:\s+[^\]]+)?\] retro \| loop cycle(?: (\d+))?: (.+)$/;
|
|
5733
6219
|
var FIELD_RE = {
|
|
5734
6220
|
improve: /^-\s+\*?\*?Improve:?\*?\*?\s*(.+)$/m,
|
|
@@ -5826,17 +6312,17 @@ function extractRetroFields(date, cycleName, block) {
|
|
|
5826
6312
|
};
|
|
5827
6313
|
}
|
|
5828
6314
|
async function runCompound(input) {
|
|
5829
|
-
const logPath =
|
|
6315
|
+
const logPath = join33(input.vault, "log.md");
|
|
5830
6316
|
let logText;
|
|
5831
6317
|
try {
|
|
5832
|
-
logText = await
|
|
6318
|
+
logText = await readFile23(logPath, "utf8");
|
|
5833
6319
|
} catch {
|
|
5834
6320
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
|
|
5835
6321
|
}
|
|
5836
6322
|
const entries = parseRetroEntries(logText);
|
|
5837
6323
|
const promoted = [];
|
|
5838
6324
|
const skipped = [];
|
|
5839
|
-
const compoundDir =
|
|
6325
|
+
const compoundDir = join33(input.vault, "projects", input.project, "compound");
|
|
5840
6326
|
for (const entry of entries) {
|
|
5841
6327
|
const generalizeValue = entry.generalize.trim();
|
|
5842
6328
|
if (!/^yes/i.test(generalizeValue)) {
|
|
@@ -5844,8 +6330,8 @@ async function runCompound(input) {
|
|
|
5844
6330
|
continue;
|
|
5845
6331
|
}
|
|
5846
6332
|
const slug = slugify(entry.cycleName);
|
|
5847
|
-
const compoundPath =
|
|
5848
|
-
if (
|
|
6333
|
+
const compoundPath = join33(compoundDir, `${slug}.md`);
|
|
6334
|
+
if (existsSync11(compoundPath)) {
|
|
5849
6335
|
skipped.push(entry.date);
|
|
5850
6336
|
continue;
|
|
5851
6337
|
}
|
|
@@ -5883,10 +6369,10 @@ async function runCompound(input) {
|
|
|
5883
6369
|
].join("\n");
|
|
5884
6370
|
const content = frontmatter + "\n" + body;
|
|
5885
6371
|
if (!input.dryRun) {
|
|
5886
|
-
if (!
|
|
5887
|
-
await
|
|
6372
|
+
if (!existsSync11(compoundDir)) {
|
|
6373
|
+
await mkdir11(compoundDir, { recursive: true });
|
|
5888
6374
|
}
|
|
5889
|
-
await
|
|
6375
|
+
await writeFile12(compoundPath, content, "utf8");
|
|
5890
6376
|
}
|
|
5891
6377
|
promoted.push(`${slug}.md`);
|
|
5892
6378
|
}
|
|
@@ -5905,23 +6391,23 @@ async function runCompound(input) {
|
|
|
5905
6391
|
};
|
|
5906
6392
|
}
|
|
5907
6393
|
async function runCompoundDelete(input) {
|
|
5908
|
-
const projectDir =
|
|
5909
|
-
if (!
|
|
6394
|
+
const projectDir = join33(input.vault, "projects", input.project);
|
|
6395
|
+
if (!existsSync11(projectDir)) {
|
|
5910
6396
|
return {
|
|
5911
6397
|
exitCode: ExitCode.PROJECT_NOT_FOUND,
|
|
5912
6398
|
result: err("PROJECT_NOT_FOUND", { slug: input.project, path: projectDir })
|
|
5913
6399
|
};
|
|
5914
6400
|
}
|
|
5915
6401
|
const entryName = input.entry.replace(/\.md$/, "");
|
|
5916
|
-
const compoundPath =
|
|
5917
|
-
if (!
|
|
6402
|
+
const compoundPath = join33(projectDir, "compound", `${entryName}.md`);
|
|
6403
|
+
if (!existsSync11(compoundPath)) {
|
|
5918
6404
|
return {
|
|
5919
6405
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
5920
6406
|
result: err("FILE_NOT_FOUND", { path: compoundPath })
|
|
5921
6407
|
};
|
|
5922
6408
|
}
|
|
5923
6409
|
try {
|
|
5924
|
-
await
|
|
6410
|
+
await unlink4(compoundPath);
|
|
5925
6411
|
} catch (e) {
|
|
5926
6412
|
return {
|
|
5927
6413
|
exitCode: ExitCode.WRITE_FAILED,
|
|
@@ -5947,8 +6433,8 @@ knowledge.md regenerated`
|
|
|
5947
6433
|
};
|
|
5948
6434
|
}
|
|
5949
6435
|
async function runCompoundList(input) {
|
|
5950
|
-
const compoundDir =
|
|
5951
|
-
if (!
|
|
6436
|
+
const compoundDir = join33(input.vault, "projects", input.project, "compound");
|
|
6437
|
+
if (!existsSync11(compoundDir)) {
|
|
5952
6438
|
return {
|
|
5953
6439
|
exitCode: ExitCode.OK,
|
|
5954
6440
|
result: ok({
|
|
@@ -5978,10 +6464,10 @@ could not read compound directory`
|
|
|
5978
6464
|
const entries = [];
|
|
5979
6465
|
for (const dirent of dirents) {
|
|
5980
6466
|
if (!dirent.isFile() || !dirent.name.endsWith(".md")) continue;
|
|
5981
|
-
const filePath =
|
|
6467
|
+
const filePath = join33(compoundDir, dirent.name);
|
|
5982
6468
|
let text;
|
|
5983
6469
|
try {
|
|
5984
|
-
text = await
|
|
6470
|
+
text = await readFile23(filePath, "utf8");
|
|
5985
6471
|
} catch {
|
|
5986
6472
|
continue;
|
|
5987
6473
|
}
|
|
@@ -6010,9 +6496,9 @@ no compound entries found`;
|
|
|
6010
6496
|
}
|
|
6011
6497
|
|
|
6012
6498
|
// src/commands/observe.ts
|
|
6013
|
-
import { mkdir as
|
|
6014
|
-
import { existsSync as
|
|
6015
|
-
import { join as
|
|
6499
|
+
import { mkdir as mkdir12, writeFile as writeFile13 } from "fs/promises";
|
|
6500
|
+
import { existsSync as existsSync12, statSync as statSync4 } from "fs";
|
|
6501
|
+
import { join as join34 } from "path";
|
|
6016
6502
|
import { createHash as createHash4 } from "crypto";
|
|
6017
6503
|
var ALLOWED_KINDS = /* @__PURE__ */ new Set(["note", "bug", "task", "idea", "session-log"]);
|
|
6018
6504
|
function slugify2(text) {
|
|
@@ -6035,15 +6521,15 @@ async function runObserve(input) {
|
|
|
6035
6521
|
result: err("SCHEME_REJECTED", { message: "Text must not be empty" })
|
|
6036
6522
|
};
|
|
6037
6523
|
}
|
|
6038
|
-
if (!
|
|
6524
|
+
if (!existsSync12(input.vault) || !statSync4(input.vault).isDirectory()) {
|
|
6039
6525
|
return {
|
|
6040
6526
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
6041
6527
|
result: err("VAULT_PATH_INVALID", { path: input.vault })
|
|
6042
6528
|
};
|
|
6043
6529
|
}
|
|
6044
|
-
const transcriptsDir =
|
|
6530
|
+
const transcriptsDir = join34(input.vault, "raw", "transcripts");
|
|
6045
6531
|
try {
|
|
6046
|
-
await
|
|
6532
|
+
await mkdir12(transcriptsDir, { recursive: true });
|
|
6047
6533
|
} catch {
|
|
6048
6534
|
return {
|
|
6049
6535
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
@@ -6053,7 +6539,7 @@ async function runObserve(input) {
|
|
|
6053
6539
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6054
6540
|
const slug = slugify2(input.text);
|
|
6055
6541
|
const fileName = `${today}-observation-${slug}.md`;
|
|
6056
|
-
const filePath =
|
|
6542
|
+
const filePath = join34(transcriptsDir, fileName);
|
|
6057
6543
|
const body = `
|
|
6058
6544
|
${input.text.trim()}
|
|
6059
6545
|
`;
|
|
@@ -6071,7 +6557,7 @@ ${input.text.trim()}
|
|
|
6071
6557
|
frontmatterLines.push("---");
|
|
6072
6558
|
const content = frontmatterLines.join("\n") + body;
|
|
6073
6559
|
try {
|
|
6074
|
-
await
|
|
6560
|
+
await writeFile13(filePath, content, "utf8");
|
|
6075
6561
|
} catch (e) {
|
|
6076
6562
|
return {
|
|
6077
6563
|
exitCode: ExitCode.WRITE_FAILED,
|
|
@@ -6093,8 +6579,8 @@ ${input.text.trim()}
|
|
|
6093
6579
|
}
|
|
6094
6580
|
|
|
6095
6581
|
// src/commands/ingest.ts
|
|
6096
|
-
import { readFile as
|
|
6097
|
-
import { join as
|
|
6582
|
+
import { readFile as readFile24, writeFile as writeFile14, mkdir as mkdir13 } from "fs/promises";
|
|
6583
|
+
import { join as join35 } from "path";
|
|
6098
6584
|
import { createHash as createHash5 } from "crypto";
|
|
6099
6585
|
var ALLOWED_TYPES = /* @__PURE__ */ new Set(["entity", "concept", "comparison", "query"]);
|
|
6100
6586
|
var TYPE_DIR = {
|
|
@@ -6253,7 +6739,7 @@ async function runIngest(input) {
|
|
|
6253
6739
|
sourceContent = fetchResult.data.body;
|
|
6254
6740
|
} else {
|
|
6255
6741
|
try {
|
|
6256
|
-
sourceContent = await
|
|
6742
|
+
sourceContent = await readFile24(input.source, "utf8");
|
|
6257
6743
|
} catch {
|
|
6258
6744
|
return {
|
|
6259
6745
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
@@ -6268,8 +6754,8 @@ async function runIngest(input) {
|
|
|
6268
6754
|
const rawRelPath = `raw/articles/${slug}.md`;
|
|
6269
6755
|
const typedDir = TYPE_DIR[input.type] ?? `${input.type}s`;
|
|
6270
6756
|
const typedRelPath = `${typedDir}/${slug}.md`;
|
|
6271
|
-
const rawAbsPath =
|
|
6272
|
-
const typedAbsPath =
|
|
6757
|
+
const rawAbsPath = join35(input.vault, rawRelPath);
|
|
6758
|
+
const typedAbsPath = join35(input.vault, typedRelPath);
|
|
6273
6759
|
const rawContent = buildRawContent(sourceUrl, today, sha256, sourceContent);
|
|
6274
6760
|
const typedContent = buildTypedContent(
|
|
6275
6761
|
input.title,
|
|
@@ -6332,8 +6818,8 @@ async function runIngest(input) {
|
|
|
6332
6818
|
};
|
|
6333
6819
|
}
|
|
6334
6820
|
try {
|
|
6335
|
-
await
|
|
6336
|
-
await
|
|
6821
|
+
await mkdir13(join35(input.vault, "raw", "articles"), { recursive: true });
|
|
6822
|
+
await writeFile14(rawAbsPath, rawContent, "utf8");
|
|
6337
6823
|
} catch (e) {
|
|
6338
6824
|
return {
|
|
6339
6825
|
exitCode: ExitCode.WRITE_FAILED,
|
|
@@ -6341,8 +6827,8 @@ async function runIngest(input) {
|
|
|
6341
6827
|
};
|
|
6342
6828
|
}
|
|
6343
6829
|
try {
|
|
6344
|
-
await
|
|
6345
|
-
await
|
|
6830
|
+
await mkdir13(join35(input.vault, typedDir), { recursive: true });
|
|
6831
|
+
await writeFile14(typedAbsPath, typedContent, "utf8");
|
|
6346
6832
|
} catch (e) {
|
|
6347
6833
|
return {
|
|
6348
6834
|
exitCode: ExitCode.WRITE_FAILED,
|
|
@@ -6520,12 +7006,12 @@ ${body}`;
|
|
|
6520
7006
|
}
|
|
6521
7007
|
|
|
6522
7008
|
// src/commands/sync.ts
|
|
6523
|
-
import { existsSync as
|
|
6524
|
-
import { join as
|
|
7009
|
+
import { existsSync as existsSync14 } from "fs";
|
|
7010
|
+
import { join as join37 } from "path";
|
|
6525
7011
|
|
|
6526
7012
|
// src/utils/sync-lock.ts
|
|
6527
|
-
import { existsSync as
|
|
6528
|
-
import { join as
|
|
7013
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync4, readFileSync as readFileSync10, renameSync, unlinkSync as unlinkSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
7014
|
+
import { join as join36 } from "path";
|
|
6529
7015
|
import { createHash as createHash6 } from "crypto";
|
|
6530
7016
|
function getSessionId() {
|
|
6531
7017
|
if (process.env.CLAUDE_SESSION_ID) return process.env.CLAUDE_SESSION_ID;
|
|
@@ -6533,11 +7019,11 @@ function getSessionId() {
|
|
|
6533
7019
|
return process.pid.toString();
|
|
6534
7020
|
}
|
|
6535
7021
|
function lockPath(vault) {
|
|
6536
|
-
return
|
|
7022
|
+
return join36(vault, ".skillwiki", "sync.lock");
|
|
6537
7023
|
}
|
|
6538
7024
|
function readLock(vault) {
|
|
6539
7025
|
const path = lockPath(vault);
|
|
6540
|
-
if (!
|
|
7026
|
+
if (!existsSync13(path)) return null;
|
|
6541
7027
|
try {
|
|
6542
7028
|
const raw = readFileSync10(path, "utf8");
|
|
6543
7029
|
return JSON.parse(raw);
|
|
@@ -6552,9 +7038,9 @@ function isStale(lock, now) {
|
|
|
6552
7038
|
}
|
|
6553
7039
|
function acquireLock(vault, opts = {}) {
|
|
6554
7040
|
const path = lockPath(vault);
|
|
6555
|
-
const dir =
|
|
6556
|
-
if (!
|
|
6557
|
-
|
|
7041
|
+
const dir = join36(vault, ".skillwiki");
|
|
7042
|
+
if (!existsSync13(dir)) {
|
|
7043
|
+
mkdirSync4(dir, { recursive: true });
|
|
6558
7044
|
}
|
|
6559
7045
|
const sessionId = opts.sessionId ?? getSessionId();
|
|
6560
7046
|
const summary = opts.summary ?? "skillwiki sync";
|
|
@@ -6573,7 +7059,7 @@ function acquireLock(vault, opts = {}) {
|
|
|
6573
7059
|
};
|
|
6574
7060
|
try {
|
|
6575
7061
|
const content = JSON.stringify(lock, null, 2) + "\n";
|
|
6576
|
-
|
|
7062
|
+
writeFileSync6(path, content, { flag: "wx" });
|
|
6577
7063
|
return { ok: true, lock };
|
|
6578
7064
|
} catch (e) {
|
|
6579
7065
|
const err3 = e;
|
|
@@ -6593,19 +7079,19 @@ function acquireLock(vault, opts = {}) {
|
|
|
6593
7079
|
function writeLockedFile(path, lock) {
|
|
6594
7080
|
const tmp = path + ".tmp";
|
|
6595
7081
|
const content = JSON.stringify(lock, null, 2) + "\n";
|
|
6596
|
-
|
|
7082
|
+
writeFileSync6(tmp, content);
|
|
6597
7083
|
renameSync(tmp, path);
|
|
6598
7084
|
}
|
|
6599
7085
|
function releaseLock(vault, opts = {}) {
|
|
6600
7086
|
const path = lockPath(vault);
|
|
6601
|
-
if (!
|
|
7087
|
+
if (!existsSync13(path)) {
|
|
6602
7088
|
return { released: false };
|
|
6603
7089
|
}
|
|
6604
7090
|
const sessionId = opts.sessionId ?? getSessionId();
|
|
6605
7091
|
const existing = readLock(vault);
|
|
6606
7092
|
if (opts.force) {
|
|
6607
7093
|
try {
|
|
6608
|
-
|
|
7094
|
+
unlinkSync5(path);
|
|
6609
7095
|
const prior = existing && existing.session_id !== sessionId ? existing : void 0;
|
|
6610
7096
|
return { released: true, prior };
|
|
6611
7097
|
} catch {
|
|
@@ -6616,7 +7102,7 @@ function releaseLock(vault, opts = {}) {
|
|
|
6616
7102
|
return { released: false };
|
|
6617
7103
|
}
|
|
6618
7104
|
try {
|
|
6619
|
-
|
|
7105
|
+
unlinkSync5(path);
|
|
6620
7106
|
return { released: true };
|
|
6621
7107
|
} catch {
|
|
6622
7108
|
return { released: false };
|
|
@@ -6627,7 +7113,7 @@ function releaseLock(vault, opts = {}) {
|
|
|
6627
7113
|
function runSyncStatus(input) {
|
|
6628
7114
|
const vault = input.vault;
|
|
6629
7115
|
const includeStashes = input.includeStashes ?? false;
|
|
6630
|
-
if (!
|
|
7116
|
+
if (!existsSync14(join37(vault, ".git"))) {
|
|
6631
7117
|
return {
|
|
6632
7118
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
6633
7119
|
result: ok({
|
|
@@ -6641,6 +7127,7 @@ function runSyncStatus(input) {
|
|
|
6641
7127
|
})
|
|
6642
7128
|
};
|
|
6643
7129
|
}
|
|
7130
|
+
enableGitLongPathsOnWindows(vault);
|
|
6644
7131
|
const porcelain = git(vault, ["status", "--porcelain"]);
|
|
6645
7132
|
const dirty = porcelain ? porcelain.split("\n").filter((l) => l.trim().length > 0).length : 0;
|
|
6646
7133
|
const revOutput = git(vault, ["rev-list", "--left-right", "--count", "origin/HEAD...HEAD"]);
|
|
@@ -6704,12 +7191,24 @@ function runSyncStatus(input) {
|
|
|
6704
7191
|
}
|
|
6705
7192
|
async function runSyncPush(input) {
|
|
6706
7193
|
const vault = input.vault;
|
|
6707
|
-
if (!
|
|
7194
|
+
if (!existsSync14(join37(vault, ".git"))) {
|
|
6708
7195
|
return {
|
|
6709
7196
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
6710
7197
|
result: err("NOT_A_GIT_REPO", { path: vault })
|
|
6711
7198
|
};
|
|
6712
7199
|
}
|
|
7200
|
+
enableGitLongPathsOnWindows(vault);
|
|
7201
|
+
let pathFixes = 0;
|
|
7202
|
+
const pathFix = await fixPathTooLong({ vault });
|
|
7203
|
+
if (pathFix.result.ok && pathFix.result.data.fixed.length > 0) {
|
|
7204
|
+
pathFixes = pathFix.result.data.fixed.length;
|
|
7205
|
+
appendLastOp(vault, {
|
|
7206
|
+
operation: "lint-fix",
|
|
7207
|
+
summary: `fixed ${pathFixes} long path(s)`,
|
|
7208
|
+
files: pathFix.result.data.fixed.flatMap((f) => [f.from, f.to]),
|
|
7209
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
7210
|
+
});
|
|
7211
|
+
}
|
|
6713
7212
|
const porcelain = git(vault, ["status", "--porcelain"]);
|
|
6714
7213
|
const dirtyFiles = porcelain ? porcelain.split("\n").filter((l) => l.trim().length > 0) : [];
|
|
6715
7214
|
if (dirtyFiles.length === 0) {
|
|
@@ -6719,6 +7218,7 @@ async function runSyncPush(input) {
|
|
|
6719
7218
|
files_committed: 0,
|
|
6720
7219
|
commit_message: "",
|
|
6721
7220
|
pushed: false,
|
|
7221
|
+
path_fixes: pathFixes,
|
|
6722
7222
|
humanHint: "nothing to commit, working tree clean"
|
|
6723
7223
|
})
|
|
6724
7224
|
};
|
|
@@ -6773,7 +7273,8 @@ async function runSyncPush(input) {
|
|
|
6773
7273
|
files_committed: dirtyFiles.length,
|
|
6774
7274
|
commit_message: commitMessage,
|
|
6775
7275
|
pushed: false,
|
|
6776
|
-
|
|
7276
|
+
path_fixes: pathFixes,
|
|
7277
|
+
humanHint: `committed ${dirtyFiles.length} file(s)${pathFixes > 0 ? ` after ${pathFixes} long-path fix(es)` : ""} but push failed: ${String(e)}`
|
|
6777
7278
|
})
|
|
6778
7279
|
};
|
|
6779
7280
|
}
|
|
@@ -6783,7 +7284,8 @@ async function runSyncPush(input) {
|
|
|
6783
7284
|
files_committed: dirtyFiles.length,
|
|
6784
7285
|
commit_message: commitMessage,
|
|
6785
7286
|
pushed,
|
|
6786
|
-
|
|
7287
|
+
path_fixes: pathFixes,
|
|
7288
|
+
humanHint: `committed and pushed ${dirtyFiles.length} file(s)${pathFixes > 0 ? ` after ${pathFixes} long-path fix(es)` : ""}`
|
|
6787
7289
|
})
|
|
6788
7290
|
};
|
|
6789
7291
|
}
|
|
@@ -6806,14 +7308,19 @@ function enumerateStashes(vault) {
|
|
|
6806
7308
|
}
|
|
6807
7309
|
return stashes;
|
|
6808
7310
|
}
|
|
7311
|
+
function enableGitLongPathsOnWindows(vault) {
|
|
7312
|
+
if (process.platform !== "win32") return;
|
|
7313
|
+
git(vault, ["config", "core.longpaths", "true"]);
|
|
7314
|
+
}
|
|
6809
7315
|
async function runSyncPull(input) {
|
|
6810
7316
|
const vault = input.vault;
|
|
6811
|
-
if (!
|
|
7317
|
+
if (!existsSync14(join37(vault, ".git"))) {
|
|
6812
7318
|
return {
|
|
6813
7319
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
6814
7320
|
result: err("NOT_A_GIT_REPO", { path: vault })
|
|
6815
7321
|
};
|
|
6816
7322
|
}
|
|
7323
|
+
enableGitLongPathsOnWindows(vault);
|
|
6817
7324
|
let fetched = false;
|
|
6818
7325
|
try {
|
|
6819
7326
|
gitStrict(vault, ["fetch", "origin"]);
|
|
@@ -6902,6 +7409,8 @@ async function runSyncPull(input) {
|
|
|
6902
7409
|
};
|
|
6903
7410
|
}
|
|
6904
7411
|
}
|
|
7412
|
+
const pathFix = await fixPathTooLong({ vault });
|
|
7413
|
+
const pathFixCount = pathFix.result.ok ? pathFix.result.data.fixed.length : 0;
|
|
6905
7414
|
let lintErrors = 0;
|
|
6906
7415
|
let lintWarnings = 0;
|
|
6907
7416
|
const lintResult = await runLint({ vault, days: 90, lines: 200, logThreshold: 500 });
|
|
@@ -6913,6 +7422,7 @@ async function runSyncPull(input) {
|
|
|
6913
7422
|
if (filesUpdated > 0) hintParts.push(`updated ${filesUpdated} file(s)`);
|
|
6914
7423
|
else hintParts.push("already up to date");
|
|
6915
7424
|
if (autoResolved > 0) hintParts.push(`${autoResolved} conflict(s) auto-resolved`);
|
|
7425
|
+
if (pathFixCount > 0) hintParts.push(`${pathFixCount} long path(s) fixed`);
|
|
6916
7426
|
if (lintErrors > 0) hintParts.push(`${lintErrors} lint error(s)`);
|
|
6917
7427
|
if (lintWarnings > 0) hintParts.push(`${lintWarnings} lint warning(s)`);
|
|
6918
7428
|
const exitCode = lintErrors > 0 ? ExitCode.LINT_HAS_ERRORS : lintWarnings > 0 ? ExitCode.LINT_HAS_WARNINGS : ExitCode.OK;
|
|
@@ -6976,7 +7486,7 @@ function runSyncPeers(input) {
|
|
|
6976
7486
|
}
|
|
6977
7487
|
function runSyncLock(input) {
|
|
6978
7488
|
const vault = input.vault;
|
|
6979
|
-
if (!
|
|
7489
|
+
if (!existsSync14(vault)) {
|
|
6980
7490
|
return {
|
|
6981
7491
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
6982
7492
|
result: err("VAULT_PATH_INVALID", { path: vault })
|
|
@@ -7011,7 +7521,7 @@ function runSyncLock(input) {
|
|
|
7011
7521
|
}
|
|
7012
7522
|
function runSyncUnlock(input) {
|
|
7013
7523
|
const vault = input.vault;
|
|
7014
|
-
if (!
|
|
7524
|
+
if (!existsSync14(vault)) {
|
|
7015
7525
|
return {
|
|
7016
7526
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
7017
7527
|
result: err("VAULT_PATH_INVALID", { path: vault })
|
|
@@ -7044,8 +7554,8 @@ function runSyncUnlock(input) {
|
|
|
7044
7554
|
}
|
|
7045
7555
|
|
|
7046
7556
|
// src/commands/backup.ts
|
|
7047
|
-
import { statSync as
|
|
7048
|
-
import { join as
|
|
7557
|
+
import { statSync as statSync5, readdirSync as readdirSync2, readFileSync as readFileSync11, mkdirSync as mkdirSync5, writeFileSync as writeFileSync7 } from "fs";
|
|
7558
|
+
import { join as join38, relative as relative3, dirname as dirname12 } from "path";
|
|
7049
7559
|
import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
|
|
7050
7560
|
|
|
7051
7561
|
// src/utils/s3-client.ts
|
|
@@ -7065,11 +7575,11 @@ function createS3Client(config) {
|
|
|
7065
7575
|
}
|
|
7066
7576
|
|
|
7067
7577
|
// src/commands/backup.ts
|
|
7068
|
-
var
|
|
7578
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node_modules", ".skillwiki"]);
|
|
7069
7579
|
function* walkMarkdown(dir, base) {
|
|
7070
7580
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
7071
|
-
if (
|
|
7072
|
-
const full =
|
|
7581
|
+
if (SKIP_DIRS2.has(entry.name)) continue;
|
|
7582
|
+
const full = join38(dir, entry.name);
|
|
7073
7583
|
if (entry.isDirectory()) {
|
|
7074
7584
|
yield* walkMarkdown(full, base);
|
|
7075
7585
|
} else if (entry.name.endsWith(".md")) {
|
|
@@ -7092,8 +7602,8 @@ async function runBackupSync(input) {
|
|
|
7092
7602
|
let failed = 0;
|
|
7093
7603
|
const files = [...walkMarkdown(input.vault, input.vault)];
|
|
7094
7604
|
for (const relPath of files) {
|
|
7095
|
-
const absPath =
|
|
7096
|
-
const localStat =
|
|
7605
|
+
const absPath = join38(input.vault, relPath);
|
|
7606
|
+
const localStat = statSync5(absPath);
|
|
7097
7607
|
let needsUpload = true;
|
|
7098
7608
|
try {
|
|
7099
7609
|
const head = await client.send(new HeadObjectCommand({ Bucket: input.bucket, Key: relPath }));
|
|
@@ -7168,9 +7678,9 @@ async function runBackupRestore(input) {
|
|
|
7168
7678
|
const objects = list.Contents ?? [];
|
|
7169
7679
|
for (const obj of objects) {
|
|
7170
7680
|
if (!obj.Key) continue;
|
|
7171
|
-
const localPath =
|
|
7681
|
+
const localPath = join38(target, obj.Key);
|
|
7172
7682
|
try {
|
|
7173
|
-
const localStat =
|
|
7683
|
+
const localStat = statSync5(localPath);
|
|
7174
7684
|
if (obj.LastModified && localStat.mtime > obj.LastModified) {
|
|
7175
7685
|
conflicts++;
|
|
7176
7686
|
continue;
|
|
@@ -7181,8 +7691,8 @@ async function runBackupRestore(input) {
|
|
|
7181
7691
|
const resp = await client.send(new GetObjectCommand({ Bucket: input.bucket, Key: obj.Key }));
|
|
7182
7692
|
const body = await resp.Body?.transformToByteArray();
|
|
7183
7693
|
if (body) {
|
|
7184
|
-
|
|
7185
|
-
|
|
7694
|
+
mkdirSync5(dirname12(localPath), { recursive: true });
|
|
7695
|
+
writeFileSync7(localPath, Buffer.from(body));
|
|
7186
7696
|
downloaded++;
|
|
7187
7697
|
}
|
|
7188
7698
|
} catch {
|
|
@@ -7214,11 +7724,11 @@ async function runBackupRestore(input) {
|
|
|
7214
7724
|
}
|
|
7215
7725
|
|
|
7216
7726
|
// src/commands/status.ts
|
|
7217
|
-
import { existsSync as
|
|
7218
|
-
import { readFile as
|
|
7219
|
-
import { join as
|
|
7727
|
+
import { existsSync as existsSync15, statSync as statSync6 } from "fs";
|
|
7728
|
+
import { readFile as readFile25 } from "fs/promises";
|
|
7729
|
+
import { join as join39 } from "path";
|
|
7220
7730
|
async function runStatus(input) {
|
|
7221
|
-
if (!
|
|
7731
|
+
if (!existsSync15(input.vault)) {
|
|
7222
7732
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
7223
7733
|
}
|
|
7224
7734
|
const scan = await scanVault(input.vault);
|
|
@@ -7243,7 +7753,7 @@ async function runStatus(input) {
|
|
|
7243
7753
|
const compound = scan.data.compound.length;
|
|
7244
7754
|
let schemaVersion = "v1";
|
|
7245
7755
|
try {
|
|
7246
|
-
const schemaContent = await
|
|
7756
|
+
const schemaContent = await readFile25(join39(input.vault, "SCHEMA.md"), "utf8");
|
|
7247
7757
|
const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
|
|
7248
7758
|
if (versionMatch) schemaVersion = versionMatch[1];
|
|
7249
7759
|
} catch {
|
|
@@ -7259,7 +7769,7 @@ async function runStatus(input) {
|
|
|
7259
7769
|
let maxTime = 0;
|
|
7260
7770
|
for (const page of allPages) {
|
|
7261
7771
|
try {
|
|
7262
|
-
const st =
|
|
7772
|
+
const st = statSync6(page.absPath);
|
|
7263
7773
|
if (st.mtimeMs > maxTime) {
|
|
7264
7774
|
maxTime = st.mtimeMs;
|
|
7265
7775
|
lastModified = st.mtime.toISOString();
|
|
@@ -7303,8 +7813,8 @@ async function runStatus(input) {
|
|
|
7303
7813
|
}
|
|
7304
7814
|
|
|
7305
7815
|
// src/commands/seed.ts
|
|
7306
|
-
import { mkdir as
|
|
7307
|
-
import { join as
|
|
7816
|
+
import { mkdir as mkdir14, writeFile as writeFile15, stat as stat8 } from "fs/promises";
|
|
7817
|
+
import { join as join40 } from "path";
|
|
7308
7818
|
var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
7309
7819
|
var EXAMPLE_PAGES = {
|
|
7310
7820
|
"entities/example-project.md": `---
|
|
@@ -7373,30 +7883,30 @@ Real sources are immutable after ingestion \u2014 never edit them.
|
|
|
7373
7883
|
`;
|
|
7374
7884
|
async function runSeed(input) {
|
|
7375
7885
|
try {
|
|
7376
|
-
await
|
|
7886
|
+
await stat8(join40(input.vault, "SCHEMA.md"));
|
|
7377
7887
|
} catch {
|
|
7378
7888
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
|
|
7379
7889
|
}
|
|
7380
7890
|
const created = [];
|
|
7381
7891
|
const skipped = [];
|
|
7382
7892
|
for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
|
|
7383
|
-
const absPath =
|
|
7893
|
+
const absPath = join40(input.vault, relPath);
|
|
7384
7894
|
try {
|
|
7385
|
-
await
|
|
7895
|
+
await stat8(absPath);
|
|
7386
7896
|
skipped.push(relPath);
|
|
7387
7897
|
} catch {
|
|
7388
|
-
await
|
|
7389
|
-
await
|
|
7898
|
+
await mkdir14(join40(absPath, ".."), { recursive: true });
|
|
7899
|
+
await writeFile15(absPath, content, "utf8");
|
|
7390
7900
|
created.push(relPath);
|
|
7391
7901
|
}
|
|
7392
7902
|
}
|
|
7393
|
-
const rawPath =
|
|
7903
|
+
const rawPath = join40(input.vault, "raw", "articles", "example-source.md");
|
|
7394
7904
|
try {
|
|
7395
|
-
await
|
|
7905
|
+
await stat8(rawPath);
|
|
7396
7906
|
skipped.push("raw/articles/example-source.md");
|
|
7397
7907
|
} catch {
|
|
7398
|
-
await
|
|
7399
|
-
await
|
|
7908
|
+
await mkdir14(join40(rawPath, ".."), { recursive: true });
|
|
7909
|
+
await writeFile15(rawPath, EXAMPLE_RAW, "utf8");
|
|
7400
7910
|
created.push("raw/articles/example-source.md");
|
|
7401
7911
|
}
|
|
7402
7912
|
if (created.length > 0) {
|
|
@@ -7418,9 +7928,9 @@ async function runSeed(input) {
|
|
|
7418
7928
|
}
|
|
7419
7929
|
|
|
7420
7930
|
// src/commands/canvas.ts
|
|
7421
|
-
import { readFile as
|
|
7422
|
-
import { existsSync as
|
|
7423
|
-
import { join as
|
|
7931
|
+
import { readFile as readFile26, writeFile as writeFile16 } from "fs/promises";
|
|
7932
|
+
import { existsSync as existsSync16 } from "fs";
|
|
7933
|
+
import { join as join41 } from "path";
|
|
7424
7934
|
var NODE_WIDTH = 240;
|
|
7425
7935
|
var NODE_HEIGHT = 60;
|
|
7426
7936
|
var COLUMN_SPACING = 400;
|
|
@@ -7498,8 +8008,8 @@ function buildCanvasEdges(adjacency) {
|
|
|
7498
8008
|
return edges;
|
|
7499
8009
|
}
|
|
7500
8010
|
async function runCanvasGenerate(input) {
|
|
7501
|
-
const graphPath = input.graphPath ??
|
|
7502
|
-
if (!
|
|
8011
|
+
const graphPath = input.graphPath ?? join41(input.vault, ".skillwiki", "graph.json");
|
|
8012
|
+
if (!existsSync16(graphPath)) {
|
|
7503
8013
|
return {
|
|
7504
8014
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
7505
8015
|
result: err("FILE_NOT_FOUND", {
|
|
@@ -7510,7 +8020,7 @@ async function runCanvasGenerate(input) {
|
|
|
7510
8020
|
}
|
|
7511
8021
|
let raw;
|
|
7512
8022
|
try {
|
|
7513
|
-
raw = await
|
|
8023
|
+
raw = await readFile26(graphPath, "utf8");
|
|
7514
8024
|
} catch (e) {
|
|
7515
8025
|
return {
|
|
7516
8026
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
@@ -7536,9 +8046,9 @@ async function runCanvasGenerate(input) {
|
|
|
7536
8046
|
const nodes = buildCanvasNodes(paths);
|
|
7537
8047
|
const edges = buildCanvasEdges(graph.adjacency);
|
|
7538
8048
|
const canvas = { nodes, edges };
|
|
7539
|
-
const outPath =
|
|
8049
|
+
const outPath = join41(input.vault, "vault-graph.canvas");
|
|
7540
8050
|
try {
|
|
7541
|
-
await
|
|
8051
|
+
await writeFile16(outPath, JSON.stringify(canvas, null, 2));
|
|
7542
8052
|
} catch (e) {
|
|
7543
8053
|
return {
|
|
7544
8054
|
exitCode: ExitCode.WRITE_FAILED,
|
|
@@ -7558,8 +8068,8 @@ written: ${outPath}`
|
|
|
7558
8068
|
}
|
|
7559
8069
|
|
|
7560
8070
|
// src/commands/query.ts
|
|
7561
|
-
import { readFile as
|
|
7562
|
-
import { join as
|
|
8071
|
+
import { readFile as readFile27, stat as stat9 } from "fs/promises";
|
|
8072
|
+
import { join as join42 } from "path";
|
|
7563
8073
|
var W_KEYWORD = 2;
|
|
7564
8074
|
var W_SOURCE_OVERLAP = 4;
|
|
7565
8075
|
var W_WIKILINK = 3;
|
|
@@ -7680,10 +8190,10 @@ function computeKeywordScore(terms, title, tags, body) {
|
|
|
7680
8190
|
return score;
|
|
7681
8191
|
}
|
|
7682
8192
|
async function loadOrBuildGraph(vault) {
|
|
7683
|
-
const graphPath =
|
|
8193
|
+
const graphPath = join42(vault, ".skillwiki", "graph.json");
|
|
7684
8194
|
let needsBuild = false;
|
|
7685
8195
|
try {
|
|
7686
|
-
const fileStat = await
|
|
8196
|
+
const fileStat = await stat9(graphPath);
|
|
7687
8197
|
const ageHours = (Date.now() - fileStat.mtimeMs) / (1e3 * 60 * 60);
|
|
7688
8198
|
if (ageHours > 24) needsBuild = true;
|
|
7689
8199
|
} catch {
|
|
@@ -7694,7 +8204,7 @@ async function loadOrBuildGraph(vault) {
|
|
|
7694
8204
|
if (buildResult.exitCode !== 0) return null;
|
|
7695
8205
|
}
|
|
7696
8206
|
try {
|
|
7697
|
-
const raw = await
|
|
8207
|
+
const raw = await readFile27(graphPath, "utf8");
|
|
7698
8208
|
return JSON.parse(raw);
|
|
7699
8209
|
} catch {
|
|
7700
8210
|
return null;
|
|
@@ -7702,14 +8212,14 @@ async function loadOrBuildGraph(vault) {
|
|
|
7702
8212
|
}
|
|
7703
8213
|
|
|
7704
8214
|
// src/utils/auto-commit.ts
|
|
7705
|
-
import { existsSync as
|
|
7706
|
-
import { join as
|
|
8215
|
+
import { existsSync as existsSync17 } from "fs";
|
|
8216
|
+
import { join as join43 } from "path";
|
|
7707
8217
|
async function postCommit(vault, exitCode) {
|
|
7708
8218
|
if (exitCode !== 0) return;
|
|
7709
8219
|
const home = process.env.HOME ?? "";
|
|
7710
8220
|
const dotenv = await parseDotenvFile(configPath(home));
|
|
7711
8221
|
if (dotenv["AUTO_COMMIT"] === "false") return;
|
|
7712
|
-
if (!
|
|
8222
|
+
if (!existsSync17(join43(vault, ".git"))) return;
|
|
7713
8223
|
const lastOps = readLastOp(vault);
|
|
7714
8224
|
if (lastOps.length === 0) return;
|
|
7715
8225
|
const porcelain = git(vault, ["status", "--porcelain"]);
|
|
@@ -7760,7 +8270,7 @@ program.command("validate <file>").description("validate vault page frontmatter
|
|
|
7760
8270
|
emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
|
|
7761
8271
|
});
|
|
7762
8272
|
program.command("graph").description("graph subcommands").command("build <vault>").option("--out <path>", "graph output path (default: <vault>/.skillwiki/graph.json)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
7763
|
-
const out = opts.out ??
|
|
8273
|
+
const out = opts.out ?? join44(vault, ".skillwiki", "graph.json");
|
|
7764
8274
|
emit(await runGraphBuild({ vault, out }), vault);
|
|
7765
8275
|
});
|
|
7766
8276
|
var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
|
|
@@ -7886,6 +8396,11 @@ program.command("log-rotate [vault]").description("rotate or trim the vault log
|
|
|
7886
8396
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
7887
8397
|
else emit(await runLogRotate({ vault: v.vault, threshold: opts.threshold, apply: !!opts.apply }), v.vault);
|
|
7888
8398
|
});
|
|
8399
|
+
program.command("log-append [vault]").description("append a single entry to the vault log under a short advisory lock").requiredOption("--content <text>", "log entry text to append").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
8400
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
8401
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
8402
|
+
else emit(await runLogAppend({ vault: v.vault, content: opts.content }), v.vault);
|
|
8403
|
+
});
|
|
7889
8404
|
program.command("lint [vault]").description("run all vault health checks").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("--fix", "auto-fix supported lint violations").option("--only <bucket>", "run only the specified lint bucket").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
7890
8405
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
7891
8406
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|