skillwiki 0.10.12 → 0.10.14
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-YYWGP7DN.js → chunk-2AOOMDZ7.js} +148 -73
- package/dist/{chunk-TYN2IHBY.js → chunk-OWPQJOFG.js} +19 -11
- package/dist/{chunk-3ZVZ2YEE.js → chunk-UJLSUJB6.js} +5 -2
- package/dist/cli.js +476 -52
- package/dist/{managed-write-preflight-7TWSEZJZ.js → managed-write-preflight-M2HL6DDY.js} +2 -2
- package/dist/skillwiki-mcp.js +2 -2
- package/dist/vault-sync/scripts/lib/git-operation-journal.sh +106 -13
- package/dist/vault-sync/scripts/lib/managed-write-lock.sh +10 -7
- package/dist/vault-sync/scripts/wiki-snapshot.sh +14 -0
- package/package.json +1 -1
- package/skills/.claude-plugin/plugin.json +1 -1
- package/skills/.codex-plugin/plugin.json +1 -1
- package/skills/package.json +1 -1
- package/skills/skills/using-skillwiki/SKILL.md +3 -2
- package/skills/using-skillwiki/SKILL.md +3 -2
package/dist/cli.js
CHANGED
|
@@ -78,11 +78,12 @@ import {
|
|
|
78
78
|
safeWritePage,
|
|
79
79
|
satelliteLatestRunPath,
|
|
80
80
|
scanConflictMarkerBlocksInText,
|
|
81
|
+
snapshotterHealthChecks,
|
|
81
82
|
taxonomyCommentForPage,
|
|
82
83
|
upsertIndexEntry,
|
|
83
84
|
validateLogEvent,
|
|
84
85
|
writeLogEvent
|
|
85
|
-
} from "./chunk-
|
|
86
|
+
} from "./chunk-2AOOMDZ7.js";
|
|
86
87
|
import {
|
|
87
88
|
normalizeDistTag,
|
|
88
89
|
readCache,
|
|
@@ -110,15 +111,20 @@ import {
|
|
|
110
111
|
releaseManagedWriteLock,
|
|
111
112
|
runManagedWritePreflight,
|
|
112
113
|
runManagedWriteTransaction
|
|
113
|
-
} from "./chunk-
|
|
114
|
+
} from "./chunk-UJLSUJB6.js";
|
|
114
115
|
import {
|
|
115
116
|
FLEET_REL_PATH,
|
|
117
|
+
canSupersedeJournal,
|
|
116
118
|
git,
|
|
117
119
|
gitStrict,
|
|
120
|
+
hasActiveGitSequencer,
|
|
121
|
+
hasUnmergedPaths,
|
|
122
|
+
isWorktreeClean,
|
|
118
123
|
listJournalOpIds,
|
|
119
124
|
listReviewRequiredOps,
|
|
120
125
|
loadFleetManifest,
|
|
121
126
|
loadFleetManifestAndHost,
|
|
127
|
+
markJournalSuperseded,
|
|
122
128
|
parseDotenvFile,
|
|
123
129
|
parseDotenvText,
|
|
124
130
|
parseJournalEnv,
|
|
@@ -132,7 +138,7 @@ import {
|
|
|
132
138
|
snapshotterAliasForLocalHost,
|
|
133
139
|
supersedeStaleReviewRequiredJournals,
|
|
134
140
|
writeDotenv
|
|
135
|
-
} from "./chunk-
|
|
141
|
+
} from "./chunk-OWPQJOFG.js";
|
|
136
142
|
import {
|
|
137
143
|
ExitCode,
|
|
138
144
|
MetaSchema,
|
|
@@ -143,7 +149,7 @@ import {
|
|
|
143
149
|
} from "./chunk-C5OLZRRM.js";
|
|
144
150
|
|
|
145
151
|
// src/cli.ts
|
|
146
|
-
import { join as
|
|
152
|
+
import { join as join37 } from "path";
|
|
147
153
|
import { Command } from "commander";
|
|
148
154
|
|
|
149
155
|
// src/utils/output.ts
|
|
@@ -2247,6 +2253,9 @@ function writeWorkJournal(vault, opId, fields) {
|
|
|
2247
2253
|
writeFileSync3(tmp, body, "utf8");
|
|
2248
2254
|
renameSync(tmp, path);
|
|
2249
2255
|
}
|
|
2256
|
+
var DEFAULT_DEPS = {
|
|
2257
|
+
writeEvidenceText: atomicWriteText
|
|
2258
|
+
};
|
|
2250
2259
|
var PHASE_ORDER = ["validate", "evidence", "log", "projection", "commit", "done"];
|
|
2251
2260
|
function resolveWorkDir(vault, workItem) {
|
|
2252
2261
|
const rel = normalizeWorkItemRel(workItem);
|
|
@@ -2301,7 +2310,7 @@ ${body}`);
|
|
|
2301
2310
|
async function setStatusCompleted(filePath) {
|
|
2302
2311
|
return patchFrontmatterStatus(filePath);
|
|
2303
2312
|
}
|
|
2304
|
-
async function writeEvidence(workDir, opId, phases) {
|
|
2313
|
+
async function writeEvidence(workDir, opId, phases, writeText) {
|
|
2305
2314
|
const path = join16(workDir, "evidence.md");
|
|
2306
2315
|
const body = [
|
|
2307
2316
|
"---",
|
|
@@ -2317,7 +2326,7 @@ async function writeEvidence(workDir, opId, phases) {
|
|
|
2317
2326
|
`- completed_at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
2318
2327
|
""
|
|
2319
2328
|
].join("\n");
|
|
2320
|
-
return
|
|
2329
|
+
return writeText(path, body);
|
|
2321
2330
|
}
|
|
2322
2331
|
async function markPlanComplete(workDir) {
|
|
2323
2332
|
const planPath = join16(workDir, "plan.md");
|
|
@@ -2330,7 +2339,7 @@ function writeFailure(stage, result) {
|
|
|
2330
2339
|
result: result.ok ? err("WRITE_FAILED", { stage, message: "unexpected ok" }) : result
|
|
2331
2340
|
};
|
|
2332
2341
|
}
|
|
2333
|
-
async function runWorkComplete(input) {
|
|
2342
|
+
async function runWorkComplete(input, deps = DEFAULT_DEPS) {
|
|
2334
2343
|
const workDirResult = resolveWorkDir(input.vault, input.workItem);
|
|
2335
2344
|
if (!workDirResult.ok) {
|
|
2336
2345
|
return {
|
|
@@ -2411,7 +2420,12 @@ async function runWorkComplete(input) {
|
|
|
2411
2420
|
if (!statusWrite.ok) return writeFailure("evidence-spec", statusWrite);
|
|
2412
2421
|
const planWrite = await markPlanComplete(workDir);
|
|
2413
2422
|
if (!planWrite.ok) return writeFailure("evidence-plan", planWrite);
|
|
2414
|
-
const evidenceWrite = await writeEvidence(
|
|
2423
|
+
const evidenceWrite = await writeEvidence(
|
|
2424
|
+
workDir,
|
|
2425
|
+
opId,
|
|
2426
|
+
completedPhases,
|
|
2427
|
+
deps.writeEvidenceText
|
|
2428
|
+
);
|
|
2415
2429
|
if (!evidenceWrite.ok) return writeFailure("evidence", evidenceWrite);
|
|
2416
2430
|
if (input.failAfter === "evidence") {
|
|
2417
2431
|
throw new Error("simulated failure after evidence");
|
|
@@ -2442,7 +2456,12 @@ async function runWorkComplete(input) {
|
|
|
2442
2456
|
}
|
|
2443
2457
|
if (phaseIndex(phase) <= phaseIndex("projection")) {
|
|
2444
2458
|
if (!existsSync4(join16(workDir, "evidence.md"))) {
|
|
2445
|
-
const evidenceWrite = await writeEvidence(
|
|
2459
|
+
const evidenceWrite = await writeEvidence(
|
|
2460
|
+
workDir,
|
|
2461
|
+
opId,
|
|
2462
|
+
completedPhases,
|
|
2463
|
+
deps.writeEvidenceText
|
|
2464
|
+
);
|
|
2446
2465
|
if (!evidenceWrite.ok) return writeFailure("projection-evidence", evidenceWrite);
|
|
2447
2466
|
}
|
|
2448
2467
|
const finalCheck = await runWorkValidate({
|
|
@@ -2649,7 +2668,30 @@ function classifyLog(path, id, label, okPattern) {
|
|
|
2649
2668
|
if (okPattern.test(last)) return { id, label, status: "pass", detail: last.slice(0, 120) };
|
|
2650
2669
|
return { id, label, status: "warn", detail: last.slice(0, 120) };
|
|
2651
2670
|
}
|
|
2652
|
-
function
|
|
2671
|
+
function readVaultSyncRoleAndScope(home) {
|
|
2672
|
+
try {
|
|
2673
|
+
const content = readFileSync11(join17(home, ".skillwiki", ".env"), "utf8");
|
|
2674
|
+
let role;
|
|
2675
|
+
let serviceScope;
|
|
2676
|
+
let snapshotScript;
|
|
2677
|
+
for (const line of content.split(/\r?\n/)) {
|
|
2678
|
+
const trimmed = line.trim();
|
|
2679
|
+
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
|
|
2680
|
+
const eq = trimmed.indexOf("=");
|
|
2681
|
+
if (eq <= 0) continue;
|
|
2682
|
+
const k = trimmed.slice(0, eq).trim();
|
|
2683
|
+
const v = trimmed.slice(eq + 1).trim();
|
|
2684
|
+
if (v.length === 0) continue;
|
|
2685
|
+
if (k === "vault_sync.role") role = v;
|
|
2686
|
+
if (k === "vault_sync.service_scope") serviceScope = v;
|
|
2687
|
+
if (k === "vault_sync.snapshot_script") snapshotScript = v;
|
|
2688
|
+
}
|
|
2689
|
+
return { role, serviceScope, snapshotScript };
|
|
2690
|
+
} catch {
|
|
2691
|
+
return {};
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
function runVaultSyncHealth(home, syncMode, env = process.env) {
|
|
2653
2695
|
if (syncMode === "off") {
|
|
2654
2696
|
return {
|
|
2655
2697
|
status: "pass",
|
|
@@ -2678,6 +2720,30 @@ function runVaultSyncHealth(home, syncMode) {
|
|
|
2678
2720
|
}]
|
|
2679
2721
|
};
|
|
2680
2722
|
}
|
|
2723
|
+
const vsConfig = readVaultSyncRoleAndScope(home);
|
|
2724
|
+
if (vsConfig.role === "snapshotter") {
|
|
2725
|
+
const snapshotScript = vsConfig.snapshotScript ?? join17(shareDir, "wiki-snapshot.sh");
|
|
2726
|
+
const installed = existsSync5(snapshotScript) ? { id: "vault_sync_installed", label: "Vault sync installed", status: "pass", detail: `Found snapshot script: ${snapshotScript}` } : { id: "vault_sync_installed", label: "Vault sync installed", status: "error", detail: `Snapshot script not found at ${snapshotScript}` };
|
|
2727
|
+
const healthChecks = snapshotterHealthChecks(vsConfig.serviceScope ?? "user", logDir, env);
|
|
2728
|
+
const fetchNA = { id: "vault_sync_last_fetch_status", label: "Vault sync last fetch status", status: "pass", detail: "Snapshotter host \u2014 leaf wiki-fetch-notify log not applicable" };
|
|
2729
|
+
const filterNA = { id: "vault_sync_filter_present", label: "Vault sync filter file present", status: "pass", detail: "Snapshotter host \u2014 leaf wiki-push filter not applicable" };
|
|
2730
|
+
let guard;
|
|
2731
|
+
if (!existsSync5(snapshotScript)) {
|
|
2732
|
+
guard = { id: "vault_sync_snapshot_guard", label: "Snapshot script guard", status: "error", detail: `Snapshot script not found at ${snapshotScript}` };
|
|
2733
|
+
} else {
|
|
2734
|
+
const content = readFileSync11(snapshotScript, "utf8");
|
|
2735
|
+
guard = content.includes("--max-delete") ? { id: "vault_sync_snapshot_guard", label: "Snapshot script guard", status: "pass", detail: `--max-delete present in ${snapshotScript}` } : { id: "vault_sync_snapshot_guard", label: "Snapshot script guard", status: "error", detail: `${snapshotScript} is missing --max-delete guard` };
|
|
2736
|
+
}
|
|
2737
|
+
const allChecks = [installed, ...healthChecks, fetchNA, filterNA, guard];
|
|
2738
|
+
const summary2 = summarizeChecks(allChecks);
|
|
2739
|
+
return {
|
|
2740
|
+
status: statusFromCounts(summary2),
|
|
2741
|
+
blocking: syncMode === "required",
|
|
2742
|
+
installed: installed.status === "pass",
|
|
2743
|
+
summary: summary2,
|
|
2744
|
+
checks: allChecks.map((c) => ({ id: c.id, status: c.status, detail: c.detail }))
|
|
2745
|
+
};
|
|
2746
|
+
}
|
|
2681
2747
|
checks.push(existsSync5(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}` });
|
|
2682
2748
|
if (isMac) {
|
|
2683
2749
|
const pushPlist = join17(home, "Library", "LaunchAgents", "com.karlchow.wiki-push.plist");
|
|
@@ -4698,7 +4764,7 @@ import { createHash as createHash4 } from "crypto";
|
|
|
4698
4764
|
import { realpathSync } from "fs";
|
|
4699
4765
|
import { readFile as readFile12 } from "fs/promises";
|
|
4700
4766
|
import { join as join26, resolve as resolve3 } from "path";
|
|
4701
|
-
var
|
|
4767
|
+
var DEFAULT_DEPS2 = {
|
|
4702
4768
|
afterStage: async () => void 0,
|
|
4703
4769
|
preflight: (input) => runManagedWritePreflight(input)
|
|
4704
4770
|
};
|
|
@@ -5018,7 +5084,7 @@ async function previewPreparedPagePublication(input, vault) {
|
|
|
5018
5084
|
true
|
|
5019
5085
|
);
|
|
5020
5086
|
}
|
|
5021
|
-
async function publishPreparedPage(input, vault, deps =
|
|
5087
|
+
async function publishPreparedPage(input, vault, deps = DEFAULT_DEPS2) {
|
|
5022
5088
|
const managed = acquireManagedWriteLock(vault, `page publish ${input.page.target}`);
|
|
5023
5089
|
if (!managed.ok) return { exitCode: errorExitCode(managed.error), result: managed };
|
|
5024
5090
|
try {
|
|
@@ -5173,7 +5239,7 @@ async function publishPreparedPage(input, vault, deps = DEFAULT_DEPS) {
|
|
|
5173
5239
|
releaseManagedWriteLock(managed.data);
|
|
5174
5240
|
}
|
|
5175
5241
|
}
|
|
5176
|
-
async function runPagePublish(input, deps =
|
|
5242
|
+
async function runPagePublish(input, deps = DEFAULT_DEPS2) {
|
|
5177
5243
|
const prepared = await preparePagePublication(input);
|
|
5178
5244
|
if (!prepared.ok) return { exitCode: errorExitCode(prepared.error), result: prepared };
|
|
5179
5245
|
if (!input.write) return previewPreparedPagePublication(prepared.data, input.vault);
|
|
@@ -6490,9 +6556,352 @@ function runSyncJournalClearStale(input) {
|
|
|
6490
6556
|
};
|
|
6491
6557
|
}
|
|
6492
6558
|
|
|
6559
|
+
// src/commands/snapshot-maintenance.ts
|
|
6560
|
+
import { existsSync as existsSync9, readFileSync as readFileSync13, mkdirSync as mkdirSync3, appendFileSync } from "fs";
|
|
6561
|
+
import { createHash as createHash5 } from "crypto";
|
|
6562
|
+
import { execSync as execSync3, spawn } from "child_process";
|
|
6563
|
+
import { platform as platform2 } from "os";
|
|
6564
|
+
import { join as join30, resolve as resolvePath2 } from "path";
|
|
6565
|
+
var MAINTENANCE_SCHEMA_VERSION = 1;
|
|
6566
|
+
var MAINTENANCE_COMMAND = "snapshot-maintenance journal clear-stale";
|
|
6567
|
+
function resolveConfiguredSnapshotWorktree(home) {
|
|
6568
|
+
const skillwikiEnv = join30(home, ".skillwiki", ".env");
|
|
6569
|
+
const explicit = readEnvKey2(skillwikiEnv, ["vault_sync.snapshot_worktree"]);
|
|
6570
|
+
if (explicit) return resolvePath2(explicit);
|
|
6571
|
+
const snapshotProfile = readEnvKey2(skillwikiEnv, ["vault_sync.snapshot_profile"]);
|
|
6572
|
+
if (snapshotProfile) {
|
|
6573
|
+
const fromProfile = readEnvKey2(snapshotProfile, ["WIKI_GIT_WORKTREE", "SNAPSHOT_WORKTREE", "GIT_DIR"]);
|
|
6574
|
+
if (fromProfile) return resolvePath2(fromProfile);
|
|
6575
|
+
}
|
|
6576
|
+
return void 0;
|
|
6577
|
+
}
|
|
6578
|
+
function readEnvKey2(path, keys) {
|
|
6579
|
+
try {
|
|
6580
|
+
const content = readFileSync13(path, "utf8");
|
|
6581
|
+
for (const line of content.split(/\r?\n/)) {
|
|
6582
|
+
const trimmed = line.trim();
|
|
6583
|
+
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
|
|
6584
|
+
const eq = trimmed.indexOf("=");
|
|
6585
|
+
if (eq <= 0) continue;
|
|
6586
|
+
const key = trimmed.slice(0, eq).trim();
|
|
6587
|
+
if (!keys.includes(key)) continue;
|
|
6588
|
+
const value = trimmed.slice(eq + 1).trim();
|
|
6589
|
+
if (value.length > 0) return value;
|
|
6590
|
+
}
|
|
6591
|
+
} catch {
|
|
6592
|
+
return void 0;
|
|
6593
|
+
}
|
|
6594
|
+
return void 0;
|
|
6595
|
+
}
|
|
6596
|
+
function canonicalize(p) {
|
|
6597
|
+
return resolvePath2(p);
|
|
6598
|
+
}
|
|
6599
|
+
function computeApprovalId(plan) {
|
|
6600
|
+
const eligible = [...plan.eligible_journals].sort((a, b) => a.operation_id.localeCompare(b.operation_id));
|
|
6601
|
+
const targets = eligible.map((j) => `${j.operation_id}:${j.target_oid}`).join(",");
|
|
6602
|
+
const payload = [
|
|
6603
|
+
`v${plan.schema_version}`,
|
|
6604
|
+
plan.command,
|
|
6605
|
+
plan.host_id,
|
|
6606
|
+
plan.role,
|
|
6607
|
+
plan.snapshot_worktree,
|
|
6608
|
+
plan.snapshot_lock_path,
|
|
6609
|
+
plan.git_directory,
|
|
6610
|
+
plan.branch,
|
|
6611
|
+
plan.head_oid,
|
|
6612
|
+
targets,
|
|
6613
|
+
normalizeReason(plan.operator_reason)
|
|
6614
|
+
].join("|");
|
|
6615
|
+
return "smap1-" + createHash5("sha256").update(payload).digest("hex").slice(0, 32);
|
|
6616
|
+
}
|
|
6617
|
+
function normalizeReason(reason) {
|
|
6618
|
+
return reason.trim().replace(/\s+/g, " ");
|
|
6619
|
+
}
|
|
6620
|
+
function refusalErr(code, reason, guidance) {
|
|
6621
|
+
return err(code, { reason, guidance: guidance ?? "" });
|
|
6622
|
+
}
|
|
6623
|
+
async function runSnapshotMaintenanceDryRun(input) {
|
|
6624
|
+
const env = input.env ?? process.env;
|
|
6625
|
+
const home = input.home ?? env.HOME ?? "";
|
|
6626
|
+
const audit = input.auditSink ?? defaultAuditSink(home);
|
|
6627
|
+
const now = input.now ?? Date.now();
|
|
6628
|
+
const fleetLoad = input.fleetLoad !== void 0 ? input.fleetLoad : await loadFleetManifestAndHost({
|
|
6629
|
+
vault: input.liveVaultPath ?? "",
|
|
6630
|
+
env,
|
|
6631
|
+
home,
|
|
6632
|
+
cwd: process.cwd(),
|
|
6633
|
+
osHostname: env.HOSTNAME,
|
|
6634
|
+
user: env.USER
|
|
6635
|
+
});
|
|
6636
|
+
if (!fleetLoad || !fleetLoad.hostId || fleetLoad.identityStatus !== "known") {
|
|
6637
|
+
audit(makeAuditEvent(input, fleetLoad?.hostId ?? "unknown", now, "refusal", "MAINTENANCE_UNKNOWN_IDENTITY"));
|
|
6638
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_UNKNOWN_IDENTITY", "unknown or unresolved fleet identity; cannot authorize snapshot maintenance") };
|
|
6639
|
+
}
|
|
6640
|
+
const host = fleetLoad.manifest.hosts[fleetLoad.hostId];
|
|
6641
|
+
if (!host || host.role !== "snapshotter" || host.protected !== true) {
|
|
6642
|
+
audit(makeAuditEvent(input, fleetLoad.hostId, now, "refusal", "MAINTENANCE_NOT_PROTECTED_SNAPSHOTTER"));
|
|
6643
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_NOT_PROTECTED_SNAPSHOTTER", `host '${fleetLoad.hostId}' is not a protected snapshotter (role=${host?.role ?? "missing"}, protected=${host?.protected ?? false})`) };
|
|
6644
|
+
}
|
|
6645
|
+
const configuredWorktree = resolveConfiguredSnapshotWorktree(home);
|
|
6646
|
+
if (!configuredWorktree) {
|
|
6647
|
+
audit(makeAuditEvent(input, fleetLoad.hostId, now, "refusal", "MAINTENANCE_NO_CONFIGURED_WORKTREE"));
|
|
6648
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_NO_CONFIGURED_WORKTREE", "no configured snapshot worktree (vault_sync.snapshot_worktree or snapshot_profile required)") };
|
|
6649
|
+
}
|
|
6650
|
+
const requested = canonicalize(input.snapshotWorktree);
|
|
6651
|
+
if (requested !== canonicalize(configuredWorktree)) {
|
|
6652
|
+
audit(makeAuditEvent(input, fleetLoad.hostId, now, "refusal", "MAINTENANCE_WRONG_WORKTREE"));
|
|
6653
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_WRONG_WORKTREE", `requested path '${requested}' is not the configured snapshot worktree '${canonicalize(configuredWorktree)}'`) };
|
|
6654
|
+
}
|
|
6655
|
+
if (!existsSync9(requested) || !existsSync9(join30(requested, ".git"))) {
|
|
6656
|
+
audit(makeAuditEvent(input, fleetLoad.hostId, now, "refusal", "MAINTENANCE_MISSING_GIT_REPO"));
|
|
6657
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_MISSING_GIT_REPO", `snapshot worktree is not a git repository: ${requested}`) };
|
|
6658
|
+
}
|
|
6659
|
+
const liveVaultPath = input.liveVaultPath ? canonicalize(input.liveVaultPath) : await resolveLiveVault({ env, home });
|
|
6660
|
+
if (liveVaultPath && requested === canonicalize(liveVaultPath)) {
|
|
6661
|
+
audit(makeAuditEvent(input, fleetLoad.hostId, now, "refusal", "MAINTENANCE_LIVE_VAULT_TARGET"));
|
|
6662
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_LIVE_VAULT_TARGET", "requested path is the live vault, not the snapshot worktree") };
|
|
6663
|
+
}
|
|
6664
|
+
const branch = git(requested, ["rev-parse", "--abbrev-ref", "HEAD"]) || "HEAD";
|
|
6665
|
+
const headOid = git(requested, ["rev-parse", "HEAD"]) || "";
|
|
6666
|
+
const gitDirectory = git(requested, ["rev-parse", "--absolute-git-dir"]) || "";
|
|
6667
|
+
const worktreeClean = isWorktreeClean(requested);
|
|
6668
|
+
const activeSequencer = hasActiveGitSequencer(requested);
|
|
6669
|
+
const unmerged = hasUnmergedPaths(requested);
|
|
6670
|
+
const reviewRequired = listReviewRequiredOps(requested);
|
|
6671
|
+
const eligible = [];
|
|
6672
|
+
const skipped = [];
|
|
6673
|
+
for (const { opId, fields } of reviewRequired) {
|
|
6674
|
+
const target = fields.target_oid?.trim();
|
|
6675
|
+
if (!target) {
|
|
6676
|
+
skipped.push({ operation_id: opId, target_oid: "", reason: fields.reason, prior_reason: fields.prior_reason, eligible: false, refusal_reason: "missing-target-oid" });
|
|
6677
|
+
continue;
|
|
6678
|
+
}
|
|
6679
|
+
if (!canSupersedeJournal(requested, fields, { requireClean: false })) {
|
|
6680
|
+
let refusal = "not-ancestor";
|
|
6681
|
+
if (activeSequencer) refusal = "active-sequencer";
|
|
6682
|
+
else if (unmerged.length > 0) refusal = "unmerged-paths";
|
|
6683
|
+
else if (!gitMergeBaseIsAncestor(requested, target, headOid)) refusal = "not-ancestor";
|
|
6684
|
+
skipped.push({ operation_id: opId, target_oid: target, reason: fields.reason, prior_reason: fields.prior_reason, eligible: false, refusal_reason: refusal });
|
|
6685
|
+
continue;
|
|
6686
|
+
}
|
|
6687
|
+
eligible.push({ operation_id: opId, target_oid: target, reason: fields.reason, prior_reason: fields.prior_reason, eligible: true });
|
|
6688
|
+
}
|
|
6689
|
+
const snapshotLockPath = input.snapshotLockPath ?? "/var/lock/wiki-snapshot.lock";
|
|
6690
|
+
const planBase = {
|
|
6691
|
+
schema_version: MAINTENANCE_SCHEMA_VERSION,
|
|
6692
|
+
command: MAINTENANCE_COMMAND,
|
|
6693
|
+
host_id: fleetLoad.hostId,
|
|
6694
|
+
role: host.role,
|
|
6695
|
+
protected: host.protected === true,
|
|
6696
|
+
snapshot_worktree: requested,
|
|
6697
|
+
snapshot_lock_path: snapshotLockPath,
|
|
6698
|
+
git_directory: gitDirectory,
|
|
6699
|
+
branch,
|
|
6700
|
+
head_oid: headOid,
|
|
6701
|
+
worktree_clean: worktreeClean,
|
|
6702
|
+
active_sequencer: activeSequencer,
|
|
6703
|
+
unmerged_paths: unmerged,
|
|
6704
|
+
eligible_journals: eligible,
|
|
6705
|
+
skipped_journals: skipped,
|
|
6706
|
+
operator_reason: normalizeReason(input.reason ?? "")
|
|
6707
|
+
};
|
|
6708
|
+
const approvalId = eligible.length > 0 ? computeApprovalId(planBase) : null;
|
|
6709
|
+
const plan = { ...planBase, approval_id: approvalId };
|
|
6710
|
+
audit(makeAuditEvent(input, fleetLoad.hostId, now, "dry-run", void 0, approvalId ?? void 0));
|
|
6711
|
+
return {
|
|
6712
|
+
exitCode: ExitCode.OK,
|
|
6713
|
+
result: ok({
|
|
6714
|
+
dry_run: true,
|
|
6715
|
+
plan,
|
|
6716
|
+
humanHint: eligible.length === 0 ? `dry-run: 0 eligible journals; skipped=${skipped.length}` : `dry-run: ${eligible.length} eligible journal(s); approval_id=${approvalId}`
|
|
6717
|
+
})
|
|
6718
|
+
};
|
|
6719
|
+
}
|
|
6720
|
+
function gitMergeBaseIsAncestor(repo, ancestor, tip) {
|
|
6721
|
+
if (ancestor === tip) return true;
|
|
6722
|
+
const mb = git(repo, ["merge-base", ancestor, tip]);
|
|
6723
|
+
return mb !== "" && mb === ancestor;
|
|
6724
|
+
}
|
|
6725
|
+
async function resolveLiveVault(input) {
|
|
6726
|
+
const resolved = await resolveRuntimePath({
|
|
6727
|
+
flag: void 0,
|
|
6728
|
+
envValue: input.env.WIKI_PATH,
|
|
6729
|
+
wikiEnv: input.env.WIKI,
|
|
6730
|
+
home: input.home,
|
|
6731
|
+
cwd: process.cwd()
|
|
6732
|
+
});
|
|
6733
|
+
return resolved.ok ? canonicalize(resolved.data.path) : void 0;
|
|
6734
|
+
}
|
|
6735
|
+
function snapshotMaintenanceAuditLogPath(home) {
|
|
6736
|
+
const stateDir = platform2() === "darwin" ? join30(home, "Library", "Application Support", "vault-sync") : join30(home, ".local", "state", "vault-sync");
|
|
6737
|
+
return join30(stateDir, "snapshot-maintenance-audit.jsonl");
|
|
6738
|
+
}
|
|
6739
|
+
function defaultAuditSink(home) {
|
|
6740
|
+
return (event) => {
|
|
6741
|
+
try {
|
|
6742
|
+
const logPath = snapshotMaintenanceAuditLogPath(home);
|
|
6743
|
+
mkdirSync3(join30(logPath, ".."), { recursive: true });
|
|
6744
|
+
appendFileSync(logPath, JSON.stringify(event) + "\n", { encoding: "utf8" });
|
|
6745
|
+
} catch {
|
|
6746
|
+
}
|
|
6747
|
+
};
|
|
6748
|
+
}
|
|
6749
|
+
function makeAuditEvent(input, hostId, now, result, errorCode, approvalId) {
|
|
6750
|
+
return {
|
|
6751
|
+
ts: new Date(now).toISOString(),
|
|
6752
|
+
schema_version: MAINTENANCE_SCHEMA_VERSION,
|
|
6753
|
+
command: MAINTENANCE_COMMAND,
|
|
6754
|
+
host: hostId,
|
|
6755
|
+
actor: input.env?.USER ?? process.env.USER ?? "unknown",
|
|
6756
|
+
session: input.sessionId ?? "unknown",
|
|
6757
|
+
canonical_target: canonicalize(input.snapshotWorktree),
|
|
6758
|
+
reason: normalizeReason(input.reason ?? ""),
|
|
6759
|
+
approval_id: approvalId,
|
|
6760
|
+
result,
|
|
6761
|
+
error_code: errorCode
|
|
6762
|
+
};
|
|
6763
|
+
}
|
|
6764
|
+
async function runSnapshotMaintenanceExecute(input) {
|
|
6765
|
+
const env = input.env ?? process.env;
|
|
6766
|
+
const home = input.home ?? env.HOME ?? "";
|
|
6767
|
+
const audit = input.auditSink ?? defaultAuditSink(home);
|
|
6768
|
+
const now = input.now ?? Date.now();
|
|
6769
|
+
const isTty = input.isTty ?? !!process.stdin.isTTY;
|
|
6770
|
+
if (!isTty) {
|
|
6771
|
+
audit(makeAuditEvent(input, "unknown", now, "refusal", "MAINTENANCE_NO_TTY"));
|
|
6772
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_NO_TTY", "snapshot maintenance requires an attended TTY") };
|
|
6773
|
+
}
|
|
6774
|
+
const reason = normalizeReason(input.reason ?? "");
|
|
6775
|
+
if (!reason) {
|
|
6776
|
+
audit(makeAuditEvent(input, "unknown", now, "refusal", "MAINTENANCE_NO_REASON"));
|
|
6777
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_NO_REASON", "snapshot maintenance requires a non-empty operator reason") };
|
|
6778
|
+
}
|
|
6779
|
+
if (!input.approvalId) {
|
|
6780
|
+
audit(makeAuditEvent(input, "unknown", now, "refusal", "MAINTENANCE_NO_APPROVAL_ID"));
|
|
6781
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_NO_APPROVAL_ID", "snapshot maintenance requires an approval ID from a prior dry run") };
|
|
6782
|
+
}
|
|
6783
|
+
const dryRunResult = await runSnapshotMaintenanceDryRun(input);
|
|
6784
|
+
if (!dryRunResult.result.ok) return dryRunResult;
|
|
6785
|
+
const plan = dryRunResult.result.data.plan;
|
|
6786
|
+
if (!plan.approval_id) {
|
|
6787
|
+
audit(makeAuditEvent(input, plan.host_id, now, "refusal", "MAINTENANCE_NO_ELIGIBLE_JOURNALS"));
|
|
6788
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_NO_ELIGIBLE_JOURNALS", "no eligible journals to supersede") };
|
|
6789
|
+
}
|
|
6790
|
+
if (plan.approval_id !== input.approvalId) {
|
|
6791
|
+
audit(makeAuditEvent(input, plan.host_id, now, "refusal", "MAINTENANCE_STALE_APPROVAL_ID"));
|
|
6792
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_STALE_APPROVAL_ID", "approval ID does not match the current state; rerun dry run") };
|
|
6793
|
+
}
|
|
6794
|
+
if (!plan.worktree_clean) {
|
|
6795
|
+
audit(makeAuditEvent(input, plan.host_id, now, "refusal", "MAINTENANCE_DIRTY_WORKTREE"));
|
|
6796
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_DIRTY_WORKTREE", "snapshot worktree is dirty") };
|
|
6797
|
+
}
|
|
6798
|
+
if (plan.active_sequencer) {
|
|
6799
|
+
audit(makeAuditEvent(input, plan.host_id, now, "refusal", "MAINTENANCE_ACTIVE_SEQUENCER"));
|
|
6800
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_ACTIVE_SEQUENCER", "git sequencer (merge/rebase/cherry-pick/revert) is active") };
|
|
6801
|
+
}
|
|
6802
|
+
if (plan.unmerged_paths.length > 0) {
|
|
6803
|
+
audit(makeAuditEvent(input, plan.host_id, now, "refusal", "MAINTENANCE_UNMERGED_PATHS"));
|
|
6804
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_UNMERGED_PATHS", `unmerged paths: ${plan.unmerged_paths.join(", ")}`) };
|
|
6805
|
+
}
|
|
6806
|
+
if (!input.skipFlock) {
|
|
6807
|
+
const flockResult = acquireSnapshotFlock(plan.snapshot_lock_path);
|
|
6808
|
+
if (!flockResult.acquired) {
|
|
6809
|
+
audit(makeAuditEvent(input, plan.host_id, now, "refusal", "MAINTENANCE_FLOCK_BUSY"));
|
|
6810
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_FLOCK_BUSY", `snapshot flock busy: ${plan.snapshot_lock_path}`) };
|
|
6811
|
+
}
|
|
6812
|
+
try {
|
|
6813
|
+
return await performSupersession(input, plan, audit, now);
|
|
6814
|
+
} finally {
|
|
6815
|
+
releaseSnapshotFlock(flockResult);
|
|
6816
|
+
}
|
|
6817
|
+
}
|
|
6818
|
+
return performSupersession(input, plan, audit, now);
|
|
6819
|
+
}
|
|
6820
|
+
async function performSupersession(input, plan, audit, now) {
|
|
6821
|
+
const reDryRun = await runSnapshotMaintenanceDryRun(input);
|
|
6822
|
+
if (!reDryRun.result.ok) return reDryRun;
|
|
6823
|
+
const recomputed = reDryRun.result.data.plan;
|
|
6824
|
+
if (recomputed.head_oid !== plan.head_oid) {
|
|
6825
|
+
audit(makeAuditEvent(input, plan.host_id, now, "refusal", "MAINTENANCE_HEAD_CHANGED"));
|
|
6826
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_HEAD_CHANGED", "HEAD changed after dry run") };
|
|
6827
|
+
}
|
|
6828
|
+
const reEligible = new Set(recomputed.eligible_journals.map((j) => j.operation_id));
|
|
6829
|
+
const approvedSet = new Set(plan.eligible_journals.map((j) => j.operation_id));
|
|
6830
|
+
for (const id of approvedSet) {
|
|
6831
|
+
if (!reEligible.has(id)) {
|
|
6832
|
+
audit(makeAuditEvent(input, plan.host_id, now, "refusal", "MAINTENANCE_JOURNAL_SET_CHANGED"));
|
|
6833
|
+
return { exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED, result: refusalErr("MAINTENANCE_JOURNAL_SET_CHANGED", `approved journal '${id}' is no longer eligible`) };
|
|
6834
|
+
}
|
|
6835
|
+
}
|
|
6836
|
+
const superseded = [];
|
|
6837
|
+
const skipped = [];
|
|
6838
|
+
for (const j of plan.eligible_journals) {
|
|
6839
|
+
const fields = readJournal(plan.snapshot_worktree, j.operation_id);
|
|
6840
|
+
if (!fields) {
|
|
6841
|
+
skipped.push(j.operation_id);
|
|
6842
|
+
continue;
|
|
6843
|
+
}
|
|
6844
|
+
if (fields.target_oid?.trim() !== j.target_oid) {
|
|
6845
|
+
skipped.push(j.operation_id);
|
|
6846
|
+
continue;
|
|
6847
|
+
}
|
|
6848
|
+
const by = `snapshot-maintenance:${input.env?.USER ?? process.env.USER ?? "unknown"}:${input.sessionId ?? "unknown"}`;
|
|
6849
|
+
if (markJournalSuperseded(plan.snapshot_worktree, j.operation_id, fields, by)) {
|
|
6850
|
+
superseded.push(j.operation_id);
|
|
6851
|
+
} else {
|
|
6852
|
+
skipped.push(j.operation_id);
|
|
6853
|
+
}
|
|
6854
|
+
}
|
|
6855
|
+
const noOp = superseded.length === 0;
|
|
6856
|
+
const result = {
|
|
6857
|
+
superseded,
|
|
6858
|
+
skipped,
|
|
6859
|
+
approval_id: plan.approval_id,
|
|
6860
|
+
no_op: noOp
|
|
6861
|
+
};
|
|
6862
|
+
audit(makeAuditEvent(input, plan.host_id, now, noOp ? "no-op" : "success", void 0, plan.approval_id ?? void 0));
|
|
6863
|
+
return {
|
|
6864
|
+
exitCode: ExitCode.OK,
|
|
6865
|
+
result: ok({
|
|
6866
|
+
dry_run: false,
|
|
6867
|
+
execution: result,
|
|
6868
|
+
humanHint: noOp ? `execution: no-op (0 superseded; skipped=${skipped.length})` : `execution: ${superseded.length} journal(s) superseded; skipped=${skipped.length}`
|
|
6869
|
+
})
|
|
6870
|
+
};
|
|
6871
|
+
}
|
|
6872
|
+
function acquireSnapshotFlock(lockPath) {
|
|
6873
|
+
try {
|
|
6874
|
+
const child = spawn("bash", ["-c", `exec 9>"${lockPath}"; flock -n 9 || exit 1; while true; do sleep 3600; done`], {
|
|
6875
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
6876
|
+
detached: false
|
|
6877
|
+
});
|
|
6878
|
+
const deadline = Date.now() + 300;
|
|
6879
|
+
while (Date.now() < deadline) {
|
|
6880
|
+
if (child.exitCode !== null) {
|
|
6881
|
+
return { acquired: false, path: lockPath };
|
|
6882
|
+
}
|
|
6883
|
+
try {
|
|
6884
|
+
execSync3("sleep 0.05", { timeout: 200 });
|
|
6885
|
+
} catch {
|
|
6886
|
+
}
|
|
6887
|
+
}
|
|
6888
|
+
return { acquired: true, path: lockPath, holder: { kill: () => child.kill("SIGTERM") } };
|
|
6889
|
+
} catch {
|
|
6890
|
+
return { acquired: false, path: lockPath };
|
|
6891
|
+
}
|
|
6892
|
+
}
|
|
6893
|
+
function releaseSnapshotFlock(handle) {
|
|
6894
|
+
if (handle.holder) {
|
|
6895
|
+
try {
|
|
6896
|
+
handle.holder.kill();
|
|
6897
|
+
} catch {
|
|
6898
|
+
}
|
|
6899
|
+
}
|
|
6900
|
+
}
|
|
6901
|
+
|
|
6493
6902
|
// src/commands/backup.ts
|
|
6494
|
-
import { statSync as statSync2, readdirSync as readdirSync2, readFileSync as
|
|
6495
|
-
import { join as
|
|
6903
|
+
import { statSync as statSync2, readdirSync as readdirSync2, readFileSync as readFileSync14, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
6904
|
+
import { join as join31, relative as relative2, dirname as dirname6 } from "path";
|
|
6496
6905
|
import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
|
|
6497
6906
|
|
|
6498
6907
|
// src/utils/s3-client.ts
|
|
@@ -6516,7 +6925,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node_
|
|
|
6516
6925
|
function* walkMarkdown(dir, base) {
|
|
6517
6926
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
6518
6927
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
6519
|
-
const full =
|
|
6928
|
+
const full = join31(dir, entry.name);
|
|
6520
6929
|
if (entry.isDirectory()) {
|
|
6521
6930
|
yield* walkMarkdown(full, base);
|
|
6522
6931
|
} else if (entry.name.endsWith(".md")) {
|
|
@@ -6539,7 +6948,7 @@ async function runBackupSync(input) {
|
|
|
6539
6948
|
let failed = 0;
|
|
6540
6949
|
const files = [...walkMarkdown(input.vault, input.vault)];
|
|
6541
6950
|
for (const relPath of files) {
|
|
6542
|
-
const absPath =
|
|
6951
|
+
const absPath = join31(input.vault, relPath);
|
|
6543
6952
|
const localStat = statSync2(absPath);
|
|
6544
6953
|
let needsUpload = true;
|
|
6545
6954
|
try {
|
|
@@ -6558,7 +6967,7 @@ async function runBackupSync(input) {
|
|
|
6558
6967
|
continue;
|
|
6559
6968
|
}
|
|
6560
6969
|
try {
|
|
6561
|
-
const body =
|
|
6970
|
+
const body = readFileSync14(absPath);
|
|
6562
6971
|
await client.send(new PutObjectCommand({ Bucket: input.bucket, Key: relPath, Body: body }));
|
|
6563
6972
|
uploaded++;
|
|
6564
6973
|
} catch {
|
|
@@ -6615,7 +7024,7 @@ async function runBackupRestore(input) {
|
|
|
6615
7024
|
const objects = list.Contents ?? [];
|
|
6616
7025
|
for (const obj of objects) {
|
|
6617
7026
|
if (!obj.Key) continue;
|
|
6618
|
-
const localPath =
|
|
7027
|
+
const localPath = join31(target, obj.Key);
|
|
6619
7028
|
try {
|
|
6620
7029
|
const localStat = statSync2(localPath);
|
|
6621
7030
|
if (obj.LastModified && localStat.mtime > obj.LastModified) {
|
|
@@ -6628,7 +7037,7 @@ async function runBackupRestore(input) {
|
|
|
6628
7037
|
const resp = await client.send(new GetObjectCommand({ Bucket: input.bucket, Key: obj.Key }));
|
|
6629
7038
|
const body = await resp.Body?.transformToByteArray();
|
|
6630
7039
|
if (body) {
|
|
6631
|
-
|
|
7040
|
+
mkdirSync4(dirname6(localPath), { recursive: true });
|
|
6632
7041
|
writeFileSync5(localPath, Buffer.from(body));
|
|
6633
7042
|
downloaded++;
|
|
6634
7043
|
}
|
|
@@ -6661,11 +7070,11 @@ async function runBackupRestore(input) {
|
|
|
6661
7070
|
}
|
|
6662
7071
|
|
|
6663
7072
|
// src/commands/status.ts
|
|
6664
|
-
import { existsSync as
|
|
7073
|
+
import { existsSync as existsSync10, statSync as statSync3 } from "fs";
|
|
6665
7074
|
import { readFile as readFile15 } from "fs/promises";
|
|
6666
|
-
import { join as
|
|
7075
|
+
import { join as join32 } from "path";
|
|
6667
7076
|
async function runStatus(input) {
|
|
6668
|
-
if (!
|
|
7077
|
+
if (!existsSync10(input.vault)) {
|
|
6669
7078
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
6670
7079
|
}
|
|
6671
7080
|
const scan = await scanVault(input.vault);
|
|
@@ -6690,7 +7099,7 @@ async function runStatus(input) {
|
|
|
6690
7099
|
const compound = scan.data.compound.length;
|
|
6691
7100
|
let schemaVersion = "v1";
|
|
6692
7101
|
try {
|
|
6693
|
-
const schemaContent = await readFile15(
|
|
7102
|
+
const schemaContent = await readFile15(join32(input.vault, "SCHEMA.md"), "utf8");
|
|
6694
7103
|
const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
|
|
6695
7104
|
if (versionMatch) schemaVersion = versionMatch[1];
|
|
6696
7105
|
} catch {
|
|
@@ -6751,7 +7160,7 @@ async function runStatus(input) {
|
|
|
6751
7160
|
|
|
6752
7161
|
// src/commands/seed.ts
|
|
6753
7162
|
import { mkdir as mkdir10, writeFile as writeFile9, stat as stat4 } from "fs/promises";
|
|
6754
|
-
import { join as
|
|
7163
|
+
import { join as join33 } from "path";
|
|
6755
7164
|
var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6756
7165
|
var EXAMPLE_PAGES = {
|
|
6757
7166
|
"entities/example-project.md": `---
|
|
@@ -6820,29 +7229,29 @@ Real sources are immutable after ingestion \u2014 never edit them.
|
|
|
6820
7229
|
`;
|
|
6821
7230
|
async function runSeed(input) {
|
|
6822
7231
|
try {
|
|
6823
|
-
await stat4(
|
|
7232
|
+
await stat4(join33(input.vault, "SCHEMA.md"));
|
|
6824
7233
|
} catch {
|
|
6825
7234
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
|
|
6826
7235
|
}
|
|
6827
7236
|
const created = [];
|
|
6828
7237
|
const skipped = [];
|
|
6829
7238
|
for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
|
|
6830
|
-
const absPath =
|
|
7239
|
+
const absPath = join33(input.vault, relPath);
|
|
6831
7240
|
try {
|
|
6832
7241
|
await stat4(absPath);
|
|
6833
7242
|
skipped.push(relPath);
|
|
6834
7243
|
} catch {
|
|
6835
|
-
await mkdir10(
|
|
7244
|
+
await mkdir10(join33(absPath, ".."), { recursive: true });
|
|
6836
7245
|
await writeFile9(absPath, content, "utf8");
|
|
6837
7246
|
created.push(relPath);
|
|
6838
7247
|
}
|
|
6839
7248
|
}
|
|
6840
|
-
const rawPath =
|
|
7249
|
+
const rawPath = join33(input.vault, "raw", "articles", "example-source.md");
|
|
6841
7250
|
try {
|
|
6842
7251
|
await stat4(rawPath);
|
|
6843
7252
|
skipped.push("raw/articles/example-source.md");
|
|
6844
7253
|
} catch {
|
|
6845
|
-
await mkdir10(
|
|
7254
|
+
await mkdir10(join33(rawPath, ".."), { recursive: true });
|
|
6846
7255
|
await writeFile9(rawPath, EXAMPLE_RAW, "utf8");
|
|
6847
7256
|
created.push("raw/articles/example-source.md");
|
|
6848
7257
|
}
|
|
@@ -6866,8 +7275,8 @@ async function runSeed(input) {
|
|
|
6866
7275
|
|
|
6867
7276
|
// src/commands/canvas.ts
|
|
6868
7277
|
import { readFile as readFile16, writeFile as writeFile10 } from "fs/promises";
|
|
6869
|
-
import { existsSync as
|
|
6870
|
-
import { join as
|
|
7278
|
+
import { existsSync as existsSync11 } from "fs";
|
|
7279
|
+
import { join as join34 } from "path";
|
|
6871
7280
|
var NODE_WIDTH = 240;
|
|
6872
7281
|
var NODE_HEIGHT = 60;
|
|
6873
7282
|
var COLUMN_SPACING = 400;
|
|
@@ -6945,8 +7354,8 @@ function buildCanvasEdges(adjacency) {
|
|
|
6945
7354
|
return edges;
|
|
6946
7355
|
}
|
|
6947
7356
|
async function runCanvasGenerate(input) {
|
|
6948
|
-
const graphPath = input.graphPath ??
|
|
6949
|
-
if (!
|
|
7357
|
+
const graphPath = input.graphPath ?? join34(input.vault, ".skillwiki", "graph.json");
|
|
7358
|
+
if (!existsSync11(graphPath)) {
|
|
6950
7359
|
return {
|
|
6951
7360
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
6952
7361
|
result: err("FILE_NOT_FOUND", {
|
|
@@ -6983,7 +7392,7 @@ async function runCanvasGenerate(input) {
|
|
|
6983
7392
|
const nodes = buildCanvasNodes(paths);
|
|
6984
7393
|
const edges = buildCanvasEdges(graph.adjacency);
|
|
6985
7394
|
const canvas = { nodes, edges };
|
|
6986
|
-
const outPath =
|
|
7395
|
+
const outPath = join34(input.vault, "vault-graph.canvas");
|
|
6987
7396
|
try {
|
|
6988
7397
|
await writeFile10(outPath, JSON.stringify(canvas, null, 2));
|
|
6989
7398
|
} catch (e) {
|
|
@@ -7005,10 +7414,10 @@ written: ${outPath}`
|
|
|
7005
7414
|
}
|
|
7006
7415
|
|
|
7007
7416
|
// src/commands/fleet-health.ts
|
|
7008
|
-
import { existsSync as
|
|
7417
|
+
import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
|
|
7009
7418
|
import { execSync as nodeExecSync } from "child_process";
|
|
7010
7419
|
import { hostname as nodeHostname, platform as nodePlatform } from "os";
|
|
7011
|
-
import { join as
|
|
7420
|
+
import { join as join35 } from "path";
|
|
7012
7421
|
var SSH_TIMEOUT_MS = 15e3;
|
|
7013
7422
|
var TIMER_UNIT = "agent-memory-trends.timer";
|
|
7014
7423
|
var SERVICE_UNIT = "agent-memory-trends.service";
|
|
@@ -7105,9 +7514,9 @@ function applyServiceFailedOverlay(run, serviceFailed) {
|
|
|
7105
7514
|
function probeLocal(vaultPath, deps) {
|
|
7106
7515
|
const latestPath = satelliteLatestRunPath(vaultPath);
|
|
7107
7516
|
let parsed = null;
|
|
7108
|
-
if (
|
|
7517
|
+
if (existsSync12(latestPath)) {
|
|
7109
7518
|
try {
|
|
7110
|
-
const wire = readSatelliteLatestRunFromText(
|
|
7519
|
+
const wire = readSatelliteLatestRunFromText(readFileSync15(latestPath, "utf8"));
|
|
7111
7520
|
if (wire) {
|
|
7112
7521
|
parsed = {
|
|
7113
7522
|
status: wire.status,
|
|
@@ -7224,12 +7633,12 @@ function formatTable(rows) {
|
|
|
7224
7633
|
);
|
|
7225
7634
|
return [header, ...lines].join("\n");
|
|
7226
7635
|
}
|
|
7227
|
-
function rowHealthy(reachable, timer, runUnhealthy, timerBad,
|
|
7636
|
+
function rowHealthy(reachable, timer, runUnhealthy, timerBad, platform3) {
|
|
7228
7637
|
if (reachable === "no-access") return true;
|
|
7229
7638
|
if (reachable === "no") return false;
|
|
7230
7639
|
if (runUnhealthy) return false;
|
|
7231
|
-
if (
|
|
7232
|
-
if (
|
|
7640
|
+
if (platform3 === "linux" && timerBad) return false;
|
|
7641
|
+
if (platform3 === "linux" && timer === "inactive") return false;
|
|
7233
7642
|
return true;
|
|
7234
7643
|
}
|
|
7235
7644
|
async function runFleetHealth(input) {
|
|
@@ -7238,7 +7647,7 @@ async function runFleetHealth(input) {
|
|
|
7238
7647
|
const home = input.home ?? env.HOME ?? "";
|
|
7239
7648
|
const osHostname = input.osHostname ?? env.HOSTNAME ?? nodeHostname();
|
|
7240
7649
|
const vault = input.vault ?? env.WIKI_PATH;
|
|
7241
|
-
const file = input.file ?? (vault ?
|
|
7650
|
+
const file = input.file ?? (vault ? join35(vault, FLEET_REL_PATH) : void 0);
|
|
7242
7651
|
if (!file) {
|
|
7243
7652
|
return {
|
|
7244
7653
|
exitCode: ExitCode.NO_VAULT_CONFIGURED,
|
|
@@ -7322,15 +7731,15 @@ async function runFleetHealth(input) {
|
|
|
7322
7731
|
}
|
|
7323
7732
|
|
|
7324
7733
|
// src/utils/auto-commit.ts
|
|
7325
|
-
import { existsSync as
|
|
7326
|
-
import { join as
|
|
7734
|
+
import { existsSync as existsSync13 } from "fs";
|
|
7735
|
+
import { join as join36 } from "path";
|
|
7327
7736
|
async function postCommit(vault, exitCode) {
|
|
7328
7737
|
if (exitCode !== 0) return;
|
|
7329
7738
|
const home = process.env.HOME ?? "";
|
|
7330
7739
|
const dotenv = await parseDotenvFile(configPath(home));
|
|
7331
7740
|
const autoCommit = process.env.AUTO_COMMIT ?? dotenv["AUTO_COMMIT"];
|
|
7332
7741
|
if (autoCommit === "false") return;
|
|
7333
|
-
if (!
|
|
7742
|
+
if (!existsSync13(join36(vault, ".git"))) return;
|
|
7334
7743
|
const lastOps = readLastOp(vault);
|
|
7335
7744
|
if (lastOps.length === 0) return;
|
|
7336
7745
|
const porcelain = git(vault, ["status", "--porcelain", "--", ...VAULT_COMMIT_PATHSPEC]);
|
|
@@ -7354,9 +7763,9 @@ async function postCommit(vault, exitCode) {
|
|
|
7354
7763
|
}
|
|
7355
7764
|
|
|
7356
7765
|
// src/commands/write-preflight.ts
|
|
7357
|
-
import { existsSync as
|
|
7766
|
+
import { existsSync as existsSync14, readFileSync as readFileSync16, statSync as statSync4 } from "fs";
|
|
7358
7767
|
async function runWritePreflightCommand(input) {
|
|
7359
|
-
if (!
|
|
7768
|
+
if (!existsSync14(input.vault) || !statSync4(input.vault).isDirectory()) {
|
|
7360
7769
|
return {
|
|
7361
7770
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
7362
7771
|
result: err(GateError.VAULT_PATH_INVALID, { path: input.vault })
|
|
@@ -7364,13 +7773,13 @@ async function runWritePreflightCommand(input) {
|
|
|
7364
7773
|
}
|
|
7365
7774
|
let priorText = input.priorArtifactText;
|
|
7366
7775
|
if (input.priorArtifactFile) {
|
|
7367
|
-
if (!
|
|
7776
|
+
if (!existsSync14(input.priorArtifactFile)) {
|
|
7368
7777
|
return {
|
|
7369
7778
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
7370
7779
|
result: err("FILE_NOT_FOUND", { path: input.priorArtifactFile })
|
|
7371
7780
|
};
|
|
7372
7781
|
}
|
|
7373
|
-
priorText =
|
|
7782
|
+
priorText = readFileSync16(input.priorArtifactFile, "utf8");
|
|
7374
7783
|
}
|
|
7375
7784
|
const checkList = input.checks ? input.checks.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
7376
7785
|
const result = runWritePreflight({
|
|
@@ -7484,7 +7893,7 @@ async function emitManagedVaultWrite(vault, command, mutate, opts) {
|
|
|
7484
7893
|
if (dirty) {
|
|
7485
7894
|
return emit(dirty, void 0, { postCommit: false });
|
|
7486
7895
|
}
|
|
7487
|
-
const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-
|
|
7896
|
+
const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-M2HL6DDY.js");
|
|
7488
7897
|
const run = await runManagedWriteTransaction2({
|
|
7489
7898
|
vault,
|
|
7490
7899
|
command,
|
|
@@ -7509,7 +7918,7 @@ program.command("validate <file>").description("validate vault page frontmatter
|
|
|
7509
7918
|
emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
|
|
7510
7919
|
});
|
|
7511
7920
|
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) => {
|
|
7512
|
-
const out = opts.out ??
|
|
7921
|
+
const out = opts.out ?? join37(vault, ".skillwiki", "graph.json");
|
|
7513
7922
|
return emitGuardedVaultWrite(vault, "graph build", () => runGraphBuild({ vault, out }));
|
|
7514
7923
|
});
|
|
7515
7924
|
var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
|
|
@@ -8119,6 +8528,21 @@ syncJournalCmd.command("clear-stale [vault]").description("supersede stale revie
|
|
|
8119
8528
|
);
|
|
8120
8529
|
}
|
|
8121
8530
|
});
|
|
8531
|
+
var snapshotMaintenanceCmd = program.command("snapshot-maintenance").description("attended protected-snapshotter maintenance operations");
|
|
8532
|
+
var snapMaintJournalCmd = snapshotMaintenanceCmd.command("journal").description("snapshot operation journal maintenance");
|
|
8533
|
+
snapMaintJournalCmd.command("clear-stale [snapshot-worktree]").description("supersede safe stale review-required journals on a protected snapshotter (attended, one-shot)").option("--dry-run", "non-mutating plan + approval ID (no execution)", false).option("--approve <id>", "state-bound approval ID from a prior --dry-run").option("--reason <text>", "non-empty operator reason for the maintenance").option("--wiki <name>", "wiki profile name").action(async (snapshotWorktree, opts) => {
|
|
8534
|
+
if (!snapshotWorktree) {
|
|
8535
|
+
emit({ exitCode: ExitCode.USAGE, result: err("USAGE", "snapshot-maintenance journal clear-stale requires a snapshot-worktree path argument") });
|
|
8536
|
+
return;
|
|
8537
|
+
}
|
|
8538
|
+
if (opts.dryRun) {
|
|
8539
|
+
emit(await runSnapshotMaintenanceDryRun({ snapshotWorktree, dryRun: true, reason: opts.reason }));
|
|
8540
|
+
} else if (opts.approve) {
|
|
8541
|
+
emit(await runSnapshotMaintenanceExecute({ snapshotWorktree, dryRun: false, approvalId: opts.approve, reason: opts.reason }));
|
|
8542
|
+
} else {
|
|
8543
|
+
emit({ exitCode: ExitCode.USAGE, result: err("USAGE", "provide --dry-run for a plan, or --approve <id> --reason <text> to execute") });
|
|
8544
|
+
}
|
|
8545
|
+
});
|
|
8122
8546
|
syncCmd.command("lock [vault]").description("acquire advisory lock on vault").option("--summary <text>", "lock description", "skillwiki sync").option("--ttl-minutes <n>", "lock time-to-live in minutes", "30").option("--force", "overwrite existing lock", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
8123
8547
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
8124
8548
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|