skillwiki 0.9.62 → 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-2PENIQ3A.js → chunk-DR7KFHNH.js} +793 -1983
- package/dist/chunk-IZABIE44.js +647 -0
- package/dist/chunk-S5ABQCXQ.js +580 -0
- package/dist/cli.js +1820 -665
- 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 +2 -2
- package/skills/.codex-plugin/plugin.json +3 -3
- 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 +29 -2
- package/skills/skills/wiki-archive/SKILL.md +18 -4
- package/skills/skills/wiki-crystallize/SKILL.md +3 -0
- package/skills/skills/wiki-remove/SKILL.md +96 -0
- package/skills/using-skillwiki/SKILL.md +29 -2
- package/skills/wiki-archive/SKILL.md +18 -4
- package/skills/wiki-crystallize/SKILL.md +3 -0
- package/skills/wiki-remove/SKILL.md +96 -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-2PENIQ3A.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
|
|
@@ -625,155 +644,1154 @@ async function runInit(input) {
|
|
|
625
644
|
result: err("ENV_WRITE_CONFLICT", { key: "WIKI_LANG", existing: existingEnv.WIKI_LANG, attempted: canonicalLang })
|
|
626
645
|
};
|
|
627
646
|
}
|
|
628
|
-
const created = [];
|
|
647
|
+
const created = [];
|
|
648
|
+
try {
|
|
649
|
+
await mkdir3(target, { recursive: true });
|
|
650
|
+
for (const d of VAULT_DIRS) {
|
|
651
|
+
await mkdir3(join5(target, d), { recursive: true });
|
|
652
|
+
created.push(d + "/");
|
|
653
|
+
}
|
|
654
|
+
} catch (e) {
|
|
655
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
|
|
656
|
+
}
|
|
657
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
658
|
+
let taxonomy = input.taxonomy && input.taxonomy.length > 0 ? input.taxonomy : DEFAULT_TAXONOMY;
|
|
659
|
+
let domain = input.domain;
|
|
660
|
+
let oldTaxonomy = [];
|
|
661
|
+
if (oldSchemaText) {
|
|
662
|
+
if (!domain) {
|
|
663
|
+
const oldDomain = extractDomainFromSchema(oldSchemaText);
|
|
664
|
+
if (oldDomain) domain = oldDomain;
|
|
665
|
+
}
|
|
666
|
+
const oldTax = extractTaxonomy(oldSchemaText);
|
|
667
|
+
if (oldTax.ok) oldTaxonomy = oldTax.data;
|
|
668
|
+
}
|
|
669
|
+
const taxonomySet = new Set(taxonomy);
|
|
670
|
+
for (const t of oldTaxonomy) {
|
|
671
|
+
if (!taxonomySet.has(t)) {
|
|
672
|
+
taxonomy.push(t);
|
|
673
|
+
taxonomySet.add(t);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
const discovered = await discoverTagsFromPages(target, taxonomy);
|
|
677
|
+
const discovered_tags = discovered.length;
|
|
678
|
+
const fullTaxonomyYaml = discovered.length > 0 ? taxonomy.map((t) => ` - ${t}`).join("\n") + "\n # --- Discovered from existing pages ---\n" + discovered.map((t) => ` - ${t}`).join("\n") : taxonomy.map((t) => ` - ${t}`).join("\n");
|
|
679
|
+
try {
|
|
680
|
+
const schemaTpl = await readFile3(join5(input.templates, "SCHEMA.md"), "utf8");
|
|
681
|
+
const schema = schemaTpl.replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang).replace("{{TAXONOMY_YAML}}", fullTaxonomyYaml);
|
|
682
|
+
await writeFile2(join5(target, "SCHEMA.md"), schema, "utf8");
|
|
683
|
+
created.push("SCHEMA.md");
|
|
684
|
+
} catch (e) {
|
|
685
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: "SCHEMA.md", message: String(e) }) };
|
|
686
|
+
}
|
|
687
|
+
const preserved = [];
|
|
688
|
+
async function writeOrPreserve(fileName, render) {
|
|
689
|
+
try {
|
|
690
|
+
const existing = await readFile3(join5(target, fileName), "utf8");
|
|
691
|
+
if (existing.split("\n").length > 10) {
|
|
692
|
+
preserved.push(fileName);
|
|
693
|
+
return void 0;
|
|
694
|
+
}
|
|
695
|
+
} catch {
|
|
696
|
+
}
|
|
697
|
+
try {
|
|
698
|
+
await writeFile2(join5(target, fileName), await render(), "utf8");
|
|
699
|
+
created.push(fileName);
|
|
700
|
+
return void 0;
|
|
701
|
+
} catch (e) {
|
|
702
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: fileName, message: String(e) }) };
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
const err1 = await writeOrPreserve("index.md", async () => {
|
|
706
|
+
const tpl = await readFile3(join5(input.templates, "index.md"), "utf8");
|
|
707
|
+
return tpl.replace("{{INIT_DATE}}", today);
|
|
708
|
+
});
|
|
709
|
+
if (err1) return err1;
|
|
710
|
+
const errObsidian = await writeOrPreserve(".obsidian/app.json", async () => {
|
|
711
|
+
return JSON.stringify({ attachmentFolderPath: ATTACHMENT_FOLDER, newFileLocation: "folder", newFileFolderPath: NEW_FILE_FOLDER }, null, 2) + "\n";
|
|
712
|
+
});
|
|
713
|
+
if (errObsidian) return errObsidian;
|
|
714
|
+
const errTemplatesJson = await writeOrPreserve(".obsidian/templates.json", async () => {
|
|
715
|
+
return JSON.stringify({ folder: TEMPLATE_FOLDER }, null, 2) + "\n";
|
|
716
|
+
});
|
|
717
|
+
if (errTemplatesJson) return errTemplatesJson;
|
|
718
|
+
const errTemplate = await writeOrPreserve(`${TEMPLATE_FOLDER}/tpl-ad-hoc-capture.md`, async () => {
|
|
719
|
+
return [
|
|
720
|
+
"---",
|
|
721
|
+
"source_url:",
|
|
722
|
+
"created: {{date:YYYY-MM-DD}}",
|
|
723
|
+
"ingested: # filled by ingest pipeline",
|
|
724
|
+
"kind: # idea | bug | task | note | other",
|
|
725
|
+
'project: # optional: "[[slug]]"',
|
|
726
|
+
"---",
|
|
727
|
+
"",
|
|
728
|
+
""
|
|
729
|
+
].join("\n");
|
|
730
|
+
});
|
|
731
|
+
if (errTemplate) return errTemplate;
|
|
732
|
+
const err22 = await writeOrPreserve("log.md", async () => {
|
|
733
|
+
const tpl = await readFile3(join5(input.templates, "log.md"), "utf8");
|
|
734
|
+
return tpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang);
|
|
735
|
+
});
|
|
736
|
+
if (err22) return err22;
|
|
737
|
+
const skipEnv = !!input.noEnv || explicitTarget && !input.profile && swDotenvHadPath && !input.force;
|
|
738
|
+
let envWritten = "";
|
|
739
|
+
if (!skipEnv) {
|
|
740
|
+
try {
|
|
741
|
+
const envEntries = {};
|
|
742
|
+
if (input.profile) {
|
|
743
|
+
envEntries[profileKey(input.profile, "PATH")] = target;
|
|
744
|
+
envEntries[profileKey(input.profile, "LANG")] = canonicalLang;
|
|
745
|
+
envEntries["WIKI_DEFAULT"] = input.profile;
|
|
746
|
+
} else {
|
|
747
|
+
envEntries["WIKI_PATH"] = target;
|
|
748
|
+
envEntries["WIKI_LANG"] = canonicalLang;
|
|
749
|
+
}
|
|
750
|
+
await writeDotenv(envPath, envEntries, existingEnvRaw);
|
|
751
|
+
envWritten = envPath;
|
|
752
|
+
} catch (e) {
|
|
753
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: envPath, message: String(e) }) };
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
const importedFromHermes = pathRes.source === "hermes-dotenv" && !swDotenvHadPath;
|
|
757
|
+
const humanHint = [
|
|
758
|
+
`vault: ${target}`,
|
|
759
|
+
`domain: ${domain}`,
|
|
760
|
+
`lang: ${canonicalLang}`,
|
|
761
|
+
`created: ${created.length}, preserved: ${preserved.length}`,
|
|
762
|
+
`discovered tags: ${discovered_tags}`,
|
|
763
|
+
skipEnv ? "env: skipped" : `env: ${envWritten}`
|
|
764
|
+
].join("\n");
|
|
765
|
+
if (created.length > 0) {
|
|
766
|
+
appendLastOp(target, {
|
|
767
|
+
operation: "init",
|
|
768
|
+
summary: `initialized vault: ${domain}`,
|
|
769
|
+
files: created,
|
|
770
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
return {
|
|
774
|
+
exitCode: ExitCode.OK,
|
|
775
|
+
result: ok({
|
|
776
|
+
vault: target,
|
|
777
|
+
domain,
|
|
778
|
+
taxonomy,
|
|
779
|
+
lang: canonicalLang,
|
|
780
|
+
created,
|
|
781
|
+
preserved,
|
|
782
|
+
env_written: envWritten,
|
|
783
|
+
env_skipped: skipEnv,
|
|
784
|
+
imported_from_hermes: importedFromHermes,
|
|
785
|
+
discovered_tags,
|
|
786
|
+
humanHint,
|
|
787
|
+
templates_created: created.includes(`${TEMPLATE_FOLDER}/tpl-ad-hoc-capture.md`)
|
|
788
|
+
})
|
|
789
|
+
};
|
|
790
|
+
}
|
|
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 = "";
|
|
629
1639
|
try {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
+
};
|
|
637
1652
|
}
|
|
638
|
-
const
|
|
639
|
-
let
|
|
640
|
-
let
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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 };
|
|
646
1679
|
}
|
|
647
|
-
|
|
648
|
-
|
|
1680
|
+
if (wrote.data.created) created += 1;
|
|
1681
|
+
else already += 1;
|
|
649
1682
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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 {
|
|
656
1715
|
}
|
|
657
|
-
const discovered = await discoverTagsFromPages(target, taxonomy);
|
|
658
|
-
const discovered_tags = discovered.length;
|
|
659
|
-
const fullTaxonomyYaml = discovered.length > 0 ? taxonomy.map((t) => ` - ${t}`).join("\n") + "\n # --- Discovered from existing pages ---\n" + discovered.map((t) => ` - ${t}`).join("\n") : taxonomy.map((t) => ` - ${t}`).join("\n");
|
|
660
1716
|
try {
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
await writeFile2(join5(target, "SCHEMA.md"), schema, "utf8");
|
|
664
|
-
created.push("SCHEMA.md");
|
|
665
|
-
} catch (e) {
|
|
666
|
-
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: "SCHEMA.md", message: String(e) }) };
|
|
1717
|
+
curLog = readFileSync8(join13(input.vault, "log.md"), "utf8");
|
|
1718
|
+
} catch {
|
|
667
1719
|
}
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
+
};
|
|
685
1738
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
});
|
|
698
|
-
if (errTemplatesJson) return errTemplatesJson;
|
|
699
|
-
const errTemplate = await writeOrPreserve(`${TEMPLATE_FOLDER}/tpl-ad-hoc-capture.md`, async () => {
|
|
700
|
-
return [
|
|
701
|
-
"---",
|
|
702
|
-
"source_url:",
|
|
703
|
-
"created: {{date:YYYY-MM-DD}}",
|
|
704
|
-
"ingested: # filled by ingest pipeline",
|
|
705
|
-
"kind: # idea | bug | task | note | other",
|
|
706
|
-
'project: # optional: "[[slug]]"',
|
|
707
|
-
"---",
|
|
708
|
-
"",
|
|
709
|
-
""
|
|
710
|
-
].join("\n");
|
|
711
|
-
});
|
|
712
|
-
if (errTemplate) return errTemplate;
|
|
713
|
-
const err2 = await writeOrPreserve("log.md", async () => {
|
|
714
|
-
const tpl = await readFile3(join5(input.templates, "log.md"), "utf8");
|
|
715
|
-
return tpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang);
|
|
716
|
-
});
|
|
717
|
-
if (err2) return err2;
|
|
718
|
-
const skipEnv = !!input.noEnv || explicitTarget && !input.profile && swDotenvHadPath && !input.force;
|
|
719
|
-
let envWritten = "";
|
|
720
|
-
if (!skipEnv) {
|
|
721
|
-
try {
|
|
722
|
-
const envEntries = {};
|
|
723
|
-
if (input.profile) {
|
|
724
|
-
envEntries[profileKey(input.profile, "PATH")] = target;
|
|
725
|
-
envEntries[profileKey(input.profile, "LANG")] = canonicalLang;
|
|
726
|
-
envEntries["WIKI_DEFAULT"] = input.profile;
|
|
727
|
-
} else {
|
|
728
|
-
envEntries["WIKI_PATH"] = target;
|
|
729
|
-
envEntries["WIKI_LANG"] = canonicalLang;
|
|
730
|
-
}
|
|
731
|
-
await writeDotenv(envPath, envEntries, existingEnvRaw);
|
|
732
|
-
envWritten = envPath;
|
|
733
|
-
} catch (e) {
|
|
734
|
-
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: envPath, message: String(e) }) };
|
|
735
|
-
}
|
|
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
|
+
};
|
|
736
1750
|
}
|
|
737
|
-
const
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
+
};
|
|
753
1773
|
}
|
|
754
1774
|
return {
|
|
755
1775
|
exitCode: ExitCode.OK,
|
|
756
1776
|
result: ok({
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
humanHint,
|
|
768
|
-
templates_created: created.includes(`${TEMPLATE_FOLDER}/tpl-ad-hoc-capture.md`)
|
|
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}`
|
|
769
1787
|
})
|
|
770
1788
|
};
|
|
771
1789
|
}
|
|
772
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,8 +2377,50 @@ 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";
|
|
2382
|
+
|
|
2383
|
+
// src/utils/delete-intent.ts
|
|
2384
|
+
import { mkdir as mkdir6, writeFile as writeFile4, readdir as readdir4, readFile as readFile6 } from "fs/promises";
|
|
2385
|
+
import { join as join16 } from "path";
|
|
2386
|
+
var DELETE_INTENT_SCHEMA = "vault-delete-intent/v1";
|
|
2387
|
+
var DELETE_INTENT_DIR = "meta/delete-intents";
|
|
2388
|
+
function normalizeVaultRelPath(path) {
|
|
2389
|
+
const p = path.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
2390
|
+
if (!p || p.includes("..") || p.startsWith(".git/")) {
|
|
2391
|
+
throw new Error(`invalid vault-relative path: ${path}`);
|
|
2392
|
+
}
|
|
2393
|
+
return p;
|
|
2394
|
+
}
|
|
2395
|
+
function pathToIntentFilename(path) {
|
|
2396
|
+
const p = normalizeVaultRelPath(path);
|
|
2397
|
+
return `${p.replace(/\//g, "__")}.json`;
|
|
2398
|
+
}
|
|
2399
|
+
function intentHostId() {
|
|
2400
|
+
return process.env.SKILLWIKI_HOST_ID ?? process.env.AGENT_HOST_ID ?? "unknown";
|
|
2401
|
+
}
|
|
2402
|
+
function buildDeleteIntent(input) {
|
|
2403
|
+
return {
|
|
2404
|
+
schema: DELETE_INTENT_SCHEMA,
|
|
2405
|
+
path: normalizeVaultRelPath(input.path),
|
|
2406
|
+
action: input.action,
|
|
2407
|
+
created: input.created ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2408
|
+
host: input.host ?? intentHostId(),
|
|
2409
|
+
actor: input.actor,
|
|
2410
|
+
reason: input.reason,
|
|
2411
|
+
source: input.source,
|
|
2412
|
+
expires: input.expires ?? null
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
async function writeDeleteIntent(vault, intent) {
|
|
2416
|
+
const dir = join16(vault, DELETE_INTENT_DIR);
|
|
2417
|
+
await mkdir6(dir, { recursive: true });
|
|
2418
|
+
const rel = `${DELETE_INTENT_DIR}/${pathToIntentFilename(intent.path)}`;
|
|
2419
|
+
await writeFile4(join16(vault, rel), JSON.stringify(intent, null, 2) + "\n", "utf8");
|
|
2420
|
+
return rel;
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
// src/commands/archive.ts
|
|
1364
2424
|
function countWikilinks(body, slug) {
|
|
1365
2425
|
const escaped = slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1366
2426
|
const re = new RegExp(`\\[\\[${escaped}(?:[|#][^\\]]*)?\\]\\]`, "g");
|
|
@@ -1394,7 +2454,7 @@ async function runArchive(input) {
|
|
|
1394
2454
|
if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
|
|
1395
2455
|
if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
|
|
1396
2456
|
const slug = relPath.replace(/\.md$/, "").split("/").pop();
|
|
1397
|
-
const archivePath =
|
|
2457
|
+
const archivePath = join17("_archive", relPath).replace(/\\/g, "/");
|
|
1398
2458
|
const remoteRoot = normalizeRemoteRoot(input.remote);
|
|
1399
2459
|
const remoteObjectPath = buildRemoteObjectPath(remoteRoot, relPath);
|
|
1400
2460
|
let cascade;
|
|
@@ -1420,7 +2480,7 @@ async function runArchive(input) {
|
|
|
1420
2480
|
const indexRefs = [];
|
|
1421
2481
|
if (!isRaw) {
|
|
1422
2482
|
try {
|
|
1423
|
-
const idx = await
|
|
2483
|
+
const idx = await readFile7(join17(input.vault, "index.md"), "utf8");
|
|
1424
2484
|
idx.split("\n").forEach((line, i) => {
|
|
1425
2485
|
if (line.includes(`[[${slug}]]`)) indexRefs.push({ line: i + 1, text: line });
|
|
1426
2486
|
});
|
|
@@ -1447,8 +2507,8 @@ async function runArchive(input) {
|
|
|
1447
2507
|
}
|
|
1448
2508
|
if (input.cascade && input.apply && cascade) {
|
|
1449
2509
|
for (const ref of cascade.source_array_refs) {
|
|
1450
|
-
const absPath =
|
|
1451
|
-
const text = await
|
|
2510
|
+
const absPath = join17(input.vault, ref.page);
|
|
2511
|
+
const text = await readFile7(absPath, "utf8");
|
|
1452
2512
|
const split = splitFrontmatter(text);
|
|
1453
2513
|
if (!split.ok) continue;
|
|
1454
2514
|
const before = split.data.rawFrontmatter;
|
|
@@ -1459,33 +2519,38 @@ async function runArchive(input) {
|
|
|
1459
2519
|
);
|
|
1460
2520
|
if (fmRewritten === before) continue;
|
|
1461
2521
|
if (!arraysEqual(ref.sources_after, ref.sources_before)) {
|
|
1462
|
-
await
|
|
2522
|
+
await writeFile5(absPath, `---
|
|
1463
2523
|
${fmRewritten}
|
|
1464
2524
|
---${split.data.body}`, "utf8");
|
|
1465
2525
|
}
|
|
1466
2526
|
}
|
|
1467
2527
|
}
|
|
1468
|
-
await
|
|
2528
|
+
await mkdir7(dirname5(join17(input.vault, archivePath)), { recursive: true });
|
|
2529
|
+
await rename2(join17(input.vault, relPath), join17(input.vault, archivePath));
|
|
1469
2530
|
let indexUpdated = false;
|
|
1470
2531
|
if (!isRaw) {
|
|
1471
|
-
const
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
} catch (e) {
|
|
1481
|
-
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;
|
|
1482
2541
|
}
|
|
1483
2542
|
}
|
|
1484
|
-
|
|
2543
|
+
const archiveIntent = buildDeleteIntent({
|
|
2544
|
+
path: relPath,
|
|
2545
|
+
action: "archive",
|
|
2546
|
+
actor: "skillwiki-cli",
|
|
2547
|
+
source: "cli"
|
|
2548
|
+
});
|
|
2549
|
+
const tombstonePath = await writeDeleteIntent(input.vault, archiveIntent);
|
|
1485
2550
|
appendLastOp(input.vault, {
|
|
1486
2551
|
operation: input.cascade ? "archive-cascade" : "archive",
|
|
1487
|
-
summary: `moved ${relPath} to ${archivePath}${input.cascade ? ` (cascade: ${cascade?.source_array_refs.length ?? 0} source arrays updated)` : ""}`,
|
|
1488
|
-
files: [relPath],
|
|
2552
|
+
summary: `moved ${relPath} to ${archivePath}${input.cascade ? ` (cascade: ${cascade?.source_array_refs.length ?? 0} source arrays updated)` : ""}; tombstone ${tombstonePath}`,
|
|
2553
|
+
files: [relPath, archivePath, tombstonePath],
|
|
1489
2554
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1490
2555
|
});
|
|
1491
2556
|
let remote;
|
|
@@ -1514,8 +2579,107 @@ ${fmRewritten}
|
|
|
1514
2579
|
};
|
|
1515
2580
|
}
|
|
1516
2581
|
|
|
2582
|
+
// src/commands/remove.ts
|
|
2583
|
+
import { unlink as unlink2, access } from "fs/promises";
|
|
2584
|
+
import { join as join18 } from "path";
|
|
2585
|
+
async function pathExists(abs) {
|
|
2586
|
+
try {
|
|
2587
|
+
await access(abs);
|
|
2588
|
+
return true;
|
|
2589
|
+
} catch {
|
|
2590
|
+
return false;
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
async function runRemove(input) {
|
|
2594
|
+
if (input.remoteDelete && !input.remote) {
|
|
2595
|
+
return { exitCode: ExitCode.USAGE, result: err("USAGE", { message: "--remote-delete requires --remote" }) };
|
|
2596
|
+
}
|
|
2597
|
+
if (input.remoteDelete && !isValidRemoteDeleteCap(input.maxRemoteDeletes)) {
|
|
2598
|
+
return { exitCode: ExitCode.USAGE, result: err("USAGE", { message: "--max-remote-deletes must be a positive integer" }) };
|
|
2599
|
+
}
|
|
2600
|
+
const scan = await scanVault(input.vault);
|
|
2601
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
2602
|
+
const lookup = (pages) => {
|
|
2603
|
+
if (input.page.includes("/")) return pages.find((p) => p.relPath === input.page)?.relPath;
|
|
2604
|
+
return pages.find((p) => p.relPath.replace(/\.md$/, "").split("/").pop() === input.page)?.relPath;
|
|
2605
|
+
};
|
|
2606
|
+
let relPath = lookup(scan.data.typedKnowledge) ?? lookup(scan.data.raw) ?? null;
|
|
2607
|
+
if (!relPath) {
|
|
2608
|
+
try {
|
|
2609
|
+
const candidate = normalizeVaultRelPath(input.page);
|
|
2610
|
+
if (await pathExists(join18(input.vault, candidate))) {
|
|
2611
|
+
relPath = candidate;
|
|
2612
|
+
}
|
|
2613
|
+
} catch {
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
if (!relPath) {
|
|
2617
|
+
return {
|
|
2618
|
+
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
2619
|
+
result: err("FILE_NOT_FOUND", { page: input.page })
|
|
2620
|
+
};
|
|
2621
|
+
}
|
|
2622
|
+
if (relPath.startsWith("_archive/")) {
|
|
2623
|
+
return {
|
|
2624
|
+
exitCode: ExitCode.USAGE,
|
|
2625
|
+
result: err("USAGE", { message: "refusing to remove path already under _archive/; use restore or leave archived" })
|
|
2626
|
+
};
|
|
2627
|
+
}
|
|
2628
|
+
const remoteRoot = normalizeRemoteRoot(input.remote);
|
|
2629
|
+
const remoteObjectPath = buildRemoteObjectPath(remoteRoot, relPath);
|
|
2630
|
+
let indexUpdated = false;
|
|
2631
|
+
const intent = buildDeleteIntent({
|
|
2632
|
+
path: relPath,
|
|
2633
|
+
action: "remove",
|
|
2634
|
+
actor: "skillwiki-cli",
|
|
2635
|
+
source: "cli",
|
|
2636
|
+
reason: input.reason
|
|
2637
|
+
});
|
|
2638
|
+
const tombstonePath = await writeDeleteIntent(input.vault, intent);
|
|
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
|
+
}
|
|
2654
|
+
appendLastOp(input.vault, {
|
|
2655
|
+
operation: "remove",
|
|
2656
|
+
summary: `removed ${relPath} (tombstone ${tombstonePath})`,
|
|
2657
|
+
files: [relPath, tombstonePath],
|
|
2658
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2659
|
+
});
|
|
2660
|
+
let remote;
|
|
2661
|
+
if (remoteObjectPath) {
|
|
2662
|
+
const pruned = await planAndMaybePruneRemoteObjects([remoteObjectPath], input);
|
|
2663
|
+
if (!pruned.ok) {
|
|
2664
|
+
return { exitCode: ExitCode.SYNC_PUSH_FAILED, result: pruned };
|
|
2665
|
+
}
|
|
2666
|
+
remote = pruned.data;
|
|
2667
|
+
}
|
|
2668
|
+
const remoteNote = remote ? ` (remote ${input.remoteDelete ? `deleted ${remote.deleted.length}` : `planned ${remote.plannedDeletes.length}`})` : "";
|
|
2669
|
+
return {
|
|
2670
|
+
exitCode: ExitCode.OK,
|
|
2671
|
+
result: ok({
|
|
2672
|
+
removed: relPath,
|
|
2673
|
+
tombstone_path: tombstonePath,
|
|
2674
|
+
index_updated: indexUpdated,
|
|
2675
|
+
...remote ? { remote } : {},
|
|
2676
|
+
humanHint: `removed ${relPath}; tombstone ${tombstonePath}${indexUpdated ? " (index updated)" : ""}${remoteNote}`
|
|
2677
|
+
})
|
|
2678
|
+
};
|
|
2679
|
+
}
|
|
2680
|
+
|
|
1517
2681
|
// src/commands/drift.ts
|
|
1518
|
-
import { createHash as
|
|
2682
|
+
import { createHash as createHash3 } from "crypto";
|
|
1519
2683
|
|
|
1520
2684
|
// src/utils/fetch.ts
|
|
1521
2685
|
async function controlledFetch(url, opts) {
|
|
@@ -1594,7 +2758,7 @@ async function runDrift(input) {
|
|
|
1594
2758
|
});
|
|
1595
2759
|
continue;
|
|
1596
2760
|
}
|
|
1597
|
-
const currentHash =
|
|
2761
|
+
const currentHash = createHash3("sha256").update(Buffer.from(resp.data.body, "utf8")).digest("hex");
|
|
1598
2762
|
const identity = assessSourceIdentity({
|
|
1599
2763
|
rawPath: raw.relPath,
|
|
1600
2764
|
sourceUrl,
|
|
@@ -1825,14 +2989,14 @@ ${migratedBody}${newFooter}`;
|
|
|
1825
2989
|
|
|
1826
2990
|
// src/commands/update.ts
|
|
1827
2991
|
import { execSync } from "child_process";
|
|
1828
|
-
import { join as
|
|
2992
|
+
import { join as join19 } from "path";
|
|
1829
2993
|
function resolveGlobalSkillsRoot() {
|
|
1830
2994
|
try {
|
|
1831
2995
|
const globalRoot = execSync("npm root -g", {
|
|
1832
2996
|
encoding: "utf8",
|
|
1833
2997
|
timeout: 5e3
|
|
1834
2998
|
}).trim();
|
|
1835
|
-
return
|
|
2999
|
+
return join19(globalRoot, "skillwiki", "skills");
|
|
1836
3000
|
} catch {
|
|
1837
3001
|
return null;
|
|
1838
3002
|
}
|
|
@@ -1860,7 +3024,7 @@ async function runUpdate(input) {
|
|
|
1860
3024
|
const pkg2 = readCliPackageJson();
|
|
1861
3025
|
const currentVersion = pkg2.version;
|
|
1862
3026
|
const tag = normalizeDistTag(input.distTag);
|
|
1863
|
-
const target =
|
|
3027
|
+
const target = join19(input.home, ".claude", "skills");
|
|
1864
3028
|
let latest;
|
|
1865
3029
|
try {
|
|
1866
3030
|
latest = execSync(`npm view skillwiki@${tag} version`, {
|
|
@@ -1937,22 +3101,22 @@ async function runUpdate(input) {
|
|
|
1937
3101
|
|
|
1938
3102
|
// src/commands/self-update.ts
|
|
1939
3103
|
import { execSync as execSync2 } from "child_process";
|
|
1940
|
-
import { existsSync as
|
|
1941
|
-
import { join as
|
|
3104
|
+
import { existsSync as existsSync4, readFileSync as readFileSync10 } from "fs";
|
|
3105
|
+
import { join as join20 } from "path";
|
|
1942
3106
|
var DEFAULT_SOURCE_ROOT_SUFFIX = "/Desktop/code/llm-wiki";
|
|
1943
3107
|
async function runSelfUpdate(input) {
|
|
1944
3108
|
const currentVersion = readCliPackageJson().version;
|
|
1945
3109
|
const sourceRoot = input.sourceRoot ?? `${input.home}${DEFAULT_SOURCE_ROOT_SUFFIX}`;
|
|
1946
3110
|
const distTag = normalizeDistTag(input.distTag);
|
|
1947
|
-
const localPkgPath =
|
|
1948
|
-
const hasLocalSource =
|
|
3111
|
+
const localPkgPath = join20(sourceRoot, "packages", "cli", "package.json");
|
|
3112
|
+
const hasLocalSource = existsSync4(localPkgPath);
|
|
1949
3113
|
if (input.check) {
|
|
1950
3114
|
let availableVersion = null;
|
|
1951
3115
|
let source;
|
|
1952
3116
|
if (hasLocalSource) {
|
|
1953
3117
|
source = "local";
|
|
1954
3118
|
try {
|
|
1955
|
-
availableVersion = JSON.parse(
|
|
3119
|
+
availableVersion = JSON.parse(readFileSync10(localPkgPath, "utf8")).version ?? null;
|
|
1956
3120
|
} catch {
|
|
1957
3121
|
availableVersion = null;
|
|
1958
3122
|
}
|
|
@@ -2010,7 +3174,7 @@ async function runSelfUpdate(input) {
|
|
|
2010
3174
|
}
|
|
2011
3175
|
const newVersion = (() => {
|
|
2012
3176
|
try {
|
|
2013
|
-
return JSON.parse(
|
|
3177
|
+
return JSON.parse(readFileSync10(localPkgPath, "utf8")).version ?? "unknown";
|
|
2014
3178
|
} catch {
|
|
2015
3179
|
return "unknown";
|
|
2016
3180
|
}
|
|
@@ -2076,21 +3240,21 @@ async function runSelfUpdate(input) {
|
|
|
2076
3240
|
}
|
|
2077
3241
|
|
|
2078
3242
|
// src/commands/transcripts.ts
|
|
2079
|
-
import { readdir as
|
|
2080
|
-
import { join as
|
|
3243
|
+
import { readdir as readdir5, stat as stat3, readFile as readFile9 } from "fs/promises";
|
|
3244
|
+
import { join as join21 } from "path";
|
|
2081
3245
|
async function runTranscripts(input) {
|
|
2082
|
-
const dir =
|
|
3246
|
+
const dir = join21(input.vault, "raw", "transcripts");
|
|
2083
3247
|
let entries;
|
|
2084
3248
|
try {
|
|
2085
|
-
entries = await
|
|
3249
|
+
entries = await readdir5(dir, { withFileTypes: true });
|
|
2086
3250
|
} catch {
|
|
2087
3251
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: { ok: false, error: "VAULT_PATH_INVALID", detail: `raw/transcripts/ not found: ${dir}` } };
|
|
2088
3252
|
}
|
|
2089
3253
|
const transcripts = [];
|
|
2090
3254
|
for (const entry of entries) {
|
|
2091
3255
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
2092
|
-
const filePath =
|
|
2093
|
-
const content = await
|
|
3256
|
+
const filePath = join21(dir, entry.name);
|
|
3257
|
+
const content = await readFile9(filePath, "utf8");
|
|
2094
3258
|
const fm = extractFrontmatter(content);
|
|
2095
3259
|
if (!fm.ok) continue;
|
|
2096
3260
|
const ingested = typeof fm.data.ingested === "string" ? fm.data.ingested : "";
|
|
@@ -2107,10 +3271,10 @@ async function runTranscripts(input) {
|
|
|
2107
3271
|
}
|
|
2108
3272
|
|
|
2109
3273
|
// src/commands/compound.ts
|
|
2110
|
-
import { writeFile as
|
|
2111
|
-
import { join as
|
|
2112
|
-
import { existsSync as
|
|
2113
|
-
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";
|
|
2114
3278
|
var RETRO_HEADING_RE = /^## \[(\d{4}-\d{2}-\d{2})(?:\s+[^\]]+)?\] retro \| loop cycle(?: (\d+))?: (.+)$/;
|
|
2115
3279
|
var FIELD_RE = {
|
|
2116
3280
|
improve: /^-\s+\*?\*?Improve:?\*?\*?\s*(.+)$/m,
|
|
@@ -2208,17 +3372,17 @@ function extractRetroFields(date, cycleName, block) {
|
|
|
2208
3372
|
};
|
|
2209
3373
|
}
|
|
2210
3374
|
async function runCompound(input) {
|
|
2211
|
-
const logPath =
|
|
3375
|
+
const logPath = join22(input.vault, "log.md");
|
|
2212
3376
|
let logText;
|
|
2213
3377
|
try {
|
|
2214
|
-
logText = await
|
|
3378
|
+
logText = await readFile10(logPath, "utf8");
|
|
2215
3379
|
} catch {
|
|
2216
3380
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
|
|
2217
3381
|
}
|
|
2218
3382
|
const entries = parseRetroEntries(logText);
|
|
2219
3383
|
const promoted = [];
|
|
2220
3384
|
const skipped = [];
|
|
2221
|
-
const compoundDir =
|
|
3385
|
+
const compoundDir = join22(input.vault, "projects", input.project, "compound");
|
|
2222
3386
|
for (const entry of entries) {
|
|
2223
3387
|
const generalizeValue = entry.generalize.trim();
|
|
2224
3388
|
if (!/^yes/i.test(generalizeValue)) {
|
|
@@ -2226,8 +3390,8 @@ async function runCompound(input) {
|
|
|
2226
3390
|
continue;
|
|
2227
3391
|
}
|
|
2228
3392
|
const slug = slugify(entry.cycleName);
|
|
2229
|
-
const compoundPath =
|
|
2230
|
-
if (
|
|
3393
|
+
const compoundPath = join22(compoundDir, `${slug}.md`);
|
|
3394
|
+
if (existsSync5(compoundPath)) {
|
|
2231
3395
|
skipped.push(entry.date);
|
|
2232
3396
|
continue;
|
|
2233
3397
|
}
|
|
@@ -2265,10 +3429,10 @@ async function runCompound(input) {
|
|
|
2265
3429
|
].join("\n");
|
|
2266
3430
|
const content = frontmatter + "\n" + body;
|
|
2267
3431
|
if (!input.dryRun) {
|
|
2268
|
-
if (!
|
|
2269
|
-
await
|
|
3432
|
+
if (!existsSync5(compoundDir)) {
|
|
3433
|
+
await mkdir8(compoundDir, { recursive: true });
|
|
2270
3434
|
}
|
|
2271
|
-
await
|
|
3435
|
+
await writeFile7(compoundPath, content, "utf8");
|
|
2272
3436
|
}
|
|
2273
3437
|
promoted.push(`${slug}.md`);
|
|
2274
3438
|
}
|
|
@@ -2287,23 +3451,23 @@ async function runCompound(input) {
|
|
|
2287
3451
|
};
|
|
2288
3452
|
}
|
|
2289
3453
|
async function runCompoundDelete(input) {
|
|
2290
|
-
const projectDir =
|
|
2291
|
-
if (!
|
|
3454
|
+
const projectDir = join22(input.vault, "projects", input.project);
|
|
3455
|
+
if (!existsSync5(projectDir)) {
|
|
2292
3456
|
return {
|
|
2293
3457
|
exitCode: ExitCode.PROJECT_NOT_FOUND,
|
|
2294
3458
|
result: err("PROJECT_NOT_FOUND", { slug: input.project, path: projectDir })
|
|
2295
3459
|
};
|
|
2296
3460
|
}
|
|
2297
3461
|
const entryName = input.entry.replace(/\.md$/, "");
|
|
2298
|
-
const compoundPath =
|
|
2299
|
-
if (!
|
|
3462
|
+
const compoundPath = join22(projectDir, "compound", `${entryName}.md`);
|
|
3463
|
+
if (!existsSync5(compoundPath)) {
|
|
2300
3464
|
return {
|
|
2301
3465
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
2302
3466
|
result: err("FILE_NOT_FOUND", { path: compoundPath })
|
|
2303
3467
|
};
|
|
2304
3468
|
}
|
|
2305
3469
|
try {
|
|
2306
|
-
await
|
|
3470
|
+
await unlink3(compoundPath);
|
|
2307
3471
|
} catch (e) {
|
|
2308
3472
|
return {
|
|
2309
3473
|
exitCode: ExitCode.WRITE_FAILED,
|
|
@@ -2329,8 +3493,8 @@ knowledge.md regenerated`
|
|
|
2329
3493
|
};
|
|
2330
3494
|
}
|
|
2331
3495
|
async function runCompoundList(input) {
|
|
2332
|
-
const compoundDir =
|
|
2333
|
-
if (!
|
|
3496
|
+
const compoundDir = join22(input.vault, "projects", input.project, "compound");
|
|
3497
|
+
if (!existsSync5(compoundDir)) {
|
|
2334
3498
|
return {
|
|
2335
3499
|
exitCode: ExitCode.OK,
|
|
2336
3500
|
result: ok({
|
|
@@ -2344,7 +3508,7 @@ no compound directory found`
|
|
|
2344
3508
|
}
|
|
2345
3509
|
let dirents;
|
|
2346
3510
|
try {
|
|
2347
|
-
dirents = await
|
|
3511
|
+
dirents = await readdir6(compoundDir, { withFileTypes: true });
|
|
2348
3512
|
} catch {
|
|
2349
3513
|
return {
|
|
2350
3514
|
exitCode: ExitCode.OK,
|
|
@@ -2360,10 +3524,10 @@ could not read compound directory`
|
|
|
2360
3524
|
const entries = [];
|
|
2361
3525
|
for (const dirent of dirents) {
|
|
2362
3526
|
if (!dirent.isFile() || !dirent.name.endsWith(".md")) continue;
|
|
2363
|
-
const filePath =
|
|
3527
|
+
const filePath = join22(compoundDir, dirent.name);
|
|
2364
3528
|
let text;
|
|
2365
3529
|
try {
|
|
2366
|
-
text = await
|
|
3530
|
+
text = await readFile10(filePath, "utf8");
|
|
2367
3531
|
} catch {
|
|
2368
3532
|
continue;
|
|
2369
3533
|
}
|
|
@@ -2392,8 +3556,8 @@ no compound entries found`;
|
|
|
2392
3556
|
}
|
|
2393
3557
|
|
|
2394
3558
|
// src/commands/session-brief.ts
|
|
2395
|
-
import { mkdir as
|
|
2396
|
-
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";
|
|
2397
3561
|
var MAX_WORDS = 900;
|
|
2398
3562
|
async function runSessionBrief(input) {
|
|
2399
3563
|
const scan = await scanVault(input.vault);
|
|
@@ -2493,7 +3657,7 @@ async function resolveProject(input) {
|
|
|
2493
3657
|
const envProject = input.env?.SKILLWIKI_PROJECT;
|
|
2494
3658
|
if (envProject) return envProject;
|
|
2495
3659
|
const cwd = input.cwd ?? process.cwd();
|
|
2496
|
-
const projectDotenv = await readProjectSlug(
|
|
3660
|
+
const projectDotenv = await readProjectSlug(join23(cwd, ".skillwiki", ".env"));
|
|
2497
3661
|
if (projectDotenv) return projectDotenv;
|
|
2498
3662
|
const inferred = inferProjectFromPath(input.vault, cwd);
|
|
2499
3663
|
if (inferred) return inferred;
|
|
@@ -2502,7 +3666,7 @@ async function resolveProject(input) {
|
|
|
2502
3666
|
async function readProjectSlug(file) {
|
|
2503
3667
|
let text;
|
|
2504
3668
|
try {
|
|
2505
|
-
text = await
|
|
3669
|
+
text = await readFile11(file, "utf8");
|
|
2506
3670
|
} catch {
|
|
2507
3671
|
return void 0;
|
|
2508
3672
|
}
|
|
@@ -2579,7 +3743,7 @@ async function loadTrendDigests(typedPages) {
|
|
|
2579
3743
|
return out;
|
|
2580
3744
|
}
|
|
2581
3745
|
async function loadSessionPins(vault, project) {
|
|
2582
|
-
const text = await readIfExists(
|
|
3746
|
+
const text = await readIfExists(join23(vault, "meta", "session-pins.md"));
|
|
2583
3747
|
if (!text) return [];
|
|
2584
3748
|
const fm = extractFrontmatter(text);
|
|
2585
3749
|
if (!fm.ok) return [];
|
|
@@ -2696,7 +3860,7 @@ function satelliteHealthWarnings(warning) {
|
|
|
2696
3860
|
return warning ? [warning] : [];
|
|
2697
3861
|
}
|
|
2698
3862
|
async function loadHealthWarnings(vault) {
|
|
2699
|
-
const text = await readIfExists(
|
|
3863
|
+
const text = await readIfExists(join23(vault, ".skillwiki", "health.json"));
|
|
2700
3864
|
if (!text) return [];
|
|
2701
3865
|
try {
|
|
2702
3866
|
const parsed = JSON.parse(text);
|
|
@@ -2709,7 +3873,7 @@ async function loadHealthWarnings(vault) {
|
|
|
2709
3873
|
}
|
|
2710
3874
|
}
|
|
2711
3875
|
async function loadMemoryTopics(vault, project) {
|
|
2712
|
-
const text = await readIfExists(
|
|
3876
|
+
const text = await readIfExists(join23(vault, ".skillwiki", "memory", project, "topics.json"));
|
|
2713
3877
|
if (!text) return [];
|
|
2714
3878
|
try {
|
|
2715
3879
|
const parsed = JSON.parse(text);
|
|
@@ -2726,18 +3890,18 @@ async function loadMemoryTopics(vault, project) {
|
|
|
2726
3890
|
}
|
|
2727
3891
|
}
|
|
2728
3892
|
async function writeBriefArtifacts(vault, input) {
|
|
2729
|
-
const metaPath =
|
|
2730
|
-
const cacheMdPath =
|
|
2731
|
-
const cacheJsonPath =
|
|
2732
|
-
await
|
|
2733
|
-
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 });
|
|
2734
3898
|
const committed = renderCommittedBrief(input);
|
|
2735
3899
|
const previousComparable = comparableBrief(await readIfExists(metaPath));
|
|
2736
3900
|
const nextComparable = comparableBrief(committed);
|
|
2737
3901
|
const materialChange = previousComparable !== nextComparable;
|
|
2738
|
-
await
|
|
2739
|
-
await
|
|
2740
|
-
await
|
|
3902
|
+
await writeFile8(metaPath, committed, "utf8");
|
|
3903
|
+
await writeFile8(cacheMdPath, input.brief, "utf8");
|
|
3904
|
+
await writeFile8(cacheJsonPath, `${JSON.stringify({
|
|
2741
3905
|
project: input.project,
|
|
2742
3906
|
brief: input.brief,
|
|
2743
3907
|
word_count: input.wordCount,
|
|
@@ -2745,8 +3909,17 @@ async function writeBriefArtifacts(vault, input) {
|
|
|
2745
3909
|
memory_topics: input.memoryTopics
|
|
2746
3910
|
}, null, 2)}
|
|
2747
3911
|
`, "utf8");
|
|
2748
|
-
const indexUpdated = await
|
|
2749
|
-
|
|
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
|
+
}
|
|
2750
3923
|
if (materialChange) {
|
|
2751
3924
|
appendLastOp(vault, {
|
|
2752
3925
|
operation: "session-brief",
|
|
@@ -2787,40 +3960,21 @@ function renderCommittedBrief(input) {
|
|
|
2787
3960
|
"---",
|
|
2788
3961
|
"",
|
|
2789
3962
|
input.brief.trimEnd(),
|
|
2790
|
-
""
|
|
2791
|
-
].filter((line) => line !== "").join("\n");
|
|
2792
|
-
}
|
|
2793
|
-
async function ensureIndexEntry(vault) {
|
|
2794
|
-
const indexPath = join13(vault, "index.md");
|
|
2795
|
-
let text = await readIfExists(indexPath);
|
|
2796
|
-
if (!text) return false;
|
|
2797
|
-
if (text.includes("[[meta/latest-session-brief]]")) return false;
|
|
2798
|
-
const entry = "- [[meta/latest-session-brief]] \u2014 Latest Session Brief";
|
|
2799
|
-
const lines = text.split(/\r?\n/);
|
|
2800
|
-
const sectionIdx = lines.findIndex((line) => line.trim() === "## Meta");
|
|
2801
|
-
if (sectionIdx === -1) {
|
|
2802
|
-
while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
|
|
2803
|
-
lines.push("", "## Meta", entry);
|
|
2804
|
-
} else {
|
|
2805
|
-
let insertAt = sectionIdx + 1;
|
|
2806
|
-
while (insertAt < lines.length && !lines[insertAt].startsWith("## ")) insertAt++;
|
|
2807
|
-
lines.splice(insertAt, 0, entry);
|
|
2808
|
-
}
|
|
2809
|
-
await writeFile6(indexPath, lines.join("\n"), "utf8");
|
|
2810
|
-
return true;
|
|
3963
|
+
""
|
|
3964
|
+
].filter((line) => line !== "").join("\n");
|
|
2811
3965
|
}
|
|
2812
|
-
async function
|
|
2813
|
-
const
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
await
|
|
2819
|
-
return
|
|
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;
|
|
2820
3974
|
}
|
|
2821
3975
|
async function readIfExists(path) {
|
|
2822
3976
|
try {
|
|
2823
|
-
return await
|
|
3977
|
+
return await readFile11(path, "utf8");
|
|
2824
3978
|
} catch {
|
|
2825
3979
|
return "";
|
|
2826
3980
|
}
|
|
@@ -2862,16 +4016,18 @@ function dateFromPath(path) {
|
|
|
2862
4016
|
}
|
|
2863
4017
|
|
|
2864
4018
|
// src/commands/ingest.ts
|
|
2865
|
-
import { readFile as
|
|
2866
|
-
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";
|
|
2867
4021
|
import { createHash as createHash4 } from "crypto";
|
|
2868
4022
|
|
|
2869
4023
|
// src/commands/page-publish.ts
|
|
2870
|
-
import { createHash as createHash3 } from "crypto";
|
|
2871
4024
|
import { realpathSync } from "fs";
|
|
2872
|
-
import { readFile as
|
|
2873
|
-
import { join as
|
|
2874
|
-
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
|
+
};
|
|
2875
4031
|
function errorExitCode(error) {
|
|
2876
4032
|
switch (error) {
|
|
2877
4033
|
case "FILE_NOT_FOUND":
|
|
@@ -2887,6 +4043,9 @@ function errorExitCode(error) {
|
|
|
2887
4043
|
return ExitCode.SENSITIVE_CONTENT_DETECTED;
|
|
2888
4044
|
case "SYNC_LOCK_HELD":
|
|
2889
4045
|
return ExitCode.SYNC_LOCK_HELD;
|
|
4046
|
+
case "PREFLIGHT_FAILED":
|
|
4047
|
+
return ExitCode.PREFLIGHT_FAILED;
|
|
4048
|
+
case "EVENT_IDENTITY_COLLISION":
|
|
2890
4049
|
case "WRITE_FAILED":
|
|
2891
4050
|
return ExitCode.WRITE_FAILED;
|
|
2892
4051
|
default:
|
|
@@ -2894,7 +4053,11 @@ function errorExitCode(error) {
|
|
|
2894
4053
|
}
|
|
2895
4054
|
}
|
|
2896
4055
|
function publicationId(target, content, logNote = "") {
|
|
2897
|
-
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";
|
|
2898
4061
|
}
|
|
2899
4062
|
function prepareFrozenPublication(input, source) {
|
|
2900
4063
|
const target = assertTargetInsideVault(input.vault, input.target);
|
|
@@ -2930,7 +4093,7 @@ function preparePagePublicationFromContent(input) {
|
|
|
2930
4093
|
async function preparePagePublication(input) {
|
|
2931
4094
|
let content;
|
|
2932
4095
|
try {
|
|
2933
|
-
content = await
|
|
4096
|
+
content = await readFile12(input.draftPath, "utf8");
|
|
2934
4097
|
} catch (error) {
|
|
2935
4098
|
return err("FILE_NOT_FOUND", { path: input.draftPath, message: String(error) });
|
|
2936
4099
|
}
|
|
@@ -3001,10 +4164,10 @@ async function runLockedPrimaryStages(input, vault, deps) {
|
|
|
3001
4164
|
ExitCode.VAULT_PATH_INVALID
|
|
3002
4165
|
);
|
|
3003
4166
|
}
|
|
3004
|
-
const schemaPath =
|
|
4167
|
+
const schemaPath = join24(vault, "SCHEMA.md");
|
|
3005
4168
|
let schemaText;
|
|
3006
4169
|
try {
|
|
3007
|
-
schemaText = await
|
|
4170
|
+
schemaText = await readFile12(schemaPath, "utf8");
|
|
3008
4171
|
} catch (error) {
|
|
3009
4172
|
return lockedFailure("schema", state, err("WRITE_FAILED", { message: String(error) }));
|
|
3010
4173
|
}
|
|
@@ -3034,8 +4197,8 @@ async function runLockedPrimaryStages(input, vault, deps) {
|
|
|
3034
4197
|
let visibleSchema;
|
|
3035
4198
|
try {
|
|
3036
4199
|
[visible, visibleSchema] = await Promise.all([
|
|
3037
|
-
|
|
3038
|
-
|
|
4200
|
+
readFile12(input.targetPath, "utf8"),
|
|
4201
|
+
readFile12(schemaPath, "utf8")
|
|
3039
4202
|
]);
|
|
3040
4203
|
} catch (error) {
|
|
3041
4204
|
return lockedFailure("verify", state, err("WRITE_FAILED", { message: String(error) }));
|
|
@@ -3088,7 +4251,7 @@ function phaseFailure(stage, input, published, cause, context = {}, exitCode = E
|
|
|
3088
4251
|
})
|
|
3089
4252
|
};
|
|
3090
4253
|
}
|
|
3091
|
-
function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, logAppended, filesChanged, dryRun = false) {
|
|
4254
|
+
function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, logAppended, filesChanged, dryRun = false, receipt = null) {
|
|
3092
4255
|
return {
|
|
3093
4256
|
exitCode: ExitCode.OK,
|
|
3094
4257
|
result: ok({
|
|
@@ -3102,6 +4265,9 @@ function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, logAppe
|
|
|
3102
4265
|
operation_id: input.operationId,
|
|
3103
4266
|
dry_run: dryRun,
|
|
3104
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 } : {},
|
|
3105
4271
|
humanHint: dryRun ? `dry run: would publish ${input.page.target} (${input.operationId.slice(0, 12)})` : `published ${input.page.target} (${input.operationId.slice(0, 12)})`
|
|
3106
4272
|
})
|
|
3107
4273
|
};
|
|
@@ -3117,17 +4283,17 @@ function renderPublicationLog(input, added) {
|
|
|
3117
4283
|
}
|
|
3118
4284
|
async function readPageChanged(targetPath, content) {
|
|
3119
4285
|
try {
|
|
3120
|
-
return ok(await
|
|
4286
|
+
return ok(await readFile12(targetPath, "utf8") !== content);
|
|
3121
4287
|
} catch (error) {
|
|
3122
4288
|
if (error.code === "ENOENT") return ok(true);
|
|
3123
4289
|
return err("WRITE_FAILED", { path: targetPath, message: String(error) });
|
|
3124
4290
|
}
|
|
3125
4291
|
}
|
|
3126
4292
|
async function previewPreparedPagePublication(input, vault) {
|
|
3127
|
-
const schemaPath =
|
|
4293
|
+
const schemaPath = join24(vault, "SCHEMA.md");
|
|
3128
4294
|
let schemaText;
|
|
3129
4295
|
try {
|
|
3130
|
-
schemaText = await
|
|
4296
|
+
schemaText = await readFile12(schemaPath, "utf8");
|
|
3131
4297
|
} catch (error) {
|
|
3132
4298
|
const result = err("FILE_NOT_FOUND", { path: schemaPath, message: String(error) });
|
|
3133
4299
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result };
|
|
@@ -3139,10 +4305,10 @@ async function previewPreparedPagePublication(input, vault) {
|
|
|
3139
4305
|
if (!reconciled.ok) return { exitCode: errorExitCode(reconciled.error), result: reconciled };
|
|
3140
4306
|
const pageChanged = await readPageChanged(input.targetPath, input.page.content);
|
|
3141
4307
|
if (!pageChanged.ok) return { exitCode: errorExitCode(pageChanged.error), result: pageChanged };
|
|
3142
|
-
const indexPath =
|
|
4308
|
+
const indexPath = join24(vault, "index.md");
|
|
3143
4309
|
let indexText;
|
|
3144
4310
|
try {
|
|
3145
|
-
indexText = await
|
|
4311
|
+
indexText = await readFile12(indexPath, "utf8");
|
|
3146
4312
|
} catch (error) {
|
|
3147
4313
|
const result = err("FILE_NOT_FOUND", { path: indexPath, message: String(error) });
|
|
3148
4314
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result };
|
|
@@ -3153,10 +4319,10 @@ async function previewPreparedPagePublication(input, vault) {
|
|
|
3153
4319
|
type: input.page.type
|
|
3154
4320
|
});
|
|
3155
4321
|
if (!index.ok) return { exitCode: errorExitCode(index.error), result: index };
|
|
3156
|
-
const logPath =
|
|
4322
|
+
const logPath = join24(vault, "log.md");
|
|
3157
4323
|
let logText;
|
|
3158
4324
|
try {
|
|
3159
|
-
logText = await
|
|
4325
|
+
logText = await readFile12(logPath, "utf8");
|
|
3160
4326
|
} catch (error) {
|
|
3161
4327
|
const result = err("FILE_NOT_FOUND", { path: logPath, message: String(error) });
|
|
3162
4328
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result };
|
|
@@ -3179,93 +4345,159 @@ async function previewPreparedPagePublication(input, vault) {
|
|
|
3179
4345
|
);
|
|
3180
4346
|
}
|
|
3181
4347
|
async function publishPreparedPage(input, vault, deps = DEFAULT_DEPS) {
|
|
3182
|
-
|
|
4348
|
+
const managed = acquireManagedWriteLock(vault, `page publish ${input.page.target}`);
|
|
4349
|
+
if (!managed.ok) return { exitCode: errorExitCode(managed.error), result: managed };
|
|
3183
4350
|
try {
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
4351
|
+
const preflight = await deps.preflight({
|
|
4352
|
+
vault,
|
|
4353
|
+
command: `page publish ${input.page.target}`,
|
|
4354
|
+
lockToken: managed.data.ownerToken
|
|
3187
4355
|
});
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
);
|
|
3205
|
-
} finally {
|
|
3206
|
-
released = releaseOwnedSyncLock(lock.data);
|
|
3207
|
-
}
|
|
3208
|
-
const primaryState = primary?.ok ? primary.data : primary?.state;
|
|
3209
|
-
if (released === void 0 || !released.ok || !released.data.released) {
|
|
3210
|
-
return phaseFailure(
|
|
3211
|
-
"unlock",
|
|
3212
|
-
input,
|
|
3213
|
-
primaryState?.published ?? false,
|
|
3214
|
-
released && !released.ok ? released : err("WRITE_FAILED", { message: "lock release did not run" }),
|
|
3215
|
-
{
|
|
3216
|
-
primary_stage: primary && !primary.ok ? primary.stage : "complete",
|
|
3217
|
-
primary_error: primary && !primary.ok ? primary.cause.error : void 0
|
|
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
|
+
};
|
|
3218
4372
|
}
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
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(
|
|
3226
4489
|
input,
|
|
4490
|
+
state.taxonomyAdded,
|
|
4491
|
+
state.pageChanged,
|
|
4492
|
+
state.indexUpdated,
|
|
4493
|
+
logAppended,
|
|
4494
|
+
[...state.changed],
|
|
3227
4495
|
false,
|
|
3228
|
-
|
|
3229
|
-
);
|
|
3230
|
-
}
|
|
3231
|
-
if (!primary.ok) {
|
|
3232
|
-
return phaseFailure(
|
|
3233
|
-
primary.stage,
|
|
3234
|
-
input,
|
|
3235
|
-
primary.state.published,
|
|
3236
|
-
primary.cause,
|
|
3237
|
-
void 0,
|
|
3238
|
-
primary.exitCode
|
|
3239
|
-
);
|
|
3240
|
-
}
|
|
3241
|
-
const state = primary.data;
|
|
3242
|
-
const log = await runLogAppend({
|
|
3243
|
-
vault,
|
|
3244
|
-
content: renderPublicationLog(input, state.taxonomyAdded),
|
|
3245
|
-
operationId: input.operationId,
|
|
3246
|
-
strictLock: true,
|
|
3247
|
-
recordLastOp: false
|
|
3248
|
-
});
|
|
3249
|
-
if (!log.result.ok) return phaseFailure("log", input, true, log.result);
|
|
3250
|
-
if (log.exitCode !== ExitCode.OK) {
|
|
3251
|
-
return phaseFailure(
|
|
3252
|
-
"log",
|
|
3253
|
-
input,
|
|
3254
|
-
true,
|
|
3255
|
-
err("WRITE_FAILED", { message: "log append returned inconsistent success state" })
|
|
4496
|
+
writeReceipt
|
|
3256
4497
|
);
|
|
4498
|
+
} finally {
|
|
4499
|
+
releaseManagedWriteLock(managed.data);
|
|
3257
4500
|
}
|
|
3258
|
-
if (log.result.data.appended) state.changed.add("log.md");
|
|
3259
|
-
const logHook = await observeStage(deps, "log");
|
|
3260
|
-
if (logHook) return phaseFailure("log", input, true, logHook);
|
|
3261
|
-
return successReceipt(
|
|
3262
|
-
input,
|
|
3263
|
-
state.taxonomyAdded,
|
|
3264
|
-
state.pageChanged,
|
|
3265
|
-
state.indexUpdated,
|
|
3266
|
-
log.result.data.appended,
|
|
3267
|
-
[...state.changed]
|
|
3268
|
-
);
|
|
3269
4501
|
}
|
|
3270
4502
|
async function runPagePublish(input, deps = DEFAULT_DEPS) {
|
|
3271
4503
|
const prepared = await preparePagePublication(input);
|
|
@@ -3352,7 +4584,7 @@ function buildTypedContent(title, ingested, type, tags, rawRelPath, provenance)
|
|
|
3352
4584
|
}
|
|
3353
4585
|
async function resolveRawCapture(input) {
|
|
3354
4586
|
try {
|
|
3355
|
-
const existing = await
|
|
4587
|
+
const existing = await readFile13(input.path, "utf8");
|
|
3356
4588
|
const frontmatter = extractFrontmatter(existing);
|
|
3357
4589
|
if (!frontmatter.ok) {
|
|
3358
4590
|
return err("INGEST_VALIDATION_FAILED", {
|
|
@@ -3401,7 +4633,7 @@ async function writeResolvedRaw(input) {
|
|
|
3401
4633
|
return written.ok ? ok({ changed: written.data.changed, capture: resolved.data }) : written;
|
|
3402
4634
|
} finally {
|
|
3403
4635
|
try {
|
|
3404
|
-
await
|
|
4636
|
+
await unlink4(lock.data);
|
|
3405
4637
|
} catch {
|
|
3406
4638
|
}
|
|
3407
4639
|
}
|
|
@@ -3410,7 +4642,7 @@ async function acquireRawCaptureLock(path) {
|
|
|
3410
4642
|
const lockPath = `${path}.ingest.lock`;
|
|
3411
4643
|
for (let attempt = 0; attempt < 200; attempt++) {
|
|
3412
4644
|
try {
|
|
3413
|
-
const handle = await
|
|
4645
|
+
const handle = await open2(lockPath, "wx");
|
|
3414
4646
|
await handle.close();
|
|
3415
4647
|
return ok(lockPath);
|
|
3416
4648
|
} catch (error) {
|
|
@@ -3491,7 +4723,7 @@ async function runIngest(input) {
|
|
|
3491
4723
|
sourceContent = fetchResult.data.body;
|
|
3492
4724
|
} else {
|
|
3493
4725
|
try {
|
|
3494
|
-
sourceContent = await
|
|
4726
|
+
sourceContent = await readFile13(input.source, "utf8");
|
|
3495
4727
|
} catch {
|
|
3496
4728
|
return {
|
|
3497
4729
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
@@ -3516,7 +4748,7 @@ async function runIngest(input) {
|
|
|
3516
4748
|
const rawRelPath = `raw/articles/${slug}.md`;
|
|
3517
4749
|
const typedDir = TYPE_DIR[input.type] ?? `${input.type}s`;
|
|
3518
4750
|
const typedRelPath = `${typedDir}/${slug}.md`;
|
|
3519
|
-
const rawAbsPath =
|
|
4751
|
+
const rawAbsPath = join25(input.vault, rawRelPath);
|
|
3520
4752
|
const identity = assessSourceIdentity({
|
|
3521
4753
|
rawPath: rawRelPath,
|
|
3522
4754
|
sourceUrl: sourceUrl ?? void 0,
|
|
@@ -3560,11 +4792,11 @@ async function runIngest(input) {
|
|
|
3560
4792
|
);
|
|
3561
4793
|
if (!input.dryRun) {
|
|
3562
4794
|
try {
|
|
3563
|
-
await
|
|
4795
|
+
await mkdir10(join25(input.vault, typedDir), { recursive: true });
|
|
3564
4796
|
} catch (error) {
|
|
3565
4797
|
return {
|
|
3566
4798
|
exitCode: ExitCode.WRITE_FAILED,
|
|
3567
|
-
result: err("WRITE_FAILED", { path:
|
|
4799
|
+
result: err("WRITE_FAILED", { path: join25(input.vault, typedDir), message: String(error) })
|
|
3568
4800
|
};
|
|
3569
4801
|
}
|
|
3570
4802
|
}
|
|
@@ -3611,11 +4843,11 @@ async function runIngest(input) {
|
|
|
3611
4843
|
};
|
|
3612
4844
|
}
|
|
3613
4845
|
try {
|
|
3614
|
-
await
|
|
4846
|
+
await mkdir10(join25(input.vault, "raw", "articles"), { recursive: true });
|
|
3615
4847
|
} catch (error) {
|
|
3616
4848
|
return {
|
|
3617
4849
|
exitCode: ExitCode.WRITE_FAILED,
|
|
3618
|
-
result: err("WRITE_FAILED", { path:
|
|
4850
|
+
result: err("WRITE_FAILED", { path: join25(input.vault, "raw", "articles"), message: String(error) })
|
|
3619
4851
|
};
|
|
3620
4852
|
}
|
|
3621
4853
|
const rawWrite = await writeResolvedRaw({
|
|
@@ -3843,8 +5075,8 @@ ${body}`;
|
|
|
3843
5075
|
}
|
|
3844
5076
|
|
|
3845
5077
|
// src/commands/tag-reconcile.ts
|
|
3846
|
-
import { readFile as
|
|
3847
|
-
import { join as
|
|
5078
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
5079
|
+
import { join as join26, posix } from "path";
|
|
3848
5080
|
var TYPED_TARGET_RE = /^(entities|concepts|comparisons|queries|meta)\/[a-z0-9][a-z0-9./_-]*\.md$/;
|
|
3849
5081
|
function errorExitCode2(error) {
|
|
3850
5082
|
switch (error) {
|
|
@@ -3887,7 +5119,7 @@ function asTagArray(frontmatter, path) {
|
|
|
3887
5119
|
async function readTagsFromFile(path) {
|
|
3888
5120
|
let text;
|
|
3889
5121
|
try {
|
|
3890
|
-
text = await
|
|
5122
|
+
text = await readFile14(path, "utf8");
|
|
3891
5123
|
} catch (error) {
|
|
3892
5124
|
if (error.code === "ENOENT") {
|
|
3893
5125
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path }) };
|
|
@@ -3908,7 +5140,7 @@ async function resolveRequestedTags(input, page) {
|
|
|
3908
5140
|
result: err("INVALID_FRONTMATTER", { message: "explicit tags must be an array of strings" })
|
|
3909
5141
|
};
|
|
3910
5142
|
}
|
|
3911
|
-
const source = input.from ?? (explicit.length === 0 ?
|
|
5143
|
+
const source = input.from ?? (explicit.length === 0 ? join26(input.vault, page) : void 0);
|
|
3912
5144
|
if (!source) return { exitCode: ExitCode.OK, result: ok({ tags: [...new Set(explicit)].sort() }) };
|
|
3913
5145
|
const sourced = await readTagsFromFile(source);
|
|
3914
5146
|
if (!sourced.result.ok) return { exitCode: sourced.exitCode, result: sourced.result };
|
|
@@ -3919,7 +5151,7 @@ async function resolveRequestedTags(input, page) {
|
|
|
3919
5151
|
}
|
|
3920
5152
|
async function readSchema(schemaPath) {
|
|
3921
5153
|
try {
|
|
3922
|
-
return { exitCode: ExitCode.OK, result: ok(await
|
|
5154
|
+
return { exitCode: ExitCode.OK, result: ok(await readFile14(schemaPath, "utf8")) };
|
|
3923
5155
|
} catch (error) {
|
|
3924
5156
|
if (error.code === "ENOENT") {
|
|
3925
5157
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: schemaPath }) };
|
|
@@ -3947,7 +5179,7 @@ function previewResult(page, tags, reconciled, dryRun, filesChanged) {
|
|
|
3947
5179
|
};
|
|
3948
5180
|
}
|
|
3949
5181
|
async function reconcileTagsWhileLocked(input, page, tags, comment) {
|
|
3950
|
-
const schemaPath =
|
|
5182
|
+
const schemaPath = join26(input.vault, "SCHEMA.md");
|
|
3951
5183
|
const current = await readSchema(schemaPath);
|
|
3952
5184
|
if (!current.result.ok) return { exitCode: current.exitCode, result: current.result };
|
|
3953
5185
|
const next = reconcileTaxonomyDocument(current.result.data, { tags, comment });
|
|
@@ -3958,7 +5190,7 @@ async function reconcileTagsWhileLocked(input, page, tags, comment) {
|
|
|
3958
5190
|
}
|
|
3959
5191
|
let verifiedText;
|
|
3960
5192
|
try {
|
|
3961
|
-
verifiedText = await
|
|
5193
|
+
verifiedText = await readFile14(schemaPath, "utf8");
|
|
3962
5194
|
} catch (error) {
|
|
3963
5195
|
return {
|
|
3964
5196
|
exitCode: ExitCode.WRITE_FAILED,
|
|
@@ -3984,7 +5216,7 @@ async function runTagReconcile(input) {
|
|
|
3984
5216
|
const comment = taxonomyCommentForPage(page.data, date, input.reason);
|
|
3985
5217
|
if (!comment.ok) return { exitCode: ExitCode.SCHEME_REJECTED, result: comment };
|
|
3986
5218
|
if (!input.write) {
|
|
3987
|
-
const schema = await readSchema(
|
|
5219
|
+
const schema = await readSchema(join26(input.vault, "SCHEMA.md"));
|
|
3988
5220
|
if (!schema.result.ok) return { exitCode: schema.exitCode, result: schema.result };
|
|
3989
5221
|
const preview = reconcileTaxonomyDocument(schema.result.data, { tags, comment: comment.data });
|
|
3990
5222
|
return previewResult(page.data, tags, preview, true, []);
|
|
@@ -4036,22 +5268,9 @@ async function runTagReconcile(input) {
|
|
|
4036
5268
|
}
|
|
4037
5269
|
|
|
4038
5270
|
// src/commands/sync.ts
|
|
4039
|
-
import { existsSync as
|
|
4040
|
-
import { join as
|
|
4041
|
-
import { execFileSync as
|
|
4042
|
-
|
|
4043
|
-
// src/utils/git.ts
|
|
4044
|
-
import { execFileSync } from "child_process";
|
|
4045
|
-
function git(cwd, args) {
|
|
4046
|
-
try {
|
|
4047
|
-
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
4048
|
-
} catch {
|
|
4049
|
-
return "";
|
|
4050
|
-
}
|
|
4051
|
-
}
|
|
4052
|
-
function gitStrict(cwd, args) {
|
|
4053
|
-
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
4054
|
-
}
|
|
5271
|
+
import { existsSync as existsSync6 } from "fs";
|
|
5272
|
+
import { join as join27 } from "path";
|
|
5273
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
4055
5274
|
|
|
4056
5275
|
// src/utils/vault-git-pathspec.ts
|
|
4057
5276
|
var VAULT_GENERATED_COMMIT_PATHS = [
|
|
@@ -4092,7 +5311,7 @@ function isTrackedNotePath(path) {
|
|
|
4092
5311
|
}
|
|
4093
5312
|
function refHasPath(vault, ref, path) {
|
|
4094
5313
|
try {
|
|
4095
|
-
|
|
5314
|
+
execFileSync3("git", ["cat-file", "-e", `${ref}:${path}`], {
|
|
4096
5315
|
cwd: vault,
|
|
4097
5316
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4098
5317
|
});
|
|
@@ -4104,7 +5323,7 @@ function refHasPath(vault, ref, path) {
|
|
|
4104
5323
|
function runSyncStatus(input) {
|
|
4105
5324
|
const vault = input.vault;
|
|
4106
5325
|
const includeStashes = input.includeStashes ?? false;
|
|
4107
|
-
if (!
|
|
5326
|
+
if (!existsSync6(join27(vault, ".git"))) {
|
|
4108
5327
|
return {
|
|
4109
5328
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4110
5329
|
result: ok({
|
|
@@ -4211,7 +5430,7 @@ function runSyncStatus(input) {
|
|
|
4211
5430
|
}
|
|
4212
5431
|
async function runSyncPush(input) {
|
|
4213
5432
|
const vault = input.vault;
|
|
4214
|
-
if (!
|
|
5433
|
+
if (!existsSync6(join27(vault, ".git"))) {
|
|
4215
5434
|
return {
|
|
4216
5435
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4217
5436
|
result: err("NOT_A_GIT_REPO", { path: vault })
|
|
@@ -4371,100 +5590,44 @@ function enableGitLongPathsOnWindows(vault) {
|
|
|
4371
5590
|
}
|
|
4372
5591
|
async function runSyncPull(input) {
|
|
4373
5592
|
const vault = input.vault;
|
|
4374
|
-
if (!
|
|
5593
|
+
if (!existsSync6(join27(vault, ".git"))) {
|
|
4375
5594
|
return {
|
|
4376
5595
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4377
5596
|
result: err("NOT_A_GIT_REPO", { path: vault })
|
|
4378
5597
|
};
|
|
4379
5598
|
}
|
|
4380
5599
|
enableGitLongPathsOnWindows(vault);
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
gitStrict(vault, ["fetch", "origin"]);
|
|
4384
|
-
fetched = true;
|
|
4385
|
-
} catch (e) {
|
|
5600
|
+
const remoteUrl = git(vault, ["remote", "get-url", "origin"]);
|
|
5601
|
+
if (!remoteUrl) {
|
|
4386
5602
|
return {
|
|
4387
5603
|
exitCode: ExitCode.SYNC_PULL_FAILED,
|
|
4388
|
-
result: err("
|
|
5604
|
+
result: err("GIT_PULL_FAILED", { message: "no remote configured (origin)" })
|
|
4389
5605
|
};
|
|
4390
5606
|
}
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
let autoResolved = 0;
|
|
4395
|
-
try {
|
|
4396
|
-
const pullOutput = gitStrict(vault, ["pull", "--rebase", "origin", "HEAD"]);
|
|
4397
|
-
pulled = true;
|
|
4398
|
-
const fileMatch = pullOutput.match(/(\d+) file[s]? changed/);
|
|
4399
|
-
if (fileMatch) filesUpdated = parseInt(fileMatch[1], 10);
|
|
4400
|
-
} catch (e) {
|
|
4401
|
-
const errString = String(e);
|
|
4402
|
-
if (errString.includes("conflict")) {
|
|
4403
|
-
let inConflict = true;
|
|
4404
|
-
while (inConflict) {
|
|
4405
|
-
const stoppedSha = git(vault, ["rev-parse", "--verify", "REBASE_HEAD"]);
|
|
4406
|
-
let commitMsg = "";
|
|
4407
|
-
if (stoppedSha) {
|
|
4408
|
-
commitMsg = git(vault, ["log", "--format=%s", "-1", stoppedSha]);
|
|
4409
|
-
}
|
|
4410
|
-
const isArchiveOrSnapshot = commitMsg.startsWith("archive: moved") || commitMsg.startsWith("Snapshot ");
|
|
4411
|
-
const conflictedFiles = git(vault, ["diff", "--name-only", "--diff-filter=U"]);
|
|
4412
|
-
const conflictedList = conflictedFiles ? conflictedFiles.split("\n").filter((l) => l.trim().length > 0) : [];
|
|
4413
|
-
if (conflictedList.length === 0) {
|
|
4414
|
-
try {
|
|
4415
|
-
gitStrict(vault, ["rebase", "--continue"]);
|
|
4416
|
-
inConflict = true;
|
|
4417
|
-
} catch {
|
|
4418
|
-
inConflict = false;
|
|
4419
|
-
}
|
|
4420
|
-
continue;
|
|
4421
|
-
}
|
|
4422
|
-
if (isArchiveOrSnapshot) {
|
|
4423
|
-
for (const f of conflictedList) {
|
|
4424
|
-
try {
|
|
4425
|
-
gitStrict(vault, ["checkout", "--ours", f]);
|
|
4426
|
-
gitStrict(vault, ["add", f]);
|
|
4427
|
-
} catch {
|
|
4428
|
-
}
|
|
4429
|
-
}
|
|
4430
|
-
autoResolved += conflictedList.length;
|
|
4431
|
-
try {
|
|
4432
|
-
gitStrict(vault, ["rebase", "--continue"]);
|
|
4433
|
-
} catch (continueErr) {
|
|
4434
|
-
continue;
|
|
4435
|
-
}
|
|
4436
|
-
} else {
|
|
4437
|
-
conflicts = conflictedList.length;
|
|
4438
|
-
return {
|
|
4439
|
-
exitCode: ExitCode.SYNC_PULL_FAILED,
|
|
4440
|
-
result: ok({
|
|
4441
|
-
fetched,
|
|
4442
|
-
pulled: false,
|
|
4443
|
-
files_updated: 0,
|
|
4444
|
-
conflicts,
|
|
4445
|
-
auto_resolved: 0,
|
|
4446
|
-
lint_errors: 0,
|
|
4447
|
-
lint_warnings: 0,
|
|
4448
|
-
humanHint: `pull failed with ${conflicts} conflict(s) on non-archive commit "${commitMsg}" \u2014 resolve manually`
|
|
4449
|
-
})
|
|
4450
|
-
};
|
|
4451
|
-
}
|
|
4452
|
-
}
|
|
4453
|
-
if (autoResolved > 0) {
|
|
4454
|
-
const diffOutput = git(vault, ["diff", "--stat", "HEAD@{1}..HEAD"]);
|
|
4455
|
-
if (diffOutput) {
|
|
4456
|
-
const fileMatch = diffOutput.match(/(\d+) file[s]? changed/);
|
|
4457
|
-
if (fileMatch) filesUpdated = parseInt(fileMatch[1], 10);
|
|
4458
|
-
}
|
|
4459
|
-
pulled = true;
|
|
4460
|
-
conflicts = 0;
|
|
4461
|
-
}
|
|
4462
|
-
} else {
|
|
5607
|
+
const helper = await runVaultSyncPullHelper({ vault, remote: "origin", branch: "main" });
|
|
5608
|
+
if (!helper.ok) {
|
|
5609
|
+
if (helper.error === "PREFLIGHT_FAILED") {
|
|
4463
5610
|
return {
|
|
4464
|
-
exitCode: ExitCode.
|
|
4465
|
-
result: err("
|
|
5611
|
+
exitCode: ExitCode.PREFLIGHT_FAILED,
|
|
5612
|
+
result: err("PREFLIGHT_FAILED", helper.detail)
|
|
4466
5613
|
};
|
|
4467
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
|
+
}
|
|
4468
5631
|
}
|
|
4469
5632
|
const pathFix = await fixPathTooLong({ vault });
|
|
4470
5633
|
const pathFixCount = pathFix.result.ok ? pathFix.result.data.fixed.length : 0;
|
|
@@ -4477,8 +5640,7 @@ async function runSyncPull(input) {
|
|
|
4477
5640
|
}
|
|
4478
5641
|
const hintParts = [];
|
|
4479
5642
|
if (filesUpdated > 0) hintParts.push(`updated ${filesUpdated} file(s)`);
|
|
4480
|
-
else hintParts.push("already up to date");
|
|
4481
|
-
if (autoResolved > 0) hintParts.push(`${autoResolved} conflict(s) auto-resolved`);
|
|
5643
|
+
else hintParts.push(pulled ? "pulled via vault-sync helper" : "already up to date");
|
|
4482
5644
|
if (pathFixCount > 0) hintParts.push(`${pathFixCount} long path(s) fixed`);
|
|
4483
5645
|
if (lintErrors > 0) hintParts.push(`${lintErrors} lint error(s)`);
|
|
4484
5646
|
if (lintWarnings > 0) hintParts.push(`${lintWarnings} lint warning(s)`);
|
|
@@ -4543,7 +5705,7 @@ function runSyncPeers(input) {
|
|
|
4543
5705
|
}
|
|
4544
5706
|
function runSyncLock(input) {
|
|
4545
5707
|
const vault = input.vault;
|
|
4546
|
-
if (!
|
|
5708
|
+
if (!existsSync6(vault)) {
|
|
4547
5709
|
return {
|
|
4548
5710
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4549
5711
|
result: err("VAULT_PATH_INVALID", { path: vault })
|
|
@@ -4578,7 +5740,7 @@ function runSyncLock(input) {
|
|
|
4578
5740
|
}
|
|
4579
5741
|
function runSyncUnlock(input) {
|
|
4580
5742
|
const vault = input.vault;
|
|
4581
|
-
if (!
|
|
5743
|
+
if (!existsSync6(vault)) {
|
|
4582
5744
|
return {
|
|
4583
5745
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4584
5746
|
result: err("VAULT_PATH_INVALID", { path: vault })
|
|
@@ -4611,8 +5773,8 @@ function runSyncUnlock(input) {
|
|
|
4611
5773
|
}
|
|
4612
5774
|
|
|
4613
5775
|
// src/commands/backup.ts
|
|
4614
|
-
import { statSync as statSync2, readdirSync, readFileSync as
|
|
4615
|
-
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";
|
|
4616
5778
|
import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
|
|
4617
5779
|
|
|
4618
5780
|
// src/utils/s3-client.ts
|
|
@@ -4636,7 +5798,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node_
|
|
|
4636
5798
|
function* walkMarkdown(dir, base) {
|
|
4637
5799
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
4638
5800
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
4639
|
-
const full =
|
|
5801
|
+
const full = join28(dir, entry.name);
|
|
4640
5802
|
if (entry.isDirectory()) {
|
|
4641
5803
|
yield* walkMarkdown(full, base);
|
|
4642
5804
|
} else if (entry.name.endsWith(".md")) {
|
|
@@ -4659,7 +5821,7 @@ async function runBackupSync(input) {
|
|
|
4659
5821
|
let failed = 0;
|
|
4660
5822
|
const files = [...walkMarkdown(input.vault, input.vault)];
|
|
4661
5823
|
for (const relPath of files) {
|
|
4662
|
-
const absPath =
|
|
5824
|
+
const absPath = join28(input.vault, relPath);
|
|
4663
5825
|
const localStat = statSync2(absPath);
|
|
4664
5826
|
let needsUpload = true;
|
|
4665
5827
|
try {
|
|
@@ -4678,7 +5840,7 @@ async function runBackupSync(input) {
|
|
|
4678
5840
|
continue;
|
|
4679
5841
|
}
|
|
4680
5842
|
try {
|
|
4681
|
-
const body =
|
|
5843
|
+
const body = readFileSync11(absPath);
|
|
4682
5844
|
await client.send(new PutObjectCommand({ Bucket: input.bucket, Key: relPath, Body: body }));
|
|
4683
5845
|
uploaded++;
|
|
4684
5846
|
} catch {
|
|
@@ -4735,7 +5897,7 @@ async function runBackupRestore(input) {
|
|
|
4735
5897
|
const objects = list.Contents ?? [];
|
|
4736
5898
|
for (const obj of objects) {
|
|
4737
5899
|
if (!obj.Key) continue;
|
|
4738
|
-
const localPath =
|
|
5900
|
+
const localPath = join28(target, obj.Key);
|
|
4739
5901
|
try {
|
|
4740
5902
|
const localStat = statSync2(localPath);
|
|
4741
5903
|
if (obj.LastModified && localStat.mtime > obj.LastModified) {
|
|
@@ -4749,7 +5911,7 @@ async function runBackupRestore(input) {
|
|
|
4749
5911
|
const body = await resp.Body?.transformToByteArray();
|
|
4750
5912
|
if (body) {
|
|
4751
5913
|
mkdirSync2(dirname6(localPath), { recursive: true });
|
|
4752
|
-
|
|
5914
|
+
writeFileSync4(localPath, Buffer.from(body));
|
|
4753
5915
|
downloaded++;
|
|
4754
5916
|
}
|
|
4755
5917
|
} catch {
|
|
@@ -4781,11 +5943,11 @@ async function runBackupRestore(input) {
|
|
|
4781
5943
|
}
|
|
4782
5944
|
|
|
4783
5945
|
// src/commands/status.ts
|
|
4784
|
-
import { existsSync as
|
|
4785
|
-
import { readFile as
|
|
4786
|
-
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";
|
|
4787
5949
|
async function runStatus(input) {
|
|
4788
|
-
if (!
|
|
5950
|
+
if (!existsSync7(input.vault)) {
|
|
4789
5951
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
4790
5952
|
}
|
|
4791
5953
|
const scan = await scanVault(input.vault);
|
|
@@ -4810,7 +5972,7 @@ async function runStatus(input) {
|
|
|
4810
5972
|
const compound = scan.data.compound.length;
|
|
4811
5973
|
let schemaVersion = "v1";
|
|
4812
5974
|
try {
|
|
4813
|
-
const schemaContent = await
|
|
5975
|
+
const schemaContent = await readFile15(join29(input.vault, "SCHEMA.md"), "utf8");
|
|
4814
5976
|
const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
|
|
4815
5977
|
if (versionMatch) schemaVersion = versionMatch[1];
|
|
4816
5978
|
} catch {
|
|
@@ -4870,8 +6032,8 @@ async function runStatus(input) {
|
|
|
4870
6032
|
}
|
|
4871
6033
|
|
|
4872
6034
|
// src/commands/seed.ts
|
|
4873
|
-
import { mkdir as
|
|
4874
|
-
import { join as
|
|
6035
|
+
import { mkdir as mkdir11, writeFile as writeFile9, stat as stat4 } from "fs/promises";
|
|
6036
|
+
import { join as join30 } from "path";
|
|
4875
6037
|
var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4876
6038
|
var EXAMPLE_PAGES = {
|
|
4877
6039
|
"entities/example-project.md": `---
|
|
@@ -4940,30 +6102,30 @@ Real sources are immutable after ingestion \u2014 never edit them.
|
|
|
4940
6102
|
`;
|
|
4941
6103
|
async function runSeed(input) {
|
|
4942
6104
|
try {
|
|
4943
|
-
await stat4(
|
|
6105
|
+
await stat4(join30(input.vault, "SCHEMA.md"));
|
|
4944
6106
|
} catch {
|
|
4945
6107
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
|
|
4946
6108
|
}
|
|
4947
6109
|
const created = [];
|
|
4948
6110
|
const skipped = [];
|
|
4949
6111
|
for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
|
|
4950
|
-
const absPath =
|
|
6112
|
+
const absPath = join30(input.vault, relPath);
|
|
4951
6113
|
try {
|
|
4952
6114
|
await stat4(absPath);
|
|
4953
6115
|
skipped.push(relPath);
|
|
4954
6116
|
} catch {
|
|
4955
|
-
await
|
|
4956
|
-
await
|
|
6117
|
+
await mkdir11(join30(absPath, ".."), { recursive: true });
|
|
6118
|
+
await writeFile9(absPath, content, "utf8");
|
|
4957
6119
|
created.push(relPath);
|
|
4958
6120
|
}
|
|
4959
6121
|
}
|
|
4960
|
-
const rawPath =
|
|
6122
|
+
const rawPath = join30(input.vault, "raw", "articles", "example-source.md");
|
|
4961
6123
|
try {
|
|
4962
6124
|
await stat4(rawPath);
|
|
4963
6125
|
skipped.push("raw/articles/example-source.md");
|
|
4964
6126
|
} catch {
|
|
4965
|
-
await
|
|
4966
|
-
await
|
|
6127
|
+
await mkdir11(join30(rawPath, ".."), { recursive: true });
|
|
6128
|
+
await writeFile9(rawPath, EXAMPLE_RAW, "utf8");
|
|
4967
6129
|
created.push("raw/articles/example-source.md");
|
|
4968
6130
|
}
|
|
4969
6131
|
if (created.length > 0) {
|
|
@@ -4985,9 +6147,9 @@ async function runSeed(input) {
|
|
|
4985
6147
|
}
|
|
4986
6148
|
|
|
4987
6149
|
// src/commands/canvas.ts
|
|
4988
|
-
import { readFile as
|
|
4989
|
-
import { existsSync as
|
|
4990
|
-
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";
|
|
4991
6153
|
var NODE_WIDTH = 240;
|
|
4992
6154
|
var NODE_HEIGHT = 60;
|
|
4993
6155
|
var COLUMN_SPACING = 400;
|
|
@@ -5065,8 +6227,8 @@ function buildCanvasEdges(adjacency) {
|
|
|
5065
6227
|
return edges;
|
|
5066
6228
|
}
|
|
5067
6229
|
async function runCanvasGenerate(input) {
|
|
5068
|
-
const graphPath = input.graphPath ??
|
|
5069
|
-
if (!
|
|
6230
|
+
const graphPath = input.graphPath ?? join31(input.vault, ".skillwiki", "graph.json");
|
|
6231
|
+
if (!existsSync8(graphPath)) {
|
|
5070
6232
|
return {
|
|
5071
6233
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
5072
6234
|
result: err("FILE_NOT_FOUND", {
|
|
@@ -5077,7 +6239,7 @@ async function runCanvasGenerate(input) {
|
|
|
5077
6239
|
}
|
|
5078
6240
|
let raw;
|
|
5079
6241
|
try {
|
|
5080
|
-
raw = await
|
|
6242
|
+
raw = await readFile16(graphPath, "utf8");
|
|
5081
6243
|
} catch (e) {
|
|
5082
6244
|
return {
|
|
5083
6245
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
@@ -5103,9 +6265,9 @@ async function runCanvasGenerate(input) {
|
|
|
5103
6265
|
const nodes = buildCanvasNodes(paths);
|
|
5104
6266
|
const edges = buildCanvasEdges(graph.adjacency);
|
|
5105
6267
|
const canvas = { nodes, edges };
|
|
5106
|
-
const outPath =
|
|
6268
|
+
const outPath = join31(input.vault, "vault-graph.canvas");
|
|
5107
6269
|
try {
|
|
5108
|
-
await
|
|
6270
|
+
await writeFile10(outPath, JSON.stringify(canvas, null, 2));
|
|
5109
6271
|
} catch (e) {
|
|
5110
6272
|
return {
|
|
5111
6273
|
exitCode: ExitCode.WRITE_FAILED,
|
|
@@ -5125,15 +6287,15 @@ written: ${outPath}`
|
|
|
5125
6287
|
}
|
|
5126
6288
|
|
|
5127
6289
|
// src/commands/fleet-health.ts
|
|
5128
|
-
import { existsSync as
|
|
6290
|
+
import { existsSync as existsSync9, readFileSync as readFileSync12 } from "fs";
|
|
5129
6291
|
import { execSync as nodeExecSync } from "child_process";
|
|
5130
6292
|
import { hostname as nodeHostname, platform as nodePlatform } from "os";
|
|
5131
|
-
import { join as
|
|
6293
|
+
import { join as join32 } from "path";
|
|
5132
6294
|
var SSH_TIMEOUT_MS = 15e3;
|
|
5133
6295
|
var TIMER_UNIT = "agent-memory-trends.timer";
|
|
5134
6296
|
var SERVICE_UNIT = "agent-memory-trends.service";
|
|
5135
6297
|
var SYSTEMD_SERVICE_FAILED_CLASS = "SYSTEMD_SERVICE_FAILED";
|
|
5136
|
-
function
|
|
6298
|
+
function defaultDeps2() {
|
|
5137
6299
|
return {
|
|
5138
6300
|
platform: () => nodePlatform(),
|
|
5139
6301
|
execSync: nodeExecSync
|
|
@@ -5225,9 +6387,9 @@ function applyServiceFailedOverlay(run, serviceFailed) {
|
|
|
5225
6387
|
function probeLocal(vaultPath, deps) {
|
|
5226
6388
|
const latestPath = satelliteLatestRunPath(vaultPath);
|
|
5227
6389
|
let parsed = null;
|
|
5228
|
-
if (
|
|
6390
|
+
if (existsSync9(latestPath)) {
|
|
5229
6391
|
try {
|
|
5230
|
-
const wire = readSatelliteLatestRunFromText(
|
|
6392
|
+
const wire = readSatelliteLatestRunFromText(readFileSync12(latestPath, "utf8"));
|
|
5231
6393
|
if (wire) {
|
|
5232
6394
|
parsed = {
|
|
5233
6395
|
status: wire.status,
|
|
@@ -5353,12 +6515,12 @@ function rowHealthy(reachable, timer, runUnhealthy, timerBad, platform2) {
|
|
|
5353
6515
|
return true;
|
|
5354
6516
|
}
|
|
5355
6517
|
async function runFleetHealth(input) {
|
|
5356
|
-
const deps = input.deps ??
|
|
6518
|
+
const deps = input.deps ?? defaultDeps2();
|
|
5357
6519
|
const env = input.env ?? process.env;
|
|
5358
6520
|
const home = input.home ?? env.HOME ?? "";
|
|
5359
6521
|
const osHostname = input.osHostname ?? env.HOSTNAME ?? nodeHostname();
|
|
5360
6522
|
const vault = input.vault ?? env.WIKI_PATH;
|
|
5361
|
-
const file = input.file ?? (vault ?
|
|
6523
|
+
const file = input.file ?? (vault ? join32(vault, FLEET_REL_PATH) : void 0);
|
|
5362
6524
|
if (!file) {
|
|
5363
6525
|
return {
|
|
5364
6526
|
exitCode: ExitCode.NO_VAULT_CONFIGURED,
|
|
@@ -5442,15 +6604,15 @@ async function runFleetHealth(input) {
|
|
|
5442
6604
|
}
|
|
5443
6605
|
|
|
5444
6606
|
// src/utils/auto-commit.ts
|
|
5445
|
-
import { existsSync as
|
|
5446
|
-
import { join as
|
|
6607
|
+
import { existsSync as existsSync10 } from "fs";
|
|
6608
|
+
import { join as join33 } from "path";
|
|
5447
6609
|
async function postCommit(vault, exitCode) {
|
|
5448
6610
|
if (exitCode !== 0) return;
|
|
5449
6611
|
const home = process.env.HOME ?? "";
|
|
5450
6612
|
const dotenv = await parseDotenvFile(configPath(home));
|
|
5451
6613
|
const autoCommit = process.env.AUTO_COMMIT ?? dotenv["AUTO_COMMIT"];
|
|
5452
6614
|
if (autoCommit === "false") return;
|
|
5453
|
-
if (!
|
|
6615
|
+
if (!existsSync10(join33(vault, ".git"))) return;
|
|
5454
6616
|
const lastOps = readLastOp(vault);
|
|
5455
6617
|
if (lastOps.length === 0) return;
|
|
5456
6618
|
const porcelain = git(vault, ["status", "--porcelain", "--", ...VAULT_COMMIT_PATHSPEC]);
|
|
@@ -5473,103 +6635,6 @@ async function postCommit(vault, exitCode) {
|
|
|
5473
6635
|
clearLastOp(vault);
|
|
5474
6636
|
}
|
|
5475
6637
|
|
|
5476
|
-
// src/utils/protected-vault-write-guard.ts
|
|
5477
|
-
import { readFileSync as readFileSync6 } from "fs";
|
|
5478
|
-
import { join as join24, resolve as resolvePath } from "path";
|
|
5479
|
-
async function guardProtectedVaultWrite(input) {
|
|
5480
|
-
const env = input.env ?? process.env;
|
|
5481
|
-
const home = input.home ?? process.env.HOME ?? "";
|
|
5482
|
-
const cwd = input.cwd ?? process.cwd();
|
|
5483
|
-
const liveVaultPath = await resolveLiveVaultPath({ env, home, cwd });
|
|
5484
|
-
const load = await loadFleetManifestAndHost({
|
|
5485
|
-
vault: liveVaultPath ?? input.vault,
|
|
5486
|
-
hostId: input.hostId,
|
|
5487
|
-
env,
|
|
5488
|
-
home,
|
|
5489
|
-
cwd,
|
|
5490
|
-
osHostname: input.osHostname ?? process.env.HOSTNAME,
|
|
5491
|
-
user: input.user ?? process.env.USER
|
|
5492
|
-
});
|
|
5493
|
-
if (!load?.hostId || load.identityStatus !== "known") {
|
|
5494
|
-
return { blocked: false };
|
|
5495
|
-
}
|
|
5496
|
-
const host = load.manifest.hosts[load.hostId];
|
|
5497
|
-
if (!host || host.role !== "snapshotter" || host.protected !== true) {
|
|
5498
|
-
return { blocked: false };
|
|
5499
|
-
}
|
|
5500
|
-
const snapshotWorktree = resolveSnapshotWorktree(home);
|
|
5501
|
-
const targetVault = resolvePath(input.vault);
|
|
5502
|
-
const canonicalLiveVault = liveVaultPath ? resolvePath(liveVaultPath) : void 0;
|
|
5503
|
-
const canonicalSnapshotWorktree = snapshotWorktree ? resolvePath(snapshotWorktree) : void 0;
|
|
5504
|
-
if (canonicalLiveVault && targetVault === canonicalLiveVault) {
|
|
5505
|
-
return { blocked: false };
|
|
5506
|
-
}
|
|
5507
|
-
if (canonicalSnapshotWorktree && targetVault === canonicalSnapshotWorktree) {
|
|
5508
|
-
return {
|
|
5509
|
-
blocked: true,
|
|
5510
|
-
exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED,
|
|
5511
|
-
result: err("PROTECTED_SNAPSHOTTER_WRITE_BLOCKED", {
|
|
5512
|
-
host_id: load.hostId,
|
|
5513
|
-
command: input.command,
|
|
5514
|
-
reason: `refusing mutation of snapshot worktree '${canonicalSnapshotWorktree}' on protected snapshotter host '${load.hostId}'`,
|
|
5515
|
-
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."
|
|
5516
|
-
})
|
|
5517
|
-
};
|
|
5518
|
-
}
|
|
5519
|
-
if (canonicalLiveVault && targetVault !== canonicalLiveVault) {
|
|
5520
|
-
return {
|
|
5521
|
-
blocked: true,
|
|
5522
|
-
exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED,
|
|
5523
|
-
result: err("PROTECTED_SNAPSHOTTER_WRITE_BLOCKED", {
|
|
5524
|
-
host_id: load.hostId,
|
|
5525
|
-
command: input.command,
|
|
5526
|
-
reason: `refusing vault mutation outside the live vault path '${canonicalLiveVault}' on protected snapshotter host '${load.hostId}'`,
|
|
5527
|
-
guidance: "Use the resolved live skillwiki vault path for authoring. Keep alternate vault roots and snapshot worktrees read-only unless explicitly approved."
|
|
5528
|
-
})
|
|
5529
|
-
};
|
|
5530
|
-
}
|
|
5531
|
-
return { blocked: false };
|
|
5532
|
-
}
|
|
5533
|
-
async function resolveLiveVaultPath(input) {
|
|
5534
|
-
const resolved = await resolveRuntimePath({
|
|
5535
|
-
flag: void 0,
|
|
5536
|
-
envValue: input.env.WIKI_PATH,
|
|
5537
|
-
wikiEnv: input.env.WIKI,
|
|
5538
|
-
home: input.home,
|
|
5539
|
-
cwd: input.cwd
|
|
5540
|
-
});
|
|
5541
|
-
return resolved.ok ? resolved.data.path : void 0;
|
|
5542
|
-
}
|
|
5543
|
-
function resolveSnapshotWorktree(home) {
|
|
5544
|
-
const skillwikiEnv = join24(home, ".skillwiki", ".env");
|
|
5545
|
-
const explicitWorktree = readEnvKey(skillwikiEnv, ["vault_sync.snapshot_worktree"]);
|
|
5546
|
-
if (explicitWorktree) return explicitWorktree;
|
|
5547
|
-
const snapshotProfile = readEnvKey(skillwikiEnv, ["vault_sync.snapshot_profile"]);
|
|
5548
|
-
if (snapshotProfile) {
|
|
5549
|
-
const fromProfile = readEnvKey(snapshotProfile, ["WIKI_GIT_WORKTREE", "SNAPSHOT_WORKTREE", "GIT_DIR"]);
|
|
5550
|
-
if (fromProfile) return fromProfile;
|
|
5551
|
-
}
|
|
5552
|
-
return "/root/wiki-git";
|
|
5553
|
-
}
|
|
5554
|
-
function readEnvKey(path, keys) {
|
|
5555
|
-
try {
|
|
5556
|
-
const content = readFileSync6(path, "utf8");
|
|
5557
|
-
for (const line of content.split(/\r?\n/)) {
|
|
5558
|
-
const trimmed = line.trim();
|
|
5559
|
-
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
|
|
5560
|
-
const eq = trimmed.indexOf("=");
|
|
5561
|
-
if (eq <= 0) continue;
|
|
5562
|
-
const key = trimmed.slice(0, eq).trim();
|
|
5563
|
-
if (!keys.includes(key)) continue;
|
|
5564
|
-
const value = trimmed.slice(eq + 1).trim();
|
|
5565
|
-
if (value.length > 0) return value;
|
|
5566
|
-
}
|
|
5567
|
-
} catch {
|
|
5568
|
-
return void 0;
|
|
5569
|
-
}
|
|
5570
|
-
return void 0;
|
|
5571
|
-
}
|
|
5572
|
-
|
|
5573
6638
|
// src/cli.ts
|
|
5574
6639
|
var pkg = readCliPackageJson();
|
|
5575
6640
|
var program = new Command();
|
|
@@ -5596,6 +6661,28 @@ async function emitGuardedVaultWrite(vault, command, run, opts) {
|
|
|
5596
6661
|
}
|
|
5597
6662
|
return emit(await run(), vault, opts);
|
|
5598
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
|
+
}
|
|
5599
6686
|
program.command("hash <file>").description("compute SHA-256 hash of a vault page body").action(async (file) => emit(await runHash({ file })));
|
|
5600
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 })));
|
|
5601
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) => {
|
|
@@ -5606,12 +6693,12 @@ program.command("validate <file>").description("validate vault page frontmatter
|
|
|
5606
6693
|
else vault = v.vault;
|
|
5607
6694
|
}
|
|
5608
6695
|
if (opts.apply && vault) {
|
|
5609
|
-
return
|
|
6696
|
+
return emitManagedVaultWrite(vault, "validate --apply", () => runValidate({ file, apply: true, vault }), void 0);
|
|
5610
6697
|
}
|
|
5611
6698
|
emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
|
|
5612
6699
|
});
|
|
5613
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) => {
|
|
5614
|
-
const out = opts.out ??
|
|
6701
|
+
const out = opts.out ?? join34(vault, ".skillwiki", "graph.json");
|
|
5615
6702
|
return emitGuardedVaultWrite(vault, "graph build", () => runGraphBuild({ vault, out }));
|
|
5616
6703
|
});
|
|
5617
6704
|
var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
|
|
@@ -5744,6 +6831,12 @@ pageCmd.command("publish <draft> [vault]").description("preview or publish an un
|
|
|
5744
6831
|
{ postCommit: false }
|
|
5745
6832
|
);
|
|
5746
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
|
+
});
|
|
5747
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) => {
|
|
5748
6841
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5749
6842
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
@@ -5796,12 +6889,47 @@ program.command("log-rotate [vault]").description("rotate or trim the vault log
|
|
|
5796
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) => {
|
|
5797
6890
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5798
6891
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5799
|
-
else return
|
|
6892
|
+
else return emitManagedVaultWrite(
|
|
5800
6893
|
v.vault,
|
|
5801
6894
|
"log-append",
|
|
5802
6895
|
() => runLogAppend({ vault: v.vault, content: opts.content })
|
|
5803
6896
|
);
|
|
5804
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
|
+
});
|
|
5805
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) => {
|
|
5806
6934
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5807
6935
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
@@ -5896,7 +7024,7 @@ program.command("archive <page> [vault]").description("archive a typed-knowledge
|
|
|
5896
7024
|
remoteDelete: !!opts.remoteDelete,
|
|
5897
7025
|
maxRemoteDeletes: Number.parseInt(opts.maxRemoteDeletes, 10)
|
|
5898
7026
|
}), v.vault);
|
|
5899
|
-
else return
|
|
7027
|
+
else return emitManagedVaultWrite(
|
|
5900
7028
|
v.vault,
|
|
5901
7029
|
"archive",
|
|
5902
7030
|
() => runArchive({
|
|
@@ -5910,6 +7038,22 @@ program.command("archive <page> [vault]").description("archive a typed-knowledge
|
|
|
5910
7038
|
})
|
|
5911
7039
|
);
|
|
5912
7040
|
});
|
|
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) => {
|
|
7042
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
7043
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
7044
|
+
else return emitManagedVaultWrite(
|
|
7045
|
+
v.vault,
|
|
7046
|
+
"remove",
|
|
7047
|
+
() => runRemove({
|
|
7048
|
+
vault: v.vault,
|
|
7049
|
+
page,
|
|
7050
|
+
remote: opts.remote,
|
|
7051
|
+
remoteDelete: !!opts.remoteDelete,
|
|
7052
|
+
maxRemoteDeletes: Number.parseInt(opts.maxRemoteDeletes, 10),
|
|
7053
|
+
reason: opts.reason
|
|
7054
|
+
})
|
|
7055
|
+
);
|
|
7056
|
+
});
|
|
5913
7057
|
program.command("drift [vault]").description("detect content drift in raw sources").option("--apply", "update sha256 in drifted sources").option("--new <date>", "list raw files ingested on/after this date (YYYY-MM-DD)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5914
7058
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5915
7059
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
@@ -6070,6 +7214,17 @@ syncCmd.command("pull [vault]").description("pull remote vault changes and lint"
|
|
|
6070
7214
|
() => runSyncPull({ vault: v.vault })
|
|
6071
7215
|
);
|
|
6072
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
|
+
});
|
|
6073
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) => {
|
|
6074
7229
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6075
7230
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
@@ -6169,7 +7324,7 @@ program.command("observe [vault]").description("create a raw transcript observat
|
|
|
6169
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) => {
|
|
6170
7325
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6171
7326
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6172
|
-
else if (opts.write) return
|
|
7327
|
+
else if (opts.write) return emitManagedVaultWrite(
|
|
6173
7328
|
v.vault,
|
|
6174
7329
|
"session-brief --write",
|
|
6175
7330
|
() => runSessionBrief({
|