skillwiki 0.9.63 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-5TBIMLTZ.js +343 -0
- package/dist/chunk-C5OLZRRM.js +357 -0
- package/dist/{chunk-TUFQZ5K4.js → chunk-DR7KFHNH.js} +792 -1983
- package/dist/chunk-IZABIE44.js +647 -0
- package/dist/chunk-S5ABQCXQ.js +580 -0
- package/dist/cli.js +1540 -550
- package/dist/index-projection-ERFX76U5.js +10 -0
- package/dist/managed-write-preflight-SILEUQEV.js +11 -0
- package/dist/skillwiki-mcp.js +4 -1
- package/dist/vault-sync/scripts/lib/conflict-markers.sh +69 -0
- package/dist/vault-sync/scripts/lib/delete-intent.sh +74 -0
- package/dist/vault-sync/scripts/lib/fleet.sh +103 -0
- package/dist/vault-sync/scripts/lib/git-case.sh +71 -0
- package/dist/vault-sync/scripts/lib/git-materialization.sh +264 -0
- package/dist/vault-sync/scripts/lib/git-operation-journal.sh +469 -0
- package/dist/vault-sync/scripts/lib/git-rebase-state.sh +180 -0
- package/dist/vault-sync/scripts/lib/lockfile.sh +70 -0
- package/dist/vault-sync/scripts/lib/managed-write-lock.sh +80 -0
- package/dist/vault-sync/scripts/lib/platform.sh +184 -0
- package/dist/vault-sync/scripts/lib/runtime-manifest.sh +223 -0
- package/dist/vault-sync/scripts/wiki-fetch-notify.sh +207 -0
- package/dist/vault-sync/scripts/wiki-fuse-refresh.sh +405 -0
- package/dist/vault-sync/scripts/wiki-pull-with-auto-resolve.sh +631 -0
- package/dist/vault-sync/scripts/wiki-push.sh +364 -0
- package/dist/vault-sync/scripts/wiki-snapshot.sh +587 -0
- package/package.json +2 -2
- package/skills/.claude-plugin/plugin.json +1 -1
- package/skills/.codex-plugin/plugin.json +1 -1
- package/skills/README.md +13 -0
- package/skills/package.json +1 -1
- package/skills/proj-work/SKILL.md +3 -0
- package/skills/skills/proj-work/SKILL.md +3 -0
- package/skills/skills/using-skillwiki/SKILL.md +13 -0
- package/skills/skills/wiki-crystallize/SKILL.md +3 -0
- package/skills/using-skillwiki/SKILL.md +13 -0
- package/skills/wiki-crystallize/SKILL.md +3 -0
package/dist/cli.js
CHANGED
|
@@ -1,53 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
ExitCode,
|
|
4
|
-
FLEET_REL_PATH,
|
|
5
|
-
MetaSchema,
|
|
6
|
-
RawSourceSchema,
|
|
7
3
|
SATELLITE_STALE_MS,
|
|
8
4
|
acquireLock,
|
|
9
5
|
acquireOwnedSyncLock,
|
|
10
6
|
appendLastOp,
|
|
11
|
-
assertTargetInsideVault,
|
|
12
7
|
assessSourceIdentity,
|
|
13
|
-
atomicWriteText,
|
|
14
8
|
buildDegradedReasons,
|
|
15
9
|
buildRemoteObjectPath,
|
|
16
10
|
clearLastOp,
|
|
17
11
|
configPath,
|
|
18
|
-
err,
|
|
19
12
|
evaluateSatelliteRunHealth,
|
|
20
13
|
extractCitationMarkers,
|
|
21
|
-
extractFrontmatter,
|
|
22
14
|
extractTaxonomy,
|
|
23
15
|
findPlugin,
|
|
24
16
|
fixPathTooLong,
|
|
25
17
|
getCliSessionId,
|
|
26
18
|
getSessionId,
|
|
27
|
-
isBlockedHost,
|
|
28
19
|
isFailedRunStatus,
|
|
29
20
|
isValidRemoteDeleteCap,
|
|
30
|
-
|
|
31
|
-
loadFleetManifestAndHost,
|
|
21
|
+
mergeTaxonomyConflict,
|
|
32
22
|
normalizeRemoteRoot,
|
|
33
|
-
ok,
|
|
34
|
-
parseDotenvFile,
|
|
35
|
-
parseDotenvText,
|
|
36
23
|
planAndMaybePruneRemoteObjects,
|
|
37
|
-
prepareTypedPage,
|
|
38
24
|
probeRemoteHealth,
|
|
39
|
-
profileKey,
|
|
40
25
|
readCliPackageJson,
|
|
41
26
|
readLastOp,
|
|
42
27
|
readLock,
|
|
43
|
-
readPage,
|
|
44
28
|
readSatelliteLatestRunFromText,
|
|
45
29
|
reconcileTaxonomyDocument,
|
|
46
|
-
redactSensitiveContent,
|
|
47
30
|
releaseLock,
|
|
48
31
|
releaseOwnedSyncLock,
|
|
49
32
|
renderIndexUpsert,
|
|
50
|
-
|
|
33
|
+
renderProjectIndex,
|
|
51
34
|
resolveInitTimePath,
|
|
52
35
|
resolveRuntimePath,
|
|
53
36
|
runAudit,
|
|
@@ -57,8 +40,6 @@ import {
|
|
|
57
40
|
runConfigSet,
|
|
58
41
|
runDedup,
|
|
59
42
|
runDoctor,
|
|
60
|
-
runFleetContext,
|
|
61
|
-
runFleetValidate,
|
|
62
43
|
runFrontmatterFix,
|
|
63
44
|
runGraphBuild,
|
|
64
45
|
runIndexCheck,
|
|
@@ -85,14 +66,9 @@ import {
|
|
|
85
66
|
runValidate,
|
|
86
67
|
safeWritePage,
|
|
87
68
|
satelliteLatestRunPath,
|
|
88
|
-
scanSensitiveContent,
|
|
89
|
-
scanVault,
|
|
90
|
-
snapshotterAliasForLocalHost,
|
|
91
|
-
splitFrontmatter,
|
|
92
69
|
taxonomyCommentForPage,
|
|
93
|
-
upsertIndexEntry
|
|
94
|
-
|
|
95
|
-
} from "./chunk-TUFQZ5K4.js";
|
|
70
|
+
upsertIndexEntry
|
|
71
|
+
} from "./chunk-DR7KFHNH.js";
|
|
96
72
|
import {
|
|
97
73
|
normalizeDistTag,
|
|
98
74
|
readCache,
|
|
@@ -100,9 +76,52 @@ import {
|
|
|
100
76
|
triggerAutoUpdate,
|
|
101
77
|
writeCache
|
|
102
78
|
} from "./chunk-7I2TPIV5.js";
|
|
79
|
+
import {
|
|
80
|
+
assertTargetInsideVault,
|
|
81
|
+
atomicWriteText,
|
|
82
|
+
extractFrontmatter,
|
|
83
|
+
prepareTypedPage,
|
|
84
|
+
readPage,
|
|
85
|
+
redactSensitiveContent,
|
|
86
|
+
renderRootIndex,
|
|
87
|
+
scanSensitiveContent,
|
|
88
|
+
scanVault,
|
|
89
|
+
splitFrontmatter,
|
|
90
|
+
writeRootIndexProjection
|
|
91
|
+
} from "./chunk-IZABIE44.js";
|
|
92
|
+
import {
|
|
93
|
+
acquireManagedWriteLock,
|
|
94
|
+
git,
|
|
95
|
+
gitStrict,
|
|
96
|
+
releaseManagedWriteLock,
|
|
97
|
+
runManagedWritePreflight,
|
|
98
|
+
runManagedWriteTransaction,
|
|
99
|
+
runVaultSyncPullHelper
|
|
100
|
+
} from "./chunk-5TBIMLTZ.js";
|
|
101
|
+
import {
|
|
102
|
+
FLEET_REL_PATH,
|
|
103
|
+
loadFleetManifest,
|
|
104
|
+
loadFleetManifestAndHost,
|
|
105
|
+
parseDotenvFile,
|
|
106
|
+
parseDotenvText,
|
|
107
|
+
profileKey,
|
|
108
|
+
resolveFleetHostId,
|
|
109
|
+
runFleetContext,
|
|
110
|
+
runFleetValidate,
|
|
111
|
+
snapshotterAliasForLocalHost,
|
|
112
|
+
writeDotenv
|
|
113
|
+
} from "./chunk-S5ABQCXQ.js";
|
|
114
|
+
import {
|
|
115
|
+
ExitCode,
|
|
116
|
+
MetaSchema,
|
|
117
|
+
RawSourceSchema,
|
|
118
|
+
err,
|
|
119
|
+
isBlockedHost,
|
|
120
|
+
ok
|
|
121
|
+
} from "./chunk-C5OLZRRM.js";
|
|
103
122
|
|
|
104
123
|
// src/cli.ts
|
|
105
|
-
import { join as
|
|
124
|
+
import { join as join34 } from "path";
|
|
106
125
|
import { Command } from "commander";
|
|
107
126
|
|
|
108
127
|
// src/utils/output.ts
|
|
@@ -710,11 +729,11 @@ async function runInit(input) {
|
|
|
710
729
|
].join("\n");
|
|
711
730
|
});
|
|
712
731
|
if (errTemplate) return errTemplate;
|
|
713
|
-
const
|
|
732
|
+
const err22 = await writeOrPreserve("log.md", async () => {
|
|
714
733
|
const tpl = await readFile3(join5(input.templates, "log.md"), "utf8");
|
|
715
734
|
return tpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang);
|
|
716
735
|
});
|
|
717
|
-
if (
|
|
736
|
+
if (err22) return err22;
|
|
718
737
|
const skipEnv = !!input.noEnv || explicitTarget && !input.profile && swDotenvHadPath && !input.force;
|
|
719
738
|
let envWritten = "";
|
|
720
739
|
if (!skipEnv) {
|
|
@@ -770,10 +789,1009 @@ async function runInit(input) {
|
|
|
770
789
|
};
|
|
771
790
|
}
|
|
772
791
|
|
|
792
|
+
// src/commands/index-rebuild.ts
|
|
793
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
794
|
+
import { join as join7 } from "path";
|
|
795
|
+
|
|
796
|
+
// src/utils/protected-vault-write-guard.ts
|
|
797
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
798
|
+
import { join as join6, resolve as resolvePath } from "path";
|
|
799
|
+
async function guardProtectedVaultWrite(input) {
|
|
800
|
+
const env = input.env ?? process.env;
|
|
801
|
+
const home = input.home ?? process.env.HOME ?? "";
|
|
802
|
+
const cwd = input.cwd ?? process.cwd();
|
|
803
|
+
const liveVaultPath = await resolveLiveVaultPath({ env, home, cwd });
|
|
804
|
+
const load = await loadFleetManifestAndHost({
|
|
805
|
+
vault: liveVaultPath ?? input.vault,
|
|
806
|
+
hostId: input.hostId,
|
|
807
|
+
env,
|
|
808
|
+
home,
|
|
809
|
+
cwd,
|
|
810
|
+
osHostname: input.osHostname ?? process.env.HOSTNAME,
|
|
811
|
+
user: input.user ?? process.env.USER
|
|
812
|
+
});
|
|
813
|
+
if (!load?.hostId || load.identityStatus !== "known") {
|
|
814
|
+
return { blocked: false };
|
|
815
|
+
}
|
|
816
|
+
const host = load.manifest.hosts[load.hostId];
|
|
817
|
+
if (!host || host.role !== "snapshotter" || host.protected !== true) {
|
|
818
|
+
return { blocked: false };
|
|
819
|
+
}
|
|
820
|
+
const snapshotWorktree = resolveSnapshotWorktree(home);
|
|
821
|
+
const targetVault = resolvePath(input.vault);
|
|
822
|
+
const canonicalLiveVault = liveVaultPath ? resolvePath(liveVaultPath) : void 0;
|
|
823
|
+
const canonicalSnapshotWorktree = snapshotWorktree ? resolvePath(snapshotWorktree) : void 0;
|
|
824
|
+
if (canonicalLiveVault && targetVault === canonicalLiveVault) {
|
|
825
|
+
return { blocked: false };
|
|
826
|
+
}
|
|
827
|
+
if (canonicalSnapshotWorktree && targetVault === canonicalSnapshotWorktree) {
|
|
828
|
+
return {
|
|
829
|
+
blocked: true,
|
|
830
|
+
exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED,
|
|
831
|
+
result: err("PROTECTED_SNAPSHOTTER_WRITE_BLOCKED", {
|
|
832
|
+
host_id: load.hostId,
|
|
833
|
+
command: input.command,
|
|
834
|
+
reason: `refusing mutation of snapshot worktree '${canonicalSnapshotWorktree}' on protected snapshotter host '${load.hostId}'`,
|
|
835
|
+
guidance: canonicalLiveVault ? `Use the live vault path '${canonicalLiveVault}' for authoring. Keep snapshot worktrees read-only except explicit snapshot maintenance.` : "Use the live skillwiki vault path for authoring. Keep snapshot worktrees read-only except explicit snapshot maintenance."
|
|
836
|
+
})
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
if (canonicalLiveVault && targetVault !== canonicalLiveVault) {
|
|
840
|
+
return {
|
|
841
|
+
blocked: true,
|
|
842
|
+
exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED,
|
|
843
|
+
result: err("PROTECTED_SNAPSHOTTER_WRITE_BLOCKED", {
|
|
844
|
+
host_id: load.hostId,
|
|
845
|
+
command: input.command,
|
|
846
|
+
reason: `refusing vault mutation outside the live vault path '${canonicalLiveVault}' on protected snapshotter host '${load.hostId}'`,
|
|
847
|
+
guidance: "Use the resolved live skillwiki vault path for authoring. Keep alternate vault roots and snapshot worktrees read-only unless explicitly approved."
|
|
848
|
+
})
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
return { blocked: false };
|
|
852
|
+
}
|
|
853
|
+
async function resolveLiveVaultPath(input) {
|
|
854
|
+
const resolved = await resolveRuntimePath({
|
|
855
|
+
flag: void 0,
|
|
856
|
+
envValue: input.env.WIKI_PATH,
|
|
857
|
+
wikiEnv: input.env.WIKI,
|
|
858
|
+
home: input.home,
|
|
859
|
+
cwd: input.cwd
|
|
860
|
+
});
|
|
861
|
+
return resolved.ok ? resolved.data.path : void 0;
|
|
862
|
+
}
|
|
863
|
+
function resolveSnapshotWorktree(home) {
|
|
864
|
+
const skillwikiEnv = join6(home, ".skillwiki", ".env");
|
|
865
|
+
const explicitWorktree = readEnvKey(skillwikiEnv, ["vault_sync.snapshot_worktree"]);
|
|
866
|
+
if (explicitWorktree) return explicitWorktree;
|
|
867
|
+
const snapshotProfile = readEnvKey(skillwikiEnv, ["vault_sync.snapshot_profile"]);
|
|
868
|
+
if (snapshotProfile) {
|
|
869
|
+
const fromProfile = readEnvKey(snapshotProfile, ["WIKI_GIT_WORKTREE", "SNAPSHOT_WORKTREE", "GIT_DIR"]);
|
|
870
|
+
if (fromProfile) return fromProfile;
|
|
871
|
+
}
|
|
872
|
+
return "/root/wiki-git";
|
|
873
|
+
}
|
|
874
|
+
function readEnvKey(path, keys) {
|
|
875
|
+
try {
|
|
876
|
+
const content = readFileSync2(path, "utf8");
|
|
877
|
+
for (const line of content.split(/\r?\n/)) {
|
|
878
|
+
const trimmed = line.trim();
|
|
879
|
+
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
|
|
880
|
+
const eq = trimmed.indexOf("=");
|
|
881
|
+
if (eq <= 0) continue;
|
|
882
|
+
const key = trimmed.slice(0, eq).trim();
|
|
883
|
+
if (!keys.includes(key)) continue;
|
|
884
|
+
const value = trimmed.slice(eq + 1).trim();
|
|
885
|
+
if (value.length > 0) return value;
|
|
886
|
+
}
|
|
887
|
+
} catch {
|
|
888
|
+
return void 0;
|
|
889
|
+
}
|
|
890
|
+
return void 0;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// src/commands/index-rebuild.ts
|
|
894
|
+
async function runIndexRebuild(input) {
|
|
895
|
+
const projection = await renderRootIndex({ vault: input.vault });
|
|
896
|
+
if (!projection.ok) {
|
|
897
|
+
const code = projection.error === "SCHEME_REJECTED" ? ExitCode.SCHEME_REJECTED : ExitCode.VAULT_PATH_INVALID;
|
|
898
|
+
return { exitCode: code, result: projection };
|
|
899
|
+
}
|
|
900
|
+
let current = "";
|
|
901
|
+
try {
|
|
902
|
+
current = readFileSync3(join7(input.vault, "index.md"), "utf8");
|
|
903
|
+
} catch {
|
|
904
|
+
current = "";
|
|
905
|
+
}
|
|
906
|
+
const changed = current !== projection.data.text;
|
|
907
|
+
if (!input.write) {
|
|
908
|
+
return {
|
|
909
|
+
exitCode: ExitCode.OK,
|
|
910
|
+
result: ok({
|
|
911
|
+
changed,
|
|
912
|
+
dry_run: true,
|
|
913
|
+
entry_count: projection.data.entries.length,
|
|
914
|
+
duplicates_removed: projection.data.duplicates_removed,
|
|
915
|
+
ghosts_removed: projection.data.ghosts_removed,
|
|
916
|
+
humanHint: changed ? `dry run: would rewrite index.md (${projection.data.entries.length} entries)` : `dry run: index.md already canonical (${projection.data.entries.length} entries)`
|
|
917
|
+
})
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
const guard = await guardProtectedVaultWrite({
|
|
921
|
+
vault: input.vault,
|
|
922
|
+
command: "index rebuild"
|
|
923
|
+
});
|
|
924
|
+
if (guard.blocked) {
|
|
925
|
+
return { exitCode: guard.exitCode, result: guard.result };
|
|
926
|
+
}
|
|
927
|
+
return runManagedWriteTransaction({
|
|
928
|
+
vault: input.vault,
|
|
929
|
+
command: "index rebuild",
|
|
930
|
+
allowImmutableRecord: false,
|
|
931
|
+
mutate: async () => {
|
|
932
|
+
if (!changed) {
|
|
933
|
+
return {
|
|
934
|
+
exitCode: ExitCode.OK,
|
|
935
|
+
result: ok({
|
|
936
|
+
changed: false,
|
|
937
|
+
dry_run: false,
|
|
938
|
+
entry_count: projection.data.entries.length,
|
|
939
|
+
duplicates_removed: projection.data.duplicates_removed,
|
|
940
|
+
ghosts_removed: projection.data.ghosts_removed,
|
|
941
|
+
humanHint: `index.md already canonical (${projection.data.entries.length} entries)`
|
|
942
|
+
})
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
const written = await writeRootIndexProjection(input.vault, projection.data);
|
|
946
|
+
if (!written.ok) {
|
|
947
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: written };
|
|
948
|
+
}
|
|
949
|
+
let installed = "";
|
|
950
|
+
try {
|
|
951
|
+
installed = readFileSync3(join7(input.vault, "index.md"), "utf8");
|
|
952
|
+
} catch (error) {
|
|
953
|
+
return {
|
|
954
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
955
|
+
result: err("WRITE_FAILED", { message: String(error) })
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
if (installed !== projection.data.text) {
|
|
959
|
+
return {
|
|
960
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
961
|
+
result: err("WRITE_FAILED", { message: "installed index.md differs from projection" })
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
return {
|
|
965
|
+
exitCode: ExitCode.OK,
|
|
966
|
+
result: ok({
|
|
967
|
+
changed: true,
|
|
968
|
+
dry_run: false,
|
|
969
|
+
entry_count: projection.data.entries.length,
|
|
970
|
+
duplicates_removed: projection.data.duplicates_removed,
|
|
971
|
+
ghosts_removed: projection.data.ghosts_removed,
|
|
972
|
+
humanHint: `rewrote index.md (${projection.data.entries.length} entries)`
|
|
973
|
+
})
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// src/commands/derived-conflicts.ts
|
|
980
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
981
|
+
import { existsSync, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
982
|
+
import { join as join9 } from "path";
|
|
983
|
+
|
|
984
|
+
// src/utils/log-merge.ts
|
|
985
|
+
import { execFileSync } from "child_process";
|
|
986
|
+
import { mkdtempSync, writeFileSync, rmSync } from "fs";
|
|
987
|
+
import { tmpdir } from "os";
|
|
988
|
+
import { join as join8 } from "path";
|
|
989
|
+
var ENTRY_RE = /^## \[/m;
|
|
990
|
+
var PUBLISH_MARKER_RE = /<!--\s*skillwiki-page-publish:([a-f0-9]{64})\s*-->/i;
|
|
991
|
+
function splitLog(text) {
|
|
992
|
+
const normalized = text.replace(/\r\n/g, "\n");
|
|
993
|
+
const match = ENTRY_RE.exec(normalized);
|
|
994
|
+
if (!match || match.index === void 0) {
|
|
995
|
+
return { preamble: normalized.replace(/\n+$/, ""), entries: [] };
|
|
996
|
+
}
|
|
997
|
+
const preamble = normalized.slice(0, match.index).replace(/\n+$/, "");
|
|
998
|
+
const body = normalized.slice(match.index);
|
|
999
|
+
const parts = body.split(/(?=^## \[)/m).filter((p) => p.length > 0);
|
|
1000
|
+
const entries = parts.map((p) => p.replace(/\n+$/, ""));
|
|
1001
|
+
return { preamble, entries };
|
|
1002
|
+
}
|
|
1003
|
+
function publishId(entry) {
|
|
1004
|
+
return PUBLISH_MARKER_RE.exec(entry)?.[1]?.toLowerCase();
|
|
1005
|
+
}
|
|
1006
|
+
function unionUnstructured(base, ours, theirs) {
|
|
1007
|
+
const dir = mkdtempSync(join8(tmpdir(), "log-union-"));
|
|
1008
|
+
try {
|
|
1009
|
+
const basePath = join8(dir, "base");
|
|
1010
|
+
const oursPath = join8(dir, "ours");
|
|
1011
|
+
const theirsPath = join8(dir, "theirs");
|
|
1012
|
+
writeFileSync(basePath, base.endsWith("\n") || base.length === 0 ? base : `${base}
|
|
1013
|
+
`);
|
|
1014
|
+
writeFileSync(oursPath, ours.endsWith("\n") || ours.length === 0 ? ours : `${ours}
|
|
1015
|
+
`);
|
|
1016
|
+
writeFileSync(theirsPath, theirs.endsWith("\n") || theirs.length === 0 ? theirs : `${theirs}
|
|
1017
|
+
`);
|
|
1018
|
+
const text = execFileSync("git", ["merge-file", "--union", "-p", oursPath, basePath, theirsPath], {
|
|
1019
|
+
encoding: "utf8"
|
|
1020
|
+
});
|
|
1021
|
+
return ok(text.endsWith("\n") ? text : `${text}
|
|
1022
|
+
`);
|
|
1023
|
+
} catch (error) {
|
|
1024
|
+
return err("WRITE_FAILED", { message: `unstructured log union failed: ${String(error)}` });
|
|
1025
|
+
} finally {
|
|
1026
|
+
rmSync(dir, { recursive: true, force: true });
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
function mergeLogConflictStages(input) {
|
|
1030
|
+
const ours = splitLog(input.ours);
|
|
1031
|
+
const theirs = splitLog(input.theirs);
|
|
1032
|
+
const baseSplit = splitLog(input.base);
|
|
1033
|
+
if (ours.entries.length === 0 && theirs.entries.length === 0) {
|
|
1034
|
+
const unioned = unionUnstructured(input.base, input.ours, input.theirs);
|
|
1035
|
+
if (!unioned.ok) return unioned;
|
|
1036
|
+
return ok({ text: unioned.data, deduplicated_operation_ids: [] });
|
|
1037
|
+
}
|
|
1038
|
+
const preamble = ours.preamble || theirs.preamble || baseSplit.preamble || "# Log";
|
|
1039
|
+
const seenMarkers = /* @__PURE__ */ new Set();
|
|
1040
|
+
const seenExact = /* @__PURE__ */ new Set();
|
|
1041
|
+
const out = [];
|
|
1042
|
+
const deduped = [];
|
|
1043
|
+
const push = (entry) => {
|
|
1044
|
+
const marker2 = publishId(entry);
|
|
1045
|
+
if (marker2) {
|
|
1046
|
+
if (seenMarkers.has(marker2)) {
|
|
1047
|
+
deduped.push(marker2);
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
seenMarkers.add(marker2);
|
|
1051
|
+
} else if (seenExact.has(entry)) {
|
|
1052
|
+
return;
|
|
1053
|
+
} else {
|
|
1054
|
+
seenExact.add(entry);
|
|
1055
|
+
}
|
|
1056
|
+
out.push(entry);
|
|
1057
|
+
};
|
|
1058
|
+
for (const e of ours.entries) push(e);
|
|
1059
|
+
for (const e of theirs.entries) push(e);
|
|
1060
|
+
let text = preamble;
|
|
1061
|
+
if (out.length > 0) {
|
|
1062
|
+
text = `${preamble.replace(/\n+$/, "")}
|
|
1063
|
+
|
|
1064
|
+
${out.join("\n\n")}`;
|
|
1065
|
+
}
|
|
1066
|
+
if (!text.endsWith("\n")) text += "\n";
|
|
1067
|
+
return ok({ text, deduplicated_operation_ids: deduped });
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// src/commands/derived-conflicts.ts
|
|
1071
|
+
function classifyDerivedPath(path) {
|
|
1072
|
+
if (path === "index.md") return "root-index";
|
|
1073
|
+
if (path === "SCHEMA.md") return "taxonomy";
|
|
1074
|
+
if (path === "log.md" || path.endsWith("/log.md")) return "log";
|
|
1075
|
+
if (/^projects\/[^/]+\/knowledge\.md$/.test(path)) return "project-index";
|
|
1076
|
+
return "unknown";
|
|
1077
|
+
}
|
|
1078
|
+
function journalPath(vault, operationId2) {
|
|
1079
|
+
const gitPath = git(vault, ["rev-parse", "--git-path", `vault-sync/operations/${operationId2}.env`]);
|
|
1080
|
+
if (!gitPath) return null;
|
|
1081
|
+
return gitPath.startsWith("/") ? gitPath : join9(vault, gitPath);
|
|
1082
|
+
}
|
|
1083
|
+
function readJournalFields(path) {
|
|
1084
|
+
const text = readFileSync5(path, "utf8");
|
|
1085
|
+
const out = {};
|
|
1086
|
+
for (const line of text.split("\n")) {
|
|
1087
|
+
const i = line.indexOf("=");
|
|
1088
|
+
if (i <= 0) continue;
|
|
1089
|
+
out[line.slice(0, i)] = line.slice(i + 1);
|
|
1090
|
+
}
|
|
1091
|
+
return out;
|
|
1092
|
+
}
|
|
1093
|
+
function showStage(vault, stage, path) {
|
|
1094
|
+
try {
|
|
1095
|
+
return execFileSync2("git", ["show", `:${stage}:${path}`], {
|
|
1096
|
+
cwd: vault,
|
|
1097
|
+
encoding: "utf8",
|
|
1098
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1099
|
+
});
|
|
1100
|
+
} catch {
|
|
1101
|
+
return null;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
function stageOid(vault, stage, path) {
|
|
1105
|
+
return git(vault, ["rev-parse", `:${stage}:${path}`]);
|
|
1106
|
+
}
|
|
1107
|
+
function unmergedPaths(vault) {
|
|
1108
|
+
const raw = git(vault, ["diff", "--name-only", "--diff-filter=U"]);
|
|
1109
|
+
return raw ? raw.split("\n").map((s) => s.trim()).filter(Boolean) : [];
|
|
1110
|
+
}
|
|
1111
|
+
function verifyOperation(vault, operationId2) {
|
|
1112
|
+
const path = journalPath(vault, operationId2);
|
|
1113
|
+
if (!path || !existsSync(path)) {
|
|
1114
|
+
return err("PREFLIGHT_FAILED", { reason: "missing-operation-journal", operation_id: operationId2 });
|
|
1115
|
+
}
|
|
1116
|
+
const fields = readJournalFields(path);
|
|
1117
|
+
if (fields.operation_id !== operationId2) {
|
|
1118
|
+
return err("PREFLIGHT_FAILED", { reason: "operation-id-mismatch", operation_id: operationId2 });
|
|
1119
|
+
}
|
|
1120
|
+
if (fields.handoff === "1") {
|
|
1121
|
+
return err("PREFLIGHT_FAILED", { reason: "operation-handoff", operation_id: operationId2 });
|
|
1122
|
+
}
|
|
1123
|
+
const phase = fields.phase ?? "";
|
|
1124
|
+
if (phase !== "rebasing" && phase !== "restoring") {
|
|
1125
|
+
return err("PREFLIGHT_FAILED", { reason: "operation-phase", phase, operation_id: operationId2 });
|
|
1126
|
+
}
|
|
1127
|
+
const currentGitDir = git(vault, ["rev-parse", "--absolute-git-dir"]);
|
|
1128
|
+
const journalGitDir = fields.worktree_git_dir ?? "";
|
|
1129
|
+
if (journalGitDir && currentGitDir && journalGitDir !== currentGitDir) {
|
|
1130
|
+
return err("PREFLIGHT_FAILED", { reason: "worktree-mismatch", operation_id: operationId2 });
|
|
1131
|
+
}
|
|
1132
|
+
if (!journalGitDir && unmergedPaths(vault).length === 0) {
|
|
1133
|
+
return err("PREFLIGHT_FAILED", { reason: "legacy-journal-without-unmerged", operation_id: operationId2 });
|
|
1134
|
+
}
|
|
1135
|
+
return ok({ phase });
|
|
1136
|
+
}
|
|
1137
|
+
async function resolveOne(vault, path, klass) {
|
|
1138
|
+
if (klass === "root-index") {
|
|
1139
|
+
const rendered = await renderRootIndex({ vault });
|
|
1140
|
+
if (!rendered.ok) return rendered;
|
|
1141
|
+
return ok(rendered.data.text);
|
|
1142
|
+
}
|
|
1143
|
+
if (klass === "taxonomy") {
|
|
1144
|
+
const base = showStage(vault, 1, path) ?? "";
|
|
1145
|
+
const ours = showStage(vault, 2, path);
|
|
1146
|
+
const theirs = showStage(vault, 3, path);
|
|
1147
|
+
if (ours === null || theirs === null) {
|
|
1148
|
+
return err("WRITE_FAILED", { path, message: "missing taxonomy stages" });
|
|
1149
|
+
}
|
|
1150
|
+
const merged = mergeTaxonomyConflict(base, ours, theirs);
|
|
1151
|
+
if (!merged.ok) return merged;
|
|
1152
|
+
return ok(merged.data.text);
|
|
1153
|
+
}
|
|
1154
|
+
if (klass === "log") {
|
|
1155
|
+
const base = showStage(vault, 1, path) ?? "";
|
|
1156
|
+
const ours = showStage(vault, 2, path) ?? "";
|
|
1157
|
+
const theirs = showStage(vault, 3, path) ?? "";
|
|
1158
|
+
const merged = mergeLogConflictStages({ base, ours, theirs });
|
|
1159
|
+
if (!merged.ok) return merged;
|
|
1160
|
+
return ok(merged.data.text);
|
|
1161
|
+
}
|
|
1162
|
+
if (klass === "project-index") {
|
|
1163
|
+
const slug = path.replace(/^projects\//, "").replace(/\/knowledge\.md$/, "");
|
|
1164
|
+
const rendered = await renderProjectIndex(vault, slug);
|
|
1165
|
+
if (!rendered.ok) return rendered;
|
|
1166
|
+
return ok(rendered.data.text);
|
|
1167
|
+
}
|
|
1168
|
+
return err("SCHEME_REJECTED", { path, reason: "unknown-derived-class" });
|
|
1169
|
+
}
|
|
1170
|
+
function rollbackPath(vault, path, stage2, stage3) {
|
|
1171
|
+
try {
|
|
1172
|
+
execFileSync2("git", ["checkout", "--conflict=merge", "--", path], {
|
|
1173
|
+
cwd: vault,
|
|
1174
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1175
|
+
});
|
|
1176
|
+
} catch {
|
|
1177
|
+
return false;
|
|
1178
|
+
}
|
|
1179
|
+
const s2 = stageOid(vault, 2, path);
|
|
1180
|
+
const s3 = stageOid(vault, 3, path);
|
|
1181
|
+
return s2 === stage2 && s3 === stage3;
|
|
1182
|
+
}
|
|
1183
|
+
function hasMarkers(path, text) {
|
|
1184
|
+
const lines = text.split(/\r?\n/);
|
|
1185
|
+
let open3 = false;
|
|
1186
|
+
let sep2 = false;
|
|
1187
|
+
for (const line of lines) {
|
|
1188
|
+
if (line.startsWith("<<<<<<< ")) {
|
|
1189
|
+
open3 = true;
|
|
1190
|
+
sep2 = false;
|
|
1191
|
+
continue;
|
|
1192
|
+
}
|
|
1193
|
+
if (line === "=======" && open3) {
|
|
1194
|
+
sep2 = true;
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
if (line.startsWith(">>>>>>> ") && open3 && sep2) return true;
|
|
1198
|
+
if (line.startsWith(">>>>>>> ")) {
|
|
1199
|
+
open3 = false;
|
|
1200
|
+
sep2 = false;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
return false;
|
|
1204
|
+
}
|
|
1205
|
+
async function runDerivedConflictResolution(input) {
|
|
1206
|
+
const vault = input.vault;
|
|
1207
|
+
const verified = verifyOperation(vault, input.operationId);
|
|
1208
|
+
if (!verified.ok) {
|
|
1209
|
+
return {
|
|
1210
|
+
exitCode: ExitCode.PREFLIGHT_FAILED,
|
|
1211
|
+
result: err("PREFLIGHT_FAILED", verified.detail)
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
const paths = unmergedPaths(vault);
|
|
1215
|
+
if (paths.length === 0) {
|
|
1216
|
+
return {
|
|
1217
|
+
exitCode: ExitCode.OK,
|
|
1218
|
+
result: ok({
|
|
1219
|
+
resolved: true,
|
|
1220
|
+
resolved_paths: [],
|
|
1221
|
+
unknown_paths: [],
|
|
1222
|
+
rolled_back: false,
|
|
1223
|
+
humanHint: "no unmerged paths"
|
|
1224
|
+
})
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
const classes = paths.map((p) => ({ path: p, klass: classifyDerivedPath(p) }));
|
|
1228
|
+
const unknown = classes.filter((c) => c.klass === "unknown").map((c) => c.path);
|
|
1229
|
+
if (unknown.length > 0) {
|
|
1230
|
+
return {
|
|
1231
|
+
exitCode: ExitCode.SYNC_PULL_FAILED,
|
|
1232
|
+
result: ok({
|
|
1233
|
+
resolved: false,
|
|
1234
|
+
resolved_paths: [],
|
|
1235
|
+
unknown_paths: unknown,
|
|
1236
|
+
rolled_back: false,
|
|
1237
|
+
humanHint: `review-required: unknown conflict paths ${unknown.join(", ")}`
|
|
1238
|
+
})
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
1242
|
+
for (const { path } of classes) {
|
|
1243
|
+
snapshots.set(path, {
|
|
1244
|
+
stage2: stageOid(vault, 2, path),
|
|
1245
|
+
stage3: stageOid(vault, 3, path)
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
const staged = [];
|
|
1249
|
+
try {
|
|
1250
|
+
for (const { path, klass } of classes) {
|
|
1251
|
+
const resolved = await resolveOne(vault, path, klass);
|
|
1252
|
+
if (!resolved.ok) throw new Error(`${path}: ${resolved.error}`);
|
|
1253
|
+
if (hasMarkers(path, resolved.data)) {
|
|
1254
|
+
throw new Error(`${path}: conflict markers remain after resolve`);
|
|
1255
|
+
}
|
|
1256
|
+
writeFileSync2(join9(vault, path), resolved.data, "utf8");
|
|
1257
|
+
execFileSync2("git", ["add", "--", path], { cwd: vault, stdio: ["pipe", "pipe", "pipe"] });
|
|
1258
|
+
staged.push(path);
|
|
1259
|
+
}
|
|
1260
|
+
const remaining = unmergedPaths(vault);
|
|
1261
|
+
if (remaining.length > 0) {
|
|
1262
|
+
throw new Error(`still unmerged: ${remaining.join(",")}`);
|
|
1263
|
+
}
|
|
1264
|
+
return {
|
|
1265
|
+
exitCode: ExitCode.OK,
|
|
1266
|
+
result: ok({
|
|
1267
|
+
resolved: true,
|
|
1268
|
+
resolved_paths: staged,
|
|
1269
|
+
unknown_paths: [],
|
|
1270
|
+
rolled_back: false,
|
|
1271
|
+
humanHint: `resolved ${staged.length} derived path(s)`
|
|
1272
|
+
})
|
|
1273
|
+
};
|
|
1274
|
+
} catch (error) {
|
|
1275
|
+
let rolled = true;
|
|
1276
|
+
for (const path of staged) {
|
|
1277
|
+
const snap = snapshots.get(path);
|
|
1278
|
+
if (!snap || !rollbackPath(vault, path, snap.stage2, snap.stage3)) {
|
|
1279
|
+
rolled = false;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
for (const { path } of classes) {
|
|
1283
|
+
if (staged.includes(path)) continue;
|
|
1284
|
+
const snap = snapshots.get(path);
|
|
1285
|
+
if (snap) rollbackPath(vault, path, snap.stage2, snap.stage3);
|
|
1286
|
+
}
|
|
1287
|
+
return {
|
|
1288
|
+
exitCode: ExitCode.SYNC_PULL_FAILED,
|
|
1289
|
+
result: ok({
|
|
1290
|
+
resolved: false,
|
|
1291
|
+
resolved_paths: [],
|
|
1292
|
+
unknown_paths: [],
|
|
1293
|
+
rolled_back: rolled,
|
|
1294
|
+
humanHint: `derived resolve failed: ${String(error)}`
|
|
1295
|
+
})
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// src/commands/log-materialize.ts
|
|
1301
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
1302
|
+
import { join as join11 } from "path";
|
|
1303
|
+
|
|
1304
|
+
// src/utils/log-events.ts
|
|
1305
|
+
import { mkdir as mkdir4, open, readFile as readFile4, readdir as readdir3 } from "fs/promises";
|
|
1306
|
+
import { join as join10 } from "path";
|
|
1307
|
+
function isPlainObject(value) {
|
|
1308
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1309
|
+
}
|
|
1310
|
+
function canonicalize(value) {
|
|
1311
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
1312
|
+
if (!isPlainObject(value)) return value;
|
|
1313
|
+
const out = {};
|
|
1314
|
+
for (const key of Object.keys(value).sort()) {
|
|
1315
|
+
out[key] = canonicalize(value[key]);
|
|
1316
|
+
}
|
|
1317
|
+
return out;
|
|
1318
|
+
}
|
|
1319
|
+
function eventPathFor(event) {
|
|
1320
|
+
const day2 = event.occurred_at.slice(0, 10);
|
|
1321
|
+
return `meta/log-events/${day2}/${event.operation_id}.json`;
|
|
1322
|
+
}
|
|
1323
|
+
function validateLogEvent(event) {
|
|
1324
|
+
if (event.schema !== "skillwiki-log-event/v1") {
|
|
1325
|
+
return err("SCHEME_REJECTED", { message: "invalid event schema" });
|
|
1326
|
+
}
|
|
1327
|
+
if (!/^[0-9a-f]{64}$/.test(event.operation_id)) {
|
|
1328
|
+
return err("SCHEME_REJECTED", { message: "operation_id must be 64 hex chars" });
|
|
1329
|
+
}
|
|
1330
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(event.occurred_at)) {
|
|
1331
|
+
return err("SCHEME_REJECTED", { message: "occurred_at must be UTC ISO with milliseconds" });
|
|
1332
|
+
}
|
|
1333
|
+
for (const field of ["host_id", "actor", "kind", "target", "note"]) {
|
|
1334
|
+
const v = event[field];
|
|
1335
|
+
if (typeof v !== "string" || v.trim().length === 0 || v.length > 500) {
|
|
1336
|
+
return err("SCHEME_REJECTED", { message: `invalid ${field}` });
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
if (!isPlainObject(event.metadata)) {
|
|
1340
|
+
return err("SCHEME_REJECTED", { message: "metadata must be a plain object" });
|
|
1341
|
+
}
|
|
1342
|
+
const sensitive = scanSensitiveContent(JSON.stringify(event));
|
|
1343
|
+
if (sensitive.length > 0) {
|
|
1344
|
+
return err("SENSITIVE_CONTENT_DETECTED", { findings: sensitive });
|
|
1345
|
+
}
|
|
1346
|
+
return ok({
|
|
1347
|
+
schema: "skillwiki-log-event/v1",
|
|
1348
|
+
operation_id: event.operation_id,
|
|
1349
|
+
occurred_at: event.occurred_at,
|
|
1350
|
+
host_id: event.host_id,
|
|
1351
|
+
actor: event.actor,
|
|
1352
|
+
kind: event.kind,
|
|
1353
|
+
target: event.target,
|
|
1354
|
+
note: event.note,
|
|
1355
|
+
metadata: canonicalize(event.metadata)
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
function canonicalEventJson(event) {
|
|
1359
|
+
const ordered = {
|
|
1360
|
+
schema: event.schema,
|
|
1361
|
+
operation_id: event.operation_id,
|
|
1362
|
+
occurred_at: event.occurred_at,
|
|
1363
|
+
host_id: event.host_id,
|
|
1364
|
+
actor: event.actor,
|
|
1365
|
+
kind: event.kind,
|
|
1366
|
+
target: event.target,
|
|
1367
|
+
note: event.note,
|
|
1368
|
+
metadata: event.metadata
|
|
1369
|
+
};
|
|
1370
|
+
return `${JSON.stringify(ordered, null, 2)}
|
|
1371
|
+
`;
|
|
1372
|
+
}
|
|
1373
|
+
async function writeLogEvent(vault, event) {
|
|
1374
|
+
const validated = validateLogEvent(event);
|
|
1375
|
+
if (!validated.ok) return validated;
|
|
1376
|
+
const rel = eventPathFor(validated.data);
|
|
1377
|
+
const abs = join10(vault, rel);
|
|
1378
|
+
await mkdir4(join10(vault, "meta", "log-events", validated.data.occurred_at.slice(0, 10)), {
|
|
1379
|
+
recursive: true
|
|
1380
|
+
});
|
|
1381
|
+
const body = canonicalEventJson(validated.data);
|
|
1382
|
+
try {
|
|
1383
|
+
const handle = await open(abs, "wx");
|
|
1384
|
+
try {
|
|
1385
|
+
await handle.writeFile(body, "utf8");
|
|
1386
|
+
} finally {
|
|
1387
|
+
await handle.close();
|
|
1388
|
+
}
|
|
1389
|
+
return ok({ path: rel, created: true });
|
|
1390
|
+
} catch (error) {
|
|
1391
|
+
if (error.code === "EEXIST") {
|
|
1392
|
+
let existing;
|
|
1393
|
+
try {
|
|
1394
|
+
existing = await readFile4(abs, "utf8");
|
|
1395
|
+
} catch (readErr) {
|
|
1396
|
+
return err("WRITE_FAILED", { path: rel, message: String(readErr) });
|
|
1397
|
+
}
|
|
1398
|
+
if (existing === body) return ok({ path: rel, created: false });
|
|
1399
|
+
return err("EVENT_IDENTITY_COLLISION", { path: rel });
|
|
1400
|
+
}
|
|
1401
|
+
return err("WRITE_FAILED", { path: rel, message: String(error) });
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
async function readLogEvents(vault) {
|
|
1405
|
+
const root = join10(vault, "meta", "log-events");
|
|
1406
|
+
let days;
|
|
1407
|
+
try {
|
|
1408
|
+
days = (await readdir3(root, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
1409
|
+
} catch {
|
|
1410
|
+
return ok([]);
|
|
1411
|
+
}
|
|
1412
|
+
const events = [];
|
|
1413
|
+
for (const day2 of days) {
|
|
1414
|
+
const files = (await readdir3(join10(root, day2))).filter((f) => f.endsWith(".json")).sort();
|
|
1415
|
+
for (const file of files) {
|
|
1416
|
+
const text = await readFile4(join10(root, day2, file), "utf8");
|
|
1417
|
+
let parsed;
|
|
1418
|
+
try {
|
|
1419
|
+
parsed = JSON.parse(text);
|
|
1420
|
+
} catch {
|
|
1421
|
+
return err("SCHEME_REJECTED", { path: `meta/log-events/${day2}/${file}`, message: "invalid JSON" });
|
|
1422
|
+
}
|
|
1423
|
+
const validated = validateLogEvent(parsed);
|
|
1424
|
+
if (!validated.ok) return validated;
|
|
1425
|
+
if (eventPathFor(validated.data) !== `meta/log-events/${day2}/${file}`) {
|
|
1426
|
+
return err("SCHEME_REJECTED", {
|
|
1427
|
+
path: `meta/log-events/${day2}/${file}`,
|
|
1428
|
+
message: "path/identity mismatch"
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
events.push(validated.data);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
events.sort((a, b) => {
|
|
1435
|
+
if (a.occurred_at !== b.occurred_at) return a.occurred_at < b.occurred_at ? -1 : 1;
|
|
1436
|
+
return a.operation_id < b.operation_id ? -1 : a.operation_id > b.operation_id ? 1 : 0;
|
|
1437
|
+
});
|
|
1438
|
+
return ok(events);
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// src/utils/log-projection.ts
|
|
1442
|
+
function day(event) {
|
|
1443
|
+
return event.occurred_at.slice(0, 10);
|
|
1444
|
+
}
|
|
1445
|
+
function marker(event) {
|
|
1446
|
+
return `<!-- skillwiki-log-event:${event.operation_id} -->`;
|
|
1447
|
+
}
|
|
1448
|
+
function bodyFor(event) {
|
|
1449
|
+
if (event.kind === "page-publish") {
|
|
1450
|
+
const tax = Array.isArray(event.metadata.taxonomy_added) ? event.metadata.taxonomy_added : [];
|
|
1451
|
+
return [
|
|
1452
|
+
`- Published: [[${event.target.replace(/\.md$/, "")}]]`,
|
|
1453
|
+
`- Taxonomy: ${tax.length > 0 ? `added ${tax.join(", ")}` : "no additions"}`,
|
|
1454
|
+
event.note ? `- Note: ${event.note}` : ""
|
|
1455
|
+
].filter(Boolean).join("\n");
|
|
1456
|
+
}
|
|
1457
|
+
if (event.kind === "legacy-log-entry" && typeof event.metadata.legacy_markdown === "string") {
|
|
1458
|
+
const legacy = event.metadata.legacy_markdown.trimEnd();
|
|
1459
|
+
if (legacy.includes(marker(event))) return legacy;
|
|
1460
|
+
return `${legacy}
|
|
1461
|
+
${marker(event)}`;
|
|
1462
|
+
}
|
|
1463
|
+
if (event.kind === "session-brief" || event.kind === "archive" || event.kind === "remove" || event.kind === "log-entry") {
|
|
1464
|
+
return [
|
|
1465
|
+
`- Target: ${event.target}`,
|
|
1466
|
+
`- Note: ${event.note}`,
|
|
1467
|
+
`- Actor: ${event.actor}`,
|
|
1468
|
+
`- Host: ${event.host_id}`
|
|
1469
|
+
].join("\n");
|
|
1470
|
+
}
|
|
1471
|
+
return [
|
|
1472
|
+
`- Event kind: ${event.kind}`,
|
|
1473
|
+
`- Actor: ${event.actor}`,
|
|
1474
|
+
`- Host: ${event.host_id}`,
|
|
1475
|
+
`- Target: ${event.target}`,
|
|
1476
|
+
`- Note: ${event.note}`,
|
|
1477
|
+
`- Metadata: ${JSON.stringify(event.metadata)}`
|
|
1478
|
+
].join("\n");
|
|
1479
|
+
}
|
|
1480
|
+
function renderLogProjection(events) {
|
|
1481
|
+
const ordered = [...events].sort((a, b) => {
|
|
1482
|
+
if (a.occurred_at !== b.occurred_at) return a.occurred_at < b.occurred_at ? -1 : 1;
|
|
1483
|
+
return a.operation_id < b.operation_id ? -1 : a.operation_id > b.operation_id ? 1 : 0;
|
|
1484
|
+
});
|
|
1485
|
+
const lines = [
|
|
1486
|
+
"# Vault Log",
|
|
1487
|
+
"",
|
|
1488
|
+
"Generated by `skillwiki log materialize` from immutable events.",
|
|
1489
|
+
""
|
|
1490
|
+
];
|
|
1491
|
+
for (const event of ordered) {
|
|
1492
|
+
if (event.kind === "legacy-log-entry" && typeof event.metadata.legacy_markdown === "string") {
|
|
1493
|
+
const block = bodyFor(event);
|
|
1494
|
+
lines.push(block.endsWith("\n") ? block.trimEnd() : block, "");
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
lines.push(`## [${day(event)}] ${event.kind} | ${event.target}`, "", bodyFor(event), marker(event), "");
|
|
1498
|
+
}
|
|
1499
|
+
return `${lines.join("\n").replace(/\n+$/, "")}
|
|
1500
|
+
`;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
// src/utils/projection-authority.ts
|
|
1504
|
+
function resolveProjectionAuthority(load) {
|
|
1505
|
+
if (!load) {
|
|
1506
|
+
return ok({ authority_host_id: "standalone", current_host_id: "standalone", can_write: true });
|
|
1507
|
+
}
|
|
1508
|
+
const snapshotters = Object.entries(load.manifest.hosts).filter(([, h]) => h.role === "snapshotter").map(([id]) => id);
|
|
1509
|
+
const configured = load.manifest.projection_authority;
|
|
1510
|
+
let authority = configured;
|
|
1511
|
+
if (!authority) {
|
|
1512
|
+
if (snapshotters.length === 1) authority = snapshotters[0];
|
|
1513
|
+
else {
|
|
1514
|
+
return err("PREFLIGHT_FAILED", {
|
|
1515
|
+
reason: "projection-authority-unresolved",
|
|
1516
|
+
snapshotters
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
if (!load.manifest.hosts[authority]) {
|
|
1521
|
+
return err("PREFLIGHT_FAILED", { reason: "projection-authority-unknown-host", authority });
|
|
1522
|
+
}
|
|
1523
|
+
if (load.identityStatus !== "known" || !load.hostId) {
|
|
1524
|
+
return err("PREFLIGHT_FAILED", {
|
|
1525
|
+
reason: "fleet-identity-unresolved",
|
|
1526
|
+
identity_status: load.identityStatus
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
return ok({
|
|
1530
|
+
authority_host_id: authority,
|
|
1531
|
+
current_host_id: load.hostId,
|
|
1532
|
+
can_write: load.hostId === authority
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// src/commands/log-materialize.ts
|
|
1537
|
+
async function runLogMaterialize(input) {
|
|
1538
|
+
const events = await readLogEvents(input.vault);
|
|
1539
|
+
if (!events.ok) {
|
|
1540
|
+
return { exitCode: ExitCode.SCHEME_REJECTED, result: events };
|
|
1541
|
+
}
|
|
1542
|
+
const text = renderLogProjection(events.data);
|
|
1543
|
+
let current = "";
|
|
1544
|
+
try {
|
|
1545
|
+
current = readFileSync6(join11(input.vault, "log.md"), "utf8");
|
|
1546
|
+
} catch {
|
|
1547
|
+
current = "";
|
|
1548
|
+
}
|
|
1549
|
+
const changed = current !== text;
|
|
1550
|
+
if (!input.write) {
|
|
1551
|
+
return {
|
|
1552
|
+
exitCode: ExitCode.OK,
|
|
1553
|
+
result: ok({
|
|
1554
|
+
changed,
|
|
1555
|
+
dry_run: true,
|
|
1556
|
+
event_count: events.data.length,
|
|
1557
|
+
humanHint: changed ? `dry run: would rewrite log.md (${events.data.length} events)` : `dry run: log.md already canonical (${events.data.length} events)`
|
|
1558
|
+
})
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
if (!input.skipAuthority) {
|
|
1562
|
+
const fleet = await loadFleetManifestAndHost({ vault: input.vault, hostId: input.hostId });
|
|
1563
|
+
const auth = resolveProjectionAuthority(fleet);
|
|
1564
|
+
if (!auth.ok) return { exitCode: ExitCode.PREFLIGHT_FAILED, result: auth };
|
|
1565
|
+
if (!auth.data.can_write) {
|
|
1566
|
+
return {
|
|
1567
|
+
exitCode: ExitCode.PREFLIGHT_FAILED,
|
|
1568
|
+
result: err("PREFLIGHT_FAILED", {
|
|
1569
|
+
reason: "projection-authority",
|
|
1570
|
+
authority_host_id: auth.data.authority_host_id,
|
|
1571
|
+
current_host_id: auth.data.current_host_id
|
|
1572
|
+
})
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
const guard = await guardProtectedVaultWrite({ vault: input.vault, command: "log materialize" });
|
|
1576
|
+
if (guard.blocked) return { exitCode: guard.exitCode, result: guard.result };
|
|
1577
|
+
}
|
|
1578
|
+
if (!changed) {
|
|
1579
|
+
return {
|
|
1580
|
+
exitCode: ExitCode.OK,
|
|
1581
|
+
result: ok({
|
|
1582
|
+
changed: false,
|
|
1583
|
+
dry_run: false,
|
|
1584
|
+
event_count: events.data.length,
|
|
1585
|
+
humanHint: `log.md already canonical (${events.data.length} events)`
|
|
1586
|
+
})
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
const written = await atomicWriteText(join11(input.vault, "log.md"), text);
|
|
1590
|
+
if (!written.ok) return { exitCode: ExitCode.WRITE_FAILED, result: written };
|
|
1591
|
+
const installed = readFileSync6(join11(input.vault, "log.md"), "utf8");
|
|
1592
|
+
if (installed !== text) {
|
|
1593
|
+
return {
|
|
1594
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
1595
|
+
result: err("WRITE_FAILED", { message: "installed log.md differs from projection" })
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
return {
|
|
1599
|
+
exitCode: ExitCode.OK,
|
|
1600
|
+
result: ok({
|
|
1601
|
+
changed: true,
|
|
1602
|
+
dry_run: false,
|
|
1603
|
+
event_count: events.data.length,
|
|
1604
|
+
humanHint: `rewrote log.md (${events.data.length} events)`
|
|
1605
|
+
})
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// src/commands/log-migrate-legacy.ts
|
|
1610
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
1611
|
+
import { join as join12 } from "path";
|
|
1612
|
+
|
|
1613
|
+
// src/utils/operation-id.ts
|
|
1614
|
+
import { createHash as createHash2 } from "crypto";
|
|
1615
|
+
function operationId(namespace, parts) {
|
|
1616
|
+
const hash = createHash2("sha256").update(namespace).update("\0");
|
|
1617
|
+
for (const part of parts) hash.update(part).update("\0");
|
|
1618
|
+
return hash.digest("hex");
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
// src/commands/log-migrate-legacy.ts
|
|
1622
|
+
var BLOCK_RE = /^## \[(\d{4}-\d{2}-\d{2})\][^\n]*$/gm;
|
|
1623
|
+
var PUBLISH_RE = /<!--\s*skillwiki-page-publish:([a-f0-9]{64})\s*-->/i;
|
|
1624
|
+
function splitLegacyBlocks(text) {
|
|
1625
|
+
const matches = [...text.matchAll(BLOCK_RE)];
|
|
1626
|
+
if (matches.length === 0) return [];
|
|
1627
|
+
const out = [];
|
|
1628
|
+
for (let i = 0; i < matches.length; i++) {
|
|
1629
|
+
const m = matches[i];
|
|
1630
|
+
const start = m.index ?? 0;
|
|
1631
|
+
const end = i + 1 < matches.length ? matches[i + 1].index ?? text.length : text.length;
|
|
1632
|
+
const block = text.slice(start, end).replace(/\s+$/, "") + "\n";
|
|
1633
|
+
out.push({ date: m[1], block, ordinal: i });
|
|
1634
|
+
}
|
|
1635
|
+
return out;
|
|
1636
|
+
}
|
|
1637
|
+
async function runLogMigrateLegacy(input) {
|
|
1638
|
+
let text = "";
|
|
1639
|
+
try {
|
|
1640
|
+
text = readFileSync7(join12(input.vault, "log.md"), "utf8");
|
|
1641
|
+
} catch {
|
|
1642
|
+
return {
|
|
1643
|
+
exitCode: ExitCode.OK,
|
|
1644
|
+
result: ok({
|
|
1645
|
+
planned: 0,
|
|
1646
|
+
created: 0,
|
|
1647
|
+
already_covered: 0,
|
|
1648
|
+
dry_run: !input.write,
|
|
1649
|
+
humanHint: "no root log.md to migrate"
|
|
1650
|
+
})
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
const blocks = splitLegacyBlocks(text);
|
|
1654
|
+
let created = 0;
|
|
1655
|
+
let already = 0;
|
|
1656
|
+
for (const b of blocks) {
|
|
1657
|
+
const publish = PUBLISH_RE.exec(b.block)?.[1]?.toLowerCase();
|
|
1658
|
+
const op = publish && /^[0-9a-f]{64}$/.test(publish) ? publish : operationId("skillwiki-legacy-log-v1", [b.date, String(b.ordinal), b.block]);
|
|
1659
|
+
const heading = b.block.split("\n")[0] ?? `## [${b.date}] legacy`;
|
|
1660
|
+
const event = {
|
|
1661
|
+
schema: "skillwiki-log-event/v1",
|
|
1662
|
+
operation_id: op,
|
|
1663
|
+
occurred_at: `${b.date}T00:00:00.000Z`,
|
|
1664
|
+
host_id: input.hostId ?? "standalone",
|
|
1665
|
+
actor: "skillwiki-cli",
|
|
1666
|
+
kind: "legacy-log-entry",
|
|
1667
|
+
target: "log.md",
|
|
1668
|
+
note: heading,
|
|
1669
|
+
metadata: { legacy_markdown: b.block }
|
|
1670
|
+
};
|
|
1671
|
+
if (!input.write) continue;
|
|
1672
|
+
const wrote = await writeLogEvent(input.vault, event);
|
|
1673
|
+
if (!wrote.ok) {
|
|
1674
|
+
if (wrote.error === "EVENT_IDENTITY_COLLISION") {
|
|
1675
|
+
already += 1;
|
|
1676
|
+
continue;
|
|
1677
|
+
}
|
|
1678
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: wrote };
|
|
1679
|
+
}
|
|
1680
|
+
if (wrote.data.created) created += 1;
|
|
1681
|
+
else already += 1;
|
|
1682
|
+
}
|
|
1683
|
+
return {
|
|
1684
|
+
exitCode: ExitCode.OK,
|
|
1685
|
+
result: ok({
|
|
1686
|
+
planned: blocks.length,
|
|
1687
|
+
created: input.write ? created : 0,
|
|
1688
|
+
already_covered: input.write ? already : 0,
|
|
1689
|
+
dry_run: !input.write,
|
|
1690
|
+
humanHint: input.write ? `migrated legacy log: created=${created} already=${already} planned=${blocks.length}` : `dry run: would migrate ${blocks.length} legacy log block(s)`
|
|
1691
|
+
})
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// src/commands/projections-materialize.ts
|
|
1696
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
1697
|
+
import { join as join13 } from "path";
|
|
1698
|
+
var defaultDeps = {
|
|
1699
|
+
writeText: (path, text) => atomicWriteText(path, text)
|
|
1700
|
+
};
|
|
1701
|
+
async function runProjectionsMaterialize(input, deps = defaultDeps) {
|
|
1702
|
+
const fleet = await loadFleetManifestAndHost({ vault: input.vault, hostId: input.hostId });
|
|
1703
|
+
const auth = resolveProjectionAuthority(fleet);
|
|
1704
|
+
if (!auth.ok) return { exitCode: ExitCode.PREFLIGHT_FAILED, result: auth };
|
|
1705
|
+
const indexProj = await renderRootIndex({ vault: input.vault });
|
|
1706
|
+
if (!indexProj.ok) return { exitCode: ExitCode.SCHEME_REJECTED, result: indexProj };
|
|
1707
|
+
const events = await readLogEvents(input.vault);
|
|
1708
|
+
if (!events.ok) return { exitCode: ExitCode.SCHEME_REJECTED, result: events };
|
|
1709
|
+
const logText = renderLogProjection(events.data);
|
|
1710
|
+
let curIndex = "";
|
|
1711
|
+
let curLog = "";
|
|
1712
|
+
try {
|
|
1713
|
+
curIndex = readFileSync8(join13(input.vault, "index.md"), "utf8");
|
|
1714
|
+
} catch {
|
|
1715
|
+
}
|
|
1716
|
+
try {
|
|
1717
|
+
curLog = readFileSync8(join13(input.vault, "log.md"), "utf8");
|
|
1718
|
+
} catch {
|
|
1719
|
+
}
|
|
1720
|
+
const indexDrift = curIndex !== indexProj.data.text;
|
|
1721
|
+
const logDrift = curLog !== logText;
|
|
1722
|
+
if (!input.write) {
|
|
1723
|
+
return {
|
|
1724
|
+
exitCode: ExitCode.OK,
|
|
1725
|
+
result: ok({
|
|
1726
|
+
authority_host_id: auth.data.authority_host_id,
|
|
1727
|
+
current_host_id: auth.data.current_host_id,
|
|
1728
|
+
can_write: auth.data.can_write,
|
|
1729
|
+
index_changed: false,
|
|
1730
|
+
log_changed: false,
|
|
1731
|
+
index_drift: indexDrift,
|
|
1732
|
+
log_drift: logDrift,
|
|
1733
|
+
rolled_back: false,
|
|
1734
|
+
dry_run: true,
|
|
1735
|
+
humanHint: `dry run: index_drift=${indexDrift} log_drift=${logDrift} can_write=${auth.data.can_write}`
|
|
1736
|
+
})
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
if (!auth.data.can_write) {
|
|
1740
|
+
return {
|
|
1741
|
+
exitCode: ExitCode.PREFLIGHT_FAILED,
|
|
1742
|
+
result: err("PREFLIGHT_FAILED", {
|
|
1743
|
+
reason: "projection-authority",
|
|
1744
|
+
authority_host_id: auth.data.authority_host_id,
|
|
1745
|
+
current_host_id: auth.data.current_host_id,
|
|
1746
|
+
index_drift: indexDrift,
|
|
1747
|
+
log_drift: logDrift
|
|
1748
|
+
})
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
const indexPath = join13(input.vault, "index.md");
|
|
1752
|
+
const logPath = join13(input.vault, "log.md");
|
|
1753
|
+
const indexWrite = await deps.writeText(indexPath, indexProj.data.text);
|
|
1754
|
+
if (!indexWrite.ok) return { exitCode: ExitCode.WRITE_FAILED, result: indexWrite };
|
|
1755
|
+
const logWrite = await deps.writeText(logPath, logText);
|
|
1756
|
+
if (!logWrite.ok) {
|
|
1757
|
+
await deps.writeText(indexPath, curIndex);
|
|
1758
|
+
return {
|
|
1759
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
1760
|
+
result: ok({
|
|
1761
|
+
authority_host_id: auth.data.authority_host_id,
|
|
1762
|
+
current_host_id: auth.data.current_host_id,
|
|
1763
|
+
can_write: true,
|
|
1764
|
+
index_changed: false,
|
|
1765
|
+
log_changed: false,
|
|
1766
|
+
index_drift: indexDrift,
|
|
1767
|
+
log_drift: logDrift,
|
|
1768
|
+
rolled_back: true,
|
|
1769
|
+
dry_run: false,
|
|
1770
|
+
humanHint: "projection materialize rolled back after log write failure"
|
|
1771
|
+
})
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
return {
|
|
1775
|
+
exitCode: ExitCode.OK,
|
|
1776
|
+
result: ok({
|
|
1777
|
+
authority_host_id: auth.data.authority_host_id,
|
|
1778
|
+
current_host_id: auth.data.current_host_id,
|
|
1779
|
+
can_write: true,
|
|
1780
|
+
index_changed: indexWrite.data.changed,
|
|
1781
|
+
log_changed: logWrite.data.changed,
|
|
1782
|
+
index_drift: indexDrift,
|
|
1783
|
+
log_drift: logDrift,
|
|
1784
|
+
rolled_back: false,
|
|
1785
|
+
dry_run: false,
|
|
1786
|
+
humanHint: `materialized projections index_changed=${indexWrite.data.changed} log_changed=${logWrite.data.changed}`
|
|
1787
|
+
})
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
|
|
773
1791
|
// src/commands/claim.ts
|
|
774
|
-
import { mkdir as
|
|
775
|
-
import { existsSync, statSync } from "fs";
|
|
776
|
-
import { join as
|
|
1792
|
+
import { mkdir as mkdir5, writeFile as writeFile3, readFile as readFile5 } from "fs/promises";
|
|
1793
|
+
import { existsSync as existsSync2, statSync } from "fs";
|
|
1794
|
+
import { join as join14 } from "path";
|
|
777
1795
|
function extractDate(filename) {
|
|
778
1796
|
const m = filename.match(/^(\d{4}-\d{2}-\d{2})/);
|
|
779
1797
|
return m?.[1] ?? "";
|
|
@@ -785,14 +1803,14 @@ function extractProjectSlug(projectField) {
|
|
|
785
1803
|
return projectField.replace(/^\[\[/, "").replace(/\]\]$/, "").replace(/^"|"$/g, "");
|
|
786
1804
|
}
|
|
787
1805
|
async function runClaim(input) {
|
|
788
|
-
if (!
|
|
1806
|
+
if (!existsSync2(input.vault) || !statSync(input.vault).isDirectory()) {
|
|
789
1807
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { path: input.vault }) };
|
|
790
1808
|
}
|
|
791
|
-
const absTranscript =
|
|
792
|
-
if (!
|
|
1809
|
+
const absTranscript = join14(input.vault, input.transcript);
|
|
1810
|
+
if (!existsSync2(absTranscript)) {
|
|
793
1811
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: input.transcript }) };
|
|
794
1812
|
}
|
|
795
|
-
const content = await
|
|
1813
|
+
const content = await readFile5(absTranscript, "utf8");
|
|
796
1814
|
const fm = extractFrontmatter(content);
|
|
797
1815
|
let projectSlug = input.project;
|
|
798
1816
|
if (!projectSlug && fm.ok && typeof fm.data.project === "string") {
|
|
@@ -804,8 +1822,8 @@ async function runClaim(input) {
|
|
|
804
1822
|
result: err("SCHEME_REJECTED", { message: "No project specified. Use --project or set project in transcript frontmatter." })
|
|
805
1823
|
};
|
|
806
1824
|
}
|
|
807
|
-
const projectDir =
|
|
808
|
-
if (!
|
|
1825
|
+
const projectDir = join14(input.vault, "projects", projectSlug);
|
|
1826
|
+
if (!existsSync2(projectDir) || !statSync(projectDir).isDirectory()) {
|
|
809
1827
|
return {
|
|
810
1828
|
exitCode: ExitCode.PROJECT_NOT_FOUND,
|
|
811
1829
|
result: err("PROJECT_NOT_FOUND", { project: projectSlug })
|
|
@@ -821,10 +1839,10 @@ async function runClaim(input) {
|
|
|
821
1839
|
}
|
|
822
1840
|
const workSlug = input.slug || extractSlugFromFilename(filename);
|
|
823
1841
|
const dirName = `${date}-${workSlug}`;
|
|
824
|
-
const workDir =
|
|
1842
|
+
const workDir = join14(projectDir, "work", dirName);
|
|
825
1843
|
const relWorkDir = `projects/${projectSlug}/work/${dirName}`;
|
|
826
1844
|
const relSpecPath = `${relWorkDir}/spec.md`;
|
|
827
|
-
if (
|
|
1845
|
+
if (existsSync2(workDir)) {
|
|
828
1846
|
return {
|
|
829
1847
|
exitCode: ExitCode.OK,
|
|
830
1848
|
result: ok({
|
|
@@ -835,7 +1853,7 @@ async function runClaim(input) {
|
|
|
835
1853
|
})
|
|
836
1854
|
};
|
|
837
1855
|
}
|
|
838
|
-
await
|
|
1856
|
+
await mkdir5(workDir, { recursive: true });
|
|
839
1857
|
const kind = fm.ok && typeof fm.data.kind === "string" ? fm.data.kind : "task";
|
|
840
1858
|
const specLines = [
|
|
841
1859
|
"---",
|
|
@@ -850,7 +1868,7 @@ async function runClaim(input) {
|
|
|
850
1868
|
`Claimed from ${input.transcript}`,
|
|
851
1869
|
""
|
|
852
1870
|
];
|
|
853
|
-
await writeFile3(
|
|
1871
|
+
await writeFile3(join14(workDir, "spec.md"), specLines.join("\n"), "utf8");
|
|
854
1872
|
appendLastOp(input.vault, {
|
|
855
1873
|
operation: "claim",
|
|
856
1874
|
summary: `claimed ${input.transcript} \u2192 ${relWorkDir}`,
|
|
@@ -869,8 +1887,8 @@ async function runClaim(input) {
|
|
|
869
1887
|
}
|
|
870
1888
|
|
|
871
1889
|
// src/commands/health.ts
|
|
872
|
-
import { existsSync as
|
|
873
|
-
import { dirname as dirname4, join as
|
|
1890
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync9, renameSync, writeFileSync as writeFileSync3 } from "fs";
|
|
1891
|
+
import { dirname as dirname4, join as join15, resolve as resolve2 } from "path";
|
|
874
1892
|
import { platform } from "os";
|
|
875
1893
|
function statusFromCounts(counts) {
|
|
876
1894
|
if ((counts.error ?? 0) > 0) return "error";
|
|
@@ -981,8 +1999,8 @@ function summarizeChecks(checks) {
|
|
|
981
1999
|
};
|
|
982
2000
|
}
|
|
983
2001
|
function classifyLog(path, id, label, okPattern) {
|
|
984
|
-
if (!
|
|
985
|
-
const lines =
|
|
2002
|
+
if (!existsSync3(path)) return { id, label, status: "warn", detail: `log file missing: ${path}` };
|
|
2003
|
+
const lines = readFileSync9(path, "utf8").split(/\r?\n/).filter(Boolean);
|
|
986
2004
|
if (lines.length === 0) return { id, label, status: "warn", detail: `log file empty: ${path}` };
|
|
987
2005
|
const statusLine = [...lines].reverse().find(
|
|
988
2006
|
(line) => /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z /.test(line) && (okPattern.test(line) || /\bFAIL\b|\bERROR\b/i.test(line))
|
|
@@ -1003,12 +2021,12 @@ function runVaultSyncHealth(home, syncMode) {
|
|
|
1003
2021
|
};
|
|
1004
2022
|
}
|
|
1005
2023
|
const isMac = platform() === "darwin";
|
|
1006
|
-
const shareDir = isMac ?
|
|
1007
|
-
const logDir = isMac ?
|
|
1008
|
-
const filterPath =
|
|
2024
|
+
const shareDir = isMac ? join15(home, "Library", "Application Support", "vault-sync", "bin") : join15(home, ".local", "share", "vault-sync", "bin");
|
|
2025
|
+
const logDir = isMac ? join15(home, "Library", "Logs") : join15(home, ".local", "state", "vault-sync", "log");
|
|
2026
|
+
const filterPath = join15(home, ".config", "rclone", "wiki-push-filters.txt");
|
|
1009
2027
|
const checks = [];
|
|
1010
|
-
const pushScript =
|
|
1011
|
-
if (syncMode === "optional" && !
|
|
2028
|
+
const pushScript = join15(shareDir, "wiki-push.sh");
|
|
2029
|
+
if (syncMode === "optional" && !existsSync3(pushScript)) {
|
|
1012
2030
|
return {
|
|
1013
2031
|
status: "pass",
|
|
1014
2032
|
blocking: false,
|
|
@@ -1021,26 +2039,26 @@ function runVaultSyncHealth(home, syncMode) {
|
|
|
1021
2039
|
}]
|
|
1022
2040
|
};
|
|
1023
2041
|
}
|
|
1024
|
-
checks.push(
|
|
2042
|
+
checks.push(existsSync3(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}` });
|
|
1025
2043
|
if (isMac) {
|
|
1026
|
-
const pushPlist =
|
|
1027
|
-
const fetchPlist =
|
|
1028
|
-
checks.push(
|
|
2044
|
+
const pushPlist = join15(home, "Library", "LaunchAgents", "com.karlchow.wiki-push.plist");
|
|
2045
|
+
const fetchPlist = join15(home, "Library", "LaunchAgents", "com.karlchow.wiki-fetch.plist");
|
|
2046
|
+
checks.push(existsSync3(pushPlist) && existsSync3(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)" });
|
|
1029
2047
|
checks.push({ id: "vault_sync_fuse_refresh_job", label: "Vault sync fuse refresh job", status: "pass", detail: "macOS host \u2014 check skipped" });
|
|
1030
2048
|
} else {
|
|
1031
|
-
const pushTimer =
|
|
1032
|
-
const fetchTimer =
|
|
1033
|
-
const fuseTimer =
|
|
1034
|
-
const fuseService =
|
|
1035
|
-
checks.push(
|
|
1036
|
-
checks.push(
|
|
1037
|
-
}
|
|
1038
|
-
checks.push(classifyLog(
|
|
1039
|
-
checks.push(classifyLog(
|
|
1040
|
-
if (!
|
|
2049
|
+
const pushTimer = join15(home, ".config", "systemd", "user", "wiki-push.timer");
|
|
2050
|
+
const fetchTimer = join15(home, ".config", "systemd", "user", "wiki-fetch.timer");
|
|
2051
|
+
const fuseTimer = join15(home, ".config", "systemd", "user", "wiki-fuse-refresh.timer");
|
|
2052
|
+
const fuseService = join15(home, ".config", "systemd", "user", "wiki-fuse-refresh.service");
|
|
2053
|
+
checks.push(existsSync3(pushTimer) && existsSync3(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)" });
|
|
2054
|
+
checks.push(existsSync3(fuseTimer) && existsSync3(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)" });
|
|
2055
|
+
}
|
|
2056
|
+
checks.push(classifyLog(join15(logDir, "wiki-push.log"), "vault_sync_last_push_age", "Vault sync last push recency", /OK push/));
|
|
2057
|
+
checks.push(classifyLog(join15(logDir, "wiki-fetch.log"), "vault_sync_last_fetch_status", "Vault sync last fetch status", /NOTIFY|OK behind|OK/));
|
|
2058
|
+
if (!existsSync3(filterPath)) {
|
|
1041
2059
|
checks.push({ id: "vault_sync_filter_present", label: "Vault sync filter file present", status: "error", detail: `Filter missing: ${filterPath}` });
|
|
1042
2060
|
} else {
|
|
1043
|
-
const content =
|
|
2061
|
+
const content = readFileSync9(filterPath, "utf8");
|
|
1044
2062
|
const missing = [
|
|
1045
2063
|
"remotely-save/data.json",
|
|
1046
2064
|
".skillwiki/sync.lock",
|
|
@@ -1135,7 +2153,7 @@ function buildHumanHint(report) {
|
|
|
1135
2153
|
function writeReport(out, report) {
|
|
1136
2154
|
mkdirSync(dirname4(out), { recursive: true });
|
|
1137
2155
|
const tmp = `${out}.tmp-${process.pid}-${Date.now()}`;
|
|
1138
|
-
|
|
2156
|
+
writeFileSync3(tmp, JSON.stringify(ok(report), null, 2) + "\n", "utf8");
|
|
1139
2157
|
renameSync(tmp, out);
|
|
1140
2158
|
}
|
|
1141
2159
|
function buildIncompleteReport(input, syncMode) {
|
|
@@ -1359,12 +2377,12 @@ async function runHealth(input) {
|
|
|
1359
2377
|
}
|
|
1360
2378
|
|
|
1361
2379
|
// src/commands/archive.ts
|
|
1362
|
-
import { rename as rename2, mkdir as
|
|
1363
|
-
import { join as
|
|
2380
|
+
import { rename as rename2, mkdir as mkdir7, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
|
|
2381
|
+
import { join as join17, dirname as dirname5 } from "path";
|
|
1364
2382
|
|
|
1365
2383
|
// src/utils/delete-intent.ts
|
|
1366
|
-
import { mkdir as
|
|
1367
|
-
import { join as
|
|
2384
|
+
import { mkdir as mkdir6, writeFile as writeFile4, readdir as readdir4, readFile as readFile6 } from "fs/promises";
|
|
2385
|
+
import { join as join16 } from "path";
|
|
1368
2386
|
var DELETE_INTENT_SCHEMA = "vault-delete-intent/v1";
|
|
1369
2387
|
var DELETE_INTENT_DIR = "meta/delete-intents";
|
|
1370
2388
|
function normalizeVaultRelPath(path) {
|
|
@@ -1395,10 +2413,10 @@ function buildDeleteIntent(input) {
|
|
|
1395
2413
|
};
|
|
1396
2414
|
}
|
|
1397
2415
|
async function writeDeleteIntent(vault, intent) {
|
|
1398
|
-
const dir =
|
|
1399
|
-
await
|
|
2416
|
+
const dir = join16(vault, DELETE_INTENT_DIR);
|
|
2417
|
+
await mkdir6(dir, { recursive: true });
|
|
1400
2418
|
const rel = `${DELETE_INTENT_DIR}/${pathToIntentFilename(intent.path)}`;
|
|
1401
|
-
await writeFile4(
|
|
2419
|
+
await writeFile4(join16(vault, rel), JSON.stringify(intent, null, 2) + "\n", "utf8");
|
|
1402
2420
|
return rel;
|
|
1403
2421
|
}
|
|
1404
2422
|
|
|
@@ -1436,7 +2454,7 @@ async function runArchive(input) {
|
|
|
1436
2454
|
if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
|
|
1437
2455
|
if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
|
|
1438
2456
|
const slug = relPath.replace(/\.md$/, "").split("/").pop();
|
|
1439
|
-
const archivePath =
|
|
2457
|
+
const archivePath = join17("_archive", relPath).replace(/\\/g, "/");
|
|
1440
2458
|
const remoteRoot = normalizeRemoteRoot(input.remote);
|
|
1441
2459
|
const remoteObjectPath = buildRemoteObjectPath(remoteRoot, relPath);
|
|
1442
2460
|
let cascade;
|
|
@@ -1462,7 +2480,7 @@ async function runArchive(input) {
|
|
|
1462
2480
|
const indexRefs = [];
|
|
1463
2481
|
if (!isRaw) {
|
|
1464
2482
|
try {
|
|
1465
|
-
const idx = await
|
|
2483
|
+
const idx = await readFile7(join17(input.vault, "index.md"), "utf8");
|
|
1466
2484
|
idx.split("\n").forEach((line, i) => {
|
|
1467
2485
|
if (line.includes(`[[${slug}]]`)) indexRefs.push({ line: i + 1, text: line });
|
|
1468
2486
|
});
|
|
@@ -1489,8 +2507,8 @@ async function runArchive(input) {
|
|
|
1489
2507
|
}
|
|
1490
2508
|
if (input.cascade && input.apply && cascade) {
|
|
1491
2509
|
for (const ref of cascade.source_array_refs) {
|
|
1492
|
-
const absPath =
|
|
1493
|
-
const text = await
|
|
2510
|
+
const absPath = join17(input.vault, ref.page);
|
|
2511
|
+
const text = await readFile7(absPath, "utf8");
|
|
1494
2512
|
const split = splitFrontmatter(text);
|
|
1495
2513
|
if (!split.ok) continue;
|
|
1496
2514
|
const before = split.data.rawFrontmatter;
|
|
@@ -1507,23 +2525,21 @@ ${fmRewritten}
|
|
|
1507
2525
|
}
|
|
1508
2526
|
}
|
|
1509
2527
|
}
|
|
1510
|
-
await
|
|
2528
|
+
await mkdir7(dirname5(join17(input.vault, archivePath)), { recursive: true });
|
|
2529
|
+
await rename2(join17(input.vault, relPath), join17(input.vault, archivePath));
|
|
1511
2530
|
let indexUpdated = false;
|
|
1512
2531
|
if (!isRaw) {
|
|
1513
|
-
const
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
} catch (e) {
|
|
1523
|
-
if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
|
|
2532
|
+
const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-ERFX76U5.js");
|
|
2533
|
+
const before = await readFile7(join17(input.vault, "index.md"), "utf8").catch(() => "");
|
|
2534
|
+
const fullTarget = relPath.replace(/\.md$/, "");
|
|
2535
|
+
const bare = fullTarget.split("/").pop() ?? fullTarget;
|
|
2536
|
+
const hadIndexEntry = before.includes(`[[${fullTarget}]]`) || before.includes(`[[${bare}]]`);
|
|
2537
|
+
const projection = await renderRootIndex2({ vault: input.vault, currentText: before });
|
|
2538
|
+
if (projection.ok && projection.data.text !== before) {
|
|
2539
|
+
const written = await writeRootIndexProjection2(input.vault, projection.data);
|
|
2540
|
+
if (written.ok && written.data.changed && hadIndexEntry) indexUpdated = true;
|
|
1524
2541
|
}
|
|
1525
2542
|
}
|
|
1526
|
-
await rename2(join9(input.vault, relPath), join9(input.vault, archivePath));
|
|
1527
2543
|
const archiveIntent = buildDeleteIntent({
|
|
1528
2544
|
path: relPath,
|
|
1529
2545
|
action: "archive",
|
|
@@ -1564,8 +2580,8 @@ ${fmRewritten}
|
|
|
1564
2580
|
}
|
|
1565
2581
|
|
|
1566
2582
|
// src/commands/remove.ts
|
|
1567
|
-
import { unlink as unlink2,
|
|
1568
|
-
import { join as
|
|
2583
|
+
import { unlink as unlink2, access } from "fs/promises";
|
|
2584
|
+
import { join as join18 } from "path";
|
|
1569
2585
|
async function pathExists(abs) {
|
|
1570
2586
|
try {
|
|
1571
2587
|
await access(abs);
|
|
@@ -1591,7 +2607,7 @@ async function runRemove(input) {
|
|
|
1591
2607
|
if (!relPath) {
|
|
1592
2608
|
try {
|
|
1593
2609
|
const candidate = normalizeVaultRelPath(input.page);
|
|
1594
|
-
if (await pathExists(
|
|
2610
|
+
if (await pathExists(join18(input.vault, candidate))) {
|
|
1595
2611
|
relPath = candidate;
|
|
1596
2612
|
}
|
|
1597
2613
|
} catch {
|
|
@@ -1611,22 +2627,7 @@ async function runRemove(input) {
|
|
|
1611
2627
|
}
|
|
1612
2628
|
const remoteRoot = normalizeRemoteRoot(input.remote);
|
|
1613
2629
|
const remoteObjectPath = buildRemoteObjectPath(remoteRoot, relPath);
|
|
1614
|
-
const slug = relPath.replace(/\.md$/, "").split("/").pop() ?? relPath;
|
|
1615
2630
|
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
2631
|
const intent = buildDeleteIntent({
|
|
1631
2632
|
path: relPath,
|
|
1632
2633
|
action: "remove",
|
|
@@ -1635,7 +2636,21 @@ async function runRemove(input) {
|
|
|
1635
2636
|
reason: input.reason
|
|
1636
2637
|
});
|
|
1637
2638
|
const tombstonePath = await writeDeleteIntent(input.vault, intent);
|
|
1638
|
-
await unlink2(
|
|
2639
|
+
await unlink2(join18(input.vault, relPath));
|
|
2640
|
+
if (relPath.endsWith(".md") && !relPath.startsWith("raw/")) {
|
|
2641
|
+
const { readFile: readFile17 } = await import("fs/promises");
|
|
2642
|
+
const { join: pathJoin } = await import("path");
|
|
2643
|
+
const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-ERFX76U5.js");
|
|
2644
|
+
const before = await readFile17(pathJoin(input.vault, "index.md"), "utf8").catch(() => "");
|
|
2645
|
+
const fullTarget = relPath.replace(/\.md$/, "");
|
|
2646
|
+
const bare = fullTarget.split("/").pop() ?? fullTarget;
|
|
2647
|
+
const hadIndexEntry = before.includes(`[[${fullTarget}]]`) || before.includes(`[[${bare}]]`);
|
|
2648
|
+
const projection = await renderRootIndex2({ vault: input.vault, currentText: before });
|
|
2649
|
+
if (projection.ok && projection.data.text !== before) {
|
|
2650
|
+
const written = await writeRootIndexProjection2(input.vault, projection.data);
|
|
2651
|
+
if (written.ok && written.data.changed && hadIndexEntry) indexUpdated = true;
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
1639
2654
|
appendLastOp(input.vault, {
|
|
1640
2655
|
operation: "remove",
|
|
1641
2656
|
summary: `removed ${relPath} (tombstone ${tombstonePath})`,
|
|
@@ -1664,7 +2679,7 @@ async function runRemove(input) {
|
|
|
1664
2679
|
}
|
|
1665
2680
|
|
|
1666
2681
|
// src/commands/drift.ts
|
|
1667
|
-
import { createHash as
|
|
2682
|
+
import { createHash as createHash3 } from "crypto";
|
|
1668
2683
|
|
|
1669
2684
|
// src/utils/fetch.ts
|
|
1670
2685
|
async function controlledFetch(url, opts) {
|
|
@@ -1743,7 +2758,7 @@ async function runDrift(input) {
|
|
|
1743
2758
|
});
|
|
1744
2759
|
continue;
|
|
1745
2760
|
}
|
|
1746
|
-
const currentHash =
|
|
2761
|
+
const currentHash = createHash3("sha256").update(Buffer.from(resp.data.body, "utf8")).digest("hex");
|
|
1747
2762
|
const identity = assessSourceIdentity({
|
|
1748
2763
|
rawPath: raw.relPath,
|
|
1749
2764
|
sourceUrl,
|
|
@@ -1974,14 +2989,14 @@ ${migratedBody}${newFooter}`;
|
|
|
1974
2989
|
|
|
1975
2990
|
// src/commands/update.ts
|
|
1976
2991
|
import { execSync } from "child_process";
|
|
1977
|
-
import { join as
|
|
2992
|
+
import { join as join19 } from "path";
|
|
1978
2993
|
function resolveGlobalSkillsRoot() {
|
|
1979
2994
|
try {
|
|
1980
2995
|
const globalRoot = execSync("npm root -g", {
|
|
1981
2996
|
encoding: "utf8",
|
|
1982
2997
|
timeout: 5e3
|
|
1983
2998
|
}).trim();
|
|
1984
|
-
return
|
|
2999
|
+
return join19(globalRoot, "skillwiki", "skills");
|
|
1985
3000
|
} catch {
|
|
1986
3001
|
return null;
|
|
1987
3002
|
}
|
|
@@ -2009,7 +3024,7 @@ async function runUpdate(input) {
|
|
|
2009
3024
|
const pkg2 = readCliPackageJson();
|
|
2010
3025
|
const currentVersion = pkg2.version;
|
|
2011
3026
|
const tag = normalizeDistTag(input.distTag);
|
|
2012
|
-
const target =
|
|
3027
|
+
const target = join19(input.home, ".claude", "skills");
|
|
2013
3028
|
let latest;
|
|
2014
3029
|
try {
|
|
2015
3030
|
latest = execSync(`npm view skillwiki@${tag} version`, {
|
|
@@ -2086,22 +3101,22 @@ async function runUpdate(input) {
|
|
|
2086
3101
|
|
|
2087
3102
|
// src/commands/self-update.ts
|
|
2088
3103
|
import { execSync as execSync2 } from "child_process";
|
|
2089
|
-
import { existsSync as
|
|
2090
|
-
import { join as
|
|
3104
|
+
import { existsSync as existsSync4, readFileSync as readFileSync10 } from "fs";
|
|
3105
|
+
import { join as join20 } from "path";
|
|
2091
3106
|
var DEFAULT_SOURCE_ROOT_SUFFIX = "/Desktop/code/llm-wiki";
|
|
2092
3107
|
async function runSelfUpdate(input) {
|
|
2093
3108
|
const currentVersion = readCliPackageJson().version;
|
|
2094
3109
|
const sourceRoot = input.sourceRoot ?? `${input.home}${DEFAULT_SOURCE_ROOT_SUFFIX}`;
|
|
2095
3110
|
const distTag = normalizeDistTag(input.distTag);
|
|
2096
|
-
const localPkgPath =
|
|
2097
|
-
const hasLocalSource =
|
|
3111
|
+
const localPkgPath = join20(sourceRoot, "packages", "cli", "package.json");
|
|
3112
|
+
const hasLocalSource = existsSync4(localPkgPath);
|
|
2098
3113
|
if (input.check) {
|
|
2099
3114
|
let availableVersion = null;
|
|
2100
3115
|
let source;
|
|
2101
3116
|
if (hasLocalSource) {
|
|
2102
3117
|
source = "local";
|
|
2103
3118
|
try {
|
|
2104
|
-
availableVersion = JSON.parse(
|
|
3119
|
+
availableVersion = JSON.parse(readFileSync10(localPkgPath, "utf8")).version ?? null;
|
|
2105
3120
|
} catch {
|
|
2106
3121
|
availableVersion = null;
|
|
2107
3122
|
}
|
|
@@ -2159,7 +3174,7 @@ async function runSelfUpdate(input) {
|
|
|
2159
3174
|
}
|
|
2160
3175
|
const newVersion = (() => {
|
|
2161
3176
|
try {
|
|
2162
|
-
return JSON.parse(
|
|
3177
|
+
return JSON.parse(readFileSync10(localPkgPath, "utf8")).version ?? "unknown";
|
|
2163
3178
|
} catch {
|
|
2164
3179
|
return "unknown";
|
|
2165
3180
|
}
|
|
@@ -2225,21 +3240,21 @@ async function runSelfUpdate(input) {
|
|
|
2225
3240
|
}
|
|
2226
3241
|
|
|
2227
3242
|
// src/commands/transcripts.ts
|
|
2228
|
-
import { readdir as
|
|
2229
|
-
import { join as
|
|
3243
|
+
import { readdir as readdir5, stat as stat3, readFile as readFile9 } from "fs/promises";
|
|
3244
|
+
import { join as join21 } from "path";
|
|
2230
3245
|
async function runTranscripts(input) {
|
|
2231
|
-
const dir =
|
|
3246
|
+
const dir = join21(input.vault, "raw", "transcripts");
|
|
2232
3247
|
let entries;
|
|
2233
3248
|
try {
|
|
2234
|
-
entries = await
|
|
3249
|
+
entries = await readdir5(dir, { withFileTypes: true });
|
|
2235
3250
|
} catch {
|
|
2236
3251
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: { ok: false, error: "VAULT_PATH_INVALID", detail: `raw/transcripts/ not found: ${dir}` } };
|
|
2237
3252
|
}
|
|
2238
3253
|
const transcripts = [];
|
|
2239
3254
|
for (const entry of entries) {
|
|
2240
3255
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
2241
|
-
const filePath =
|
|
2242
|
-
const content = await
|
|
3256
|
+
const filePath = join21(dir, entry.name);
|
|
3257
|
+
const content = await readFile9(filePath, "utf8");
|
|
2243
3258
|
const fm = extractFrontmatter(content);
|
|
2244
3259
|
if (!fm.ok) continue;
|
|
2245
3260
|
const ingested = typeof fm.data.ingested === "string" ? fm.data.ingested : "";
|
|
@@ -2256,10 +3271,10 @@ async function runTranscripts(input) {
|
|
|
2256
3271
|
}
|
|
2257
3272
|
|
|
2258
3273
|
// src/commands/compound.ts
|
|
2259
|
-
import { writeFile as writeFile7, mkdir as
|
|
2260
|
-
import { join as
|
|
2261
|
-
import { existsSync as
|
|
2262
|
-
import { readFile as
|
|
3274
|
+
import { writeFile as writeFile7, mkdir as mkdir8, readdir as readdir6, unlink as unlink3 } from "fs/promises";
|
|
3275
|
+
import { join as join22 } from "path";
|
|
3276
|
+
import { existsSync as existsSync5 } from "fs";
|
|
3277
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
2263
3278
|
var RETRO_HEADING_RE = /^## \[(\d{4}-\d{2}-\d{2})(?:\s+[^\]]+)?\] retro \| loop cycle(?: (\d+))?: (.+)$/;
|
|
2264
3279
|
var FIELD_RE = {
|
|
2265
3280
|
improve: /^-\s+\*?\*?Improve:?\*?\*?\s*(.+)$/m,
|
|
@@ -2357,17 +3372,17 @@ function extractRetroFields(date, cycleName, block) {
|
|
|
2357
3372
|
};
|
|
2358
3373
|
}
|
|
2359
3374
|
async function runCompound(input) {
|
|
2360
|
-
const logPath =
|
|
3375
|
+
const logPath = join22(input.vault, "log.md");
|
|
2361
3376
|
let logText;
|
|
2362
3377
|
try {
|
|
2363
|
-
logText = await
|
|
3378
|
+
logText = await readFile10(logPath, "utf8");
|
|
2364
3379
|
} catch {
|
|
2365
3380
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
|
|
2366
3381
|
}
|
|
2367
3382
|
const entries = parseRetroEntries(logText);
|
|
2368
3383
|
const promoted = [];
|
|
2369
3384
|
const skipped = [];
|
|
2370
|
-
const compoundDir =
|
|
3385
|
+
const compoundDir = join22(input.vault, "projects", input.project, "compound");
|
|
2371
3386
|
for (const entry of entries) {
|
|
2372
3387
|
const generalizeValue = entry.generalize.trim();
|
|
2373
3388
|
if (!/^yes/i.test(generalizeValue)) {
|
|
@@ -2375,8 +3390,8 @@ async function runCompound(input) {
|
|
|
2375
3390
|
continue;
|
|
2376
3391
|
}
|
|
2377
3392
|
const slug = slugify(entry.cycleName);
|
|
2378
|
-
const compoundPath =
|
|
2379
|
-
if (
|
|
3393
|
+
const compoundPath = join22(compoundDir, `${slug}.md`);
|
|
3394
|
+
if (existsSync5(compoundPath)) {
|
|
2380
3395
|
skipped.push(entry.date);
|
|
2381
3396
|
continue;
|
|
2382
3397
|
}
|
|
@@ -2414,8 +3429,8 @@ async function runCompound(input) {
|
|
|
2414
3429
|
].join("\n");
|
|
2415
3430
|
const content = frontmatter + "\n" + body;
|
|
2416
3431
|
if (!input.dryRun) {
|
|
2417
|
-
if (!
|
|
2418
|
-
await
|
|
3432
|
+
if (!existsSync5(compoundDir)) {
|
|
3433
|
+
await mkdir8(compoundDir, { recursive: true });
|
|
2419
3434
|
}
|
|
2420
3435
|
await writeFile7(compoundPath, content, "utf8");
|
|
2421
3436
|
}
|
|
@@ -2436,16 +3451,16 @@ async function runCompound(input) {
|
|
|
2436
3451
|
};
|
|
2437
3452
|
}
|
|
2438
3453
|
async function runCompoundDelete(input) {
|
|
2439
|
-
const projectDir =
|
|
2440
|
-
if (!
|
|
3454
|
+
const projectDir = join22(input.vault, "projects", input.project);
|
|
3455
|
+
if (!existsSync5(projectDir)) {
|
|
2441
3456
|
return {
|
|
2442
3457
|
exitCode: ExitCode.PROJECT_NOT_FOUND,
|
|
2443
3458
|
result: err("PROJECT_NOT_FOUND", { slug: input.project, path: projectDir })
|
|
2444
3459
|
};
|
|
2445
3460
|
}
|
|
2446
3461
|
const entryName = input.entry.replace(/\.md$/, "");
|
|
2447
|
-
const compoundPath =
|
|
2448
|
-
if (!
|
|
3462
|
+
const compoundPath = join22(projectDir, "compound", `${entryName}.md`);
|
|
3463
|
+
if (!existsSync5(compoundPath)) {
|
|
2449
3464
|
return {
|
|
2450
3465
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
2451
3466
|
result: err("FILE_NOT_FOUND", { path: compoundPath })
|
|
@@ -2478,8 +3493,8 @@ knowledge.md regenerated`
|
|
|
2478
3493
|
};
|
|
2479
3494
|
}
|
|
2480
3495
|
async function runCompoundList(input) {
|
|
2481
|
-
const compoundDir =
|
|
2482
|
-
if (!
|
|
3496
|
+
const compoundDir = join22(input.vault, "projects", input.project, "compound");
|
|
3497
|
+
if (!existsSync5(compoundDir)) {
|
|
2483
3498
|
return {
|
|
2484
3499
|
exitCode: ExitCode.OK,
|
|
2485
3500
|
result: ok({
|
|
@@ -2493,7 +3508,7 @@ no compound directory found`
|
|
|
2493
3508
|
}
|
|
2494
3509
|
let dirents;
|
|
2495
3510
|
try {
|
|
2496
|
-
dirents = await
|
|
3511
|
+
dirents = await readdir6(compoundDir, { withFileTypes: true });
|
|
2497
3512
|
} catch {
|
|
2498
3513
|
return {
|
|
2499
3514
|
exitCode: ExitCode.OK,
|
|
@@ -2509,10 +3524,10 @@ could not read compound directory`
|
|
|
2509
3524
|
const entries = [];
|
|
2510
3525
|
for (const dirent of dirents) {
|
|
2511
3526
|
if (!dirent.isFile() || !dirent.name.endsWith(".md")) continue;
|
|
2512
|
-
const filePath =
|
|
3527
|
+
const filePath = join22(compoundDir, dirent.name);
|
|
2513
3528
|
let text;
|
|
2514
3529
|
try {
|
|
2515
|
-
text = await
|
|
3530
|
+
text = await readFile10(filePath, "utf8");
|
|
2516
3531
|
} catch {
|
|
2517
3532
|
continue;
|
|
2518
3533
|
}
|
|
@@ -2541,8 +3556,8 @@ no compound entries found`;
|
|
|
2541
3556
|
}
|
|
2542
3557
|
|
|
2543
3558
|
// src/commands/session-brief.ts
|
|
2544
|
-
import { mkdir as
|
|
2545
|
-
import { join as
|
|
3559
|
+
import { mkdir as mkdir9, readFile as readFile11, writeFile as writeFile8 } from "fs/promises";
|
|
3560
|
+
import { join as join23, relative, sep } from "path";
|
|
2546
3561
|
var MAX_WORDS = 900;
|
|
2547
3562
|
async function runSessionBrief(input) {
|
|
2548
3563
|
const scan = await scanVault(input.vault);
|
|
@@ -2642,7 +3657,7 @@ async function resolveProject(input) {
|
|
|
2642
3657
|
const envProject = input.env?.SKILLWIKI_PROJECT;
|
|
2643
3658
|
if (envProject) return envProject;
|
|
2644
3659
|
const cwd = input.cwd ?? process.cwd();
|
|
2645
|
-
const projectDotenv = await readProjectSlug(
|
|
3660
|
+
const projectDotenv = await readProjectSlug(join23(cwd, ".skillwiki", ".env"));
|
|
2646
3661
|
if (projectDotenv) return projectDotenv;
|
|
2647
3662
|
const inferred = inferProjectFromPath(input.vault, cwd);
|
|
2648
3663
|
if (inferred) return inferred;
|
|
@@ -2651,7 +3666,7 @@ async function resolveProject(input) {
|
|
|
2651
3666
|
async function readProjectSlug(file) {
|
|
2652
3667
|
let text;
|
|
2653
3668
|
try {
|
|
2654
|
-
text = await
|
|
3669
|
+
text = await readFile11(file, "utf8");
|
|
2655
3670
|
} catch {
|
|
2656
3671
|
return void 0;
|
|
2657
3672
|
}
|
|
@@ -2728,7 +3743,7 @@ async function loadTrendDigests(typedPages) {
|
|
|
2728
3743
|
return out;
|
|
2729
3744
|
}
|
|
2730
3745
|
async function loadSessionPins(vault, project) {
|
|
2731
|
-
const text = await readIfExists(
|
|
3746
|
+
const text = await readIfExists(join23(vault, "meta", "session-pins.md"));
|
|
2732
3747
|
if (!text) return [];
|
|
2733
3748
|
const fm = extractFrontmatter(text);
|
|
2734
3749
|
if (!fm.ok) return [];
|
|
@@ -2845,7 +3860,7 @@ function satelliteHealthWarnings(warning) {
|
|
|
2845
3860
|
return warning ? [warning] : [];
|
|
2846
3861
|
}
|
|
2847
3862
|
async function loadHealthWarnings(vault) {
|
|
2848
|
-
const text = await readIfExists(
|
|
3863
|
+
const text = await readIfExists(join23(vault, ".skillwiki", "health.json"));
|
|
2849
3864
|
if (!text) return [];
|
|
2850
3865
|
try {
|
|
2851
3866
|
const parsed = JSON.parse(text);
|
|
@@ -2858,7 +3873,7 @@ async function loadHealthWarnings(vault) {
|
|
|
2858
3873
|
}
|
|
2859
3874
|
}
|
|
2860
3875
|
async function loadMemoryTopics(vault, project) {
|
|
2861
|
-
const text = await readIfExists(
|
|
3876
|
+
const text = await readIfExists(join23(vault, ".skillwiki", "memory", project, "topics.json"));
|
|
2862
3877
|
if (!text) return [];
|
|
2863
3878
|
try {
|
|
2864
3879
|
const parsed = JSON.parse(text);
|
|
@@ -2875,11 +3890,11 @@ async function loadMemoryTopics(vault, project) {
|
|
|
2875
3890
|
}
|
|
2876
3891
|
}
|
|
2877
3892
|
async function writeBriefArtifacts(vault, input) {
|
|
2878
|
-
const metaPath =
|
|
2879
|
-
const cacheMdPath =
|
|
2880
|
-
const cacheJsonPath =
|
|
2881
|
-
await
|
|
2882
|
-
await
|
|
3893
|
+
const metaPath = join23(vault, "meta", "latest-session-brief.md");
|
|
3894
|
+
const cacheMdPath = join23(vault, ".skillwiki", "session-brief.md");
|
|
3895
|
+
const cacheJsonPath = join23(vault, ".skillwiki", "session-brief.json");
|
|
3896
|
+
await mkdir9(join23(vault, "meta"), { recursive: true });
|
|
3897
|
+
await mkdir9(join23(vault, ".skillwiki"), { recursive: true });
|
|
2883
3898
|
const committed = renderCommittedBrief(input);
|
|
2884
3899
|
const previousComparable = comparableBrief(await readIfExists(metaPath));
|
|
2885
3900
|
const nextComparable = comparableBrief(committed);
|
|
@@ -2894,8 +3909,17 @@ async function writeBriefArtifacts(vault, input) {
|
|
|
2894
3909
|
memory_topics: input.memoryTopics
|
|
2895
3910
|
}, null, 2)}
|
|
2896
3911
|
`, "utf8");
|
|
2897
|
-
const indexUpdated = await
|
|
2898
|
-
|
|
3912
|
+
const indexUpdated = await rebuildRootIndexProjection(vault);
|
|
3913
|
+
let logUpdated = false;
|
|
3914
|
+
if (materialChange) {
|
|
3915
|
+
const log = await runLogAppend({
|
|
3916
|
+
vault,
|
|
3917
|
+
content: `## [${input.today}] session-brief | refreshed: meta/latest-session-brief.md`,
|
|
3918
|
+
strictLock: true,
|
|
3919
|
+
recordLastOp: false
|
|
3920
|
+
});
|
|
3921
|
+
logUpdated = log.result.ok && log.result.data.appended;
|
|
3922
|
+
}
|
|
2899
3923
|
if (materialChange) {
|
|
2900
3924
|
appendLastOp(vault, {
|
|
2901
3925
|
operation: "session-brief",
|
|
@@ -2939,37 +3963,18 @@ function renderCommittedBrief(input) {
|
|
|
2939
3963
|
""
|
|
2940
3964
|
].filter((line) => line !== "").join("\n");
|
|
2941
3965
|
}
|
|
2942
|
-
async function
|
|
2943
|
-
const
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
if (
|
|
2947
|
-
|
|
2948
|
-
const
|
|
2949
|
-
|
|
2950
|
-
if (sectionIdx === -1) {
|
|
2951
|
-
while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
|
|
2952
|
-
lines.push("", "## Meta", entry);
|
|
2953
|
-
} else {
|
|
2954
|
-
let insertAt = sectionIdx + 1;
|
|
2955
|
-
while (insertAt < lines.length && !lines[insertAt].startsWith("## ")) insertAt++;
|
|
2956
|
-
lines.splice(insertAt, 0, entry);
|
|
2957
|
-
}
|
|
2958
|
-
await writeFile8(indexPath, lines.join("\n"), "utf8");
|
|
2959
|
-
return true;
|
|
2960
|
-
}
|
|
2961
|
-
async function appendMaterialLog(vault, today) {
|
|
2962
|
-
const logPath = join15(vault, "log.md");
|
|
2963
|
-
const text = await readIfExists(logPath);
|
|
2964
|
-
if (!text) return false;
|
|
2965
|
-
const entry = `
|
|
2966
|
-
## [${today}] session-brief | refreshed: meta/latest-session-brief.md`;
|
|
2967
|
-
await writeFile8(logPath, text.trimEnd() + entry + "\n", "utf8");
|
|
2968
|
-
return true;
|
|
3966
|
+
async function rebuildRootIndexProjection(vault) {
|
|
3967
|
+
const before = await readIfExists(join23(vault, "index.md"));
|
|
3968
|
+
if (!before) return false;
|
|
3969
|
+
const projection = await renderRootIndex({ vault, currentText: before });
|
|
3970
|
+
if (!projection.ok) return false;
|
|
3971
|
+
if (projection.data.text === before) return false;
|
|
3972
|
+
const written = await writeRootIndexProjection(vault, projection.data);
|
|
3973
|
+
return written.ok && written.data.changed;
|
|
2969
3974
|
}
|
|
2970
3975
|
async function readIfExists(path) {
|
|
2971
3976
|
try {
|
|
2972
|
-
return await
|
|
3977
|
+
return await readFile11(path, "utf8");
|
|
2973
3978
|
} catch {
|
|
2974
3979
|
return "";
|
|
2975
3980
|
}
|
|
@@ -3011,16 +4016,18 @@ function dateFromPath(path) {
|
|
|
3011
4016
|
}
|
|
3012
4017
|
|
|
3013
4018
|
// src/commands/ingest.ts
|
|
3014
|
-
import { readFile as
|
|
3015
|
-
import { join as
|
|
4019
|
+
import { readFile as readFile13, open as open2, unlink as unlink4, mkdir as mkdir10 } from "fs/promises";
|
|
4020
|
+
import { join as join25 } from "path";
|
|
3016
4021
|
import { createHash as createHash4 } from "crypto";
|
|
3017
4022
|
|
|
3018
4023
|
// src/commands/page-publish.ts
|
|
3019
|
-
import { createHash as createHash3 } from "crypto";
|
|
3020
4024
|
import { realpathSync } from "fs";
|
|
3021
|
-
import { readFile as
|
|
3022
|
-
import { join as
|
|
3023
|
-
var DEFAULT_DEPS = {
|
|
4025
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
4026
|
+
import { join as join24, resolve as resolve3 } from "path";
|
|
4027
|
+
var DEFAULT_DEPS = {
|
|
4028
|
+
afterStage: async () => void 0,
|
|
4029
|
+
preflight: (input) => runManagedWritePreflight(input)
|
|
4030
|
+
};
|
|
3024
4031
|
function errorExitCode(error) {
|
|
3025
4032
|
switch (error) {
|
|
3026
4033
|
case "FILE_NOT_FOUND":
|
|
@@ -3036,6 +4043,9 @@ function errorExitCode(error) {
|
|
|
3036
4043
|
return ExitCode.SENSITIVE_CONTENT_DETECTED;
|
|
3037
4044
|
case "SYNC_LOCK_HELD":
|
|
3038
4045
|
return ExitCode.SYNC_LOCK_HELD;
|
|
4046
|
+
case "PREFLIGHT_FAILED":
|
|
4047
|
+
return ExitCode.PREFLIGHT_FAILED;
|
|
4048
|
+
case "EVENT_IDENTITY_COLLISION":
|
|
3039
4049
|
case "WRITE_FAILED":
|
|
3040
4050
|
return ExitCode.WRITE_FAILED;
|
|
3041
4051
|
default:
|
|
@@ -3043,7 +4053,11 @@ function errorExitCode(error) {
|
|
|
3043
4053
|
}
|
|
3044
4054
|
}
|
|
3045
4055
|
function publicationId(target, content, logNote = "") {
|
|
3046
|
-
return
|
|
4056
|
+
return operationId("skillwiki-page-publish-v1", [target, content, logNote]);
|
|
4057
|
+
}
|
|
4058
|
+
function resolveRootAggregateMode(env = process.env) {
|
|
4059
|
+
if (env.SKILLWIKI_ROOT_AGGREGATE_MODE === "source-only") return "source-only";
|
|
4060
|
+
return "dual";
|
|
3047
4061
|
}
|
|
3048
4062
|
function prepareFrozenPublication(input, source) {
|
|
3049
4063
|
const target = assertTargetInsideVault(input.vault, input.target);
|
|
@@ -3079,7 +4093,7 @@ function preparePagePublicationFromContent(input) {
|
|
|
3079
4093
|
async function preparePagePublication(input) {
|
|
3080
4094
|
let content;
|
|
3081
4095
|
try {
|
|
3082
|
-
content = await
|
|
4096
|
+
content = await readFile12(input.draftPath, "utf8");
|
|
3083
4097
|
} catch (error) {
|
|
3084
4098
|
return err("FILE_NOT_FOUND", { path: input.draftPath, message: String(error) });
|
|
3085
4099
|
}
|
|
@@ -3150,10 +4164,10 @@ async function runLockedPrimaryStages(input, vault, deps) {
|
|
|
3150
4164
|
ExitCode.VAULT_PATH_INVALID
|
|
3151
4165
|
);
|
|
3152
4166
|
}
|
|
3153
|
-
const schemaPath =
|
|
4167
|
+
const schemaPath = join24(vault, "SCHEMA.md");
|
|
3154
4168
|
let schemaText;
|
|
3155
4169
|
try {
|
|
3156
|
-
schemaText = await
|
|
4170
|
+
schemaText = await readFile12(schemaPath, "utf8");
|
|
3157
4171
|
} catch (error) {
|
|
3158
4172
|
return lockedFailure("schema", state, err("WRITE_FAILED", { message: String(error) }));
|
|
3159
4173
|
}
|
|
@@ -3183,8 +4197,8 @@ async function runLockedPrimaryStages(input, vault, deps) {
|
|
|
3183
4197
|
let visibleSchema;
|
|
3184
4198
|
try {
|
|
3185
4199
|
[visible, visibleSchema] = await Promise.all([
|
|
3186
|
-
|
|
3187
|
-
|
|
4200
|
+
readFile12(input.targetPath, "utf8"),
|
|
4201
|
+
readFile12(schemaPath, "utf8")
|
|
3188
4202
|
]);
|
|
3189
4203
|
} catch (error) {
|
|
3190
4204
|
return lockedFailure("verify", state, err("WRITE_FAILED", { message: String(error) }));
|
|
@@ -3237,7 +4251,7 @@ function phaseFailure(stage, input, published, cause, context = {}, exitCode = E
|
|
|
3237
4251
|
})
|
|
3238
4252
|
};
|
|
3239
4253
|
}
|
|
3240
|
-
function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, logAppended, filesChanged, dryRun = false) {
|
|
4254
|
+
function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, logAppended, filesChanged, dryRun = false, receipt = null) {
|
|
3241
4255
|
return {
|
|
3242
4256
|
exitCode: ExitCode.OK,
|
|
3243
4257
|
result: ok({
|
|
@@ -3251,6 +4265,9 @@ function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, logAppe
|
|
|
3251
4265
|
operation_id: input.operationId,
|
|
3252
4266
|
dry_run: dryRun,
|
|
3253
4267
|
files_changed: filesChanged,
|
|
4268
|
+
base_oid: receipt?.base_oid ?? null,
|
|
4269
|
+
write_mode: receipt?.mode ?? null,
|
|
4270
|
+
...receipt?.host_id ? { host_id: receipt.host_id } : {},
|
|
3254
4271
|
humanHint: dryRun ? `dry run: would publish ${input.page.target} (${input.operationId.slice(0, 12)})` : `published ${input.page.target} (${input.operationId.slice(0, 12)})`
|
|
3255
4272
|
})
|
|
3256
4273
|
};
|
|
@@ -3266,17 +4283,17 @@ function renderPublicationLog(input, added) {
|
|
|
3266
4283
|
}
|
|
3267
4284
|
async function readPageChanged(targetPath, content) {
|
|
3268
4285
|
try {
|
|
3269
|
-
return ok(await
|
|
4286
|
+
return ok(await readFile12(targetPath, "utf8") !== content);
|
|
3270
4287
|
} catch (error) {
|
|
3271
4288
|
if (error.code === "ENOENT") return ok(true);
|
|
3272
4289
|
return err("WRITE_FAILED", { path: targetPath, message: String(error) });
|
|
3273
4290
|
}
|
|
3274
4291
|
}
|
|
3275
4292
|
async function previewPreparedPagePublication(input, vault) {
|
|
3276
|
-
const schemaPath =
|
|
4293
|
+
const schemaPath = join24(vault, "SCHEMA.md");
|
|
3277
4294
|
let schemaText;
|
|
3278
4295
|
try {
|
|
3279
|
-
schemaText = await
|
|
4296
|
+
schemaText = await readFile12(schemaPath, "utf8");
|
|
3280
4297
|
} catch (error) {
|
|
3281
4298
|
const result = err("FILE_NOT_FOUND", { path: schemaPath, message: String(error) });
|
|
3282
4299
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result };
|
|
@@ -3288,10 +4305,10 @@ async function previewPreparedPagePublication(input, vault) {
|
|
|
3288
4305
|
if (!reconciled.ok) return { exitCode: errorExitCode(reconciled.error), result: reconciled };
|
|
3289
4306
|
const pageChanged = await readPageChanged(input.targetPath, input.page.content);
|
|
3290
4307
|
if (!pageChanged.ok) return { exitCode: errorExitCode(pageChanged.error), result: pageChanged };
|
|
3291
|
-
const indexPath =
|
|
4308
|
+
const indexPath = join24(vault, "index.md");
|
|
3292
4309
|
let indexText;
|
|
3293
4310
|
try {
|
|
3294
|
-
indexText = await
|
|
4311
|
+
indexText = await readFile12(indexPath, "utf8");
|
|
3295
4312
|
} catch (error) {
|
|
3296
4313
|
const result = err("FILE_NOT_FOUND", { path: indexPath, message: String(error) });
|
|
3297
4314
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result };
|
|
@@ -3302,10 +4319,10 @@ async function previewPreparedPagePublication(input, vault) {
|
|
|
3302
4319
|
type: input.page.type
|
|
3303
4320
|
});
|
|
3304
4321
|
if (!index.ok) return { exitCode: errorExitCode(index.error), result: index };
|
|
3305
|
-
const logPath =
|
|
4322
|
+
const logPath = join24(vault, "log.md");
|
|
3306
4323
|
let logText;
|
|
3307
4324
|
try {
|
|
3308
|
-
logText = await
|
|
4325
|
+
logText = await readFile12(logPath, "utf8");
|
|
3309
4326
|
} catch (error) {
|
|
3310
4327
|
const result = err("FILE_NOT_FOUND", { path: logPath, message: String(error) });
|
|
3311
4328
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result };
|
|
@@ -3328,93 +4345,159 @@ async function previewPreparedPagePublication(input, vault) {
|
|
|
3328
4345
|
);
|
|
3329
4346
|
}
|
|
3330
4347
|
async function publishPreparedPage(input, vault, deps = DEFAULT_DEPS) {
|
|
3331
|
-
|
|
4348
|
+
const managed = acquireManagedWriteLock(vault, `page publish ${input.page.target}`);
|
|
4349
|
+
if (!managed.ok) return { exitCode: errorExitCode(managed.error), result: managed };
|
|
3332
4350
|
try {
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
4351
|
+
const preflight = await deps.preflight({
|
|
4352
|
+
vault,
|
|
4353
|
+
command: `page publish ${input.page.target}`,
|
|
4354
|
+
lockToken: managed.data.ownerToken
|
|
3336
4355
|
});
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
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
|
|
4356
|
+
if (!preflight.result.ok) {
|
|
4357
|
+
return { exitCode: preflight.exitCode, result: preflight.result };
|
|
4358
|
+
}
|
|
4359
|
+
const writeReceipt = preflight.result.data;
|
|
4360
|
+
const aggregateMode = resolveRootAggregateMode();
|
|
4361
|
+
if (writeReceipt.base_oid) {
|
|
4362
|
+
const head = git(vault, ["rev-parse", "HEAD"]);
|
|
4363
|
+
if (head !== writeReceipt.base_oid) {
|
|
4364
|
+
return {
|
|
4365
|
+
exitCode: ExitCode.PREFLIGHT_FAILED,
|
|
4366
|
+
result: err("PREFLIGHT_FAILED", {
|
|
4367
|
+
reason: "base-oid-drift",
|
|
4368
|
+
expected: writeReceipt.base_oid,
|
|
4369
|
+
actual: head
|
|
4370
|
+
})
|
|
4371
|
+
};
|
|
3367
4372
|
}
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
4373
|
+
}
|
|
4374
|
+
let lock;
|
|
4375
|
+
try {
|
|
4376
|
+
lock = acquireOwnedSyncLock(vault, {
|
|
4377
|
+
summary: `page publish ${input.page.target}`,
|
|
4378
|
+
ttlMinutes: 1
|
|
4379
|
+
});
|
|
4380
|
+
} catch (error) {
|
|
4381
|
+
return {
|
|
4382
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
4383
|
+
result: err("WRITE_FAILED", { stage: "lock", message: String(error) })
|
|
4384
|
+
};
|
|
4385
|
+
}
|
|
4386
|
+
if (!lock.ok) return { exitCode: errorExitCode(lock.error), result: lock };
|
|
4387
|
+
let primary;
|
|
4388
|
+
let released;
|
|
4389
|
+
try {
|
|
4390
|
+
primary = await runLockedPrimaryStages(input, vault, deps);
|
|
4391
|
+
} catch (error) {
|
|
4392
|
+
primary = lockedFailure(
|
|
4393
|
+
"schema",
|
|
4394
|
+
emptyLockedState(),
|
|
4395
|
+
err("WRITE_FAILED", { message: `unexpected primary-stage failure: ${String(error)}` })
|
|
4396
|
+
);
|
|
4397
|
+
} finally {
|
|
4398
|
+
released = releaseOwnedSyncLock(lock.data);
|
|
4399
|
+
}
|
|
4400
|
+
const primaryState = primary?.ok ? primary.data : primary?.state;
|
|
4401
|
+
if (released === void 0 || !released.ok || !released.data.released) {
|
|
4402
|
+
return phaseFailure(
|
|
4403
|
+
"unlock",
|
|
4404
|
+
input,
|
|
4405
|
+
primaryState?.published ?? false,
|
|
4406
|
+
released && !released.ok ? released : err("WRITE_FAILED", { message: "lock release did not run" }),
|
|
4407
|
+
{
|
|
4408
|
+
primary_stage: primary && !primary.ok ? primary.stage : "complete",
|
|
4409
|
+
primary_error: primary && !primary.ok ? primary.cause.error : void 0
|
|
4410
|
+
}
|
|
4411
|
+
);
|
|
4412
|
+
}
|
|
4413
|
+
const unlockHook = await observeStage(deps, "unlock");
|
|
4414
|
+
if (unlockHook) return phaseFailure("unlock", input, primaryState?.published ?? false, unlockHook);
|
|
4415
|
+
if (primary === void 0) {
|
|
4416
|
+
return phaseFailure(
|
|
4417
|
+
"schema",
|
|
4418
|
+
input,
|
|
4419
|
+
false,
|
|
4420
|
+
err("WRITE_FAILED", { message: "locked publication produced no result" })
|
|
4421
|
+
);
|
|
4422
|
+
}
|
|
4423
|
+
if (!primary.ok) {
|
|
4424
|
+
return phaseFailure(
|
|
4425
|
+
primary.stage,
|
|
4426
|
+
input,
|
|
4427
|
+
primary.state.published,
|
|
4428
|
+
primary.cause,
|
|
4429
|
+
void 0,
|
|
4430
|
+
primary.exitCode
|
|
4431
|
+
);
|
|
4432
|
+
}
|
|
4433
|
+
const state = primary.data;
|
|
4434
|
+
const event = await writeLogEvent(vault, {
|
|
4435
|
+
schema: "skillwiki-log-event/v1",
|
|
4436
|
+
operation_id: input.operationId,
|
|
4437
|
+
occurred_at: `${input.date}T00:00:00.000Z`,
|
|
4438
|
+
host_id: writeReceipt.host_id ?? "standalone",
|
|
4439
|
+
actor: "skillwiki-cli",
|
|
4440
|
+
kind: "page-publish",
|
|
4441
|
+
target: input.page.target,
|
|
4442
|
+
note: input.logNote ?? `Published ${input.page.title}`,
|
|
4443
|
+
metadata: {
|
|
4444
|
+
page_type: input.page.type,
|
|
4445
|
+
// Stable for replay: do not embed schema-delta taxonomy_added (varies after first write).
|
|
4446
|
+
tags: [...input.page.tags].sort(),
|
|
4447
|
+
base_oid: writeReceipt.base_oid
|
|
4448
|
+
}
|
|
4449
|
+
});
|
|
4450
|
+
if (!event.ok) {
|
|
4451
|
+
return phaseFailure(
|
|
4452
|
+
"event",
|
|
4453
|
+
input,
|
|
4454
|
+
true,
|
|
4455
|
+
event,
|
|
4456
|
+
void 0,
|
|
4457
|
+
event.error === "EVENT_IDENTITY_COLLISION" ? ExitCode.WRITE_FAILED : errorExitCode(event.error)
|
|
4458
|
+
);
|
|
4459
|
+
}
|
|
4460
|
+
if (event.data.created) state.changed.add(event.data.path);
|
|
4461
|
+
const eventHook = await observeStage(deps, "event");
|
|
4462
|
+
if (eventHook) return phaseFailure("event", input, true, eventHook);
|
|
4463
|
+
let logAppended = false;
|
|
4464
|
+
if (aggregateMode === "dual" || writeReceipt.mode !== "immutable-record") {
|
|
4465
|
+
if (aggregateMode === "dual") {
|
|
4466
|
+
const log = await runLogAppend({
|
|
4467
|
+
vault,
|
|
4468
|
+
content: renderPublicationLog(input, state.taxonomyAdded),
|
|
4469
|
+
operationId: input.operationId,
|
|
4470
|
+
strictLock: true,
|
|
4471
|
+
recordLastOp: false
|
|
4472
|
+
});
|
|
4473
|
+
if (!log.result.ok) return phaseFailure("log", input, true, log.result);
|
|
4474
|
+
if (log.exitCode !== ExitCode.OK) {
|
|
4475
|
+
return phaseFailure(
|
|
4476
|
+
"log",
|
|
4477
|
+
input,
|
|
4478
|
+
true,
|
|
4479
|
+
err("WRITE_FAILED", { message: "log append returned inconsistent success state" })
|
|
4480
|
+
);
|
|
4481
|
+
}
|
|
4482
|
+
logAppended = log.result.data.appended;
|
|
4483
|
+
if (logAppended) state.changed.add("log.md");
|
|
4484
|
+
const logHook = await observeStage(deps, "log");
|
|
4485
|
+
if (logHook) return phaseFailure("log", input, true, logHook);
|
|
4486
|
+
}
|
|
4487
|
+
}
|
|
4488
|
+
return successReceipt(
|
|
3375
4489
|
input,
|
|
4490
|
+
state.taxonomyAdded,
|
|
4491
|
+
state.pageChanged,
|
|
4492
|
+
state.indexUpdated,
|
|
4493
|
+
logAppended,
|
|
4494
|
+
[...state.changed],
|
|
3376
4495
|
false,
|
|
3377
|
-
|
|
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" })
|
|
4496
|
+
writeReceipt
|
|
3405
4497
|
);
|
|
4498
|
+
} finally {
|
|
4499
|
+
releaseManagedWriteLock(managed.data);
|
|
3406
4500
|
}
|
|
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
4501
|
}
|
|
3419
4502
|
async function runPagePublish(input, deps = DEFAULT_DEPS) {
|
|
3420
4503
|
const prepared = await preparePagePublication(input);
|
|
@@ -3501,7 +4584,7 @@ function buildTypedContent(title, ingested, type, tags, rawRelPath, provenance)
|
|
|
3501
4584
|
}
|
|
3502
4585
|
async function resolveRawCapture(input) {
|
|
3503
4586
|
try {
|
|
3504
|
-
const existing = await
|
|
4587
|
+
const existing = await readFile13(input.path, "utf8");
|
|
3505
4588
|
const frontmatter = extractFrontmatter(existing);
|
|
3506
4589
|
if (!frontmatter.ok) {
|
|
3507
4590
|
return err("INGEST_VALIDATION_FAILED", {
|
|
@@ -3559,7 +4642,7 @@ async function acquireRawCaptureLock(path) {
|
|
|
3559
4642
|
const lockPath = `${path}.ingest.lock`;
|
|
3560
4643
|
for (let attempt = 0; attempt < 200; attempt++) {
|
|
3561
4644
|
try {
|
|
3562
|
-
const handle = await
|
|
4645
|
+
const handle = await open2(lockPath, "wx");
|
|
3563
4646
|
await handle.close();
|
|
3564
4647
|
return ok(lockPath);
|
|
3565
4648
|
} catch (error) {
|
|
@@ -3640,7 +4723,7 @@ async function runIngest(input) {
|
|
|
3640
4723
|
sourceContent = fetchResult.data.body;
|
|
3641
4724
|
} else {
|
|
3642
4725
|
try {
|
|
3643
|
-
sourceContent = await
|
|
4726
|
+
sourceContent = await readFile13(input.source, "utf8");
|
|
3644
4727
|
} catch {
|
|
3645
4728
|
return {
|
|
3646
4729
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
@@ -3665,7 +4748,7 @@ async function runIngest(input) {
|
|
|
3665
4748
|
const rawRelPath = `raw/articles/${slug}.md`;
|
|
3666
4749
|
const typedDir = TYPE_DIR[input.type] ?? `${input.type}s`;
|
|
3667
4750
|
const typedRelPath = `${typedDir}/${slug}.md`;
|
|
3668
|
-
const rawAbsPath =
|
|
4751
|
+
const rawAbsPath = join25(input.vault, rawRelPath);
|
|
3669
4752
|
const identity = assessSourceIdentity({
|
|
3670
4753
|
rawPath: rawRelPath,
|
|
3671
4754
|
sourceUrl: sourceUrl ?? void 0,
|
|
@@ -3709,11 +4792,11 @@ async function runIngest(input) {
|
|
|
3709
4792
|
);
|
|
3710
4793
|
if (!input.dryRun) {
|
|
3711
4794
|
try {
|
|
3712
|
-
await
|
|
4795
|
+
await mkdir10(join25(input.vault, typedDir), { recursive: true });
|
|
3713
4796
|
} catch (error) {
|
|
3714
4797
|
return {
|
|
3715
4798
|
exitCode: ExitCode.WRITE_FAILED,
|
|
3716
|
-
result: err("WRITE_FAILED", { path:
|
|
4799
|
+
result: err("WRITE_FAILED", { path: join25(input.vault, typedDir), message: String(error) })
|
|
3717
4800
|
};
|
|
3718
4801
|
}
|
|
3719
4802
|
}
|
|
@@ -3760,11 +4843,11 @@ async function runIngest(input) {
|
|
|
3760
4843
|
};
|
|
3761
4844
|
}
|
|
3762
4845
|
try {
|
|
3763
|
-
await
|
|
4846
|
+
await mkdir10(join25(input.vault, "raw", "articles"), { recursive: true });
|
|
3764
4847
|
} catch (error) {
|
|
3765
4848
|
return {
|
|
3766
4849
|
exitCode: ExitCode.WRITE_FAILED,
|
|
3767
|
-
result: err("WRITE_FAILED", { path:
|
|
4850
|
+
result: err("WRITE_FAILED", { path: join25(input.vault, "raw", "articles"), message: String(error) })
|
|
3768
4851
|
};
|
|
3769
4852
|
}
|
|
3770
4853
|
const rawWrite = await writeResolvedRaw({
|
|
@@ -3992,8 +5075,8 @@ ${body}`;
|
|
|
3992
5075
|
}
|
|
3993
5076
|
|
|
3994
5077
|
// src/commands/tag-reconcile.ts
|
|
3995
|
-
import { readFile as
|
|
3996
|
-
import { join as
|
|
5078
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
5079
|
+
import { join as join26, posix } from "path";
|
|
3997
5080
|
var TYPED_TARGET_RE = /^(entities|concepts|comparisons|queries|meta)\/[a-z0-9][a-z0-9./_-]*\.md$/;
|
|
3998
5081
|
function errorExitCode2(error) {
|
|
3999
5082
|
switch (error) {
|
|
@@ -4036,7 +5119,7 @@ function asTagArray(frontmatter, path) {
|
|
|
4036
5119
|
async function readTagsFromFile(path) {
|
|
4037
5120
|
let text;
|
|
4038
5121
|
try {
|
|
4039
|
-
text = await
|
|
5122
|
+
text = await readFile14(path, "utf8");
|
|
4040
5123
|
} catch (error) {
|
|
4041
5124
|
if (error.code === "ENOENT") {
|
|
4042
5125
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path }) };
|
|
@@ -4057,7 +5140,7 @@ async function resolveRequestedTags(input, page) {
|
|
|
4057
5140
|
result: err("INVALID_FRONTMATTER", { message: "explicit tags must be an array of strings" })
|
|
4058
5141
|
};
|
|
4059
5142
|
}
|
|
4060
|
-
const source = input.from ?? (explicit.length === 0 ?
|
|
5143
|
+
const source = input.from ?? (explicit.length === 0 ? join26(input.vault, page) : void 0);
|
|
4061
5144
|
if (!source) return { exitCode: ExitCode.OK, result: ok({ tags: [...new Set(explicit)].sort() }) };
|
|
4062
5145
|
const sourced = await readTagsFromFile(source);
|
|
4063
5146
|
if (!sourced.result.ok) return { exitCode: sourced.exitCode, result: sourced.result };
|
|
@@ -4068,7 +5151,7 @@ async function resolveRequestedTags(input, page) {
|
|
|
4068
5151
|
}
|
|
4069
5152
|
async function readSchema(schemaPath) {
|
|
4070
5153
|
try {
|
|
4071
|
-
return { exitCode: ExitCode.OK, result: ok(await
|
|
5154
|
+
return { exitCode: ExitCode.OK, result: ok(await readFile14(schemaPath, "utf8")) };
|
|
4072
5155
|
} catch (error) {
|
|
4073
5156
|
if (error.code === "ENOENT") {
|
|
4074
5157
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: schemaPath }) };
|
|
@@ -4096,7 +5179,7 @@ function previewResult(page, tags, reconciled, dryRun, filesChanged) {
|
|
|
4096
5179
|
};
|
|
4097
5180
|
}
|
|
4098
5181
|
async function reconcileTagsWhileLocked(input, page, tags, comment) {
|
|
4099
|
-
const schemaPath =
|
|
5182
|
+
const schemaPath = join26(input.vault, "SCHEMA.md");
|
|
4100
5183
|
const current = await readSchema(schemaPath);
|
|
4101
5184
|
if (!current.result.ok) return { exitCode: current.exitCode, result: current.result };
|
|
4102
5185
|
const next = reconcileTaxonomyDocument(current.result.data, { tags, comment });
|
|
@@ -4107,7 +5190,7 @@ async function reconcileTagsWhileLocked(input, page, tags, comment) {
|
|
|
4107
5190
|
}
|
|
4108
5191
|
let verifiedText;
|
|
4109
5192
|
try {
|
|
4110
|
-
verifiedText = await
|
|
5193
|
+
verifiedText = await readFile14(schemaPath, "utf8");
|
|
4111
5194
|
} catch (error) {
|
|
4112
5195
|
return {
|
|
4113
5196
|
exitCode: ExitCode.WRITE_FAILED,
|
|
@@ -4133,7 +5216,7 @@ async function runTagReconcile(input) {
|
|
|
4133
5216
|
const comment = taxonomyCommentForPage(page.data, date, input.reason);
|
|
4134
5217
|
if (!comment.ok) return { exitCode: ExitCode.SCHEME_REJECTED, result: comment };
|
|
4135
5218
|
if (!input.write) {
|
|
4136
|
-
const schema = await readSchema(
|
|
5219
|
+
const schema = await readSchema(join26(input.vault, "SCHEMA.md"));
|
|
4137
5220
|
if (!schema.result.ok) return { exitCode: schema.exitCode, result: schema.result };
|
|
4138
5221
|
const preview = reconcileTaxonomyDocument(schema.result.data, { tags, comment: comment.data });
|
|
4139
5222
|
return previewResult(page.data, tags, preview, true, []);
|
|
@@ -4185,22 +5268,9 @@ async function runTagReconcile(input) {
|
|
|
4185
5268
|
}
|
|
4186
5269
|
|
|
4187
5270
|
// src/commands/sync.ts
|
|
4188
|
-
import { existsSync as
|
|
4189
|
-
import { join as
|
|
4190
|
-
import { execFileSync as
|
|
4191
|
-
|
|
4192
|
-
// src/utils/git.ts
|
|
4193
|
-
import { execFileSync } from "child_process";
|
|
4194
|
-
function git(cwd, args) {
|
|
4195
|
-
try {
|
|
4196
|
-
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
4197
|
-
} catch {
|
|
4198
|
-
return "";
|
|
4199
|
-
}
|
|
4200
|
-
}
|
|
4201
|
-
function gitStrict(cwd, args) {
|
|
4202
|
-
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
4203
|
-
}
|
|
5271
|
+
import { existsSync as existsSync6 } from "fs";
|
|
5272
|
+
import { join as join27 } from "path";
|
|
5273
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
4204
5274
|
|
|
4205
5275
|
// src/utils/vault-git-pathspec.ts
|
|
4206
5276
|
var VAULT_GENERATED_COMMIT_PATHS = [
|
|
@@ -4241,7 +5311,7 @@ function isTrackedNotePath(path) {
|
|
|
4241
5311
|
}
|
|
4242
5312
|
function refHasPath(vault, ref, path) {
|
|
4243
5313
|
try {
|
|
4244
|
-
|
|
5314
|
+
execFileSync3("git", ["cat-file", "-e", `${ref}:${path}`], {
|
|
4245
5315
|
cwd: vault,
|
|
4246
5316
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4247
5317
|
});
|
|
@@ -4253,7 +5323,7 @@ function refHasPath(vault, ref, path) {
|
|
|
4253
5323
|
function runSyncStatus(input) {
|
|
4254
5324
|
const vault = input.vault;
|
|
4255
5325
|
const includeStashes = input.includeStashes ?? false;
|
|
4256
|
-
if (!
|
|
5326
|
+
if (!existsSync6(join27(vault, ".git"))) {
|
|
4257
5327
|
return {
|
|
4258
5328
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4259
5329
|
result: ok({
|
|
@@ -4360,7 +5430,7 @@ function runSyncStatus(input) {
|
|
|
4360
5430
|
}
|
|
4361
5431
|
async function runSyncPush(input) {
|
|
4362
5432
|
const vault = input.vault;
|
|
4363
|
-
if (!
|
|
5433
|
+
if (!existsSync6(join27(vault, ".git"))) {
|
|
4364
5434
|
return {
|
|
4365
5435
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4366
5436
|
result: err("NOT_A_GIT_REPO", { path: vault })
|
|
@@ -4520,100 +5590,44 @@ function enableGitLongPathsOnWindows(vault) {
|
|
|
4520
5590
|
}
|
|
4521
5591
|
async function runSyncPull(input) {
|
|
4522
5592
|
const vault = input.vault;
|
|
4523
|
-
if (!
|
|
5593
|
+
if (!existsSync6(join27(vault, ".git"))) {
|
|
4524
5594
|
return {
|
|
4525
5595
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4526
5596
|
result: err("NOT_A_GIT_REPO", { path: vault })
|
|
4527
5597
|
};
|
|
4528
5598
|
}
|
|
4529
5599
|
enableGitLongPathsOnWindows(vault);
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
gitStrict(vault, ["fetch", "origin"]);
|
|
4533
|
-
fetched = true;
|
|
4534
|
-
} catch (e) {
|
|
5600
|
+
const remoteUrl = git(vault, ["remote", "get-url", "origin"]);
|
|
5601
|
+
if (!remoteUrl) {
|
|
4535
5602
|
return {
|
|
4536
5603
|
exitCode: ExitCode.SYNC_PULL_FAILED,
|
|
4537
|
-
result: err("
|
|
5604
|
+
result: err("GIT_PULL_FAILED", { message: "no remote configured (origin)" })
|
|
4538
5605
|
};
|
|
4539
5606
|
}
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
let autoResolved = 0;
|
|
4544
|
-
try {
|
|
4545
|
-
const pullOutput = gitStrict(vault, ["pull", "--rebase", "origin", "HEAD"]);
|
|
4546
|
-
pulled = true;
|
|
4547
|
-
const fileMatch = pullOutput.match(/(\d+) file[s]? changed/);
|
|
4548
|
-
if (fileMatch) filesUpdated = parseInt(fileMatch[1], 10);
|
|
4549
|
-
} catch (e) {
|
|
4550
|
-
const errString = String(e);
|
|
4551
|
-
if (errString.includes("conflict")) {
|
|
4552
|
-
let inConflict = true;
|
|
4553
|
-
while (inConflict) {
|
|
4554
|
-
const stoppedSha = git(vault, ["rev-parse", "--verify", "REBASE_HEAD"]);
|
|
4555
|
-
let commitMsg = "";
|
|
4556
|
-
if (stoppedSha) {
|
|
4557
|
-
commitMsg = git(vault, ["log", "--format=%s", "-1", stoppedSha]);
|
|
4558
|
-
}
|
|
4559
|
-
const isArchiveOrSnapshot = commitMsg.startsWith("archive: moved") || commitMsg.startsWith("Snapshot ");
|
|
4560
|
-
const conflictedFiles = git(vault, ["diff", "--name-only", "--diff-filter=U"]);
|
|
4561
|
-
const conflictedList = conflictedFiles ? conflictedFiles.split("\n").filter((l) => l.trim().length > 0) : [];
|
|
4562
|
-
if (conflictedList.length === 0) {
|
|
4563
|
-
try {
|
|
4564
|
-
gitStrict(vault, ["rebase", "--continue"]);
|
|
4565
|
-
inConflict = true;
|
|
4566
|
-
} catch {
|
|
4567
|
-
inConflict = false;
|
|
4568
|
-
}
|
|
4569
|
-
continue;
|
|
4570
|
-
}
|
|
4571
|
-
if (isArchiveOrSnapshot) {
|
|
4572
|
-
for (const f of conflictedList) {
|
|
4573
|
-
try {
|
|
4574
|
-
gitStrict(vault, ["checkout", "--ours", f]);
|
|
4575
|
-
gitStrict(vault, ["add", f]);
|
|
4576
|
-
} catch {
|
|
4577
|
-
}
|
|
4578
|
-
}
|
|
4579
|
-
autoResolved += conflictedList.length;
|
|
4580
|
-
try {
|
|
4581
|
-
gitStrict(vault, ["rebase", "--continue"]);
|
|
4582
|
-
} catch (continueErr) {
|
|
4583
|
-
continue;
|
|
4584
|
-
}
|
|
4585
|
-
} else {
|
|
4586
|
-
conflicts = conflictedList.length;
|
|
4587
|
-
return {
|
|
4588
|
-
exitCode: ExitCode.SYNC_PULL_FAILED,
|
|
4589
|
-
result: ok({
|
|
4590
|
-
fetched,
|
|
4591
|
-
pulled: false,
|
|
4592
|
-
files_updated: 0,
|
|
4593
|
-
conflicts,
|
|
4594
|
-
auto_resolved: 0,
|
|
4595
|
-
lint_errors: 0,
|
|
4596
|
-
lint_warnings: 0,
|
|
4597
|
-
humanHint: `pull failed with ${conflicts} conflict(s) on non-archive commit "${commitMsg}" \u2014 resolve manually`
|
|
4598
|
-
})
|
|
4599
|
-
};
|
|
4600
|
-
}
|
|
4601
|
-
}
|
|
4602
|
-
if (autoResolved > 0) {
|
|
4603
|
-
const diffOutput = git(vault, ["diff", "--stat", "HEAD@{1}..HEAD"]);
|
|
4604
|
-
if (diffOutput) {
|
|
4605
|
-
const fileMatch = diffOutput.match(/(\d+) file[s]? changed/);
|
|
4606
|
-
if (fileMatch) filesUpdated = parseInt(fileMatch[1], 10);
|
|
4607
|
-
}
|
|
4608
|
-
pulled = true;
|
|
4609
|
-
conflicts = 0;
|
|
4610
|
-
}
|
|
4611
|
-
} else {
|
|
5607
|
+
const helper = await runVaultSyncPullHelper({ vault, remote: "origin", branch: "main" });
|
|
5608
|
+
if (!helper.ok) {
|
|
5609
|
+
if (helper.error === "PREFLIGHT_FAILED") {
|
|
4612
5610
|
return {
|
|
4613
|
-
exitCode: ExitCode.
|
|
4614
|
-
result: err("
|
|
5611
|
+
exitCode: ExitCode.PREFLIGHT_FAILED,
|
|
5612
|
+
result: err("PREFLIGHT_FAILED", helper.detail)
|
|
4615
5613
|
};
|
|
4616
5614
|
}
|
|
5615
|
+
return {
|
|
5616
|
+
exitCode: ExitCode.SYNC_PULL_FAILED,
|
|
5617
|
+
result: err("GIT_PULL_FAILED", helper.detail)
|
|
5618
|
+
};
|
|
5619
|
+
}
|
|
5620
|
+
const fetched = true;
|
|
5621
|
+
const pulled = helper.data.changed;
|
|
5622
|
+
const conflicts = 0;
|
|
5623
|
+
const autoResolved = 0;
|
|
5624
|
+
let filesUpdated = 0;
|
|
5625
|
+
if (helper.data.changed) {
|
|
5626
|
+
const diffOutput = git(vault, ["diff", "--stat", `${helper.data.before_oid}..${helper.data.after_oid}`]);
|
|
5627
|
+
if (diffOutput) {
|
|
5628
|
+
const fileMatch = diffOutput.match(/(\d+) file[s]? changed/);
|
|
5629
|
+
if (fileMatch) filesUpdated = parseInt(fileMatch[1], 10);
|
|
5630
|
+
}
|
|
4617
5631
|
}
|
|
4618
5632
|
const pathFix = await fixPathTooLong({ vault });
|
|
4619
5633
|
const pathFixCount = pathFix.result.ok ? pathFix.result.data.fixed.length : 0;
|
|
@@ -4626,8 +5640,7 @@ async function runSyncPull(input) {
|
|
|
4626
5640
|
}
|
|
4627
5641
|
const hintParts = [];
|
|
4628
5642
|
if (filesUpdated > 0) hintParts.push(`updated ${filesUpdated} file(s)`);
|
|
4629
|
-
else hintParts.push("already up to date");
|
|
4630
|
-
if (autoResolved > 0) hintParts.push(`${autoResolved} conflict(s) auto-resolved`);
|
|
5643
|
+
else hintParts.push(pulled ? "pulled via vault-sync helper" : "already up to date");
|
|
4631
5644
|
if (pathFixCount > 0) hintParts.push(`${pathFixCount} long path(s) fixed`);
|
|
4632
5645
|
if (lintErrors > 0) hintParts.push(`${lintErrors} lint error(s)`);
|
|
4633
5646
|
if (lintWarnings > 0) hintParts.push(`${lintWarnings} lint warning(s)`);
|
|
@@ -4692,7 +5705,7 @@ function runSyncPeers(input) {
|
|
|
4692
5705
|
}
|
|
4693
5706
|
function runSyncLock(input) {
|
|
4694
5707
|
const vault = input.vault;
|
|
4695
|
-
if (!
|
|
5708
|
+
if (!existsSync6(vault)) {
|
|
4696
5709
|
return {
|
|
4697
5710
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4698
5711
|
result: err("VAULT_PATH_INVALID", { path: vault })
|
|
@@ -4727,7 +5740,7 @@ function runSyncLock(input) {
|
|
|
4727
5740
|
}
|
|
4728
5741
|
function runSyncUnlock(input) {
|
|
4729
5742
|
const vault = input.vault;
|
|
4730
|
-
if (!
|
|
5743
|
+
if (!existsSync6(vault)) {
|
|
4731
5744
|
return {
|
|
4732
5745
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4733
5746
|
result: err("VAULT_PATH_INVALID", { path: vault })
|
|
@@ -4760,8 +5773,8 @@ function runSyncUnlock(input) {
|
|
|
4760
5773
|
}
|
|
4761
5774
|
|
|
4762
5775
|
// src/commands/backup.ts
|
|
4763
|
-
import { statSync as statSync2, readdirSync, readFileSync as
|
|
4764
|
-
import { join as
|
|
5776
|
+
import { statSync as statSync2, readdirSync, readFileSync as readFileSync11, mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
5777
|
+
import { join as join28, relative as relative2, dirname as dirname6 } from "path";
|
|
4765
5778
|
import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
|
|
4766
5779
|
|
|
4767
5780
|
// src/utils/s3-client.ts
|
|
@@ -4785,7 +5798,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node_
|
|
|
4785
5798
|
function* walkMarkdown(dir, base) {
|
|
4786
5799
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
4787
5800
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
4788
|
-
const full =
|
|
5801
|
+
const full = join28(dir, entry.name);
|
|
4789
5802
|
if (entry.isDirectory()) {
|
|
4790
5803
|
yield* walkMarkdown(full, base);
|
|
4791
5804
|
} else if (entry.name.endsWith(".md")) {
|
|
@@ -4808,7 +5821,7 @@ async function runBackupSync(input) {
|
|
|
4808
5821
|
let failed = 0;
|
|
4809
5822
|
const files = [...walkMarkdown(input.vault, input.vault)];
|
|
4810
5823
|
for (const relPath of files) {
|
|
4811
|
-
const absPath =
|
|
5824
|
+
const absPath = join28(input.vault, relPath);
|
|
4812
5825
|
const localStat = statSync2(absPath);
|
|
4813
5826
|
let needsUpload = true;
|
|
4814
5827
|
try {
|
|
@@ -4827,7 +5840,7 @@ async function runBackupSync(input) {
|
|
|
4827
5840
|
continue;
|
|
4828
5841
|
}
|
|
4829
5842
|
try {
|
|
4830
|
-
const body =
|
|
5843
|
+
const body = readFileSync11(absPath);
|
|
4831
5844
|
await client.send(new PutObjectCommand({ Bucket: input.bucket, Key: relPath, Body: body }));
|
|
4832
5845
|
uploaded++;
|
|
4833
5846
|
} catch {
|
|
@@ -4884,7 +5897,7 @@ async function runBackupRestore(input) {
|
|
|
4884
5897
|
const objects = list.Contents ?? [];
|
|
4885
5898
|
for (const obj of objects) {
|
|
4886
5899
|
if (!obj.Key) continue;
|
|
4887
|
-
const localPath =
|
|
5900
|
+
const localPath = join28(target, obj.Key);
|
|
4888
5901
|
try {
|
|
4889
5902
|
const localStat = statSync2(localPath);
|
|
4890
5903
|
if (obj.LastModified && localStat.mtime > obj.LastModified) {
|
|
@@ -4898,7 +5911,7 @@ async function runBackupRestore(input) {
|
|
|
4898
5911
|
const body = await resp.Body?.transformToByteArray();
|
|
4899
5912
|
if (body) {
|
|
4900
5913
|
mkdirSync2(dirname6(localPath), { recursive: true });
|
|
4901
|
-
|
|
5914
|
+
writeFileSync4(localPath, Buffer.from(body));
|
|
4902
5915
|
downloaded++;
|
|
4903
5916
|
}
|
|
4904
5917
|
} catch {
|
|
@@ -4930,11 +5943,11 @@ async function runBackupRestore(input) {
|
|
|
4930
5943
|
}
|
|
4931
5944
|
|
|
4932
5945
|
// src/commands/status.ts
|
|
4933
|
-
import { existsSync as
|
|
4934
|
-
import { readFile as
|
|
4935
|
-
import { join as
|
|
5946
|
+
import { existsSync as existsSync7, statSync as statSync3 } from "fs";
|
|
5947
|
+
import { readFile as readFile15 } from "fs/promises";
|
|
5948
|
+
import { join as join29 } from "path";
|
|
4936
5949
|
async function runStatus(input) {
|
|
4937
|
-
if (!
|
|
5950
|
+
if (!existsSync7(input.vault)) {
|
|
4938
5951
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
4939
5952
|
}
|
|
4940
5953
|
const scan = await scanVault(input.vault);
|
|
@@ -4959,7 +5972,7 @@ async function runStatus(input) {
|
|
|
4959
5972
|
const compound = scan.data.compound.length;
|
|
4960
5973
|
let schemaVersion = "v1";
|
|
4961
5974
|
try {
|
|
4962
|
-
const schemaContent = await
|
|
5975
|
+
const schemaContent = await readFile15(join29(input.vault, "SCHEMA.md"), "utf8");
|
|
4963
5976
|
const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
|
|
4964
5977
|
if (versionMatch) schemaVersion = versionMatch[1];
|
|
4965
5978
|
} catch {
|
|
@@ -5019,8 +6032,8 @@ async function runStatus(input) {
|
|
|
5019
6032
|
}
|
|
5020
6033
|
|
|
5021
6034
|
// src/commands/seed.ts
|
|
5022
|
-
import { mkdir as
|
|
5023
|
-
import { join as
|
|
6035
|
+
import { mkdir as mkdir11, writeFile as writeFile9, stat as stat4 } from "fs/promises";
|
|
6036
|
+
import { join as join30 } from "path";
|
|
5024
6037
|
var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
5025
6038
|
var EXAMPLE_PAGES = {
|
|
5026
6039
|
"entities/example-project.md": `---
|
|
@@ -5089,29 +6102,29 @@ Real sources are immutable after ingestion \u2014 never edit them.
|
|
|
5089
6102
|
`;
|
|
5090
6103
|
async function runSeed(input) {
|
|
5091
6104
|
try {
|
|
5092
|
-
await stat4(
|
|
6105
|
+
await stat4(join30(input.vault, "SCHEMA.md"));
|
|
5093
6106
|
} catch {
|
|
5094
6107
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
|
|
5095
6108
|
}
|
|
5096
6109
|
const created = [];
|
|
5097
6110
|
const skipped = [];
|
|
5098
6111
|
for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
|
|
5099
|
-
const absPath =
|
|
6112
|
+
const absPath = join30(input.vault, relPath);
|
|
5100
6113
|
try {
|
|
5101
6114
|
await stat4(absPath);
|
|
5102
6115
|
skipped.push(relPath);
|
|
5103
6116
|
} catch {
|
|
5104
|
-
await
|
|
6117
|
+
await mkdir11(join30(absPath, ".."), { recursive: true });
|
|
5105
6118
|
await writeFile9(absPath, content, "utf8");
|
|
5106
6119
|
created.push(relPath);
|
|
5107
6120
|
}
|
|
5108
6121
|
}
|
|
5109
|
-
const rawPath =
|
|
6122
|
+
const rawPath = join30(input.vault, "raw", "articles", "example-source.md");
|
|
5110
6123
|
try {
|
|
5111
6124
|
await stat4(rawPath);
|
|
5112
6125
|
skipped.push("raw/articles/example-source.md");
|
|
5113
6126
|
} catch {
|
|
5114
|
-
await
|
|
6127
|
+
await mkdir11(join30(rawPath, ".."), { recursive: true });
|
|
5115
6128
|
await writeFile9(rawPath, EXAMPLE_RAW, "utf8");
|
|
5116
6129
|
created.push("raw/articles/example-source.md");
|
|
5117
6130
|
}
|
|
@@ -5134,9 +6147,9 @@ async function runSeed(input) {
|
|
|
5134
6147
|
}
|
|
5135
6148
|
|
|
5136
6149
|
// src/commands/canvas.ts
|
|
5137
|
-
import { readFile as
|
|
5138
|
-
import { existsSync as
|
|
5139
|
-
import { join as
|
|
6150
|
+
import { readFile as readFile16, writeFile as writeFile10 } from "fs/promises";
|
|
6151
|
+
import { existsSync as existsSync8 } from "fs";
|
|
6152
|
+
import { join as join31 } from "path";
|
|
5140
6153
|
var NODE_WIDTH = 240;
|
|
5141
6154
|
var NODE_HEIGHT = 60;
|
|
5142
6155
|
var COLUMN_SPACING = 400;
|
|
@@ -5214,8 +6227,8 @@ function buildCanvasEdges(adjacency) {
|
|
|
5214
6227
|
return edges;
|
|
5215
6228
|
}
|
|
5216
6229
|
async function runCanvasGenerate(input) {
|
|
5217
|
-
const graphPath = input.graphPath ??
|
|
5218
|
-
if (!
|
|
6230
|
+
const graphPath = input.graphPath ?? join31(input.vault, ".skillwiki", "graph.json");
|
|
6231
|
+
if (!existsSync8(graphPath)) {
|
|
5219
6232
|
return {
|
|
5220
6233
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
5221
6234
|
result: err("FILE_NOT_FOUND", {
|
|
@@ -5226,7 +6239,7 @@ async function runCanvasGenerate(input) {
|
|
|
5226
6239
|
}
|
|
5227
6240
|
let raw;
|
|
5228
6241
|
try {
|
|
5229
|
-
raw = await
|
|
6242
|
+
raw = await readFile16(graphPath, "utf8");
|
|
5230
6243
|
} catch (e) {
|
|
5231
6244
|
return {
|
|
5232
6245
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
@@ -5252,7 +6265,7 @@ async function runCanvasGenerate(input) {
|
|
|
5252
6265
|
const nodes = buildCanvasNodes(paths);
|
|
5253
6266
|
const edges = buildCanvasEdges(graph.adjacency);
|
|
5254
6267
|
const canvas = { nodes, edges };
|
|
5255
|
-
const outPath =
|
|
6268
|
+
const outPath = join31(input.vault, "vault-graph.canvas");
|
|
5256
6269
|
try {
|
|
5257
6270
|
await writeFile10(outPath, JSON.stringify(canvas, null, 2));
|
|
5258
6271
|
} catch (e) {
|
|
@@ -5274,15 +6287,15 @@ written: ${outPath}`
|
|
|
5274
6287
|
}
|
|
5275
6288
|
|
|
5276
6289
|
// src/commands/fleet-health.ts
|
|
5277
|
-
import { existsSync as
|
|
6290
|
+
import { existsSync as existsSync9, readFileSync as readFileSync12 } from "fs";
|
|
5278
6291
|
import { execSync as nodeExecSync } from "child_process";
|
|
5279
6292
|
import { hostname as nodeHostname, platform as nodePlatform } from "os";
|
|
5280
|
-
import { join as
|
|
6293
|
+
import { join as join32 } from "path";
|
|
5281
6294
|
var SSH_TIMEOUT_MS = 15e3;
|
|
5282
6295
|
var TIMER_UNIT = "agent-memory-trends.timer";
|
|
5283
6296
|
var SERVICE_UNIT = "agent-memory-trends.service";
|
|
5284
6297
|
var SYSTEMD_SERVICE_FAILED_CLASS = "SYSTEMD_SERVICE_FAILED";
|
|
5285
|
-
function
|
|
6298
|
+
function defaultDeps2() {
|
|
5286
6299
|
return {
|
|
5287
6300
|
platform: () => nodePlatform(),
|
|
5288
6301
|
execSync: nodeExecSync
|
|
@@ -5374,9 +6387,9 @@ function applyServiceFailedOverlay(run, serviceFailed) {
|
|
|
5374
6387
|
function probeLocal(vaultPath, deps) {
|
|
5375
6388
|
const latestPath = satelliteLatestRunPath(vaultPath);
|
|
5376
6389
|
let parsed = null;
|
|
5377
|
-
if (
|
|
6390
|
+
if (existsSync9(latestPath)) {
|
|
5378
6391
|
try {
|
|
5379
|
-
const wire = readSatelliteLatestRunFromText(
|
|
6392
|
+
const wire = readSatelliteLatestRunFromText(readFileSync12(latestPath, "utf8"));
|
|
5380
6393
|
if (wire) {
|
|
5381
6394
|
parsed = {
|
|
5382
6395
|
status: wire.status,
|
|
@@ -5502,12 +6515,12 @@ function rowHealthy(reachable, timer, runUnhealthy, timerBad, platform2) {
|
|
|
5502
6515
|
return true;
|
|
5503
6516
|
}
|
|
5504
6517
|
async function runFleetHealth(input) {
|
|
5505
|
-
const deps = input.deps ??
|
|
6518
|
+
const deps = input.deps ?? defaultDeps2();
|
|
5506
6519
|
const env = input.env ?? process.env;
|
|
5507
6520
|
const home = input.home ?? env.HOME ?? "";
|
|
5508
6521
|
const osHostname = input.osHostname ?? env.HOSTNAME ?? nodeHostname();
|
|
5509
6522
|
const vault = input.vault ?? env.WIKI_PATH;
|
|
5510
|
-
const file = input.file ?? (vault ?
|
|
6523
|
+
const file = input.file ?? (vault ? join32(vault, FLEET_REL_PATH) : void 0);
|
|
5511
6524
|
if (!file) {
|
|
5512
6525
|
return {
|
|
5513
6526
|
exitCode: ExitCode.NO_VAULT_CONFIGURED,
|
|
@@ -5591,15 +6604,15 @@ async function runFleetHealth(input) {
|
|
|
5591
6604
|
}
|
|
5592
6605
|
|
|
5593
6606
|
// src/utils/auto-commit.ts
|
|
5594
|
-
import { existsSync as
|
|
5595
|
-
import { join as
|
|
6607
|
+
import { existsSync as existsSync10 } from "fs";
|
|
6608
|
+
import { join as join33 } from "path";
|
|
5596
6609
|
async function postCommit(vault, exitCode) {
|
|
5597
6610
|
if (exitCode !== 0) return;
|
|
5598
6611
|
const home = process.env.HOME ?? "";
|
|
5599
6612
|
const dotenv = await parseDotenvFile(configPath(home));
|
|
5600
6613
|
const autoCommit = process.env.AUTO_COMMIT ?? dotenv["AUTO_COMMIT"];
|
|
5601
6614
|
if (autoCommit === "false") return;
|
|
5602
|
-
if (!
|
|
6615
|
+
if (!existsSync10(join33(vault, ".git"))) return;
|
|
5603
6616
|
const lastOps = readLastOp(vault);
|
|
5604
6617
|
if (lastOps.length === 0) return;
|
|
5605
6618
|
const porcelain = git(vault, ["status", "--porcelain", "--", ...VAULT_COMMIT_PATHSPEC]);
|
|
@@ -5622,103 +6635,6 @@ async function postCommit(vault, exitCode) {
|
|
|
5622
6635
|
clearLastOp(vault);
|
|
5623
6636
|
}
|
|
5624
6637
|
|
|
5625
|
-
// src/utils/protected-vault-write-guard.ts
|
|
5626
|
-
import { readFileSync as readFileSync6 } from "fs";
|
|
5627
|
-
import { join as join26, resolve as resolvePath } from "path";
|
|
5628
|
-
async function guardProtectedVaultWrite(input) {
|
|
5629
|
-
const env = input.env ?? process.env;
|
|
5630
|
-
const home = input.home ?? process.env.HOME ?? "";
|
|
5631
|
-
const cwd = input.cwd ?? process.cwd();
|
|
5632
|
-
const liveVaultPath = await resolveLiveVaultPath({ env, home, cwd });
|
|
5633
|
-
const load = await loadFleetManifestAndHost({
|
|
5634
|
-
vault: liveVaultPath ?? input.vault,
|
|
5635
|
-
hostId: input.hostId,
|
|
5636
|
-
env,
|
|
5637
|
-
home,
|
|
5638
|
-
cwd,
|
|
5639
|
-
osHostname: input.osHostname ?? process.env.HOSTNAME,
|
|
5640
|
-
user: input.user ?? process.env.USER
|
|
5641
|
-
});
|
|
5642
|
-
if (!load?.hostId || load.identityStatus !== "known") {
|
|
5643
|
-
return { blocked: false };
|
|
5644
|
-
}
|
|
5645
|
-
const host = load.manifest.hosts[load.hostId];
|
|
5646
|
-
if (!host || host.role !== "snapshotter" || host.protected !== true) {
|
|
5647
|
-
return { blocked: false };
|
|
5648
|
-
}
|
|
5649
|
-
const snapshotWorktree = resolveSnapshotWorktree(home);
|
|
5650
|
-
const targetVault = resolvePath(input.vault);
|
|
5651
|
-
const canonicalLiveVault = liveVaultPath ? resolvePath(liveVaultPath) : void 0;
|
|
5652
|
-
const canonicalSnapshotWorktree = snapshotWorktree ? resolvePath(snapshotWorktree) : void 0;
|
|
5653
|
-
if (canonicalLiveVault && targetVault === canonicalLiveVault) {
|
|
5654
|
-
return { blocked: false };
|
|
5655
|
-
}
|
|
5656
|
-
if (canonicalSnapshotWorktree && targetVault === canonicalSnapshotWorktree) {
|
|
5657
|
-
return {
|
|
5658
|
-
blocked: true,
|
|
5659
|
-
exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED,
|
|
5660
|
-
result: err("PROTECTED_SNAPSHOTTER_WRITE_BLOCKED", {
|
|
5661
|
-
host_id: load.hostId,
|
|
5662
|
-
command: input.command,
|
|
5663
|
-
reason: `refusing mutation of snapshot worktree '${canonicalSnapshotWorktree}' on protected snapshotter host '${load.hostId}'`,
|
|
5664
|
-
guidance: canonicalLiveVault ? `Use the live vault path '${canonicalLiveVault}' for authoring. Keep snapshot worktrees read-only except explicit snapshot maintenance.` : "Use the live skillwiki vault path for authoring. Keep snapshot worktrees read-only except explicit snapshot maintenance."
|
|
5665
|
-
})
|
|
5666
|
-
};
|
|
5667
|
-
}
|
|
5668
|
-
if (canonicalLiveVault && targetVault !== canonicalLiveVault) {
|
|
5669
|
-
return {
|
|
5670
|
-
blocked: true,
|
|
5671
|
-
exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED,
|
|
5672
|
-
result: err("PROTECTED_SNAPSHOTTER_WRITE_BLOCKED", {
|
|
5673
|
-
host_id: load.hostId,
|
|
5674
|
-
command: input.command,
|
|
5675
|
-
reason: `refusing vault mutation outside the live vault path '${canonicalLiveVault}' on protected snapshotter host '${load.hostId}'`,
|
|
5676
|
-
guidance: "Use the resolved live skillwiki vault path for authoring. Keep alternate vault roots and snapshot worktrees read-only unless explicitly approved."
|
|
5677
|
-
})
|
|
5678
|
-
};
|
|
5679
|
-
}
|
|
5680
|
-
return { blocked: false };
|
|
5681
|
-
}
|
|
5682
|
-
async function resolveLiveVaultPath(input) {
|
|
5683
|
-
const resolved = await resolveRuntimePath({
|
|
5684
|
-
flag: void 0,
|
|
5685
|
-
envValue: input.env.WIKI_PATH,
|
|
5686
|
-
wikiEnv: input.env.WIKI,
|
|
5687
|
-
home: input.home,
|
|
5688
|
-
cwd: input.cwd
|
|
5689
|
-
});
|
|
5690
|
-
return resolved.ok ? resolved.data.path : void 0;
|
|
5691
|
-
}
|
|
5692
|
-
function resolveSnapshotWorktree(home) {
|
|
5693
|
-
const skillwikiEnv = join26(home, ".skillwiki", ".env");
|
|
5694
|
-
const explicitWorktree = readEnvKey(skillwikiEnv, ["vault_sync.snapshot_worktree"]);
|
|
5695
|
-
if (explicitWorktree) return explicitWorktree;
|
|
5696
|
-
const snapshotProfile = readEnvKey(skillwikiEnv, ["vault_sync.snapshot_profile"]);
|
|
5697
|
-
if (snapshotProfile) {
|
|
5698
|
-
const fromProfile = readEnvKey(snapshotProfile, ["WIKI_GIT_WORKTREE", "SNAPSHOT_WORKTREE", "GIT_DIR"]);
|
|
5699
|
-
if (fromProfile) return fromProfile;
|
|
5700
|
-
}
|
|
5701
|
-
return "/root/wiki-git";
|
|
5702
|
-
}
|
|
5703
|
-
function readEnvKey(path, keys) {
|
|
5704
|
-
try {
|
|
5705
|
-
const content = readFileSync6(path, "utf8");
|
|
5706
|
-
for (const line of content.split(/\r?\n/)) {
|
|
5707
|
-
const trimmed = line.trim();
|
|
5708
|
-
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
|
|
5709
|
-
const eq = trimmed.indexOf("=");
|
|
5710
|
-
if (eq <= 0) continue;
|
|
5711
|
-
const key = trimmed.slice(0, eq).trim();
|
|
5712
|
-
if (!keys.includes(key)) continue;
|
|
5713
|
-
const value = trimmed.slice(eq + 1).trim();
|
|
5714
|
-
if (value.length > 0) return value;
|
|
5715
|
-
}
|
|
5716
|
-
} catch {
|
|
5717
|
-
return void 0;
|
|
5718
|
-
}
|
|
5719
|
-
return void 0;
|
|
5720
|
-
}
|
|
5721
|
-
|
|
5722
6638
|
// src/cli.ts
|
|
5723
6639
|
var pkg = readCliPackageJson();
|
|
5724
6640
|
var program = new Command();
|
|
@@ -5745,6 +6661,28 @@ async function emitGuardedVaultWrite(vault, command, run, opts) {
|
|
|
5745
6661
|
}
|
|
5746
6662
|
return emit(await run(), vault, opts);
|
|
5747
6663
|
}
|
|
6664
|
+
async function emitManagedVaultWrite(vault, command, mutate, opts) {
|
|
6665
|
+
const guard = await guardProtectedVaultWrite({
|
|
6666
|
+
vault,
|
|
6667
|
+
command,
|
|
6668
|
+
env: process.env,
|
|
6669
|
+
home: process.env.HOME ?? "",
|
|
6670
|
+
cwd: process.cwd(),
|
|
6671
|
+
osHostname: process.env.HOSTNAME,
|
|
6672
|
+
user: process.env.USER
|
|
6673
|
+
});
|
|
6674
|
+
if (guard.blocked) {
|
|
6675
|
+
return emit({ exitCode: guard.exitCode, result: guard.result }, void 0, { postCommit: false });
|
|
6676
|
+
}
|
|
6677
|
+
const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-SILEUQEV.js");
|
|
6678
|
+
const run = await runManagedWriteTransaction2({
|
|
6679
|
+
vault,
|
|
6680
|
+
command,
|
|
6681
|
+
allowImmutableRecord: opts?.allowImmutableRecord === true,
|
|
6682
|
+
mutate: async (receipt) => await mutate(receipt)
|
|
6683
|
+
});
|
|
6684
|
+
return emit(run, vault, opts);
|
|
6685
|
+
}
|
|
5748
6686
|
program.command("hash <file>").description("compute SHA-256 hash of a vault page body").action(async (file) => emit(await runHash({ file })));
|
|
5749
6687
|
program.command("fetch-guard <url>").description("check if a URL passes fetch guard rules and sanitize secrets").action(async (url) => emit(await runFetchGuard({ url })));
|
|
5750
6688
|
program.command("validate <file>").description("validate vault page frontmatter against its detected schema").option("--apply", "auto-update vault index.md and log.md after successful validation", false).option("--vault <dir>", "vault root directory (required with --apply)").option("--wiki <name>", "wiki profile name").action(async (file, opts) => {
|
|
@@ -5755,12 +6693,12 @@ program.command("validate <file>").description("validate vault page frontmatter
|
|
|
5755
6693
|
else vault = v.vault;
|
|
5756
6694
|
}
|
|
5757
6695
|
if (opts.apply && vault) {
|
|
5758
|
-
return
|
|
6696
|
+
return emitManagedVaultWrite(vault, "validate --apply", () => runValidate({ file, apply: true, vault }), void 0);
|
|
5759
6697
|
}
|
|
5760
6698
|
emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
|
|
5761
6699
|
});
|
|
5762
6700
|
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) => {
|
|
5763
|
-
const out = opts.out ??
|
|
6701
|
+
const out = opts.out ?? join34(vault, ".skillwiki", "graph.json");
|
|
5764
6702
|
return emitGuardedVaultWrite(vault, "graph build", () => runGraphBuild({ vault, out }));
|
|
5765
6703
|
});
|
|
5766
6704
|
var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
|
|
@@ -5893,6 +6831,12 @@ pageCmd.command("publish <draft> [vault]").description("preview or publish an un
|
|
|
5893
6831
|
{ postCommit: false }
|
|
5894
6832
|
);
|
|
5895
6833
|
});
|
|
6834
|
+
var indexCmd = program.command("index").description("root index projection and checks");
|
|
6835
|
+
indexCmd.command("rebuild [vault]").description("preview or write deterministic root index.md projection").option("--write", "write projected index.md", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6836
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6837
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6838
|
+
else emit(await runIndexRebuild({ vault: v.vault, write: Boolean(opts.write) }), v.vault);
|
|
6839
|
+
});
|
|
5896
6840
|
program.command("index-check [vault]").description("verify index.md entries match actual vault pages").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5897
6841
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5898
6842
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
@@ -5945,12 +6889,47 @@ program.command("log-rotate [vault]").description("rotate or trim the vault log
|
|
|
5945
6889
|
program.command("log-append [vault]").description("append a single entry to the vault log under a short advisory lock").requiredOption("--content <text>", "log entry text to append").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5946
6890
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5947
6891
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5948
|
-
else return
|
|
6892
|
+
else return emitManagedVaultWrite(
|
|
5949
6893
|
v.vault,
|
|
5950
6894
|
"log-append",
|
|
5951
6895
|
() => runLogAppend({ vault: v.vault, content: opts.content })
|
|
5952
6896
|
);
|
|
5953
6897
|
});
|
|
6898
|
+
var logCmd = program.command("log").description("immutable log events and projections");
|
|
6899
|
+
logCmd.command("materialize [vault]").description("preview or write root log.md from immutable events").option("--write", "write projected log.md", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6900
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6901
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6902
|
+
else if (opts.write) {
|
|
6903
|
+
return emitManagedVaultWrite(
|
|
6904
|
+
v.vault,
|
|
6905
|
+
"log materialize",
|
|
6906
|
+
() => runLogMaterialize({ vault: v.vault, write: true })
|
|
6907
|
+
);
|
|
6908
|
+
} else emit(await runLogMaterialize({ vault: v.vault, write: false }), v.vault);
|
|
6909
|
+
});
|
|
6910
|
+
logCmd.command("migrate-legacy [vault]").description("backfill immutable events from historical root log.md blocks").option("--write", "write events", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6911
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6912
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6913
|
+
else if (opts.write) {
|
|
6914
|
+
return emitManagedVaultWrite(
|
|
6915
|
+
v.vault,
|
|
6916
|
+
"log migrate-legacy",
|
|
6917
|
+
() => runLogMigrateLegacy({ vault: v.vault, write: true })
|
|
6918
|
+
);
|
|
6919
|
+
} else emit(await runLogMigrateLegacy({ vault: v.vault, write: false }), v.vault);
|
|
6920
|
+
});
|
|
6921
|
+
var projectionsCmd = program.command("projections").description("root index/log projection materialization");
|
|
6922
|
+
projectionsCmd.command("materialize [vault]").description("preview or write root index.md and log.md as a pair").option("--write", "write projections", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6923
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6924
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6925
|
+
else if (opts.write) {
|
|
6926
|
+
return emitManagedVaultWrite(
|
|
6927
|
+
v.vault,
|
|
6928
|
+
"projections materialize",
|
|
6929
|
+
() => runProjectionsMaterialize({ vault: v.vault, write: true })
|
|
6930
|
+
);
|
|
6931
|
+
} else emit(await runProjectionsMaterialize({ vault: v.vault, write: false }), v.vault);
|
|
6932
|
+
});
|
|
5954
6933
|
program.command("lint [vault]").description("run all vault health checks").option("--days <n>", "stale threshold", (s) => parseInt(s, 10), 90).option("--lines <n>", "pagesize threshold", (s) => parseInt(s, 10), 200).option("--log-threshold <n>", "log rotation threshold", (s) => parseInt(s, 10), 500).option("--fix", "auto-fix supported lint violations").option("--only <bucket>", "run only the specified lint bucket").option("--summary", "emit bounded bucket counts instead of full item arrays", false).option("--examples <n>", "example count per bucket in summary mode", (s) => parseInt(s, 10), 3).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5955
6934
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5956
6935
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
@@ -6045,7 +7024,7 @@ program.command("archive <page> [vault]").description("archive a typed-knowledge
|
|
|
6045
7024
|
remoteDelete: !!opts.remoteDelete,
|
|
6046
7025
|
maxRemoteDeletes: Number.parseInt(opts.maxRemoteDeletes, 10)
|
|
6047
7026
|
}), v.vault);
|
|
6048
|
-
else return
|
|
7027
|
+
else return emitManagedVaultWrite(
|
|
6049
7028
|
v.vault,
|
|
6050
7029
|
"archive",
|
|
6051
7030
|
() => runArchive({
|
|
@@ -6062,7 +7041,7 @@ program.command("archive <page> [vault]").description("archive a typed-knowledge
|
|
|
6062
7041
|
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
7042
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6064
7043
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6065
|
-
else return
|
|
7044
|
+
else return emitManagedVaultWrite(
|
|
6066
7045
|
v.vault,
|
|
6067
7046
|
"remove",
|
|
6068
7047
|
() => runRemove({
|
|
@@ -6235,6 +7214,17 @@ syncCmd.command("pull [vault]").description("pull remote vault changes and lint"
|
|
|
6235
7214
|
() => runSyncPull({ vault: v.vault })
|
|
6236
7215
|
);
|
|
6237
7216
|
});
|
|
7217
|
+
syncCmd.command("resolve-derived [vault]").description("resolve mixed derived conflicts for an owned vault-sync operation (internal)").requiredOption("--operation-id <id>", "vault-sync operation journal id").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
7218
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
7219
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
7220
|
+
else {
|
|
7221
|
+
return emitGuardedVaultWrite(
|
|
7222
|
+
v.vault,
|
|
7223
|
+
"sync resolve-derived",
|
|
7224
|
+
() => runDerivedConflictResolution({ vault: v.vault, operationId: opts.operationId })
|
|
7225
|
+
);
|
|
7226
|
+
}
|
|
7227
|
+
});
|
|
6238
7228
|
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) => {
|
|
6239
7229
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6240
7230
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
@@ -6334,7 +7324,7 @@ program.command("observe [vault]").description("create a raw transcript observat
|
|
|
6334
7324
|
program.command("session-brief [vault]").description("render or refresh the bounded startup session brief").option("--project <slug>", "project slug, or auto for deterministic detection", "auto").option("--write", "write meta/latest-session-brief.md and local cache files", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6335
7325
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6336
7326
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6337
|
-
else if (opts.write) return
|
|
7327
|
+
else if (opts.write) return emitManagedVaultWrite(
|
|
6338
7328
|
v.vault,
|
|
6339
7329
|
"session-brief --write",
|
|
6340
7330
|
() => runSessionBrief({
|