skillwiki 0.9.61 → 0.9.62
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/{chunk-FU462DVS.js → chunk-2PENIQ3A.js} +1181 -518
- package/dist/cli.js +994 -438
- package/dist/skillwiki-mcp.js +1 -1
- package/package.json +1 -1
- package/skills/.claude-plugin/plugin.json +1 -1
- package/skills/.codex-plugin/plugin.json +1 -1
- package/skills/README.md +6 -0
- package/skills/agents/wiki-ingest.md +30 -37
- package/skills/agents/wiki-query.md +13 -3
- package/skills/package.json +1 -1
- package/skills/skills/using-skillwiki/SKILL.md +16 -1
- package/skills/skills/wiki-ingest/SKILL.md +23 -27
- package/skills/skills/wiki-query/SKILL.md +11 -2
- package/skills/using-skillwiki/SKILL.md +16 -1
- package/skills/wiki-ingest/SKILL.md +23 -27
- package/skills/wiki-query/SKILL.md +11 -2
package/dist/cli.js
CHANGED
|
@@ -3,15 +3,18 @@ import {
|
|
|
3
3
|
ExitCode,
|
|
4
4
|
FLEET_REL_PATH,
|
|
5
5
|
MetaSchema,
|
|
6
|
+
RawSourceSchema,
|
|
6
7
|
SATELLITE_STALE_MS,
|
|
7
|
-
|
|
8
|
+
acquireLock,
|
|
9
|
+
acquireOwnedSyncLock,
|
|
8
10
|
appendLastOp,
|
|
11
|
+
assertTargetInsideVault,
|
|
9
12
|
assessSourceIdentity,
|
|
13
|
+
atomicWriteText,
|
|
10
14
|
buildDegradedReasons,
|
|
11
15
|
buildRemoteObjectPath,
|
|
12
16
|
clearLastOp,
|
|
13
17
|
configPath,
|
|
14
|
-
detectSchema,
|
|
15
18
|
err,
|
|
16
19
|
evaluateSatelliteRunHealth,
|
|
17
20
|
extractCitationMarkers,
|
|
@@ -19,6 +22,8 @@ import {
|
|
|
19
22
|
extractTaxonomy,
|
|
20
23
|
findPlugin,
|
|
21
24
|
fixPathTooLong,
|
|
25
|
+
getCliSessionId,
|
|
26
|
+
getSessionId,
|
|
22
27
|
isBlockedHost,
|
|
23
28
|
isFailedRunStatus,
|
|
24
29
|
isValidRemoteDeleteCap,
|
|
@@ -29,12 +34,19 @@ import {
|
|
|
29
34
|
parseDotenvFile,
|
|
30
35
|
parseDotenvText,
|
|
31
36
|
planAndMaybePruneRemoteObjects,
|
|
37
|
+
prepareTypedPage,
|
|
32
38
|
probeRemoteHealth,
|
|
33
39
|
profileKey,
|
|
34
40
|
readCliPackageJson,
|
|
35
41
|
readLastOp,
|
|
42
|
+
readLock,
|
|
36
43
|
readPage,
|
|
37
44
|
readSatelliteLatestRunFromText,
|
|
45
|
+
reconcileTaxonomyDocument,
|
|
46
|
+
redactSensitiveContent,
|
|
47
|
+
releaseLock,
|
|
48
|
+
releaseOwnedSyncLock,
|
|
49
|
+
renderIndexUpsert,
|
|
38
50
|
resolveFleetHostId,
|
|
39
51
|
resolveInitTimePath,
|
|
40
52
|
resolveRuntimePath,
|
|
@@ -53,6 +65,7 @@ import {
|
|
|
53
65
|
runIndexLinkFormat,
|
|
54
66
|
runLinks,
|
|
55
67
|
runLint,
|
|
68
|
+
runLogAppend,
|
|
56
69
|
runLogRotate,
|
|
57
70
|
runMemoryImport,
|
|
58
71
|
runMemoryIndex,
|
|
@@ -76,8 +89,10 @@ import {
|
|
|
76
89
|
scanVault,
|
|
77
90
|
snapshotterAliasForLocalHost,
|
|
78
91
|
splitFrontmatter,
|
|
92
|
+
taxonomyCommentForPage,
|
|
93
|
+
upsertIndexEntry,
|
|
79
94
|
writeDotenv
|
|
80
|
-
} from "./chunk-
|
|
95
|
+
} from "./chunk-2PENIQ3A.js";
|
|
81
96
|
import {
|
|
82
97
|
normalizeDistTag,
|
|
83
98
|
readCache,
|
|
@@ -87,7 +102,7 @@ import {
|
|
|
87
102
|
} from "./chunk-7I2TPIV5.js";
|
|
88
103
|
|
|
89
104
|
// src/cli.ts
|
|
90
|
-
import { join as
|
|
105
|
+
import { join as join25 } from "path";
|
|
91
106
|
import { Command } from "commander";
|
|
92
107
|
|
|
93
108
|
// src/utils/output.ts
|
|
@@ -853,110 +868,9 @@ async function runClaim(input) {
|
|
|
853
868
|
};
|
|
854
869
|
}
|
|
855
870
|
|
|
856
|
-
// src/commands/log-append.ts
|
|
857
|
-
import { readFile as readFile5, rename as rename2, writeFile as writeFile4, stat as stat3 } from "fs/promises";
|
|
858
|
-
import { join as join8 } from "path";
|
|
859
|
-
|
|
860
|
-
// src/utils/log-lock.ts
|
|
861
|
-
import { existsSync as existsSync2, mkdirSync, statSync as statSync2, unlinkSync, writeFileSync } from "fs";
|
|
862
|
-
import { join as join7 } from "path";
|
|
863
|
-
function logLockPath(vault) {
|
|
864
|
-
return join7(vault, ".skillwiki", "log-append.lock");
|
|
865
|
-
}
|
|
866
|
-
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
867
|
-
async function acquireLogLock(vault, opts = {}) {
|
|
868
|
-
const retryMs = opts.retryMs ?? 2e3;
|
|
869
|
-
const pollMs = opts.pollMs ?? 50;
|
|
870
|
-
const staleMs = opts.staleMs ?? 1e4;
|
|
871
|
-
const path = logLockPath(vault);
|
|
872
|
-
const dir = join7(vault, ".skillwiki");
|
|
873
|
-
if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
|
|
874
|
-
const deadline = Date.now() + retryMs;
|
|
875
|
-
const content = JSON.stringify({ pid: process.pid, acquired: (/* @__PURE__ */ new Date()).toISOString() }) + "\n";
|
|
876
|
-
for (; ; ) {
|
|
877
|
-
try {
|
|
878
|
-
writeFileSync(path, content, { flag: "wx" });
|
|
879
|
-
return { ok: true };
|
|
880
|
-
} catch (e) {
|
|
881
|
-
const err2 = e;
|
|
882
|
-
if (err2.code !== "EEXIST") throw err2;
|
|
883
|
-
}
|
|
884
|
-
try {
|
|
885
|
-
const age = Date.now() - statSync2(path).mtimeMs;
|
|
886
|
-
if (age > staleMs) {
|
|
887
|
-
unlinkSync(path);
|
|
888
|
-
continue;
|
|
889
|
-
}
|
|
890
|
-
} catch {
|
|
891
|
-
continue;
|
|
892
|
-
}
|
|
893
|
-
if (Date.now() >= deadline) return { ok: false };
|
|
894
|
-
await sleep(pollMs);
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
function releaseLogLock(vault) {
|
|
898
|
-
try {
|
|
899
|
-
unlinkSync(logLockPath(vault));
|
|
900
|
-
} catch {
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
// src/commands/log-append.ts
|
|
905
|
-
var ENTRY_RE = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
|
|
906
|
-
async function runLogAppend(input) {
|
|
907
|
-
try {
|
|
908
|
-
await stat3(join8(input.vault, "SCHEMA.md"));
|
|
909
|
-
} catch {
|
|
910
|
-
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
911
|
-
}
|
|
912
|
-
const content = (input.content ?? "").trim();
|
|
913
|
-
if (content.length === 0) {
|
|
914
|
-
return { exitCode: ExitCode.USAGE, result: err("USAGE", { message: "--content must be a non-empty log entry" }) };
|
|
915
|
-
}
|
|
916
|
-
const acquired = await acquireLogLock(input.vault);
|
|
917
|
-
if (!acquired.ok) {
|
|
918
|
-
return { exitCode: ExitCode.LOG_APPEND_LOCK_HELD, result: err("LOG_APPEND_LOCK_HELD", { vault: input.vault }) };
|
|
919
|
-
}
|
|
920
|
-
const logPath = join8(input.vault, "log.md");
|
|
921
|
-
try {
|
|
922
|
-
let logText;
|
|
923
|
-
try {
|
|
924
|
-
logText = await readFile5(logPath, "utf8");
|
|
925
|
-
} catch {
|
|
926
|
-
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
|
|
927
|
-
}
|
|
928
|
-
const entriesBefore = [...logText.matchAll(ENTRY_RE)].length;
|
|
929
|
-
const body = logText.replace(/\s+$/, "");
|
|
930
|
-
const next = `${body}
|
|
931
|
-
|
|
932
|
-
${content}
|
|
933
|
-
`;
|
|
934
|
-
try {
|
|
935
|
-
const tmp = logPath + ".tmp";
|
|
936
|
-
await writeFile4(tmp, next, "utf8");
|
|
937
|
-
await rename2(tmp, logPath);
|
|
938
|
-
} catch (e) {
|
|
939
|
-
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
|
|
940
|
-
}
|
|
941
|
-
appendLastOp(input.vault, {
|
|
942
|
-
operation: "log-append",
|
|
943
|
-
summary: `appended log entry (${entriesBefore}->${entriesBefore + 1})`,
|
|
944
|
-
files: ["log.md"],
|
|
945
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
946
|
-
});
|
|
947
|
-
const entriesAfter = entriesBefore + 1;
|
|
948
|
-
return {
|
|
949
|
-
exitCode: ExitCode.OK,
|
|
950
|
-
result: ok({ entries_before: entriesBefore, entries_after: entriesAfter, appended: true, humanHint: `appended log entry (${entriesBefore}->${entriesAfter})` })
|
|
951
|
-
};
|
|
952
|
-
} finally {
|
|
953
|
-
releaseLogLock(input.vault);
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
|
|
957
871
|
// src/commands/health.ts
|
|
958
|
-
import { existsSync as
|
|
959
|
-
import { dirname as dirname4, join as
|
|
872
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
|
|
873
|
+
import { dirname as dirname4, join as join7, resolve as resolve2 } from "path";
|
|
960
874
|
import { platform } from "os";
|
|
961
875
|
function statusFromCounts(counts) {
|
|
962
876
|
if ((counts.error ?? 0) > 0) return "error";
|
|
@@ -1067,7 +981,7 @@ function summarizeChecks(checks) {
|
|
|
1067
981
|
};
|
|
1068
982
|
}
|
|
1069
983
|
function classifyLog(path, id, label, okPattern) {
|
|
1070
|
-
if (!
|
|
984
|
+
if (!existsSync2(path)) return { id, label, status: "warn", detail: `log file missing: ${path}` };
|
|
1071
985
|
const lines = readFileSync2(path, "utf8").split(/\r?\n/).filter(Boolean);
|
|
1072
986
|
if (lines.length === 0) return { id, label, status: "warn", detail: `log file empty: ${path}` };
|
|
1073
987
|
const statusLine = [...lines].reverse().find(
|
|
@@ -1089,12 +1003,12 @@ function runVaultSyncHealth(home, syncMode) {
|
|
|
1089
1003
|
};
|
|
1090
1004
|
}
|
|
1091
1005
|
const isMac = platform() === "darwin";
|
|
1092
|
-
const shareDir = isMac ?
|
|
1093
|
-
const logDir = isMac ?
|
|
1094
|
-
const filterPath =
|
|
1006
|
+
const shareDir = isMac ? join7(home, "Library", "Application Support", "vault-sync", "bin") : join7(home, ".local", "share", "vault-sync", "bin");
|
|
1007
|
+
const logDir = isMac ? join7(home, "Library", "Logs") : join7(home, ".local", "state", "vault-sync", "log");
|
|
1008
|
+
const filterPath = join7(home, ".config", "rclone", "wiki-push-filters.txt");
|
|
1095
1009
|
const checks = [];
|
|
1096
|
-
const pushScript =
|
|
1097
|
-
if (syncMode === "optional" && !
|
|
1010
|
+
const pushScript = join7(shareDir, "wiki-push.sh");
|
|
1011
|
+
if (syncMode === "optional" && !existsSync2(pushScript)) {
|
|
1098
1012
|
return {
|
|
1099
1013
|
status: "pass",
|
|
1100
1014
|
blocking: false,
|
|
@@ -1107,23 +1021,23 @@ function runVaultSyncHealth(home, syncMode) {
|
|
|
1107
1021
|
}]
|
|
1108
1022
|
};
|
|
1109
1023
|
}
|
|
1110
|
-
checks.push(
|
|
1024
|
+
checks.push(existsSync2(pushScript) ? { id: "vault_sync_installed", label: "Vault sync installed", status: "pass", detail: `Found: ${pushScript}` } : { id: "vault_sync_installed", label: "Vault sync installed", status: "error", detail: `Script missing: ${pushScript}` });
|
|
1111
1025
|
if (isMac) {
|
|
1112
|
-
const pushPlist =
|
|
1113
|
-
const fetchPlist =
|
|
1114
|
-
checks.push(
|
|
1026
|
+
const pushPlist = join7(home, "Library", "LaunchAgents", "com.karlchow.wiki-push.plist");
|
|
1027
|
+
const fetchPlist = join7(home, "Library", "LaunchAgents", "com.karlchow.wiki-fetch.plist");
|
|
1028
|
+
checks.push(existsSync2(pushPlist) && existsSync2(fetchPlist) ? { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "pass", detail: "launchd unit files present (read-only mode)" } : { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "warn", detail: "launchd unit files missing (read-only mode)" });
|
|
1115
1029
|
checks.push({ id: "vault_sync_fuse_refresh_job", label: "Vault sync fuse refresh job", status: "pass", detail: "macOS host \u2014 check skipped" });
|
|
1116
1030
|
} else {
|
|
1117
|
-
const pushTimer =
|
|
1118
|
-
const fetchTimer =
|
|
1119
|
-
const fuseTimer =
|
|
1120
|
-
const fuseService =
|
|
1121
|
-
checks.push(
|
|
1122
|
-
checks.push(
|
|
1123
|
-
}
|
|
1124
|
-
checks.push(classifyLog(
|
|
1125
|
-
checks.push(classifyLog(
|
|
1126
|
-
if (!
|
|
1031
|
+
const pushTimer = join7(home, ".config", "systemd", "user", "wiki-push.timer");
|
|
1032
|
+
const fetchTimer = join7(home, ".config", "systemd", "user", "wiki-fetch.timer");
|
|
1033
|
+
const fuseTimer = join7(home, ".config", "systemd", "user", "wiki-fuse-refresh.timer");
|
|
1034
|
+
const fuseService = join7(home, ".config", "systemd", "user", "wiki-fuse-refresh.service");
|
|
1035
|
+
checks.push(existsSync2(pushTimer) && existsSync2(fetchTimer) ? { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "pass", detail: "systemd timer unit files present (read-only mode)" } : { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "warn", detail: "systemd timer unit files missing (read-only mode)" });
|
|
1036
|
+
checks.push(existsSync2(fuseTimer) && existsSync2(fuseService) ? { id: "vault_sync_fuse_refresh_job", label: "Vault sync fuse refresh job", status: "pass", detail: "wiki-fuse-refresh unit files present (read-only mode)" } : { id: "vault_sync_fuse_refresh_job", label: "Vault sync fuse refresh job", status: "warn", detail: "wiki-fuse-refresh unit files missing (read-only mode)" });
|
|
1037
|
+
}
|
|
1038
|
+
checks.push(classifyLog(join7(logDir, "wiki-push.log"), "vault_sync_last_push_age", "Vault sync last push recency", /OK push/));
|
|
1039
|
+
checks.push(classifyLog(join7(logDir, "wiki-fetch.log"), "vault_sync_last_fetch_status", "Vault sync last fetch status", /NOTIFY|OK behind|OK/));
|
|
1040
|
+
if (!existsSync2(filterPath)) {
|
|
1127
1041
|
checks.push({ id: "vault_sync_filter_present", label: "Vault sync filter file present", status: "error", detail: `Filter missing: ${filterPath}` });
|
|
1128
1042
|
} else {
|
|
1129
1043
|
const content = readFileSync2(filterPath, "utf8");
|
|
@@ -1219,9 +1133,9 @@ function buildHumanHint(report) {
|
|
|
1219
1133
|
return lines.join("\n");
|
|
1220
1134
|
}
|
|
1221
1135
|
function writeReport(out, report) {
|
|
1222
|
-
|
|
1136
|
+
mkdirSync(dirname4(out), { recursive: true });
|
|
1223
1137
|
const tmp = `${out}.tmp-${process.pid}-${Date.now()}`;
|
|
1224
|
-
|
|
1138
|
+
writeFileSync(tmp, JSON.stringify(ok(report), null, 2) + "\n", "utf8");
|
|
1225
1139
|
renameSync(tmp, out);
|
|
1226
1140
|
}
|
|
1227
1141
|
function buildIncompleteReport(input, syncMode) {
|
|
@@ -1445,8 +1359,8 @@ async function runHealth(input) {
|
|
|
1445
1359
|
}
|
|
1446
1360
|
|
|
1447
1361
|
// src/commands/archive.ts
|
|
1448
|
-
import { rename as
|
|
1449
|
-
import { join as
|
|
1362
|
+
import { rename as rename2, mkdir as mkdir5, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
|
|
1363
|
+
import { join as join8, dirname as dirname5 } from "path";
|
|
1450
1364
|
function countWikilinks(body, slug) {
|
|
1451
1365
|
const escaped = slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1452
1366
|
const re = new RegExp(`\\[\\[${escaped}(?:[|#][^\\]]*)?\\]\\]`, "g");
|
|
@@ -1480,7 +1394,7 @@ async function runArchive(input) {
|
|
|
1480
1394
|
if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
|
|
1481
1395
|
if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
|
|
1482
1396
|
const slug = relPath.replace(/\.md$/, "").split("/").pop();
|
|
1483
|
-
const archivePath =
|
|
1397
|
+
const archivePath = join8("_archive", relPath).replace(/\\/g, "/");
|
|
1484
1398
|
const remoteRoot = normalizeRemoteRoot(input.remote);
|
|
1485
1399
|
const remoteObjectPath = buildRemoteObjectPath(remoteRoot, relPath);
|
|
1486
1400
|
let cascade;
|
|
@@ -1506,7 +1420,7 @@ async function runArchive(input) {
|
|
|
1506
1420
|
const indexRefs = [];
|
|
1507
1421
|
if (!isRaw) {
|
|
1508
1422
|
try {
|
|
1509
|
-
const idx = await
|
|
1423
|
+
const idx = await readFile5(join8(input.vault, "index.md"), "utf8");
|
|
1510
1424
|
idx.split("\n").forEach((line, i) => {
|
|
1511
1425
|
if (line.includes(`[[${slug}]]`)) indexRefs.push({ line: i + 1, text: line });
|
|
1512
1426
|
});
|
|
@@ -1533,8 +1447,8 @@ async function runArchive(input) {
|
|
|
1533
1447
|
}
|
|
1534
1448
|
if (input.cascade && input.apply && cascade) {
|
|
1535
1449
|
for (const ref of cascade.source_array_refs) {
|
|
1536
|
-
const absPath =
|
|
1537
|
-
const text = await
|
|
1450
|
+
const absPath = join8(input.vault, ref.page);
|
|
1451
|
+
const text = await readFile5(absPath, "utf8");
|
|
1538
1452
|
const split = splitFrontmatter(text);
|
|
1539
1453
|
if (!split.ok) continue;
|
|
1540
1454
|
const before = split.data.rawFrontmatter;
|
|
@@ -1545,29 +1459,29 @@ async function runArchive(input) {
|
|
|
1545
1459
|
);
|
|
1546
1460
|
if (fmRewritten === before) continue;
|
|
1547
1461
|
if (!arraysEqual(ref.sources_after, ref.sources_before)) {
|
|
1548
|
-
await
|
|
1462
|
+
await writeFile4(absPath, `---
|
|
1549
1463
|
${fmRewritten}
|
|
1550
1464
|
---${split.data.body}`, "utf8");
|
|
1551
1465
|
}
|
|
1552
1466
|
}
|
|
1553
1467
|
}
|
|
1554
|
-
await mkdir5(dirname5(
|
|
1468
|
+
await mkdir5(dirname5(join8(input.vault, archivePath)), { recursive: true });
|
|
1555
1469
|
let indexUpdated = false;
|
|
1556
1470
|
if (!isRaw) {
|
|
1557
|
-
const indexPath =
|
|
1471
|
+
const indexPath = join8(input.vault, "index.md");
|
|
1558
1472
|
try {
|
|
1559
|
-
const idx = await
|
|
1473
|
+
const idx = await readFile5(indexPath, "utf8");
|
|
1560
1474
|
const originalLines = idx.split("\n");
|
|
1561
1475
|
const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
|
|
1562
1476
|
if (filtered.length !== originalLines.length) {
|
|
1563
|
-
await
|
|
1477
|
+
await writeFile4(indexPath, filtered.join("\n"), "utf8");
|
|
1564
1478
|
indexUpdated = true;
|
|
1565
1479
|
}
|
|
1566
1480
|
} catch (e) {
|
|
1567
1481
|
if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
|
|
1568
1482
|
}
|
|
1569
1483
|
}
|
|
1570
|
-
await
|
|
1484
|
+
await rename2(join8(input.vault, relPath), join8(input.vault, archivePath));
|
|
1571
1485
|
appendLastOp(input.vault, {
|
|
1572
1486
|
operation: input.cascade ? "archive-cascade" : "archive",
|
|
1573
1487
|
summary: `moved ${relPath} to ${archivePath}${input.cascade ? ` (cascade: ${cascade?.source_array_refs.length ?? 0} source arrays updated)` : ""}`,
|
|
@@ -1911,14 +1825,14 @@ ${migratedBody}${newFooter}`;
|
|
|
1911
1825
|
|
|
1912
1826
|
// src/commands/update.ts
|
|
1913
1827
|
import { execSync } from "child_process";
|
|
1914
|
-
import { join as
|
|
1828
|
+
import { join as join9 } from "path";
|
|
1915
1829
|
function resolveGlobalSkillsRoot() {
|
|
1916
1830
|
try {
|
|
1917
1831
|
const globalRoot = execSync("npm root -g", {
|
|
1918
1832
|
encoding: "utf8",
|
|
1919
1833
|
timeout: 5e3
|
|
1920
1834
|
}).trim();
|
|
1921
|
-
return
|
|
1835
|
+
return join9(globalRoot, "skillwiki", "skills");
|
|
1922
1836
|
} catch {
|
|
1923
1837
|
return null;
|
|
1924
1838
|
}
|
|
@@ -1946,7 +1860,7 @@ async function runUpdate(input) {
|
|
|
1946
1860
|
const pkg2 = readCliPackageJson();
|
|
1947
1861
|
const currentVersion = pkg2.version;
|
|
1948
1862
|
const tag = normalizeDistTag(input.distTag);
|
|
1949
|
-
const target =
|
|
1863
|
+
const target = join9(input.home, ".claude", "skills");
|
|
1950
1864
|
let latest;
|
|
1951
1865
|
try {
|
|
1952
1866
|
latest = execSync(`npm view skillwiki@${tag} version`, {
|
|
@@ -2023,15 +1937,15 @@ async function runUpdate(input) {
|
|
|
2023
1937
|
|
|
2024
1938
|
// src/commands/self-update.ts
|
|
2025
1939
|
import { execSync as execSync2 } from "child_process";
|
|
2026
|
-
import { existsSync as
|
|
2027
|
-
import { join as
|
|
1940
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
1941
|
+
import { join as join10 } from "path";
|
|
2028
1942
|
var DEFAULT_SOURCE_ROOT_SUFFIX = "/Desktop/code/llm-wiki";
|
|
2029
1943
|
async function runSelfUpdate(input) {
|
|
2030
1944
|
const currentVersion = readCliPackageJson().version;
|
|
2031
1945
|
const sourceRoot = input.sourceRoot ?? `${input.home}${DEFAULT_SOURCE_ROOT_SUFFIX}`;
|
|
2032
1946
|
const distTag = normalizeDistTag(input.distTag);
|
|
2033
|
-
const localPkgPath =
|
|
2034
|
-
const hasLocalSource =
|
|
1947
|
+
const localPkgPath = join10(sourceRoot, "packages", "cli", "package.json");
|
|
1948
|
+
const hasLocalSource = existsSync3(localPkgPath);
|
|
2035
1949
|
if (input.check) {
|
|
2036
1950
|
let availableVersion = null;
|
|
2037
1951
|
let source;
|
|
@@ -2162,10 +2076,10 @@ async function runSelfUpdate(input) {
|
|
|
2162
2076
|
}
|
|
2163
2077
|
|
|
2164
2078
|
// src/commands/transcripts.ts
|
|
2165
|
-
import { readdir as readdir3, stat as
|
|
2166
|
-
import { join as
|
|
2079
|
+
import { readdir as readdir3, stat as stat3, readFile as readFile6 } from "fs/promises";
|
|
2080
|
+
import { join as join11 } from "path";
|
|
2167
2081
|
async function runTranscripts(input) {
|
|
2168
|
-
const dir =
|
|
2082
|
+
const dir = join11(input.vault, "raw", "transcripts");
|
|
2169
2083
|
let entries;
|
|
2170
2084
|
try {
|
|
2171
2085
|
entries = await readdir3(dir, { withFileTypes: true });
|
|
@@ -2175,13 +2089,13 @@ async function runTranscripts(input) {
|
|
|
2175
2089
|
const transcripts = [];
|
|
2176
2090
|
for (const entry of entries) {
|
|
2177
2091
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
2178
|
-
const filePath =
|
|
2179
|
-
const content = await
|
|
2092
|
+
const filePath = join11(dir, entry.name);
|
|
2093
|
+
const content = await readFile6(filePath, "utf8");
|
|
2180
2094
|
const fm = extractFrontmatter(content);
|
|
2181
2095
|
if (!fm.ok) continue;
|
|
2182
2096
|
const ingested = typeof fm.data.ingested === "string" ? fm.data.ingested : "";
|
|
2183
2097
|
if (input.since && ingested && ingested < input.since) continue;
|
|
2184
|
-
const s = await
|
|
2098
|
+
const s = await stat3(filePath);
|
|
2185
2099
|
transcripts.push({
|
|
2186
2100
|
file: `raw/transcripts/${entry.name}`,
|
|
2187
2101
|
ingested,
|
|
@@ -2193,10 +2107,10 @@ async function runTranscripts(input) {
|
|
|
2193
2107
|
}
|
|
2194
2108
|
|
|
2195
2109
|
// src/commands/compound.ts
|
|
2196
|
-
import { writeFile as
|
|
2197
|
-
import { join as
|
|
2198
|
-
import { existsSync as
|
|
2199
|
-
import { readFile as
|
|
2110
|
+
import { writeFile as writeFile5, mkdir as mkdir6, readdir as readdir4, unlink as unlink2 } from "fs/promises";
|
|
2111
|
+
import { join as join12 } from "path";
|
|
2112
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2113
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
2200
2114
|
var RETRO_HEADING_RE = /^## \[(\d{4}-\d{2}-\d{2})(?:\s+[^\]]+)?\] retro \| loop cycle(?: (\d+))?: (.+)$/;
|
|
2201
2115
|
var FIELD_RE = {
|
|
2202
2116
|
improve: /^-\s+\*?\*?Improve:?\*?\*?\s*(.+)$/m,
|
|
@@ -2294,17 +2208,17 @@ function extractRetroFields(date, cycleName, block) {
|
|
|
2294
2208
|
};
|
|
2295
2209
|
}
|
|
2296
2210
|
async function runCompound(input) {
|
|
2297
|
-
const logPath =
|
|
2211
|
+
const logPath = join12(input.vault, "log.md");
|
|
2298
2212
|
let logText;
|
|
2299
2213
|
try {
|
|
2300
|
-
logText = await
|
|
2214
|
+
logText = await readFile7(logPath, "utf8");
|
|
2301
2215
|
} catch {
|
|
2302
2216
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
|
|
2303
2217
|
}
|
|
2304
2218
|
const entries = parseRetroEntries(logText);
|
|
2305
2219
|
const promoted = [];
|
|
2306
2220
|
const skipped = [];
|
|
2307
|
-
const compoundDir =
|
|
2221
|
+
const compoundDir = join12(input.vault, "projects", input.project, "compound");
|
|
2308
2222
|
for (const entry of entries) {
|
|
2309
2223
|
const generalizeValue = entry.generalize.trim();
|
|
2310
2224
|
if (!/^yes/i.test(generalizeValue)) {
|
|
@@ -2312,8 +2226,8 @@ async function runCompound(input) {
|
|
|
2312
2226
|
continue;
|
|
2313
2227
|
}
|
|
2314
2228
|
const slug = slugify(entry.cycleName);
|
|
2315
|
-
const compoundPath =
|
|
2316
|
-
if (
|
|
2229
|
+
const compoundPath = join12(compoundDir, `${slug}.md`);
|
|
2230
|
+
if (existsSync4(compoundPath)) {
|
|
2317
2231
|
skipped.push(entry.date);
|
|
2318
2232
|
continue;
|
|
2319
2233
|
}
|
|
@@ -2351,10 +2265,10 @@ async function runCompound(input) {
|
|
|
2351
2265
|
].join("\n");
|
|
2352
2266
|
const content = frontmatter + "\n" + body;
|
|
2353
2267
|
if (!input.dryRun) {
|
|
2354
|
-
if (!
|
|
2268
|
+
if (!existsSync4(compoundDir)) {
|
|
2355
2269
|
await mkdir6(compoundDir, { recursive: true });
|
|
2356
2270
|
}
|
|
2357
|
-
await
|
|
2271
|
+
await writeFile5(compoundPath, content, "utf8");
|
|
2358
2272
|
}
|
|
2359
2273
|
promoted.push(`${slug}.md`);
|
|
2360
2274
|
}
|
|
@@ -2373,16 +2287,16 @@ async function runCompound(input) {
|
|
|
2373
2287
|
};
|
|
2374
2288
|
}
|
|
2375
2289
|
async function runCompoundDelete(input) {
|
|
2376
|
-
const projectDir =
|
|
2377
|
-
if (!
|
|
2290
|
+
const projectDir = join12(input.vault, "projects", input.project);
|
|
2291
|
+
if (!existsSync4(projectDir)) {
|
|
2378
2292
|
return {
|
|
2379
2293
|
exitCode: ExitCode.PROJECT_NOT_FOUND,
|
|
2380
2294
|
result: err("PROJECT_NOT_FOUND", { slug: input.project, path: projectDir })
|
|
2381
2295
|
};
|
|
2382
2296
|
}
|
|
2383
2297
|
const entryName = input.entry.replace(/\.md$/, "");
|
|
2384
|
-
const compoundPath =
|
|
2385
|
-
if (!
|
|
2298
|
+
const compoundPath = join12(projectDir, "compound", `${entryName}.md`);
|
|
2299
|
+
if (!existsSync4(compoundPath)) {
|
|
2386
2300
|
return {
|
|
2387
2301
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
2388
2302
|
result: err("FILE_NOT_FOUND", { path: compoundPath })
|
|
@@ -2415,8 +2329,8 @@ knowledge.md regenerated`
|
|
|
2415
2329
|
};
|
|
2416
2330
|
}
|
|
2417
2331
|
async function runCompoundList(input) {
|
|
2418
|
-
const compoundDir =
|
|
2419
|
-
if (!
|
|
2332
|
+
const compoundDir = join12(input.vault, "projects", input.project, "compound");
|
|
2333
|
+
if (!existsSync4(compoundDir)) {
|
|
2420
2334
|
return {
|
|
2421
2335
|
exitCode: ExitCode.OK,
|
|
2422
2336
|
result: ok({
|
|
@@ -2446,10 +2360,10 @@ could not read compound directory`
|
|
|
2446
2360
|
const entries = [];
|
|
2447
2361
|
for (const dirent of dirents) {
|
|
2448
2362
|
if (!dirent.isFile() || !dirent.name.endsWith(".md")) continue;
|
|
2449
|
-
const filePath =
|
|
2363
|
+
const filePath = join12(compoundDir, dirent.name);
|
|
2450
2364
|
let text;
|
|
2451
2365
|
try {
|
|
2452
|
-
text = await
|
|
2366
|
+
text = await readFile7(filePath, "utf8");
|
|
2453
2367
|
} catch {
|
|
2454
2368
|
continue;
|
|
2455
2369
|
}
|
|
@@ -2478,8 +2392,8 @@ no compound entries found`;
|
|
|
2478
2392
|
}
|
|
2479
2393
|
|
|
2480
2394
|
// src/commands/session-brief.ts
|
|
2481
|
-
import { mkdir as mkdir7, readFile as
|
|
2482
|
-
import { join as
|
|
2395
|
+
import { mkdir as mkdir7, readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
|
|
2396
|
+
import { join as join13, relative, sep } from "path";
|
|
2483
2397
|
var MAX_WORDS = 900;
|
|
2484
2398
|
async function runSessionBrief(input) {
|
|
2485
2399
|
const scan = await scanVault(input.vault);
|
|
@@ -2579,7 +2493,7 @@ async function resolveProject(input) {
|
|
|
2579
2493
|
const envProject = input.env?.SKILLWIKI_PROJECT;
|
|
2580
2494
|
if (envProject) return envProject;
|
|
2581
2495
|
const cwd = input.cwd ?? process.cwd();
|
|
2582
|
-
const projectDotenv = await readProjectSlug(
|
|
2496
|
+
const projectDotenv = await readProjectSlug(join13(cwd, ".skillwiki", ".env"));
|
|
2583
2497
|
if (projectDotenv) return projectDotenv;
|
|
2584
2498
|
const inferred = inferProjectFromPath(input.vault, cwd);
|
|
2585
2499
|
if (inferred) return inferred;
|
|
@@ -2588,7 +2502,7 @@ async function resolveProject(input) {
|
|
|
2588
2502
|
async function readProjectSlug(file) {
|
|
2589
2503
|
let text;
|
|
2590
2504
|
try {
|
|
2591
|
-
text = await
|
|
2505
|
+
text = await readFile8(file, "utf8");
|
|
2592
2506
|
} catch {
|
|
2593
2507
|
return void 0;
|
|
2594
2508
|
}
|
|
@@ -2665,7 +2579,7 @@ async function loadTrendDigests(typedPages) {
|
|
|
2665
2579
|
return out;
|
|
2666
2580
|
}
|
|
2667
2581
|
async function loadSessionPins(vault, project) {
|
|
2668
|
-
const text = await readIfExists(
|
|
2582
|
+
const text = await readIfExists(join13(vault, "meta", "session-pins.md"));
|
|
2669
2583
|
if (!text) return [];
|
|
2670
2584
|
const fm = extractFrontmatter(text);
|
|
2671
2585
|
if (!fm.ok) return [];
|
|
@@ -2782,7 +2696,7 @@ function satelliteHealthWarnings(warning) {
|
|
|
2782
2696
|
return warning ? [warning] : [];
|
|
2783
2697
|
}
|
|
2784
2698
|
async function loadHealthWarnings(vault) {
|
|
2785
|
-
const text = await readIfExists(
|
|
2699
|
+
const text = await readIfExists(join13(vault, ".skillwiki", "health.json"));
|
|
2786
2700
|
if (!text) return [];
|
|
2787
2701
|
try {
|
|
2788
2702
|
const parsed = JSON.parse(text);
|
|
@@ -2795,7 +2709,7 @@ async function loadHealthWarnings(vault) {
|
|
|
2795
2709
|
}
|
|
2796
2710
|
}
|
|
2797
2711
|
async function loadMemoryTopics(vault, project) {
|
|
2798
|
-
const text = await readIfExists(
|
|
2712
|
+
const text = await readIfExists(join13(vault, ".skillwiki", "memory", project, "topics.json"));
|
|
2799
2713
|
if (!text) return [];
|
|
2800
2714
|
try {
|
|
2801
2715
|
const parsed = JSON.parse(text);
|
|
@@ -2812,18 +2726,18 @@ async function loadMemoryTopics(vault, project) {
|
|
|
2812
2726
|
}
|
|
2813
2727
|
}
|
|
2814
2728
|
async function writeBriefArtifacts(vault, input) {
|
|
2815
|
-
const metaPath =
|
|
2816
|
-
const cacheMdPath =
|
|
2817
|
-
const cacheJsonPath =
|
|
2818
|
-
await mkdir7(
|
|
2819
|
-
await mkdir7(
|
|
2729
|
+
const metaPath = join13(vault, "meta", "latest-session-brief.md");
|
|
2730
|
+
const cacheMdPath = join13(vault, ".skillwiki", "session-brief.md");
|
|
2731
|
+
const cacheJsonPath = join13(vault, ".skillwiki", "session-brief.json");
|
|
2732
|
+
await mkdir7(join13(vault, "meta"), { recursive: true });
|
|
2733
|
+
await mkdir7(join13(vault, ".skillwiki"), { recursive: true });
|
|
2820
2734
|
const committed = renderCommittedBrief(input);
|
|
2821
2735
|
const previousComparable = comparableBrief(await readIfExists(metaPath));
|
|
2822
2736
|
const nextComparable = comparableBrief(committed);
|
|
2823
2737
|
const materialChange = previousComparable !== nextComparable;
|
|
2824
|
-
await
|
|
2825
|
-
await
|
|
2826
|
-
await
|
|
2738
|
+
await writeFile6(metaPath, committed, "utf8");
|
|
2739
|
+
await writeFile6(cacheMdPath, input.brief, "utf8");
|
|
2740
|
+
await writeFile6(cacheJsonPath, `${JSON.stringify({
|
|
2827
2741
|
project: input.project,
|
|
2828
2742
|
brief: input.brief,
|
|
2829
2743
|
word_count: input.wordCount,
|
|
@@ -2877,7 +2791,7 @@ function renderCommittedBrief(input) {
|
|
|
2877
2791
|
].filter((line) => line !== "").join("\n");
|
|
2878
2792
|
}
|
|
2879
2793
|
async function ensureIndexEntry(vault) {
|
|
2880
|
-
const indexPath =
|
|
2794
|
+
const indexPath = join13(vault, "index.md");
|
|
2881
2795
|
let text = await readIfExists(indexPath);
|
|
2882
2796
|
if (!text) return false;
|
|
2883
2797
|
if (text.includes("[[meta/latest-session-brief]]")) return false;
|
|
@@ -2892,21 +2806,21 @@ async function ensureIndexEntry(vault) {
|
|
|
2892
2806
|
while (insertAt < lines.length && !lines[insertAt].startsWith("## ")) insertAt++;
|
|
2893
2807
|
lines.splice(insertAt, 0, entry);
|
|
2894
2808
|
}
|
|
2895
|
-
await
|
|
2809
|
+
await writeFile6(indexPath, lines.join("\n"), "utf8");
|
|
2896
2810
|
return true;
|
|
2897
2811
|
}
|
|
2898
2812
|
async function appendMaterialLog(vault, today) {
|
|
2899
|
-
const logPath =
|
|
2813
|
+
const logPath = join13(vault, "log.md");
|
|
2900
2814
|
const text = await readIfExists(logPath);
|
|
2901
2815
|
if (!text) return false;
|
|
2902
2816
|
const entry = `
|
|
2903
2817
|
## [${today}] session-brief | refreshed: meta/latest-session-brief.md`;
|
|
2904
|
-
await
|
|
2818
|
+
await writeFile6(logPath, text.trimEnd() + entry + "\n", "utf8");
|
|
2905
2819
|
return true;
|
|
2906
2820
|
}
|
|
2907
2821
|
async function readIfExists(path) {
|
|
2908
2822
|
try {
|
|
2909
|
-
return await
|
|
2823
|
+
return await readFile8(path, "utf8");
|
|
2910
2824
|
} catch {
|
|
2911
2825
|
return "";
|
|
2912
2826
|
}
|
|
@@ -2948,9 +2862,419 @@ function dateFromPath(path) {
|
|
|
2948
2862
|
}
|
|
2949
2863
|
|
|
2950
2864
|
// src/commands/ingest.ts
|
|
2951
|
-
import { readFile as readFile10,
|
|
2952
|
-
import { join as
|
|
2865
|
+
import { readFile as readFile10, open, unlink as unlink3, mkdir as mkdir8 } from "fs/promises";
|
|
2866
|
+
import { join as join15 } from "path";
|
|
2867
|
+
import { createHash as createHash4 } from "crypto";
|
|
2868
|
+
|
|
2869
|
+
// src/commands/page-publish.ts
|
|
2953
2870
|
import { createHash as createHash3 } from "crypto";
|
|
2871
|
+
import { realpathSync } from "fs";
|
|
2872
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
2873
|
+
import { join as join14, resolve as resolve3 } from "path";
|
|
2874
|
+
var DEFAULT_DEPS = { afterStage: async () => void 0 };
|
|
2875
|
+
function errorExitCode(error) {
|
|
2876
|
+
switch (error) {
|
|
2877
|
+
case "FILE_NOT_FOUND":
|
|
2878
|
+
return ExitCode.FILE_NOT_FOUND;
|
|
2879
|
+
case "MISSING_CLOSING_DELIMITER":
|
|
2880
|
+
return ExitCode.MISSING_CLOSING_DELIMITER;
|
|
2881
|
+
case "SCHEME_REJECTED":
|
|
2882
|
+
case "NO_TAXONOMY_BLOCK":
|
|
2883
|
+
return ExitCode.SCHEME_REJECTED;
|
|
2884
|
+
case "VAULT_PATH_INVALID":
|
|
2885
|
+
return ExitCode.VAULT_PATH_INVALID;
|
|
2886
|
+
case "SENSITIVE_CONTENT_DETECTED":
|
|
2887
|
+
return ExitCode.SENSITIVE_CONTENT_DETECTED;
|
|
2888
|
+
case "SYNC_LOCK_HELD":
|
|
2889
|
+
return ExitCode.SYNC_LOCK_HELD;
|
|
2890
|
+
case "WRITE_FAILED":
|
|
2891
|
+
return ExitCode.WRITE_FAILED;
|
|
2892
|
+
default:
|
|
2893
|
+
return ExitCode.INVALID_FRONTMATTER;
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
function publicationId(target, content, logNote = "") {
|
|
2897
|
+
return createHash3("sha256").update("skillwiki-page-publish-v1\0").update(target).update("\0").update(content).update("\0").update(logNote).digest("hex");
|
|
2898
|
+
}
|
|
2899
|
+
function prepareFrozenPublication(input, source) {
|
|
2900
|
+
const target = assertTargetInsideVault(input.vault, input.target);
|
|
2901
|
+
if (!target.ok) return target;
|
|
2902
|
+
const page = prepareTypedPage(input.content, input.target);
|
|
2903
|
+
if (!page.ok) return page;
|
|
2904
|
+
if (input.logNote !== void 0 && /[\r\n]/.test(input.logNote)) {
|
|
2905
|
+
return err("SCHEME_REJECTED", { message: "log note must be one line" });
|
|
2906
|
+
}
|
|
2907
|
+
const logNote = input.logNote?.trim() || void 0;
|
|
2908
|
+
if (logNote && Buffer.byteLength(logNote, "utf8") > 500) {
|
|
2909
|
+
return err("SCHEME_REJECTED", { message: "log note must be one line and at most 500 UTF-8 bytes" });
|
|
2910
|
+
}
|
|
2911
|
+
if (logNote && scanSensitiveContent(logNote, { file: "page-publish log note" }).length > 0) {
|
|
2912
|
+
return err("SENSITIVE_CONTENT_DETECTED", { message: "log note contains sensitive authentication material" });
|
|
2913
|
+
}
|
|
2914
|
+
const date = (input.now ?? /* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2915
|
+
const taxonomyComment = taxonomyCommentForPage(input.target, date);
|
|
2916
|
+
if (!taxonomyComment.ok) return taxonomyComment;
|
|
2917
|
+
return ok({
|
|
2918
|
+
page: page.data,
|
|
2919
|
+
source,
|
|
2920
|
+
targetPath: target.data.absolutePath,
|
|
2921
|
+
logNote,
|
|
2922
|
+
operationId: publicationId(input.target, input.content, logNote),
|
|
2923
|
+
date,
|
|
2924
|
+
taxonomyComment: taxonomyComment.data
|
|
2925
|
+
});
|
|
2926
|
+
}
|
|
2927
|
+
function preparePagePublicationFromContent(input) {
|
|
2928
|
+
return prepareFrozenPublication(input, { kind: "content" });
|
|
2929
|
+
}
|
|
2930
|
+
async function preparePagePublication(input) {
|
|
2931
|
+
let content;
|
|
2932
|
+
try {
|
|
2933
|
+
content = await readFile9(input.draftPath, "utf8");
|
|
2934
|
+
} catch (error) {
|
|
2935
|
+
return err("FILE_NOT_FOUND", { path: input.draftPath, message: String(error) });
|
|
2936
|
+
}
|
|
2937
|
+
let draftRealPath;
|
|
2938
|
+
try {
|
|
2939
|
+
draftRealPath = realpathSync(input.draftPath);
|
|
2940
|
+
} catch (error) {
|
|
2941
|
+
return err("VAULT_PATH_INVALID", {
|
|
2942
|
+
path: input.draftPath,
|
|
2943
|
+
message: `draft realpath failed: ${String(error)}`
|
|
2944
|
+
});
|
|
2945
|
+
}
|
|
2946
|
+
const target = assertTargetInsideVault(input.vault, input.target);
|
|
2947
|
+
if (!target.ok) return target;
|
|
2948
|
+
if (resolve3(input.draftPath) === target.data.absolutePath || target.data.existingRealPath !== void 0 && draftRealPath === target.data.existingRealPath) {
|
|
2949
|
+
return err("VAULT_PATH_INVALID", { message: "draft must not alias the final target" });
|
|
2950
|
+
}
|
|
2951
|
+
return prepareFrozenPublication(
|
|
2952
|
+
{
|
|
2953
|
+
vault: input.vault,
|
|
2954
|
+
content,
|
|
2955
|
+
target: input.target,
|
|
2956
|
+
logNote: input.logNote,
|
|
2957
|
+
now: input.now
|
|
2958
|
+
},
|
|
2959
|
+
{ kind: "file", realPath: draftRealPath }
|
|
2960
|
+
);
|
|
2961
|
+
}
|
|
2962
|
+
function emptyLockedState() {
|
|
2963
|
+
return {
|
|
2964
|
+
taxonomyAdded: [],
|
|
2965
|
+
pageChanged: false,
|
|
2966
|
+
indexUpdated: false,
|
|
2967
|
+
published: false,
|
|
2968
|
+
changed: /* @__PURE__ */ new Set()
|
|
2969
|
+
};
|
|
2970
|
+
}
|
|
2971
|
+
function lockedFailure(stage, state, cause, exitCode = ExitCode.WRITE_FAILED) {
|
|
2972
|
+
return { ok: false, exitCode, stage, state, cause };
|
|
2973
|
+
}
|
|
2974
|
+
async function observeStage(deps, stage) {
|
|
2975
|
+
try {
|
|
2976
|
+
await deps.afterStage(stage);
|
|
2977
|
+
return void 0;
|
|
2978
|
+
} catch (error) {
|
|
2979
|
+
return err("WRITE_FAILED", { message: `stage hook failed at ${stage}: ${String(error)}` });
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
async function runLockedPrimaryStages(input, vault, deps) {
|
|
2983
|
+
const state = emptyLockedState();
|
|
2984
|
+
const freshTarget = assertTargetInsideVault(vault, input.page.target);
|
|
2985
|
+
if (!freshTarget.ok) {
|
|
2986
|
+
return lockedFailure("target", state, freshTarget, ExitCode.VAULT_PATH_INVALID);
|
|
2987
|
+
}
|
|
2988
|
+
if (freshTarget.data.absolutePath !== input.targetPath) {
|
|
2989
|
+
return lockedFailure(
|
|
2990
|
+
"target",
|
|
2991
|
+
state,
|
|
2992
|
+
err("VAULT_PATH_INVALID", { message: "target canonical path changed after preparation" }),
|
|
2993
|
+
ExitCode.VAULT_PATH_INVALID
|
|
2994
|
+
);
|
|
2995
|
+
}
|
|
2996
|
+
if (input.source.kind === "file" && freshTarget.data.existingRealPath !== void 0 && freshTarget.data.existingRealPath === input.source.realPath) {
|
|
2997
|
+
return lockedFailure(
|
|
2998
|
+
"target",
|
|
2999
|
+
state,
|
|
3000
|
+
err("VAULT_PATH_INVALID", { message: "draft now aliases the final target" }),
|
|
3001
|
+
ExitCode.VAULT_PATH_INVALID
|
|
3002
|
+
);
|
|
3003
|
+
}
|
|
3004
|
+
const schemaPath = join14(vault, "SCHEMA.md");
|
|
3005
|
+
let schemaText;
|
|
3006
|
+
try {
|
|
3007
|
+
schemaText = await readFile9(schemaPath, "utf8");
|
|
3008
|
+
} catch (error) {
|
|
3009
|
+
return lockedFailure("schema", state, err("WRITE_FAILED", { message: String(error) }));
|
|
3010
|
+
}
|
|
3011
|
+
const reconciled = reconcileTaxonomyDocument(schemaText, {
|
|
3012
|
+
tags: input.page.tags,
|
|
3013
|
+
comment: input.taxonomyComment
|
|
3014
|
+
});
|
|
3015
|
+
if (!reconciled.ok) {
|
|
3016
|
+
return lockedFailure("schema", state, reconciled, errorExitCode(reconciled.error));
|
|
3017
|
+
}
|
|
3018
|
+
state.taxonomyAdded = reconciled.data.added;
|
|
3019
|
+
if (reconciled.data.changed) {
|
|
3020
|
+
const schemaWrite = await atomicWriteText(schemaPath, reconciled.data.text);
|
|
3021
|
+
if (!schemaWrite.ok) return lockedFailure("schema", state, schemaWrite);
|
|
3022
|
+
if (schemaWrite.data.changed) state.changed.add("SCHEMA.md");
|
|
3023
|
+
}
|
|
3024
|
+
const schemaHook = await observeStage(deps, "schema");
|
|
3025
|
+
if (schemaHook) return lockedFailure("schema", state, schemaHook);
|
|
3026
|
+
const pageWrite = await safeWritePage(input.targetPath, input.page.content);
|
|
3027
|
+
if (!pageWrite.ok) return lockedFailure("page", state, pageWrite);
|
|
3028
|
+
state.pageChanged = pageWrite.data.changed;
|
|
3029
|
+
if (state.pageChanged) state.changed.add(input.page.target);
|
|
3030
|
+
state.published = true;
|
|
3031
|
+
const pageHook = await observeStage(deps, "page");
|
|
3032
|
+
if (pageHook) return lockedFailure("page", state, pageHook);
|
|
3033
|
+
let visible;
|
|
3034
|
+
let visibleSchema;
|
|
3035
|
+
try {
|
|
3036
|
+
[visible, visibleSchema] = await Promise.all([
|
|
3037
|
+
readFile9(input.targetPath, "utf8"),
|
|
3038
|
+
readFile9(schemaPath, "utf8")
|
|
3039
|
+
]);
|
|
3040
|
+
} catch (error) {
|
|
3041
|
+
return lockedFailure("verify", state, err("WRITE_FAILED", { message: String(error) }));
|
|
3042
|
+
}
|
|
3043
|
+
const visiblePage = prepareTypedPage(visible, input.page.target);
|
|
3044
|
+
const visibleTaxonomy = extractTaxonomy(visibleSchema);
|
|
3045
|
+
if (!visiblePage.ok || visible !== input.page.content || !visibleTaxonomy.ok || input.page.tags.some((tag) => !visibleTaxonomy.data.includes(tag))) {
|
|
3046
|
+
return lockedFailure(
|
|
3047
|
+
"verify",
|
|
3048
|
+
state,
|
|
3049
|
+
err("WRITE_FAILED", { message: "published bytes or taxonomy verification failed" })
|
|
3050
|
+
);
|
|
3051
|
+
}
|
|
3052
|
+
const verifyHook = await observeStage(deps, "verify");
|
|
3053
|
+
if (verifyHook) return lockedFailure("verify", state, verifyHook);
|
|
3054
|
+
const index = await upsertIndexEntry({
|
|
3055
|
+
vault,
|
|
3056
|
+
target: input.page.target,
|
|
3057
|
+
title: input.page.title,
|
|
3058
|
+
type: input.page.type
|
|
3059
|
+
});
|
|
3060
|
+
if (!index.ok) return lockedFailure("index", state, index);
|
|
3061
|
+
state.indexUpdated = index.data.changed;
|
|
3062
|
+
if (state.indexUpdated) state.changed.add("index.md");
|
|
3063
|
+
const indexHook = await observeStage(deps, "index");
|
|
3064
|
+
if (indexHook) return lockedFailure("index", state, indexHook);
|
|
3065
|
+
return { ok: true, data: state };
|
|
3066
|
+
}
|
|
3067
|
+
function redactDetail(detail) {
|
|
3068
|
+
if (detail === void 0) return void 0;
|
|
3069
|
+
try {
|
|
3070
|
+
const encoded = JSON.stringify(detail);
|
|
3071
|
+
return JSON.parse(redactSensitiveContent(encoded).text);
|
|
3072
|
+
} catch {
|
|
3073
|
+
return { message: "unserializable error detail omitted" };
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
function phaseFailure(stage, input, published, cause, context = {}, exitCode = ExitCode.WRITE_FAILED) {
|
|
3077
|
+
return {
|
|
3078
|
+
exitCode,
|
|
3079
|
+
result: err("WRITE_FAILED", {
|
|
3080
|
+
...context,
|
|
3081
|
+
stage,
|
|
3082
|
+
published,
|
|
3083
|
+
target: input.page.target,
|
|
3084
|
+
operation_id: input.operationId,
|
|
3085
|
+
retry_safe: stage !== "target",
|
|
3086
|
+
cause_error: cause.error,
|
|
3087
|
+
cause_detail: redactDetail(cause.detail)
|
|
3088
|
+
})
|
|
3089
|
+
};
|
|
3090
|
+
}
|
|
3091
|
+
function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, logAppended, filesChanged, dryRun = false) {
|
|
3092
|
+
return {
|
|
3093
|
+
exitCode: ExitCode.OK,
|
|
3094
|
+
result: ok({
|
|
3095
|
+
target: input.page.target,
|
|
3096
|
+
page_type: input.page.type,
|
|
3097
|
+
tags: [...input.page.tags],
|
|
3098
|
+
taxonomy_added: [...taxonomyAdded],
|
|
3099
|
+
page_changed: pageChanged,
|
|
3100
|
+
index_updated: indexUpdated,
|
|
3101
|
+
log_appended: logAppended,
|
|
3102
|
+
operation_id: input.operationId,
|
|
3103
|
+
dry_run: dryRun,
|
|
3104
|
+
files_changed: filesChanged,
|
|
3105
|
+
humanHint: dryRun ? `dry run: would publish ${input.page.target} (${input.operationId.slice(0, 12)})` : `published ${input.page.target} (${input.operationId.slice(0, 12)})`
|
|
3106
|
+
})
|
|
3107
|
+
};
|
|
3108
|
+
}
|
|
3109
|
+
function renderPublicationLog(input, added) {
|
|
3110
|
+
return [
|
|
3111
|
+
`## [${input.date}] page-publish | ${input.page.target}`,
|
|
3112
|
+
"",
|
|
3113
|
+
`- Published: [[${input.page.target.replace(/\.md$/, "")}]]`,
|
|
3114
|
+
`- Taxonomy: ${added.length > 0 ? `added ${added.join(", ")}` : "no additions"}`,
|
|
3115
|
+
...input.logNote ? [`- Note: ${input.logNote}`] : []
|
|
3116
|
+
].join("\n");
|
|
3117
|
+
}
|
|
3118
|
+
async function readPageChanged(targetPath, content) {
|
|
3119
|
+
try {
|
|
3120
|
+
return ok(await readFile9(targetPath, "utf8") !== content);
|
|
3121
|
+
} catch (error) {
|
|
3122
|
+
if (error.code === "ENOENT") return ok(true);
|
|
3123
|
+
return err("WRITE_FAILED", { path: targetPath, message: String(error) });
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
async function previewPreparedPagePublication(input, vault) {
|
|
3127
|
+
const schemaPath = join14(vault, "SCHEMA.md");
|
|
3128
|
+
let schemaText;
|
|
3129
|
+
try {
|
|
3130
|
+
schemaText = await readFile9(schemaPath, "utf8");
|
|
3131
|
+
} catch (error) {
|
|
3132
|
+
const result = err("FILE_NOT_FOUND", { path: schemaPath, message: String(error) });
|
|
3133
|
+
return { exitCode: ExitCode.FILE_NOT_FOUND, result };
|
|
3134
|
+
}
|
|
3135
|
+
const reconciled = reconcileTaxonomyDocument(schemaText, {
|
|
3136
|
+
tags: input.page.tags,
|
|
3137
|
+
comment: input.taxonomyComment
|
|
3138
|
+
});
|
|
3139
|
+
if (!reconciled.ok) return { exitCode: errorExitCode(reconciled.error), result: reconciled };
|
|
3140
|
+
const pageChanged = await readPageChanged(input.targetPath, input.page.content);
|
|
3141
|
+
if (!pageChanged.ok) return { exitCode: errorExitCode(pageChanged.error), result: pageChanged };
|
|
3142
|
+
const indexPath = join14(vault, "index.md");
|
|
3143
|
+
let indexText;
|
|
3144
|
+
try {
|
|
3145
|
+
indexText = await readFile9(indexPath, "utf8");
|
|
3146
|
+
} catch (error) {
|
|
3147
|
+
const result = err("FILE_NOT_FOUND", { path: indexPath, message: String(error) });
|
|
3148
|
+
return { exitCode: ExitCode.FILE_NOT_FOUND, result };
|
|
3149
|
+
}
|
|
3150
|
+
const index = renderIndexUpsert(indexText, {
|
|
3151
|
+
target: input.page.target,
|
|
3152
|
+
title: input.page.title,
|
|
3153
|
+
type: input.page.type
|
|
3154
|
+
});
|
|
3155
|
+
if (!index.ok) return { exitCode: errorExitCode(index.error), result: index };
|
|
3156
|
+
const logPath = join14(vault, "log.md");
|
|
3157
|
+
let logText;
|
|
3158
|
+
try {
|
|
3159
|
+
logText = await readFile9(logPath, "utf8");
|
|
3160
|
+
} catch (error) {
|
|
3161
|
+
const result = err("FILE_NOT_FOUND", { path: logPath, message: String(error) });
|
|
3162
|
+
return { exitCode: ExitCode.FILE_NOT_FOUND, result };
|
|
3163
|
+
}
|
|
3164
|
+
const logAppended = !logText.includes(`<!-- skillwiki-page-publish:${input.operationId} -->`);
|
|
3165
|
+
const filesChanged = [
|
|
3166
|
+
...reconciled.data.changed ? ["SCHEMA.md"] : [],
|
|
3167
|
+
...pageChanged.data ? [input.page.target] : [],
|
|
3168
|
+
...index.data.changed ? ["index.md"] : [],
|
|
3169
|
+
...logAppended ? ["log.md"] : []
|
|
3170
|
+
];
|
|
3171
|
+
return successReceipt(
|
|
3172
|
+
input,
|
|
3173
|
+
reconciled.data.added,
|
|
3174
|
+
pageChanged.data,
|
|
3175
|
+
index.data.changed,
|
|
3176
|
+
logAppended,
|
|
3177
|
+
filesChanged,
|
|
3178
|
+
true
|
|
3179
|
+
);
|
|
3180
|
+
}
|
|
3181
|
+
async function publishPreparedPage(input, vault, deps = DEFAULT_DEPS) {
|
|
3182
|
+
let lock;
|
|
3183
|
+
try {
|
|
3184
|
+
lock = acquireOwnedSyncLock(vault, {
|
|
3185
|
+
summary: `page publish ${input.page.target}`,
|
|
3186
|
+
ttlMinutes: 1
|
|
3187
|
+
});
|
|
3188
|
+
} catch (error) {
|
|
3189
|
+
return {
|
|
3190
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
3191
|
+
result: err("WRITE_FAILED", { stage: "lock", message: String(error) })
|
|
3192
|
+
};
|
|
3193
|
+
}
|
|
3194
|
+
if (!lock.ok) return { exitCode: errorExitCode(lock.error), result: lock };
|
|
3195
|
+
let primary;
|
|
3196
|
+
let released;
|
|
3197
|
+
try {
|
|
3198
|
+
primary = await runLockedPrimaryStages(input, vault, deps);
|
|
3199
|
+
} catch (error) {
|
|
3200
|
+
primary = lockedFailure(
|
|
3201
|
+
"schema",
|
|
3202
|
+
emptyLockedState(),
|
|
3203
|
+
err("WRITE_FAILED", { message: `unexpected primary-stage failure: ${String(error)}` })
|
|
3204
|
+
);
|
|
3205
|
+
} finally {
|
|
3206
|
+
released = releaseOwnedSyncLock(lock.data);
|
|
3207
|
+
}
|
|
3208
|
+
const primaryState = primary?.ok ? primary.data : primary?.state;
|
|
3209
|
+
if (released === void 0 || !released.ok || !released.data.released) {
|
|
3210
|
+
return phaseFailure(
|
|
3211
|
+
"unlock",
|
|
3212
|
+
input,
|
|
3213
|
+
primaryState?.published ?? false,
|
|
3214
|
+
released && !released.ok ? released : err("WRITE_FAILED", { message: "lock release did not run" }),
|
|
3215
|
+
{
|
|
3216
|
+
primary_stage: primary && !primary.ok ? primary.stage : "complete",
|
|
3217
|
+
primary_error: primary && !primary.ok ? primary.cause.error : void 0
|
|
3218
|
+
}
|
|
3219
|
+
);
|
|
3220
|
+
}
|
|
3221
|
+
const unlockHook = await observeStage(deps, "unlock");
|
|
3222
|
+
if (unlockHook) return phaseFailure("unlock", input, primaryState?.published ?? false, unlockHook);
|
|
3223
|
+
if (primary === void 0) {
|
|
3224
|
+
return phaseFailure(
|
|
3225
|
+
"schema",
|
|
3226
|
+
input,
|
|
3227
|
+
false,
|
|
3228
|
+
err("WRITE_FAILED", { message: "locked publication produced no result" })
|
|
3229
|
+
);
|
|
3230
|
+
}
|
|
3231
|
+
if (!primary.ok) {
|
|
3232
|
+
return phaseFailure(
|
|
3233
|
+
primary.stage,
|
|
3234
|
+
input,
|
|
3235
|
+
primary.state.published,
|
|
3236
|
+
primary.cause,
|
|
3237
|
+
void 0,
|
|
3238
|
+
primary.exitCode
|
|
3239
|
+
);
|
|
3240
|
+
}
|
|
3241
|
+
const state = primary.data;
|
|
3242
|
+
const log = await runLogAppend({
|
|
3243
|
+
vault,
|
|
3244
|
+
content: renderPublicationLog(input, state.taxonomyAdded),
|
|
3245
|
+
operationId: input.operationId,
|
|
3246
|
+
strictLock: true,
|
|
3247
|
+
recordLastOp: false
|
|
3248
|
+
});
|
|
3249
|
+
if (!log.result.ok) return phaseFailure("log", input, true, log.result);
|
|
3250
|
+
if (log.exitCode !== ExitCode.OK) {
|
|
3251
|
+
return phaseFailure(
|
|
3252
|
+
"log",
|
|
3253
|
+
input,
|
|
3254
|
+
true,
|
|
3255
|
+
err("WRITE_FAILED", { message: "log append returned inconsistent success state" })
|
|
3256
|
+
);
|
|
3257
|
+
}
|
|
3258
|
+
if (log.result.data.appended) state.changed.add("log.md");
|
|
3259
|
+
const logHook = await observeStage(deps, "log");
|
|
3260
|
+
if (logHook) return phaseFailure("log", input, true, logHook);
|
|
3261
|
+
return successReceipt(
|
|
3262
|
+
input,
|
|
3263
|
+
state.taxonomyAdded,
|
|
3264
|
+
state.pageChanged,
|
|
3265
|
+
state.indexUpdated,
|
|
3266
|
+
log.result.data.appended,
|
|
3267
|
+
[...state.changed]
|
|
3268
|
+
);
|
|
3269
|
+
}
|
|
3270
|
+
async function runPagePublish(input, deps = DEFAULT_DEPS) {
|
|
3271
|
+
const prepared = await preparePagePublication(input);
|
|
3272
|
+
if (!prepared.ok) return { exitCode: errorExitCode(prepared.error), result: prepared };
|
|
3273
|
+
if (!input.write) return previewPreparedPagePublication(prepared.data, input.vault);
|
|
3274
|
+
return publishPreparedPage(prepared.data, input.vault, deps);
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
// src/commands/ingest.ts
|
|
2954
3278
|
var ALLOWED_TYPES = /* @__PURE__ */ new Set(["entity", "concept", "comparison", "query"]);
|
|
2955
3279
|
var TYPE_DIR = {
|
|
2956
3280
|
entity: "entities",
|
|
@@ -2991,19 +3315,6 @@ function buildTypedContent(title, ingested, type, tags, rawRelPath, provenance)
|
|
|
2991
3315
|
const aliases = [];
|
|
2992
3316
|
const sourcesYaml = ` - ${rawRelPath}`;
|
|
2993
3317
|
const tagsYaml = tags.length > 0 ? tags.map((t) => ` - ${t}`).join("\n") : " []";
|
|
2994
|
-
const fm = {
|
|
2995
|
-
title,
|
|
2996
|
-
aliases,
|
|
2997
|
-
created: ingested,
|
|
2998
|
-
updated: ingested,
|
|
2999
|
-
type,
|
|
3000
|
-
tags,
|
|
3001
|
-
sources: [rawRelPath],
|
|
3002
|
-
confidence: "medium"
|
|
3003
|
-
};
|
|
3004
|
-
if (provenance) {
|
|
3005
|
-
fm.provenance = provenance;
|
|
3006
|
-
}
|
|
3007
3318
|
const fmLines = ["---"];
|
|
3008
3319
|
fmLines.push(`title: "${title}"`);
|
|
3009
3320
|
if (aliases.length > 0) {
|
|
@@ -3039,6 +3350,78 @@ function buildTypedContent(title, ingested, type, tags, rawRelPath, provenance)
|
|
|
3039
3350
|
].join("\n");
|
|
3040
3351
|
return fmLines.join("\n") + body;
|
|
3041
3352
|
}
|
|
3353
|
+
async function resolveRawCapture(input) {
|
|
3354
|
+
try {
|
|
3355
|
+
const existing = await readFile10(input.path, "utf8");
|
|
3356
|
+
const frontmatter = extractFrontmatter(existing);
|
|
3357
|
+
if (!frontmatter.ok) {
|
|
3358
|
+
return err("INGEST_VALIDATION_FAILED", {
|
|
3359
|
+
path: input.path,
|
|
3360
|
+
message: "existing immutable raw source has invalid frontmatter",
|
|
3361
|
+
source_error: frontmatter.error
|
|
3362
|
+
});
|
|
3363
|
+
}
|
|
3364
|
+
const parsed = RawSourceSchema.safeParse(frontmatter.data);
|
|
3365
|
+
if (!parsed.success || parsed.data.sha256 !== input.sha256 || (parsed.data.source_url ?? null) !== input.sourceUrl || existing !== buildRawContent(
|
|
3366
|
+
input.sourceUrl,
|
|
3367
|
+
String(parsed.data.ingested),
|
|
3368
|
+
input.sha256,
|
|
3369
|
+
input.sourceContent
|
|
3370
|
+
)) {
|
|
3371
|
+
return err("INGEST_VALIDATION_FAILED", {
|
|
3372
|
+
path: input.path,
|
|
3373
|
+
message: "existing immutable raw source differs from the fetched source"
|
|
3374
|
+
});
|
|
3375
|
+
}
|
|
3376
|
+
return ok({
|
|
3377
|
+
content: existing,
|
|
3378
|
+
ingested: String(parsed.data.ingested),
|
|
3379
|
+
shouldWrite: false
|
|
3380
|
+
});
|
|
3381
|
+
} catch (error) {
|
|
3382
|
+
if (error.code !== "ENOENT") {
|
|
3383
|
+
return err("WRITE_FAILED", { path: input.path, message: String(error) });
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
return ok({
|
|
3387
|
+
content: buildRawContent(input.sourceUrl, input.today, input.sha256, input.sourceContent),
|
|
3388
|
+
ingested: input.today,
|
|
3389
|
+
shouldWrite: true
|
|
3390
|
+
});
|
|
3391
|
+
}
|
|
3392
|
+
async function writeResolvedRaw(input) {
|
|
3393
|
+
if (!input.capture.shouldWrite) return ok({ changed: false, capture: input.capture });
|
|
3394
|
+
const lock = await acquireRawCaptureLock(input.path);
|
|
3395
|
+
if (!lock.ok) return lock;
|
|
3396
|
+
try {
|
|
3397
|
+
const resolved = await resolveRawCapture(input);
|
|
3398
|
+
if (!resolved.ok) return resolved;
|
|
3399
|
+
if (!resolved.data.shouldWrite) return ok({ changed: false, capture: resolved.data });
|
|
3400
|
+
const written = await atomicWriteText(input.path, resolved.data.content);
|
|
3401
|
+
return written.ok ? ok({ changed: written.data.changed, capture: resolved.data }) : written;
|
|
3402
|
+
} finally {
|
|
3403
|
+
try {
|
|
3404
|
+
await unlink3(lock.data);
|
|
3405
|
+
} catch {
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
async function acquireRawCaptureLock(path) {
|
|
3410
|
+
const lockPath = `${path}.ingest.lock`;
|
|
3411
|
+
for (let attempt = 0; attempt < 200; attempt++) {
|
|
3412
|
+
try {
|
|
3413
|
+
const handle = await open(lockPath, "wx");
|
|
3414
|
+
await handle.close();
|
|
3415
|
+
return ok(lockPath);
|
|
3416
|
+
} catch (error) {
|
|
3417
|
+
if (error.code !== "EEXIST") {
|
|
3418
|
+
return err("WRITE_FAILED", { path: lockPath, phase: "raw-lock", message: String(error) });
|
|
3419
|
+
}
|
|
3420
|
+
await new Promise((resolve4) => setTimeout(resolve4, 10));
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
return err("WRITE_FAILED", { path: lockPath, phase: "raw-lock", message: "raw capture lock held" });
|
|
3424
|
+
}
|
|
3042
3425
|
async function runIngest(input) {
|
|
3043
3426
|
if (!input.source || input.source.trim().length === 0) {
|
|
3044
3427
|
return {
|
|
@@ -3126,15 +3509,14 @@ async function runIngest(input) {
|
|
|
3126
3509
|
})
|
|
3127
3510
|
};
|
|
3128
3511
|
}
|
|
3129
|
-
const sha256 =
|
|
3512
|
+
const sha256 = createHash4("sha256").update(Buffer.from(sourceContent, "utf8")).digest("hex");
|
|
3130
3513
|
const today = todayIso();
|
|
3131
3514
|
const slug = slugify2(input.title);
|
|
3132
3515
|
const tags = input.tags && input.tags.length > 0 ? input.tags : [];
|
|
3133
3516
|
const rawRelPath = `raw/articles/${slug}.md`;
|
|
3134
3517
|
const typedDir = TYPE_DIR[input.type] ?? `${input.type}s`;
|
|
3135
3518
|
const typedRelPath = `${typedDir}/${slug}.md`;
|
|
3136
|
-
const rawAbsPath =
|
|
3137
|
-
const typedAbsPath = join16(input.vault, typedRelPath);
|
|
3519
|
+
const rawAbsPath = join15(input.vault, rawRelPath);
|
|
3138
3520
|
const identity = assessSourceIdentity({
|
|
3139
3521
|
rawPath: rawRelPath,
|
|
3140
3522
|
sourceUrl: sourceUrl ?? void 0,
|
|
@@ -3154,16 +3536,62 @@ async function runIngest(input) {
|
|
|
3154
3536
|
})
|
|
3155
3537
|
};
|
|
3156
3538
|
}
|
|
3157
|
-
const
|
|
3158
|
-
|
|
3539
|
+
const resolvedRaw = await resolveRawCapture({
|
|
3540
|
+
path: rawAbsPath,
|
|
3541
|
+
sourceUrl,
|
|
3542
|
+
sourceContent,
|
|
3543
|
+
sha256,
|
|
3544
|
+
today
|
|
3545
|
+
});
|
|
3546
|
+
if (!resolvedRaw.ok) {
|
|
3547
|
+
return {
|
|
3548
|
+
exitCode: resolvedRaw.error === "INGEST_VALIDATION_FAILED" ? ExitCode.INGEST_VALIDATION_FAILED : ExitCode.WRITE_FAILED,
|
|
3549
|
+
result: resolvedRaw
|
|
3550
|
+
};
|
|
3551
|
+
}
|
|
3552
|
+
let publicationDate = resolvedRaw.data.ingested;
|
|
3553
|
+
let typedContent = buildTypedContent(
|
|
3159
3554
|
input.title,
|
|
3160
|
-
|
|
3555
|
+
publicationDate,
|
|
3161
3556
|
input.type,
|
|
3162
3557
|
tags,
|
|
3163
3558
|
rawRelPath,
|
|
3164
3559
|
input.provenance
|
|
3165
3560
|
);
|
|
3561
|
+
if (!input.dryRun) {
|
|
3562
|
+
try {
|
|
3563
|
+
await mkdir8(join15(input.vault, typedDir), { recursive: true });
|
|
3564
|
+
} catch (error) {
|
|
3565
|
+
return {
|
|
3566
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
3567
|
+
result: err("WRITE_FAILED", { path: join15(input.vault, typedDir), message: String(error) })
|
|
3568
|
+
};
|
|
3569
|
+
}
|
|
3570
|
+
}
|
|
3571
|
+
let publication = preparePagePublicationFromContent({
|
|
3572
|
+
vault: input.vault,
|
|
3573
|
+
content: typedContent,
|
|
3574
|
+
target: typedRelPath,
|
|
3575
|
+
logNote: `ingested from ${rawRelPath}`,
|
|
3576
|
+
now: /* @__PURE__ */ new Date(`${publicationDate}T00:00:00Z`)
|
|
3577
|
+
});
|
|
3578
|
+
if (!publication.ok) {
|
|
3579
|
+
return {
|
|
3580
|
+
exitCode: ExitCode.INGEST_VALIDATION_FAILED,
|
|
3581
|
+
result: publication
|
|
3582
|
+
};
|
|
3583
|
+
}
|
|
3166
3584
|
if (input.dryRun) {
|
|
3585
|
+
const preview = await previewPreparedPagePublication(publication.data, input.vault);
|
|
3586
|
+
if (!preview.result.ok) {
|
|
3587
|
+
return { exitCode: preview.exitCode, result: preview.result };
|
|
3588
|
+
}
|
|
3589
|
+
if (preview.exitCode !== ExitCode.OK) {
|
|
3590
|
+
return {
|
|
3591
|
+
exitCode: preview.exitCode,
|
|
3592
|
+
result: err("WRITE_FAILED", { message: "publication preview returned inconsistent success state" })
|
|
3593
|
+
};
|
|
3594
|
+
}
|
|
3167
3595
|
return {
|
|
3168
3596
|
exitCode: ExitCode.OK,
|
|
3169
3597
|
result: ok({
|
|
@@ -3172,78 +3600,84 @@ async function runIngest(input) {
|
|
|
3172
3600
|
sha256,
|
|
3173
3601
|
dry_run: true,
|
|
3174
3602
|
humanHint: [
|
|
3175
|
-
|
|
3603
|
+
"DRY RUN \u2014 would create:",
|
|
3176
3604
|
` ${rawRelPath} (sha256: ${sha256.slice(0, 12)}...)`,
|
|
3177
3605
|
` ${typedRelPath}`,
|
|
3178
3606
|
` type: ${input.type}, tags: [${tags.join(", ")}]`,
|
|
3179
3607
|
input.provenance ? ` provenance: ${input.provenance}` : ""
|
|
3180
|
-
].filter(Boolean).join("\n")
|
|
3608
|
+
].filter(Boolean).join("\n"),
|
|
3609
|
+
publication: preview.result.data
|
|
3181
3610
|
})
|
|
3182
3611
|
};
|
|
3183
3612
|
}
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
created: today,
|
|
3188
|
-
updated: today,
|
|
3189
|
-
type: input.type,
|
|
3190
|
-
tags,
|
|
3191
|
-
sources: [rawRelPath],
|
|
3192
|
-
confidence: "medium",
|
|
3193
|
-
...input.provenance ? { provenance: input.provenance } : {}
|
|
3194
|
-
};
|
|
3195
|
-
const det = detectSchema(typedFm);
|
|
3196
|
-
if (!det.schema) {
|
|
3613
|
+
try {
|
|
3614
|
+
await mkdir8(join15(input.vault, "raw", "articles"), { recursive: true });
|
|
3615
|
+
} catch (error) {
|
|
3197
3616
|
return {
|
|
3198
|
-
exitCode: ExitCode.
|
|
3199
|
-
result: err("
|
|
3200
|
-
message: "generated typed-knowledge page could not be detected as a valid schema"
|
|
3201
|
-
})
|
|
3617
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
3618
|
+
result: err("WRITE_FAILED", { path: join15(input.vault, "raw", "articles"), message: String(error) })
|
|
3202
3619
|
};
|
|
3203
3620
|
}
|
|
3204
|
-
const
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3621
|
+
const rawWrite = await writeResolvedRaw({
|
|
3622
|
+
path: rawAbsPath,
|
|
3623
|
+
sourceUrl,
|
|
3624
|
+
sourceContent,
|
|
3625
|
+
sha256,
|
|
3626
|
+
today,
|
|
3627
|
+
capture: resolvedRaw.data
|
|
3628
|
+
});
|
|
3629
|
+
if (!rawWrite.ok) {
|
|
3210
3630
|
return {
|
|
3211
|
-
exitCode: ExitCode.INGEST_VALIDATION_FAILED,
|
|
3212
|
-
result:
|
|
3213
|
-
message: "generated typed-knowledge page failed schema validation",
|
|
3214
|
-
errors
|
|
3215
|
-
})
|
|
3631
|
+
exitCode: rawWrite.error === "INGEST_VALIDATION_FAILED" ? ExitCode.INGEST_VALIDATION_FAILED : ExitCode.WRITE_FAILED,
|
|
3632
|
+
result: rawWrite
|
|
3216
3633
|
};
|
|
3217
3634
|
}
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3635
|
+
if (rawWrite.data.capture.ingested !== publicationDate) {
|
|
3636
|
+
publicationDate = rawWrite.data.capture.ingested;
|
|
3637
|
+
typedContent = buildTypedContent(
|
|
3638
|
+
input.title,
|
|
3639
|
+
publicationDate,
|
|
3640
|
+
input.type,
|
|
3641
|
+
tags,
|
|
3642
|
+
rawRelPath,
|
|
3643
|
+
input.provenance
|
|
3644
|
+
);
|
|
3645
|
+
publication = preparePagePublicationFromContent({
|
|
3646
|
+
vault: input.vault,
|
|
3647
|
+
content: typedContent,
|
|
3648
|
+
target: typedRelPath,
|
|
3649
|
+
logNote: `ingested from ${rawRelPath}`,
|
|
3650
|
+
now: /* @__PURE__ */ new Date(`${publicationDate}T00:00:00Z`)
|
|
3651
|
+
});
|
|
3652
|
+
if (!publication.ok) {
|
|
3653
|
+
return {
|
|
3654
|
+
exitCode: ExitCode.INGEST_VALIDATION_FAILED,
|
|
3655
|
+
result: publication
|
|
3656
|
+
};
|
|
3657
|
+
}
|
|
3226
3658
|
}
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
}
|
|
3659
|
+
const published = await publishPreparedPage(publication.data, input.vault);
|
|
3660
|
+
if (!published.result.ok) {
|
|
3661
|
+
return { exitCode: published.exitCode, result: published.result };
|
|
3662
|
+
}
|
|
3663
|
+
if (published.exitCode !== ExitCode.OK) {
|
|
3231
3664
|
return {
|
|
3232
|
-
exitCode:
|
|
3233
|
-
result: err("WRITE_FAILED", {
|
|
3665
|
+
exitCode: published.exitCode,
|
|
3666
|
+
result: err("WRITE_FAILED", { message: "publisher returned inconsistent success state" })
|
|
3234
3667
|
};
|
|
3235
3668
|
}
|
|
3236
|
-
const
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3669
|
+
const changedFiles = [
|
|
3670
|
+
...rawWrite.data.changed ? [rawRelPath] : [],
|
|
3671
|
+
...published.result.data.files_changed
|
|
3672
|
+
];
|
|
3673
|
+
if (changedFiles.length > 0) {
|
|
3674
|
+
appendLastOp(input.vault, {
|
|
3675
|
+
operation: "ingest",
|
|
3676
|
+
summary: `added ${slug}`,
|
|
3677
|
+
files: [...new Set(changedFiles)],
|
|
3678
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3679
|
+
});
|
|
3680
|
+
}
|
|
3247
3681
|
return {
|
|
3248
3682
|
exitCode: ExitCode.OK,
|
|
3249
3683
|
result: ok({
|
|
@@ -3251,7 +3685,12 @@ async function runIngest(input) {
|
|
|
3251
3685
|
typed_path: typedRelPath,
|
|
3252
3686
|
sha256,
|
|
3253
3687
|
dry_run: false,
|
|
3254
|
-
humanHint
|
|
3688
|
+
humanHint: [
|
|
3689
|
+
"created:",
|
|
3690
|
+
` ${rawRelPath} (sha256: ${sha256.slice(0, 12)}...)`,
|
|
3691
|
+
` ${typedRelPath}`
|
|
3692
|
+
].join("\n"),
|
|
3693
|
+
publication: published.result.data
|
|
3255
3694
|
})
|
|
3256
3695
|
};
|
|
3257
3696
|
}
|
|
@@ -3403,138 +3842,216 @@ ${body}`;
|
|
|
3403
3842
|
};
|
|
3404
3843
|
}
|
|
3405
3844
|
|
|
3406
|
-
// src/commands/
|
|
3407
|
-
import {
|
|
3408
|
-
import { join as
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3845
|
+
// src/commands/tag-reconcile.ts
|
|
3846
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
3847
|
+
import { join as join16, posix } from "path";
|
|
3848
|
+
var TYPED_TARGET_RE = /^(entities|concepts|comparisons|queries|meta)\/[a-z0-9][a-z0-9./_-]*\.md$/;
|
|
3849
|
+
function errorExitCode2(error) {
|
|
3850
|
+
switch (error) {
|
|
3851
|
+
case "FILE_NOT_FOUND":
|
|
3852
|
+
return ExitCode.FILE_NOT_FOUND;
|
|
3853
|
+
case "MISSING_CLOSING_DELIMITER":
|
|
3854
|
+
return ExitCode.MISSING_CLOSING_DELIMITER;
|
|
3855
|
+
case "SCHEME_REJECTED":
|
|
3856
|
+
return ExitCode.SCHEME_REJECTED;
|
|
3857
|
+
case "VAULT_PATH_INVALID":
|
|
3858
|
+
return ExitCode.VAULT_PATH_INVALID;
|
|
3859
|
+
case "WRITE_FAILED":
|
|
3860
|
+
return ExitCode.WRITE_FAILED;
|
|
3861
|
+
case "SYNC_LOCK_HELD":
|
|
3862
|
+
return ExitCode.SYNC_LOCK_HELD;
|
|
3863
|
+
default:
|
|
3864
|
+
return ExitCode.INVALID_FRONTMATTER;
|
|
3865
|
+
}
|
|
3866
|
+
}
|
|
3867
|
+
function validatePageIdentity(page) {
|
|
3868
|
+
const segments = page.split("/");
|
|
3869
|
+
if (page.length === 0 || posix.isAbsolute(page) || page.includes("\\") || posix.normalize(page) !== page || segments.some((segment) => segment === "" || segment === "." || segment === "..") || !TYPED_TARGET_RE.test(page)) {
|
|
3870
|
+
return err("VAULT_PATH_INVALID", {
|
|
3871
|
+
page,
|
|
3872
|
+
message: "page must be a normalized vault-relative typed Markdown path"
|
|
3873
|
+
});
|
|
3418
3874
|
}
|
|
3875
|
+
return ok(page);
|
|
3419
3876
|
}
|
|
3420
|
-
function
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
if (process.env.CLAUDE_SESSION_ID) return process.env.CLAUDE_SESSION_ID;
|
|
3430
|
-
if (process.env.SKILLWIKI_SESSION_ID) return process.env.SKILLWIKI_SESSION_ID;
|
|
3431
|
-
return void 0;
|
|
3432
|
-
}
|
|
3433
|
-
function getSessionId() {
|
|
3434
|
-
const envSessionId = getEnvSessionId();
|
|
3435
|
-
if (envSessionId) return envSessionId;
|
|
3436
|
-
return process.pid.toString();
|
|
3437
|
-
}
|
|
3438
|
-
function getCwdHash(cwd) {
|
|
3439
|
-
const path = cwd || process.cwd();
|
|
3440
|
-
const hash = createHash4("sha256").update(path).digest("hex");
|
|
3441
|
-
return hash.slice(0, 8);
|
|
3877
|
+
function asTagArray(frontmatter, path) {
|
|
3878
|
+
const tags = frontmatter.tags;
|
|
3879
|
+
if (!Array.isArray(tags) || !tags.every((tag) => typeof tag === "string")) {
|
|
3880
|
+
return err("INVALID_FRONTMATTER", {
|
|
3881
|
+
path,
|
|
3882
|
+
message: "frontmatter tags must be an array of strings"
|
|
3883
|
+
});
|
|
3884
|
+
}
|
|
3885
|
+
return ok(tags);
|
|
3442
3886
|
}
|
|
3443
|
-
function
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3887
|
+
async function readTagsFromFile(path) {
|
|
3888
|
+
let text;
|
|
3889
|
+
try {
|
|
3890
|
+
text = await readFile11(path, "utf8");
|
|
3891
|
+
} catch (error) {
|
|
3892
|
+
if (error.code === "ENOENT") {
|
|
3893
|
+
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path }) };
|
|
3894
|
+
}
|
|
3895
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { path, message: String(error) }) };
|
|
3896
|
+
}
|
|
3897
|
+
const frontmatter = extractFrontmatter(text);
|
|
3898
|
+
if (!frontmatter.ok) return { exitCode: errorExitCode2(frontmatter.error), result: frontmatter };
|
|
3899
|
+
const tags = asTagArray(frontmatter.data, path);
|
|
3900
|
+
if (!tags.ok) return { exitCode: ExitCode.INVALID_FRONTMATTER, result: tags };
|
|
3901
|
+
return { exitCode: ExitCode.OK, result: tags };
|
|
3447
3902
|
}
|
|
3448
|
-
function
|
|
3449
|
-
|
|
3903
|
+
async function resolveRequestedTags(input, page) {
|
|
3904
|
+
const explicit = input.tags ?? [];
|
|
3905
|
+
if (!Array.isArray(explicit) || !explicit.every((tag) => typeof tag === "string")) {
|
|
3906
|
+
return {
|
|
3907
|
+
exitCode: ExitCode.INVALID_FRONTMATTER,
|
|
3908
|
+
result: err("INVALID_FRONTMATTER", { message: "explicit tags must be an array of strings" })
|
|
3909
|
+
};
|
|
3910
|
+
}
|
|
3911
|
+
const source = input.from ?? (explicit.length === 0 ? join16(input.vault, page) : void 0);
|
|
3912
|
+
if (!source) return { exitCode: ExitCode.OK, result: ok({ tags: [...new Set(explicit)].sort() }) };
|
|
3913
|
+
const sourced = await readTagsFromFile(source);
|
|
3914
|
+
if (!sourced.result.ok) return { exitCode: sourced.exitCode, result: sourced.result };
|
|
3915
|
+
return {
|
|
3916
|
+
exitCode: ExitCode.OK,
|
|
3917
|
+
result: ok({ tags: [.../* @__PURE__ */ new Set([...explicit, ...sourced.result.data])].sort() })
|
|
3918
|
+
};
|
|
3450
3919
|
}
|
|
3451
|
-
function
|
|
3452
|
-
const path = lockPath(vault);
|
|
3453
|
-
if (!existsSync6(path)) return null;
|
|
3920
|
+
async function readSchema(schemaPath) {
|
|
3454
3921
|
try {
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3922
|
+
return { exitCode: ExitCode.OK, result: ok(await readFile11(schemaPath, "utf8")) };
|
|
3923
|
+
} catch (error) {
|
|
3924
|
+
if (error.code === "ENOENT") {
|
|
3925
|
+
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: schemaPath }) };
|
|
3926
|
+
}
|
|
3927
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { path: schemaPath, message: String(error) }) };
|
|
3459
3928
|
}
|
|
3460
3929
|
}
|
|
3461
|
-
function
|
|
3462
|
-
|
|
3463
|
-
const
|
|
3464
|
-
|
|
3930
|
+
function previewResult(page, tags, reconciled, dryRun, filesChanged) {
|
|
3931
|
+
if (!reconciled.ok) return { exitCode: errorExitCode2(reconciled.error), result: reconciled };
|
|
3932
|
+
const missingTags = reconciled.data.missing;
|
|
3933
|
+
const addedTags = dryRun ? [] : reconciled.data.added;
|
|
3934
|
+
const humanHint = dryRun ? missingTags.length > 0 ? `dry run: would add ${missingTags.join(", ")} to taxonomy for ${page}` : `dry run: taxonomy already includes requested tags for ${page}` : addedTags.length > 0 ? `added ${addedTags.join(", ")} to taxonomy for ${page}` : `taxonomy already includes requested tags for ${page}`;
|
|
3935
|
+
return {
|
|
3936
|
+
exitCode: ExitCode.OK,
|
|
3937
|
+
result: ok({
|
|
3938
|
+
page,
|
|
3939
|
+
requested_tags: tags,
|
|
3940
|
+
missing_tags: missingTags,
|
|
3941
|
+
added_tags: addedTags,
|
|
3942
|
+
changed: reconciled.data.changed,
|
|
3943
|
+
dry_run: dryRun,
|
|
3944
|
+
files_changed: filesChanged,
|
|
3945
|
+
humanHint
|
|
3946
|
+
})
|
|
3947
|
+
};
|
|
3465
3948
|
}
|
|
3466
|
-
function
|
|
3467
|
-
const
|
|
3468
|
-
const
|
|
3469
|
-
if (!
|
|
3470
|
-
|
|
3949
|
+
async function reconcileTagsWhileLocked(input, page, tags, comment) {
|
|
3950
|
+
const schemaPath = join16(input.vault, "SCHEMA.md");
|
|
3951
|
+
const current = await readSchema(schemaPath);
|
|
3952
|
+
if (!current.result.ok) return { exitCode: current.exitCode, result: current.result };
|
|
3953
|
+
const next = reconcileTaxonomyDocument(current.result.data, { tags, comment });
|
|
3954
|
+
if (!next.ok) return { exitCode: errorExitCode2(next.error), result: next };
|
|
3955
|
+
if (next.data.changed) {
|
|
3956
|
+
const written = await atomicWriteText(schemaPath, next.data.text);
|
|
3957
|
+
if (!written.ok) return { exitCode: ExitCode.WRITE_FAILED, result: written };
|
|
3958
|
+
}
|
|
3959
|
+
let verifiedText;
|
|
3960
|
+
try {
|
|
3961
|
+
verifiedText = await readFile11(schemaPath, "utf8");
|
|
3962
|
+
} catch (error) {
|
|
3963
|
+
return {
|
|
3964
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
3965
|
+
result: err("WRITE_FAILED", { stage: "verify-taxonomy", page, message: String(error) })
|
|
3966
|
+
};
|
|
3471
3967
|
}
|
|
3472
|
-
const
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3968
|
+
const verified = extractTaxonomy(verifiedText);
|
|
3969
|
+
if (!verified.ok || tags.some((tag) => !verified.data.includes(tag))) {
|
|
3970
|
+
return {
|
|
3971
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
3972
|
+
result: err("WRITE_FAILED", { stage: "verify-taxonomy", page })
|
|
3973
|
+
};
|
|
3974
|
+
}
|
|
3975
|
+
return previewResult(page, tags, next, false, next.data.changed ? ["SCHEMA.md"] : []);
|
|
3976
|
+
}
|
|
3977
|
+
async function runTagReconcile(input) {
|
|
3978
|
+
const page = validatePageIdentity(input.page);
|
|
3979
|
+
if (!page.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: page };
|
|
3980
|
+
const resolvedTags = await resolveRequestedTags(input, page.data);
|
|
3981
|
+
if (!resolvedTags.result.ok) return { exitCode: resolvedTags.exitCode, result: resolvedTags.result };
|
|
3982
|
+
const tags = resolvedTags.result.data.tags;
|
|
3983
|
+
const date = (input.now ?? /* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3984
|
+
const comment = taxonomyCommentForPage(page.data, date, input.reason);
|
|
3985
|
+
if (!comment.ok) return { exitCode: ExitCode.SCHEME_REJECTED, result: comment };
|
|
3986
|
+
if (!input.write) {
|
|
3987
|
+
const schema = await readSchema(join16(input.vault, "SCHEMA.md"));
|
|
3988
|
+
if (!schema.result.ok) return { exitCode: schema.exitCode, result: schema.result };
|
|
3989
|
+
const preview = reconcileTaxonomyDocument(schema.result.data, { tags, comment: comment.data });
|
|
3990
|
+
return previewResult(page.data, tags, preview, true, []);
|
|
3991
|
+
}
|
|
3992
|
+
let lock;
|
|
3487
3993
|
try {
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
writeLockedFile(path, lock);
|
|
3498
|
-
return { ok: true, lock };
|
|
3499
|
-
}
|
|
3500
|
-
if (force || isStale(existing)) {
|
|
3501
|
-
writeLockedFile(path, lock);
|
|
3502
|
-
return { ok: true, lock };
|
|
3503
|
-
}
|
|
3504
|
-
return { ok: false, held: existing };
|
|
3505
|
-
}
|
|
3506
|
-
function writeLockedFile(path, lock) {
|
|
3507
|
-
const tmp = path + ".tmp";
|
|
3508
|
-
const content = JSON.stringify(lock, null, 2) + "\n";
|
|
3509
|
-
writeFileSync3(tmp, content);
|
|
3510
|
-
renameSync2(tmp, path);
|
|
3511
|
-
}
|
|
3512
|
-
function releaseLock(vault, opts = {}) {
|
|
3513
|
-
const path = lockPath(vault);
|
|
3514
|
-
if (!existsSync6(path)) {
|
|
3515
|
-
return { released: false };
|
|
3516
|
-
}
|
|
3517
|
-
const sessionId = opts.sessionId ?? getSessionId();
|
|
3518
|
-
const existing = readLock(vault);
|
|
3519
|
-
if (opts.force) {
|
|
3520
|
-
try {
|
|
3521
|
-
unlinkSync2(path);
|
|
3522
|
-
const prior = existing && existing.session_id !== sessionId ? existing : void 0;
|
|
3523
|
-
return { released: true, prior };
|
|
3524
|
-
} catch {
|
|
3525
|
-
return { released: false };
|
|
3526
|
-
}
|
|
3994
|
+
lock = acquireOwnedSyncLock(input.vault, {
|
|
3995
|
+
summary: `tag reconcile ${page.data}`,
|
|
3996
|
+
ttlMinutes: 1
|
|
3997
|
+
});
|
|
3998
|
+
} catch (error) {
|
|
3999
|
+
return {
|
|
4000
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
4001
|
+
result: err("WRITE_FAILED", { stage: "lock", page: page.data, message: String(error) })
|
|
4002
|
+
};
|
|
3527
4003
|
}
|
|
3528
|
-
if (!
|
|
3529
|
-
|
|
4004
|
+
if (!lock.ok) return { exitCode: errorExitCode2(lock.error), result: lock };
|
|
4005
|
+
let outcome;
|
|
4006
|
+
let released;
|
|
4007
|
+
try {
|
|
4008
|
+
outcome = await reconcileTagsWhileLocked(input, page.data, tags, comment.data);
|
|
4009
|
+
} catch (error) {
|
|
4010
|
+
outcome = {
|
|
4011
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
4012
|
+
result: err("WRITE_FAILED", { stage: "reconcile", page: page.data, message: String(error) })
|
|
4013
|
+
};
|
|
4014
|
+
} finally {
|
|
4015
|
+
released = releaseOwnedSyncLock(lock.data);
|
|
3530
4016
|
}
|
|
4017
|
+
if (released === void 0 || !released.ok) {
|
|
4018
|
+
return {
|
|
4019
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
4020
|
+
result: err("WRITE_FAILED", {
|
|
4021
|
+
stage: "unlock",
|
|
4022
|
+
page: page.data,
|
|
4023
|
+
primary_error: outcome && !outcome.result.ok ? outcome.result.error : void 0,
|
|
4024
|
+
release_error: released && !released.ok ? released.detail : "release did not run"
|
|
4025
|
+
})
|
|
4026
|
+
};
|
|
4027
|
+
}
|
|
4028
|
+
return outcome ?? {
|
|
4029
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
4030
|
+
result: err("WRITE_FAILED", {
|
|
4031
|
+
stage: "reconcile",
|
|
4032
|
+
page: page.data,
|
|
4033
|
+
message: "locked reconciliation produced no result"
|
|
4034
|
+
})
|
|
4035
|
+
};
|
|
4036
|
+
}
|
|
4037
|
+
|
|
4038
|
+
// src/commands/sync.ts
|
|
4039
|
+
import { existsSync as existsSync5 } from "fs";
|
|
4040
|
+
import { join as join17 } from "path";
|
|
4041
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
4042
|
+
|
|
4043
|
+
// src/utils/git.ts
|
|
4044
|
+
import { execFileSync } from "child_process";
|
|
4045
|
+
function git(cwd, args) {
|
|
3531
4046
|
try {
|
|
3532
|
-
|
|
3533
|
-
return { released: true };
|
|
4047
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
3534
4048
|
} catch {
|
|
3535
|
-
return
|
|
4049
|
+
return "";
|
|
3536
4050
|
}
|
|
3537
4051
|
}
|
|
4052
|
+
function gitStrict(cwd, args) {
|
|
4053
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
4054
|
+
}
|
|
3538
4055
|
|
|
3539
4056
|
// src/utils/vault-git-pathspec.ts
|
|
3540
4057
|
var VAULT_GENERATED_COMMIT_PATHS = [
|
|
@@ -3587,7 +4104,7 @@ function refHasPath(vault, ref, path) {
|
|
|
3587
4104
|
function runSyncStatus(input) {
|
|
3588
4105
|
const vault = input.vault;
|
|
3589
4106
|
const includeStashes = input.includeStashes ?? false;
|
|
3590
|
-
if (!
|
|
4107
|
+
if (!existsSync5(join17(vault, ".git"))) {
|
|
3591
4108
|
return {
|
|
3592
4109
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
3593
4110
|
result: ok({
|
|
@@ -3694,7 +4211,7 @@ function runSyncStatus(input) {
|
|
|
3694
4211
|
}
|
|
3695
4212
|
async function runSyncPush(input) {
|
|
3696
4213
|
const vault = input.vault;
|
|
3697
|
-
if (!
|
|
4214
|
+
if (!existsSync5(join17(vault, ".git"))) {
|
|
3698
4215
|
return {
|
|
3699
4216
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
3700
4217
|
result: err("NOT_A_GIT_REPO", { path: vault })
|
|
@@ -3854,7 +4371,7 @@ function enableGitLongPathsOnWindows(vault) {
|
|
|
3854
4371
|
}
|
|
3855
4372
|
async function runSyncPull(input) {
|
|
3856
4373
|
const vault = input.vault;
|
|
3857
|
-
if (!
|
|
4374
|
+
if (!existsSync5(join17(vault, ".git"))) {
|
|
3858
4375
|
return {
|
|
3859
4376
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
3860
4377
|
result: err("NOT_A_GIT_REPO", { path: vault })
|
|
@@ -4026,7 +4543,7 @@ function runSyncPeers(input) {
|
|
|
4026
4543
|
}
|
|
4027
4544
|
function runSyncLock(input) {
|
|
4028
4545
|
const vault = input.vault;
|
|
4029
|
-
if (!
|
|
4546
|
+
if (!existsSync5(vault)) {
|
|
4030
4547
|
return {
|
|
4031
4548
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4032
4549
|
result: err("VAULT_PATH_INVALID", { path: vault })
|
|
@@ -4061,7 +4578,7 @@ function runSyncLock(input) {
|
|
|
4061
4578
|
}
|
|
4062
4579
|
function runSyncUnlock(input) {
|
|
4063
4580
|
const vault = input.vault;
|
|
4064
|
-
if (!
|
|
4581
|
+
if (!existsSync5(vault)) {
|
|
4065
4582
|
return {
|
|
4066
4583
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4067
4584
|
result: err("VAULT_PATH_INVALID", { path: vault })
|
|
@@ -4094,8 +4611,8 @@ function runSyncUnlock(input) {
|
|
|
4094
4611
|
}
|
|
4095
4612
|
|
|
4096
4613
|
// src/commands/backup.ts
|
|
4097
|
-
import { statSync as
|
|
4098
|
-
import { join as
|
|
4614
|
+
import { statSync as statSync2, readdirSync, readFileSync as readFileSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
4615
|
+
import { join as join18, relative as relative2, dirname as dirname6 } from "path";
|
|
4099
4616
|
import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
|
|
4100
4617
|
|
|
4101
4618
|
// src/utils/s3-client.ts
|
|
@@ -4119,7 +4636,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node_
|
|
|
4119
4636
|
function* walkMarkdown(dir, base) {
|
|
4120
4637
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
4121
4638
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
4122
|
-
const full =
|
|
4639
|
+
const full = join18(dir, entry.name);
|
|
4123
4640
|
if (entry.isDirectory()) {
|
|
4124
4641
|
yield* walkMarkdown(full, base);
|
|
4125
4642
|
} else if (entry.name.endsWith(".md")) {
|
|
@@ -4142,8 +4659,8 @@ async function runBackupSync(input) {
|
|
|
4142
4659
|
let failed = 0;
|
|
4143
4660
|
const files = [...walkMarkdown(input.vault, input.vault)];
|
|
4144
4661
|
for (const relPath of files) {
|
|
4145
|
-
const absPath =
|
|
4146
|
-
const localStat =
|
|
4662
|
+
const absPath = join18(input.vault, relPath);
|
|
4663
|
+
const localStat = statSync2(absPath);
|
|
4147
4664
|
let needsUpload = true;
|
|
4148
4665
|
try {
|
|
4149
4666
|
const head = await client.send(new HeadObjectCommand({ Bucket: input.bucket, Key: relPath }));
|
|
@@ -4161,7 +4678,7 @@ async function runBackupSync(input) {
|
|
|
4161
4678
|
continue;
|
|
4162
4679
|
}
|
|
4163
4680
|
try {
|
|
4164
|
-
const body =
|
|
4681
|
+
const body = readFileSync4(absPath);
|
|
4165
4682
|
await client.send(new PutObjectCommand({ Bucket: input.bucket, Key: relPath, Body: body }));
|
|
4166
4683
|
uploaded++;
|
|
4167
4684
|
} catch {
|
|
@@ -4218,9 +4735,9 @@ async function runBackupRestore(input) {
|
|
|
4218
4735
|
const objects = list.Contents ?? [];
|
|
4219
4736
|
for (const obj of objects) {
|
|
4220
4737
|
if (!obj.Key) continue;
|
|
4221
|
-
const localPath =
|
|
4738
|
+
const localPath = join18(target, obj.Key);
|
|
4222
4739
|
try {
|
|
4223
|
-
const localStat =
|
|
4740
|
+
const localStat = statSync2(localPath);
|
|
4224
4741
|
if (obj.LastModified && localStat.mtime > obj.LastModified) {
|
|
4225
4742
|
conflicts++;
|
|
4226
4743
|
continue;
|
|
@@ -4231,8 +4748,8 @@ async function runBackupRestore(input) {
|
|
|
4231
4748
|
const resp = await client.send(new GetObjectCommand({ Bucket: input.bucket, Key: obj.Key }));
|
|
4232
4749
|
const body = await resp.Body?.transformToByteArray();
|
|
4233
4750
|
if (body) {
|
|
4234
|
-
|
|
4235
|
-
|
|
4751
|
+
mkdirSync2(dirname6(localPath), { recursive: true });
|
|
4752
|
+
writeFileSync2(localPath, Buffer.from(body));
|
|
4236
4753
|
downloaded++;
|
|
4237
4754
|
}
|
|
4238
4755
|
} catch {
|
|
@@ -4264,11 +4781,11 @@ async function runBackupRestore(input) {
|
|
|
4264
4781
|
}
|
|
4265
4782
|
|
|
4266
4783
|
// src/commands/status.ts
|
|
4267
|
-
import { existsSync as
|
|
4268
|
-
import { readFile as
|
|
4269
|
-
import { join as
|
|
4784
|
+
import { existsSync as existsSync6, statSync as statSync3 } from "fs";
|
|
4785
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
4786
|
+
import { join as join19 } from "path";
|
|
4270
4787
|
async function runStatus(input) {
|
|
4271
|
-
if (!
|
|
4788
|
+
if (!existsSync6(input.vault)) {
|
|
4272
4789
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
4273
4790
|
}
|
|
4274
4791
|
const scan = await scanVault(input.vault);
|
|
@@ -4293,7 +4810,7 @@ async function runStatus(input) {
|
|
|
4293
4810
|
const compound = scan.data.compound.length;
|
|
4294
4811
|
let schemaVersion = "v1";
|
|
4295
4812
|
try {
|
|
4296
|
-
const schemaContent = await
|
|
4813
|
+
const schemaContent = await readFile12(join19(input.vault, "SCHEMA.md"), "utf8");
|
|
4297
4814
|
const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
|
|
4298
4815
|
if (versionMatch) schemaVersion = versionMatch[1];
|
|
4299
4816
|
} catch {
|
|
@@ -4309,7 +4826,7 @@ async function runStatus(input) {
|
|
|
4309
4826
|
let maxTime = 0;
|
|
4310
4827
|
for (const page of allPages) {
|
|
4311
4828
|
try {
|
|
4312
|
-
const st =
|
|
4829
|
+
const st = statSync3(page.absPath);
|
|
4313
4830
|
if (st.mtimeMs > maxTime) {
|
|
4314
4831
|
maxTime = st.mtimeMs;
|
|
4315
4832
|
lastModified = st.mtime.toISOString();
|
|
@@ -4353,8 +4870,8 @@ async function runStatus(input) {
|
|
|
4353
4870
|
}
|
|
4354
4871
|
|
|
4355
4872
|
// src/commands/seed.ts
|
|
4356
|
-
import { mkdir as mkdir9, writeFile as
|
|
4357
|
-
import { join as
|
|
4873
|
+
import { mkdir as mkdir9, writeFile as writeFile7, stat as stat4 } from "fs/promises";
|
|
4874
|
+
import { join as join20 } from "path";
|
|
4358
4875
|
var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4359
4876
|
var EXAMPLE_PAGES = {
|
|
4360
4877
|
"entities/example-project.md": `---
|
|
@@ -4423,30 +4940,30 @@ Real sources are immutable after ingestion \u2014 never edit them.
|
|
|
4423
4940
|
`;
|
|
4424
4941
|
async function runSeed(input) {
|
|
4425
4942
|
try {
|
|
4426
|
-
await
|
|
4943
|
+
await stat4(join20(input.vault, "SCHEMA.md"));
|
|
4427
4944
|
} catch {
|
|
4428
4945
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
|
|
4429
4946
|
}
|
|
4430
4947
|
const created = [];
|
|
4431
4948
|
const skipped = [];
|
|
4432
4949
|
for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
|
|
4433
|
-
const absPath =
|
|
4950
|
+
const absPath = join20(input.vault, relPath);
|
|
4434
4951
|
try {
|
|
4435
|
-
await
|
|
4952
|
+
await stat4(absPath);
|
|
4436
4953
|
skipped.push(relPath);
|
|
4437
4954
|
} catch {
|
|
4438
|
-
await mkdir9(
|
|
4439
|
-
await
|
|
4955
|
+
await mkdir9(join20(absPath, ".."), { recursive: true });
|
|
4956
|
+
await writeFile7(absPath, content, "utf8");
|
|
4440
4957
|
created.push(relPath);
|
|
4441
4958
|
}
|
|
4442
4959
|
}
|
|
4443
|
-
const rawPath =
|
|
4960
|
+
const rawPath = join20(input.vault, "raw", "articles", "example-source.md");
|
|
4444
4961
|
try {
|
|
4445
|
-
await
|
|
4962
|
+
await stat4(rawPath);
|
|
4446
4963
|
skipped.push("raw/articles/example-source.md");
|
|
4447
4964
|
} catch {
|
|
4448
|
-
await mkdir9(
|
|
4449
|
-
await
|
|
4965
|
+
await mkdir9(join20(rawPath, ".."), { recursive: true });
|
|
4966
|
+
await writeFile7(rawPath, EXAMPLE_RAW, "utf8");
|
|
4450
4967
|
created.push("raw/articles/example-source.md");
|
|
4451
4968
|
}
|
|
4452
4969
|
if (created.length > 0) {
|
|
@@ -4468,9 +4985,9 @@ async function runSeed(input) {
|
|
|
4468
4985
|
}
|
|
4469
4986
|
|
|
4470
4987
|
// src/commands/canvas.ts
|
|
4471
|
-
import { readFile as
|
|
4472
|
-
import { existsSync as
|
|
4473
|
-
import { join as
|
|
4988
|
+
import { readFile as readFile13, writeFile as writeFile8 } from "fs/promises";
|
|
4989
|
+
import { existsSync as existsSync7 } from "fs";
|
|
4990
|
+
import { join as join21 } from "path";
|
|
4474
4991
|
var NODE_WIDTH = 240;
|
|
4475
4992
|
var NODE_HEIGHT = 60;
|
|
4476
4993
|
var COLUMN_SPACING = 400;
|
|
@@ -4548,8 +5065,8 @@ function buildCanvasEdges(adjacency) {
|
|
|
4548
5065
|
return edges;
|
|
4549
5066
|
}
|
|
4550
5067
|
async function runCanvasGenerate(input) {
|
|
4551
|
-
const graphPath = input.graphPath ??
|
|
4552
|
-
if (!
|
|
5068
|
+
const graphPath = input.graphPath ?? join21(input.vault, ".skillwiki", "graph.json");
|
|
5069
|
+
if (!existsSync7(graphPath)) {
|
|
4553
5070
|
return {
|
|
4554
5071
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
4555
5072
|
result: err("FILE_NOT_FOUND", {
|
|
@@ -4560,7 +5077,7 @@ async function runCanvasGenerate(input) {
|
|
|
4560
5077
|
}
|
|
4561
5078
|
let raw;
|
|
4562
5079
|
try {
|
|
4563
|
-
raw = await
|
|
5080
|
+
raw = await readFile13(graphPath, "utf8");
|
|
4564
5081
|
} catch (e) {
|
|
4565
5082
|
return {
|
|
4566
5083
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
@@ -4586,9 +5103,9 @@ async function runCanvasGenerate(input) {
|
|
|
4586
5103
|
const nodes = buildCanvasNodes(paths);
|
|
4587
5104
|
const edges = buildCanvasEdges(graph.adjacency);
|
|
4588
5105
|
const canvas = { nodes, edges };
|
|
4589
|
-
const outPath =
|
|
5106
|
+
const outPath = join21(input.vault, "vault-graph.canvas");
|
|
4590
5107
|
try {
|
|
4591
|
-
await
|
|
5108
|
+
await writeFile8(outPath, JSON.stringify(canvas, null, 2));
|
|
4592
5109
|
} catch (e) {
|
|
4593
5110
|
return {
|
|
4594
5111
|
exitCode: ExitCode.WRITE_FAILED,
|
|
@@ -4608,10 +5125,10 @@ written: ${outPath}`
|
|
|
4608
5125
|
}
|
|
4609
5126
|
|
|
4610
5127
|
// src/commands/fleet-health.ts
|
|
4611
|
-
import { existsSync as
|
|
5128
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
|
|
4612
5129
|
import { execSync as nodeExecSync } from "child_process";
|
|
4613
5130
|
import { hostname as nodeHostname, platform as nodePlatform } from "os";
|
|
4614
|
-
import { join as
|
|
5131
|
+
import { join as join22 } from "path";
|
|
4615
5132
|
var SSH_TIMEOUT_MS = 15e3;
|
|
4616
5133
|
var TIMER_UNIT = "agent-memory-trends.timer";
|
|
4617
5134
|
var SERVICE_UNIT = "agent-memory-trends.service";
|
|
@@ -4708,9 +5225,9 @@ function applyServiceFailedOverlay(run, serviceFailed) {
|
|
|
4708
5225
|
function probeLocal(vaultPath, deps) {
|
|
4709
5226
|
const latestPath = satelliteLatestRunPath(vaultPath);
|
|
4710
5227
|
let parsed = null;
|
|
4711
|
-
if (
|
|
5228
|
+
if (existsSync8(latestPath)) {
|
|
4712
5229
|
try {
|
|
4713
|
-
const wire = readSatelliteLatestRunFromText(
|
|
5230
|
+
const wire = readSatelliteLatestRunFromText(readFileSync5(latestPath, "utf8"));
|
|
4714
5231
|
if (wire) {
|
|
4715
5232
|
parsed = {
|
|
4716
5233
|
status: wire.status,
|
|
@@ -4841,7 +5358,7 @@ async function runFleetHealth(input) {
|
|
|
4841
5358
|
const home = input.home ?? env.HOME ?? "";
|
|
4842
5359
|
const osHostname = input.osHostname ?? env.HOSTNAME ?? nodeHostname();
|
|
4843
5360
|
const vault = input.vault ?? env.WIKI_PATH;
|
|
4844
|
-
const file = input.file ?? (vault ?
|
|
5361
|
+
const file = input.file ?? (vault ? join22(vault, FLEET_REL_PATH) : void 0);
|
|
4845
5362
|
if (!file) {
|
|
4846
5363
|
return {
|
|
4847
5364
|
exitCode: ExitCode.NO_VAULT_CONFIGURED,
|
|
@@ -4925,15 +5442,15 @@ async function runFleetHealth(input) {
|
|
|
4925
5442
|
}
|
|
4926
5443
|
|
|
4927
5444
|
// src/utils/auto-commit.ts
|
|
4928
|
-
import { existsSync as
|
|
4929
|
-
import { join as
|
|
5445
|
+
import { existsSync as existsSync9 } from "fs";
|
|
5446
|
+
import { join as join23 } from "path";
|
|
4930
5447
|
async function postCommit(vault, exitCode) {
|
|
4931
5448
|
if (exitCode !== 0) return;
|
|
4932
5449
|
const home = process.env.HOME ?? "";
|
|
4933
5450
|
const dotenv = await parseDotenvFile(configPath(home));
|
|
4934
5451
|
const autoCommit = process.env.AUTO_COMMIT ?? dotenv["AUTO_COMMIT"];
|
|
4935
5452
|
if (autoCommit === "false") return;
|
|
4936
|
-
if (!
|
|
5453
|
+
if (!existsSync9(join23(vault, ".git"))) return;
|
|
4937
5454
|
const lastOps = readLastOp(vault);
|
|
4938
5455
|
if (lastOps.length === 0) return;
|
|
4939
5456
|
const porcelain = git(vault, ["status", "--porcelain", "--", ...VAULT_COMMIT_PATHSPEC]);
|
|
@@ -4957,8 +5474,8 @@ async function postCommit(vault, exitCode) {
|
|
|
4957
5474
|
}
|
|
4958
5475
|
|
|
4959
5476
|
// src/utils/protected-vault-write-guard.ts
|
|
4960
|
-
import { readFileSync as
|
|
4961
|
-
import { join as
|
|
5477
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
5478
|
+
import { join as join24, resolve as resolvePath } from "path";
|
|
4962
5479
|
async function guardProtectedVaultWrite(input) {
|
|
4963
5480
|
const env = input.env ?? process.env;
|
|
4964
5481
|
const home = input.home ?? process.env.HOME ?? "";
|
|
@@ -5024,7 +5541,7 @@ async function resolveLiveVaultPath(input) {
|
|
|
5024
5541
|
return resolved.ok ? resolved.data.path : void 0;
|
|
5025
5542
|
}
|
|
5026
5543
|
function resolveSnapshotWorktree(home) {
|
|
5027
|
-
const skillwikiEnv =
|
|
5544
|
+
const skillwikiEnv = join24(home, ".skillwiki", ".env");
|
|
5028
5545
|
const explicitWorktree = readEnvKey(skillwikiEnv, ["vault_sync.snapshot_worktree"]);
|
|
5029
5546
|
if (explicitWorktree) return explicitWorktree;
|
|
5030
5547
|
const snapshotProfile = readEnvKey(skillwikiEnv, ["vault_sync.snapshot_profile"]);
|
|
@@ -5036,7 +5553,7 @@ function resolveSnapshotWorktree(home) {
|
|
|
5036
5553
|
}
|
|
5037
5554
|
function readEnvKey(path, keys) {
|
|
5038
5555
|
try {
|
|
5039
|
-
const content =
|
|
5556
|
+
const content = readFileSync6(path, "utf8");
|
|
5040
5557
|
for (const line of content.split(/\r?\n/)) {
|
|
5041
5558
|
const trimmed = line.trim();
|
|
5042
5559
|
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
|
|
@@ -5094,7 +5611,7 @@ program.command("validate <file>").description("validate vault page frontmatter
|
|
|
5094
5611
|
emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
|
|
5095
5612
|
});
|
|
5096
5613
|
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) => {
|
|
5097
|
-
const out = opts.out ??
|
|
5614
|
+
const out = opts.out ?? join25(vault, ".skillwiki", "graph.json");
|
|
5098
5615
|
return emitGuardedVaultWrite(vault, "graph build", () => runGraphBuild({ vault, out }));
|
|
5099
5616
|
});
|
|
5100
5617
|
var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
|
|
@@ -5188,6 +5705,45 @@ program.command("tag-audit [vault]").description("audit tag taxonomy consistency
|
|
|
5188
5705
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5189
5706
|
else emit(await runTagAudit({ vault: v.vault }), v.vault);
|
|
5190
5707
|
});
|
|
5708
|
+
var tagCmd = program.command("tag").description("manage the vault tag taxonomy");
|
|
5709
|
+
tagCmd.command("reconcile [vault]").description("preview or add prospective page tags to SCHEMA taxonomy").requiredOption("--page <path>", "vault-relative typed page target").option("--from <path>", "unpublished draft or existing page to read tags from").option("--tags <csv>", "comma-separated prospective tags").option("--reason <text>", "reconciliation comment reason").option("--write", "write SCHEMA.md after successful preview", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5710
|
+
const resolved = await resolveVaultArg(vault, opts.wiki);
|
|
5711
|
+
if (!resolved.ok) return emit({ exitCode: resolved.exitCode, result: resolved.payload });
|
|
5712
|
+
const input = {
|
|
5713
|
+
vault: resolved.vault,
|
|
5714
|
+
page: opts.page,
|
|
5715
|
+
from: opts.from,
|
|
5716
|
+
tags: opts.tags?.split(",").map((tag) => tag.trim()).filter(Boolean),
|
|
5717
|
+
reason: opts.reason,
|
|
5718
|
+
write: !!opts.write
|
|
5719
|
+
};
|
|
5720
|
+
if (!opts.write) return emit(await runTagReconcile(input), resolved.vault, { postCommit: false });
|
|
5721
|
+
return emitGuardedVaultWrite(
|
|
5722
|
+
resolved.vault,
|
|
5723
|
+
"tag reconcile",
|
|
5724
|
+
() => runTagReconcile(input),
|
|
5725
|
+
{ postCommit: false }
|
|
5726
|
+
);
|
|
5727
|
+
});
|
|
5728
|
+
var pageCmd = program.command("page").description("validate and publish typed vault pages");
|
|
5729
|
+
pageCmd.command("publish <draft> [vault]").description("preview or publish an unpublished typed-page draft").requiredOption("--target <path>", "vault-relative typed page target").option("--log-note <text>", "single-line publication log note").option("--write", "publish SCHEMA, page, index, and log", false).option("--wiki <name>", "wiki profile name").action(async (draft, vault, opts) => {
|
|
5730
|
+
const resolved = await resolveVaultArg(vault, opts.wiki);
|
|
5731
|
+
if (!resolved.ok) return emit({ exitCode: resolved.exitCode, result: resolved.payload });
|
|
5732
|
+
const input = {
|
|
5733
|
+
vault: resolved.vault,
|
|
5734
|
+
draftPath: draft,
|
|
5735
|
+
target: opts.target,
|
|
5736
|
+
logNote: opts.logNote,
|
|
5737
|
+
write: !!opts.write
|
|
5738
|
+
};
|
|
5739
|
+
if (!opts.write) return emit(await runPagePublish(input), resolved.vault, { postCommit: false });
|
|
5740
|
+
return emitGuardedVaultWrite(
|
|
5741
|
+
resolved.vault,
|
|
5742
|
+
"page publish",
|
|
5743
|
+
() => runPagePublish(input),
|
|
5744
|
+
{ postCommit: false }
|
|
5745
|
+
);
|
|
5746
|
+
});
|
|
5191
5747
|
program.command("index-check [vault]").description("verify index.md entries match actual vault pages").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5192
5748
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5193
5749
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|