skillwiki 0.9.61 → 0.9.63
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-TUFQZ5K4.js} +1182 -518
- package/dist/cli.js +1133 -412
- package/dist/skillwiki-mcp.js +1 -1
- package/package.json +1 -1
- package/skills/.claude-plugin/plugin.json +2 -2
- package/skills/.codex-plugin/plugin.json +3 -3
- 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 +31 -2
- package/skills/skills/wiki-archive/SKILL.md +18 -4
- package/skills/skills/wiki-ingest/SKILL.md +23 -27
- package/skills/skills/wiki-query/SKILL.md +11 -2
- package/skills/skills/wiki-remove/SKILL.md +96 -0
- package/skills/using-skillwiki/SKILL.md +31 -2
- package/skills/wiki-archive/SKILL.md +18 -4
- package/skills/wiki-ingest/SKILL.md +23 -27
- package/skills/wiki-query/SKILL.md +11 -2
- package/skills/wiki-remove/SKILL.md +96 -0
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-TUFQZ5K4.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 join27 } 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,50 @@ 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 mkdir6, readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
|
|
1363
|
+
import { join as join9, dirname as dirname5 } from "path";
|
|
1364
|
+
|
|
1365
|
+
// src/utils/delete-intent.ts
|
|
1366
|
+
import { mkdir as mkdir5, writeFile as writeFile4, readdir as readdir3, readFile as readFile5 } from "fs/promises";
|
|
1367
|
+
import { join as join8 } from "path";
|
|
1368
|
+
var DELETE_INTENT_SCHEMA = "vault-delete-intent/v1";
|
|
1369
|
+
var DELETE_INTENT_DIR = "meta/delete-intents";
|
|
1370
|
+
function normalizeVaultRelPath(path) {
|
|
1371
|
+
const p = path.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
1372
|
+
if (!p || p.includes("..") || p.startsWith(".git/")) {
|
|
1373
|
+
throw new Error(`invalid vault-relative path: ${path}`);
|
|
1374
|
+
}
|
|
1375
|
+
return p;
|
|
1376
|
+
}
|
|
1377
|
+
function pathToIntentFilename(path) {
|
|
1378
|
+
const p = normalizeVaultRelPath(path);
|
|
1379
|
+
return `${p.replace(/\//g, "__")}.json`;
|
|
1380
|
+
}
|
|
1381
|
+
function intentHostId() {
|
|
1382
|
+
return process.env.SKILLWIKI_HOST_ID ?? process.env.AGENT_HOST_ID ?? "unknown";
|
|
1383
|
+
}
|
|
1384
|
+
function buildDeleteIntent(input) {
|
|
1385
|
+
return {
|
|
1386
|
+
schema: DELETE_INTENT_SCHEMA,
|
|
1387
|
+
path: normalizeVaultRelPath(input.path),
|
|
1388
|
+
action: input.action,
|
|
1389
|
+
created: input.created ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1390
|
+
host: input.host ?? intentHostId(),
|
|
1391
|
+
actor: input.actor,
|
|
1392
|
+
reason: input.reason,
|
|
1393
|
+
source: input.source,
|
|
1394
|
+
expires: input.expires ?? null
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
async function writeDeleteIntent(vault, intent) {
|
|
1398
|
+
const dir = join8(vault, DELETE_INTENT_DIR);
|
|
1399
|
+
await mkdir5(dir, { recursive: true });
|
|
1400
|
+
const rel = `${DELETE_INTENT_DIR}/${pathToIntentFilename(intent.path)}`;
|
|
1401
|
+
await writeFile4(join8(vault, rel), JSON.stringify(intent, null, 2) + "\n", "utf8");
|
|
1402
|
+
return rel;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// src/commands/archive.ts
|
|
1450
1406
|
function countWikilinks(body, slug) {
|
|
1451
1407
|
const escaped = slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1452
1408
|
const re = new RegExp(`\\[\\[${escaped}(?:[|#][^\\]]*)?\\]\\]`, "g");
|
|
@@ -1480,7 +1436,7 @@ async function runArchive(input) {
|
|
|
1480
1436
|
if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
|
|
1481
1437
|
if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
|
|
1482
1438
|
const slug = relPath.replace(/\.md$/, "").split("/").pop();
|
|
1483
|
-
const archivePath =
|
|
1439
|
+
const archivePath = join9("_archive", relPath).replace(/\\/g, "/");
|
|
1484
1440
|
const remoteRoot = normalizeRemoteRoot(input.remote);
|
|
1485
1441
|
const remoteObjectPath = buildRemoteObjectPath(remoteRoot, relPath);
|
|
1486
1442
|
let cascade;
|
|
@@ -1506,7 +1462,7 @@ async function runArchive(input) {
|
|
|
1506
1462
|
const indexRefs = [];
|
|
1507
1463
|
if (!isRaw) {
|
|
1508
1464
|
try {
|
|
1509
|
-
const idx = await readFile6(
|
|
1465
|
+
const idx = await readFile6(join9(input.vault, "index.md"), "utf8");
|
|
1510
1466
|
idx.split("\n").forEach((line, i) => {
|
|
1511
1467
|
if (line.includes(`[[${slug}]]`)) indexRefs.push({ line: i + 1, text: line });
|
|
1512
1468
|
});
|
|
@@ -1533,7 +1489,7 @@ async function runArchive(input) {
|
|
|
1533
1489
|
}
|
|
1534
1490
|
if (input.cascade && input.apply && cascade) {
|
|
1535
1491
|
for (const ref of cascade.source_array_refs) {
|
|
1536
|
-
const absPath =
|
|
1492
|
+
const absPath = join9(input.vault, ref.page);
|
|
1537
1493
|
const text = await readFile6(absPath, "utf8");
|
|
1538
1494
|
const split = splitFrontmatter(text);
|
|
1539
1495
|
if (!split.ok) continue;
|
|
@@ -1551,10 +1507,10 @@ ${fmRewritten}
|
|
|
1551
1507
|
}
|
|
1552
1508
|
}
|
|
1553
1509
|
}
|
|
1554
|
-
await
|
|
1510
|
+
await mkdir6(dirname5(join9(input.vault, archivePath)), { recursive: true });
|
|
1555
1511
|
let indexUpdated = false;
|
|
1556
1512
|
if (!isRaw) {
|
|
1557
|
-
const indexPath =
|
|
1513
|
+
const indexPath = join9(input.vault, "index.md");
|
|
1558
1514
|
try {
|
|
1559
1515
|
const idx = await readFile6(indexPath, "utf8");
|
|
1560
1516
|
const originalLines = idx.split("\n");
|
|
@@ -1567,11 +1523,18 @@ ${fmRewritten}
|
|
|
1567
1523
|
if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
|
|
1568
1524
|
}
|
|
1569
1525
|
}
|
|
1570
|
-
await
|
|
1526
|
+
await rename2(join9(input.vault, relPath), join9(input.vault, archivePath));
|
|
1527
|
+
const archiveIntent = buildDeleteIntent({
|
|
1528
|
+
path: relPath,
|
|
1529
|
+
action: "archive",
|
|
1530
|
+
actor: "skillwiki-cli",
|
|
1531
|
+
source: "cli"
|
|
1532
|
+
});
|
|
1533
|
+
const tombstonePath = await writeDeleteIntent(input.vault, archiveIntent);
|
|
1571
1534
|
appendLastOp(input.vault, {
|
|
1572
1535
|
operation: input.cascade ? "archive-cascade" : "archive",
|
|
1573
|
-
summary: `moved ${relPath} to ${archivePath}${input.cascade ? ` (cascade: ${cascade?.source_array_refs.length ?? 0} source arrays updated)` : ""}`,
|
|
1574
|
-
files: [relPath],
|
|
1536
|
+
summary: `moved ${relPath} to ${archivePath}${input.cascade ? ` (cascade: ${cascade?.source_array_refs.length ?? 0} source arrays updated)` : ""}; tombstone ${tombstonePath}`,
|
|
1537
|
+
files: [relPath, archivePath, tombstonePath],
|
|
1575
1538
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1576
1539
|
});
|
|
1577
1540
|
let remote;
|
|
@@ -1600,6 +1563,106 @@ ${fmRewritten}
|
|
|
1600
1563
|
};
|
|
1601
1564
|
}
|
|
1602
1565
|
|
|
1566
|
+
// src/commands/remove.ts
|
|
1567
|
+
import { unlink as unlink2, readFile as readFile7, writeFile as writeFile6, access } from "fs/promises";
|
|
1568
|
+
import { join as join10 } from "path";
|
|
1569
|
+
async function pathExists(abs) {
|
|
1570
|
+
try {
|
|
1571
|
+
await access(abs);
|
|
1572
|
+
return true;
|
|
1573
|
+
} catch {
|
|
1574
|
+
return false;
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
async function runRemove(input) {
|
|
1578
|
+
if (input.remoteDelete && !input.remote) {
|
|
1579
|
+
return { exitCode: ExitCode.USAGE, result: err("USAGE", { message: "--remote-delete requires --remote" }) };
|
|
1580
|
+
}
|
|
1581
|
+
if (input.remoteDelete && !isValidRemoteDeleteCap(input.maxRemoteDeletes)) {
|
|
1582
|
+
return { exitCode: ExitCode.USAGE, result: err("USAGE", { message: "--max-remote-deletes must be a positive integer" }) };
|
|
1583
|
+
}
|
|
1584
|
+
const scan = await scanVault(input.vault);
|
|
1585
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1586
|
+
const lookup = (pages) => {
|
|
1587
|
+
if (input.page.includes("/")) return pages.find((p) => p.relPath === input.page)?.relPath;
|
|
1588
|
+
return pages.find((p) => p.relPath.replace(/\.md$/, "").split("/").pop() === input.page)?.relPath;
|
|
1589
|
+
};
|
|
1590
|
+
let relPath = lookup(scan.data.typedKnowledge) ?? lookup(scan.data.raw) ?? null;
|
|
1591
|
+
if (!relPath) {
|
|
1592
|
+
try {
|
|
1593
|
+
const candidate = normalizeVaultRelPath(input.page);
|
|
1594
|
+
if (await pathExists(join10(input.vault, candidate))) {
|
|
1595
|
+
relPath = candidate;
|
|
1596
|
+
}
|
|
1597
|
+
} catch {
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
if (!relPath) {
|
|
1601
|
+
return {
|
|
1602
|
+
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
1603
|
+
result: err("FILE_NOT_FOUND", { page: input.page })
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
if (relPath.startsWith("_archive/")) {
|
|
1607
|
+
return {
|
|
1608
|
+
exitCode: ExitCode.USAGE,
|
|
1609
|
+
result: err("USAGE", { message: "refusing to remove path already under _archive/; use restore or leave archived" })
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
const remoteRoot = normalizeRemoteRoot(input.remote);
|
|
1613
|
+
const remoteObjectPath = buildRemoteObjectPath(remoteRoot, relPath);
|
|
1614
|
+
const slug = relPath.replace(/\.md$/, "").split("/").pop() ?? relPath;
|
|
1615
|
+
let indexUpdated = false;
|
|
1616
|
+
if (relPath.endsWith(".md") && !relPath.startsWith("raw/")) {
|
|
1617
|
+
const indexPath = join10(input.vault, "index.md");
|
|
1618
|
+
try {
|
|
1619
|
+
const idx = await readFile7(indexPath, "utf8");
|
|
1620
|
+
const originalLines = idx.split("\n");
|
|
1621
|
+
const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
|
|
1622
|
+
if (filtered.length !== originalLines.length) {
|
|
1623
|
+
await writeFile6(indexPath, filtered.join("\n"), "utf8");
|
|
1624
|
+
indexUpdated = true;
|
|
1625
|
+
}
|
|
1626
|
+
} catch (e) {
|
|
1627
|
+
if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
const intent = buildDeleteIntent({
|
|
1631
|
+
path: relPath,
|
|
1632
|
+
action: "remove",
|
|
1633
|
+
actor: "skillwiki-cli",
|
|
1634
|
+
source: "cli",
|
|
1635
|
+
reason: input.reason
|
|
1636
|
+
});
|
|
1637
|
+
const tombstonePath = await writeDeleteIntent(input.vault, intent);
|
|
1638
|
+
await unlink2(join10(input.vault, relPath));
|
|
1639
|
+
appendLastOp(input.vault, {
|
|
1640
|
+
operation: "remove",
|
|
1641
|
+
summary: `removed ${relPath} (tombstone ${tombstonePath})`,
|
|
1642
|
+
files: [relPath, tombstonePath],
|
|
1643
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1644
|
+
});
|
|
1645
|
+
let remote;
|
|
1646
|
+
if (remoteObjectPath) {
|
|
1647
|
+
const pruned = await planAndMaybePruneRemoteObjects([remoteObjectPath], input);
|
|
1648
|
+
if (!pruned.ok) {
|
|
1649
|
+
return { exitCode: ExitCode.SYNC_PUSH_FAILED, result: pruned };
|
|
1650
|
+
}
|
|
1651
|
+
remote = pruned.data;
|
|
1652
|
+
}
|
|
1653
|
+
const remoteNote = remote ? ` (remote ${input.remoteDelete ? `deleted ${remote.deleted.length}` : `planned ${remote.plannedDeletes.length}`})` : "";
|
|
1654
|
+
return {
|
|
1655
|
+
exitCode: ExitCode.OK,
|
|
1656
|
+
result: ok({
|
|
1657
|
+
removed: relPath,
|
|
1658
|
+
tombstone_path: tombstonePath,
|
|
1659
|
+
index_updated: indexUpdated,
|
|
1660
|
+
...remote ? { remote } : {},
|
|
1661
|
+
humanHint: `removed ${relPath}; tombstone ${tombstonePath}${indexUpdated ? " (index updated)" : ""}${remoteNote}`
|
|
1662
|
+
})
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1603
1666
|
// src/commands/drift.ts
|
|
1604
1667
|
import { createHash as createHash2 } from "crypto";
|
|
1605
1668
|
|
|
@@ -2023,7 +2086,7 @@ async function runUpdate(input) {
|
|
|
2023
2086
|
|
|
2024
2087
|
// src/commands/self-update.ts
|
|
2025
2088
|
import { execSync as execSync2 } from "child_process";
|
|
2026
|
-
import { existsSync as
|
|
2089
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
2027
2090
|
import { join as join12 } from "path";
|
|
2028
2091
|
var DEFAULT_SOURCE_ROOT_SUFFIX = "/Desktop/code/llm-wiki";
|
|
2029
2092
|
async function runSelfUpdate(input) {
|
|
@@ -2031,7 +2094,7 @@ async function runSelfUpdate(input) {
|
|
|
2031
2094
|
const sourceRoot = input.sourceRoot ?? `${input.home}${DEFAULT_SOURCE_ROOT_SUFFIX}`;
|
|
2032
2095
|
const distTag = normalizeDistTag(input.distTag);
|
|
2033
2096
|
const localPkgPath = join12(sourceRoot, "packages", "cli", "package.json");
|
|
2034
|
-
const hasLocalSource =
|
|
2097
|
+
const hasLocalSource = existsSync3(localPkgPath);
|
|
2035
2098
|
if (input.check) {
|
|
2036
2099
|
let availableVersion = null;
|
|
2037
2100
|
let source;
|
|
@@ -2162,13 +2225,13 @@ async function runSelfUpdate(input) {
|
|
|
2162
2225
|
}
|
|
2163
2226
|
|
|
2164
2227
|
// src/commands/transcripts.ts
|
|
2165
|
-
import { readdir as
|
|
2228
|
+
import { readdir as readdir4, stat as stat3, readFile as readFile8 } from "fs/promises";
|
|
2166
2229
|
import { join as join13 } from "path";
|
|
2167
2230
|
async function runTranscripts(input) {
|
|
2168
2231
|
const dir = join13(input.vault, "raw", "transcripts");
|
|
2169
2232
|
let entries;
|
|
2170
2233
|
try {
|
|
2171
|
-
entries = await
|
|
2234
|
+
entries = await readdir4(dir, { withFileTypes: true });
|
|
2172
2235
|
} catch {
|
|
2173
2236
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: { ok: false, error: "VAULT_PATH_INVALID", detail: `raw/transcripts/ not found: ${dir}` } };
|
|
2174
2237
|
}
|
|
@@ -2176,12 +2239,12 @@ async function runTranscripts(input) {
|
|
|
2176
2239
|
for (const entry of entries) {
|
|
2177
2240
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
2178
2241
|
const filePath = join13(dir, entry.name);
|
|
2179
|
-
const content = await
|
|
2242
|
+
const content = await readFile8(filePath, "utf8");
|
|
2180
2243
|
const fm = extractFrontmatter(content);
|
|
2181
2244
|
if (!fm.ok) continue;
|
|
2182
2245
|
const ingested = typeof fm.data.ingested === "string" ? fm.data.ingested : "";
|
|
2183
2246
|
if (input.since && ingested && ingested < input.since) continue;
|
|
2184
|
-
const s = await
|
|
2247
|
+
const s = await stat3(filePath);
|
|
2185
2248
|
transcripts.push({
|
|
2186
2249
|
file: `raw/transcripts/${entry.name}`,
|
|
2187
2250
|
ingested,
|
|
@@ -2193,10 +2256,10 @@ async function runTranscripts(input) {
|
|
|
2193
2256
|
}
|
|
2194
2257
|
|
|
2195
2258
|
// src/commands/compound.ts
|
|
2196
|
-
import { writeFile as
|
|
2259
|
+
import { writeFile as writeFile7, mkdir as mkdir7, readdir as readdir5, unlink as unlink3 } from "fs/promises";
|
|
2197
2260
|
import { join as join14 } from "path";
|
|
2198
|
-
import { existsSync as
|
|
2199
|
-
import { readFile as
|
|
2261
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2262
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
2200
2263
|
var RETRO_HEADING_RE = /^## \[(\d{4}-\d{2}-\d{2})(?:\s+[^\]]+)?\] retro \| loop cycle(?: (\d+))?: (.+)$/;
|
|
2201
2264
|
var FIELD_RE = {
|
|
2202
2265
|
improve: /^-\s+\*?\*?Improve:?\*?\*?\s*(.+)$/m,
|
|
@@ -2297,7 +2360,7 @@ async function runCompound(input) {
|
|
|
2297
2360
|
const logPath = join14(input.vault, "log.md");
|
|
2298
2361
|
let logText;
|
|
2299
2362
|
try {
|
|
2300
|
-
logText = await
|
|
2363
|
+
logText = await readFile9(logPath, "utf8");
|
|
2301
2364
|
} catch {
|
|
2302
2365
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
|
|
2303
2366
|
}
|
|
@@ -2313,7 +2376,7 @@ async function runCompound(input) {
|
|
|
2313
2376
|
}
|
|
2314
2377
|
const slug = slugify(entry.cycleName);
|
|
2315
2378
|
const compoundPath = join14(compoundDir, `${slug}.md`);
|
|
2316
|
-
if (
|
|
2379
|
+
if (existsSync4(compoundPath)) {
|
|
2317
2380
|
skipped.push(entry.date);
|
|
2318
2381
|
continue;
|
|
2319
2382
|
}
|
|
@@ -2351,10 +2414,10 @@ async function runCompound(input) {
|
|
|
2351
2414
|
].join("\n");
|
|
2352
2415
|
const content = frontmatter + "\n" + body;
|
|
2353
2416
|
if (!input.dryRun) {
|
|
2354
|
-
if (!
|
|
2355
|
-
await
|
|
2417
|
+
if (!existsSync4(compoundDir)) {
|
|
2418
|
+
await mkdir7(compoundDir, { recursive: true });
|
|
2356
2419
|
}
|
|
2357
|
-
await
|
|
2420
|
+
await writeFile7(compoundPath, content, "utf8");
|
|
2358
2421
|
}
|
|
2359
2422
|
promoted.push(`${slug}.md`);
|
|
2360
2423
|
}
|
|
@@ -2374,7 +2437,7 @@ async function runCompound(input) {
|
|
|
2374
2437
|
}
|
|
2375
2438
|
async function runCompoundDelete(input) {
|
|
2376
2439
|
const projectDir = join14(input.vault, "projects", input.project);
|
|
2377
|
-
if (!
|
|
2440
|
+
if (!existsSync4(projectDir)) {
|
|
2378
2441
|
return {
|
|
2379
2442
|
exitCode: ExitCode.PROJECT_NOT_FOUND,
|
|
2380
2443
|
result: err("PROJECT_NOT_FOUND", { slug: input.project, path: projectDir })
|
|
@@ -2382,14 +2445,14 @@ async function runCompoundDelete(input) {
|
|
|
2382
2445
|
}
|
|
2383
2446
|
const entryName = input.entry.replace(/\.md$/, "");
|
|
2384
2447
|
const compoundPath = join14(projectDir, "compound", `${entryName}.md`);
|
|
2385
|
-
if (!
|
|
2448
|
+
if (!existsSync4(compoundPath)) {
|
|
2386
2449
|
return {
|
|
2387
2450
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
2388
2451
|
result: err("FILE_NOT_FOUND", { path: compoundPath })
|
|
2389
2452
|
};
|
|
2390
2453
|
}
|
|
2391
2454
|
try {
|
|
2392
|
-
await
|
|
2455
|
+
await unlink3(compoundPath);
|
|
2393
2456
|
} catch (e) {
|
|
2394
2457
|
return {
|
|
2395
2458
|
exitCode: ExitCode.WRITE_FAILED,
|
|
@@ -2416,7 +2479,7 @@ knowledge.md regenerated`
|
|
|
2416
2479
|
}
|
|
2417
2480
|
async function runCompoundList(input) {
|
|
2418
2481
|
const compoundDir = join14(input.vault, "projects", input.project, "compound");
|
|
2419
|
-
if (!
|
|
2482
|
+
if (!existsSync4(compoundDir)) {
|
|
2420
2483
|
return {
|
|
2421
2484
|
exitCode: ExitCode.OK,
|
|
2422
2485
|
result: ok({
|
|
@@ -2430,7 +2493,7 @@ no compound directory found`
|
|
|
2430
2493
|
}
|
|
2431
2494
|
let dirents;
|
|
2432
2495
|
try {
|
|
2433
|
-
dirents = await
|
|
2496
|
+
dirents = await readdir5(compoundDir, { withFileTypes: true });
|
|
2434
2497
|
} catch {
|
|
2435
2498
|
return {
|
|
2436
2499
|
exitCode: ExitCode.OK,
|
|
@@ -2449,7 +2512,7 @@ could not read compound directory`
|
|
|
2449
2512
|
const filePath = join14(compoundDir, dirent.name);
|
|
2450
2513
|
let text;
|
|
2451
2514
|
try {
|
|
2452
|
-
text = await
|
|
2515
|
+
text = await readFile9(filePath, "utf8");
|
|
2453
2516
|
} catch {
|
|
2454
2517
|
continue;
|
|
2455
2518
|
}
|
|
@@ -2478,7 +2541,7 @@ no compound entries found`;
|
|
|
2478
2541
|
}
|
|
2479
2542
|
|
|
2480
2543
|
// src/commands/session-brief.ts
|
|
2481
|
-
import { mkdir as
|
|
2544
|
+
import { mkdir as mkdir8, readFile as readFile10, writeFile as writeFile8 } from "fs/promises";
|
|
2482
2545
|
import { join as join15, relative, sep } from "path";
|
|
2483
2546
|
var MAX_WORDS = 900;
|
|
2484
2547
|
async function runSessionBrief(input) {
|
|
@@ -2588,7 +2651,7 @@ async function resolveProject(input) {
|
|
|
2588
2651
|
async function readProjectSlug(file) {
|
|
2589
2652
|
let text;
|
|
2590
2653
|
try {
|
|
2591
|
-
text = await
|
|
2654
|
+
text = await readFile10(file, "utf8");
|
|
2592
2655
|
} catch {
|
|
2593
2656
|
return void 0;
|
|
2594
2657
|
}
|
|
@@ -2815,15 +2878,15 @@ async function writeBriefArtifacts(vault, input) {
|
|
|
2815
2878
|
const metaPath = join15(vault, "meta", "latest-session-brief.md");
|
|
2816
2879
|
const cacheMdPath = join15(vault, ".skillwiki", "session-brief.md");
|
|
2817
2880
|
const cacheJsonPath = join15(vault, ".skillwiki", "session-brief.json");
|
|
2818
|
-
await
|
|
2819
|
-
await
|
|
2881
|
+
await mkdir8(join15(vault, "meta"), { recursive: true });
|
|
2882
|
+
await mkdir8(join15(vault, ".skillwiki"), { recursive: true });
|
|
2820
2883
|
const committed = renderCommittedBrief(input);
|
|
2821
2884
|
const previousComparable = comparableBrief(await readIfExists(metaPath));
|
|
2822
2885
|
const nextComparable = comparableBrief(committed);
|
|
2823
2886
|
const materialChange = previousComparable !== nextComparable;
|
|
2824
|
-
await
|
|
2825
|
-
await
|
|
2826
|
-
await
|
|
2887
|
+
await writeFile8(metaPath, committed, "utf8");
|
|
2888
|
+
await writeFile8(cacheMdPath, input.brief, "utf8");
|
|
2889
|
+
await writeFile8(cacheJsonPath, `${JSON.stringify({
|
|
2827
2890
|
project: input.project,
|
|
2828
2891
|
brief: input.brief,
|
|
2829
2892
|
word_count: input.wordCount,
|
|
@@ -2892,7 +2955,7 @@ async function ensureIndexEntry(vault) {
|
|
|
2892
2955
|
while (insertAt < lines.length && !lines[insertAt].startsWith("## ")) insertAt++;
|
|
2893
2956
|
lines.splice(insertAt, 0, entry);
|
|
2894
2957
|
}
|
|
2895
|
-
await
|
|
2958
|
+
await writeFile8(indexPath, lines.join("\n"), "utf8");
|
|
2896
2959
|
return true;
|
|
2897
2960
|
}
|
|
2898
2961
|
async function appendMaterialLog(vault, today) {
|
|
@@ -2901,12 +2964,12 @@ async function appendMaterialLog(vault, today) {
|
|
|
2901
2964
|
if (!text) return false;
|
|
2902
2965
|
const entry = `
|
|
2903
2966
|
## [${today}] session-brief | refreshed: meta/latest-session-brief.md`;
|
|
2904
|
-
await
|
|
2967
|
+
await writeFile8(logPath, text.trimEnd() + entry + "\n", "utf8");
|
|
2905
2968
|
return true;
|
|
2906
2969
|
}
|
|
2907
2970
|
async function readIfExists(path) {
|
|
2908
2971
|
try {
|
|
2909
|
-
return await
|
|
2972
|
+
return await readFile10(path, "utf8");
|
|
2910
2973
|
} catch {
|
|
2911
2974
|
return "";
|
|
2912
2975
|
}
|
|
@@ -2948,9 +3011,419 @@ function dateFromPath(path) {
|
|
|
2948
3011
|
}
|
|
2949
3012
|
|
|
2950
3013
|
// src/commands/ingest.ts
|
|
2951
|
-
import { readFile as
|
|
2952
|
-
import { join as
|
|
3014
|
+
import { readFile as readFile12, open, unlink as unlink4, mkdir as mkdir9 } from "fs/promises";
|
|
3015
|
+
import { join as join17 } from "path";
|
|
3016
|
+
import { createHash as createHash4 } from "crypto";
|
|
3017
|
+
|
|
3018
|
+
// src/commands/page-publish.ts
|
|
2953
3019
|
import { createHash as createHash3 } from "crypto";
|
|
3020
|
+
import { realpathSync } from "fs";
|
|
3021
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
3022
|
+
import { join as join16, resolve as resolve3 } from "path";
|
|
3023
|
+
var DEFAULT_DEPS = { afterStage: async () => void 0 };
|
|
3024
|
+
function errorExitCode(error) {
|
|
3025
|
+
switch (error) {
|
|
3026
|
+
case "FILE_NOT_FOUND":
|
|
3027
|
+
return ExitCode.FILE_NOT_FOUND;
|
|
3028
|
+
case "MISSING_CLOSING_DELIMITER":
|
|
3029
|
+
return ExitCode.MISSING_CLOSING_DELIMITER;
|
|
3030
|
+
case "SCHEME_REJECTED":
|
|
3031
|
+
case "NO_TAXONOMY_BLOCK":
|
|
3032
|
+
return ExitCode.SCHEME_REJECTED;
|
|
3033
|
+
case "VAULT_PATH_INVALID":
|
|
3034
|
+
return ExitCode.VAULT_PATH_INVALID;
|
|
3035
|
+
case "SENSITIVE_CONTENT_DETECTED":
|
|
3036
|
+
return ExitCode.SENSITIVE_CONTENT_DETECTED;
|
|
3037
|
+
case "SYNC_LOCK_HELD":
|
|
3038
|
+
return ExitCode.SYNC_LOCK_HELD;
|
|
3039
|
+
case "WRITE_FAILED":
|
|
3040
|
+
return ExitCode.WRITE_FAILED;
|
|
3041
|
+
default:
|
|
3042
|
+
return ExitCode.INVALID_FRONTMATTER;
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
function publicationId(target, content, logNote = "") {
|
|
3046
|
+
return createHash3("sha256").update("skillwiki-page-publish-v1\0").update(target).update("\0").update(content).update("\0").update(logNote).digest("hex");
|
|
3047
|
+
}
|
|
3048
|
+
function prepareFrozenPublication(input, source) {
|
|
3049
|
+
const target = assertTargetInsideVault(input.vault, input.target);
|
|
3050
|
+
if (!target.ok) return target;
|
|
3051
|
+
const page = prepareTypedPage(input.content, input.target);
|
|
3052
|
+
if (!page.ok) return page;
|
|
3053
|
+
if (input.logNote !== void 0 && /[\r\n]/.test(input.logNote)) {
|
|
3054
|
+
return err("SCHEME_REJECTED", { message: "log note must be one line" });
|
|
3055
|
+
}
|
|
3056
|
+
const logNote = input.logNote?.trim() || void 0;
|
|
3057
|
+
if (logNote && Buffer.byteLength(logNote, "utf8") > 500) {
|
|
3058
|
+
return err("SCHEME_REJECTED", { message: "log note must be one line and at most 500 UTF-8 bytes" });
|
|
3059
|
+
}
|
|
3060
|
+
if (logNote && scanSensitiveContent(logNote, { file: "page-publish log note" }).length > 0) {
|
|
3061
|
+
return err("SENSITIVE_CONTENT_DETECTED", { message: "log note contains sensitive authentication material" });
|
|
3062
|
+
}
|
|
3063
|
+
const date = (input.now ?? /* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3064
|
+
const taxonomyComment = taxonomyCommentForPage(input.target, date);
|
|
3065
|
+
if (!taxonomyComment.ok) return taxonomyComment;
|
|
3066
|
+
return ok({
|
|
3067
|
+
page: page.data,
|
|
3068
|
+
source,
|
|
3069
|
+
targetPath: target.data.absolutePath,
|
|
3070
|
+
logNote,
|
|
3071
|
+
operationId: publicationId(input.target, input.content, logNote),
|
|
3072
|
+
date,
|
|
3073
|
+
taxonomyComment: taxonomyComment.data
|
|
3074
|
+
});
|
|
3075
|
+
}
|
|
3076
|
+
function preparePagePublicationFromContent(input) {
|
|
3077
|
+
return prepareFrozenPublication(input, { kind: "content" });
|
|
3078
|
+
}
|
|
3079
|
+
async function preparePagePublication(input) {
|
|
3080
|
+
let content;
|
|
3081
|
+
try {
|
|
3082
|
+
content = await readFile11(input.draftPath, "utf8");
|
|
3083
|
+
} catch (error) {
|
|
3084
|
+
return err("FILE_NOT_FOUND", { path: input.draftPath, message: String(error) });
|
|
3085
|
+
}
|
|
3086
|
+
let draftRealPath;
|
|
3087
|
+
try {
|
|
3088
|
+
draftRealPath = realpathSync(input.draftPath);
|
|
3089
|
+
} catch (error) {
|
|
3090
|
+
return err("VAULT_PATH_INVALID", {
|
|
3091
|
+
path: input.draftPath,
|
|
3092
|
+
message: `draft realpath failed: ${String(error)}`
|
|
3093
|
+
});
|
|
3094
|
+
}
|
|
3095
|
+
const target = assertTargetInsideVault(input.vault, input.target);
|
|
3096
|
+
if (!target.ok) return target;
|
|
3097
|
+
if (resolve3(input.draftPath) === target.data.absolutePath || target.data.existingRealPath !== void 0 && draftRealPath === target.data.existingRealPath) {
|
|
3098
|
+
return err("VAULT_PATH_INVALID", { message: "draft must not alias the final target" });
|
|
3099
|
+
}
|
|
3100
|
+
return prepareFrozenPublication(
|
|
3101
|
+
{
|
|
3102
|
+
vault: input.vault,
|
|
3103
|
+
content,
|
|
3104
|
+
target: input.target,
|
|
3105
|
+
logNote: input.logNote,
|
|
3106
|
+
now: input.now
|
|
3107
|
+
},
|
|
3108
|
+
{ kind: "file", realPath: draftRealPath }
|
|
3109
|
+
);
|
|
3110
|
+
}
|
|
3111
|
+
function emptyLockedState() {
|
|
3112
|
+
return {
|
|
3113
|
+
taxonomyAdded: [],
|
|
3114
|
+
pageChanged: false,
|
|
3115
|
+
indexUpdated: false,
|
|
3116
|
+
published: false,
|
|
3117
|
+
changed: /* @__PURE__ */ new Set()
|
|
3118
|
+
};
|
|
3119
|
+
}
|
|
3120
|
+
function lockedFailure(stage, state, cause, exitCode = ExitCode.WRITE_FAILED) {
|
|
3121
|
+
return { ok: false, exitCode, stage, state, cause };
|
|
3122
|
+
}
|
|
3123
|
+
async function observeStage(deps, stage) {
|
|
3124
|
+
try {
|
|
3125
|
+
await deps.afterStage(stage);
|
|
3126
|
+
return void 0;
|
|
3127
|
+
} catch (error) {
|
|
3128
|
+
return err("WRITE_FAILED", { message: `stage hook failed at ${stage}: ${String(error)}` });
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
async function runLockedPrimaryStages(input, vault, deps) {
|
|
3132
|
+
const state = emptyLockedState();
|
|
3133
|
+
const freshTarget = assertTargetInsideVault(vault, input.page.target);
|
|
3134
|
+
if (!freshTarget.ok) {
|
|
3135
|
+
return lockedFailure("target", state, freshTarget, ExitCode.VAULT_PATH_INVALID);
|
|
3136
|
+
}
|
|
3137
|
+
if (freshTarget.data.absolutePath !== input.targetPath) {
|
|
3138
|
+
return lockedFailure(
|
|
3139
|
+
"target",
|
|
3140
|
+
state,
|
|
3141
|
+
err("VAULT_PATH_INVALID", { message: "target canonical path changed after preparation" }),
|
|
3142
|
+
ExitCode.VAULT_PATH_INVALID
|
|
3143
|
+
);
|
|
3144
|
+
}
|
|
3145
|
+
if (input.source.kind === "file" && freshTarget.data.existingRealPath !== void 0 && freshTarget.data.existingRealPath === input.source.realPath) {
|
|
3146
|
+
return lockedFailure(
|
|
3147
|
+
"target",
|
|
3148
|
+
state,
|
|
3149
|
+
err("VAULT_PATH_INVALID", { message: "draft now aliases the final target" }),
|
|
3150
|
+
ExitCode.VAULT_PATH_INVALID
|
|
3151
|
+
);
|
|
3152
|
+
}
|
|
3153
|
+
const schemaPath = join16(vault, "SCHEMA.md");
|
|
3154
|
+
let schemaText;
|
|
3155
|
+
try {
|
|
3156
|
+
schemaText = await readFile11(schemaPath, "utf8");
|
|
3157
|
+
} catch (error) {
|
|
3158
|
+
return lockedFailure("schema", state, err("WRITE_FAILED", { message: String(error) }));
|
|
3159
|
+
}
|
|
3160
|
+
const reconciled = reconcileTaxonomyDocument(schemaText, {
|
|
3161
|
+
tags: input.page.tags,
|
|
3162
|
+
comment: input.taxonomyComment
|
|
3163
|
+
});
|
|
3164
|
+
if (!reconciled.ok) {
|
|
3165
|
+
return lockedFailure("schema", state, reconciled, errorExitCode(reconciled.error));
|
|
3166
|
+
}
|
|
3167
|
+
state.taxonomyAdded = reconciled.data.added;
|
|
3168
|
+
if (reconciled.data.changed) {
|
|
3169
|
+
const schemaWrite = await atomicWriteText(schemaPath, reconciled.data.text);
|
|
3170
|
+
if (!schemaWrite.ok) return lockedFailure("schema", state, schemaWrite);
|
|
3171
|
+
if (schemaWrite.data.changed) state.changed.add("SCHEMA.md");
|
|
3172
|
+
}
|
|
3173
|
+
const schemaHook = await observeStage(deps, "schema");
|
|
3174
|
+
if (schemaHook) return lockedFailure("schema", state, schemaHook);
|
|
3175
|
+
const pageWrite = await safeWritePage(input.targetPath, input.page.content);
|
|
3176
|
+
if (!pageWrite.ok) return lockedFailure("page", state, pageWrite);
|
|
3177
|
+
state.pageChanged = pageWrite.data.changed;
|
|
3178
|
+
if (state.pageChanged) state.changed.add(input.page.target);
|
|
3179
|
+
state.published = true;
|
|
3180
|
+
const pageHook = await observeStage(deps, "page");
|
|
3181
|
+
if (pageHook) return lockedFailure("page", state, pageHook);
|
|
3182
|
+
let visible;
|
|
3183
|
+
let visibleSchema;
|
|
3184
|
+
try {
|
|
3185
|
+
[visible, visibleSchema] = await Promise.all([
|
|
3186
|
+
readFile11(input.targetPath, "utf8"),
|
|
3187
|
+
readFile11(schemaPath, "utf8")
|
|
3188
|
+
]);
|
|
3189
|
+
} catch (error) {
|
|
3190
|
+
return lockedFailure("verify", state, err("WRITE_FAILED", { message: String(error) }));
|
|
3191
|
+
}
|
|
3192
|
+
const visiblePage = prepareTypedPage(visible, input.page.target);
|
|
3193
|
+
const visibleTaxonomy = extractTaxonomy(visibleSchema);
|
|
3194
|
+
if (!visiblePage.ok || visible !== input.page.content || !visibleTaxonomy.ok || input.page.tags.some((tag) => !visibleTaxonomy.data.includes(tag))) {
|
|
3195
|
+
return lockedFailure(
|
|
3196
|
+
"verify",
|
|
3197
|
+
state,
|
|
3198
|
+
err("WRITE_FAILED", { message: "published bytes or taxonomy verification failed" })
|
|
3199
|
+
);
|
|
3200
|
+
}
|
|
3201
|
+
const verifyHook = await observeStage(deps, "verify");
|
|
3202
|
+
if (verifyHook) return lockedFailure("verify", state, verifyHook);
|
|
3203
|
+
const index = await upsertIndexEntry({
|
|
3204
|
+
vault,
|
|
3205
|
+
target: input.page.target,
|
|
3206
|
+
title: input.page.title,
|
|
3207
|
+
type: input.page.type
|
|
3208
|
+
});
|
|
3209
|
+
if (!index.ok) return lockedFailure("index", state, index);
|
|
3210
|
+
state.indexUpdated = index.data.changed;
|
|
3211
|
+
if (state.indexUpdated) state.changed.add("index.md");
|
|
3212
|
+
const indexHook = await observeStage(deps, "index");
|
|
3213
|
+
if (indexHook) return lockedFailure("index", state, indexHook);
|
|
3214
|
+
return { ok: true, data: state };
|
|
3215
|
+
}
|
|
3216
|
+
function redactDetail(detail) {
|
|
3217
|
+
if (detail === void 0) return void 0;
|
|
3218
|
+
try {
|
|
3219
|
+
const encoded = JSON.stringify(detail);
|
|
3220
|
+
return JSON.parse(redactSensitiveContent(encoded).text);
|
|
3221
|
+
} catch {
|
|
3222
|
+
return { message: "unserializable error detail omitted" };
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
function phaseFailure(stage, input, published, cause, context = {}, exitCode = ExitCode.WRITE_FAILED) {
|
|
3226
|
+
return {
|
|
3227
|
+
exitCode,
|
|
3228
|
+
result: err("WRITE_FAILED", {
|
|
3229
|
+
...context,
|
|
3230
|
+
stage,
|
|
3231
|
+
published,
|
|
3232
|
+
target: input.page.target,
|
|
3233
|
+
operation_id: input.operationId,
|
|
3234
|
+
retry_safe: stage !== "target",
|
|
3235
|
+
cause_error: cause.error,
|
|
3236
|
+
cause_detail: redactDetail(cause.detail)
|
|
3237
|
+
})
|
|
3238
|
+
};
|
|
3239
|
+
}
|
|
3240
|
+
function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, logAppended, filesChanged, dryRun = false) {
|
|
3241
|
+
return {
|
|
3242
|
+
exitCode: ExitCode.OK,
|
|
3243
|
+
result: ok({
|
|
3244
|
+
target: input.page.target,
|
|
3245
|
+
page_type: input.page.type,
|
|
3246
|
+
tags: [...input.page.tags],
|
|
3247
|
+
taxonomy_added: [...taxonomyAdded],
|
|
3248
|
+
page_changed: pageChanged,
|
|
3249
|
+
index_updated: indexUpdated,
|
|
3250
|
+
log_appended: logAppended,
|
|
3251
|
+
operation_id: input.operationId,
|
|
3252
|
+
dry_run: dryRun,
|
|
3253
|
+
files_changed: filesChanged,
|
|
3254
|
+
humanHint: dryRun ? `dry run: would publish ${input.page.target} (${input.operationId.slice(0, 12)})` : `published ${input.page.target} (${input.operationId.slice(0, 12)})`
|
|
3255
|
+
})
|
|
3256
|
+
};
|
|
3257
|
+
}
|
|
3258
|
+
function renderPublicationLog(input, added) {
|
|
3259
|
+
return [
|
|
3260
|
+
`## [${input.date}] page-publish | ${input.page.target}`,
|
|
3261
|
+
"",
|
|
3262
|
+
`- Published: [[${input.page.target.replace(/\.md$/, "")}]]`,
|
|
3263
|
+
`- Taxonomy: ${added.length > 0 ? `added ${added.join(", ")}` : "no additions"}`,
|
|
3264
|
+
...input.logNote ? [`- Note: ${input.logNote}`] : []
|
|
3265
|
+
].join("\n");
|
|
3266
|
+
}
|
|
3267
|
+
async function readPageChanged(targetPath, content) {
|
|
3268
|
+
try {
|
|
3269
|
+
return ok(await readFile11(targetPath, "utf8") !== content);
|
|
3270
|
+
} catch (error) {
|
|
3271
|
+
if (error.code === "ENOENT") return ok(true);
|
|
3272
|
+
return err("WRITE_FAILED", { path: targetPath, message: String(error) });
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
async function previewPreparedPagePublication(input, vault) {
|
|
3276
|
+
const schemaPath = join16(vault, "SCHEMA.md");
|
|
3277
|
+
let schemaText;
|
|
3278
|
+
try {
|
|
3279
|
+
schemaText = await readFile11(schemaPath, "utf8");
|
|
3280
|
+
} catch (error) {
|
|
3281
|
+
const result = err("FILE_NOT_FOUND", { path: schemaPath, message: String(error) });
|
|
3282
|
+
return { exitCode: ExitCode.FILE_NOT_FOUND, result };
|
|
3283
|
+
}
|
|
3284
|
+
const reconciled = reconcileTaxonomyDocument(schemaText, {
|
|
3285
|
+
tags: input.page.tags,
|
|
3286
|
+
comment: input.taxonomyComment
|
|
3287
|
+
});
|
|
3288
|
+
if (!reconciled.ok) return { exitCode: errorExitCode(reconciled.error), result: reconciled };
|
|
3289
|
+
const pageChanged = await readPageChanged(input.targetPath, input.page.content);
|
|
3290
|
+
if (!pageChanged.ok) return { exitCode: errorExitCode(pageChanged.error), result: pageChanged };
|
|
3291
|
+
const indexPath = join16(vault, "index.md");
|
|
3292
|
+
let indexText;
|
|
3293
|
+
try {
|
|
3294
|
+
indexText = await readFile11(indexPath, "utf8");
|
|
3295
|
+
} catch (error) {
|
|
3296
|
+
const result = err("FILE_NOT_FOUND", { path: indexPath, message: String(error) });
|
|
3297
|
+
return { exitCode: ExitCode.FILE_NOT_FOUND, result };
|
|
3298
|
+
}
|
|
3299
|
+
const index = renderIndexUpsert(indexText, {
|
|
3300
|
+
target: input.page.target,
|
|
3301
|
+
title: input.page.title,
|
|
3302
|
+
type: input.page.type
|
|
3303
|
+
});
|
|
3304
|
+
if (!index.ok) return { exitCode: errorExitCode(index.error), result: index };
|
|
3305
|
+
const logPath = join16(vault, "log.md");
|
|
3306
|
+
let logText;
|
|
3307
|
+
try {
|
|
3308
|
+
logText = await readFile11(logPath, "utf8");
|
|
3309
|
+
} catch (error) {
|
|
3310
|
+
const result = err("FILE_NOT_FOUND", { path: logPath, message: String(error) });
|
|
3311
|
+
return { exitCode: ExitCode.FILE_NOT_FOUND, result };
|
|
3312
|
+
}
|
|
3313
|
+
const logAppended = !logText.includes(`<!-- skillwiki-page-publish:${input.operationId} -->`);
|
|
3314
|
+
const filesChanged = [
|
|
3315
|
+
...reconciled.data.changed ? ["SCHEMA.md"] : [],
|
|
3316
|
+
...pageChanged.data ? [input.page.target] : [],
|
|
3317
|
+
...index.data.changed ? ["index.md"] : [],
|
|
3318
|
+
...logAppended ? ["log.md"] : []
|
|
3319
|
+
];
|
|
3320
|
+
return successReceipt(
|
|
3321
|
+
input,
|
|
3322
|
+
reconciled.data.added,
|
|
3323
|
+
pageChanged.data,
|
|
3324
|
+
index.data.changed,
|
|
3325
|
+
logAppended,
|
|
3326
|
+
filesChanged,
|
|
3327
|
+
true
|
|
3328
|
+
);
|
|
3329
|
+
}
|
|
3330
|
+
async function publishPreparedPage(input, vault, deps = DEFAULT_DEPS) {
|
|
3331
|
+
let lock;
|
|
3332
|
+
try {
|
|
3333
|
+
lock = acquireOwnedSyncLock(vault, {
|
|
3334
|
+
summary: `page publish ${input.page.target}`,
|
|
3335
|
+
ttlMinutes: 1
|
|
3336
|
+
});
|
|
3337
|
+
} catch (error) {
|
|
3338
|
+
return {
|
|
3339
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
3340
|
+
result: err("WRITE_FAILED", { stage: "lock", message: String(error) })
|
|
3341
|
+
};
|
|
3342
|
+
}
|
|
3343
|
+
if (!lock.ok) return { exitCode: errorExitCode(lock.error), result: lock };
|
|
3344
|
+
let primary;
|
|
3345
|
+
let released;
|
|
3346
|
+
try {
|
|
3347
|
+
primary = await runLockedPrimaryStages(input, vault, deps);
|
|
3348
|
+
} catch (error) {
|
|
3349
|
+
primary = lockedFailure(
|
|
3350
|
+
"schema",
|
|
3351
|
+
emptyLockedState(),
|
|
3352
|
+
err("WRITE_FAILED", { message: `unexpected primary-stage failure: ${String(error)}` })
|
|
3353
|
+
);
|
|
3354
|
+
} finally {
|
|
3355
|
+
released = releaseOwnedSyncLock(lock.data);
|
|
3356
|
+
}
|
|
3357
|
+
const primaryState = primary?.ok ? primary.data : primary?.state;
|
|
3358
|
+
if (released === void 0 || !released.ok || !released.data.released) {
|
|
3359
|
+
return phaseFailure(
|
|
3360
|
+
"unlock",
|
|
3361
|
+
input,
|
|
3362
|
+
primaryState?.published ?? false,
|
|
3363
|
+
released && !released.ok ? released : err("WRITE_FAILED", { message: "lock release did not run" }),
|
|
3364
|
+
{
|
|
3365
|
+
primary_stage: primary && !primary.ok ? primary.stage : "complete",
|
|
3366
|
+
primary_error: primary && !primary.ok ? primary.cause.error : void 0
|
|
3367
|
+
}
|
|
3368
|
+
);
|
|
3369
|
+
}
|
|
3370
|
+
const unlockHook = await observeStage(deps, "unlock");
|
|
3371
|
+
if (unlockHook) return phaseFailure("unlock", input, primaryState?.published ?? false, unlockHook);
|
|
3372
|
+
if (primary === void 0) {
|
|
3373
|
+
return phaseFailure(
|
|
3374
|
+
"schema",
|
|
3375
|
+
input,
|
|
3376
|
+
false,
|
|
3377
|
+
err("WRITE_FAILED", { message: "locked publication produced no result" })
|
|
3378
|
+
);
|
|
3379
|
+
}
|
|
3380
|
+
if (!primary.ok) {
|
|
3381
|
+
return phaseFailure(
|
|
3382
|
+
primary.stage,
|
|
3383
|
+
input,
|
|
3384
|
+
primary.state.published,
|
|
3385
|
+
primary.cause,
|
|
3386
|
+
void 0,
|
|
3387
|
+
primary.exitCode
|
|
3388
|
+
);
|
|
3389
|
+
}
|
|
3390
|
+
const state = primary.data;
|
|
3391
|
+
const log = await runLogAppend({
|
|
3392
|
+
vault,
|
|
3393
|
+
content: renderPublicationLog(input, state.taxonomyAdded),
|
|
3394
|
+
operationId: input.operationId,
|
|
3395
|
+
strictLock: true,
|
|
3396
|
+
recordLastOp: false
|
|
3397
|
+
});
|
|
3398
|
+
if (!log.result.ok) return phaseFailure("log", input, true, log.result);
|
|
3399
|
+
if (log.exitCode !== ExitCode.OK) {
|
|
3400
|
+
return phaseFailure(
|
|
3401
|
+
"log",
|
|
3402
|
+
input,
|
|
3403
|
+
true,
|
|
3404
|
+
err("WRITE_FAILED", { message: "log append returned inconsistent success state" })
|
|
3405
|
+
);
|
|
3406
|
+
}
|
|
3407
|
+
if (log.result.data.appended) state.changed.add("log.md");
|
|
3408
|
+
const logHook = await observeStage(deps, "log");
|
|
3409
|
+
if (logHook) return phaseFailure("log", input, true, logHook);
|
|
3410
|
+
return successReceipt(
|
|
3411
|
+
input,
|
|
3412
|
+
state.taxonomyAdded,
|
|
3413
|
+
state.pageChanged,
|
|
3414
|
+
state.indexUpdated,
|
|
3415
|
+
log.result.data.appended,
|
|
3416
|
+
[...state.changed]
|
|
3417
|
+
);
|
|
3418
|
+
}
|
|
3419
|
+
async function runPagePublish(input, deps = DEFAULT_DEPS) {
|
|
3420
|
+
const prepared = await preparePagePublication(input);
|
|
3421
|
+
if (!prepared.ok) return { exitCode: errorExitCode(prepared.error), result: prepared };
|
|
3422
|
+
if (!input.write) return previewPreparedPagePublication(prepared.data, input.vault);
|
|
3423
|
+
return publishPreparedPage(prepared.data, input.vault, deps);
|
|
3424
|
+
}
|
|
3425
|
+
|
|
3426
|
+
// src/commands/ingest.ts
|
|
2954
3427
|
var ALLOWED_TYPES = /* @__PURE__ */ new Set(["entity", "concept", "comparison", "query"]);
|
|
2955
3428
|
var TYPE_DIR = {
|
|
2956
3429
|
entity: "entities",
|
|
@@ -2991,19 +3464,6 @@ function buildTypedContent(title, ingested, type, tags, rawRelPath, provenance)
|
|
|
2991
3464
|
const aliases = [];
|
|
2992
3465
|
const sourcesYaml = ` - ${rawRelPath}`;
|
|
2993
3466
|
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
3467
|
const fmLines = ["---"];
|
|
3008
3468
|
fmLines.push(`title: "${title}"`);
|
|
3009
3469
|
if (aliases.length > 0) {
|
|
@@ -3039,6 +3499,78 @@ function buildTypedContent(title, ingested, type, tags, rawRelPath, provenance)
|
|
|
3039
3499
|
].join("\n");
|
|
3040
3500
|
return fmLines.join("\n") + body;
|
|
3041
3501
|
}
|
|
3502
|
+
async function resolveRawCapture(input) {
|
|
3503
|
+
try {
|
|
3504
|
+
const existing = await readFile12(input.path, "utf8");
|
|
3505
|
+
const frontmatter = extractFrontmatter(existing);
|
|
3506
|
+
if (!frontmatter.ok) {
|
|
3507
|
+
return err("INGEST_VALIDATION_FAILED", {
|
|
3508
|
+
path: input.path,
|
|
3509
|
+
message: "existing immutable raw source has invalid frontmatter",
|
|
3510
|
+
source_error: frontmatter.error
|
|
3511
|
+
});
|
|
3512
|
+
}
|
|
3513
|
+
const parsed = RawSourceSchema.safeParse(frontmatter.data);
|
|
3514
|
+
if (!parsed.success || parsed.data.sha256 !== input.sha256 || (parsed.data.source_url ?? null) !== input.sourceUrl || existing !== buildRawContent(
|
|
3515
|
+
input.sourceUrl,
|
|
3516
|
+
String(parsed.data.ingested),
|
|
3517
|
+
input.sha256,
|
|
3518
|
+
input.sourceContent
|
|
3519
|
+
)) {
|
|
3520
|
+
return err("INGEST_VALIDATION_FAILED", {
|
|
3521
|
+
path: input.path,
|
|
3522
|
+
message: "existing immutable raw source differs from the fetched source"
|
|
3523
|
+
});
|
|
3524
|
+
}
|
|
3525
|
+
return ok({
|
|
3526
|
+
content: existing,
|
|
3527
|
+
ingested: String(parsed.data.ingested),
|
|
3528
|
+
shouldWrite: false
|
|
3529
|
+
});
|
|
3530
|
+
} catch (error) {
|
|
3531
|
+
if (error.code !== "ENOENT") {
|
|
3532
|
+
return err("WRITE_FAILED", { path: input.path, message: String(error) });
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
3535
|
+
return ok({
|
|
3536
|
+
content: buildRawContent(input.sourceUrl, input.today, input.sha256, input.sourceContent),
|
|
3537
|
+
ingested: input.today,
|
|
3538
|
+
shouldWrite: true
|
|
3539
|
+
});
|
|
3540
|
+
}
|
|
3541
|
+
async function writeResolvedRaw(input) {
|
|
3542
|
+
if (!input.capture.shouldWrite) return ok({ changed: false, capture: input.capture });
|
|
3543
|
+
const lock = await acquireRawCaptureLock(input.path);
|
|
3544
|
+
if (!lock.ok) return lock;
|
|
3545
|
+
try {
|
|
3546
|
+
const resolved = await resolveRawCapture(input);
|
|
3547
|
+
if (!resolved.ok) return resolved;
|
|
3548
|
+
if (!resolved.data.shouldWrite) return ok({ changed: false, capture: resolved.data });
|
|
3549
|
+
const written = await atomicWriteText(input.path, resolved.data.content);
|
|
3550
|
+
return written.ok ? ok({ changed: written.data.changed, capture: resolved.data }) : written;
|
|
3551
|
+
} finally {
|
|
3552
|
+
try {
|
|
3553
|
+
await unlink4(lock.data);
|
|
3554
|
+
} catch {
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
async function acquireRawCaptureLock(path) {
|
|
3559
|
+
const lockPath = `${path}.ingest.lock`;
|
|
3560
|
+
for (let attempt = 0; attempt < 200; attempt++) {
|
|
3561
|
+
try {
|
|
3562
|
+
const handle = await open(lockPath, "wx");
|
|
3563
|
+
await handle.close();
|
|
3564
|
+
return ok(lockPath);
|
|
3565
|
+
} catch (error) {
|
|
3566
|
+
if (error.code !== "EEXIST") {
|
|
3567
|
+
return err("WRITE_FAILED", { path: lockPath, phase: "raw-lock", message: String(error) });
|
|
3568
|
+
}
|
|
3569
|
+
await new Promise((resolve4) => setTimeout(resolve4, 10));
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
return err("WRITE_FAILED", { path: lockPath, phase: "raw-lock", message: "raw capture lock held" });
|
|
3573
|
+
}
|
|
3042
3574
|
async function runIngest(input) {
|
|
3043
3575
|
if (!input.source || input.source.trim().length === 0) {
|
|
3044
3576
|
return {
|
|
@@ -3108,7 +3640,7 @@ async function runIngest(input) {
|
|
|
3108
3640
|
sourceContent = fetchResult.data.body;
|
|
3109
3641
|
} else {
|
|
3110
3642
|
try {
|
|
3111
|
-
sourceContent = await
|
|
3643
|
+
sourceContent = await readFile12(input.source, "utf8");
|
|
3112
3644
|
} catch {
|
|
3113
3645
|
return {
|
|
3114
3646
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
@@ -3126,15 +3658,14 @@ async function runIngest(input) {
|
|
|
3126
3658
|
})
|
|
3127
3659
|
};
|
|
3128
3660
|
}
|
|
3129
|
-
const sha256 =
|
|
3661
|
+
const sha256 = createHash4("sha256").update(Buffer.from(sourceContent, "utf8")).digest("hex");
|
|
3130
3662
|
const today = todayIso();
|
|
3131
3663
|
const slug = slugify2(input.title);
|
|
3132
3664
|
const tags = input.tags && input.tags.length > 0 ? input.tags : [];
|
|
3133
3665
|
const rawRelPath = `raw/articles/${slug}.md`;
|
|
3134
3666
|
const typedDir = TYPE_DIR[input.type] ?? `${input.type}s`;
|
|
3135
3667
|
const typedRelPath = `${typedDir}/${slug}.md`;
|
|
3136
|
-
const rawAbsPath =
|
|
3137
|
-
const typedAbsPath = join16(input.vault, typedRelPath);
|
|
3668
|
+
const rawAbsPath = join17(input.vault, rawRelPath);
|
|
3138
3669
|
const identity = assessSourceIdentity({
|
|
3139
3670
|
rawPath: rawRelPath,
|
|
3140
3671
|
sourceUrl: sourceUrl ?? void 0,
|
|
@@ -3154,16 +3685,62 @@ async function runIngest(input) {
|
|
|
3154
3685
|
})
|
|
3155
3686
|
};
|
|
3156
3687
|
}
|
|
3157
|
-
const
|
|
3158
|
-
|
|
3688
|
+
const resolvedRaw = await resolveRawCapture({
|
|
3689
|
+
path: rawAbsPath,
|
|
3690
|
+
sourceUrl,
|
|
3691
|
+
sourceContent,
|
|
3692
|
+
sha256,
|
|
3693
|
+
today
|
|
3694
|
+
});
|
|
3695
|
+
if (!resolvedRaw.ok) {
|
|
3696
|
+
return {
|
|
3697
|
+
exitCode: resolvedRaw.error === "INGEST_VALIDATION_FAILED" ? ExitCode.INGEST_VALIDATION_FAILED : ExitCode.WRITE_FAILED,
|
|
3698
|
+
result: resolvedRaw
|
|
3699
|
+
};
|
|
3700
|
+
}
|
|
3701
|
+
let publicationDate = resolvedRaw.data.ingested;
|
|
3702
|
+
let typedContent = buildTypedContent(
|
|
3159
3703
|
input.title,
|
|
3160
|
-
|
|
3704
|
+
publicationDate,
|
|
3161
3705
|
input.type,
|
|
3162
3706
|
tags,
|
|
3163
3707
|
rawRelPath,
|
|
3164
3708
|
input.provenance
|
|
3165
3709
|
);
|
|
3710
|
+
if (!input.dryRun) {
|
|
3711
|
+
try {
|
|
3712
|
+
await mkdir9(join17(input.vault, typedDir), { recursive: true });
|
|
3713
|
+
} catch (error) {
|
|
3714
|
+
return {
|
|
3715
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
3716
|
+
result: err("WRITE_FAILED", { path: join17(input.vault, typedDir), message: String(error) })
|
|
3717
|
+
};
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
let publication = preparePagePublicationFromContent({
|
|
3721
|
+
vault: input.vault,
|
|
3722
|
+
content: typedContent,
|
|
3723
|
+
target: typedRelPath,
|
|
3724
|
+
logNote: `ingested from ${rawRelPath}`,
|
|
3725
|
+
now: /* @__PURE__ */ new Date(`${publicationDate}T00:00:00Z`)
|
|
3726
|
+
});
|
|
3727
|
+
if (!publication.ok) {
|
|
3728
|
+
return {
|
|
3729
|
+
exitCode: ExitCode.INGEST_VALIDATION_FAILED,
|
|
3730
|
+
result: publication
|
|
3731
|
+
};
|
|
3732
|
+
}
|
|
3166
3733
|
if (input.dryRun) {
|
|
3734
|
+
const preview = await previewPreparedPagePublication(publication.data, input.vault);
|
|
3735
|
+
if (!preview.result.ok) {
|
|
3736
|
+
return { exitCode: preview.exitCode, result: preview.result };
|
|
3737
|
+
}
|
|
3738
|
+
if (preview.exitCode !== ExitCode.OK) {
|
|
3739
|
+
return {
|
|
3740
|
+
exitCode: preview.exitCode,
|
|
3741
|
+
result: err("WRITE_FAILED", { message: "publication preview returned inconsistent success state" })
|
|
3742
|
+
};
|
|
3743
|
+
}
|
|
3167
3744
|
return {
|
|
3168
3745
|
exitCode: ExitCode.OK,
|
|
3169
3746
|
result: ok({
|
|
@@ -3172,78 +3749,84 @@ async function runIngest(input) {
|
|
|
3172
3749
|
sha256,
|
|
3173
3750
|
dry_run: true,
|
|
3174
3751
|
humanHint: [
|
|
3175
|
-
|
|
3752
|
+
"DRY RUN \u2014 would create:",
|
|
3176
3753
|
` ${rawRelPath} (sha256: ${sha256.slice(0, 12)}...)`,
|
|
3177
3754
|
` ${typedRelPath}`,
|
|
3178
3755
|
` type: ${input.type}, tags: [${tags.join(", ")}]`,
|
|
3179
3756
|
input.provenance ? ` provenance: ${input.provenance}` : ""
|
|
3180
|
-
].filter(Boolean).join("\n")
|
|
3757
|
+
].filter(Boolean).join("\n"),
|
|
3758
|
+
publication: preview.result.data
|
|
3181
3759
|
})
|
|
3182
3760
|
};
|
|
3183
3761
|
}
|
|
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) {
|
|
3762
|
+
try {
|
|
3763
|
+
await mkdir9(join17(input.vault, "raw", "articles"), { recursive: true });
|
|
3764
|
+
} catch (error) {
|
|
3197
3765
|
return {
|
|
3198
|
-
exitCode: ExitCode.
|
|
3199
|
-
result: err("
|
|
3200
|
-
message: "generated typed-knowledge page could not be detected as a valid schema"
|
|
3201
|
-
})
|
|
3766
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
3767
|
+
result: err("WRITE_FAILED", { path: join17(input.vault, "raw", "articles"), message: String(error) })
|
|
3202
3768
|
};
|
|
3203
3769
|
}
|
|
3204
|
-
const
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3770
|
+
const rawWrite = await writeResolvedRaw({
|
|
3771
|
+
path: rawAbsPath,
|
|
3772
|
+
sourceUrl,
|
|
3773
|
+
sourceContent,
|
|
3774
|
+
sha256,
|
|
3775
|
+
today,
|
|
3776
|
+
capture: resolvedRaw.data
|
|
3777
|
+
});
|
|
3778
|
+
if (!rawWrite.ok) {
|
|
3210
3779
|
return {
|
|
3211
|
-
exitCode: ExitCode.INGEST_VALIDATION_FAILED,
|
|
3212
|
-
result:
|
|
3213
|
-
message: "generated typed-knowledge page failed schema validation",
|
|
3214
|
-
errors
|
|
3215
|
-
})
|
|
3780
|
+
exitCode: rawWrite.error === "INGEST_VALIDATION_FAILED" ? ExitCode.INGEST_VALIDATION_FAILED : ExitCode.WRITE_FAILED,
|
|
3781
|
+
result: rawWrite
|
|
3216
3782
|
};
|
|
3217
3783
|
}
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3784
|
+
if (rawWrite.data.capture.ingested !== publicationDate) {
|
|
3785
|
+
publicationDate = rawWrite.data.capture.ingested;
|
|
3786
|
+
typedContent = buildTypedContent(
|
|
3787
|
+
input.title,
|
|
3788
|
+
publicationDate,
|
|
3789
|
+
input.type,
|
|
3790
|
+
tags,
|
|
3791
|
+
rawRelPath,
|
|
3792
|
+
input.provenance
|
|
3793
|
+
);
|
|
3794
|
+
publication = preparePagePublicationFromContent({
|
|
3795
|
+
vault: input.vault,
|
|
3796
|
+
content: typedContent,
|
|
3797
|
+
target: typedRelPath,
|
|
3798
|
+
logNote: `ingested from ${rawRelPath}`,
|
|
3799
|
+
now: /* @__PURE__ */ new Date(`${publicationDate}T00:00:00Z`)
|
|
3800
|
+
});
|
|
3801
|
+
if (!publication.ok) {
|
|
3802
|
+
return {
|
|
3803
|
+
exitCode: ExitCode.INGEST_VALIDATION_FAILED,
|
|
3804
|
+
result: publication
|
|
3805
|
+
};
|
|
3806
|
+
}
|
|
3226
3807
|
}
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
}
|
|
3808
|
+
const published = await publishPreparedPage(publication.data, input.vault);
|
|
3809
|
+
if (!published.result.ok) {
|
|
3810
|
+
return { exitCode: published.exitCode, result: published.result };
|
|
3811
|
+
}
|
|
3812
|
+
if (published.exitCode !== ExitCode.OK) {
|
|
3231
3813
|
return {
|
|
3232
|
-
exitCode:
|
|
3233
|
-
result: err("WRITE_FAILED", {
|
|
3814
|
+
exitCode: published.exitCode,
|
|
3815
|
+
result: err("WRITE_FAILED", { message: "publisher returned inconsistent success state" })
|
|
3234
3816
|
};
|
|
3235
3817
|
}
|
|
3236
|
-
const
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3818
|
+
const changedFiles = [
|
|
3819
|
+
...rawWrite.data.changed ? [rawRelPath] : [],
|
|
3820
|
+
...published.result.data.files_changed
|
|
3821
|
+
];
|
|
3822
|
+
if (changedFiles.length > 0) {
|
|
3823
|
+
appendLastOp(input.vault, {
|
|
3824
|
+
operation: "ingest",
|
|
3825
|
+
summary: `added ${slug}`,
|
|
3826
|
+
files: [...new Set(changedFiles)],
|
|
3827
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3828
|
+
});
|
|
3829
|
+
}
|
|
3247
3830
|
return {
|
|
3248
3831
|
exitCode: ExitCode.OK,
|
|
3249
3832
|
result: ok({
|
|
@@ -3251,7 +3834,12 @@ async function runIngest(input) {
|
|
|
3251
3834
|
typed_path: typedRelPath,
|
|
3252
3835
|
sha256,
|
|
3253
3836
|
dry_run: false,
|
|
3254
|
-
humanHint
|
|
3837
|
+
humanHint: [
|
|
3838
|
+
"created:",
|
|
3839
|
+
` ${rawRelPath} (sha256: ${sha256.slice(0, 12)}...)`,
|
|
3840
|
+
` ${typedRelPath}`
|
|
3841
|
+
].join("\n"),
|
|
3842
|
+
publication: published.result.data
|
|
3255
3843
|
})
|
|
3256
3844
|
};
|
|
3257
3845
|
}
|
|
@@ -3403,138 +3991,216 @@ ${body}`;
|
|
|
3403
3991
|
};
|
|
3404
3992
|
}
|
|
3405
3993
|
|
|
3406
|
-
// src/commands/
|
|
3407
|
-
import {
|
|
3408
|
-
import { join as join18 } from "path";
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3994
|
+
// src/commands/tag-reconcile.ts
|
|
3995
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
3996
|
+
import { join as join18, posix } from "path";
|
|
3997
|
+
var TYPED_TARGET_RE = /^(entities|concepts|comparisons|queries|meta)\/[a-z0-9][a-z0-9./_-]*\.md$/;
|
|
3998
|
+
function errorExitCode2(error) {
|
|
3999
|
+
switch (error) {
|
|
4000
|
+
case "FILE_NOT_FOUND":
|
|
4001
|
+
return ExitCode.FILE_NOT_FOUND;
|
|
4002
|
+
case "MISSING_CLOSING_DELIMITER":
|
|
4003
|
+
return ExitCode.MISSING_CLOSING_DELIMITER;
|
|
4004
|
+
case "SCHEME_REJECTED":
|
|
4005
|
+
return ExitCode.SCHEME_REJECTED;
|
|
4006
|
+
case "VAULT_PATH_INVALID":
|
|
4007
|
+
return ExitCode.VAULT_PATH_INVALID;
|
|
4008
|
+
case "WRITE_FAILED":
|
|
4009
|
+
return ExitCode.WRITE_FAILED;
|
|
4010
|
+
case "SYNC_LOCK_HELD":
|
|
4011
|
+
return ExitCode.SYNC_LOCK_HELD;
|
|
4012
|
+
default:
|
|
4013
|
+
return ExitCode.INVALID_FRONTMATTER;
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
function validatePageIdentity(page) {
|
|
4017
|
+
const segments = page.split("/");
|
|
4018
|
+
if (page.length === 0 || posix.isAbsolute(page) || page.includes("\\") || posix.normalize(page) !== page || segments.some((segment) => segment === "" || segment === "." || segment === "..") || !TYPED_TARGET_RE.test(page)) {
|
|
4019
|
+
return err("VAULT_PATH_INVALID", {
|
|
4020
|
+
page,
|
|
4021
|
+
message: "page must be a normalized vault-relative typed Markdown path"
|
|
4022
|
+
});
|
|
3418
4023
|
}
|
|
4024
|
+
return ok(page);
|
|
3419
4025
|
}
|
|
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);
|
|
4026
|
+
function asTagArray(frontmatter, path) {
|
|
4027
|
+
const tags = frontmatter.tags;
|
|
4028
|
+
if (!Array.isArray(tags) || !tags.every((tag) => typeof tag === "string")) {
|
|
4029
|
+
return err("INVALID_FRONTMATTER", {
|
|
4030
|
+
path,
|
|
4031
|
+
message: "frontmatter tags must be an array of strings"
|
|
4032
|
+
});
|
|
4033
|
+
}
|
|
4034
|
+
return ok(tags);
|
|
3442
4035
|
}
|
|
3443
|
-
function
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
4036
|
+
async function readTagsFromFile(path) {
|
|
4037
|
+
let text;
|
|
4038
|
+
try {
|
|
4039
|
+
text = await readFile13(path, "utf8");
|
|
4040
|
+
} catch (error) {
|
|
4041
|
+
if (error.code === "ENOENT") {
|
|
4042
|
+
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path }) };
|
|
4043
|
+
}
|
|
4044
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { path, message: String(error) }) };
|
|
4045
|
+
}
|
|
4046
|
+
const frontmatter = extractFrontmatter(text);
|
|
4047
|
+
if (!frontmatter.ok) return { exitCode: errorExitCode2(frontmatter.error), result: frontmatter };
|
|
4048
|
+
const tags = asTagArray(frontmatter.data, path);
|
|
4049
|
+
if (!tags.ok) return { exitCode: ExitCode.INVALID_FRONTMATTER, result: tags };
|
|
4050
|
+
return { exitCode: ExitCode.OK, result: tags };
|
|
3447
4051
|
}
|
|
3448
|
-
function
|
|
3449
|
-
|
|
4052
|
+
async function resolveRequestedTags(input, page) {
|
|
4053
|
+
const explicit = input.tags ?? [];
|
|
4054
|
+
if (!Array.isArray(explicit) || !explicit.every((tag) => typeof tag === "string")) {
|
|
4055
|
+
return {
|
|
4056
|
+
exitCode: ExitCode.INVALID_FRONTMATTER,
|
|
4057
|
+
result: err("INVALID_FRONTMATTER", { message: "explicit tags must be an array of strings" })
|
|
4058
|
+
};
|
|
4059
|
+
}
|
|
4060
|
+
const source = input.from ?? (explicit.length === 0 ? join18(input.vault, page) : void 0);
|
|
4061
|
+
if (!source) return { exitCode: ExitCode.OK, result: ok({ tags: [...new Set(explicit)].sort() }) };
|
|
4062
|
+
const sourced = await readTagsFromFile(source);
|
|
4063
|
+
if (!sourced.result.ok) return { exitCode: sourced.exitCode, result: sourced.result };
|
|
4064
|
+
return {
|
|
4065
|
+
exitCode: ExitCode.OK,
|
|
4066
|
+
result: ok({ tags: [.../* @__PURE__ */ new Set([...explicit, ...sourced.result.data])].sort() })
|
|
4067
|
+
};
|
|
3450
4068
|
}
|
|
3451
|
-
function
|
|
3452
|
-
const path = lockPath(vault);
|
|
3453
|
-
if (!existsSync6(path)) return null;
|
|
4069
|
+
async function readSchema(schemaPath) {
|
|
3454
4070
|
try {
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
4071
|
+
return { exitCode: ExitCode.OK, result: ok(await readFile13(schemaPath, "utf8")) };
|
|
4072
|
+
} catch (error) {
|
|
4073
|
+
if (error.code === "ENOENT") {
|
|
4074
|
+
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: schemaPath }) };
|
|
4075
|
+
}
|
|
4076
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { path: schemaPath, message: String(error) }) };
|
|
3459
4077
|
}
|
|
3460
4078
|
}
|
|
3461
|
-
function
|
|
3462
|
-
|
|
3463
|
-
const
|
|
3464
|
-
|
|
4079
|
+
function previewResult(page, tags, reconciled, dryRun, filesChanged) {
|
|
4080
|
+
if (!reconciled.ok) return { exitCode: errorExitCode2(reconciled.error), result: reconciled };
|
|
4081
|
+
const missingTags = reconciled.data.missing;
|
|
4082
|
+
const addedTags = dryRun ? [] : reconciled.data.added;
|
|
4083
|
+
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}`;
|
|
4084
|
+
return {
|
|
4085
|
+
exitCode: ExitCode.OK,
|
|
4086
|
+
result: ok({
|
|
4087
|
+
page,
|
|
4088
|
+
requested_tags: tags,
|
|
4089
|
+
missing_tags: missingTags,
|
|
4090
|
+
added_tags: addedTags,
|
|
4091
|
+
changed: reconciled.data.changed,
|
|
4092
|
+
dry_run: dryRun,
|
|
4093
|
+
files_changed: filesChanged,
|
|
4094
|
+
humanHint
|
|
4095
|
+
})
|
|
4096
|
+
};
|
|
3465
4097
|
}
|
|
3466
|
-
function
|
|
3467
|
-
const
|
|
3468
|
-
const
|
|
3469
|
-
if (!
|
|
3470
|
-
|
|
4098
|
+
async function reconcileTagsWhileLocked(input, page, tags, comment) {
|
|
4099
|
+
const schemaPath = join18(input.vault, "SCHEMA.md");
|
|
4100
|
+
const current = await readSchema(schemaPath);
|
|
4101
|
+
if (!current.result.ok) return { exitCode: current.exitCode, result: current.result };
|
|
4102
|
+
const next = reconcileTaxonomyDocument(current.result.data, { tags, comment });
|
|
4103
|
+
if (!next.ok) return { exitCode: errorExitCode2(next.error), result: next };
|
|
4104
|
+
if (next.data.changed) {
|
|
4105
|
+
const written = await atomicWriteText(schemaPath, next.data.text);
|
|
4106
|
+
if (!written.ok) return { exitCode: ExitCode.WRITE_FAILED, result: written };
|
|
4107
|
+
}
|
|
4108
|
+
let verifiedText;
|
|
4109
|
+
try {
|
|
4110
|
+
verifiedText = await readFile13(schemaPath, "utf8");
|
|
4111
|
+
} catch (error) {
|
|
4112
|
+
return {
|
|
4113
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
4114
|
+
result: err("WRITE_FAILED", { stage: "verify-taxonomy", page, message: String(error) })
|
|
4115
|
+
};
|
|
3471
4116
|
}
|
|
3472
|
-
const
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
4117
|
+
const verified = extractTaxonomy(verifiedText);
|
|
4118
|
+
if (!verified.ok || tags.some((tag) => !verified.data.includes(tag))) {
|
|
4119
|
+
return {
|
|
4120
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
4121
|
+
result: err("WRITE_FAILED", { stage: "verify-taxonomy", page })
|
|
4122
|
+
};
|
|
4123
|
+
}
|
|
4124
|
+
return previewResult(page, tags, next, false, next.data.changed ? ["SCHEMA.md"] : []);
|
|
4125
|
+
}
|
|
4126
|
+
async function runTagReconcile(input) {
|
|
4127
|
+
const page = validatePageIdentity(input.page);
|
|
4128
|
+
if (!page.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: page };
|
|
4129
|
+
const resolvedTags = await resolveRequestedTags(input, page.data);
|
|
4130
|
+
if (!resolvedTags.result.ok) return { exitCode: resolvedTags.exitCode, result: resolvedTags.result };
|
|
4131
|
+
const tags = resolvedTags.result.data.tags;
|
|
4132
|
+
const date = (input.now ?? /* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4133
|
+
const comment = taxonomyCommentForPage(page.data, date, input.reason);
|
|
4134
|
+
if (!comment.ok) return { exitCode: ExitCode.SCHEME_REJECTED, result: comment };
|
|
4135
|
+
if (!input.write) {
|
|
4136
|
+
const schema = await readSchema(join18(input.vault, "SCHEMA.md"));
|
|
4137
|
+
if (!schema.result.ok) return { exitCode: schema.exitCode, result: schema.result };
|
|
4138
|
+
const preview = reconcileTaxonomyDocument(schema.result.data, { tags, comment: comment.data });
|
|
4139
|
+
return previewResult(page.data, tags, preview, true, []);
|
|
4140
|
+
}
|
|
4141
|
+
let lock;
|
|
3487
4142
|
try {
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
}
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
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
|
-
}
|
|
4143
|
+
lock = acquireOwnedSyncLock(input.vault, {
|
|
4144
|
+
summary: `tag reconcile ${page.data}`,
|
|
4145
|
+
ttlMinutes: 1
|
|
4146
|
+
});
|
|
4147
|
+
} catch (error) {
|
|
4148
|
+
return {
|
|
4149
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
4150
|
+
result: err("WRITE_FAILED", { stage: "lock", page: page.data, message: String(error) })
|
|
4151
|
+
};
|
|
4152
|
+
}
|
|
4153
|
+
if (!lock.ok) return { exitCode: errorExitCode2(lock.error), result: lock };
|
|
4154
|
+
let outcome;
|
|
4155
|
+
let released;
|
|
4156
|
+
try {
|
|
4157
|
+
outcome = await reconcileTagsWhileLocked(input, page.data, tags, comment.data);
|
|
4158
|
+
} catch (error) {
|
|
4159
|
+
outcome = {
|
|
4160
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
4161
|
+
result: err("WRITE_FAILED", { stage: "reconcile", page: page.data, message: String(error) })
|
|
4162
|
+
};
|
|
4163
|
+
} finally {
|
|
4164
|
+
released = releaseOwnedSyncLock(lock.data);
|
|
3527
4165
|
}
|
|
3528
|
-
if (
|
|
3529
|
-
return {
|
|
4166
|
+
if (released === void 0 || !released.ok) {
|
|
4167
|
+
return {
|
|
4168
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
4169
|
+
result: err("WRITE_FAILED", {
|
|
4170
|
+
stage: "unlock",
|
|
4171
|
+
page: page.data,
|
|
4172
|
+
primary_error: outcome && !outcome.result.ok ? outcome.result.error : void 0,
|
|
4173
|
+
release_error: released && !released.ok ? released.detail : "release did not run"
|
|
4174
|
+
})
|
|
4175
|
+
};
|
|
3530
4176
|
}
|
|
4177
|
+
return outcome ?? {
|
|
4178
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
4179
|
+
result: err("WRITE_FAILED", {
|
|
4180
|
+
stage: "reconcile",
|
|
4181
|
+
page: page.data,
|
|
4182
|
+
message: "locked reconciliation produced no result"
|
|
4183
|
+
})
|
|
4184
|
+
};
|
|
4185
|
+
}
|
|
4186
|
+
|
|
4187
|
+
// src/commands/sync.ts
|
|
4188
|
+
import { existsSync as existsSync5 } from "fs";
|
|
4189
|
+
import { join as join19 } from "path";
|
|
4190
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
4191
|
+
|
|
4192
|
+
// src/utils/git.ts
|
|
4193
|
+
import { execFileSync } from "child_process";
|
|
4194
|
+
function git(cwd, args) {
|
|
3531
4195
|
try {
|
|
3532
|
-
|
|
3533
|
-
return { released: true };
|
|
4196
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
3534
4197
|
} catch {
|
|
3535
|
-
return
|
|
4198
|
+
return "";
|
|
3536
4199
|
}
|
|
3537
4200
|
}
|
|
4201
|
+
function gitStrict(cwd, args) {
|
|
4202
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
4203
|
+
}
|
|
3538
4204
|
|
|
3539
4205
|
// src/utils/vault-git-pathspec.ts
|
|
3540
4206
|
var VAULT_GENERATED_COMMIT_PATHS = [
|
|
@@ -3587,7 +4253,7 @@ function refHasPath(vault, ref, path) {
|
|
|
3587
4253
|
function runSyncStatus(input) {
|
|
3588
4254
|
const vault = input.vault;
|
|
3589
4255
|
const includeStashes = input.includeStashes ?? false;
|
|
3590
|
-
if (!
|
|
4256
|
+
if (!existsSync5(join19(vault, ".git"))) {
|
|
3591
4257
|
return {
|
|
3592
4258
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
3593
4259
|
result: ok({
|
|
@@ -3694,7 +4360,7 @@ function runSyncStatus(input) {
|
|
|
3694
4360
|
}
|
|
3695
4361
|
async function runSyncPush(input) {
|
|
3696
4362
|
const vault = input.vault;
|
|
3697
|
-
if (!
|
|
4363
|
+
if (!existsSync5(join19(vault, ".git"))) {
|
|
3698
4364
|
return {
|
|
3699
4365
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
3700
4366
|
result: err("NOT_A_GIT_REPO", { path: vault })
|
|
@@ -3854,7 +4520,7 @@ function enableGitLongPathsOnWindows(vault) {
|
|
|
3854
4520
|
}
|
|
3855
4521
|
async function runSyncPull(input) {
|
|
3856
4522
|
const vault = input.vault;
|
|
3857
|
-
if (!
|
|
4523
|
+
if (!existsSync5(join19(vault, ".git"))) {
|
|
3858
4524
|
return {
|
|
3859
4525
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
3860
4526
|
result: err("NOT_A_GIT_REPO", { path: vault })
|
|
@@ -4026,7 +4692,7 @@ function runSyncPeers(input) {
|
|
|
4026
4692
|
}
|
|
4027
4693
|
function runSyncLock(input) {
|
|
4028
4694
|
const vault = input.vault;
|
|
4029
|
-
if (!
|
|
4695
|
+
if (!existsSync5(vault)) {
|
|
4030
4696
|
return {
|
|
4031
4697
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4032
4698
|
result: err("VAULT_PATH_INVALID", { path: vault })
|
|
@@ -4061,7 +4727,7 @@ function runSyncLock(input) {
|
|
|
4061
4727
|
}
|
|
4062
4728
|
function runSyncUnlock(input) {
|
|
4063
4729
|
const vault = input.vault;
|
|
4064
|
-
if (!
|
|
4730
|
+
if (!existsSync5(vault)) {
|
|
4065
4731
|
return {
|
|
4066
4732
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4067
4733
|
result: err("VAULT_PATH_INVALID", { path: vault })
|
|
@@ -4094,8 +4760,8 @@ function runSyncUnlock(input) {
|
|
|
4094
4760
|
}
|
|
4095
4761
|
|
|
4096
4762
|
// src/commands/backup.ts
|
|
4097
|
-
import { statSync as
|
|
4098
|
-
import { join as
|
|
4763
|
+
import { statSync as statSync2, readdirSync, readFileSync as readFileSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
4764
|
+
import { join as join20, relative as relative2, dirname as dirname6 } from "path";
|
|
4099
4765
|
import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
|
|
4100
4766
|
|
|
4101
4767
|
// src/utils/s3-client.ts
|
|
@@ -4119,7 +4785,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node_
|
|
|
4119
4785
|
function* walkMarkdown(dir, base) {
|
|
4120
4786
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
4121
4787
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
4122
|
-
const full =
|
|
4788
|
+
const full = join20(dir, entry.name);
|
|
4123
4789
|
if (entry.isDirectory()) {
|
|
4124
4790
|
yield* walkMarkdown(full, base);
|
|
4125
4791
|
} else if (entry.name.endsWith(".md")) {
|
|
@@ -4142,8 +4808,8 @@ async function runBackupSync(input) {
|
|
|
4142
4808
|
let failed = 0;
|
|
4143
4809
|
const files = [...walkMarkdown(input.vault, input.vault)];
|
|
4144
4810
|
for (const relPath of files) {
|
|
4145
|
-
const absPath =
|
|
4146
|
-
const localStat =
|
|
4811
|
+
const absPath = join20(input.vault, relPath);
|
|
4812
|
+
const localStat = statSync2(absPath);
|
|
4147
4813
|
let needsUpload = true;
|
|
4148
4814
|
try {
|
|
4149
4815
|
const head = await client.send(new HeadObjectCommand({ Bucket: input.bucket, Key: relPath }));
|
|
@@ -4161,7 +4827,7 @@ async function runBackupSync(input) {
|
|
|
4161
4827
|
continue;
|
|
4162
4828
|
}
|
|
4163
4829
|
try {
|
|
4164
|
-
const body =
|
|
4830
|
+
const body = readFileSync4(absPath);
|
|
4165
4831
|
await client.send(new PutObjectCommand({ Bucket: input.bucket, Key: relPath, Body: body }));
|
|
4166
4832
|
uploaded++;
|
|
4167
4833
|
} catch {
|
|
@@ -4218,9 +4884,9 @@ async function runBackupRestore(input) {
|
|
|
4218
4884
|
const objects = list.Contents ?? [];
|
|
4219
4885
|
for (const obj of objects) {
|
|
4220
4886
|
if (!obj.Key) continue;
|
|
4221
|
-
const localPath =
|
|
4887
|
+
const localPath = join20(target, obj.Key);
|
|
4222
4888
|
try {
|
|
4223
|
-
const localStat =
|
|
4889
|
+
const localStat = statSync2(localPath);
|
|
4224
4890
|
if (obj.LastModified && localStat.mtime > obj.LastModified) {
|
|
4225
4891
|
conflicts++;
|
|
4226
4892
|
continue;
|
|
@@ -4231,8 +4897,8 @@ async function runBackupRestore(input) {
|
|
|
4231
4897
|
const resp = await client.send(new GetObjectCommand({ Bucket: input.bucket, Key: obj.Key }));
|
|
4232
4898
|
const body = await resp.Body?.transformToByteArray();
|
|
4233
4899
|
if (body) {
|
|
4234
|
-
|
|
4235
|
-
|
|
4900
|
+
mkdirSync2(dirname6(localPath), { recursive: true });
|
|
4901
|
+
writeFileSync2(localPath, Buffer.from(body));
|
|
4236
4902
|
downloaded++;
|
|
4237
4903
|
}
|
|
4238
4904
|
} catch {
|
|
@@ -4264,11 +4930,11 @@ async function runBackupRestore(input) {
|
|
|
4264
4930
|
}
|
|
4265
4931
|
|
|
4266
4932
|
// src/commands/status.ts
|
|
4267
|
-
import { existsSync as
|
|
4268
|
-
import { readFile as
|
|
4269
|
-
import { join as
|
|
4933
|
+
import { existsSync as existsSync6, statSync as statSync3 } from "fs";
|
|
4934
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
4935
|
+
import { join as join21 } from "path";
|
|
4270
4936
|
async function runStatus(input) {
|
|
4271
|
-
if (!
|
|
4937
|
+
if (!existsSync6(input.vault)) {
|
|
4272
4938
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
4273
4939
|
}
|
|
4274
4940
|
const scan = await scanVault(input.vault);
|
|
@@ -4293,7 +4959,7 @@ async function runStatus(input) {
|
|
|
4293
4959
|
const compound = scan.data.compound.length;
|
|
4294
4960
|
let schemaVersion = "v1";
|
|
4295
4961
|
try {
|
|
4296
|
-
const schemaContent = await
|
|
4962
|
+
const schemaContent = await readFile14(join21(input.vault, "SCHEMA.md"), "utf8");
|
|
4297
4963
|
const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
|
|
4298
4964
|
if (versionMatch) schemaVersion = versionMatch[1];
|
|
4299
4965
|
} catch {
|
|
@@ -4309,7 +4975,7 @@ async function runStatus(input) {
|
|
|
4309
4975
|
let maxTime = 0;
|
|
4310
4976
|
for (const page of allPages) {
|
|
4311
4977
|
try {
|
|
4312
|
-
const st =
|
|
4978
|
+
const st = statSync3(page.absPath);
|
|
4313
4979
|
if (st.mtimeMs > maxTime) {
|
|
4314
4980
|
maxTime = st.mtimeMs;
|
|
4315
4981
|
lastModified = st.mtime.toISOString();
|
|
@@ -4353,8 +5019,8 @@ async function runStatus(input) {
|
|
|
4353
5019
|
}
|
|
4354
5020
|
|
|
4355
5021
|
// src/commands/seed.ts
|
|
4356
|
-
import { mkdir as
|
|
4357
|
-
import { join as
|
|
5022
|
+
import { mkdir as mkdir10, writeFile as writeFile9, stat as stat4 } from "fs/promises";
|
|
5023
|
+
import { join as join22 } from "path";
|
|
4358
5024
|
var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4359
5025
|
var EXAMPLE_PAGES = {
|
|
4360
5026
|
"entities/example-project.md": `---
|
|
@@ -4423,29 +5089,29 @@ Real sources are immutable after ingestion \u2014 never edit them.
|
|
|
4423
5089
|
`;
|
|
4424
5090
|
async function runSeed(input) {
|
|
4425
5091
|
try {
|
|
4426
|
-
await
|
|
5092
|
+
await stat4(join22(input.vault, "SCHEMA.md"));
|
|
4427
5093
|
} catch {
|
|
4428
5094
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
|
|
4429
5095
|
}
|
|
4430
5096
|
const created = [];
|
|
4431
5097
|
const skipped = [];
|
|
4432
5098
|
for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
|
|
4433
|
-
const absPath =
|
|
5099
|
+
const absPath = join22(input.vault, relPath);
|
|
4434
5100
|
try {
|
|
4435
|
-
await
|
|
5101
|
+
await stat4(absPath);
|
|
4436
5102
|
skipped.push(relPath);
|
|
4437
5103
|
} catch {
|
|
4438
|
-
await
|
|
5104
|
+
await mkdir10(join22(absPath, ".."), { recursive: true });
|
|
4439
5105
|
await writeFile9(absPath, content, "utf8");
|
|
4440
5106
|
created.push(relPath);
|
|
4441
5107
|
}
|
|
4442
5108
|
}
|
|
4443
|
-
const rawPath =
|
|
5109
|
+
const rawPath = join22(input.vault, "raw", "articles", "example-source.md");
|
|
4444
5110
|
try {
|
|
4445
|
-
await
|
|
5111
|
+
await stat4(rawPath);
|
|
4446
5112
|
skipped.push("raw/articles/example-source.md");
|
|
4447
5113
|
} catch {
|
|
4448
|
-
await
|
|
5114
|
+
await mkdir10(join22(rawPath, ".."), { recursive: true });
|
|
4449
5115
|
await writeFile9(rawPath, EXAMPLE_RAW, "utf8");
|
|
4450
5116
|
created.push("raw/articles/example-source.md");
|
|
4451
5117
|
}
|
|
@@ -4468,9 +5134,9 @@ async function runSeed(input) {
|
|
|
4468
5134
|
}
|
|
4469
5135
|
|
|
4470
5136
|
// src/commands/canvas.ts
|
|
4471
|
-
import { readFile as
|
|
4472
|
-
import { existsSync as
|
|
4473
|
-
import { join as
|
|
5137
|
+
import { readFile as readFile15, writeFile as writeFile10 } from "fs/promises";
|
|
5138
|
+
import { existsSync as existsSync7 } from "fs";
|
|
5139
|
+
import { join as join23 } from "path";
|
|
4474
5140
|
var NODE_WIDTH = 240;
|
|
4475
5141
|
var NODE_HEIGHT = 60;
|
|
4476
5142
|
var COLUMN_SPACING = 400;
|
|
@@ -4548,8 +5214,8 @@ function buildCanvasEdges(adjacency) {
|
|
|
4548
5214
|
return edges;
|
|
4549
5215
|
}
|
|
4550
5216
|
async function runCanvasGenerate(input) {
|
|
4551
|
-
const graphPath = input.graphPath ??
|
|
4552
|
-
if (!
|
|
5217
|
+
const graphPath = input.graphPath ?? join23(input.vault, ".skillwiki", "graph.json");
|
|
5218
|
+
if (!existsSync7(graphPath)) {
|
|
4553
5219
|
return {
|
|
4554
5220
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
4555
5221
|
result: err("FILE_NOT_FOUND", {
|
|
@@ -4560,7 +5226,7 @@ async function runCanvasGenerate(input) {
|
|
|
4560
5226
|
}
|
|
4561
5227
|
let raw;
|
|
4562
5228
|
try {
|
|
4563
|
-
raw = await
|
|
5229
|
+
raw = await readFile15(graphPath, "utf8");
|
|
4564
5230
|
} catch (e) {
|
|
4565
5231
|
return {
|
|
4566
5232
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
@@ -4586,7 +5252,7 @@ async function runCanvasGenerate(input) {
|
|
|
4586
5252
|
const nodes = buildCanvasNodes(paths);
|
|
4587
5253
|
const edges = buildCanvasEdges(graph.adjacency);
|
|
4588
5254
|
const canvas = { nodes, edges };
|
|
4589
|
-
const outPath =
|
|
5255
|
+
const outPath = join23(input.vault, "vault-graph.canvas");
|
|
4590
5256
|
try {
|
|
4591
5257
|
await writeFile10(outPath, JSON.stringify(canvas, null, 2));
|
|
4592
5258
|
} catch (e) {
|
|
@@ -4608,10 +5274,10 @@ written: ${outPath}`
|
|
|
4608
5274
|
}
|
|
4609
5275
|
|
|
4610
5276
|
// src/commands/fleet-health.ts
|
|
4611
|
-
import { existsSync as
|
|
5277
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
|
|
4612
5278
|
import { execSync as nodeExecSync } from "child_process";
|
|
4613
5279
|
import { hostname as nodeHostname, platform as nodePlatform } from "os";
|
|
4614
|
-
import { join as
|
|
5280
|
+
import { join as join24 } from "path";
|
|
4615
5281
|
var SSH_TIMEOUT_MS = 15e3;
|
|
4616
5282
|
var TIMER_UNIT = "agent-memory-trends.timer";
|
|
4617
5283
|
var SERVICE_UNIT = "agent-memory-trends.service";
|
|
@@ -4708,9 +5374,9 @@ function applyServiceFailedOverlay(run, serviceFailed) {
|
|
|
4708
5374
|
function probeLocal(vaultPath, deps) {
|
|
4709
5375
|
const latestPath = satelliteLatestRunPath(vaultPath);
|
|
4710
5376
|
let parsed = null;
|
|
4711
|
-
if (
|
|
5377
|
+
if (existsSync8(latestPath)) {
|
|
4712
5378
|
try {
|
|
4713
|
-
const wire = readSatelliteLatestRunFromText(
|
|
5379
|
+
const wire = readSatelliteLatestRunFromText(readFileSync5(latestPath, "utf8"));
|
|
4714
5380
|
if (wire) {
|
|
4715
5381
|
parsed = {
|
|
4716
5382
|
status: wire.status,
|
|
@@ -4841,7 +5507,7 @@ async function runFleetHealth(input) {
|
|
|
4841
5507
|
const home = input.home ?? env.HOME ?? "";
|
|
4842
5508
|
const osHostname = input.osHostname ?? env.HOSTNAME ?? nodeHostname();
|
|
4843
5509
|
const vault = input.vault ?? env.WIKI_PATH;
|
|
4844
|
-
const file = input.file ?? (vault ?
|
|
5510
|
+
const file = input.file ?? (vault ? join24(vault, FLEET_REL_PATH) : void 0);
|
|
4845
5511
|
if (!file) {
|
|
4846
5512
|
return {
|
|
4847
5513
|
exitCode: ExitCode.NO_VAULT_CONFIGURED,
|
|
@@ -4925,15 +5591,15 @@ async function runFleetHealth(input) {
|
|
|
4925
5591
|
}
|
|
4926
5592
|
|
|
4927
5593
|
// src/utils/auto-commit.ts
|
|
4928
|
-
import { existsSync as
|
|
4929
|
-
import { join as
|
|
5594
|
+
import { existsSync as existsSync9 } from "fs";
|
|
5595
|
+
import { join as join25 } from "path";
|
|
4930
5596
|
async function postCommit(vault, exitCode) {
|
|
4931
5597
|
if (exitCode !== 0) return;
|
|
4932
5598
|
const home = process.env.HOME ?? "";
|
|
4933
5599
|
const dotenv = await parseDotenvFile(configPath(home));
|
|
4934
5600
|
const autoCommit = process.env.AUTO_COMMIT ?? dotenv["AUTO_COMMIT"];
|
|
4935
5601
|
if (autoCommit === "false") return;
|
|
4936
|
-
if (!
|
|
5602
|
+
if (!existsSync9(join25(vault, ".git"))) return;
|
|
4937
5603
|
const lastOps = readLastOp(vault);
|
|
4938
5604
|
if (lastOps.length === 0) return;
|
|
4939
5605
|
const porcelain = git(vault, ["status", "--porcelain", "--", ...VAULT_COMMIT_PATHSPEC]);
|
|
@@ -4957,8 +5623,8 @@ async function postCommit(vault, exitCode) {
|
|
|
4957
5623
|
}
|
|
4958
5624
|
|
|
4959
5625
|
// src/utils/protected-vault-write-guard.ts
|
|
4960
|
-
import { readFileSync as
|
|
4961
|
-
import { join as
|
|
5626
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
5627
|
+
import { join as join26, resolve as resolvePath } from "path";
|
|
4962
5628
|
async function guardProtectedVaultWrite(input) {
|
|
4963
5629
|
const env = input.env ?? process.env;
|
|
4964
5630
|
const home = input.home ?? process.env.HOME ?? "";
|
|
@@ -5024,7 +5690,7 @@ async function resolveLiveVaultPath(input) {
|
|
|
5024
5690
|
return resolved.ok ? resolved.data.path : void 0;
|
|
5025
5691
|
}
|
|
5026
5692
|
function resolveSnapshotWorktree(home) {
|
|
5027
|
-
const skillwikiEnv =
|
|
5693
|
+
const skillwikiEnv = join26(home, ".skillwiki", ".env");
|
|
5028
5694
|
const explicitWorktree = readEnvKey(skillwikiEnv, ["vault_sync.snapshot_worktree"]);
|
|
5029
5695
|
if (explicitWorktree) return explicitWorktree;
|
|
5030
5696
|
const snapshotProfile = readEnvKey(skillwikiEnv, ["vault_sync.snapshot_profile"]);
|
|
@@ -5036,7 +5702,7 @@ function resolveSnapshotWorktree(home) {
|
|
|
5036
5702
|
}
|
|
5037
5703
|
function readEnvKey(path, keys) {
|
|
5038
5704
|
try {
|
|
5039
|
-
const content =
|
|
5705
|
+
const content = readFileSync6(path, "utf8");
|
|
5040
5706
|
for (const line of content.split(/\r?\n/)) {
|
|
5041
5707
|
const trimmed = line.trim();
|
|
5042
5708
|
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
|
|
@@ -5094,7 +5760,7 @@ program.command("validate <file>").description("validate vault page frontmatter
|
|
|
5094
5760
|
emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
|
|
5095
5761
|
});
|
|
5096
5762
|
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 ??
|
|
5763
|
+
const out = opts.out ?? join27(vault, ".skillwiki", "graph.json");
|
|
5098
5764
|
return emitGuardedVaultWrite(vault, "graph build", () => runGraphBuild({ vault, out }));
|
|
5099
5765
|
});
|
|
5100
5766
|
var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
|
|
@@ -5188,6 +5854,45 @@ program.command("tag-audit [vault]").description("audit tag taxonomy consistency
|
|
|
5188
5854
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5189
5855
|
else emit(await runTagAudit({ vault: v.vault }), v.vault);
|
|
5190
5856
|
});
|
|
5857
|
+
var tagCmd = program.command("tag").description("manage the vault tag taxonomy");
|
|
5858
|
+
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) => {
|
|
5859
|
+
const resolved = await resolveVaultArg(vault, opts.wiki);
|
|
5860
|
+
if (!resolved.ok) return emit({ exitCode: resolved.exitCode, result: resolved.payload });
|
|
5861
|
+
const input = {
|
|
5862
|
+
vault: resolved.vault,
|
|
5863
|
+
page: opts.page,
|
|
5864
|
+
from: opts.from,
|
|
5865
|
+
tags: opts.tags?.split(",").map((tag) => tag.trim()).filter(Boolean),
|
|
5866
|
+
reason: opts.reason,
|
|
5867
|
+
write: !!opts.write
|
|
5868
|
+
};
|
|
5869
|
+
if (!opts.write) return emit(await runTagReconcile(input), resolved.vault, { postCommit: false });
|
|
5870
|
+
return emitGuardedVaultWrite(
|
|
5871
|
+
resolved.vault,
|
|
5872
|
+
"tag reconcile",
|
|
5873
|
+
() => runTagReconcile(input),
|
|
5874
|
+
{ postCommit: false }
|
|
5875
|
+
);
|
|
5876
|
+
});
|
|
5877
|
+
var pageCmd = program.command("page").description("validate and publish typed vault pages");
|
|
5878
|
+
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) => {
|
|
5879
|
+
const resolved = await resolveVaultArg(vault, opts.wiki);
|
|
5880
|
+
if (!resolved.ok) return emit({ exitCode: resolved.exitCode, result: resolved.payload });
|
|
5881
|
+
const input = {
|
|
5882
|
+
vault: resolved.vault,
|
|
5883
|
+
draftPath: draft,
|
|
5884
|
+
target: opts.target,
|
|
5885
|
+
logNote: opts.logNote,
|
|
5886
|
+
write: !!opts.write
|
|
5887
|
+
};
|
|
5888
|
+
if (!opts.write) return emit(await runPagePublish(input), resolved.vault, { postCommit: false });
|
|
5889
|
+
return emitGuardedVaultWrite(
|
|
5890
|
+
resolved.vault,
|
|
5891
|
+
"page publish",
|
|
5892
|
+
() => runPagePublish(input),
|
|
5893
|
+
{ postCommit: false }
|
|
5894
|
+
);
|
|
5895
|
+
});
|
|
5191
5896
|
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
5897
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5193
5898
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
@@ -5354,6 +6059,22 @@ program.command("archive <page> [vault]").description("archive a typed-knowledge
|
|
|
5354
6059
|
})
|
|
5355
6060
|
);
|
|
5356
6061
|
});
|
|
6062
|
+
program.command("remove <page> [vault]").description("remove a vault path and write a delete-intent tombstone").option("--wiki <name>", "wiki profile name").option("--remote <remote>", "rclone remote root to prune the live path, for example seaweed-wiki:cloud/wiki").option("--remote-delete", "delete the live path from the remote after local remove", false).option("--max-remote-deletes <n>", "maximum remote object deletes allowed", "1").option("--reason <text>", "stored on the delete-intent tombstone").action(async (page, vault, opts) => {
|
|
6063
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6064
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6065
|
+
else return emitGuardedVaultWrite(
|
|
6066
|
+
v.vault,
|
|
6067
|
+
"remove",
|
|
6068
|
+
() => runRemove({
|
|
6069
|
+
vault: v.vault,
|
|
6070
|
+
page,
|
|
6071
|
+
remote: opts.remote,
|
|
6072
|
+
remoteDelete: !!opts.remoteDelete,
|
|
6073
|
+
maxRemoteDeletes: Number.parseInt(opts.maxRemoteDeletes, 10),
|
|
6074
|
+
reason: opts.reason
|
|
6075
|
+
})
|
|
6076
|
+
);
|
|
6077
|
+
});
|
|
5357
6078
|
program.command("drift [vault]").description("detect content drift in raw sources").option("--apply", "update sha256 in drifted sources").option("--new <date>", "list raw files ingested on/after this date (YYYY-MM-DD)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5358
6079
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5359
6080
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|