skillwiki 0.5.4 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +776 -204
- package/package.json +1 -1
- package/skills/.claude-plugin/plugin.json +1 -1
- package/skills/.codex-plugin/plugin.json +1 -1
- package/skills/package.json +1 -1
- package/skills/wiki-sync/SKILL.md +115 -16
package/dist/cli.js
CHANGED
|
@@ -8,8 +8,8 @@ import {
|
|
|
8
8
|
} from "./chunk-TPS5XD2J.js";
|
|
9
9
|
|
|
10
10
|
// src/cli.ts
|
|
11
|
-
import { readFileSync as
|
|
12
|
-
import { join as
|
|
11
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
12
|
+
import { join as join41 } from "path";
|
|
13
13
|
import { Command as Command2 } from "commander";
|
|
14
14
|
|
|
15
15
|
// ../shared/src/exit-codes.ts
|
|
@@ -61,7 +61,8 @@ var ExitCode = {
|
|
|
61
61
|
BACKUP_SYNC_FAILED: 44,
|
|
62
62
|
BACKUP_RESTORE_CONFLICTS: 45,
|
|
63
63
|
USAGE: 46,
|
|
64
|
-
BODY_TRUNCATION_GUARD: 47
|
|
64
|
+
BODY_TRUNCATION_GUARD: 47,
|
|
65
|
+
SYNC_LOCK_HELD: 48
|
|
65
66
|
};
|
|
66
67
|
|
|
67
68
|
// ../shared/src/json-output.ts
|
|
@@ -3439,10 +3440,9 @@ async function runConfigPath(input) {
|
|
|
3439
3440
|
}
|
|
3440
3441
|
|
|
3441
3442
|
// src/commands/doctor.ts
|
|
3442
|
-
import { existsSync as
|
|
3443
|
-
import { join as
|
|
3444
|
-
import { execSync } from "child_process";
|
|
3445
|
-
import { platform } from "os";
|
|
3443
|
+
import { existsSync as existsSync7, lstatSync, readlinkSync, readdirSync, statSync as statSync2 } from "fs";
|
|
3444
|
+
import { join as join24, resolve as resolve4 } from "path";
|
|
3445
|
+
import { execSync as execSync2 } from "child_process";
|
|
3446
3446
|
|
|
3447
3447
|
// src/utils/auto-update.ts
|
|
3448
3448
|
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
|
|
@@ -3470,9 +3470,9 @@ function readCacheRaw(home) {
|
|
|
3470
3470
|
function readCache(home) {
|
|
3471
3471
|
const cache = readCacheRaw(home);
|
|
3472
3472
|
if (!cache) return { cache: null, hasUpdate: false, isStale: true };
|
|
3473
|
-
const
|
|
3473
|
+
const isStale2 = Date.now() - cache.lastCheck >= CHECK_INTERVAL_MS;
|
|
3474
3474
|
const hasUpdate = !!cache.latestVersion && semverGt(cache.latestVersion, cache.currentVersion);
|
|
3475
|
-
return { cache, hasUpdate, isStale };
|
|
3475
|
+
return { cache, hasUpdate, isStale: isStale2 };
|
|
3476
3476
|
}
|
|
3477
3477
|
function writeCache(home, cache) {
|
|
3478
3478
|
const p = cachePath(home);
|
|
@@ -3492,8 +3492,8 @@ function isDisabled() {
|
|
|
3492
3492
|
}
|
|
3493
3493
|
function triggerAutoUpdate(home, currentVersion) {
|
|
3494
3494
|
if (isDisabled()) return;
|
|
3495
|
-
const { isStale } = readCache(home);
|
|
3496
|
-
if (!
|
|
3495
|
+
const { isStale: isStale2 } = readCache(home);
|
|
3496
|
+
if (!isStale2) return;
|
|
3497
3497
|
const bgScript = new URL("../auto-update-bg.js", import.meta.url).pathname;
|
|
3498
3498
|
if (!existsSync5(bgScript)) return;
|
|
3499
3499
|
const child = spawn(process.execPath, [bgScript, home, currentVersion], {
|
|
@@ -3526,6 +3526,208 @@ function findPlugin(home, key = PLUGIN_KEY) {
|
|
|
3526
3526
|
return entries[0];
|
|
3527
3527
|
}
|
|
3528
3528
|
|
|
3529
|
+
// src/utils/s3-mount-health.ts
|
|
3530
|
+
import { execSync } from "child_process";
|
|
3531
|
+
import { platform } from "os";
|
|
3532
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3, readFileSync as readFile17 } from "fs";
|
|
3533
|
+
import { join as join23 } from "path";
|
|
3534
|
+
var OS = platform();
|
|
3535
|
+
function findRcloneMountPid() {
|
|
3536
|
+
try {
|
|
3537
|
+
const out = execSync("pgrep -f 'rclone.*mount'", {
|
|
3538
|
+
encoding: "utf8",
|
|
3539
|
+
timeout: 2e3,
|
|
3540
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3541
|
+
}).trim();
|
|
3542
|
+
const pids = out.split("\n").filter(Boolean);
|
|
3543
|
+
if (pids.length === 0) return null;
|
|
3544
|
+
return parseInt(pids[0], 10);
|
|
3545
|
+
} catch {
|
|
3546
|
+
try {
|
|
3547
|
+
const out = execSync("ps aux", { encoding: "utf8", timeout: 2e3, stdio: ["pipe", "pipe", "pipe"] });
|
|
3548
|
+
for (const line of out.split("\n")) {
|
|
3549
|
+
if (line.includes("rclone") && line.includes("mount") && !line.includes("grep")) {
|
|
3550
|
+
const parts = line.trim().split(/\s+/);
|
|
3551
|
+
if (parts.length >= 2) return parseInt(parts[1], 10);
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
} catch {
|
|
3555
|
+
}
|
|
3556
|
+
return null;
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
function parseRcloneFlags(pid) {
|
|
3560
|
+
const flags = /* @__PURE__ */ new Map();
|
|
3561
|
+
try {
|
|
3562
|
+
const args = getRcloneArgs(pid);
|
|
3563
|
+
for (let i = 0; i < args.length; i++) {
|
|
3564
|
+
const arg = args[i];
|
|
3565
|
+
if (arg.startsWith("--") && arg.includes("=")) {
|
|
3566
|
+
const eq = arg.indexOf("=");
|
|
3567
|
+
flags.set(arg.slice(0, eq), arg.slice(eq + 1));
|
|
3568
|
+
} else if (arg.startsWith("--")) {
|
|
3569
|
+
const next = args[i + 1];
|
|
3570
|
+
if (next && !next.startsWith("-")) {
|
|
3571
|
+
flags.set(arg, next);
|
|
3572
|
+
i++;
|
|
3573
|
+
} else {
|
|
3574
|
+
flags.set(arg, "");
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
} catch {
|
|
3579
|
+
}
|
|
3580
|
+
return flags;
|
|
3581
|
+
}
|
|
3582
|
+
function getRcloneVersion() {
|
|
3583
|
+
try {
|
|
3584
|
+
const out = execSync("rclone version", {
|
|
3585
|
+
encoding: "utf8",
|
|
3586
|
+
timeout: 3e3,
|
|
3587
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3588
|
+
});
|
|
3589
|
+
const match = out.match(/rclone\s+v(\d+)\.(\d+)\.(\d+)/i);
|
|
3590
|
+
if (!match) return null;
|
|
3591
|
+
return {
|
|
3592
|
+
major: parseInt(match[1], 10),
|
|
3593
|
+
minor: parseInt(match[2], 10),
|
|
3594
|
+
patch: parseInt(match[3], 10),
|
|
3595
|
+
raw: out.split("\n")[0].trim()
|
|
3596
|
+
};
|
|
3597
|
+
} catch {
|
|
3598
|
+
return null;
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
function extractRcloneFs(args) {
|
|
3602
|
+
let foundMount = false;
|
|
3603
|
+
for (const arg of args) {
|
|
3604
|
+
if (arg === "mount") {
|
|
3605
|
+
foundMount = true;
|
|
3606
|
+
continue;
|
|
3607
|
+
}
|
|
3608
|
+
if (foundMount && arg.includes(":") && !arg.startsWith("-") && !arg.startsWith("/")) {
|
|
3609
|
+
return arg;
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
return null;
|
|
3613
|
+
}
|
|
3614
|
+
function getRcloneArgs(pid) {
|
|
3615
|
+
try {
|
|
3616
|
+
if (OS === "linux") {
|
|
3617
|
+
const raw = readFileSync6(`/proc/${pid}/cmdline`);
|
|
3618
|
+
return new TextDecoder().decode(raw).split("\0").filter(Boolean);
|
|
3619
|
+
} else {
|
|
3620
|
+
const out = execSync(`ps -o args= -p ${pid}`, {
|
|
3621
|
+
encoding: "utf8",
|
|
3622
|
+
timeout: 2e3,
|
|
3623
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3624
|
+
}).trim();
|
|
3625
|
+
return out.split(/\s+/);
|
|
3626
|
+
}
|
|
3627
|
+
} catch {
|
|
3628
|
+
return [];
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
function queryRcloneRC(rcAddr, fs) {
|
|
3632
|
+
try {
|
|
3633
|
+
const payload = JSON.stringify({ fs });
|
|
3634
|
+
const out = execSync(
|
|
3635
|
+
`curl -s --max-time 3 -X POST "http://${rcAddr}/vfs/stats" -H "Content-Type: application/json" -d '${payload}' 2>/dev/null`,
|
|
3636
|
+
{ encoding: "utf8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }
|
|
3637
|
+
);
|
|
3638
|
+
if (!out.trim()) return null;
|
|
3639
|
+
const data = JSON.parse(out);
|
|
3640
|
+
if (data.status && data.status >= 400) {
|
|
3641
|
+
return { error: data.error || `RC error (status ${data.status})`, erroredFiles: 0, uploadsInProgress: 0, uploadsQueued: 0, outOfSpace: false, bytesUsed: 0, files: 0, totalSize: "unknown" };
|
|
3642
|
+
}
|
|
3643
|
+
const dc = data.diskCache || {};
|
|
3644
|
+
return {
|
|
3645
|
+
erroredFiles: dc.erroredFiles ?? 0,
|
|
3646
|
+
uploadsInProgress: dc.uploadsInProgress ?? 0,
|
|
3647
|
+
uploadsQueued: dc.uploadsQueued ?? 0,
|
|
3648
|
+
outOfSpace: dc.outOfSpace ?? false,
|
|
3649
|
+
bytesUsed: dc.bytesUsed ?? 0,
|
|
3650
|
+
files: dc.files ?? 0,
|
|
3651
|
+
totalSize: data.totalSize || "unknown"
|
|
3652
|
+
};
|
|
3653
|
+
} catch {
|
|
3654
|
+
return { error: "RC endpoint unreachable", erroredFiles: 0, uploadsInProgress: 0, uploadsQueued: 0, outOfSpace: false, bytesUsed: 0, files: 0, totalSize: "unknown" };
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3657
|
+
function detectFuseMount(vaultPath) {
|
|
3658
|
+
try {
|
|
3659
|
+
if (OS === "linux") {
|
|
3660
|
+
const mounts = readFileSync6("/proc/mounts", "utf8");
|
|
3661
|
+
let best = null;
|
|
3662
|
+
for (const line of mounts.split("\n")) {
|
|
3663
|
+
const parts = line.split(" ");
|
|
3664
|
+
if (parts.length < 3) continue;
|
|
3665
|
+
const point = parts[1];
|
|
3666
|
+
const fs = parts[2];
|
|
3667
|
+
if (vaultPath.startsWith(point) && (!best || point.length > best.point.length)) {
|
|
3668
|
+
best = { point, fs };
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
if (best && best.fs.includes("fuse")) return { mountPoint: best.point, fsType: best.fs };
|
|
3672
|
+
} else if (OS === "darwin") {
|
|
3673
|
+
const out = execSync("mount", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
3674
|
+
let best = null;
|
|
3675
|
+
for (const line of out.split("\n")) {
|
|
3676
|
+
const match = line.match(/^(\S+) on (\S+) \((.*?)\)/);
|
|
3677
|
+
if (!match) continue;
|
|
3678
|
+
const point = match[2];
|
|
3679
|
+
const opts = match[3];
|
|
3680
|
+
if (opts.includes("fuse") && vaultPath.startsWith(point) && (!best || point.length > best.point.length)) {
|
|
3681
|
+
best = { point, fsType: `fuse.${match[1].split(":")[0] || "unknown"}` };
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
3684
|
+
if (best) return best;
|
|
3685
|
+
}
|
|
3686
|
+
} catch {
|
|
3687
|
+
}
|
|
3688
|
+
return null;
|
|
3689
|
+
}
|
|
3690
|
+
function writeTest(dir) {
|
|
3691
|
+
const testFile = join23(dir, `.doctor-write-test-${process.pid}.tmp`);
|
|
3692
|
+
const payload = `skillwiki doctor write test \u2014 ${Date.now()} \u2014 ${Math.random().toString(36).slice(2)}`;
|
|
3693
|
+
const start = Date.now();
|
|
3694
|
+
try {
|
|
3695
|
+
writeFileSync4(testFile, payload, "utf8");
|
|
3696
|
+
} catch (e) {
|
|
3697
|
+
return { success: false, writeMs: Date.now() - start, readMs: 0, size: 0, error: `write failed: ${e.message}` };
|
|
3698
|
+
}
|
|
3699
|
+
const writeMs = Date.now() - start;
|
|
3700
|
+
const readStart = Date.now();
|
|
3701
|
+
try {
|
|
3702
|
+
const back = readFile17(testFile, "utf8");
|
|
3703
|
+
const readMs = Date.now() - readStart;
|
|
3704
|
+
if (back !== payload) {
|
|
3705
|
+
try {
|
|
3706
|
+
unlinkSync3(testFile);
|
|
3707
|
+
} catch {
|
|
3708
|
+
}
|
|
3709
|
+
return { success: false, writeMs, readMs, size: Buffer.byteLength(payload, "utf8"), error: "content mismatch \u2014 wrote and read-back differ" };
|
|
3710
|
+
}
|
|
3711
|
+
} catch (e) {
|
|
3712
|
+
try {
|
|
3713
|
+
unlinkSync3(testFile);
|
|
3714
|
+
} catch {
|
|
3715
|
+
}
|
|
3716
|
+
return { success: false, writeMs, readMs: Date.now() - readStart, size: 0, error: `read failed: ${e.message}` };
|
|
3717
|
+
}
|
|
3718
|
+
try {
|
|
3719
|
+
unlinkSync3(testFile);
|
|
3720
|
+
} catch {
|
|
3721
|
+
}
|
|
3722
|
+
return { success: true, writeMs, readMs: Date.now() - readStart, size: Buffer.byteLength(payload, "utf8") };
|
|
3723
|
+
}
|
|
3724
|
+
var FLAG_THRESHOLDS = {
|
|
3725
|
+
"--vfs-write-back": { min: 15, unit: "s", label: "VFS write-back window" },
|
|
3726
|
+
"--vfs-write-wait": { min: 10, unit: "s", label: "VFS write-wait" },
|
|
3727
|
+
"--vfs-cache-max-age": { min: 24, unit: "h", label: "VFS cache max age" }
|
|
3728
|
+
};
|
|
3729
|
+
var MIN_RCLONE_VERSION = { major: 1, minor: 65, patch: 0 };
|
|
3730
|
+
|
|
3529
3731
|
// src/commands/doctor.ts
|
|
3530
3732
|
function check(status, id, label, detail) {
|
|
3531
3733
|
return { id, label, status, detail };
|
|
@@ -3544,7 +3746,7 @@ function detectCliChannels(argv, home) {
|
|
|
3544
3746
|
channels.push({ name: "dev", path: devPath, isDevLink: true });
|
|
3545
3747
|
}
|
|
3546
3748
|
try {
|
|
3547
|
-
const whichOut =
|
|
3749
|
+
const whichOut = execSync2("which skillwiki 2>/dev/null", { encoding: "utf8" }).trim();
|
|
3548
3750
|
if (whichOut) {
|
|
3549
3751
|
const isDev = isDevSymlink(whichOut);
|
|
3550
3752
|
if (!channels.some((c) => c.path === resolve4(whichOut))) {
|
|
@@ -3555,13 +3757,13 @@ function detectCliChannels(argv, home) {
|
|
|
3555
3757
|
}
|
|
3556
3758
|
const plugin = findPlugin(home);
|
|
3557
3759
|
if (plugin) {
|
|
3558
|
-
const pluginBin =
|
|
3559
|
-
if (
|
|
3760
|
+
const pluginBin = join24(plugin.installPath, "bin", "skillwiki");
|
|
3761
|
+
if (existsSync7(pluginBin)) {
|
|
3560
3762
|
channels.push({ name: "plugin", path: pluginBin, isDevLink: false });
|
|
3561
3763
|
}
|
|
3562
3764
|
}
|
|
3563
|
-
const installBin =
|
|
3564
|
-
if (
|
|
3765
|
+
const installBin = join24(home, ".claude", "skills", "bin", "skillwiki");
|
|
3766
|
+
if (existsSync7(installBin)) {
|
|
3565
3767
|
channels.push({ name: "install", path: installBin, isDevLink: false });
|
|
3566
3768
|
}
|
|
3567
3769
|
return channels;
|
|
@@ -3613,7 +3815,7 @@ function checkCliChannels(argv, home) {
|
|
|
3613
3815
|
}
|
|
3614
3816
|
async function checkConfigFile(home) {
|
|
3615
3817
|
const cfgPath = configPath(home);
|
|
3616
|
-
if (!
|
|
3818
|
+
if (!existsSync7(cfgPath)) {
|
|
3617
3819
|
return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
|
|
3618
3820
|
}
|
|
3619
3821
|
try {
|
|
@@ -3628,7 +3830,7 @@ function checkWikiPathExists(resolvedPath) {
|
|
|
3628
3830
|
if (resolvedPath === void 0) {
|
|
3629
3831
|
return check("error", "wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
3630
3832
|
}
|
|
3631
|
-
if (
|
|
3833
|
+
if (existsSync7(resolvedPath) && statSync2(resolvedPath).isDirectory()) {
|
|
3632
3834
|
return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
|
|
3633
3835
|
}
|
|
3634
3836
|
return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
|
|
@@ -3637,13 +3839,13 @@ function checkVaultStructure(resolvedPath) {
|
|
|
3637
3839
|
if (resolvedPath === void 0) {
|
|
3638
3840
|
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
3639
3841
|
}
|
|
3640
|
-
if (!
|
|
3842
|
+
if (!existsSync7(resolvedPath)) {
|
|
3641
3843
|
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
|
|
3642
3844
|
}
|
|
3643
3845
|
const missing = [];
|
|
3644
|
-
if (!
|
|
3846
|
+
if (!existsSync7(join24(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
|
|
3645
3847
|
for (const dir of ["raw", "entities", "concepts", "meta"]) {
|
|
3646
|
-
if (!
|
|
3848
|
+
if (!existsSync7(join24(resolvedPath, dir))) missing.push(dir + "/");
|
|
3647
3849
|
}
|
|
3648
3850
|
if (missing.length === 0) {
|
|
3649
3851
|
return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
|
|
@@ -3651,8 +3853,8 @@ function checkVaultStructure(resolvedPath) {
|
|
|
3651
3853
|
return check("warn", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to add CodeWiki structure`);
|
|
3652
3854
|
}
|
|
3653
3855
|
function checkSkillsInstalled(home, cwd) {
|
|
3654
|
-
const srcDir = cwd ?
|
|
3655
|
-
if (srcDir &&
|
|
3856
|
+
const srcDir = cwd ? join24(cwd, "packages", "skills") : void 0;
|
|
3857
|
+
if (srcDir && existsSync7(srcDir)) {
|
|
3656
3858
|
const found = findSkillMd(srcDir);
|
|
3657
3859
|
if (found.length > 0) {
|
|
3658
3860
|
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (source)`);
|
|
@@ -3665,8 +3867,8 @@ function checkSkillsInstalled(home, cwd) {
|
|
|
3665
3867
|
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (plugin v${plugin.version})`);
|
|
3666
3868
|
}
|
|
3667
3869
|
}
|
|
3668
|
-
const skillsDir =
|
|
3669
|
-
if (
|
|
3870
|
+
const skillsDir = join24(home, ".claude", "skills");
|
|
3871
|
+
if (existsSync7(skillsDir)) {
|
|
3670
3872
|
const found = findSkillMd(skillsDir);
|
|
3671
3873
|
if (found.length > 0) {
|
|
3672
3874
|
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (CLI install)`);
|
|
@@ -3676,10 +3878,10 @@ function checkSkillsInstalled(home, cwd) {
|
|
|
3676
3878
|
}
|
|
3677
3879
|
function checkDuplicateSkills(home) {
|
|
3678
3880
|
const plugin = findPlugin(home);
|
|
3679
|
-
const skillsDir =
|
|
3881
|
+
const skillsDir = join24(home, ".claude", "skills");
|
|
3680
3882
|
const agentSkillDirs = [
|
|
3681
|
-
{ label: "~/.codex/skills/", path:
|
|
3682
|
-
{ label: "~/.agents/skills/", path:
|
|
3883
|
+
{ label: "~/.codex/skills/", path: join24(home, ".codex", "skills") },
|
|
3884
|
+
{ label: "~/.agents/skills/", path: join24(home, ".agents", "skills") }
|
|
3683
3885
|
];
|
|
3684
3886
|
if (!plugin) {
|
|
3685
3887
|
return check("pass", "skills_duplicate", "Skills not duplicated", "Single install channel");
|
|
@@ -3756,8 +3958,8 @@ async function checkProfiles(home) {
|
|
|
3756
3958
|
}
|
|
3757
3959
|
async function checkProjectLocalOverride(cwd) {
|
|
3758
3960
|
const dir = cwd ?? process.cwd();
|
|
3759
|
-
const envPath =
|
|
3760
|
-
if (
|
|
3961
|
+
const envPath = join24(dir, ".skillwiki", ".env");
|
|
3962
|
+
if (existsSync7(envPath)) {
|
|
3761
3963
|
return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
|
|
3762
3964
|
}
|
|
3763
3965
|
return check("pass", "project_local", "Project-local config", "None");
|
|
@@ -3766,17 +3968,17 @@ function checkVaultGitRemote(resolvedPath) {
|
|
|
3766
3968
|
if (resolvedPath === void 0) {
|
|
3767
3969
|
return check("error", "vault_git_remote", "Vault git remote", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
3768
3970
|
}
|
|
3769
|
-
if (!
|
|
3971
|
+
if (!existsSync7(join24(resolvedPath, ".git"))) {
|
|
3770
3972
|
return check("warn", "vault_git_remote", "Vault git remote", "Vault is not a git repository \u2014 sync features unavailable");
|
|
3771
3973
|
}
|
|
3772
3974
|
try {
|
|
3773
|
-
const remote =
|
|
3975
|
+
const remote = execSync2("git remote", { cwd: resolvedPath, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
3774
3976
|
if (!remote) {
|
|
3775
3977
|
return check("warn", "vault_git_remote", "Vault git remote", "No remote configured \u2014 push/pull unavailable");
|
|
3776
3978
|
}
|
|
3777
3979
|
let branch = "(no commits yet)";
|
|
3778
3980
|
try {
|
|
3779
|
-
branch =
|
|
3981
|
+
branch = execSync2("git rev-parse --abbrev-ref HEAD", { cwd: resolvedPath, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
3780
3982
|
} catch {
|
|
3781
3983
|
}
|
|
3782
3984
|
return check("pass", "vault_git_remote", "Vault git remote", `Remote: ${remote.split("\n")[0]}, branch: ${branch}`);
|
|
@@ -3789,9 +3991,9 @@ function checkObsidianTemplates(resolvedPath) {
|
|
|
3789
3991
|
return check("error", "obsidian_templates", "Obsidian templates", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
3790
3992
|
}
|
|
3791
3993
|
const missing = [];
|
|
3792
|
-
if (!
|
|
3793
|
-
if (!
|
|
3794
|
-
if (!
|
|
3994
|
+
if (!existsSync7(join24(resolvedPath, "_Templates"))) missing.push("_Templates/");
|
|
3995
|
+
if (!existsSync7(join24(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
|
|
3996
|
+
if (!existsSync7(join24(resolvedPath, ".obsidian", "app.json"))) missing.push(".obsidian/app.json");
|
|
3795
3997
|
if (missing.length === 0) {
|
|
3796
3998
|
return check("pass", "obsidian_templates", "Obsidian templates", "Template folder and config present");
|
|
3797
3999
|
}
|
|
@@ -3801,8 +4003,8 @@ function checkDotStoreClean(resolvedPath) {
|
|
|
3801
4003
|
if (resolvedPath === void 0) {
|
|
3802
4004
|
return check("error", "dsstore_clean", "No .DS_Store in raw/", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
3803
4005
|
}
|
|
3804
|
-
const rawDir =
|
|
3805
|
-
if (!
|
|
4006
|
+
const rawDir = join24(resolvedPath, "raw");
|
|
4007
|
+
if (!existsSync7(rawDir)) {
|
|
3806
4008
|
return check("pass", "dsstore_clean", "No .DS_Store in raw/", "raw/ directory not found \u2014 check skipped");
|
|
3807
4009
|
}
|
|
3808
4010
|
const found = [];
|
|
@@ -3817,7 +4019,7 @@ function checkDotStoreClean(resolvedPath) {
|
|
|
3817
4019
|
if (entry.name === ".DS_Store") {
|
|
3818
4020
|
found.push(rel ? `${rel}/.DS_Store` : ".DS_Store");
|
|
3819
4021
|
} else if (entry.isDirectory()) {
|
|
3820
|
-
walk2(
|
|
4022
|
+
walk2(join24(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
|
|
3821
4023
|
}
|
|
3822
4024
|
}
|
|
3823
4025
|
})(rawDir, "");
|
|
@@ -3830,12 +4032,12 @@ function checkSyncLastPush(resolvedPath) {
|
|
|
3830
4032
|
if (resolvedPath === void 0) {
|
|
3831
4033
|
return check("error", "sync_last_push", "Vault sync recency", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
3832
4034
|
}
|
|
3833
|
-
if (!
|
|
4035
|
+
if (!existsSync7(join24(resolvedPath, ".git"))) {
|
|
3834
4036
|
return check("pass", "sync_last_push", "Vault sync recency", "No git repo \u2014 sync check skipped");
|
|
3835
4037
|
}
|
|
3836
4038
|
let timestamp;
|
|
3837
4039
|
try {
|
|
3838
|
-
const out =
|
|
4040
|
+
const out = execSync2("git log -1 --format=%ct origin/HEAD", {
|
|
3839
4041
|
cwd: resolvedPath,
|
|
3840
4042
|
encoding: "utf8",
|
|
3841
4043
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -3843,7 +4045,7 @@ function checkSyncLastPush(resolvedPath) {
|
|
|
3843
4045
|
timestamp = parseInt(out, 10);
|
|
3844
4046
|
} catch {
|
|
3845
4047
|
try {
|
|
3846
|
-
const out =
|
|
4048
|
+
const out = execSync2("git log -1 --format=%ct HEAD", {
|
|
3847
4049
|
cwd: resolvedPath,
|
|
3848
4050
|
encoding: "utf8",
|
|
3849
4051
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -3862,56 +4064,23 @@ function checkSyncLastPush(resolvedPath) {
|
|
|
3862
4064
|
}
|
|
3863
4065
|
return check("pass", "sync_last_push", "Vault sync recency", `Last push: ${dateStr} (${daysSince2} day(s) ago)`);
|
|
3864
4066
|
}
|
|
3865
|
-
function detectFuseMount(vaultPath) {
|
|
3866
|
-
const os = platform();
|
|
3867
|
-
try {
|
|
3868
|
-
if (os === "linux") {
|
|
3869
|
-
const mounts = readFileSync6("/proc/mounts", "utf8");
|
|
3870
|
-
let best = null;
|
|
3871
|
-
for (const line of mounts.split("\n")) {
|
|
3872
|
-
const parts = line.split(" ");
|
|
3873
|
-
if (parts.length < 3) continue;
|
|
3874
|
-
const point = parts[1];
|
|
3875
|
-
const fs = parts[2];
|
|
3876
|
-
if (vaultPath.startsWith(point) && (!best || point.length > best.point.length)) {
|
|
3877
|
-
best = { point, fs };
|
|
3878
|
-
}
|
|
3879
|
-
}
|
|
3880
|
-
if (best && best.fs.includes("fuse")) return best.point;
|
|
3881
|
-
} else if (os === "darwin") {
|
|
3882
|
-
const out = execSync("mount", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
3883
|
-
let best = null;
|
|
3884
|
-
for (const line of out.split("\n")) {
|
|
3885
|
-
const match = line.match(/^(\S+) on (\S+) \((.*?)\)/);
|
|
3886
|
-
if (!match) continue;
|
|
3887
|
-
const point = match[2];
|
|
3888
|
-
const opts = match[3];
|
|
3889
|
-
if (opts.includes("fuse") && vaultPath.startsWith(point) && (!best || point.length > best.point.length)) {
|
|
3890
|
-
best = { point };
|
|
3891
|
-
}
|
|
3892
|
-
}
|
|
3893
|
-
if (best) return best.point;
|
|
3894
|
-
}
|
|
3895
|
-
} catch {
|
|
3896
|
-
}
|
|
3897
|
-
return null;
|
|
3898
|
-
}
|
|
3899
4067
|
function checkS3MountPerf(resolvedPath) {
|
|
3900
4068
|
if (resolvedPath === void 0) {
|
|
3901
4069
|
return check("pass", "s3_mount_perf", "S3 mount performance", "No vault path \u2014 check skipped");
|
|
3902
4070
|
}
|
|
3903
|
-
const
|
|
3904
|
-
if (!
|
|
4071
|
+
const fuse = detectFuseMount(resolvedPath);
|
|
4072
|
+
if (!fuse) {
|
|
3905
4073
|
return check("pass", "s3_mount_perf", "S3 mount performance", "local disk");
|
|
3906
4074
|
}
|
|
3907
|
-
const
|
|
3908
|
-
|
|
4075
|
+
const mountPoint = fuse.mountPoint;
|
|
4076
|
+
const conceptsDir = join24(resolvedPath, "concepts");
|
|
4077
|
+
if (!existsSync7(conceptsDir)) {
|
|
3909
4078
|
return check("pass", "s3_mount_perf", "S3 mount performance", `S3 FUSE mount (${mountPoint}), no concepts/ to benchmark`);
|
|
3910
4079
|
}
|
|
3911
4080
|
const start = Date.now();
|
|
3912
4081
|
let timedOut = false;
|
|
3913
4082
|
try {
|
|
3914
|
-
|
|
4083
|
+
execSync2(`rg -l "." "${conceptsDir}"`, {
|
|
3915
4084
|
timeout: 5e3,
|
|
3916
4085
|
encoding: "utf8",
|
|
3917
4086
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -3944,6 +4113,170 @@ function checkS3MountPerf(resolvedPath) {
|
|
|
3944
4113
|
`S3 FUSE mount, cache warm (rg scan: ${elapsed.toFixed(3)}s)`
|
|
3945
4114
|
);
|
|
3946
4115
|
}
|
|
4116
|
+
function checkRcloneFlagAudit(resolvedPath) {
|
|
4117
|
+
if (!resolvedPath) {
|
|
4118
|
+
return check("pass", "rclone_flags", "rclone VFS flags", "No vault path \u2014 check skipped");
|
|
4119
|
+
}
|
|
4120
|
+
const fuse = detectFuseMount(resolvedPath);
|
|
4121
|
+
if (!fuse) {
|
|
4122
|
+
return check("pass", "rclone_flags", "rclone VFS flags", "local disk \u2014 check skipped");
|
|
4123
|
+
}
|
|
4124
|
+
const pid = findRcloneMountPid();
|
|
4125
|
+
if (pid === null) {
|
|
4126
|
+
return check("warn", "rclone_flags", "rclone VFS flags", `S3 FUSE mount (${fuse.mountPoint}) but no rclone process found \u2014 cannot audit flags`);
|
|
4127
|
+
}
|
|
4128
|
+
const flags = parseRcloneFlags(pid);
|
|
4129
|
+
if (flags.size === 0) {
|
|
4130
|
+
return check("warn", "rclone_flags", "rclone VFS flags", `rclone PID ${pid} found but could not parse flags`);
|
|
4131
|
+
}
|
|
4132
|
+
const warnings = [];
|
|
4133
|
+
for (const [flag, threshold] of Object.entries(FLAG_THRESHOLDS)) {
|
|
4134
|
+
const raw = flags.get(flag);
|
|
4135
|
+
if (raw === void 0) {
|
|
4136
|
+
warnings.push(`${flag} not set (default may be unsafe)`);
|
|
4137
|
+
continue;
|
|
4138
|
+
}
|
|
4139
|
+
const value = parseFloat(raw);
|
|
4140
|
+
if (isNaN(value)) continue;
|
|
4141
|
+
let inSeconds = value;
|
|
4142
|
+
if (raw.endsWith("h")) inSeconds = value * 3600;
|
|
4143
|
+
else if (raw.endsWith("m")) inSeconds = value * 60;
|
|
4144
|
+
const thresholdSec = threshold.unit === "h" ? threshold.min * 3600 : threshold.unit === "m" ? threshold.min * 60 : threshold.min;
|
|
4145
|
+
if (inSeconds < thresholdSec) {
|
|
4146
|
+
warnings.push(`${flag}=${raw} (recommended \u2265${threshold.min}${threshold.unit})`);
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
const cacheMode = flags.get("--vfs-cache-mode");
|
|
4150
|
+
if (!cacheMode) {
|
|
4151
|
+
warnings.push("--vfs-cache-mode not set (recommended: full)");
|
|
4152
|
+
} else if (cacheMode !== "full") {
|
|
4153
|
+
warnings.push(`--vfs-cache-mode=${cacheMode} (recommended: full)`);
|
|
4154
|
+
}
|
|
4155
|
+
if (!flags.has("--log-file")) {
|
|
4156
|
+
warnings.push("--log-file not set \u2014 no rclone error log configured");
|
|
4157
|
+
}
|
|
4158
|
+
if (warnings.length > 0) {
|
|
4159
|
+
return check("warn", "rclone_flags", "rclone VFS flags", warnings.join("; "));
|
|
4160
|
+
}
|
|
4161
|
+
return check("pass", "rclone_flags", "rclone VFS flags", `PID ${pid}: all critical flags at safe values`);
|
|
4162
|
+
}
|
|
4163
|
+
function checkRcloneVersion(resolvedPath) {
|
|
4164
|
+
if (!resolvedPath) {
|
|
4165
|
+
return check("pass", "rclone_version", "rclone version", "No vault path \u2014 check skipped");
|
|
4166
|
+
}
|
|
4167
|
+
const fuse = detectFuseMount(resolvedPath);
|
|
4168
|
+
if (!fuse) {
|
|
4169
|
+
return check("pass", "rclone_version", "rclone version", "local disk \u2014 check skipped");
|
|
4170
|
+
}
|
|
4171
|
+
const ver = getRcloneVersion();
|
|
4172
|
+
if (!ver) {
|
|
4173
|
+
return check("warn", "rclone_version", "rclone version", "rclone not found on PATH \u2014 cannot verify version");
|
|
4174
|
+
}
|
|
4175
|
+
const min = MIN_RCLONE_VERSION;
|
|
4176
|
+
const tooOld = ver.major < min.major || ver.major === min.major && ver.minor < min.minor || ver.major === min.major && ver.minor === min.minor && ver.patch < min.patch;
|
|
4177
|
+
if (tooOld) {
|
|
4178
|
+
return check(
|
|
4179
|
+
"warn",
|
|
4180
|
+
"rclone_version",
|
|
4181
|
+
"rclone version",
|
|
4182
|
+
`${ver.raw} \u2014 upgrade to \u2265v${min.major}.${min.minor}.${min.patch} for --vfs-write-wait support (current version may silently ignore this flag)`
|
|
4183
|
+
);
|
|
4184
|
+
}
|
|
4185
|
+
return check("pass", "rclone_version", "rclone version", ver.raw);
|
|
4186
|
+
}
|
|
4187
|
+
function checkWriteTest(resolvedPath) {
|
|
4188
|
+
if (!resolvedPath) {
|
|
4189
|
+
return check("pass", "s3_write_test", "S3 write test", "No vault path \u2014 check skipped");
|
|
4190
|
+
}
|
|
4191
|
+
const fuse = detectFuseMount(resolvedPath);
|
|
4192
|
+
if (!fuse) {
|
|
4193
|
+
return check("pass", "s3_write_test", "S3 write test", "local disk \u2014 check skipped");
|
|
4194
|
+
}
|
|
4195
|
+
const conceptsDir = join24(resolvedPath, "concepts");
|
|
4196
|
+
if (!existsSync7(conceptsDir)) {
|
|
4197
|
+
return check("pass", "s3_write_test", "S3 write test", "no concepts/ dir to test \u2014 check skipped");
|
|
4198
|
+
}
|
|
4199
|
+
const result = writeTest(conceptsDir);
|
|
4200
|
+
if (result.success) {
|
|
4201
|
+
const totalMs = result.writeMs + result.readMs;
|
|
4202
|
+
if (totalMs > 3e3) {
|
|
4203
|
+
return check(
|
|
4204
|
+
"warn",
|
|
4205
|
+
"s3_write_test",
|
|
4206
|
+
"S3 write test",
|
|
4207
|
+
`write+read ${totalMs}ms (write ${result.writeMs}ms, read ${result.readMs}ms, ${result.size}B) \u2014 S3 mount is slow`
|
|
4208
|
+
);
|
|
4209
|
+
}
|
|
4210
|
+
return check(
|
|
4211
|
+
"pass",
|
|
4212
|
+
"s3_write_test",
|
|
4213
|
+
"S3 write test",
|
|
4214
|
+
`write+read ${totalMs}ms (write ${result.writeMs}ms, read ${result.readMs}ms)`
|
|
4215
|
+
);
|
|
4216
|
+
}
|
|
4217
|
+
return check(
|
|
4218
|
+
"warn",
|
|
4219
|
+
"s3_write_test",
|
|
4220
|
+
"S3 write test",
|
|
4221
|
+
`${result.error} \u2014 S3 mount may have a stale FUSE handle or write-back failure`
|
|
4222
|
+
);
|
|
4223
|
+
}
|
|
4224
|
+
function checkVfsCacheHealth(resolvedPath) {
|
|
4225
|
+
if (!resolvedPath) {
|
|
4226
|
+
return check("pass", "vfs_cache_health", "VFS cache health", "No vault path \u2014 check skipped");
|
|
4227
|
+
}
|
|
4228
|
+
const fuse = detectFuseMount(resolvedPath);
|
|
4229
|
+
if (!fuse) {
|
|
4230
|
+
return check("pass", "vfs_cache_health", "VFS cache health", "local disk \u2014 check skipped");
|
|
4231
|
+
}
|
|
4232
|
+
const pid = findRcloneMountPid();
|
|
4233
|
+
if (pid === null) {
|
|
4234
|
+
return check("warn", "vfs_cache_health", "VFS cache health", "no rclone process found \u2014 cannot query VFS stats");
|
|
4235
|
+
}
|
|
4236
|
+
const flags = parseRcloneFlags(pid);
|
|
4237
|
+
const rcAddr = flags.get("--rc-addr") || "127.0.0.1:5572";
|
|
4238
|
+
if (!flags.has("--rc")) {
|
|
4239
|
+
return check(
|
|
4240
|
+
"info",
|
|
4241
|
+
"vfs_cache_health",
|
|
4242
|
+
"VFS cache health",
|
|
4243
|
+
`rclone RC not enabled \u2014 add --rc --rc-addr ${rcAddr} to enable cache health monitoring`
|
|
4244
|
+
);
|
|
4245
|
+
}
|
|
4246
|
+
const args = getRcloneArgs(pid);
|
|
4247
|
+
const fs = extractRcloneFs(args) || "unknown:";
|
|
4248
|
+
const stats = queryRcloneRC(rcAddr, fs || "unknown:");
|
|
4249
|
+
if (!stats) {
|
|
4250
|
+
return check(
|
|
4251
|
+
"warn",
|
|
4252
|
+
"vfs_cache_health",
|
|
4253
|
+
"VFS cache health",
|
|
4254
|
+
`RC endpoint ${rcAddr} unreachable \u2014 is rclone --rc enabled?`
|
|
4255
|
+
);
|
|
4256
|
+
}
|
|
4257
|
+
if (stats.error) {
|
|
4258
|
+
return check("warn", "vfs_cache_health", "VFS cache health", stats.error);
|
|
4259
|
+
}
|
|
4260
|
+
const issues = [];
|
|
4261
|
+
if (stats.uploadsInProgress > 0) issues.push(`${stats.uploadsInProgress} upload(s) in progress`);
|
|
4262
|
+
if (stats.uploadsQueued > 10) issues.push(`${stats.uploadsQueued} upload(s) queued (backlog)`);
|
|
4263
|
+
if (stats.erroredFiles > 0) issues.push(`${stats.erroredFiles} errored file(s)`);
|
|
4264
|
+
if (stats.outOfSpace) issues.push("cache disk full");
|
|
4265
|
+
if (issues.length > 0) {
|
|
4266
|
+
return check(
|
|
4267
|
+
"warn",
|
|
4268
|
+
"vfs_cache_health",
|
|
4269
|
+
"VFS cache health",
|
|
4270
|
+
`${stats.files} files, ${stats.bytesUsed} bytes \u2014 ${issues.join("; ")}`
|
|
4271
|
+
);
|
|
4272
|
+
}
|
|
4273
|
+
return check(
|
|
4274
|
+
"pass",
|
|
4275
|
+
"vfs_cache_health",
|
|
4276
|
+
"VFS cache health",
|
|
4277
|
+
`${stats.files} files, ${(stats.bytesUsed / 1024 / 1024).toFixed(1)}MB \u2014 clean (0 errored, 0 pending)`
|
|
4278
|
+
);
|
|
4279
|
+
}
|
|
3947
4280
|
function findSkillMd(dir) {
|
|
3948
4281
|
const results = [];
|
|
3949
4282
|
let entries;
|
|
@@ -3954,9 +4287,9 @@ function findSkillMd(dir) {
|
|
|
3954
4287
|
}
|
|
3955
4288
|
for (const entry of entries) {
|
|
3956
4289
|
if (entry.isFile() && entry.name === "SKILL.md") {
|
|
3957
|
-
results.push(
|
|
4290
|
+
results.push(join24(dir, entry.name));
|
|
3958
4291
|
} else if (entry.isDirectory()) {
|
|
3959
|
-
results.push(...findSkillMd(
|
|
4292
|
+
results.push(...findSkillMd(join24(dir, entry.name)));
|
|
3960
4293
|
}
|
|
3961
4294
|
}
|
|
3962
4295
|
return results;
|
|
@@ -3970,7 +4303,7 @@ function findSkillNames(dir) {
|
|
|
3970
4303
|
return results;
|
|
3971
4304
|
}
|
|
3972
4305
|
for (const entry of entries) {
|
|
3973
|
-
if (entry.isDirectory() &&
|
|
4306
|
+
if (entry.isDirectory() && existsSync7(join24(dir, entry.name, "SKILL.md"))) {
|
|
3974
4307
|
results.push(entry.name);
|
|
3975
4308
|
}
|
|
3976
4309
|
}
|
|
@@ -3997,6 +4330,10 @@ async function runDoctor(input) {
|
|
|
3997
4330
|
checks.push(checkSyncLastPush(resolvedPath));
|
|
3998
4331
|
checks.push(checkDotStoreClean(resolvedPath));
|
|
3999
4332
|
checks.push(checkS3MountPerf(resolvedPath));
|
|
4333
|
+
checks.push(checkRcloneFlagAudit(resolvedPath));
|
|
4334
|
+
checks.push(checkRcloneVersion(resolvedPath));
|
|
4335
|
+
checks.push(checkWriteTest(resolvedPath));
|
|
4336
|
+
checks.push(checkVfsCacheHealth(resolvedPath));
|
|
4000
4337
|
checks.push(checkSkillsInstalled(input.home, input.cwd));
|
|
4001
4338
|
checks.push(checkDuplicateSkills(input.home));
|
|
4002
4339
|
checks.push(checkNpmUpdate(input.home, input.currentVersion));
|
|
@@ -4024,8 +4361,8 @@ async function runDoctor(input) {
|
|
|
4024
4361
|
}
|
|
4025
4362
|
|
|
4026
4363
|
// src/commands/archive.ts
|
|
4027
|
-
import { rename as rename5, mkdir as mkdir8, readFile as
|
|
4028
|
-
import { join as
|
|
4364
|
+
import { rename as rename5, mkdir as mkdir8, readFile as readFile18, writeFile as writeFile9 } from "fs/promises";
|
|
4365
|
+
import { join as join25, dirname as dirname9 } from "path";
|
|
4029
4366
|
async function runArchive(input) {
|
|
4030
4367
|
const scan = await scanVault(input.vault);
|
|
4031
4368
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
@@ -4041,13 +4378,13 @@ async function runArchive(input) {
|
|
|
4041
4378
|
}
|
|
4042
4379
|
if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
|
|
4043
4380
|
if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
|
|
4044
|
-
const archivePath =
|
|
4045
|
-
await mkdir8(dirname9(
|
|
4381
|
+
const archivePath = join25("_archive", relPath).replace(/\\/g, "/");
|
|
4382
|
+
await mkdir8(dirname9(join25(input.vault, archivePath)), { recursive: true });
|
|
4046
4383
|
let indexUpdated = false;
|
|
4047
4384
|
if (!isRaw) {
|
|
4048
|
-
const indexPath =
|
|
4385
|
+
const indexPath = join25(input.vault, "index.md");
|
|
4049
4386
|
try {
|
|
4050
|
-
const idx = await
|
|
4387
|
+
const idx = await readFile18(indexPath, "utf8");
|
|
4051
4388
|
const slug = relPath.replace(/\.md$/, "").split("/").pop();
|
|
4052
4389
|
const originalLines = idx.split("\n");
|
|
4053
4390
|
const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
|
|
@@ -4059,7 +4396,7 @@ async function runArchive(input) {
|
|
|
4059
4396
|
if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
|
|
4060
4397
|
}
|
|
4061
4398
|
}
|
|
4062
|
-
await rename5(
|
|
4399
|
+
await rename5(join25(input.vault, relPath), join25(input.vault, archivePath));
|
|
4063
4400
|
appendLastOp(input.vault, {
|
|
4064
4401
|
operation: "archive",
|
|
4065
4402
|
summary: `moved ${relPath} to ${archivePath}`,
|
|
@@ -4432,16 +4769,16 @@ ${newBody}`;
|
|
|
4432
4769
|
}
|
|
4433
4770
|
|
|
4434
4771
|
// src/commands/update.ts
|
|
4435
|
-
import { execSync as
|
|
4772
|
+
import { execSync as execSync3 } from "child_process";
|
|
4436
4773
|
import { readFileSync as readFileSync7 } from "fs";
|
|
4437
|
-
import { join as
|
|
4774
|
+
import { join as join26 } from "path";
|
|
4438
4775
|
function resolveGlobalSkillsRoot() {
|
|
4439
4776
|
try {
|
|
4440
|
-
const globalRoot =
|
|
4777
|
+
const globalRoot = execSync3("npm root -g", {
|
|
4441
4778
|
encoding: "utf8",
|
|
4442
4779
|
timeout: 5e3
|
|
4443
4780
|
}).trim();
|
|
4444
|
-
return
|
|
4781
|
+
return join26(globalRoot, "skillwiki", "skills");
|
|
4445
4782
|
} catch {
|
|
4446
4783
|
return null;
|
|
4447
4784
|
}
|
|
@@ -4467,10 +4804,10 @@ async function runUpdate(input) {
|
|
|
4467
4804
|
);
|
|
4468
4805
|
const currentVersion = pkg2.version;
|
|
4469
4806
|
const tag = input.distTag ?? "latest";
|
|
4470
|
-
const target =
|
|
4807
|
+
const target = join26(input.home, ".claude", "skills");
|
|
4471
4808
|
let latest;
|
|
4472
4809
|
try {
|
|
4473
|
-
latest =
|
|
4810
|
+
latest = execSync3(`npm view skillwiki@${tag} version`, {
|
|
4474
4811
|
encoding: "utf8",
|
|
4475
4812
|
timeout: 15e3
|
|
4476
4813
|
}).trim();
|
|
@@ -4500,7 +4837,7 @@ async function runUpdate(input) {
|
|
|
4500
4837
|
};
|
|
4501
4838
|
}
|
|
4502
4839
|
try {
|
|
4503
|
-
|
|
4840
|
+
execSync3(`npm install -g skillwiki@${tag}`, {
|
|
4504
4841
|
stdio: "pipe",
|
|
4505
4842
|
timeout: 6e4
|
|
4506
4843
|
});
|
|
@@ -4536,17 +4873,17 @@ async function runUpdate(input) {
|
|
|
4536
4873
|
}
|
|
4537
4874
|
|
|
4538
4875
|
// src/commands/self-update.ts
|
|
4539
|
-
import { execSync as
|
|
4540
|
-
import { existsSync as
|
|
4541
|
-
import { join as
|
|
4876
|
+
import { execSync as execSync4 } from "child_process";
|
|
4877
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
|
|
4878
|
+
import { join as join27 } from "path";
|
|
4542
4879
|
var DEFAULT_SOURCE_ROOT_SUFFIX = "/Desktop/code/llm-wiki";
|
|
4543
4880
|
async function runSelfUpdate(input) {
|
|
4544
4881
|
const currentVersion = JSON.parse(
|
|
4545
4882
|
readFileSync8(new URL("../../package.json", import.meta.url), "utf8")
|
|
4546
4883
|
).version;
|
|
4547
4884
|
const sourceRoot = input.sourceRoot ?? `${input.home}${DEFAULT_SOURCE_ROOT_SUFFIX}`;
|
|
4548
|
-
const localPkgPath =
|
|
4549
|
-
const hasLocalSource =
|
|
4885
|
+
const localPkgPath = join27(sourceRoot, "packages", "cli", "package.json");
|
|
4886
|
+
const hasLocalSource = existsSync8(localPkgPath);
|
|
4550
4887
|
if (input.check) {
|
|
4551
4888
|
let availableVersion = null;
|
|
4552
4889
|
let source;
|
|
@@ -4560,7 +4897,7 @@ async function runSelfUpdate(input) {
|
|
|
4560
4897
|
} else {
|
|
4561
4898
|
source = "npm";
|
|
4562
4899
|
try {
|
|
4563
|
-
availableVersion =
|
|
4900
|
+
availableVersion = execSync4("npm view skillwiki@latest version", {
|
|
4564
4901
|
encoding: "utf8",
|
|
4565
4902
|
timeout: 15e3
|
|
4566
4903
|
}).trim();
|
|
@@ -4586,7 +4923,7 @@ async function runSelfUpdate(input) {
|
|
|
4586
4923
|
}
|
|
4587
4924
|
if (hasLocalSource) {
|
|
4588
4925
|
try {
|
|
4589
|
-
|
|
4926
|
+
execSync4("npm run build -w packages/cli", {
|
|
4590
4927
|
cwd: sourceRoot,
|
|
4591
4928
|
stdio: "pipe",
|
|
4592
4929
|
timeout: 6e4
|
|
@@ -4598,7 +4935,7 @@ async function runSelfUpdate(input) {
|
|
|
4598
4935
|
};
|
|
4599
4936
|
}
|
|
4600
4937
|
try {
|
|
4601
|
-
|
|
4938
|
+
execSync4("npm link ./packages/cli", {
|
|
4602
4939
|
cwd: sourceRoot,
|
|
4603
4940
|
stdio: "pipe",
|
|
4604
4941
|
timeout: 3e4
|
|
@@ -4630,7 +4967,7 @@ async function runSelfUpdate(input) {
|
|
|
4630
4967
|
}
|
|
4631
4968
|
let latestVersion;
|
|
4632
4969
|
try {
|
|
4633
|
-
latestVersion =
|
|
4970
|
+
latestVersion = execSync4("npm view skillwiki@latest version", {
|
|
4634
4971
|
encoding: "utf8",
|
|
4635
4972
|
timeout: 15e3
|
|
4636
4973
|
}).trim();
|
|
@@ -4653,7 +4990,7 @@ async function runSelfUpdate(input) {
|
|
|
4653
4990
|
};
|
|
4654
4991
|
}
|
|
4655
4992
|
try {
|
|
4656
|
-
|
|
4993
|
+
execSync4("npm install -g skillwiki@latest", {
|
|
4657
4994
|
stdio: "pipe",
|
|
4658
4995
|
timeout: 6e4
|
|
4659
4996
|
});
|
|
@@ -4677,10 +5014,10 @@ async function runSelfUpdate(input) {
|
|
|
4677
5014
|
}
|
|
4678
5015
|
|
|
4679
5016
|
// src/commands/transcripts.ts
|
|
4680
|
-
import { readdir as readdir5, stat as stat6, readFile as
|
|
4681
|
-
import { join as
|
|
5017
|
+
import { readdir as readdir5, stat as stat6, readFile as readFile19 } from "fs/promises";
|
|
5018
|
+
import { join as join28 } from "path";
|
|
4682
5019
|
async function runTranscripts(input) {
|
|
4683
|
-
const dir =
|
|
5020
|
+
const dir = join28(input.vault, "raw", "transcripts");
|
|
4684
5021
|
let entries;
|
|
4685
5022
|
try {
|
|
4686
5023
|
entries = await readdir5(dir, { withFileTypes: true });
|
|
@@ -4690,8 +5027,8 @@ async function runTranscripts(input) {
|
|
|
4690
5027
|
const transcripts = [];
|
|
4691
5028
|
for (const entry of entries) {
|
|
4692
5029
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
4693
|
-
const filePath =
|
|
4694
|
-
const content = await
|
|
5030
|
+
const filePath = join28(dir, entry.name);
|
|
5031
|
+
const content = await readFile19(filePath, "utf8");
|
|
4695
5032
|
const fm = extractFrontmatter(content);
|
|
4696
5033
|
if (!fm.ok) continue;
|
|
4697
5034
|
const ingested = typeof fm.data.ingested === "string" ? fm.data.ingested : "";
|
|
@@ -4708,12 +5045,12 @@ async function runTranscripts(input) {
|
|
|
4708
5045
|
}
|
|
4709
5046
|
|
|
4710
5047
|
// src/commands/project-index.ts
|
|
4711
|
-
import { readdir as readdir6, readFile as
|
|
4712
|
-
import { join as
|
|
5048
|
+
import { readdir as readdir6, readFile as readFile20, writeFile as writeFile10, mkdir as mkdir9 } from "fs/promises";
|
|
5049
|
+
import { join as join29, dirname as dirname10 } from "path";
|
|
4713
5050
|
var LAYER2_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
|
|
4714
5051
|
async function runProjectIndex(input) {
|
|
4715
5052
|
const slug = input.slug;
|
|
4716
|
-
const projectDir =
|
|
5053
|
+
const projectDir = join29(input.vault, "projects", slug);
|
|
4717
5054
|
try {
|
|
4718
5055
|
await readdir6(projectDir);
|
|
4719
5056
|
} catch {
|
|
@@ -4724,15 +5061,15 @@ async function runProjectIndex(input) {
|
|
|
4724
5061
|
}
|
|
4725
5062
|
const wikilinkPattern = `[[${slug}]]`;
|
|
4726
5063
|
const entries = [];
|
|
4727
|
-
const compoundDir =
|
|
5064
|
+
const compoundDir = join29(input.vault, "projects", slug, "compound");
|
|
4728
5065
|
try {
|
|
4729
5066
|
const compoundFiles = await readdir6(compoundDir, { withFileTypes: true });
|
|
4730
5067
|
for (const entry of compoundFiles) {
|
|
4731
5068
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
4732
|
-
const filePath =
|
|
5069
|
+
const filePath = join29(compoundDir, entry.name);
|
|
4733
5070
|
let text;
|
|
4734
5071
|
try {
|
|
4735
|
-
text = await
|
|
5072
|
+
text = await readFile20(filePath, "utf8");
|
|
4736
5073
|
} catch {
|
|
4737
5074
|
continue;
|
|
4738
5075
|
}
|
|
@@ -4749,16 +5086,16 @@ async function runProjectIndex(input) {
|
|
|
4749
5086
|
for (const dir of LAYER2_DIRS) {
|
|
4750
5087
|
let files;
|
|
4751
5088
|
try {
|
|
4752
|
-
files = await readdir6(
|
|
5089
|
+
files = await readdir6(join29(input.vault, dir), { withFileTypes: true });
|
|
4753
5090
|
} catch {
|
|
4754
5091
|
continue;
|
|
4755
5092
|
}
|
|
4756
5093
|
for (const entry of files) {
|
|
4757
5094
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
4758
|
-
const filePath =
|
|
5095
|
+
const filePath = join29(input.vault, dir, entry.name);
|
|
4759
5096
|
let text;
|
|
4760
5097
|
try {
|
|
4761
|
-
text = await
|
|
5098
|
+
text = await readFile20(filePath, "utf8");
|
|
4762
5099
|
} catch {
|
|
4763
5100
|
continue;
|
|
4764
5101
|
}
|
|
@@ -4779,11 +5116,11 @@ async function runProjectIndex(input) {
|
|
|
4779
5116
|
const tb = typeOrder[b.type] ?? 99;
|
|
4780
5117
|
return ta !== tb ? ta - tb : a.title.localeCompare(b.title);
|
|
4781
5118
|
});
|
|
4782
|
-
const indexPath =
|
|
5119
|
+
const indexPath = join29(projectDir, "knowledge.md");
|
|
4783
5120
|
let existing = false;
|
|
4784
5121
|
let stale = false;
|
|
4785
5122
|
try {
|
|
4786
|
-
const existingText = await
|
|
5123
|
+
const existingText = await readFile20(indexPath, "utf8");
|
|
4787
5124
|
existing = true;
|
|
4788
5125
|
const existingEntries = existingText.split("\n").filter((l) => l.startsWith("- [["));
|
|
4789
5126
|
const existingPages = new Set(existingEntries.map((l) => {
|
|
@@ -4853,9 +5190,9 @@ ${entries.map((e) => ` ${e.type}: [[${e.page.replace(/\.md$/, "")}]] \u2014 ${e
|
|
|
4853
5190
|
|
|
4854
5191
|
// src/commands/compound.ts
|
|
4855
5192
|
import { writeFile as writeFile11, mkdir as mkdir10, readdir as readdir7, unlink as unlink3 } from "fs/promises";
|
|
4856
|
-
import { join as
|
|
4857
|
-
import { existsSync as
|
|
4858
|
-
import { readFile as
|
|
5193
|
+
import { join as join30 } from "path";
|
|
5194
|
+
import { existsSync as existsSync9 } from "fs";
|
|
5195
|
+
import { readFile as readFile21 } from "fs/promises";
|
|
4859
5196
|
var RETRO_HEADING_RE = /^## \[(\d{4}-\d{2}-\d{2})(?:\s+[^\]]+)?\] retro \| loop cycle(?: (\d+))?: (.+)$/;
|
|
4860
5197
|
var FIELD_RE = {
|
|
4861
5198
|
improve: /^-\s+\*?\*?Improve:?\*?\*?\s*(.+)$/m,
|
|
@@ -4953,17 +5290,17 @@ function extractRetroFields(date, cycleName, block) {
|
|
|
4953
5290
|
};
|
|
4954
5291
|
}
|
|
4955
5292
|
async function runCompound(input) {
|
|
4956
|
-
const logPath =
|
|
5293
|
+
const logPath = join30(input.vault, "log.md");
|
|
4957
5294
|
let logText;
|
|
4958
5295
|
try {
|
|
4959
|
-
logText = await
|
|
5296
|
+
logText = await readFile21(logPath, "utf8");
|
|
4960
5297
|
} catch {
|
|
4961
5298
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
|
|
4962
5299
|
}
|
|
4963
5300
|
const entries = parseRetroEntries(logText);
|
|
4964
5301
|
const promoted = [];
|
|
4965
5302
|
const skipped = [];
|
|
4966
|
-
const compoundDir =
|
|
5303
|
+
const compoundDir = join30(input.vault, "projects", input.project, "compound");
|
|
4967
5304
|
for (const entry of entries) {
|
|
4968
5305
|
const generalizeValue = entry.generalize.trim();
|
|
4969
5306
|
if (!/^yes/i.test(generalizeValue)) {
|
|
@@ -4971,8 +5308,8 @@ async function runCompound(input) {
|
|
|
4971
5308
|
continue;
|
|
4972
5309
|
}
|
|
4973
5310
|
const slug = slugify(entry.cycleName);
|
|
4974
|
-
const compoundPath =
|
|
4975
|
-
if (
|
|
5311
|
+
const compoundPath = join30(compoundDir, `${slug}.md`);
|
|
5312
|
+
if (existsSync9(compoundPath)) {
|
|
4976
5313
|
skipped.push(entry.date);
|
|
4977
5314
|
continue;
|
|
4978
5315
|
}
|
|
@@ -5010,7 +5347,7 @@ async function runCompound(input) {
|
|
|
5010
5347
|
].join("\n");
|
|
5011
5348
|
const content = frontmatter + "\n" + body;
|
|
5012
5349
|
if (!input.dryRun) {
|
|
5013
|
-
if (!
|
|
5350
|
+
if (!existsSync9(compoundDir)) {
|
|
5014
5351
|
await mkdir10(compoundDir, { recursive: true });
|
|
5015
5352
|
}
|
|
5016
5353
|
await writeFile11(compoundPath, content, "utf8");
|
|
@@ -5032,16 +5369,16 @@ async function runCompound(input) {
|
|
|
5032
5369
|
};
|
|
5033
5370
|
}
|
|
5034
5371
|
async function runCompoundDelete(input) {
|
|
5035
|
-
const projectDir =
|
|
5036
|
-
if (!
|
|
5372
|
+
const projectDir = join30(input.vault, "projects", input.project);
|
|
5373
|
+
if (!existsSync9(projectDir)) {
|
|
5037
5374
|
return {
|
|
5038
5375
|
exitCode: ExitCode.PROJECT_NOT_FOUND,
|
|
5039
5376
|
result: err("PROJECT_NOT_FOUND", { slug: input.project, path: projectDir })
|
|
5040
5377
|
};
|
|
5041
5378
|
}
|
|
5042
5379
|
const entryName = input.entry.replace(/\.md$/, "");
|
|
5043
|
-
const compoundPath =
|
|
5044
|
-
if (!
|
|
5380
|
+
const compoundPath = join30(projectDir, "compound", `${entryName}.md`);
|
|
5381
|
+
if (!existsSync9(compoundPath)) {
|
|
5045
5382
|
return {
|
|
5046
5383
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
5047
5384
|
result: err("FILE_NOT_FOUND", { path: compoundPath })
|
|
@@ -5074,8 +5411,8 @@ knowledge.md regenerated`
|
|
|
5074
5411
|
};
|
|
5075
5412
|
}
|
|
5076
5413
|
async function runCompoundList(input) {
|
|
5077
|
-
const compoundDir =
|
|
5078
|
-
if (!
|
|
5414
|
+
const compoundDir = join30(input.vault, "projects", input.project, "compound");
|
|
5415
|
+
if (!existsSync9(compoundDir)) {
|
|
5079
5416
|
return {
|
|
5080
5417
|
exitCode: ExitCode.OK,
|
|
5081
5418
|
result: ok({
|
|
@@ -5105,10 +5442,10 @@ could not read compound directory`
|
|
|
5105
5442
|
const entries = [];
|
|
5106
5443
|
for (const dirent of dirents) {
|
|
5107
5444
|
if (!dirent.isFile() || !dirent.name.endsWith(".md")) continue;
|
|
5108
|
-
const filePath =
|
|
5445
|
+
const filePath = join30(compoundDir, dirent.name);
|
|
5109
5446
|
let text;
|
|
5110
5447
|
try {
|
|
5111
|
-
text = await
|
|
5448
|
+
text = await readFile21(filePath, "utf8");
|
|
5112
5449
|
} catch {
|
|
5113
5450
|
continue;
|
|
5114
5451
|
}
|
|
@@ -5138,8 +5475,8 @@ no compound entries found`;
|
|
|
5138
5475
|
|
|
5139
5476
|
// src/commands/observe.ts
|
|
5140
5477
|
import { mkdir as mkdir11, writeFile as writeFile12 } from "fs/promises";
|
|
5141
|
-
import { existsSync as
|
|
5142
|
-
import { join as
|
|
5478
|
+
import { existsSync as existsSync10, statSync as statSync3 } from "fs";
|
|
5479
|
+
import { join as join31 } from "path";
|
|
5143
5480
|
import { createHash as createHash4 } from "crypto";
|
|
5144
5481
|
var ALLOWED_KINDS = /* @__PURE__ */ new Set(["note", "bug", "task", "idea", "session-log"]);
|
|
5145
5482
|
function slugify2(text) {
|
|
@@ -5162,13 +5499,13 @@ async function runObserve(input) {
|
|
|
5162
5499
|
result: err("SCHEME_REJECTED", { message: "Text must not be empty" })
|
|
5163
5500
|
};
|
|
5164
5501
|
}
|
|
5165
|
-
if (!
|
|
5502
|
+
if (!existsSync10(input.vault) || !statSync3(input.vault).isDirectory()) {
|
|
5166
5503
|
return {
|
|
5167
5504
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
5168
5505
|
result: err("VAULT_PATH_INVALID", { path: input.vault })
|
|
5169
5506
|
};
|
|
5170
5507
|
}
|
|
5171
|
-
const transcriptsDir =
|
|
5508
|
+
const transcriptsDir = join31(input.vault, "raw", "transcripts");
|
|
5172
5509
|
try {
|
|
5173
5510
|
await mkdir11(transcriptsDir, { recursive: true });
|
|
5174
5511
|
} catch {
|
|
@@ -5180,7 +5517,7 @@ async function runObserve(input) {
|
|
|
5180
5517
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
5181
5518
|
const slug = slugify2(input.text);
|
|
5182
5519
|
const fileName = `${today}-observation-${slug}.md`;
|
|
5183
|
-
const filePath =
|
|
5520
|
+
const filePath = join31(transcriptsDir, fileName);
|
|
5184
5521
|
const body = `
|
|
5185
5522
|
${input.text.trim()}
|
|
5186
5523
|
`;
|
|
@@ -5220,8 +5557,8 @@ ${input.text.trim()}
|
|
|
5220
5557
|
}
|
|
5221
5558
|
|
|
5222
5559
|
// src/commands/ingest.ts
|
|
5223
|
-
import { readFile as
|
|
5224
|
-
import { join as
|
|
5560
|
+
import { readFile as readFile22, writeFile as writeFile13, mkdir as mkdir12 } from "fs/promises";
|
|
5561
|
+
import { join as join32 } from "path";
|
|
5225
5562
|
import { createHash as createHash5 } from "crypto";
|
|
5226
5563
|
var ALLOWED_TYPES = /* @__PURE__ */ new Set(["entity", "concept", "comparison", "query"]);
|
|
5227
5564
|
var TYPE_DIR = {
|
|
@@ -5380,7 +5717,7 @@ async function runIngest(input) {
|
|
|
5380
5717
|
sourceContent = fetchResult.data.body;
|
|
5381
5718
|
} else {
|
|
5382
5719
|
try {
|
|
5383
|
-
sourceContent = await
|
|
5720
|
+
sourceContent = await readFile22(input.source, "utf8");
|
|
5384
5721
|
} catch {
|
|
5385
5722
|
return {
|
|
5386
5723
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
@@ -5395,8 +5732,8 @@ async function runIngest(input) {
|
|
|
5395
5732
|
const rawRelPath = `raw/articles/${slug}.md`;
|
|
5396
5733
|
const typedDir = TYPE_DIR[input.type] ?? `${input.type}s`;
|
|
5397
5734
|
const typedRelPath = `${typedDir}/${slug}.md`;
|
|
5398
|
-
const rawAbsPath =
|
|
5399
|
-
const typedAbsPath =
|
|
5735
|
+
const rawAbsPath = join32(input.vault, rawRelPath);
|
|
5736
|
+
const typedAbsPath = join32(input.vault, typedRelPath);
|
|
5400
5737
|
const rawContent = buildRawContent(sourceUrl, today, sha256, sourceContent);
|
|
5401
5738
|
const typedContent = buildTypedContent(
|
|
5402
5739
|
input.title,
|
|
@@ -5459,7 +5796,7 @@ async function runIngest(input) {
|
|
|
5459
5796
|
};
|
|
5460
5797
|
}
|
|
5461
5798
|
try {
|
|
5462
|
-
await mkdir12(
|
|
5799
|
+
await mkdir12(join32(input.vault, "raw", "articles"), { recursive: true });
|
|
5463
5800
|
await writeFile13(rawAbsPath, rawContent, "utf8");
|
|
5464
5801
|
} catch (e) {
|
|
5465
5802
|
return {
|
|
@@ -5468,7 +5805,7 @@ async function runIngest(input) {
|
|
|
5468
5805
|
};
|
|
5469
5806
|
}
|
|
5470
5807
|
try {
|
|
5471
|
-
await mkdir12(
|
|
5808
|
+
await mkdir12(join32(input.vault, typedDir), { recursive: true });
|
|
5472
5809
|
await writeFile13(typedAbsPath, typedContent, "utf8");
|
|
5473
5810
|
} catch (e) {
|
|
5474
5811
|
return {
|
|
@@ -5647,11 +5984,105 @@ ${body}`;
|
|
|
5647
5984
|
}
|
|
5648
5985
|
|
|
5649
5986
|
// src/commands/sync.ts
|
|
5650
|
-
import { existsSync as
|
|
5651
|
-
import { join as
|
|
5987
|
+
import { existsSync as existsSync12 } from "fs";
|
|
5988
|
+
import { join as join34 } from "path";
|
|
5989
|
+
|
|
5990
|
+
// src/utils/sync-lock.ts
|
|
5991
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync9, renameSync, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
5992
|
+
import { join as join33 } from "path";
|
|
5993
|
+
import { createHash as createHash6 } from "crypto";
|
|
5994
|
+
function getSessionId() {
|
|
5995
|
+
if (process.env.CLAUDE_SESSION_ID) return process.env.CLAUDE_SESSION_ID;
|
|
5996
|
+
if (process.env.SKILLWIKI_SESSION_ID) return process.env.SKILLWIKI_SESSION_ID;
|
|
5997
|
+
return process.pid.toString();
|
|
5998
|
+
}
|
|
5999
|
+
function lockPath(vault) {
|
|
6000
|
+
return join33(vault, ".skillwiki", "sync.lock");
|
|
6001
|
+
}
|
|
6002
|
+
function readLock(vault) {
|
|
6003
|
+
const path = lockPath(vault);
|
|
6004
|
+
if (!existsSync11(path)) return null;
|
|
6005
|
+
try {
|
|
6006
|
+
const raw = readFileSync9(path, "utf8");
|
|
6007
|
+
return JSON.parse(raw);
|
|
6008
|
+
} catch {
|
|
6009
|
+
return null;
|
|
6010
|
+
}
|
|
6011
|
+
}
|
|
6012
|
+
function isStale(lock, now) {
|
|
6013
|
+
const nowTime = (now ?? /* @__PURE__ */ new Date()).getTime();
|
|
6014
|
+
const expiresTime = new Date(lock.expires).getTime();
|
|
6015
|
+
return expiresTime < nowTime;
|
|
6016
|
+
}
|
|
6017
|
+
function acquireLock(vault, opts = {}) {
|
|
6018
|
+
const path = lockPath(vault);
|
|
6019
|
+
const dir = join33(vault, ".skillwiki");
|
|
6020
|
+
if (!existsSync11(dir)) {
|
|
6021
|
+
mkdirSync3(dir, { recursive: true });
|
|
6022
|
+
}
|
|
6023
|
+
const sessionId = opts.sessionId ?? getSessionId();
|
|
6024
|
+
const summary = opts.summary ?? "skillwiki sync";
|
|
6025
|
+
const ttlMinutes = opts.ttlMinutes ?? 30;
|
|
6026
|
+
const force = opts.force ?? false;
|
|
6027
|
+
const now = /* @__PURE__ */ new Date();
|
|
6028
|
+
const acquired = now.toISOString();
|
|
6029
|
+
const expires = new Date(now.getTime() + ttlMinutes * 60 * 1e3).toISOString();
|
|
6030
|
+
const lock = {
|
|
6031
|
+
session_id: sessionId,
|
|
6032
|
+
pid: process.pid,
|
|
6033
|
+
cwd: process.cwd(),
|
|
6034
|
+
summary,
|
|
6035
|
+
acquired,
|
|
6036
|
+
expires
|
|
6037
|
+
};
|
|
6038
|
+
try {
|
|
6039
|
+
const content = JSON.stringify(lock, null, 2) + "\n";
|
|
6040
|
+
writeFileSync5(path, content, { flag: "wx" });
|
|
6041
|
+
return { ok: true, lock };
|
|
6042
|
+
} catch (e) {
|
|
6043
|
+
const err3 = e;
|
|
6044
|
+
if (err3.code !== "EEXIST") throw err3;
|
|
6045
|
+
}
|
|
6046
|
+
const existing = readLock(vault);
|
|
6047
|
+
if (!existing) {
|
|
6048
|
+
writeLockedFile(path, lock);
|
|
6049
|
+
return { ok: true, lock };
|
|
6050
|
+
}
|
|
6051
|
+
if (force || isStale(existing)) {
|
|
6052
|
+
writeLockedFile(path, lock);
|
|
6053
|
+
return { ok: true, lock };
|
|
6054
|
+
}
|
|
6055
|
+
return { ok: false, held: existing };
|
|
6056
|
+
}
|
|
6057
|
+
function writeLockedFile(path, lock) {
|
|
6058
|
+
const tmp = path + ".tmp";
|
|
6059
|
+
const content = JSON.stringify(lock, null, 2) + "\n";
|
|
6060
|
+
writeFileSync5(tmp, content);
|
|
6061
|
+
renameSync(tmp, path);
|
|
6062
|
+
}
|
|
6063
|
+
function releaseLock(vault, opts = {}) {
|
|
6064
|
+
const path = lockPath(vault);
|
|
6065
|
+
if (!existsSync11(path)) {
|
|
6066
|
+
return { released: false };
|
|
6067
|
+
}
|
|
6068
|
+
const sessionId = opts.sessionId ?? getSessionId();
|
|
6069
|
+
const existing = readLock(vault);
|
|
6070
|
+
if (!existing || existing.session_id !== sessionId) {
|
|
6071
|
+
return { released: false };
|
|
6072
|
+
}
|
|
6073
|
+
try {
|
|
6074
|
+
unlinkSync4(path);
|
|
6075
|
+
return { released: true };
|
|
6076
|
+
} catch {
|
|
6077
|
+
return { released: false };
|
|
6078
|
+
}
|
|
6079
|
+
}
|
|
6080
|
+
|
|
6081
|
+
// src/commands/sync.ts
|
|
5652
6082
|
function runSyncStatus(input) {
|
|
5653
6083
|
const vault = input.vault;
|
|
5654
|
-
|
|
6084
|
+
const includeStashes = input.includeStashes ?? false;
|
|
6085
|
+
if (!existsSync12(join34(vault, ".git"))) {
|
|
5655
6086
|
return {
|
|
5656
6087
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
5657
6088
|
result: ok({
|
|
@@ -5705,22 +6136,30 @@ function runSyncStatus(input) {
|
|
|
5705
6136
|
`last_commit: ${last_commit}`
|
|
5706
6137
|
];
|
|
5707
6138
|
const exitCode = status === "clean" ? ExitCode.OK : ExitCode.LINT_HAS_WARNINGS;
|
|
6139
|
+
let stashes;
|
|
6140
|
+
if (includeStashes) {
|
|
6141
|
+
stashes = enumerateStashes(vault);
|
|
6142
|
+
}
|
|
6143
|
+
const output = {
|
|
6144
|
+
is_git_repo: true,
|
|
6145
|
+
dirty,
|
|
6146
|
+
ahead,
|
|
6147
|
+
behind,
|
|
6148
|
+
last_commit,
|
|
6149
|
+
status,
|
|
6150
|
+
humanHint: hintLines.join("\n")
|
|
6151
|
+
};
|
|
6152
|
+
if (stashes !== void 0) {
|
|
6153
|
+
output.stashes = stashes;
|
|
6154
|
+
}
|
|
5708
6155
|
return {
|
|
5709
6156
|
exitCode,
|
|
5710
|
-
result: ok(
|
|
5711
|
-
is_git_repo: true,
|
|
5712
|
-
dirty,
|
|
5713
|
-
ahead,
|
|
5714
|
-
behind,
|
|
5715
|
-
last_commit,
|
|
5716
|
-
status,
|
|
5717
|
-
humanHint: hintLines.join("\n")
|
|
5718
|
-
})
|
|
6157
|
+
result: ok(output)
|
|
5719
6158
|
};
|
|
5720
6159
|
}
|
|
5721
6160
|
async function runSyncPush(input) {
|
|
5722
6161
|
const vault = input.vault;
|
|
5723
|
-
if (!
|
|
6162
|
+
if (!existsSync12(join34(vault, ".git"))) {
|
|
5724
6163
|
return {
|
|
5725
6164
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
5726
6165
|
result: err("NOT_A_GIT_REPO", { path: vault })
|
|
@@ -5803,9 +6242,28 @@ async function runSyncPush(input) {
|
|
|
5803
6242
|
})
|
|
5804
6243
|
};
|
|
5805
6244
|
}
|
|
6245
|
+
function enumerateStashes(vault) {
|
|
6246
|
+
const output = git(vault, ["log", "--format=%gd%x09%s%x09%ct", "-g", "stash"]);
|
|
6247
|
+
if (!output) return [];
|
|
6248
|
+
const now = Date.now();
|
|
6249
|
+
const stashes = [];
|
|
6250
|
+
const lines = output.split("\n").filter((l) => l.trim().length > 0);
|
|
6251
|
+
for (const line of lines) {
|
|
6252
|
+
const parts = line.split(" ");
|
|
6253
|
+
if (parts.length < 3) continue;
|
|
6254
|
+
const ref = parts[0];
|
|
6255
|
+
const message = parts[1];
|
|
6256
|
+
const ctStr = parts[2];
|
|
6257
|
+
const ct = parseInt(ctStr, 10);
|
|
6258
|
+
if (isNaN(ct)) continue;
|
|
6259
|
+
const age_minutes = Math.floor((now - ct * 1e3) / (60 * 1e3));
|
|
6260
|
+
stashes.push({ ref, message, age_minutes });
|
|
6261
|
+
}
|
|
6262
|
+
return stashes;
|
|
6263
|
+
}
|
|
5806
6264
|
async function runSyncPull(input) {
|
|
5807
6265
|
const vault = input.vault;
|
|
5808
|
-
if (!
|
|
6266
|
+
if (!existsSync12(join34(vault, ".git"))) {
|
|
5809
6267
|
return {
|
|
5810
6268
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
5811
6269
|
result: err("NOT_A_GIT_REPO", { path: vault })
|
|
@@ -5878,10 +6336,106 @@ async function runSyncPull(input) {
|
|
|
5878
6336
|
})
|
|
5879
6337
|
};
|
|
5880
6338
|
}
|
|
6339
|
+
function runSyncPeers(input) {
|
|
6340
|
+
const vault = input.vault;
|
|
6341
|
+
const locks = [];
|
|
6342
|
+
const existingLock = readLock(vault);
|
|
6343
|
+
if (existingLock) {
|
|
6344
|
+
const self = existingLock.session_id === getSessionId();
|
|
6345
|
+
locks.push({ ...existingLock, is_self: self });
|
|
6346
|
+
}
|
|
6347
|
+
const allStashes = enumerateStashes(vault);
|
|
6348
|
+
const stashes = [];
|
|
6349
|
+
for (const stash of allStashes) {
|
|
6350
|
+
let actualMessage = stash.message;
|
|
6351
|
+
const prefixMatch = stash.message.match(/^On [^:]+:\s*(.*)/);
|
|
6352
|
+
if (prefixMatch) {
|
|
6353
|
+
actualMessage = prefixMatch[1];
|
|
6354
|
+
}
|
|
6355
|
+
const match = actualMessage.match(/^wiki-sync:([^:]+):([^:]+):(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z):(.*)$/);
|
|
6356
|
+
if (!match) continue;
|
|
6357
|
+
const session_id = match[1];
|
|
6358
|
+
const cwd_hash = match[2];
|
|
6359
|
+
const timestamp = match[3];
|
|
6360
|
+
const summary = match[4];
|
|
6361
|
+
stashes.push({
|
|
6362
|
+
ref: stash.ref,
|
|
6363
|
+
session_id,
|
|
6364
|
+
cwd_hash,
|
|
6365
|
+
timestamp,
|
|
6366
|
+
summary,
|
|
6367
|
+
age_minutes: stash.age_minutes
|
|
6368
|
+
});
|
|
6369
|
+
}
|
|
6370
|
+
const hintParts = [];
|
|
6371
|
+
if (locks.length > 0) hintParts.push(`${locks.length} lock(s)`);
|
|
6372
|
+
if (stashes.length > 0) hintParts.push(`${stashes.length} wiki-sync stash(es)`);
|
|
6373
|
+
const humanHint = hintParts.length > 0 ? hintParts.join(", ") : "no peers detected";
|
|
6374
|
+
return {
|
|
6375
|
+
exitCode: ExitCode.OK,
|
|
6376
|
+
result: ok({
|
|
6377
|
+
locks,
|
|
6378
|
+
stashes,
|
|
6379
|
+
humanHint
|
|
6380
|
+
})
|
|
6381
|
+
};
|
|
6382
|
+
}
|
|
6383
|
+
function runSyncLock(input) {
|
|
6384
|
+
const vault = input.vault;
|
|
6385
|
+
if (!existsSync12(vault)) {
|
|
6386
|
+
return {
|
|
6387
|
+
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
6388
|
+
result: err("VAULT_PATH_INVALID", { path: vault })
|
|
6389
|
+
};
|
|
6390
|
+
}
|
|
6391
|
+
const result = acquireLock(vault, {
|
|
6392
|
+
sessionId: input.sessionId,
|
|
6393
|
+
summary: input.summary,
|
|
6394
|
+
ttlMinutes: input.ttlMinutes,
|
|
6395
|
+
force: input.force
|
|
6396
|
+
});
|
|
6397
|
+
if (result.ok) {
|
|
6398
|
+
return {
|
|
6399
|
+
exitCode: ExitCode.OK,
|
|
6400
|
+
result: ok({
|
|
6401
|
+
acquired: true,
|
|
6402
|
+
lock: result.lock,
|
|
6403
|
+
humanHint: `lock acquired for ${result.lock.summary} (expires ${result.lock.expires})`
|
|
6404
|
+
})
|
|
6405
|
+
};
|
|
6406
|
+
} else {
|
|
6407
|
+
return {
|
|
6408
|
+
exitCode: ExitCode.SYNC_LOCK_HELD,
|
|
6409
|
+
result: ok({
|
|
6410
|
+
acquired: false,
|
|
6411
|
+
lock: result.held,
|
|
6412
|
+
held_by: result.held,
|
|
6413
|
+
humanHint: `lock held by session ${result.held.session_id} (PID ${result.held.pid}) for ${result.held.summary}`
|
|
6414
|
+
})
|
|
6415
|
+
};
|
|
6416
|
+
}
|
|
6417
|
+
}
|
|
6418
|
+
function runSyncUnlock(input) {
|
|
6419
|
+
const vault = input.vault;
|
|
6420
|
+
if (!existsSync12(vault)) {
|
|
6421
|
+
return {
|
|
6422
|
+
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
6423
|
+
result: err("VAULT_PATH_INVALID", { path: vault })
|
|
6424
|
+
};
|
|
6425
|
+
}
|
|
6426
|
+
const result = releaseLock(vault, { sessionId: input.sessionId });
|
|
6427
|
+
return {
|
|
6428
|
+
exitCode: ExitCode.OK,
|
|
6429
|
+
result: ok({
|
|
6430
|
+
released: result.released,
|
|
6431
|
+
humanHint: result.released ? "lock released" : "lock not held by this session (no-op)"
|
|
6432
|
+
})
|
|
6433
|
+
};
|
|
6434
|
+
}
|
|
5881
6435
|
|
|
5882
6436
|
// src/commands/backup.ts
|
|
5883
|
-
import { statSync as statSync4, readdirSync as readdirSync2, readFileSync as
|
|
5884
|
-
import { join as
|
|
6437
|
+
import { statSync as statSync4, readdirSync as readdirSync2, readFileSync as readFileSync10, mkdirSync as mkdirSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
6438
|
+
import { join as join35, relative as relative3, dirname as dirname11 } from "path";
|
|
5885
6439
|
import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
|
|
5886
6440
|
|
|
5887
6441
|
// src/utils/s3-client.ts
|
|
@@ -5905,7 +6459,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node_
|
|
|
5905
6459
|
function* walkMarkdown(dir, base) {
|
|
5906
6460
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
5907
6461
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
5908
|
-
const full =
|
|
6462
|
+
const full = join35(dir, entry.name);
|
|
5909
6463
|
if (entry.isDirectory()) {
|
|
5910
6464
|
yield* walkMarkdown(full, base);
|
|
5911
6465
|
} else if (entry.name.endsWith(".md")) {
|
|
@@ -5928,7 +6482,7 @@ async function runBackupSync(input) {
|
|
|
5928
6482
|
let failed = 0;
|
|
5929
6483
|
const files = [...walkMarkdown(input.vault, input.vault)];
|
|
5930
6484
|
for (const relPath of files) {
|
|
5931
|
-
const absPath =
|
|
6485
|
+
const absPath = join35(input.vault, relPath);
|
|
5932
6486
|
const localStat = statSync4(absPath);
|
|
5933
6487
|
let needsUpload = true;
|
|
5934
6488
|
try {
|
|
@@ -5947,7 +6501,7 @@ async function runBackupSync(input) {
|
|
|
5947
6501
|
continue;
|
|
5948
6502
|
}
|
|
5949
6503
|
try {
|
|
5950
|
-
const body =
|
|
6504
|
+
const body = readFileSync10(absPath);
|
|
5951
6505
|
await client.send(new PutObjectCommand({ Bucket: input.bucket, Key: relPath, Body: body }));
|
|
5952
6506
|
uploaded++;
|
|
5953
6507
|
} catch {
|
|
@@ -6004,7 +6558,7 @@ async function runBackupRestore(input) {
|
|
|
6004
6558
|
const objects = list.Contents ?? [];
|
|
6005
6559
|
for (const obj of objects) {
|
|
6006
6560
|
if (!obj.Key) continue;
|
|
6007
|
-
const localPath =
|
|
6561
|
+
const localPath = join35(target, obj.Key);
|
|
6008
6562
|
try {
|
|
6009
6563
|
const localStat = statSync4(localPath);
|
|
6010
6564
|
if (obj.LastModified && localStat.mtime > obj.LastModified) {
|
|
@@ -6017,8 +6571,8 @@ async function runBackupRestore(input) {
|
|
|
6017
6571
|
const resp = await client.send(new GetObjectCommand({ Bucket: input.bucket, Key: obj.Key }));
|
|
6018
6572
|
const body = await resp.Body?.transformToByteArray();
|
|
6019
6573
|
if (body) {
|
|
6020
|
-
|
|
6021
|
-
|
|
6574
|
+
mkdirSync4(dirname11(localPath), { recursive: true });
|
|
6575
|
+
writeFileSync6(localPath, Buffer.from(body));
|
|
6022
6576
|
downloaded++;
|
|
6023
6577
|
}
|
|
6024
6578
|
} catch {
|
|
@@ -6050,11 +6604,11 @@ async function runBackupRestore(input) {
|
|
|
6050
6604
|
}
|
|
6051
6605
|
|
|
6052
6606
|
// src/commands/status.ts
|
|
6053
|
-
import { existsSync as
|
|
6054
|
-
import { readFile as
|
|
6055
|
-
import { join as
|
|
6607
|
+
import { existsSync as existsSync13, statSync as statSync5 } from "fs";
|
|
6608
|
+
import { readFile as readFile23 } from "fs/promises";
|
|
6609
|
+
import { join as join36 } from "path";
|
|
6056
6610
|
async function runStatus(input) {
|
|
6057
|
-
if (!
|
|
6611
|
+
if (!existsSync13(input.vault)) {
|
|
6058
6612
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
6059
6613
|
}
|
|
6060
6614
|
const scan = await scanVault(input.vault);
|
|
@@ -6079,7 +6633,7 @@ async function runStatus(input) {
|
|
|
6079
6633
|
const compound = scan.data.compound.length;
|
|
6080
6634
|
let schemaVersion = "v1";
|
|
6081
6635
|
try {
|
|
6082
|
-
const schemaContent = await
|
|
6636
|
+
const schemaContent = await readFile23(join36(input.vault, "SCHEMA.md"), "utf8");
|
|
6083
6637
|
const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
|
|
6084
6638
|
if (versionMatch) schemaVersion = versionMatch[1];
|
|
6085
6639
|
} catch {
|
|
@@ -6140,7 +6694,7 @@ async function runStatus(input) {
|
|
|
6140
6694
|
|
|
6141
6695
|
// src/commands/seed.ts
|
|
6142
6696
|
import { mkdir as mkdir13, writeFile as writeFile14, stat as stat7 } from "fs/promises";
|
|
6143
|
-
import { join as
|
|
6697
|
+
import { join as join37 } from "path";
|
|
6144
6698
|
var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6145
6699
|
var EXAMPLE_PAGES = {
|
|
6146
6700
|
"entities/example-project.md": `---
|
|
@@ -6209,29 +6763,29 @@ Real sources are immutable after ingestion \u2014 never edit them.
|
|
|
6209
6763
|
`;
|
|
6210
6764
|
async function runSeed(input) {
|
|
6211
6765
|
try {
|
|
6212
|
-
await stat7(
|
|
6766
|
+
await stat7(join37(input.vault, "SCHEMA.md"));
|
|
6213
6767
|
} catch {
|
|
6214
6768
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
|
|
6215
6769
|
}
|
|
6216
6770
|
const created = [];
|
|
6217
6771
|
const skipped = [];
|
|
6218
6772
|
for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
|
|
6219
|
-
const absPath =
|
|
6773
|
+
const absPath = join37(input.vault, relPath);
|
|
6220
6774
|
try {
|
|
6221
6775
|
await stat7(absPath);
|
|
6222
6776
|
skipped.push(relPath);
|
|
6223
6777
|
} catch {
|
|
6224
|
-
await mkdir13(
|
|
6778
|
+
await mkdir13(join37(absPath, ".."), { recursive: true });
|
|
6225
6779
|
await writeFile14(absPath, content, "utf8");
|
|
6226
6780
|
created.push(relPath);
|
|
6227
6781
|
}
|
|
6228
6782
|
}
|
|
6229
|
-
const rawPath =
|
|
6783
|
+
const rawPath = join37(input.vault, "raw", "articles", "example-source.md");
|
|
6230
6784
|
try {
|
|
6231
6785
|
await stat7(rawPath);
|
|
6232
6786
|
skipped.push("raw/articles/example-source.md");
|
|
6233
6787
|
} catch {
|
|
6234
|
-
await mkdir13(
|
|
6788
|
+
await mkdir13(join37(rawPath, ".."), { recursive: true });
|
|
6235
6789
|
await writeFile14(rawPath, EXAMPLE_RAW, "utf8");
|
|
6236
6790
|
created.push("raw/articles/example-source.md");
|
|
6237
6791
|
}
|
|
@@ -6254,9 +6808,9 @@ async function runSeed(input) {
|
|
|
6254
6808
|
}
|
|
6255
6809
|
|
|
6256
6810
|
// src/commands/canvas.ts
|
|
6257
|
-
import { readFile as
|
|
6258
|
-
import { existsSync as
|
|
6259
|
-
import { join as
|
|
6811
|
+
import { readFile as readFile24, writeFile as writeFile15 } from "fs/promises";
|
|
6812
|
+
import { existsSync as existsSync14 } from "fs";
|
|
6813
|
+
import { join as join38 } from "path";
|
|
6260
6814
|
var NODE_WIDTH = 240;
|
|
6261
6815
|
var NODE_HEIGHT = 60;
|
|
6262
6816
|
var COLUMN_SPACING = 400;
|
|
@@ -6334,8 +6888,8 @@ function buildCanvasEdges(adjacency) {
|
|
|
6334
6888
|
return edges;
|
|
6335
6889
|
}
|
|
6336
6890
|
async function runCanvasGenerate(input) {
|
|
6337
|
-
const graphPath = input.graphPath ??
|
|
6338
|
-
if (!
|
|
6891
|
+
const graphPath = input.graphPath ?? join38(input.vault, ".skillwiki", "graph.json");
|
|
6892
|
+
if (!existsSync14(graphPath)) {
|
|
6339
6893
|
return {
|
|
6340
6894
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
6341
6895
|
result: err("FILE_NOT_FOUND", {
|
|
@@ -6346,7 +6900,7 @@ async function runCanvasGenerate(input) {
|
|
|
6346
6900
|
}
|
|
6347
6901
|
let raw;
|
|
6348
6902
|
try {
|
|
6349
|
-
raw = await
|
|
6903
|
+
raw = await readFile24(graphPath, "utf8");
|
|
6350
6904
|
} catch (e) {
|
|
6351
6905
|
return {
|
|
6352
6906
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
@@ -6372,7 +6926,7 @@ async function runCanvasGenerate(input) {
|
|
|
6372
6926
|
const nodes = buildCanvasNodes(paths);
|
|
6373
6927
|
const edges = buildCanvasEdges(graph.adjacency);
|
|
6374
6928
|
const canvas = { nodes, edges };
|
|
6375
|
-
const outPath =
|
|
6929
|
+
const outPath = join38(input.vault, "vault-graph.canvas");
|
|
6376
6930
|
try {
|
|
6377
6931
|
await writeFile15(outPath, JSON.stringify(canvas, null, 2));
|
|
6378
6932
|
} catch (e) {
|
|
@@ -6394,8 +6948,8 @@ written: ${outPath}`
|
|
|
6394
6948
|
}
|
|
6395
6949
|
|
|
6396
6950
|
// src/commands/query.ts
|
|
6397
|
-
import { readFile as
|
|
6398
|
-
import { join as
|
|
6951
|
+
import { readFile as readFile25, stat as stat8 } from "fs/promises";
|
|
6952
|
+
import { join as join39 } from "path";
|
|
6399
6953
|
var W_KEYWORD = 2;
|
|
6400
6954
|
var W_SOURCE_OVERLAP = 4;
|
|
6401
6955
|
var W_WIKILINK = 3;
|
|
@@ -6516,7 +7070,7 @@ function computeKeywordScore(terms, title, tags, body) {
|
|
|
6516
7070
|
return score;
|
|
6517
7071
|
}
|
|
6518
7072
|
async function loadOrBuildGraph(vault) {
|
|
6519
|
-
const graphPath =
|
|
7073
|
+
const graphPath = join39(vault, ".skillwiki", "graph.json");
|
|
6520
7074
|
let needsBuild = false;
|
|
6521
7075
|
try {
|
|
6522
7076
|
const fileStat = await stat8(graphPath);
|
|
@@ -6530,7 +7084,7 @@ async function loadOrBuildGraph(vault) {
|
|
|
6530
7084
|
if (buildResult.exitCode !== 0) return null;
|
|
6531
7085
|
}
|
|
6532
7086
|
try {
|
|
6533
|
-
const raw = await
|
|
7087
|
+
const raw = await readFile25(graphPath, "utf8");
|
|
6534
7088
|
return JSON.parse(raw);
|
|
6535
7089
|
} catch {
|
|
6536
7090
|
return null;
|
|
@@ -6538,14 +7092,14 @@ async function loadOrBuildGraph(vault) {
|
|
|
6538
7092
|
}
|
|
6539
7093
|
|
|
6540
7094
|
// src/utils/auto-commit.ts
|
|
6541
|
-
import { existsSync as
|
|
6542
|
-
import { join as
|
|
7095
|
+
import { existsSync as existsSync15 } from "fs";
|
|
7096
|
+
import { join as join40 } from "path";
|
|
6543
7097
|
async function postCommit(vault, exitCode) {
|
|
6544
7098
|
if (exitCode !== 0) return;
|
|
6545
7099
|
const home = process.env.HOME ?? "";
|
|
6546
7100
|
const dotenv = await parseDotenvFile(configPath(home));
|
|
6547
7101
|
if (dotenv["AUTO_COMMIT"] === "false") return;
|
|
6548
|
-
if (!
|
|
7102
|
+
if (!existsSync15(join40(vault, ".git"))) return;
|
|
6549
7103
|
const lastOps = readLastOp(vault);
|
|
6550
7104
|
if (lastOps.length === 0) return;
|
|
6551
7105
|
const porcelain = git(vault, ["status", "--porcelain"]);
|
|
@@ -6574,7 +7128,7 @@ async function postCommit(vault, exitCode) {
|
|
|
6574
7128
|
}
|
|
6575
7129
|
|
|
6576
7130
|
// src/cli.ts
|
|
6577
|
-
var pkg = JSON.parse(
|
|
7131
|
+
var pkg = JSON.parse(readFileSync11(new URL("../package.json", import.meta.url), "utf8"));
|
|
6578
7132
|
var program = new Command2();
|
|
6579
7133
|
program.name("skillwiki").description("Deterministic helpers for CodeWiki skills").version(pkg.version);
|
|
6580
7134
|
program.option("--human", "render terminal-readable output instead of JSON");
|
|
@@ -6596,7 +7150,7 @@ program.command("validate <file>").description("validate vault page frontmatter
|
|
|
6596
7150
|
emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
|
|
6597
7151
|
});
|
|
6598
7152
|
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) => {
|
|
6599
|
-
const out = opts.out ??
|
|
7153
|
+
const out = opts.out ?? join41(vault, ".skillwiki", "graph.json");
|
|
6600
7154
|
emit(await runGraphBuild({ vault, out }), vault);
|
|
6601
7155
|
});
|
|
6602
7156
|
var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
|
|
@@ -6821,10 +7375,10 @@ program.command("tag-sync [vault]").description("mirror frontmatter enum values
|
|
|
6821
7375
|
else emit(await runTagSync({ vault: v.vault, dryRun: !!opts.dryRun }), v.vault);
|
|
6822
7376
|
});
|
|
6823
7377
|
var syncCmd = program.command("sync").description("manage vault sync");
|
|
6824
|
-
syncCmd.command("status [vault]").description("check vault git sync status").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
7378
|
+
syncCmd.command("status [vault]").description("check vault git sync status").option("--wiki <name>", "wiki profile name").option("--include-stashes", "enumerate all stashes in output", false).action(async (vault, opts) => {
|
|
6825
7379
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6826
7380
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6827
|
-
else emit(runSyncStatus({ vault: v.vault }));
|
|
7381
|
+
else emit(runSyncStatus({ vault: v.vault, includeStashes: !!opts.includeStashes }));
|
|
6828
7382
|
});
|
|
6829
7383
|
syncCmd.command("push [vault]").description("lint, commit, and push vault changes to remote").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6830
7384
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
@@ -6836,6 +7390,24 @@ syncCmd.command("pull [vault]").description("pull remote vault changes and lint"
|
|
|
6836
7390
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6837
7391
|
else emit(await runSyncPull({ vault: v.vault }), v.vault);
|
|
6838
7392
|
});
|
|
7393
|
+
syncCmd.command("lock [vault]").description("acquire advisory lock on vault").option("--summary <text>", "lock description", "skillwiki sync").option("--ttl-minutes <n>", "lock time-to-live in minutes", "30").option("--force", "overwrite existing lock", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
7394
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
7395
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
7396
|
+
else {
|
|
7397
|
+
const ttl = parseInt(opts.ttlMinutes, 10) || 30;
|
|
7398
|
+
emit(runSyncLock({ vault: v.vault, summary: opts.summary, ttlMinutes: ttl, force: !!opts.force }));
|
|
7399
|
+
}
|
|
7400
|
+
});
|
|
7401
|
+
syncCmd.command("unlock [vault]").description("release advisory lock on vault").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
7402
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
7403
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
7404
|
+
else emit(runSyncUnlock({ vault: v.vault }));
|
|
7405
|
+
});
|
|
7406
|
+
syncCmd.command("peers [vault]").description("list active locks and recent wiki-sync stashes").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
7407
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
7408
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
7409
|
+
else emit(runSyncPeers({ vault: v.vault }));
|
|
7410
|
+
});
|
|
6839
7411
|
var backupCmd = program.command("backup").description("manage S3-compatible remote backup");
|
|
6840
7412
|
backupCmd.command("sync [vault]").description("sync vault to S3-compatible remote backup").option("--dry-run", "list actions without executing").option("--bucket <name>", "S3 bucket name").option("--endpoint <url>", "S3 endpoint URL").option("--region <region>", "S3 region", "us-east-1").option("--prune", "delete orphaned S3 objects not in vault", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6841
7413
|
const v = await resolveVaultArg(vault, opts.wiki);
|