skillwiki 0.9.63 → 0.10.1
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-C5OLZRRM.js +357 -0
- package/dist/chunk-IZABIE44.js +647 -0
- package/dist/chunk-R6BKJWVC.js +890 -0
- package/dist/chunk-SCGC7YNM.js +213 -0
- package/dist/{chunk-TUFQZ5K4.js → chunk-XSTXPA34.js} +834 -1981
- package/dist/cli.js +1780 -697
- package/dist/index-projection-ERFX76U5.js +10 -0
- package/dist/managed-write-preflight-PW4OOOMV.js +11 -0
- package/dist/skillwiki-mcp.js +4 -1
- package/dist/vault-sync/scripts/lib/conflict-markers.sh +69 -0
- package/dist/vault-sync/scripts/lib/delete-intent.sh +74 -0
- package/dist/vault-sync/scripts/lib/fleet.sh +103 -0
- package/dist/vault-sync/scripts/lib/git-case.sh +71 -0
- package/dist/vault-sync/scripts/lib/git-materialization.sh +264 -0
- package/dist/vault-sync/scripts/lib/git-operation-journal.sh +469 -0
- package/dist/vault-sync/scripts/lib/git-rebase-state.sh +180 -0
- package/dist/vault-sync/scripts/lib/lockfile.sh +70 -0
- package/dist/vault-sync/scripts/lib/managed-write-lock.sh +80 -0
- package/dist/vault-sync/scripts/lib/platform.sh +184 -0
- package/dist/vault-sync/scripts/lib/runtime-manifest.sh +223 -0
- package/dist/vault-sync/scripts/wiki-fetch-notify.sh +207 -0
- package/dist/vault-sync/scripts/wiki-fuse-refresh.sh +405 -0
- package/dist/vault-sync/scripts/wiki-pull-with-auto-resolve.sh +631 -0
- package/dist/vault-sync/scripts/wiki-push.sh +364 -0
- package/dist/vault-sync/scripts/wiki-snapshot.sh +587 -0
- package/package.json +2 -2
- package/skills/.claude-plugin/plugin.json +1 -1
- package/skills/.codex-plugin/plugin.json +1 -1
- package/skills/README.md +13 -0
- package/skills/package.json +1 -1
- package/skills/proj-work/SKILL.md +3 -0
- package/skills/skills/proj-work/SKILL.md +3 -0
- package/skills/skills/using-skillwiki/SKILL.md +24 -0
- package/skills/skills/wiki-crystallize/SKILL.md +3 -0
- package/skills/using-skillwiki/SKILL.md +24 -0
- package/skills/wiki-crystallize/SKILL.md +3 -0
package/dist/cli.js
CHANGED
|
@@ -1,53 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
ExitCode,
|
|
4
|
-
FLEET_REL_PATH,
|
|
5
|
-
MetaSchema,
|
|
6
|
-
RawSourceSchema,
|
|
7
3
|
SATELLITE_STALE_MS,
|
|
8
4
|
acquireLock,
|
|
9
5
|
acquireOwnedSyncLock,
|
|
10
6
|
appendLastOp,
|
|
11
|
-
assertTargetInsideVault,
|
|
12
7
|
assessSourceIdentity,
|
|
13
|
-
atomicWriteText,
|
|
14
8
|
buildDegradedReasons,
|
|
15
9
|
buildRemoteObjectPath,
|
|
16
10
|
clearLastOp,
|
|
17
11
|
configPath,
|
|
18
|
-
err,
|
|
19
12
|
evaluateSatelliteRunHealth,
|
|
20
13
|
extractCitationMarkers,
|
|
21
|
-
extractFrontmatter,
|
|
22
14
|
extractTaxonomy,
|
|
23
15
|
findPlugin,
|
|
24
16
|
fixPathTooLong,
|
|
25
17
|
getCliSessionId,
|
|
26
18
|
getSessionId,
|
|
27
|
-
isBlockedHost,
|
|
28
19
|
isFailedRunStatus,
|
|
29
20
|
isValidRemoteDeleteCap,
|
|
30
|
-
|
|
31
|
-
loadFleetManifestAndHost,
|
|
21
|
+
mergeTaxonomyConflict,
|
|
32
22
|
normalizeRemoteRoot,
|
|
33
|
-
ok,
|
|
34
|
-
parseDotenvFile,
|
|
35
|
-
parseDotenvText,
|
|
36
23
|
planAndMaybePruneRemoteObjects,
|
|
37
|
-
prepareTypedPage,
|
|
38
24
|
probeRemoteHealth,
|
|
39
|
-
profileKey,
|
|
40
25
|
readCliPackageJson,
|
|
41
26
|
readLastOp,
|
|
42
27
|
readLock,
|
|
43
|
-
readPage,
|
|
44
28
|
readSatelliteLatestRunFromText,
|
|
45
29
|
reconcileTaxonomyDocument,
|
|
46
|
-
redactSensitiveContent,
|
|
47
30
|
releaseLock,
|
|
48
31
|
releaseOwnedSyncLock,
|
|
49
32
|
renderIndexUpsert,
|
|
50
|
-
|
|
33
|
+
renderProjectIndex,
|
|
51
34
|
resolveInitTimePath,
|
|
52
35
|
resolveRuntimePath,
|
|
53
36
|
runAudit,
|
|
@@ -57,8 +40,6 @@ import {
|
|
|
57
40
|
runConfigSet,
|
|
58
41
|
runDedup,
|
|
59
42
|
runDoctor,
|
|
60
|
-
runFleetContext,
|
|
61
|
-
runFleetValidate,
|
|
62
43
|
runFrontmatterFix,
|
|
63
44
|
runGraphBuild,
|
|
64
45
|
runIndexCheck,
|
|
@@ -85,14 +66,9 @@ import {
|
|
|
85
66
|
runValidate,
|
|
86
67
|
safeWritePage,
|
|
87
68
|
satelliteLatestRunPath,
|
|
88
|
-
scanSensitiveContent,
|
|
89
|
-
scanVault,
|
|
90
|
-
snapshotterAliasForLocalHost,
|
|
91
|
-
splitFrontmatter,
|
|
92
69
|
taxonomyCommentForPage,
|
|
93
|
-
upsertIndexEntry
|
|
94
|
-
|
|
95
|
-
} from "./chunk-TUFQZ5K4.js";
|
|
70
|
+
upsertIndexEntry
|
|
71
|
+
} from "./chunk-XSTXPA34.js";
|
|
96
72
|
import {
|
|
97
73
|
normalizeDistTag,
|
|
98
74
|
readCache,
|
|
@@ -100,9 +76,56 @@ 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
|
+
releaseManagedWriteLock,
|
|
95
|
+
runManagedWritePreflight,
|
|
96
|
+
runManagedWriteTransaction
|
|
97
|
+
} from "./chunk-SCGC7YNM.js";
|
|
98
|
+
import {
|
|
99
|
+
FLEET_REL_PATH,
|
|
100
|
+
git,
|
|
101
|
+
gitStrict,
|
|
102
|
+
listJournalOpIds,
|
|
103
|
+
listReviewRequiredOps,
|
|
104
|
+
loadFleetManifest,
|
|
105
|
+
loadFleetManifestAndHost,
|
|
106
|
+
parseDotenvFile,
|
|
107
|
+
parseDotenvText,
|
|
108
|
+
profileKey,
|
|
109
|
+
readJournal,
|
|
110
|
+
resolveFleetHostId,
|
|
111
|
+
runFleetContext,
|
|
112
|
+
runFleetValidate,
|
|
113
|
+
runVaultSyncPullHelper,
|
|
114
|
+
snapshotterAliasForLocalHost,
|
|
115
|
+
supersedeStaleReviewRequiredJournals,
|
|
116
|
+
writeDotenv
|
|
117
|
+
} from "./chunk-R6BKJWVC.js";
|
|
118
|
+
import {
|
|
119
|
+
ExitCode,
|
|
120
|
+
MetaSchema,
|
|
121
|
+
RawSourceSchema,
|
|
122
|
+
err,
|
|
123
|
+
isBlockedHost,
|
|
124
|
+
ok
|
|
125
|
+
} from "./chunk-C5OLZRRM.js";
|
|
103
126
|
|
|
104
127
|
// src/cli.ts
|
|
105
|
-
import { join as
|
|
128
|
+
import { join as join34 } from "path";
|
|
106
129
|
import { Command } from "commander";
|
|
107
130
|
|
|
108
131
|
// src/utils/output.ts
|
|
@@ -597,183 +620,1182 @@ async function runInit(input) {
|
|
|
597
620
|
oldSchemaText = await readFile3(join5(target, "SCHEMA.md"), "utf8");
|
|
598
621
|
} catch {
|
|
599
622
|
}
|
|
600
|
-
if (oldSchemaText && !input.force) {
|
|
623
|
+
if (oldSchemaText && !input.force) {
|
|
624
|
+
return {
|
|
625
|
+
exitCode: ExitCode.INIT_TARGET_NOT_EMPTY,
|
|
626
|
+
result: err("INIT_TARGET_NOT_EMPTY", { target })
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
const envPath = join5(input.home, ".skillwiki", ".env");
|
|
630
|
+
let existingEnvRaw = "";
|
|
631
|
+
try {
|
|
632
|
+
existingEnvRaw = await readFile3(envPath, "utf8");
|
|
633
|
+
} catch {
|
|
634
|
+
}
|
|
635
|
+
const existingEnv = parseDotenvText(existingEnvRaw);
|
|
636
|
+
const swDotenvHadPath = existingEnv.WIKI_PATH !== void 0;
|
|
637
|
+
const explicitTarget = !!input.flag;
|
|
638
|
+
const skipConflictCheck = explicitTarget || !!input.noEnv;
|
|
639
|
+
if (!input.profile && !skipConflictCheck && existingEnv.WIKI_PATH !== void 0 && existingEnv.WIKI_PATH !== target && !input.force) {
|
|
640
|
+
return {
|
|
641
|
+
exitCode: ExitCode.ENV_WRITE_CONFLICT,
|
|
642
|
+
result: err("ENV_WRITE_CONFLICT", { key: "WIKI_PATH", existing: existingEnv.WIKI_PATH, attempted: target })
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
if (!input.profile && !skipConflictCheck && existingEnv.WIKI_LANG !== void 0 && existingEnv.WIKI_LANG !== canonicalLang && !input.force) {
|
|
646
|
+
return {
|
|
647
|
+
exitCode: ExitCode.ENV_WRITE_CONFLICT,
|
|
648
|
+
result: err("ENV_WRITE_CONFLICT", { key: "WIKI_LANG", existing: existingEnv.WIKI_LANG, attempted: canonicalLang })
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
const created = [];
|
|
652
|
+
try {
|
|
653
|
+
await mkdir3(target, { recursive: true });
|
|
654
|
+
for (const d of VAULT_DIRS) {
|
|
655
|
+
await mkdir3(join5(target, d), { recursive: true });
|
|
656
|
+
created.push(d + "/");
|
|
657
|
+
}
|
|
658
|
+
} catch (e) {
|
|
659
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
|
|
660
|
+
}
|
|
661
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
662
|
+
let taxonomy = input.taxonomy && input.taxonomy.length > 0 ? input.taxonomy : DEFAULT_TAXONOMY;
|
|
663
|
+
let domain = input.domain;
|
|
664
|
+
let oldTaxonomy = [];
|
|
665
|
+
if (oldSchemaText) {
|
|
666
|
+
if (!domain) {
|
|
667
|
+
const oldDomain = extractDomainFromSchema(oldSchemaText);
|
|
668
|
+
if (oldDomain) domain = oldDomain;
|
|
669
|
+
}
|
|
670
|
+
const oldTax = extractTaxonomy(oldSchemaText);
|
|
671
|
+
if (oldTax.ok) oldTaxonomy = oldTax.data;
|
|
672
|
+
}
|
|
673
|
+
const taxonomySet = new Set(taxonomy);
|
|
674
|
+
for (const t of oldTaxonomy) {
|
|
675
|
+
if (!taxonomySet.has(t)) {
|
|
676
|
+
taxonomy.push(t);
|
|
677
|
+
taxonomySet.add(t);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
const discovered = await discoverTagsFromPages(target, taxonomy);
|
|
681
|
+
const discovered_tags = discovered.length;
|
|
682
|
+
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");
|
|
683
|
+
try {
|
|
684
|
+
const schemaTpl = await readFile3(join5(input.templates, "SCHEMA.md"), "utf8");
|
|
685
|
+
const schema = schemaTpl.replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang).replace("{{TAXONOMY_YAML}}", fullTaxonomyYaml);
|
|
686
|
+
await writeFile2(join5(target, "SCHEMA.md"), schema, "utf8");
|
|
687
|
+
created.push("SCHEMA.md");
|
|
688
|
+
} catch (e) {
|
|
689
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: "SCHEMA.md", message: String(e) }) };
|
|
690
|
+
}
|
|
691
|
+
const preserved = [];
|
|
692
|
+
async function writeOrPreserve(fileName, render) {
|
|
693
|
+
try {
|
|
694
|
+
const existing = await readFile3(join5(target, fileName), "utf8");
|
|
695
|
+
if (existing.split("\n").length > 10) {
|
|
696
|
+
preserved.push(fileName);
|
|
697
|
+
return void 0;
|
|
698
|
+
}
|
|
699
|
+
} catch {
|
|
700
|
+
}
|
|
701
|
+
try {
|
|
702
|
+
await writeFile2(join5(target, fileName), await render(), "utf8");
|
|
703
|
+
created.push(fileName);
|
|
704
|
+
return void 0;
|
|
705
|
+
} catch (e) {
|
|
706
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: fileName, message: String(e) }) };
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
const err1 = await writeOrPreserve("index.md", async () => {
|
|
710
|
+
const tpl = await readFile3(join5(input.templates, "index.md"), "utf8");
|
|
711
|
+
return tpl.replace("{{INIT_DATE}}", today);
|
|
712
|
+
});
|
|
713
|
+
if (err1) return err1;
|
|
714
|
+
const errObsidian = await writeOrPreserve(".obsidian/app.json", async () => {
|
|
715
|
+
return JSON.stringify({ attachmentFolderPath: ATTACHMENT_FOLDER, newFileLocation: "folder", newFileFolderPath: NEW_FILE_FOLDER }, null, 2) + "\n";
|
|
716
|
+
});
|
|
717
|
+
if (errObsidian) return errObsidian;
|
|
718
|
+
const errTemplatesJson = await writeOrPreserve(".obsidian/templates.json", async () => {
|
|
719
|
+
return JSON.stringify({ folder: TEMPLATE_FOLDER }, null, 2) + "\n";
|
|
720
|
+
});
|
|
721
|
+
if (errTemplatesJson) return errTemplatesJson;
|
|
722
|
+
const errTemplate = await writeOrPreserve(`${TEMPLATE_FOLDER}/tpl-ad-hoc-capture.md`, async () => {
|
|
723
|
+
return [
|
|
724
|
+
"---",
|
|
725
|
+
"source_url:",
|
|
726
|
+
"created: {{date:YYYY-MM-DD}}",
|
|
727
|
+
"ingested: # filled by ingest pipeline",
|
|
728
|
+
"kind: # idea | bug | task | note | other",
|
|
729
|
+
'project: # optional: "[[slug]]"',
|
|
730
|
+
"---",
|
|
731
|
+
"",
|
|
732
|
+
""
|
|
733
|
+
].join("\n");
|
|
734
|
+
});
|
|
735
|
+
if (errTemplate) return errTemplate;
|
|
736
|
+
const err22 = await writeOrPreserve("log.md", async () => {
|
|
737
|
+
const tpl = await readFile3(join5(input.templates, "log.md"), "utf8");
|
|
738
|
+
return tpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang);
|
|
739
|
+
});
|
|
740
|
+
if (err22) return err22;
|
|
741
|
+
const skipEnv = !!input.noEnv || explicitTarget && !input.profile && swDotenvHadPath && !input.force;
|
|
742
|
+
let envWritten = "";
|
|
743
|
+
if (!skipEnv) {
|
|
744
|
+
try {
|
|
745
|
+
const envEntries = {};
|
|
746
|
+
if (input.profile) {
|
|
747
|
+
envEntries[profileKey(input.profile, "PATH")] = target;
|
|
748
|
+
envEntries[profileKey(input.profile, "LANG")] = canonicalLang;
|
|
749
|
+
envEntries["WIKI_DEFAULT"] = input.profile;
|
|
750
|
+
} else {
|
|
751
|
+
envEntries["WIKI_PATH"] = target;
|
|
752
|
+
envEntries["WIKI_LANG"] = canonicalLang;
|
|
753
|
+
}
|
|
754
|
+
await writeDotenv(envPath, envEntries, existingEnvRaw);
|
|
755
|
+
envWritten = envPath;
|
|
756
|
+
} catch (e) {
|
|
757
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: envPath, message: String(e) }) };
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
const importedFromHermes = pathRes.source === "hermes-dotenv" && !swDotenvHadPath;
|
|
761
|
+
const humanHint = [
|
|
762
|
+
`vault: ${target}`,
|
|
763
|
+
`domain: ${domain}`,
|
|
764
|
+
`lang: ${canonicalLang}`,
|
|
765
|
+
`created: ${created.length}, preserved: ${preserved.length}`,
|
|
766
|
+
`discovered tags: ${discovered_tags}`,
|
|
767
|
+
skipEnv ? "env: skipped" : `env: ${envWritten}`
|
|
768
|
+
].join("\n");
|
|
769
|
+
if (created.length > 0) {
|
|
770
|
+
appendLastOp(target, {
|
|
771
|
+
operation: "init",
|
|
772
|
+
summary: `initialized vault: ${domain}`,
|
|
773
|
+
files: created,
|
|
774
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
return {
|
|
778
|
+
exitCode: ExitCode.OK,
|
|
779
|
+
result: ok({
|
|
780
|
+
vault: target,
|
|
781
|
+
domain,
|
|
782
|
+
taxonomy,
|
|
783
|
+
lang: canonicalLang,
|
|
784
|
+
created,
|
|
785
|
+
preserved,
|
|
786
|
+
env_written: envWritten,
|
|
787
|
+
env_skipped: skipEnv,
|
|
788
|
+
imported_from_hermes: importedFromHermes,
|
|
789
|
+
discovered_tags,
|
|
790
|
+
humanHint,
|
|
791
|
+
templates_created: created.includes(`${TEMPLATE_FOLDER}/tpl-ad-hoc-capture.md`)
|
|
792
|
+
})
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// src/commands/index-rebuild.ts
|
|
797
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
798
|
+
import { join as join7 } from "path";
|
|
799
|
+
|
|
800
|
+
// src/utils/protected-vault-write-guard.ts
|
|
801
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
802
|
+
import { join as join6, resolve as resolvePath } from "path";
|
|
803
|
+
async function guardProtectedVaultWrite(input) {
|
|
804
|
+
const env = input.env ?? process.env;
|
|
805
|
+
const home = input.home ?? process.env.HOME ?? "";
|
|
806
|
+
const cwd = input.cwd ?? process.cwd();
|
|
807
|
+
const liveVaultPath = await resolveLiveVaultPath({ env, home, cwd });
|
|
808
|
+
const load = await loadFleetManifestAndHost({
|
|
809
|
+
vault: liveVaultPath ?? input.vault,
|
|
810
|
+
hostId: input.hostId,
|
|
811
|
+
env,
|
|
812
|
+
home,
|
|
813
|
+
cwd,
|
|
814
|
+
osHostname: input.osHostname ?? process.env.HOSTNAME,
|
|
815
|
+
user: input.user ?? process.env.USER
|
|
816
|
+
});
|
|
817
|
+
if (!load?.hostId || load.identityStatus !== "known") {
|
|
818
|
+
return { blocked: false };
|
|
819
|
+
}
|
|
820
|
+
const host = load.manifest.hosts[load.hostId];
|
|
821
|
+
if (!host || host.role !== "snapshotter" || host.protected !== true) {
|
|
822
|
+
return { blocked: false };
|
|
823
|
+
}
|
|
824
|
+
const snapshotWorktree = resolveSnapshotWorktree(home);
|
|
825
|
+
const targetVault = resolvePath(input.vault);
|
|
826
|
+
const canonicalLiveVault = liveVaultPath ? resolvePath(liveVaultPath) : void 0;
|
|
827
|
+
const canonicalSnapshotWorktree = snapshotWorktree ? resolvePath(snapshotWorktree) : void 0;
|
|
828
|
+
if (canonicalLiveVault && targetVault === canonicalLiveVault) {
|
|
829
|
+
return { blocked: false };
|
|
830
|
+
}
|
|
831
|
+
if (canonicalSnapshotWorktree && targetVault === canonicalSnapshotWorktree) {
|
|
832
|
+
return {
|
|
833
|
+
blocked: true,
|
|
834
|
+
exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED,
|
|
835
|
+
result: err("PROTECTED_SNAPSHOTTER_WRITE_BLOCKED", {
|
|
836
|
+
host_id: load.hostId,
|
|
837
|
+
command: input.command,
|
|
838
|
+
reason: `refusing mutation of snapshot worktree '${canonicalSnapshotWorktree}' on protected snapshotter host '${load.hostId}'`,
|
|
839
|
+
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."
|
|
840
|
+
})
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
if (canonicalLiveVault && targetVault !== canonicalLiveVault) {
|
|
844
|
+
return {
|
|
845
|
+
blocked: true,
|
|
846
|
+
exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED,
|
|
847
|
+
result: err("PROTECTED_SNAPSHOTTER_WRITE_BLOCKED", {
|
|
848
|
+
host_id: load.hostId,
|
|
849
|
+
command: input.command,
|
|
850
|
+
reason: `refusing vault mutation outside the live vault path '${canonicalLiveVault}' on protected snapshotter host '${load.hostId}'`,
|
|
851
|
+
guidance: "Use the resolved live skillwiki vault path for authoring. Keep alternate vault roots and snapshot worktrees read-only unless explicitly approved."
|
|
852
|
+
})
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
return { blocked: false };
|
|
856
|
+
}
|
|
857
|
+
async function resolveLiveVaultPath(input) {
|
|
858
|
+
const resolved = await resolveRuntimePath({
|
|
859
|
+
flag: void 0,
|
|
860
|
+
envValue: input.env.WIKI_PATH,
|
|
861
|
+
wikiEnv: input.env.WIKI,
|
|
862
|
+
home: input.home,
|
|
863
|
+
cwd: input.cwd
|
|
864
|
+
});
|
|
865
|
+
return resolved.ok ? resolved.data.path : void 0;
|
|
866
|
+
}
|
|
867
|
+
function resolveSnapshotWorktree(home) {
|
|
868
|
+
const skillwikiEnv = join6(home, ".skillwiki", ".env");
|
|
869
|
+
const explicitWorktree = readEnvKey(skillwikiEnv, ["vault_sync.snapshot_worktree"]);
|
|
870
|
+
if (explicitWorktree) return explicitWorktree;
|
|
871
|
+
const snapshotProfile = readEnvKey(skillwikiEnv, ["vault_sync.snapshot_profile"]);
|
|
872
|
+
if (snapshotProfile) {
|
|
873
|
+
const fromProfile = readEnvKey(snapshotProfile, ["WIKI_GIT_WORKTREE", "SNAPSHOT_WORKTREE", "GIT_DIR"]);
|
|
874
|
+
if (fromProfile) return fromProfile;
|
|
875
|
+
}
|
|
876
|
+
return "/root/wiki-git";
|
|
877
|
+
}
|
|
878
|
+
function readEnvKey(path, keys) {
|
|
879
|
+
try {
|
|
880
|
+
const content = readFileSync2(path, "utf8");
|
|
881
|
+
for (const line of content.split(/\r?\n/)) {
|
|
882
|
+
const trimmed = line.trim();
|
|
883
|
+
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
|
|
884
|
+
const eq = trimmed.indexOf("=");
|
|
885
|
+
if (eq <= 0) continue;
|
|
886
|
+
const key = trimmed.slice(0, eq).trim();
|
|
887
|
+
if (!keys.includes(key)) continue;
|
|
888
|
+
const value = trimmed.slice(eq + 1).trim();
|
|
889
|
+
if (value.length > 0) return value;
|
|
890
|
+
}
|
|
891
|
+
} catch {
|
|
892
|
+
return void 0;
|
|
893
|
+
}
|
|
894
|
+
return void 0;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// src/commands/index-rebuild.ts
|
|
898
|
+
async function runIndexRebuild(input) {
|
|
899
|
+
const projection = await renderRootIndex({ vault: input.vault });
|
|
900
|
+
if (!projection.ok) {
|
|
901
|
+
const code = projection.error === "SCHEME_REJECTED" ? ExitCode.SCHEME_REJECTED : ExitCode.VAULT_PATH_INVALID;
|
|
902
|
+
return { exitCode: code, result: projection };
|
|
903
|
+
}
|
|
904
|
+
let current = "";
|
|
905
|
+
try {
|
|
906
|
+
current = readFileSync3(join7(input.vault, "index.md"), "utf8");
|
|
907
|
+
} catch {
|
|
908
|
+
current = "";
|
|
909
|
+
}
|
|
910
|
+
const changed = current !== projection.data.text;
|
|
911
|
+
if (!input.write) {
|
|
912
|
+
return {
|
|
913
|
+
exitCode: ExitCode.OK,
|
|
914
|
+
result: ok({
|
|
915
|
+
changed,
|
|
916
|
+
dry_run: true,
|
|
917
|
+
entry_count: projection.data.entries.length,
|
|
918
|
+
duplicates_removed: projection.data.duplicates_removed,
|
|
919
|
+
ghosts_removed: projection.data.ghosts_removed,
|
|
920
|
+
humanHint: changed ? `dry run: would rewrite index.md (${projection.data.entries.length} entries)` : `dry run: index.md already canonical (${projection.data.entries.length} entries)`
|
|
921
|
+
})
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
const guard = await guardProtectedVaultWrite({
|
|
925
|
+
vault: input.vault,
|
|
926
|
+
command: "index rebuild"
|
|
927
|
+
});
|
|
928
|
+
if (guard.blocked) {
|
|
929
|
+
return { exitCode: guard.exitCode, result: guard.result };
|
|
930
|
+
}
|
|
931
|
+
return runManagedWriteTransaction({
|
|
932
|
+
vault: input.vault,
|
|
933
|
+
command: "index rebuild",
|
|
934
|
+
allowImmutableRecord: false,
|
|
935
|
+
mutate: async () => {
|
|
936
|
+
if (!changed) {
|
|
937
|
+
return {
|
|
938
|
+
exitCode: ExitCode.OK,
|
|
939
|
+
result: ok({
|
|
940
|
+
changed: false,
|
|
941
|
+
dry_run: false,
|
|
942
|
+
entry_count: projection.data.entries.length,
|
|
943
|
+
duplicates_removed: projection.data.duplicates_removed,
|
|
944
|
+
ghosts_removed: projection.data.ghosts_removed,
|
|
945
|
+
humanHint: `index.md already canonical (${projection.data.entries.length} entries)`
|
|
946
|
+
})
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
const written = await writeRootIndexProjection(input.vault, projection.data);
|
|
950
|
+
if (!written.ok) {
|
|
951
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: written };
|
|
952
|
+
}
|
|
953
|
+
let installed = "";
|
|
954
|
+
try {
|
|
955
|
+
installed = readFileSync3(join7(input.vault, "index.md"), "utf8");
|
|
956
|
+
} catch (error) {
|
|
957
|
+
return {
|
|
958
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
959
|
+
result: err("WRITE_FAILED", { message: String(error) })
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
if (installed !== projection.data.text) {
|
|
963
|
+
return {
|
|
964
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
965
|
+
result: err("WRITE_FAILED", { message: "installed index.md differs from projection" })
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
return {
|
|
969
|
+
exitCode: ExitCode.OK,
|
|
970
|
+
result: ok({
|
|
971
|
+
changed: true,
|
|
972
|
+
dry_run: false,
|
|
973
|
+
entry_count: projection.data.entries.length,
|
|
974
|
+
duplicates_removed: projection.data.duplicates_removed,
|
|
975
|
+
ghosts_removed: projection.data.ghosts_removed,
|
|
976
|
+
humanHint: `rewrote index.md (${projection.data.entries.length} entries)`
|
|
977
|
+
})
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// src/commands/derived-conflicts.ts
|
|
984
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
985
|
+
import { existsSync, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
986
|
+
import { join as join9 } from "path";
|
|
987
|
+
|
|
988
|
+
// src/utils/log-merge.ts
|
|
989
|
+
import { execFileSync } from "child_process";
|
|
990
|
+
import { mkdtempSync, writeFileSync, rmSync } from "fs";
|
|
991
|
+
import { tmpdir } from "os";
|
|
992
|
+
import { join as join8 } from "path";
|
|
993
|
+
var ENTRY_RE = /^## \[/m;
|
|
994
|
+
var PUBLISH_MARKER_RE = /<!--\s*skillwiki-page-publish:([a-f0-9]{64})\s*-->/i;
|
|
995
|
+
function splitLog(text) {
|
|
996
|
+
const normalized = text.replace(/\r\n/g, "\n");
|
|
997
|
+
const match = ENTRY_RE.exec(normalized);
|
|
998
|
+
if (!match || match.index === void 0) {
|
|
999
|
+
return { preamble: normalized.replace(/\n+$/, ""), entries: [] };
|
|
1000
|
+
}
|
|
1001
|
+
const preamble = normalized.slice(0, match.index).replace(/\n+$/, "");
|
|
1002
|
+
const body = normalized.slice(match.index);
|
|
1003
|
+
const parts = body.split(/(?=^## \[)/m).filter((p) => p.length > 0);
|
|
1004
|
+
const entries = parts.map((p) => p.replace(/\n+$/, ""));
|
|
1005
|
+
return { preamble, entries };
|
|
1006
|
+
}
|
|
1007
|
+
function publishId(entry) {
|
|
1008
|
+
return PUBLISH_MARKER_RE.exec(entry)?.[1]?.toLowerCase();
|
|
1009
|
+
}
|
|
1010
|
+
function unionUnstructured(base, ours, theirs) {
|
|
1011
|
+
const dir = mkdtempSync(join8(tmpdir(), "log-union-"));
|
|
1012
|
+
try {
|
|
1013
|
+
const basePath = join8(dir, "base");
|
|
1014
|
+
const oursPath = join8(dir, "ours");
|
|
1015
|
+
const theirsPath = join8(dir, "theirs");
|
|
1016
|
+
writeFileSync(basePath, base.endsWith("\n") || base.length === 0 ? base : `${base}
|
|
1017
|
+
`);
|
|
1018
|
+
writeFileSync(oursPath, ours.endsWith("\n") || ours.length === 0 ? ours : `${ours}
|
|
1019
|
+
`);
|
|
1020
|
+
writeFileSync(theirsPath, theirs.endsWith("\n") || theirs.length === 0 ? theirs : `${theirs}
|
|
1021
|
+
`);
|
|
1022
|
+
const text = execFileSync("git", ["merge-file", "--union", "-p", oursPath, basePath, theirsPath], {
|
|
1023
|
+
encoding: "utf8"
|
|
1024
|
+
});
|
|
1025
|
+
return ok(text.endsWith("\n") ? text : `${text}
|
|
1026
|
+
`);
|
|
1027
|
+
} catch (error) {
|
|
1028
|
+
return err("WRITE_FAILED", { message: `unstructured log union failed: ${String(error)}` });
|
|
1029
|
+
} finally {
|
|
1030
|
+
rmSync(dir, { recursive: true, force: true });
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
function mergeLogConflictStages(input) {
|
|
1034
|
+
const ours = splitLog(input.ours);
|
|
1035
|
+
const theirs = splitLog(input.theirs);
|
|
1036
|
+
const baseSplit = splitLog(input.base);
|
|
1037
|
+
if (ours.entries.length === 0 && theirs.entries.length === 0) {
|
|
1038
|
+
const unioned = unionUnstructured(input.base, input.ours, input.theirs);
|
|
1039
|
+
if (!unioned.ok) return unioned;
|
|
1040
|
+
return ok({ text: unioned.data, deduplicated_operation_ids: [] });
|
|
1041
|
+
}
|
|
1042
|
+
const preamble = ours.preamble || theirs.preamble || baseSplit.preamble || "# Log";
|
|
1043
|
+
const seenMarkers = /* @__PURE__ */ new Set();
|
|
1044
|
+
const seenExact = /* @__PURE__ */ new Set();
|
|
1045
|
+
const out = [];
|
|
1046
|
+
const deduped = [];
|
|
1047
|
+
const push = (entry) => {
|
|
1048
|
+
const marker2 = publishId(entry);
|
|
1049
|
+
if (marker2) {
|
|
1050
|
+
if (seenMarkers.has(marker2)) {
|
|
1051
|
+
deduped.push(marker2);
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
seenMarkers.add(marker2);
|
|
1055
|
+
} else if (seenExact.has(entry)) {
|
|
1056
|
+
return;
|
|
1057
|
+
} else {
|
|
1058
|
+
seenExact.add(entry);
|
|
1059
|
+
}
|
|
1060
|
+
out.push(entry);
|
|
1061
|
+
};
|
|
1062
|
+
for (const e of ours.entries) push(e);
|
|
1063
|
+
for (const e of theirs.entries) push(e);
|
|
1064
|
+
let text = preamble;
|
|
1065
|
+
if (out.length > 0) {
|
|
1066
|
+
text = `${preamble.replace(/\n+$/, "")}
|
|
1067
|
+
|
|
1068
|
+
${out.join("\n\n")}`;
|
|
1069
|
+
}
|
|
1070
|
+
if (!text.endsWith("\n")) text += "\n";
|
|
1071
|
+
return ok({ text, deduplicated_operation_ids: deduped });
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// src/commands/derived-conflicts.ts
|
|
1075
|
+
function classifyDerivedPath(path) {
|
|
1076
|
+
if (path === "index.md") return "root-index";
|
|
1077
|
+
if (path === "SCHEMA.md") return "taxonomy";
|
|
1078
|
+
if (path === "log.md" || path.endsWith("/log.md")) return "log";
|
|
1079
|
+
if (/^projects\/[^/]+\/knowledge\.md$/.test(path)) return "project-index";
|
|
1080
|
+
return "unknown";
|
|
1081
|
+
}
|
|
1082
|
+
function journalPath(vault, operationId2) {
|
|
1083
|
+
const gitPath = git(vault, ["rev-parse", "--git-path", `vault-sync/operations/${operationId2}.env`]);
|
|
1084
|
+
if (!gitPath) return null;
|
|
1085
|
+
return gitPath.startsWith("/") ? gitPath : join9(vault, gitPath);
|
|
1086
|
+
}
|
|
1087
|
+
function readJournalFields(path) {
|
|
1088
|
+
const text = readFileSync5(path, "utf8");
|
|
1089
|
+
const out = {};
|
|
1090
|
+
for (const line of text.split("\n")) {
|
|
1091
|
+
const i = line.indexOf("=");
|
|
1092
|
+
if (i <= 0) continue;
|
|
1093
|
+
out[line.slice(0, i)] = line.slice(i + 1);
|
|
1094
|
+
}
|
|
1095
|
+
return out;
|
|
1096
|
+
}
|
|
1097
|
+
function showStage(vault, stage, path) {
|
|
1098
|
+
try {
|
|
1099
|
+
return execFileSync2("git", ["show", `:${stage}:${path}`], {
|
|
1100
|
+
cwd: vault,
|
|
1101
|
+
encoding: "utf8",
|
|
1102
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1103
|
+
});
|
|
1104
|
+
} catch {
|
|
1105
|
+
return null;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
function stageOid(vault, stage, path) {
|
|
1109
|
+
return git(vault, ["rev-parse", `:${stage}:${path}`]);
|
|
1110
|
+
}
|
|
1111
|
+
function unmergedPaths(vault) {
|
|
1112
|
+
const raw = git(vault, ["diff", "--name-only", "--diff-filter=U"]);
|
|
1113
|
+
return raw ? raw.split("\n").map((s) => s.trim()).filter(Boolean) : [];
|
|
1114
|
+
}
|
|
1115
|
+
function verifyOperation(vault, operationId2) {
|
|
1116
|
+
const path = journalPath(vault, operationId2);
|
|
1117
|
+
if (!path || !existsSync(path)) {
|
|
1118
|
+
return err("PREFLIGHT_FAILED", { reason: "missing-operation-journal", operation_id: operationId2 });
|
|
1119
|
+
}
|
|
1120
|
+
const fields = readJournalFields(path);
|
|
1121
|
+
if (fields.operation_id !== operationId2) {
|
|
1122
|
+
return err("PREFLIGHT_FAILED", { reason: "operation-id-mismatch", operation_id: operationId2 });
|
|
1123
|
+
}
|
|
1124
|
+
if (fields.handoff === "1") {
|
|
1125
|
+
return err("PREFLIGHT_FAILED", { reason: "operation-handoff", operation_id: operationId2 });
|
|
1126
|
+
}
|
|
1127
|
+
const phase = fields.phase ?? "";
|
|
1128
|
+
if (phase !== "rebasing" && phase !== "restoring") {
|
|
1129
|
+
return err("PREFLIGHT_FAILED", { reason: "operation-phase", phase, operation_id: operationId2 });
|
|
1130
|
+
}
|
|
1131
|
+
const currentGitDir = git(vault, ["rev-parse", "--absolute-git-dir"]);
|
|
1132
|
+
const journalGitDir = fields.worktree_git_dir ?? "";
|
|
1133
|
+
if (journalGitDir && currentGitDir && journalGitDir !== currentGitDir) {
|
|
1134
|
+
return err("PREFLIGHT_FAILED", { reason: "worktree-mismatch", operation_id: operationId2 });
|
|
1135
|
+
}
|
|
1136
|
+
if (!journalGitDir && unmergedPaths(vault).length === 0) {
|
|
1137
|
+
return err("PREFLIGHT_FAILED", { reason: "legacy-journal-without-unmerged", operation_id: operationId2 });
|
|
1138
|
+
}
|
|
1139
|
+
return ok({ phase });
|
|
1140
|
+
}
|
|
1141
|
+
async function resolveOne(vault, path, klass) {
|
|
1142
|
+
if (klass === "root-index") {
|
|
1143
|
+
const rendered = await renderRootIndex({ vault });
|
|
1144
|
+
if (!rendered.ok) return rendered;
|
|
1145
|
+
return ok(rendered.data.text);
|
|
1146
|
+
}
|
|
1147
|
+
if (klass === "taxonomy") {
|
|
1148
|
+
const base = showStage(vault, 1, path) ?? "";
|
|
1149
|
+
const ours = showStage(vault, 2, path);
|
|
1150
|
+
const theirs = showStage(vault, 3, path);
|
|
1151
|
+
if (ours === null || theirs === null) {
|
|
1152
|
+
return err("WRITE_FAILED", { path, message: "missing taxonomy stages" });
|
|
1153
|
+
}
|
|
1154
|
+
const merged = mergeTaxonomyConflict(base, ours, theirs);
|
|
1155
|
+
if (!merged.ok) return merged;
|
|
1156
|
+
return ok(merged.data.text);
|
|
1157
|
+
}
|
|
1158
|
+
if (klass === "log") {
|
|
1159
|
+
const base = showStage(vault, 1, path) ?? "";
|
|
1160
|
+
const ours = showStage(vault, 2, path) ?? "";
|
|
1161
|
+
const theirs = showStage(vault, 3, path) ?? "";
|
|
1162
|
+
const merged = mergeLogConflictStages({ base, ours, theirs });
|
|
1163
|
+
if (!merged.ok) return merged;
|
|
1164
|
+
return ok(merged.data.text);
|
|
1165
|
+
}
|
|
1166
|
+
if (klass === "project-index") {
|
|
1167
|
+
const slug = path.replace(/^projects\//, "").replace(/\/knowledge\.md$/, "");
|
|
1168
|
+
const rendered = await renderProjectIndex(vault, slug);
|
|
1169
|
+
if (!rendered.ok) return rendered;
|
|
1170
|
+
return ok(rendered.data.text);
|
|
1171
|
+
}
|
|
1172
|
+
return err("SCHEME_REJECTED", { path, reason: "unknown-derived-class" });
|
|
1173
|
+
}
|
|
1174
|
+
function rollbackPath(vault, path, stage2, stage3) {
|
|
1175
|
+
try {
|
|
1176
|
+
execFileSync2("git", ["checkout", "--conflict=merge", "--", path], {
|
|
1177
|
+
cwd: vault,
|
|
1178
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1179
|
+
});
|
|
1180
|
+
} catch {
|
|
1181
|
+
return false;
|
|
1182
|
+
}
|
|
1183
|
+
const s2 = stageOid(vault, 2, path);
|
|
1184
|
+
const s3 = stageOid(vault, 3, path);
|
|
1185
|
+
return s2 === stage2 && s3 === stage3;
|
|
1186
|
+
}
|
|
1187
|
+
function hasMarkers(path, text) {
|
|
1188
|
+
const lines = text.split(/\r?\n/);
|
|
1189
|
+
let open3 = false;
|
|
1190
|
+
let sep2 = false;
|
|
1191
|
+
for (const line of lines) {
|
|
1192
|
+
if (line.startsWith("<<<<<<< ")) {
|
|
1193
|
+
open3 = true;
|
|
1194
|
+
sep2 = false;
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
if (line === "=======" && open3) {
|
|
1198
|
+
sep2 = true;
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
if (line.startsWith(">>>>>>> ") && open3 && sep2) return true;
|
|
1202
|
+
if (line.startsWith(">>>>>>> ")) {
|
|
1203
|
+
open3 = false;
|
|
1204
|
+
sep2 = false;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
return false;
|
|
1208
|
+
}
|
|
1209
|
+
async function runDerivedConflictResolution(input) {
|
|
1210
|
+
const vault = input.vault;
|
|
1211
|
+
const verified = verifyOperation(vault, input.operationId);
|
|
1212
|
+
if (!verified.ok) {
|
|
1213
|
+
return {
|
|
1214
|
+
exitCode: ExitCode.PREFLIGHT_FAILED,
|
|
1215
|
+
result: err("PREFLIGHT_FAILED", verified.detail)
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
const paths = unmergedPaths(vault);
|
|
1219
|
+
if (paths.length === 0) {
|
|
1220
|
+
return {
|
|
1221
|
+
exitCode: ExitCode.OK,
|
|
1222
|
+
result: ok({
|
|
1223
|
+
resolved: true,
|
|
1224
|
+
resolved_paths: [],
|
|
1225
|
+
unknown_paths: [],
|
|
1226
|
+
rolled_back: false,
|
|
1227
|
+
humanHint: "no unmerged paths"
|
|
1228
|
+
})
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
const classes = paths.map((p) => ({ path: p, klass: classifyDerivedPath(p) }));
|
|
1232
|
+
const unknown = classes.filter((c) => c.klass === "unknown").map((c) => c.path);
|
|
1233
|
+
if (unknown.length > 0) {
|
|
1234
|
+
return {
|
|
1235
|
+
exitCode: ExitCode.SYNC_PULL_FAILED,
|
|
1236
|
+
result: ok({
|
|
1237
|
+
resolved: false,
|
|
1238
|
+
resolved_paths: [],
|
|
1239
|
+
unknown_paths: unknown,
|
|
1240
|
+
rolled_back: false,
|
|
1241
|
+
humanHint: `review-required: unknown conflict paths ${unknown.join(", ")}`
|
|
1242
|
+
})
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
1246
|
+
for (const { path } of classes) {
|
|
1247
|
+
snapshots.set(path, {
|
|
1248
|
+
stage2: stageOid(vault, 2, path),
|
|
1249
|
+
stage3: stageOid(vault, 3, path)
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
const staged = [];
|
|
1253
|
+
try {
|
|
1254
|
+
for (const { path, klass } of classes) {
|
|
1255
|
+
const resolved = await resolveOne(vault, path, klass);
|
|
1256
|
+
if (!resolved.ok) throw new Error(`${path}: ${resolved.error}`);
|
|
1257
|
+
if (hasMarkers(path, resolved.data)) {
|
|
1258
|
+
throw new Error(`${path}: conflict markers remain after resolve`);
|
|
1259
|
+
}
|
|
1260
|
+
writeFileSync2(join9(vault, path), resolved.data, "utf8");
|
|
1261
|
+
execFileSync2("git", ["add", "--", path], { cwd: vault, stdio: ["pipe", "pipe", "pipe"] });
|
|
1262
|
+
staged.push(path);
|
|
1263
|
+
}
|
|
1264
|
+
const remaining = unmergedPaths(vault);
|
|
1265
|
+
if (remaining.length > 0) {
|
|
1266
|
+
throw new Error(`still unmerged: ${remaining.join(",")}`);
|
|
1267
|
+
}
|
|
1268
|
+
return {
|
|
1269
|
+
exitCode: ExitCode.OK,
|
|
1270
|
+
result: ok({
|
|
1271
|
+
resolved: true,
|
|
1272
|
+
resolved_paths: staged,
|
|
1273
|
+
unknown_paths: [],
|
|
1274
|
+
rolled_back: false,
|
|
1275
|
+
humanHint: `resolved ${staged.length} derived path(s)`
|
|
1276
|
+
})
|
|
1277
|
+
};
|
|
1278
|
+
} catch (error) {
|
|
1279
|
+
let rolled = true;
|
|
1280
|
+
for (const path of staged) {
|
|
1281
|
+
const snap = snapshots.get(path);
|
|
1282
|
+
if (!snap || !rollbackPath(vault, path, snap.stage2, snap.stage3)) {
|
|
1283
|
+
rolled = false;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
for (const { path } of classes) {
|
|
1287
|
+
if (staged.includes(path)) continue;
|
|
1288
|
+
const snap = snapshots.get(path);
|
|
1289
|
+
if (snap) rollbackPath(vault, path, snap.stage2, snap.stage3);
|
|
1290
|
+
}
|
|
1291
|
+
return {
|
|
1292
|
+
exitCode: ExitCode.SYNC_PULL_FAILED,
|
|
1293
|
+
result: ok({
|
|
1294
|
+
resolved: false,
|
|
1295
|
+
resolved_paths: [],
|
|
1296
|
+
unknown_paths: [],
|
|
1297
|
+
rolled_back: rolled,
|
|
1298
|
+
humanHint: `derived resolve failed: ${String(error)}`
|
|
1299
|
+
})
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// src/commands/log-materialize.ts
|
|
1305
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
1306
|
+
import { join as join11 } from "path";
|
|
1307
|
+
|
|
1308
|
+
// src/utils/log-events.ts
|
|
1309
|
+
import { mkdir as mkdir4, open, readFile as readFile4, readdir as readdir3 } from "fs/promises";
|
|
1310
|
+
import { join as join10 } from "path";
|
|
1311
|
+
function isPlainObject(value) {
|
|
1312
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1313
|
+
}
|
|
1314
|
+
function canonicalize(value) {
|
|
1315
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
1316
|
+
if (!isPlainObject(value)) return value;
|
|
1317
|
+
const out = {};
|
|
1318
|
+
for (const key of Object.keys(value).sort()) {
|
|
1319
|
+
out[key] = canonicalize(value[key]);
|
|
1320
|
+
}
|
|
1321
|
+
return out;
|
|
1322
|
+
}
|
|
1323
|
+
function eventPathFor(event) {
|
|
1324
|
+
const day2 = event.occurred_at.slice(0, 10);
|
|
1325
|
+
return `meta/log-events/${day2}/${event.operation_id}.json`;
|
|
1326
|
+
}
|
|
1327
|
+
function validateLogEvent(event) {
|
|
1328
|
+
if (event.schema !== "skillwiki-log-event/v1") {
|
|
1329
|
+
return err("SCHEME_REJECTED", { message: "invalid event schema" });
|
|
1330
|
+
}
|
|
1331
|
+
if (!/^[0-9a-f]{64}$/.test(event.operation_id)) {
|
|
1332
|
+
return err("SCHEME_REJECTED", { message: "operation_id must be 64 hex chars" });
|
|
1333
|
+
}
|
|
1334
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(event.occurred_at)) {
|
|
1335
|
+
return err("SCHEME_REJECTED", { message: "occurred_at must be UTC ISO with milliseconds" });
|
|
1336
|
+
}
|
|
1337
|
+
for (const field of ["host_id", "actor", "kind", "target", "note"]) {
|
|
1338
|
+
const v = event[field];
|
|
1339
|
+
if (typeof v !== "string" || v.trim().length === 0 || v.length > 500) {
|
|
1340
|
+
return err("SCHEME_REJECTED", { message: `invalid ${field}` });
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
if (!isPlainObject(event.metadata)) {
|
|
1344
|
+
return err("SCHEME_REJECTED", { message: "metadata must be a plain object" });
|
|
1345
|
+
}
|
|
1346
|
+
const sensitive = scanSensitiveContent(JSON.stringify(event));
|
|
1347
|
+
if (sensitive.length > 0) {
|
|
1348
|
+
return err("SENSITIVE_CONTENT_DETECTED", { findings: sensitive });
|
|
1349
|
+
}
|
|
1350
|
+
return ok({
|
|
1351
|
+
schema: "skillwiki-log-event/v1",
|
|
1352
|
+
operation_id: event.operation_id,
|
|
1353
|
+
occurred_at: event.occurred_at,
|
|
1354
|
+
host_id: event.host_id,
|
|
1355
|
+
actor: event.actor,
|
|
1356
|
+
kind: event.kind,
|
|
1357
|
+
target: event.target,
|
|
1358
|
+
note: event.note,
|
|
1359
|
+
metadata: canonicalize(event.metadata)
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
function canonicalEventJson(event) {
|
|
1363
|
+
const ordered = {
|
|
1364
|
+
schema: event.schema,
|
|
1365
|
+
operation_id: event.operation_id,
|
|
1366
|
+
occurred_at: event.occurred_at,
|
|
1367
|
+
host_id: event.host_id,
|
|
1368
|
+
actor: event.actor,
|
|
1369
|
+
kind: event.kind,
|
|
1370
|
+
target: event.target,
|
|
1371
|
+
note: event.note,
|
|
1372
|
+
metadata: event.metadata
|
|
1373
|
+
};
|
|
1374
|
+
return `${JSON.stringify(ordered, null, 2)}
|
|
1375
|
+
`;
|
|
1376
|
+
}
|
|
1377
|
+
async function writeLogEvent(vault, event) {
|
|
1378
|
+
const validated = validateLogEvent(event);
|
|
1379
|
+
if (!validated.ok) return validated;
|
|
1380
|
+
const rel = eventPathFor(validated.data);
|
|
1381
|
+
const abs = join10(vault, rel);
|
|
1382
|
+
await mkdir4(join10(vault, "meta", "log-events", validated.data.occurred_at.slice(0, 10)), {
|
|
1383
|
+
recursive: true
|
|
1384
|
+
});
|
|
1385
|
+
const body = canonicalEventJson(validated.data);
|
|
1386
|
+
try {
|
|
1387
|
+
const handle = await open(abs, "wx");
|
|
1388
|
+
try {
|
|
1389
|
+
await handle.writeFile(body, "utf8");
|
|
1390
|
+
} finally {
|
|
1391
|
+
await handle.close();
|
|
1392
|
+
}
|
|
1393
|
+
return ok({ path: rel, created: true });
|
|
1394
|
+
} catch (error) {
|
|
1395
|
+
if (error.code === "EEXIST") {
|
|
1396
|
+
let existing;
|
|
1397
|
+
try {
|
|
1398
|
+
existing = await readFile4(abs, "utf8");
|
|
1399
|
+
} catch (readErr) {
|
|
1400
|
+
return err("WRITE_FAILED", { path: rel, message: String(readErr) });
|
|
1401
|
+
}
|
|
1402
|
+
if (existing === body) return ok({ path: rel, created: false });
|
|
1403
|
+
return err("EVENT_IDENTITY_COLLISION", { path: rel });
|
|
1404
|
+
}
|
|
1405
|
+
return err("WRITE_FAILED", { path: rel, message: String(error) });
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
async function readLogEvents(vault) {
|
|
1409
|
+
const root = join10(vault, "meta", "log-events");
|
|
1410
|
+
let days;
|
|
1411
|
+
try {
|
|
1412
|
+
days = (await readdir3(root, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
1413
|
+
} catch {
|
|
1414
|
+
return ok([]);
|
|
1415
|
+
}
|
|
1416
|
+
const events = [];
|
|
1417
|
+
for (const day2 of days) {
|
|
1418
|
+
const files = (await readdir3(join10(root, day2))).filter((f) => f.endsWith(".json")).sort();
|
|
1419
|
+
for (const file of files) {
|
|
1420
|
+
const text = await readFile4(join10(root, day2, file), "utf8");
|
|
1421
|
+
let parsed;
|
|
1422
|
+
try {
|
|
1423
|
+
parsed = JSON.parse(text);
|
|
1424
|
+
} catch {
|
|
1425
|
+
return err("SCHEME_REJECTED", { path: `meta/log-events/${day2}/${file}`, message: "invalid JSON" });
|
|
1426
|
+
}
|
|
1427
|
+
const validated = validateLogEvent(parsed);
|
|
1428
|
+
if (!validated.ok) return validated;
|
|
1429
|
+
if (eventPathFor(validated.data) !== `meta/log-events/${day2}/${file}`) {
|
|
1430
|
+
return err("SCHEME_REJECTED", {
|
|
1431
|
+
path: `meta/log-events/${day2}/${file}`,
|
|
1432
|
+
message: "path/identity mismatch"
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
events.push(validated.data);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
events.sort((a, b) => {
|
|
1439
|
+
if (a.occurred_at !== b.occurred_at) return a.occurred_at < b.occurred_at ? -1 : 1;
|
|
1440
|
+
return a.operation_id < b.operation_id ? -1 : a.operation_id > b.operation_id ? 1 : 0;
|
|
1441
|
+
});
|
|
1442
|
+
return ok(events);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
// src/utils/log-projection.ts
|
|
1446
|
+
function day(event) {
|
|
1447
|
+
return event.occurred_at.slice(0, 10);
|
|
1448
|
+
}
|
|
1449
|
+
function marker(event) {
|
|
1450
|
+
return `<!-- skillwiki-log-event:${event.operation_id} -->`;
|
|
1451
|
+
}
|
|
1452
|
+
function bodyFor(event) {
|
|
1453
|
+
if (event.kind === "page-publish") {
|
|
1454
|
+
const tax = Array.isArray(event.metadata.taxonomy_added) ? event.metadata.taxonomy_added : [];
|
|
1455
|
+
return [
|
|
1456
|
+
`- Published: [[${event.target.replace(/\.md$/, "")}]]`,
|
|
1457
|
+
`- Taxonomy: ${tax.length > 0 ? `added ${tax.join(", ")}` : "no additions"}`,
|
|
1458
|
+
event.note ? `- Note: ${event.note}` : ""
|
|
1459
|
+
].filter(Boolean).join("\n");
|
|
1460
|
+
}
|
|
1461
|
+
if (event.kind === "legacy-log-entry" && typeof event.metadata.legacy_markdown === "string") {
|
|
1462
|
+
const legacy = event.metadata.legacy_markdown.trimEnd();
|
|
1463
|
+
if (legacy.includes(marker(event))) return legacy;
|
|
1464
|
+
return `${legacy}
|
|
1465
|
+
${marker(event)}`;
|
|
1466
|
+
}
|
|
1467
|
+
if (event.kind === "session-brief" || event.kind === "archive" || event.kind === "remove" || event.kind === "log-entry") {
|
|
1468
|
+
return [
|
|
1469
|
+
`- Target: ${event.target}`,
|
|
1470
|
+
`- Note: ${event.note}`,
|
|
1471
|
+
`- Actor: ${event.actor}`,
|
|
1472
|
+
`- Host: ${event.host_id}`
|
|
1473
|
+
].join("\n");
|
|
1474
|
+
}
|
|
1475
|
+
return [
|
|
1476
|
+
`- Event kind: ${event.kind}`,
|
|
1477
|
+
`- Actor: ${event.actor}`,
|
|
1478
|
+
`- Host: ${event.host_id}`,
|
|
1479
|
+
`- Target: ${event.target}`,
|
|
1480
|
+
`- Note: ${event.note}`,
|
|
1481
|
+
`- Metadata: ${JSON.stringify(event.metadata)}`
|
|
1482
|
+
].join("\n");
|
|
1483
|
+
}
|
|
1484
|
+
function renderLogProjection(events) {
|
|
1485
|
+
const ordered = [...events].sort((a, b) => {
|
|
1486
|
+
if (a.occurred_at !== b.occurred_at) return a.occurred_at < b.occurred_at ? -1 : 1;
|
|
1487
|
+
return a.operation_id < b.operation_id ? -1 : a.operation_id > b.operation_id ? 1 : 0;
|
|
1488
|
+
});
|
|
1489
|
+
const lines = [
|
|
1490
|
+
"# Vault Log",
|
|
1491
|
+
"",
|
|
1492
|
+
"Generated by `skillwiki log materialize` from immutable events.",
|
|
1493
|
+
""
|
|
1494
|
+
];
|
|
1495
|
+
for (const event of ordered) {
|
|
1496
|
+
if (event.kind === "legacy-log-entry" && typeof event.metadata.legacy_markdown === "string") {
|
|
1497
|
+
const block = bodyFor(event);
|
|
1498
|
+
lines.push(block.endsWith("\n") ? block.trimEnd() : block, "");
|
|
1499
|
+
continue;
|
|
1500
|
+
}
|
|
1501
|
+
lines.push(`## [${day(event)}] ${event.kind} | ${event.target}`, "", bodyFor(event), marker(event), "");
|
|
1502
|
+
}
|
|
1503
|
+
return `${lines.join("\n").replace(/\n+$/, "")}
|
|
1504
|
+
`;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// src/utils/projection-authority.ts
|
|
1508
|
+
function resolveProjectionAuthority(load) {
|
|
1509
|
+
if (!load) {
|
|
1510
|
+
return ok({ authority_host_id: "standalone", current_host_id: "standalone", can_write: true });
|
|
1511
|
+
}
|
|
1512
|
+
const snapshotters = Object.entries(load.manifest.hosts).filter(([, h]) => h.role === "snapshotter").map(([id]) => id);
|
|
1513
|
+
const configured = load.manifest.projection_authority;
|
|
1514
|
+
let authority = configured;
|
|
1515
|
+
if (!authority) {
|
|
1516
|
+
if (snapshotters.length === 1) authority = snapshotters[0];
|
|
1517
|
+
else {
|
|
1518
|
+
return err("PREFLIGHT_FAILED", {
|
|
1519
|
+
reason: "projection-authority-unresolved",
|
|
1520
|
+
snapshotters
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
if (!load.manifest.hosts[authority]) {
|
|
1525
|
+
return err("PREFLIGHT_FAILED", { reason: "projection-authority-unknown-host", authority });
|
|
1526
|
+
}
|
|
1527
|
+
if (load.identityStatus !== "known" || !load.hostId) {
|
|
1528
|
+
return err("PREFLIGHT_FAILED", {
|
|
1529
|
+
reason: "fleet-identity-unresolved",
|
|
1530
|
+
identity_status: load.identityStatus
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
return ok({
|
|
1534
|
+
authority_host_id: authority,
|
|
1535
|
+
current_host_id: load.hostId,
|
|
1536
|
+
can_write: load.hostId === authority
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
// src/commands/log-materialize.ts
|
|
1541
|
+
async function runLogMaterialize(input) {
|
|
1542
|
+
const events = await readLogEvents(input.vault);
|
|
1543
|
+
if (!events.ok) {
|
|
1544
|
+
return { exitCode: ExitCode.SCHEME_REJECTED, result: events };
|
|
1545
|
+
}
|
|
1546
|
+
const text = renderLogProjection(events.data);
|
|
1547
|
+
let current = "";
|
|
1548
|
+
try {
|
|
1549
|
+
current = readFileSync6(join11(input.vault, "log.md"), "utf8");
|
|
1550
|
+
} catch {
|
|
1551
|
+
current = "";
|
|
1552
|
+
}
|
|
1553
|
+
const changed = current !== text;
|
|
1554
|
+
if (!input.write) {
|
|
1555
|
+
return {
|
|
1556
|
+
exitCode: ExitCode.OK,
|
|
1557
|
+
result: ok({
|
|
1558
|
+
changed,
|
|
1559
|
+
dry_run: true,
|
|
1560
|
+
event_count: events.data.length,
|
|
1561
|
+
humanHint: changed ? `dry run: would rewrite log.md (${events.data.length} events)` : `dry run: log.md already canonical (${events.data.length} events)`
|
|
1562
|
+
})
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
if (!input.skipAuthority) {
|
|
1566
|
+
const fleet = await loadFleetManifestAndHost({ vault: input.vault, hostId: input.hostId });
|
|
1567
|
+
const auth = resolveProjectionAuthority(fleet);
|
|
1568
|
+
if (!auth.ok) return { exitCode: ExitCode.PREFLIGHT_FAILED, result: auth };
|
|
1569
|
+
if (!auth.data.can_write) {
|
|
1570
|
+
return {
|
|
1571
|
+
exitCode: ExitCode.PREFLIGHT_FAILED,
|
|
1572
|
+
result: err("PREFLIGHT_FAILED", {
|
|
1573
|
+
reason: "projection-authority",
|
|
1574
|
+
authority_host_id: auth.data.authority_host_id,
|
|
1575
|
+
current_host_id: auth.data.current_host_id
|
|
1576
|
+
})
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
const guard = await guardProtectedVaultWrite({ vault: input.vault, command: "log materialize" });
|
|
1580
|
+
if (guard.blocked) return { exitCode: guard.exitCode, result: guard.result };
|
|
1581
|
+
}
|
|
1582
|
+
if (!changed) {
|
|
1583
|
+
return {
|
|
1584
|
+
exitCode: ExitCode.OK,
|
|
1585
|
+
result: ok({
|
|
1586
|
+
changed: false,
|
|
1587
|
+
dry_run: false,
|
|
1588
|
+
event_count: events.data.length,
|
|
1589
|
+
humanHint: `log.md already canonical (${events.data.length} events)`
|
|
1590
|
+
})
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
const written = await atomicWriteText(join11(input.vault, "log.md"), text);
|
|
1594
|
+
if (!written.ok) return { exitCode: ExitCode.WRITE_FAILED, result: written };
|
|
1595
|
+
const installed = readFileSync6(join11(input.vault, "log.md"), "utf8");
|
|
1596
|
+
if (installed !== text) {
|
|
601
1597
|
return {
|
|
602
|
-
exitCode: ExitCode.
|
|
603
|
-
result: err("
|
|
1598
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
1599
|
+
result: err("WRITE_FAILED", { message: "installed log.md differs from projection" })
|
|
604
1600
|
};
|
|
605
1601
|
}
|
|
606
|
-
|
|
607
|
-
|
|
1602
|
+
return {
|
|
1603
|
+
exitCode: ExitCode.OK,
|
|
1604
|
+
result: ok({
|
|
1605
|
+
changed: true,
|
|
1606
|
+
dry_run: false,
|
|
1607
|
+
event_count: events.data.length,
|
|
1608
|
+
humanHint: `rewrote log.md (${events.data.length} events)`
|
|
1609
|
+
})
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// src/commands/log-migrate-legacy.ts
|
|
1614
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
1615
|
+
import { join as join12 } from "path";
|
|
1616
|
+
|
|
1617
|
+
// src/utils/operation-id.ts
|
|
1618
|
+
import { createHash as createHash2 } from "crypto";
|
|
1619
|
+
function operationId(namespace, parts) {
|
|
1620
|
+
const hash = createHash2("sha256").update(namespace).update("\0");
|
|
1621
|
+
for (const part of parts) hash.update(part).update("\0");
|
|
1622
|
+
return hash.digest("hex");
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// src/commands/log-migrate-legacy.ts
|
|
1626
|
+
var BLOCK_RE = /^## \[(\d{4}-\d{2}-\d{2})\][^\n]*$/gm;
|
|
1627
|
+
var PUBLISH_RE = /<!--\s*skillwiki-page-publish:([a-f0-9]{64})\s*-->/i;
|
|
1628
|
+
function splitLegacyBlocks(text) {
|
|
1629
|
+
const matches = [...text.matchAll(BLOCK_RE)];
|
|
1630
|
+
if (matches.length === 0) return [];
|
|
1631
|
+
const out = [];
|
|
1632
|
+
for (let i = 0; i < matches.length; i++) {
|
|
1633
|
+
const m = matches[i];
|
|
1634
|
+
const start = m.index ?? 0;
|
|
1635
|
+
const end = i + 1 < matches.length ? matches[i + 1].index ?? text.length : text.length;
|
|
1636
|
+
const block = text.slice(start, end).replace(/\s+$/, "") + "\n";
|
|
1637
|
+
out.push({ date: m[1], block, ordinal: i });
|
|
1638
|
+
}
|
|
1639
|
+
return out;
|
|
1640
|
+
}
|
|
1641
|
+
async function runLogMigrateLegacy(input) {
|
|
1642
|
+
let text = "";
|
|
608
1643
|
try {
|
|
609
|
-
|
|
1644
|
+
text = readFileSync7(join12(input.vault, "log.md"), "utf8");
|
|
610
1645
|
} catch {
|
|
611
|
-
}
|
|
612
|
-
const existingEnv = parseDotenvText(existingEnvRaw);
|
|
613
|
-
const swDotenvHadPath = existingEnv.WIKI_PATH !== void 0;
|
|
614
|
-
const explicitTarget = !!input.flag;
|
|
615
|
-
const skipConflictCheck = explicitTarget || !!input.noEnv;
|
|
616
|
-
if (!input.profile && !skipConflictCheck && existingEnv.WIKI_PATH !== void 0 && existingEnv.WIKI_PATH !== target && !input.force) {
|
|
617
1646
|
return {
|
|
618
|
-
exitCode: ExitCode.
|
|
619
|
-
result:
|
|
1647
|
+
exitCode: ExitCode.OK,
|
|
1648
|
+
result: ok({
|
|
1649
|
+
planned: 0,
|
|
1650
|
+
created: 0,
|
|
1651
|
+
already_covered: 0,
|
|
1652
|
+
dry_run: !input.write,
|
|
1653
|
+
humanHint: "no root log.md to migrate"
|
|
1654
|
+
})
|
|
620
1655
|
};
|
|
621
1656
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
1657
|
+
const blocks = splitLegacyBlocks(text);
|
|
1658
|
+
let created = 0;
|
|
1659
|
+
let already = 0;
|
|
1660
|
+
for (const b of blocks) {
|
|
1661
|
+
const publish = PUBLISH_RE.exec(b.block)?.[1]?.toLowerCase();
|
|
1662
|
+
const op = publish && /^[0-9a-f]{64}$/.test(publish) ? publish : operationId("skillwiki-legacy-log-v1", [b.date, String(b.ordinal), b.block]);
|
|
1663
|
+
const heading = b.block.split("\n")[0] ?? `## [${b.date}] legacy`;
|
|
1664
|
+
const event = {
|
|
1665
|
+
schema: "skillwiki-log-event/v1",
|
|
1666
|
+
operation_id: op,
|
|
1667
|
+
occurred_at: `${b.date}T00:00:00.000Z`,
|
|
1668
|
+
host_id: input.hostId ?? "standalone",
|
|
1669
|
+
actor: "skillwiki-cli",
|
|
1670
|
+
kind: "legacy-log-entry",
|
|
1671
|
+
target: "log.md",
|
|
1672
|
+
note: heading,
|
|
1673
|
+
metadata: { legacy_markdown: b.block }
|
|
626
1674
|
};
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
} catch (e) {
|
|
636
|
-
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
|
|
637
|
-
}
|
|
638
|
-
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
639
|
-
let taxonomy = input.taxonomy && input.taxonomy.length > 0 ? input.taxonomy : DEFAULT_TAXONOMY;
|
|
640
|
-
let domain = input.domain;
|
|
641
|
-
let oldTaxonomy = [];
|
|
642
|
-
if (oldSchemaText) {
|
|
643
|
-
if (!domain) {
|
|
644
|
-
const oldDomain = extractDomainFromSchema(oldSchemaText);
|
|
645
|
-
if (oldDomain) domain = oldDomain;
|
|
1675
|
+
if (!input.write) continue;
|
|
1676
|
+
const wrote = await writeLogEvent(input.vault, event);
|
|
1677
|
+
if (!wrote.ok) {
|
|
1678
|
+
if (wrote.error === "EVENT_IDENTITY_COLLISION") {
|
|
1679
|
+
already += 1;
|
|
1680
|
+
continue;
|
|
1681
|
+
}
|
|
1682
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: wrote };
|
|
646
1683
|
}
|
|
647
|
-
|
|
648
|
-
|
|
1684
|
+
if (wrote.data.created) created += 1;
|
|
1685
|
+
else already += 1;
|
|
649
1686
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
1687
|
+
return {
|
|
1688
|
+
exitCode: ExitCode.OK,
|
|
1689
|
+
result: ok({
|
|
1690
|
+
planned: blocks.length,
|
|
1691
|
+
created: input.write ? created : 0,
|
|
1692
|
+
already_covered: input.write ? already : 0,
|
|
1693
|
+
dry_run: !input.write,
|
|
1694
|
+
humanHint: input.write ? `migrated legacy log: created=${created} already=${already} planned=${blocks.length}` : `dry run: would migrate ${blocks.length} legacy log block(s)`
|
|
1695
|
+
})
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
// src/commands/projections-materialize.ts
|
|
1700
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
1701
|
+
import { join as join13 } from "path";
|
|
1702
|
+
var defaultDeps = {
|
|
1703
|
+
writeText: (path, text) => atomicWriteText(path, text)
|
|
1704
|
+
};
|
|
1705
|
+
async function runProjectionsMaterialize(input, deps = defaultDeps) {
|
|
1706
|
+
const fleet = await loadFleetManifestAndHost({ vault: input.vault, hostId: input.hostId });
|
|
1707
|
+
const auth = resolveProjectionAuthority(fleet);
|
|
1708
|
+
if (!auth.ok) return { exitCode: ExitCode.PREFLIGHT_FAILED, result: auth };
|
|
1709
|
+
const indexProj = await renderRootIndex({ vault: input.vault });
|
|
1710
|
+
if (!indexProj.ok) return { exitCode: ExitCode.SCHEME_REJECTED, result: indexProj };
|
|
1711
|
+
const events = await readLogEvents(input.vault);
|
|
1712
|
+
if (!events.ok) return { exitCode: ExitCode.SCHEME_REJECTED, result: events };
|
|
1713
|
+
const logText = renderLogProjection(events.data);
|
|
1714
|
+
let curIndex = "";
|
|
1715
|
+
let curLog = "";
|
|
1716
|
+
try {
|
|
1717
|
+
curIndex = readFileSync8(join13(input.vault, "index.md"), "utf8");
|
|
1718
|
+
} catch {
|
|
656
1719
|
}
|
|
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
1720
|
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) }) };
|
|
1721
|
+
curLog = readFileSync8(join13(input.vault, "log.md"), "utf8");
|
|
1722
|
+
} catch {
|
|
667
1723
|
}
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
1724
|
+
const indexDrift = curIndex !== indexProj.data.text;
|
|
1725
|
+
const logDrift = curLog !== logText;
|
|
1726
|
+
if (!input.write) {
|
|
1727
|
+
return {
|
|
1728
|
+
exitCode: ExitCode.OK,
|
|
1729
|
+
result: ok({
|
|
1730
|
+
authority_host_id: auth.data.authority_host_id,
|
|
1731
|
+
current_host_id: auth.data.current_host_id,
|
|
1732
|
+
can_write: auth.data.can_write,
|
|
1733
|
+
index_changed: false,
|
|
1734
|
+
log_changed: false,
|
|
1735
|
+
index_drift: indexDrift,
|
|
1736
|
+
log_drift: logDrift,
|
|
1737
|
+
rolled_back: false,
|
|
1738
|
+
dry_run: true,
|
|
1739
|
+
humanHint: `dry run: index_drift=${indexDrift} log_drift=${logDrift} can_write=${auth.data.can_write}`
|
|
1740
|
+
})
|
|
1741
|
+
};
|
|
685
1742
|
}
|
|
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
|
-
}
|
|
1743
|
+
if (!auth.data.can_write) {
|
|
1744
|
+
return {
|
|
1745
|
+
exitCode: ExitCode.PREFLIGHT_FAILED,
|
|
1746
|
+
result: err("PREFLIGHT_FAILED", {
|
|
1747
|
+
reason: "projection-authority",
|
|
1748
|
+
authority_host_id: auth.data.authority_host_id,
|
|
1749
|
+
current_host_id: auth.data.current_host_id,
|
|
1750
|
+
index_drift: indexDrift,
|
|
1751
|
+
log_drift: logDrift
|
|
1752
|
+
})
|
|
1753
|
+
};
|
|
736
1754
|
}
|
|
737
|
-
const
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
1755
|
+
const indexPath = join13(input.vault, "index.md");
|
|
1756
|
+
const logPath = join13(input.vault, "log.md");
|
|
1757
|
+
const indexWrite = await deps.writeText(indexPath, indexProj.data.text);
|
|
1758
|
+
if (!indexWrite.ok) return { exitCode: ExitCode.WRITE_FAILED, result: indexWrite };
|
|
1759
|
+
const logWrite = await deps.writeText(logPath, logText);
|
|
1760
|
+
if (!logWrite.ok) {
|
|
1761
|
+
await deps.writeText(indexPath, curIndex);
|
|
1762
|
+
return {
|
|
1763
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
1764
|
+
result: ok({
|
|
1765
|
+
authority_host_id: auth.data.authority_host_id,
|
|
1766
|
+
current_host_id: auth.data.current_host_id,
|
|
1767
|
+
can_write: true,
|
|
1768
|
+
index_changed: false,
|
|
1769
|
+
log_changed: false,
|
|
1770
|
+
index_drift: indexDrift,
|
|
1771
|
+
log_drift: logDrift,
|
|
1772
|
+
rolled_back: true,
|
|
1773
|
+
dry_run: false,
|
|
1774
|
+
humanHint: "projection materialize rolled back after log write failure"
|
|
1775
|
+
})
|
|
1776
|
+
};
|
|
753
1777
|
}
|
|
754
1778
|
return {
|
|
755
1779
|
exitCode: ExitCode.OK,
|
|
756
1780
|
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`)
|
|
1781
|
+
authority_host_id: auth.data.authority_host_id,
|
|
1782
|
+
current_host_id: auth.data.current_host_id,
|
|
1783
|
+
can_write: true,
|
|
1784
|
+
index_changed: indexWrite.data.changed,
|
|
1785
|
+
log_changed: logWrite.data.changed,
|
|
1786
|
+
index_drift: indexDrift,
|
|
1787
|
+
log_drift: logDrift,
|
|
1788
|
+
rolled_back: false,
|
|
1789
|
+
dry_run: false,
|
|
1790
|
+
humanHint: `materialized projections index_changed=${indexWrite.data.changed} log_changed=${logWrite.data.changed}`
|
|
769
1791
|
})
|
|
770
1792
|
};
|
|
771
1793
|
}
|
|
772
1794
|
|
|
773
1795
|
// src/commands/claim.ts
|
|
774
|
-
import { mkdir as
|
|
775
|
-
import { existsSync, statSync } from "fs";
|
|
776
|
-
import { join as
|
|
1796
|
+
import { mkdir as mkdir5, writeFile as writeFile3, readFile as readFile5 } from "fs/promises";
|
|
1797
|
+
import { existsSync as existsSync2, statSync } from "fs";
|
|
1798
|
+
import { join as join14 } from "path";
|
|
777
1799
|
function extractDate(filename) {
|
|
778
1800
|
const m = filename.match(/^(\d{4}-\d{2}-\d{2})/);
|
|
779
1801
|
return m?.[1] ?? "";
|
|
@@ -785,14 +1807,14 @@ function extractProjectSlug(projectField) {
|
|
|
785
1807
|
return projectField.replace(/^\[\[/, "").replace(/\]\]$/, "").replace(/^"|"$/g, "");
|
|
786
1808
|
}
|
|
787
1809
|
async function runClaim(input) {
|
|
788
|
-
if (!
|
|
1810
|
+
if (!existsSync2(input.vault) || !statSync(input.vault).isDirectory()) {
|
|
789
1811
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { path: input.vault }) };
|
|
790
1812
|
}
|
|
791
|
-
const absTranscript =
|
|
792
|
-
if (!
|
|
1813
|
+
const absTranscript = join14(input.vault, input.transcript);
|
|
1814
|
+
if (!existsSync2(absTranscript)) {
|
|
793
1815
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: input.transcript }) };
|
|
794
1816
|
}
|
|
795
|
-
const content = await
|
|
1817
|
+
const content = await readFile5(absTranscript, "utf8");
|
|
796
1818
|
const fm = extractFrontmatter(content);
|
|
797
1819
|
let projectSlug = input.project;
|
|
798
1820
|
if (!projectSlug && fm.ok && typeof fm.data.project === "string") {
|
|
@@ -804,8 +1826,8 @@ async function runClaim(input) {
|
|
|
804
1826
|
result: err("SCHEME_REJECTED", { message: "No project specified. Use --project or set project in transcript frontmatter." })
|
|
805
1827
|
};
|
|
806
1828
|
}
|
|
807
|
-
const projectDir =
|
|
808
|
-
if (!
|
|
1829
|
+
const projectDir = join14(input.vault, "projects", projectSlug);
|
|
1830
|
+
if (!existsSync2(projectDir) || !statSync(projectDir).isDirectory()) {
|
|
809
1831
|
return {
|
|
810
1832
|
exitCode: ExitCode.PROJECT_NOT_FOUND,
|
|
811
1833
|
result: err("PROJECT_NOT_FOUND", { project: projectSlug })
|
|
@@ -821,10 +1843,10 @@ async function runClaim(input) {
|
|
|
821
1843
|
}
|
|
822
1844
|
const workSlug = input.slug || extractSlugFromFilename(filename);
|
|
823
1845
|
const dirName = `${date}-${workSlug}`;
|
|
824
|
-
const workDir =
|
|
1846
|
+
const workDir = join14(projectDir, "work", dirName);
|
|
825
1847
|
const relWorkDir = `projects/${projectSlug}/work/${dirName}`;
|
|
826
1848
|
const relSpecPath = `${relWorkDir}/spec.md`;
|
|
827
|
-
if (
|
|
1849
|
+
if (existsSync2(workDir)) {
|
|
828
1850
|
return {
|
|
829
1851
|
exitCode: ExitCode.OK,
|
|
830
1852
|
result: ok({
|
|
@@ -835,7 +1857,7 @@ async function runClaim(input) {
|
|
|
835
1857
|
})
|
|
836
1858
|
};
|
|
837
1859
|
}
|
|
838
|
-
await
|
|
1860
|
+
await mkdir5(workDir, { recursive: true });
|
|
839
1861
|
const kind = fm.ok && typeof fm.data.kind === "string" ? fm.data.kind : "task";
|
|
840
1862
|
const specLines = [
|
|
841
1863
|
"---",
|
|
@@ -850,7 +1872,7 @@ async function runClaim(input) {
|
|
|
850
1872
|
`Claimed from ${input.transcript}`,
|
|
851
1873
|
""
|
|
852
1874
|
];
|
|
853
|
-
await writeFile3(
|
|
1875
|
+
await writeFile3(join14(workDir, "spec.md"), specLines.join("\n"), "utf8");
|
|
854
1876
|
appendLastOp(input.vault, {
|
|
855
1877
|
operation: "claim",
|
|
856
1878
|
summary: `claimed ${input.transcript} \u2192 ${relWorkDir}`,
|
|
@@ -869,8 +1891,8 @@ async function runClaim(input) {
|
|
|
869
1891
|
}
|
|
870
1892
|
|
|
871
1893
|
// src/commands/health.ts
|
|
872
|
-
import { existsSync as
|
|
873
|
-
import { dirname as dirname4, join as
|
|
1894
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync9, renameSync, writeFileSync as writeFileSync3 } from "fs";
|
|
1895
|
+
import { dirname as dirname4, join as join15, resolve as resolve2 } from "path";
|
|
874
1896
|
import { platform } from "os";
|
|
875
1897
|
function statusFromCounts(counts) {
|
|
876
1898
|
if ((counts.error ?? 0) > 0) return "error";
|
|
@@ -981,8 +2003,8 @@ function summarizeChecks(checks) {
|
|
|
981
2003
|
};
|
|
982
2004
|
}
|
|
983
2005
|
function classifyLog(path, id, label, okPattern) {
|
|
984
|
-
if (!
|
|
985
|
-
const lines =
|
|
2006
|
+
if (!existsSync3(path)) return { id, label, status: "warn", detail: `log file missing: ${path}` };
|
|
2007
|
+
const lines = readFileSync9(path, "utf8").split(/\r?\n/).filter(Boolean);
|
|
986
2008
|
if (lines.length === 0) return { id, label, status: "warn", detail: `log file empty: ${path}` };
|
|
987
2009
|
const statusLine = [...lines].reverse().find(
|
|
988
2010
|
(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 +2025,12 @@ function runVaultSyncHealth(home, syncMode) {
|
|
|
1003
2025
|
};
|
|
1004
2026
|
}
|
|
1005
2027
|
const isMac = platform() === "darwin";
|
|
1006
|
-
const shareDir = isMac ?
|
|
1007
|
-
const logDir = isMac ?
|
|
1008
|
-
const filterPath =
|
|
2028
|
+
const shareDir = isMac ? join15(home, "Library", "Application Support", "vault-sync", "bin") : join15(home, ".local", "share", "vault-sync", "bin");
|
|
2029
|
+
const logDir = isMac ? join15(home, "Library", "Logs") : join15(home, ".local", "state", "vault-sync", "log");
|
|
2030
|
+
const filterPath = join15(home, ".config", "rclone", "wiki-push-filters.txt");
|
|
1009
2031
|
const checks = [];
|
|
1010
|
-
const pushScript =
|
|
1011
|
-
if (syncMode === "optional" && !
|
|
2032
|
+
const pushScript = join15(shareDir, "wiki-push.sh");
|
|
2033
|
+
if (syncMode === "optional" && !existsSync3(pushScript)) {
|
|
1012
2034
|
return {
|
|
1013
2035
|
status: "pass",
|
|
1014
2036
|
blocking: false,
|
|
@@ -1021,26 +2043,26 @@ function runVaultSyncHealth(home, syncMode) {
|
|
|
1021
2043
|
}]
|
|
1022
2044
|
};
|
|
1023
2045
|
}
|
|
1024
|
-
checks.push(
|
|
2046
|
+
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
2047
|
if (isMac) {
|
|
1026
|
-
const pushPlist =
|
|
1027
|
-
const fetchPlist =
|
|
1028
|
-
checks.push(
|
|
2048
|
+
const pushPlist = join15(home, "Library", "LaunchAgents", "com.karlchow.wiki-push.plist");
|
|
2049
|
+
const fetchPlist = join15(home, "Library", "LaunchAgents", "com.karlchow.wiki-fetch.plist");
|
|
2050
|
+
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
2051
|
checks.push({ id: "vault_sync_fuse_refresh_job", label: "Vault sync fuse refresh job", status: "pass", detail: "macOS host \u2014 check skipped" });
|
|
1030
2052
|
} 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 (!
|
|
2053
|
+
const pushTimer = join15(home, ".config", "systemd", "user", "wiki-push.timer");
|
|
2054
|
+
const fetchTimer = join15(home, ".config", "systemd", "user", "wiki-fetch.timer");
|
|
2055
|
+
const fuseTimer = join15(home, ".config", "systemd", "user", "wiki-fuse-refresh.timer");
|
|
2056
|
+
const fuseService = join15(home, ".config", "systemd", "user", "wiki-fuse-refresh.service");
|
|
2057
|
+
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)" });
|
|
2058
|
+
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)" });
|
|
2059
|
+
}
|
|
2060
|
+
checks.push(classifyLog(join15(logDir, "wiki-push.log"), "vault_sync_last_push_age", "Vault sync last push recency", /OK push/));
|
|
2061
|
+
checks.push(classifyLog(join15(logDir, "wiki-fetch.log"), "vault_sync_last_fetch_status", "Vault sync last fetch status", /NOTIFY|OK behind|OK/));
|
|
2062
|
+
if (!existsSync3(filterPath)) {
|
|
1041
2063
|
checks.push({ id: "vault_sync_filter_present", label: "Vault sync filter file present", status: "error", detail: `Filter missing: ${filterPath}` });
|
|
1042
2064
|
} else {
|
|
1043
|
-
const content =
|
|
2065
|
+
const content = readFileSync9(filterPath, "utf8");
|
|
1044
2066
|
const missing = [
|
|
1045
2067
|
"remotely-save/data.json",
|
|
1046
2068
|
".skillwiki/sync.lock",
|
|
@@ -1135,7 +2157,7 @@ function buildHumanHint(report) {
|
|
|
1135
2157
|
function writeReport(out, report) {
|
|
1136
2158
|
mkdirSync(dirname4(out), { recursive: true });
|
|
1137
2159
|
const tmp = `${out}.tmp-${process.pid}-${Date.now()}`;
|
|
1138
|
-
|
|
2160
|
+
writeFileSync3(tmp, JSON.stringify(ok(report), null, 2) + "\n", "utf8");
|
|
1139
2161
|
renameSync(tmp, out);
|
|
1140
2162
|
}
|
|
1141
2163
|
function buildIncompleteReport(input, syncMode) {
|
|
@@ -1359,12 +2381,12 @@ async function runHealth(input) {
|
|
|
1359
2381
|
}
|
|
1360
2382
|
|
|
1361
2383
|
// src/commands/archive.ts
|
|
1362
|
-
import { rename as rename2, mkdir as
|
|
1363
|
-
import { join as
|
|
2384
|
+
import { rename as rename2, mkdir as mkdir7, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
|
|
2385
|
+
import { join as join17, dirname as dirname5 } from "path";
|
|
1364
2386
|
|
|
1365
2387
|
// src/utils/delete-intent.ts
|
|
1366
|
-
import { mkdir as
|
|
1367
|
-
import { join as
|
|
2388
|
+
import { mkdir as mkdir6, writeFile as writeFile4, readdir as readdir4, readFile as readFile6 } from "fs/promises";
|
|
2389
|
+
import { join as join16 } from "path";
|
|
1368
2390
|
var DELETE_INTENT_SCHEMA = "vault-delete-intent/v1";
|
|
1369
2391
|
var DELETE_INTENT_DIR = "meta/delete-intents";
|
|
1370
2392
|
function normalizeVaultRelPath(path) {
|
|
@@ -1395,10 +2417,10 @@ function buildDeleteIntent(input) {
|
|
|
1395
2417
|
};
|
|
1396
2418
|
}
|
|
1397
2419
|
async function writeDeleteIntent(vault, intent) {
|
|
1398
|
-
const dir =
|
|
1399
|
-
await
|
|
2420
|
+
const dir = join16(vault, DELETE_INTENT_DIR);
|
|
2421
|
+
await mkdir6(dir, { recursive: true });
|
|
1400
2422
|
const rel = `${DELETE_INTENT_DIR}/${pathToIntentFilename(intent.path)}`;
|
|
1401
|
-
await writeFile4(
|
|
2423
|
+
await writeFile4(join16(vault, rel), JSON.stringify(intent, null, 2) + "\n", "utf8");
|
|
1402
2424
|
return rel;
|
|
1403
2425
|
}
|
|
1404
2426
|
|
|
@@ -1436,7 +2458,7 @@ async function runArchive(input) {
|
|
|
1436
2458
|
if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
|
|
1437
2459
|
if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
|
|
1438
2460
|
const slug = relPath.replace(/\.md$/, "").split("/").pop();
|
|
1439
|
-
const archivePath =
|
|
2461
|
+
const archivePath = join17("_archive", relPath).replace(/\\/g, "/");
|
|
1440
2462
|
const remoteRoot = normalizeRemoteRoot(input.remote);
|
|
1441
2463
|
const remoteObjectPath = buildRemoteObjectPath(remoteRoot, relPath);
|
|
1442
2464
|
let cascade;
|
|
@@ -1462,7 +2484,7 @@ async function runArchive(input) {
|
|
|
1462
2484
|
const indexRefs = [];
|
|
1463
2485
|
if (!isRaw) {
|
|
1464
2486
|
try {
|
|
1465
|
-
const idx = await
|
|
2487
|
+
const idx = await readFile7(join17(input.vault, "index.md"), "utf8");
|
|
1466
2488
|
idx.split("\n").forEach((line, i) => {
|
|
1467
2489
|
if (line.includes(`[[${slug}]]`)) indexRefs.push({ line: i + 1, text: line });
|
|
1468
2490
|
});
|
|
@@ -1489,8 +2511,8 @@ async function runArchive(input) {
|
|
|
1489
2511
|
}
|
|
1490
2512
|
if (input.cascade && input.apply && cascade) {
|
|
1491
2513
|
for (const ref of cascade.source_array_refs) {
|
|
1492
|
-
const absPath =
|
|
1493
|
-
const text = await
|
|
2514
|
+
const absPath = join17(input.vault, ref.page);
|
|
2515
|
+
const text = await readFile7(absPath, "utf8");
|
|
1494
2516
|
const split = splitFrontmatter(text);
|
|
1495
2517
|
if (!split.ok) continue;
|
|
1496
2518
|
const before = split.data.rawFrontmatter;
|
|
@@ -1507,23 +2529,21 @@ ${fmRewritten}
|
|
|
1507
2529
|
}
|
|
1508
2530
|
}
|
|
1509
2531
|
}
|
|
1510
|
-
await
|
|
2532
|
+
await mkdir7(dirname5(join17(input.vault, archivePath)), { recursive: true });
|
|
2533
|
+
await rename2(join17(input.vault, relPath), join17(input.vault, archivePath));
|
|
1511
2534
|
let indexUpdated = false;
|
|
1512
2535
|
if (!isRaw) {
|
|
1513
|
-
const
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
} catch (e) {
|
|
1523
|
-
if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
|
|
2536
|
+
const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-ERFX76U5.js");
|
|
2537
|
+
const before = await readFile7(join17(input.vault, "index.md"), "utf8").catch(() => "");
|
|
2538
|
+
const fullTarget = relPath.replace(/\.md$/, "");
|
|
2539
|
+
const bare = fullTarget.split("/").pop() ?? fullTarget;
|
|
2540
|
+
const hadIndexEntry = before.includes(`[[${fullTarget}]]`) || before.includes(`[[${bare}]]`);
|
|
2541
|
+
const projection = await renderRootIndex2({ vault: input.vault, currentText: before });
|
|
2542
|
+
if (projection.ok && projection.data.text !== before) {
|
|
2543
|
+
const written = await writeRootIndexProjection2(input.vault, projection.data);
|
|
2544
|
+
if (written.ok && written.data.changed && hadIndexEntry) indexUpdated = true;
|
|
1524
2545
|
}
|
|
1525
2546
|
}
|
|
1526
|
-
await rename2(join9(input.vault, relPath), join9(input.vault, archivePath));
|
|
1527
2547
|
const archiveIntent = buildDeleteIntent({
|
|
1528
2548
|
path: relPath,
|
|
1529
2549
|
action: "archive",
|
|
@@ -1564,8 +2584,8 @@ ${fmRewritten}
|
|
|
1564
2584
|
}
|
|
1565
2585
|
|
|
1566
2586
|
// src/commands/remove.ts
|
|
1567
|
-
import { unlink as unlink2,
|
|
1568
|
-
import { join as
|
|
2587
|
+
import { unlink as unlink2, access } from "fs/promises";
|
|
2588
|
+
import { join as join18 } from "path";
|
|
1569
2589
|
async function pathExists(abs) {
|
|
1570
2590
|
try {
|
|
1571
2591
|
await access(abs);
|
|
@@ -1591,7 +2611,7 @@ async function runRemove(input) {
|
|
|
1591
2611
|
if (!relPath) {
|
|
1592
2612
|
try {
|
|
1593
2613
|
const candidate = normalizeVaultRelPath(input.page);
|
|
1594
|
-
if (await pathExists(
|
|
2614
|
+
if (await pathExists(join18(input.vault, candidate))) {
|
|
1595
2615
|
relPath = candidate;
|
|
1596
2616
|
}
|
|
1597
2617
|
} catch {
|
|
@@ -1611,22 +2631,7 @@ async function runRemove(input) {
|
|
|
1611
2631
|
}
|
|
1612
2632
|
const remoteRoot = normalizeRemoteRoot(input.remote);
|
|
1613
2633
|
const remoteObjectPath = buildRemoteObjectPath(remoteRoot, relPath);
|
|
1614
|
-
const slug = relPath.replace(/\.md$/, "").split("/").pop() ?? relPath;
|
|
1615
2634
|
let indexUpdated = false;
|
|
1616
|
-
if (relPath.endsWith(".md") && !relPath.startsWith("raw/")) {
|
|
1617
|
-
const indexPath = join10(input.vault, "index.md");
|
|
1618
|
-
try {
|
|
1619
|
-
const idx = await readFile7(indexPath, "utf8");
|
|
1620
|
-
const originalLines = idx.split("\n");
|
|
1621
|
-
const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
|
|
1622
|
-
if (filtered.length !== originalLines.length) {
|
|
1623
|
-
await writeFile6(indexPath, filtered.join("\n"), "utf8");
|
|
1624
|
-
indexUpdated = true;
|
|
1625
|
-
}
|
|
1626
|
-
} catch (e) {
|
|
1627
|
-
if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
|
|
1628
|
-
}
|
|
1629
|
-
}
|
|
1630
2635
|
const intent = buildDeleteIntent({
|
|
1631
2636
|
path: relPath,
|
|
1632
2637
|
action: "remove",
|
|
@@ -1635,7 +2640,21 @@ async function runRemove(input) {
|
|
|
1635
2640
|
reason: input.reason
|
|
1636
2641
|
});
|
|
1637
2642
|
const tombstonePath = await writeDeleteIntent(input.vault, intent);
|
|
1638
|
-
await unlink2(
|
|
2643
|
+
await unlink2(join18(input.vault, relPath));
|
|
2644
|
+
if (relPath.endsWith(".md") && !relPath.startsWith("raw/")) {
|
|
2645
|
+
const { readFile: readFile17 } = await import("fs/promises");
|
|
2646
|
+
const { join: pathJoin } = await import("path");
|
|
2647
|
+
const { renderRootIndex: renderRootIndex2, writeRootIndexProjection: writeRootIndexProjection2 } = await import("./index-projection-ERFX76U5.js");
|
|
2648
|
+
const before = await readFile17(pathJoin(input.vault, "index.md"), "utf8").catch(() => "");
|
|
2649
|
+
const fullTarget = relPath.replace(/\.md$/, "");
|
|
2650
|
+
const bare = fullTarget.split("/").pop() ?? fullTarget;
|
|
2651
|
+
const hadIndexEntry = before.includes(`[[${fullTarget}]]`) || before.includes(`[[${bare}]]`);
|
|
2652
|
+
const projection = await renderRootIndex2({ vault: input.vault, currentText: before });
|
|
2653
|
+
if (projection.ok && projection.data.text !== before) {
|
|
2654
|
+
const written = await writeRootIndexProjection2(input.vault, projection.data);
|
|
2655
|
+
if (written.ok && written.data.changed && hadIndexEntry) indexUpdated = true;
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
1639
2658
|
appendLastOp(input.vault, {
|
|
1640
2659
|
operation: "remove",
|
|
1641
2660
|
summary: `removed ${relPath} (tombstone ${tombstonePath})`,
|
|
@@ -1664,7 +2683,7 @@ async function runRemove(input) {
|
|
|
1664
2683
|
}
|
|
1665
2684
|
|
|
1666
2685
|
// src/commands/drift.ts
|
|
1667
|
-
import { createHash as
|
|
2686
|
+
import { createHash as createHash3 } from "crypto";
|
|
1668
2687
|
|
|
1669
2688
|
// src/utils/fetch.ts
|
|
1670
2689
|
async function controlledFetch(url, opts) {
|
|
@@ -1743,7 +2762,7 @@ async function runDrift(input) {
|
|
|
1743
2762
|
});
|
|
1744
2763
|
continue;
|
|
1745
2764
|
}
|
|
1746
|
-
const currentHash =
|
|
2765
|
+
const currentHash = createHash3("sha256").update(Buffer.from(resp.data.body, "utf8")).digest("hex");
|
|
1747
2766
|
const identity = assessSourceIdentity({
|
|
1748
2767
|
rawPath: raw.relPath,
|
|
1749
2768
|
sourceUrl,
|
|
@@ -1974,14 +2993,38 @@ ${migratedBody}${newFooter}`;
|
|
|
1974
2993
|
|
|
1975
2994
|
// src/commands/update.ts
|
|
1976
2995
|
import { execSync } from "child_process";
|
|
1977
|
-
import { join as
|
|
2996
|
+
import { join as join19 } from "path";
|
|
2997
|
+
function parseCoreSemver(version) {
|
|
2998
|
+
const m = version.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
2999
|
+
if (!m) return null;
|
|
3000
|
+
return { major: parseInt(m[1], 10), minor: parseInt(m[2], 10), patch: parseInt(m[3], 10) };
|
|
3001
|
+
}
|
|
3002
|
+
function needs0101Migration(previousVersion, newVersion) {
|
|
3003
|
+
const p = parseCoreSemver(previousVersion);
|
|
3004
|
+
const n = parseCoreSemver(newVersion);
|
|
3005
|
+
if (!p || !n) return false;
|
|
3006
|
+
const prevLt = p.major < 0 || p.major === 0 && p.minor < 10 || p.major === 0 && p.minor === 10 && p.patch < 1;
|
|
3007
|
+
const newGe = n.major > 0 || n.major === 0 && n.minor > 10 || n.major === 0 && n.minor === 10 && n.patch >= 1;
|
|
3008
|
+
return prevLt && newGe;
|
|
3009
|
+
}
|
|
3010
|
+
function migrationNotesForUpgrade(previousVersion, newVersion) {
|
|
3011
|
+
if (!needs0101Migration(previousVersion, newVersion)) return [];
|
|
3012
|
+
return [
|
|
3013
|
+
"Migration 0.10.1:",
|
|
3014
|
+
"- pull helper resolves from dist/ + host vault-sync install",
|
|
3015
|
+
"- run: skillwiki doctor",
|
|
3016
|
+
"- if managed writes blocked: skillwiki sync journal list",
|
|
3017
|
+
"- then: skillwiki sync journal clear-stale --dry-run",
|
|
3018
|
+
"- legacy override: SKILLWIKI_VAULT_SYNC_PULL_HELPER=<path-to-wiki-pull-with-auto-resolve.sh>"
|
|
3019
|
+
];
|
|
3020
|
+
}
|
|
1978
3021
|
function resolveGlobalSkillsRoot() {
|
|
1979
3022
|
try {
|
|
1980
3023
|
const globalRoot = execSync("npm root -g", {
|
|
1981
3024
|
encoding: "utf8",
|
|
1982
3025
|
timeout: 5e3
|
|
1983
3026
|
}).trim();
|
|
1984
|
-
return
|
|
3027
|
+
return join19(globalRoot, "skillwiki", "skills");
|
|
1985
3028
|
} catch {
|
|
1986
3029
|
return null;
|
|
1987
3030
|
}
|
|
@@ -2009,7 +3052,7 @@ async function runUpdate(input) {
|
|
|
2009
3052
|
const pkg2 = readCliPackageJson();
|
|
2010
3053
|
const currentVersion = pkg2.version;
|
|
2011
3054
|
const tag = normalizeDistTag(input.distTag);
|
|
2012
|
-
const target =
|
|
3055
|
+
const target = join19(input.home, ".claude", "skills");
|
|
2013
3056
|
let latest;
|
|
2014
3057
|
try {
|
|
2015
3058
|
latest = execSync(`npm view skillwiki@${tag} version`, {
|
|
@@ -2070,6 +3113,9 @@ async function runUpdate(input) {
|
|
|
2070
3113
|
hintLines.push(`version warnings: ${version_warnings.length}`);
|
|
2071
3114
|
for (const w of version_warnings) hintLines.push(` ${w}`);
|
|
2072
3115
|
}
|
|
3116
|
+
for (const line of migrationNotesForUpgrade(currentVersion, latest)) {
|
|
3117
|
+
hintLines.push(line);
|
|
3118
|
+
}
|
|
2073
3119
|
return {
|
|
2074
3120
|
exitCode: ExitCode.OK,
|
|
2075
3121
|
result: ok({
|
|
@@ -2086,22 +3132,22 @@ async function runUpdate(input) {
|
|
|
2086
3132
|
|
|
2087
3133
|
// src/commands/self-update.ts
|
|
2088
3134
|
import { execSync as execSync2 } from "child_process";
|
|
2089
|
-
import { existsSync as
|
|
2090
|
-
import { join as
|
|
3135
|
+
import { existsSync as existsSync4, readFileSync as readFileSync10 } from "fs";
|
|
3136
|
+
import { join as join20 } from "path";
|
|
2091
3137
|
var DEFAULT_SOURCE_ROOT_SUFFIX = "/Desktop/code/llm-wiki";
|
|
2092
3138
|
async function runSelfUpdate(input) {
|
|
2093
3139
|
const currentVersion = readCliPackageJson().version;
|
|
2094
3140
|
const sourceRoot = input.sourceRoot ?? `${input.home}${DEFAULT_SOURCE_ROOT_SUFFIX}`;
|
|
2095
3141
|
const distTag = normalizeDistTag(input.distTag);
|
|
2096
|
-
const localPkgPath =
|
|
2097
|
-
const hasLocalSource =
|
|
3142
|
+
const localPkgPath = join20(sourceRoot, "packages", "cli", "package.json");
|
|
3143
|
+
const hasLocalSource = existsSync4(localPkgPath);
|
|
2098
3144
|
if (input.check) {
|
|
2099
3145
|
let availableVersion = null;
|
|
2100
3146
|
let source;
|
|
2101
3147
|
if (hasLocalSource) {
|
|
2102
3148
|
source = "local";
|
|
2103
3149
|
try {
|
|
2104
|
-
availableVersion = JSON.parse(
|
|
3150
|
+
availableVersion = JSON.parse(readFileSync10(localPkgPath, "utf8")).version ?? null;
|
|
2105
3151
|
} catch {
|
|
2106
3152
|
availableVersion = null;
|
|
2107
3153
|
}
|
|
@@ -2159,7 +3205,7 @@ async function runSelfUpdate(input) {
|
|
|
2159
3205
|
}
|
|
2160
3206
|
const newVersion = (() => {
|
|
2161
3207
|
try {
|
|
2162
|
-
return JSON.parse(
|
|
3208
|
+
return JSON.parse(readFileSync10(localPkgPath, "utf8")).version ?? "unknown";
|
|
2163
3209
|
} catch {
|
|
2164
3210
|
return "unknown";
|
|
2165
3211
|
}
|
|
@@ -2225,21 +3271,21 @@ async function runSelfUpdate(input) {
|
|
|
2225
3271
|
}
|
|
2226
3272
|
|
|
2227
3273
|
// src/commands/transcripts.ts
|
|
2228
|
-
import { readdir as
|
|
2229
|
-
import { join as
|
|
3274
|
+
import { readdir as readdir5, stat as stat3, readFile as readFile9 } from "fs/promises";
|
|
3275
|
+
import { join as join21 } from "path";
|
|
2230
3276
|
async function runTranscripts(input) {
|
|
2231
|
-
const dir =
|
|
3277
|
+
const dir = join21(input.vault, "raw", "transcripts");
|
|
2232
3278
|
let entries;
|
|
2233
3279
|
try {
|
|
2234
|
-
entries = await
|
|
3280
|
+
entries = await readdir5(dir, { withFileTypes: true });
|
|
2235
3281
|
} catch {
|
|
2236
3282
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: { ok: false, error: "VAULT_PATH_INVALID", detail: `raw/transcripts/ not found: ${dir}` } };
|
|
2237
3283
|
}
|
|
2238
3284
|
const transcripts = [];
|
|
2239
3285
|
for (const entry of entries) {
|
|
2240
3286
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
2241
|
-
const filePath =
|
|
2242
|
-
const content = await
|
|
3287
|
+
const filePath = join21(dir, entry.name);
|
|
3288
|
+
const content = await readFile9(filePath, "utf8");
|
|
2243
3289
|
const fm = extractFrontmatter(content);
|
|
2244
3290
|
if (!fm.ok) continue;
|
|
2245
3291
|
const ingested = typeof fm.data.ingested === "string" ? fm.data.ingested : "";
|
|
@@ -2256,10 +3302,10 @@ async function runTranscripts(input) {
|
|
|
2256
3302
|
}
|
|
2257
3303
|
|
|
2258
3304
|
// src/commands/compound.ts
|
|
2259
|
-
import { writeFile as writeFile7, mkdir as
|
|
2260
|
-
import { join as
|
|
2261
|
-
import { existsSync as
|
|
2262
|
-
import { readFile as
|
|
3305
|
+
import { writeFile as writeFile7, mkdir as mkdir8, readdir as readdir6, unlink as unlink3 } from "fs/promises";
|
|
3306
|
+
import { join as join22 } from "path";
|
|
3307
|
+
import { existsSync as existsSync5 } from "fs";
|
|
3308
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
2263
3309
|
var RETRO_HEADING_RE = /^## \[(\d{4}-\d{2}-\d{2})(?:\s+[^\]]+)?\] retro \| loop cycle(?: (\d+))?: (.+)$/;
|
|
2264
3310
|
var FIELD_RE = {
|
|
2265
3311
|
improve: /^-\s+\*?\*?Improve:?\*?\*?\s*(.+)$/m,
|
|
@@ -2357,17 +3403,17 @@ function extractRetroFields(date, cycleName, block) {
|
|
|
2357
3403
|
};
|
|
2358
3404
|
}
|
|
2359
3405
|
async function runCompound(input) {
|
|
2360
|
-
const logPath =
|
|
3406
|
+
const logPath = join22(input.vault, "log.md");
|
|
2361
3407
|
let logText;
|
|
2362
3408
|
try {
|
|
2363
|
-
logText = await
|
|
3409
|
+
logText = await readFile10(logPath, "utf8");
|
|
2364
3410
|
} catch {
|
|
2365
3411
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
|
|
2366
3412
|
}
|
|
2367
3413
|
const entries = parseRetroEntries(logText);
|
|
2368
3414
|
const promoted = [];
|
|
2369
3415
|
const skipped = [];
|
|
2370
|
-
const compoundDir =
|
|
3416
|
+
const compoundDir = join22(input.vault, "projects", input.project, "compound");
|
|
2371
3417
|
for (const entry of entries) {
|
|
2372
3418
|
const generalizeValue = entry.generalize.trim();
|
|
2373
3419
|
if (!/^yes/i.test(generalizeValue)) {
|
|
@@ -2375,8 +3421,8 @@ async function runCompound(input) {
|
|
|
2375
3421
|
continue;
|
|
2376
3422
|
}
|
|
2377
3423
|
const slug = slugify(entry.cycleName);
|
|
2378
|
-
const compoundPath =
|
|
2379
|
-
if (
|
|
3424
|
+
const compoundPath = join22(compoundDir, `${slug}.md`);
|
|
3425
|
+
if (existsSync5(compoundPath)) {
|
|
2380
3426
|
skipped.push(entry.date);
|
|
2381
3427
|
continue;
|
|
2382
3428
|
}
|
|
@@ -2414,8 +3460,8 @@ async function runCompound(input) {
|
|
|
2414
3460
|
].join("\n");
|
|
2415
3461
|
const content = frontmatter + "\n" + body;
|
|
2416
3462
|
if (!input.dryRun) {
|
|
2417
|
-
if (!
|
|
2418
|
-
await
|
|
3463
|
+
if (!existsSync5(compoundDir)) {
|
|
3464
|
+
await mkdir8(compoundDir, { recursive: true });
|
|
2419
3465
|
}
|
|
2420
3466
|
await writeFile7(compoundPath, content, "utf8");
|
|
2421
3467
|
}
|
|
@@ -2436,16 +3482,16 @@ async function runCompound(input) {
|
|
|
2436
3482
|
};
|
|
2437
3483
|
}
|
|
2438
3484
|
async function runCompoundDelete(input) {
|
|
2439
|
-
const projectDir =
|
|
2440
|
-
if (!
|
|
3485
|
+
const projectDir = join22(input.vault, "projects", input.project);
|
|
3486
|
+
if (!existsSync5(projectDir)) {
|
|
2441
3487
|
return {
|
|
2442
3488
|
exitCode: ExitCode.PROJECT_NOT_FOUND,
|
|
2443
3489
|
result: err("PROJECT_NOT_FOUND", { slug: input.project, path: projectDir })
|
|
2444
3490
|
};
|
|
2445
3491
|
}
|
|
2446
3492
|
const entryName = input.entry.replace(/\.md$/, "");
|
|
2447
|
-
const compoundPath =
|
|
2448
|
-
if (!
|
|
3493
|
+
const compoundPath = join22(projectDir, "compound", `${entryName}.md`);
|
|
3494
|
+
if (!existsSync5(compoundPath)) {
|
|
2449
3495
|
return {
|
|
2450
3496
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
2451
3497
|
result: err("FILE_NOT_FOUND", { path: compoundPath })
|
|
@@ -2478,8 +3524,8 @@ knowledge.md regenerated`
|
|
|
2478
3524
|
};
|
|
2479
3525
|
}
|
|
2480
3526
|
async function runCompoundList(input) {
|
|
2481
|
-
const compoundDir =
|
|
2482
|
-
if (!
|
|
3527
|
+
const compoundDir = join22(input.vault, "projects", input.project, "compound");
|
|
3528
|
+
if (!existsSync5(compoundDir)) {
|
|
2483
3529
|
return {
|
|
2484
3530
|
exitCode: ExitCode.OK,
|
|
2485
3531
|
result: ok({
|
|
@@ -2493,7 +3539,7 @@ no compound directory found`
|
|
|
2493
3539
|
}
|
|
2494
3540
|
let dirents;
|
|
2495
3541
|
try {
|
|
2496
|
-
dirents = await
|
|
3542
|
+
dirents = await readdir6(compoundDir, { withFileTypes: true });
|
|
2497
3543
|
} catch {
|
|
2498
3544
|
return {
|
|
2499
3545
|
exitCode: ExitCode.OK,
|
|
@@ -2509,10 +3555,10 @@ could not read compound directory`
|
|
|
2509
3555
|
const entries = [];
|
|
2510
3556
|
for (const dirent of dirents) {
|
|
2511
3557
|
if (!dirent.isFile() || !dirent.name.endsWith(".md")) continue;
|
|
2512
|
-
const filePath =
|
|
3558
|
+
const filePath = join22(compoundDir, dirent.name);
|
|
2513
3559
|
let text;
|
|
2514
3560
|
try {
|
|
2515
|
-
text = await
|
|
3561
|
+
text = await readFile10(filePath, "utf8");
|
|
2516
3562
|
} catch {
|
|
2517
3563
|
continue;
|
|
2518
3564
|
}
|
|
@@ -2541,8 +3587,8 @@ no compound entries found`;
|
|
|
2541
3587
|
}
|
|
2542
3588
|
|
|
2543
3589
|
// src/commands/session-brief.ts
|
|
2544
|
-
import { mkdir as
|
|
2545
|
-
import { join as
|
|
3590
|
+
import { mkdir as mkdir9, readFile as readFile11, writeFile as writeFile8 } from "fs/promises";
|
|
3591
|
+
import { join as join23, relative, sep } from "path";
|
|
2546
3592
|
var MAX_WORDS = 900;
|
|
2547
3593
|
async function runSessionBrief(input) {
|
|
2548
3594
|
const scan = await scanVault(input.vault);
|
|
@@ -2642,7 +3688,7 @@ async function resolveProject(input) {
|
|
|
2642
3688
|
const envProject = input.env?.SKILLWIKI_PROJECT;
|
|
2643
3689
|
if (envProject) return envProject;
|
|
2644
3690
|
const cwd = input.cwd ?? process.cwd();
|
|
2645
|
-
const projectDotenv = await readProjectSlug(
|
|
3691
|
+
const projectDotenv = await readProjectSlug(join23(cwd, ".skillwiki", ".env"));
|
|
2646
3692
|
if (projectDotenv) return projectDotenv;
|
|
2647
3693
|
const inferred = inferProjectFromPath(input.vault, cwd);
|
|
2648
3694
|
if (inferred) return inferred;
|
|
@@ -2651,7 +3697,7 @@ async function resolveProject(input) {
|
|
|
2651
3697
|
async function readProjectSlug(file) {
|
|
2652
3698
|
let text;
|
|
2653
3699
|
try {
|
|
2654
|
-
text = await
|
|
3700
|
+
text = await readFile11(file, "utf8");
|
|
2655
3701
|
} catch {
|
|
2656
3702
|
return void 0;
|
|
2657
3703
|
}
|
|
@@ -2728,7 +3774,7 @@ async function loadTrendDigests(typedPages) {
|
|
|
2728
3774
|
return out;
|
|
2729
3775
|
}
|
|
2730
3776
|
async function loadSessionPins(vault, project) {
|
|
2731
|
-
const text = await readIfExists(
|
|
3777
|
+
const text = await readIfExists(join23(vault, "meta", "session-pins.md"));
|
|
2732
3778
|
if (!text) return [];
|
|
2733
3779
|
const fm = extractFrontmatter(text);
|
|
2734
3780
|
if (!fm.ok) return [];
|
|
@@ -2845,7 +3891,7 @@ function satelliteHealthWarnings(warning) {
|
|
|
2845
3891
|
return warning ? [warning] : [];
|
|
2846
3892
|
}
|
|
2847
3893
|
async function loadHealthWarnings(vault) {
|
|
2848
|
-
const text = await readIfExists(
|
|
3894
|
+
const text = await readIfExists(join23(vault, ".skillwiki", "health.json"));
|
|
2849
3895
|
if (!text) return [];
|
|
2850
3896
|
try {
|
|
2851
3897
|
const parsed = JSON.parse(text);
|
|
@@ -2858,7 +3904,7 @@ async function loadHealthWarnings(vault) {
|
|
|
2858
3904
|
}
|
|
2859
3905
|
}
|
|
2860
3906
|
async function loadMemoryTopics(vault, project) {
|
|
2861
|
-
const text = await readIfExists(
|
|
3907
|
+
const text = await readIfExists(join23(vault, ".skillwiki", "memory", project, "topics.json"));
|
|
2862
3908
|
if (!text) return [];
|
|
2863
3909
|
try {
|
|
2864
3910
|
const parsed = JSON.parse(text);
|
|
@@ -2875,11 +3921,11 @@ async function loadMemoryTopics(vault, project) {
|
|
|
2875
3921
|
}
|
|
2876
3922
|
}
|
|
2877
3923
|
async function writeBriefArtifacts(vault, input) {
|
|
2878
|
-
const metaPath =
|
|
2879
|
-
const cacheMdPath =
|
|
2880
|
-
const cacheJsonPath =
|
|
2881
|
-
await
|
|
2882
|
-
await
|
|
3924
|
+
const metaPath = join23(vault, "meta", "latest-session-brief.md");
|
|
3925
|
+
const cacheMdPath = join23(vault, ".skillwiki", "session-brief.md");
|
|
3926
|
+
const cacheJsonPath = join23(vault, ".skillwiki", "session-brief.json");
|
|
3927
|
+
await mkdir9(join23(vault, "meta"), { recursive: true });
|
|
3928
|
+
await mkdir9(join23(vault, ".skillwiki"), { recursive: true });
|
|
2883
3929
|
const committed = renderCommittedBrief(input);
|
|
2884
3930
|
const previousComparable = comparableBrief(await readIfExists(metaPath));
|
|
2885
3931
|
const nextComparable = comparableBrief(committed);
|
|
@@ -2894,8 +3940,17 @@ async function writeBriefArtifacts(vault, input) {
|
|
|
2894
3940
|
memory_topics: input.memoryTopics
|
|
2895
3941
|
}, null, 2)}
|
|
2896
3942
|
`, "utf8");
|
|
2897
|
-
const indexUpdated = await
|
|
2898
|
-
|
|
3943
|
+
const indexUpdated = await rebuildRootIndexProjection(vault);
|
|
3944
|
+
let logUpdated = false;
|
|
3945
|
+
if (materialChange) {
|
|
3946
|
+
const log = await runLogAppend({
|
|
3947
|
+
vault,
|
|
3948
|
+
content: `## [${input.today}] session-brief | refreshed: meta/latest-session-brief.md`,
|
|
3949
|
+
strictLock: true,
|
|
3950
|
+
recordLastOp: false
|
|
3951
|
+
});
|
|
3952
|
+
logUpdated = log.result.ok && log.result.data.appended;
|
|
3953
|
+
}
|
|
2899
3954
|
if (materialChange) {
|
|
2900
3955
|
appendLastOp(vault, {
|
|
2901
3956
|
operation: "session-brief",
|
|
@@ -2939,37 +3994,18 @@ function renderCommittedBrief(input) {
|
|
|
2939
3994
|
""
|
|
2940
3995
|
].filter((line) => line !== "").join("\n");
|
|
2941
3996
|
}
|
|
2942
|
-
async function
|
|
2943
|
-
const
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
if (
|
|
2947
|
-
|
|
2948
|
-
const
|
|
2949
|
-
|
|
2950
|
-
if (sectionIdx === -1) {
|
|
2951
|
-
while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
|
|
2952
|
-
lines.push("", "## Meta", entry);
|
|
2953
|
-
} else {
|
|
2954
|
-
let insertAt = sectionIdx + 1;
|
|
2955
|
-
while (insertAt < lines.length && !lines[insertAt].startsWith("## ")) insertAt++;
|
|
2956
|
-
lines.splice(insertAt, 0, entry);
|
|
2957
|
-
}
|
|
2958
|
-
await writeFile8(indexPath, lines.join("\n"), "utf8");
|
|
2959
|
-
return true;
|
|
2960
|
-
}
|
|
2961
|
-
async function appendMaterialLog(vault, today) {
|
|
2962
|
-
const logPath = join15(vault, "log.md");
|
|
2963
|
-
const text = await readIfExists(logPath);
|
|
2964
|
-
if (!text) return false;
|
|
2965
|
-
const entry = `
|
|
2966
|
-
## [${today}] session-brief | refreshed: meta/latest-session-brief.md`;
|
|
2967
|
-
await writeFile8(logPath, text.trimEnd() + entry + "\n", "utf8");
|
|
2968
|
-
return true;
|
|
3997
|
+
async function rebuildRootIndexProjection(vault) {
|
|
3998
|
+
const before = await readIfExists(join23(vault, "index.md"));
|
|
3999
|
+
if (!before) return false;
|
|
4000
|
+
const projection = await renderRootIndex({ vault, currentText: before });
|
|
4001
|
+
if (!projection.ok) return false;
|
|
4002
|
+
if (projection.data.text === before) return false;
|
|
4003
|
+
const written = await writeRootIndexProjection(vault, projection.data);
|
|
4004
|
+
return written.ok && written.data.changed;
|
|
2969
4005
|
}
|
|
2970
4006
|
async function readIfExists(path) {
|
|
2971
4007
|
try {
|
|
2972
|
-
return await
|
|
4008
|
+
return await readFile11(path, "utf8");
|
|
2973
4009
|
} catch {
|
|
2974
4010
|
return "";
|
|
2975
4011
|
}
|
|
@@ -3011,16 +4047,18 @@ function dateFromPath(path) {
|
|
|
3011
4047
|
}
|
|
3012
4048
|
|
|
3013
4049
|
// src/commands/ingest.ts
|
|
3014
|
-
import { readFile as
|
|
3015
|
-
import { join as
|
|
4050
|
+
import { readFile as readFile13, open as open2, unlink as unlink4, mkdir as mkdir10 } from "fs/promises";
|
|
4051
|
+
import { join as join25 } from "path";
|
|
3016
4052
|
import { createHash as createHash4 } from "crypto";
|
|
3017
4053
|
|
|
3018
4054
|
// src/commands/page-publish.ts
|
|
3019
|
-
import { createHash as createHash3 } from "crypto";
|
|
3020
4055
|
import { realpathSync } from "fs";
|
|
3021
|
-
import { readFile as
|
|
3022
|
-
import { join as
|
|
3023
|
-
var DEFAULT_DEPS = {
|
|
4056
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
4057
|
+
import { join as join24, resolve as resolve3 } from "path";
|
|
4058
|
+
var DEFAULT_DEPS = {
|
|
4059
|
+
afterStage: async () => void 0,
|
|
4060
|
+
preflight: (input) => runManagedWritePreflight(input)
|
|
4061
|
+
};
|
|
3024
4062
|
function errorExitCode(error) {
|
|
3025
4063
|
switch (error) {
|
|
3026
4064
|
case "FILE_NOT_FOUND":
|
|
@@ -3036,6 +4074,9 @@ function errorExitCode(error) {
|
|
|
3036
4074
|
return ExitCode.SENSITIVE_CONTENT_DETECTED;
|
|
3037
4075
|
case "SYNC_LOCK_HELD":
|
|
3038
4076
|
return ExitCode.SYNC_LOCK_HELD;
|
|
4077
|
+
case "PREFLIGHT_FAILED":
|
|
4078
|
+
return ExitCode.PREFLIGHT_FAILED;
|
|
4079
|
+
case "EVENT_IDENTITY_COLLISION":
|
|
3039
4080
|
case "WRITE_FAILED":
|
|
3040
4081
|
return ExitCode.WRITE_FAILED;
|
|
3041
4082
|
default:
|
|
@@ -3043,7 +4084,11 @@ function errorExitCode(error) {
|
|
|
3043
4084
|
}
|
|
3044
4085
|
}
|
|
3045
4086
|
function publicationId(target, content, logNote = "") {
|
|
3046
|
-
return
|
|
4087
|
+
return operationId("skillwiki-page-publish-v1", [target, content, logNote]);
|
|
4088
|
+
}
|
|
4089
|
+
function resolveRootAggregateMode(env = process.env) {
|
|
4090
|
+
if (env.SKILLWIKI_ROOT_AGGREGATE_MODE === "source-only") return "source-only";
|
|
4091
|
+
return "dual";
|
|
3047
4092
|
}
|
|
3048
4093
|
function prepareFrozenPublication(input, source) {
|
|
3049
4094
|
const target = assertTargetInsideVault(input.vault, input.target);
|
|
@@ -3079,7 +4124,7 @@ function preparePagePublicationFromContent(input) {
|
|
|
3079
4124
|
async function preparePagePublication(input) {
|
|
3080
4125
|
let content;
|
|
3081
4126
|
try {
|
|
3082
|
-
content = await
|
|
4127
|
+
content = await readFile12(input.draftPath, "utf8");
|
|
3083
4128
|
} catch (error) {
|
|
3084
4129
|
return err("FILE_NOT_FOUND", { path: input.draftPath, message: String(error) });
|
|
3085
4130
|
}
|
|
@@ -3150,10 +4195,10 @@ async function runLockedPrimaryStages(input, vault, deps) {
|
|
|
3150
4195
|
ExitCode.VAULT_PATH_INVALID
|
|
3151
4196
|
);
|
|
3152
4197
|
}
|
|
3153
|
-
const schemaPath =
|
|
4198
|
+
const schemaPath = join24(vault, "SCHEMA.md");
|
|
3154
4199
|
let schemaText;
|
|
3155
4200
|
try {
|
|
3156
|
-
schemaText = await
|
|
4201
|
+
schemaText = await readFile12(schemaPath, "utf8");
|
|
3157
4202
|
} catch (error) {
|
|
3158
4203
|
return lockedFailure("schema", state, err("WRITE_FAILED", { message: String(error) }));
|
|
3159
4204
|
}
|
|
@@ -3183,8 +4228,8 @@ async function runLockedPrimaryStages(input, vault, deps) {
|
|
|
3183
4228
|
let visibleSchema;
|
|
3184
4229
|
try {
|
|
3185
4230
|
[visible, visibleSchema] = await Promise.all([
|
|
3186
|
-
|
|
3187
|
-
|
|
4231
|
+
readFile12(input.targetPath, "utf8"),
|
|
4232
|
+
readFile12(schemaPath, "utf8")
|
|
3188
4233
|
]);
|
|
3189
4234
|
} catch (error) {
|
|
3190
4235
|
return lockedFailure("verify", state, err("WRITE_FAILED", { message: String(error) }));
|
|
@@ -3237,7 +4282,7 @@ function phaseFailure(stage, input, published, cause, context = {}, exitCode = E
|
|
|
3237
4282
|
})
|
|
3238
4283
|
};
|
|
3239
4284
|
}
|
|
3240
|
-
function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, logAppended, filesChanged, dryRun = false) {
|
|
4285
|
+
function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, logAppended, filesChanged, dryRun = false, receipt = null) {
|
|
3241
4286
|
return {
|
|
3242
4287
|
exitCode: ExitCode.OK,
|
|
3243
4288
|
result: ok({
|
|
@@ -3251,6 +4296,9 @@ function successReceipt(input, taxonomyAdded, pageChanged, indexUpdated, logAppe
|
|
|
3251
4296
|
operation_id: input.operationId,
|
|
3252
4297
|
dry_run: dryRun,
|
|
3253
4298
|
files_changed: filesChanged,
|
|
4299
|
+
base_oid: receipt?.base_oid ?? null,
|
|
4300
|
+
write_mode: receipt?.mode ?? null,
|
|
4301
|
+
...receipt?.host_id ? { host_id: receipt.host_id } : {},
|
|
3254
4302
|
humanHint: dryRun ? `dry run: would publish ${input.page.target} (${input.operationId.slice(0, 12)})` : `published ${input.page.target} (${input.operationId.slice(0, 12)})`
|
|
3255
4303
|
})
|
|
3256
4304
|
};
|
|
@@ -3266,17 +4314,17 @@ function renderPublicationLog(input, added) {
|
|
|
3266
4314
|
}
|
|
3267
4315
|
async function readPageChanged(targetPath, content) {
|
|
3268
4316
|
try {
|
|
3269
|
-
return ok(await
|
|
4317
|
+
return ok(await readFile12(targetPath, "utf8") !== content);
|
|
3270
4318
|
} catch (error) {
|
|
3271
4319
|
if (error.code === "ENOENT") return ok(true);
|
|
3272
4320
|
return err("WRITE_FAILED", { path: targetPath, message: String(error) });
|
|
3273
4321
|
}
|
|
3274
4322
|
}
|
|
3275
4323
|
async function previewPreparedPagePublication(input, vault) {
|
|
3276
|
-
const schemaPath =
|
|
4324
|
+
const schemaPath = join24(vault, "SCHEMA.md");
|
|
3277
4325
|
let schemaText;
|
|
3278
4326
|
try {
|
|
3279
|
-
schemaText = await
|
|
4327
|
+
schemaText = await readFile12(schemaPath, "utf8");
|
|
3280
4328
|
} catch (error) {
|
|
3281
4329
|
const result = err("FILE_NOT_FOUND", { path: schemaPath, message: String(error) });
|
|
3282
4330
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result };
|
|
@@ -3288,10 +4336,10 @@ async function previewPreparedPagePublication(input, vault) {
|
|
|
3288
4336
|
if (!reconciled.ok) return { exitCode: errorExitCode(reconciled.error), result: reconciled };
|
|
3289
4337
|
const pageChanged = await readPageChanged(input.targetPath, input.page.content);
|
|
3290
4338
|
if (!pageChanged.ok) return { exitCode: errorExitCode(pageChanged.error), result: pageChanged };
|
|
3291
|
-
const indexPath =
|
|
4339
|
+
const indexPath = join24(vault, "index.md");
|
|
3292
4340
|
let indexText;
|
|
3293
4341
|
try {
|
|
3294
|
-
indexText = await
|
|
4342
|
+
indexText = await readFile12(indexPath, "utf8");
|
|
3295
4343
|
} catch (error) {
|
|
3296
4344
|
const result = err("FILE_NOT_FOUND", { path: indexPath, message: String(error) });
|
|
3297
4345
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result };
|
|
@@ -3302,10 +4350,10 @@ async function previewPreparedPagePublication(input, vault) {
|
|
|
3302
4350
|
type: input.page.type
|
|
3303
4351
|
});
|
|
3304
4352
|
if (!index.ok) return { exitCode: errorExitCode(index.error), result: index };
|
|
3305
|
-
const logPath =
|
|
4353
|
+
const logPath = join24(vault, "log.md");
|
|
3306
4354
|
let logText;
|
|
3307
4355
|
try {
|
|
3308
|
-
logText = await
|
|
4356
|
+
logText = await readFile12(logPath, "utf8");
|
|
3309
4357
|
} catch (error) {
|
|
3310
4358
|
const result = err("FILE_NOT_FOUND", { path: logPath, message: String(error) });
|
|
3311
4359
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result };
|
|
@@ -3328,93 +4376,159 @@ async function previewPreparedPagePublication(input, vault) {
|
|
|
3328
4376
|
);
|
|
3329
4377
|
}
|
|
3330
4378
|
async function publishPreparedPage(input, vault, deps = DEFAULT_DEPS) {
|
|
3331
|
-
|
|
4379
|
+
const managed = acquireManagedWriteLock(vault, `page publish ${input.page.target}`);
|
|
4380
|
+
if (!managed.ok) return { exitCode: errorExitCode(managed.error), result: managed };
|
|
3332
4381
|
try {
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
4382
|
+
const preflight = await deps.preflight({
|
|
4383
|
+
vault,
|
|
4384
|
+
command: `page publish ${input.page.target}`,
|
|
4385
|
+
lockToken: managed.data.ownerToken
|
|
4386
|
+
});
|
|
4387
|
+
if (!preflight.result.ok) {
|
|
4388
|
+
return { exitCode: preflight.exitCode, result: preflight.result };
|
|
4389
|
+
}
|
|
4390
|
+
const writeReceipt = preflight.result.data;
|
|
4391
|
+
const aggregateMode = resolveRootAggregateMode();
|
|
4392
|
+
if (writeReceipt.base_oid) {
|
|
4393
|
+
const head = git(vault, ["rev-parse", "HEAD"]);
|
|
4394
|
+
if (head !== writeReceipt.base_oid) {
|
|
4395
|
+
return {
|
|
4396
|
+
exitCode: ExitCode.PREFLIGHT_FAILED,
|
|
4397
|
+
result: err("PREFLIGHT_FAILED", {
|
|
4398
|
+
reason: "base-oid-drift",
|
|
4399
|
+
expected: writeReceipt.base_oid,
|
|
4400
|
+
actual: head
|
|
4401
|
+
})
|
|
4402
|
+
};
|
|
4403
|
+
}
|
|
4404
|
+
}
|
|
4405
|
+
let lock;
|
|
4406
|
+
try {
|
|
4407
|
+
lock = acquireOwnedSyncLock(vault, {
|
|
4408
|
+
summary: `page publish ${input.page.target}`,
|
|
4409
|
+
ttlMinutes: 1
|
|
4410
|
+
});
|
|
4411
|
+
} catch (error) {
|
|
4412
|
+
return {
|
|
4413
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
4414
|
+
result: err("WRITE_FAILED", { stage: "lock", message: String(error) })
|
|
4415
|
+
};
|
|
4416
|
+
}
|
|
4417
|
+
if (!lock.ok) return { exitCode: errorExitCode(lock.error), result: lock };
|
|
4418
|
+
let primary;
|
|
4419
|
+
let released;
|
|
4420
|
+
try {
|
|
4421
|
+
primary = await runLockedPrimaryStages(input, vault, deps);
|
|
4422
|
+
} catch (error) {
|
|
4423
|
+
primary = lockedFailure(
|
|
4424
|
+
"schema",
|
|
4425
|
+
emptyLockedState(),
|
|
4426
|
+
err("WRITE_FAILED", { message: `unexpected primary-stage failure: ${String(error)}` })
|
|
4427
|
+
);
|
|
4428
|
+
} finally {
|
|
4429
|
+
released = releaseOwnedSyncLock(lock.data);
|
|
4430
|
+
}
|
|
4431
|
+
const primaryState = primary?.ok ? primary.data : primary?.state;
|
|
4432
|
+
if (released === void 0 || !released.ok || !released.data.released) {
|
|
4433
|
+
return phaseFailure(
|
|
4434
|
+
"unlock",
|
|
4435
|
+
input,
|
|
4436
|
+
primaryState?.published ?? false,
|
|
4437
|
+
released && !released.ok ? released : err("WRITE_FAILED", { message: "lock release did not run" }),
|
|
4438
|
+
{
|
|
4439
|
+
primary_stage: primary && !primary.ok ? primary.stage : "complete",
|
|
4440
|
+
primary_error: primary && !primary.ok ? primary.cause.error : void 0
|
|
4441
|
+
}
|
|
4442
|
+
);
|
|
4443
|
+
}
|
|
4444
|
+
const unlockHook = await observeStage(deps, "unlock");
|
|
4445
|
+
if (unlockHook) return phaseFailure("unlock", input, primaryState?.published ?? false, unlockHook);
|
|
4446
|
+
if (primary === void 0) {
|
|
4447
|
+
return phaseFailure(
|
|
4448
|
+
"schema",
|
|
4449
|
+
input,
|
|
4450
|
+
false,
|
|
4451
|
+
err("WRITE_FAILED", { message: "locked publication produced no result" })
|
|
4452
|
+
);
|
|
4453
|
+
}
|
|
4454
|
+
if (!primary.ok) {
|
|
4455
|
+
return phaseFailure(
|
|
4456
|
+
primary.stage,
|
|
4457
|
+
input,
|
|
4458
|
+
primary.state.published,
|
|
4459
|
+
primary.cause,
|
|
4460
|
+
void 0,
|
|
4461
|
+
primary.exitCode
|
|
4462
|
+
);
|
|
4463
|
+
}
|
|
4464
|
+
const state = primary.data;
|
|
4465
|
+
const event = await writeLogEvent(vault, {
|
|
4466
|
+
schema: "skillwiki-log-event/v1",
|
|
4467
|
+
operation_id: input.operationId,
|
|
4468
|
+
occurred_at: `${input.date}T00:00:00.000Z`,
|
|
4469
|
+
host_id: writeReceipt.host_id ?? "standalone",
|
|
4470
|
+
actor: "skillwiki-cli",
|
|
4471
|
+
kind: "page-publish",
|
|
4472
|
+
target: input.page.target,
|
|
4473
|
+
note: input.logNote ?? `Published ${input.page.title}`,
|
|
4474
|
+
metadata: {
|
|
4475
|
+
page_type: input.page.type,
|
|
4476
|
+
// Stable for replay: do not embed schema-delta taxonomy_added (varies after first write).
|
|
4477
|
+
tags: [...input.page.tags].sort(),
|
|
4478
|
+
base_oid: writeReceipt.base_oid
|
|
4479
|
+
}
|
|
3336
4480
|
});
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
4481
|
+
if (!event.ok) {
|
|
4482
|
+
return phaseFailure(
|
|
4483
|
+
"event",
|
|
4484
|
+
input,
|
|
4485
|
+
true,
|
|
4486
|
+
event,
|
|
4487
|
+
void 0,
|
|
4488
|
+
event.error === "EVENT_IDENTITY_COLLISION" ? ExitCode.WRITE_FAILED : errorExitCode(event.error)
|
|
4489
|
+
);
|
|
4490
|
+
}
|
|
4491
|
+
if (event.data.created) state.changed.add(event.data.path);
|
|
4492
|
+
const eventHook = await observeStage(deps, "event");
|
|
4493
|
+
if (eventHook) return phaseFailure("event", input, true, eventHook);
|
|
4494
|
+
let logAppended = false;
|
|
4495
|
+
if (aggregateMode === "dual" || writeReceipt.mode !== "immutable-record") {
|
|
4496
|
+
if (aggregateMode === "dual") {
|
|
4497
|
+
const log = await runLogAppend({
|
|
4498
|
+
vault,
|
|
4499
|
+
content: renderPublicationLog(input, state.taxonomyAdded),
|
|
4500
|
+
operationId: input.operationId,
|
|
4501
|
+
strictLock: true,
|
|
4502
|
+
recordLastOp: false
|
|
4503
|
+
});
|
|
4504
|
+
if (!log.result.ok) return phaseFailure("log", input, true, log.result);
|
|
4505
|
+
if (log.exitCode !== ExitCode.OK) {
|
|
4506
|
+
return phaseFailure(
|
|
4507
|
+
"log",
|
|
4508
|
+
input,
|
|
4509
|
+
true,
|
|
4510
|
+
err("WRITE_FAILED", { message: "log append returned inconsistent success state" })
|
|
4511
|
+
);
|
|
4512
|
+
}
|
|
4513
|
+
logAppended = log.result.data.appended;
|
|
4514
|
+
if (logAppended) state.changed.add("log.md");
|
|
4515
|
+
const logHook = await observeStage(deps, "log");
|
|
4516
|
+
if (logHook) return phaseFailure("log", input, true, logHook);
|
|
3367
4517
|
}
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
const unlockHook = await observeStage(deps, "unlock");
|
|
3371
|
-
if (unlockHook) return phaseFailure("unlock", input, primaryState?.published ?? false, unlockHook);
|
|
3372
|
-
if (primary === void 0) {
|
|
3373
|
-
return phaseFailure(
|
|
3374
|
-
"schema",
|
|
4518
|
+
}
|
|
4519
|
+
return successReceipt(
|
|
3375
4520
|
input,
|
|
4521
|
+
state.taxonomyAdded,
|
|
4522
|
+
state.pageChanged,
|
|
4523
|
+
state.indexUpdated,
|
|
4524
|
+
logAppended,
|
|
4525
|
+
[...state.changed],
|
|
3376
4526
|
false,
|
|
3377
|
-
|
|
3378
|
-
);
|
|
3379
|
-
}
|
|
3380
|
-
if (!primary.ok) {
|
|
3381
|
-
return phaseFailure(
|
|
3382
|
-
primary.stage,
|
|
3383
|
-
input,
|
|
3384
|
-
primary.state.published,
|
|
3385
|
-
primary.cause,
|
|
3386
|
-
void 0,
|
|
3387
|
-
primary.exitCode
|
|
3388
|
-
);
|
|
3389
|
-
}
|
|
3390
|
-
const state = primary.data;
|
|
3391
|
-
const log = await runLogAppend({
|
|
3392
|
-
vault,
|
|
3393
|
-
content: renderPublicationLog(input, state.taxonomyAdded),
|
|
3394
|
-
operationId: input.operationId,
|
|
3395
|
-
strictLock: true,
|
|
3396
|
-
recordLastOp: false
|
|
3397
|
-
});
|
|
3398
|
-
if (!log.result.ok) return phaseFailure("log", input, true, log.result);
|
|
3399
|
-
if (log.exitCode !== ExitCode.OK) {
|
|
3400
|
-
return phaseFailure(
|
|
3401
|
-
"log",
|
|
3402
|
-
input,
|
|
3403
|
-
true,
|
|
3404
|
-
err("WRITE_FAILED", { message: "log append returned inconsistent success state" })
|
|
4527
|
+
writeReceipt
|
|
3405
4528
|
);
|
|
4529
|
+
} finally {
|
|
4530
|
+
releaseManagedWriteLock(managed.data);
|
|
3406
4531
|
}
|
|
3407
|
-
if (log.result.data.appended) state.changed.add("log.md");
|
|
3408
|
-
const logHook = await observeStage(deps, "log");
|
|
3409
|
-
if (logHook) return phaseFailure("log", input, true, logHook);
|
|
3410
|
-
return successReceipt(
|
|
3411
|
-
input,
|
|
3412
|
-
state.taxonomyAdded,
|
|
3413
|
-
state.pageChanged,
|
|
3414
|
-
state.indexUpdated,
|
|
3415
|
-
log.result.data.appended,
|
|
3416
|
-
[...state.changed]
|
|
3417
|
-
);
|
|
3418
4532
|
}
|
|
3419
4533
|
async function runPagePublish(input, deps = DEFAULT_DEPS) {
|
|
3420
4534
|
const prepared = await preparePagePublication(input);
|
|
@@ -3501,7 +4615,7 @@ function buildTypedContent(title, ingested, type, tags, rawRelPath, provenance)
|
|
|
3501
4615
|
}
|
|
3502
4616
|
async function resolveRawCapture(input) {
|
|
3503
4617
|
try {
|
|
3504
|
-
const existing = await
|
|
4618
|
+
const existing = await readFile13(input.path, "utf8");
|
|
3505
4619
|
const frontmatter = extractFrontmatter(existing);
|
|
3506
4620
|
if (!frontmatter.ok) {
|
|
3507
4621
|
return err("INGEST_VALIDATION_FAILED", {
|
|
@@ -3559,7 +4673,7 @@ async function acquireRawCaptureLock(path) {
|
|
|
3559
4673
|
const lockPath = `${path}.ingest.lock`;
|
|
3560
4674
|
for (let attempt = 0; attempt < 200; attempt++) {
|
|
3561
4675
|
try {
|
|
3562
|
-
const handle = await
|
|
4676
|
+
const handle = await open2(lockPath, "wx");
|
|
3563
4677
|
await handle.close();
|
|
3564
4678
|
return ok(lockPath);
|
|
3565
4679
|
} catch (error) {
|
|
@@ -3640,7 +4754,7 @@ async function runIngest(input) {
|
|
|
3640
4754
|
sourceContent = fetchResult.data.body;
|
|
3641
4755
|
} else {
|
|
3642
4756
|
try {
|
|
3643
|
-
sourceContent = await
|
|
4757
|
+
sourceContent = await readFile13(input.source, "utf8");
|
|
3644
4758
|
} catch {
|
|
3645
4759
|
return {
|
|
3646
4760
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
@@ -3665,7 +4779,7 @@ async function runIngest(input) {
|
|
|
3665
4779
|
const rawRelPath = `raw/articles/${slug}.md`;
|
|
3666
4780
|
const typedDir = TYPE_DIR[input.type] ?? `${input.type}s`;
|
|
3667
4781
|
const typedRelPath = `${typedDir}/${slug}.md`;
|
|
3668
|
-
const rawAbsPath =
|
|
4782
|
+
const rawAbsPath = join25(input.vault, rawRelPath);
|
|
3669
4783
|
const identity = assessSourceIdentity({
|
|
3670
4784
|
rawPath: rawRelPath,
|
|
3671
4785
|
sourceUrl: sourceUrl ?? void 0,
|
|
@@ -3709,11 +4823,11 @@ async function runIngest(input) {
|
|
|
3709
4823
|
);
|
|
3710
4824
|
if (!input.dryRun) {
|
|
3711
4825
|
try {
|
|
3712
|
-
await
|
|
4826
|
+
await mkdir10(join25(input.vault, typedDir), { recursive: true });
|
|
3713
4827
|
} catch (error) {
|
|
3714
4828
|
return {
|
|
3715
4829
|
exitCode: ExitCode.WRITE_FAILED,
|
|
3716
|
-
result: err("WRITE_FAILED", { path:
|
|
4830
|
+
result: err("WRITE_FAILED", { path: join25(input.vault, typedDir), message: String(error) })
|
|
3717
4831
|
};
|
|
3718
4832
|
}
|
|
3719
4833
|
}
|
|
@@ -3760,11 +4874,11 @@ async function runIngest(input) {
|
|
|
3760
4874
|
};
|
|
3761
4875
|
}
|
|
3762
4876
|
try {
|
|
3763
|
-
await
|
|
4877
|
+
await mkdir10(join25(input.vault, "raw", "articles"), { recursive: true });
|
|
3764
4878
|
} catch (error) {
|
|
3765
4879
|
return {
|
|
3766
4880
|
exitCode: ExitCode.WRITE_FAILED,
|
|
3767
|
-
result: err("WRITE_FAILED", { path:
|
|
4881
|
+
result: err("WRITE_FAILED", { path: join25(input.vault, "raw", "articles"), message: String(error) })
|
|
3768
4882
|
};
|
|
3769
4883
|
}
|
|
3770
4884
|
const rawWrite = await writeResolvedRaw({
|
|
@@ -3992,8 +5106,8 @@ ${body}`;
|
|
|
3992
5106
|
}
|
|
3993
5107
|
|
|
3994
5108
|
// src/commands/tag-reconcile.ts
|
|
3995
|
-
import { readFile as
|
|
3996
|
-
import { join as
|
|
5109
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
5110
|
+
import { join as join26, posix } from "path";
|
|
3997
5111
|
var TYPED_TARGET_RE = /^(entities|concepts|comparisons|queries|meta)\/[a-z0-9][a-z0-9./_-]*\.md$/;
|
|
3998
5112
|
function errorExitCode2(error) {
|
|
3999
5113
|
switch (error) {
|
|
@@ -4036,7 +5150,7 @@ function asTagArray(frontmatter, path) {
|
|
|
4036
5150
|
async function readTagsFromFile(path) {
|
|
4037
5151
|
let text;
|
|
4038
5152
|
try {
|
|
4039
|
-
text = await
|
|
5153
|
+
text = await readFile14(path, "utf8");
|
|
4040
5154
|
} catch (error) {
|
|
4041
5155
|
if (error.code === "ENOENT") {
|
|
4042
5156
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path }) };
|
|
@@ -4057,7 +5171,7 @@ async function resolveRequestedTags(input, page) {
|
|
|
4057
5171
|
result: err("INVALID_FRONTMATTER", { message: "explicit tags must be an array of strings" })
|
|
4058
5172
|
};
|
|
4059
5173
|
}
|
|
4060
|
-
const source = input.from ?? (explicit.length === 0 ?
|
|
5174
|
+
const source = input.from ?? (explicit.length === 0 ? join26(input.vault, page) : void 0);
|
|
4061
5175
|
if (!source) return { exitCode: ExitCode.OK, result: ok({ tags: [...new Set(explicit)].sort() }) };
|
|
4062
5176
|
const sourced = await readTagsFromFile(source);
|
|
4063
5177
|
if (!sourced.result.ok) return { exitCode: sourced.exitCode, result: sourced.result };
|
|
@@ -4068,7 +5182,7 @@ async function resolveRequestedTags(input, page) {
|
|
|
4068
5182
|
}
|
|
4069
5183
|
async function readSchema(schemaPath) {
|
|
4070
5184
|
try {
|
|
4071
|
-
return { exitCode: ExitCode.OK, result: ok(await
|
|
5185
|
+
return { exitCode: ExitCode.OK, result: ok(await readFile14(schemaPath, "utf8")) };
|
|
4072
5186
|
} catch (error) {
|
|
4073
5187
|
if (error.code === "ENOENT") {
|
|
4074
5188
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: schemaPath }) };
|
|
@@ -4096,7 +5210,7 @@ function previewResult(page, tags, reconciled, dryRun, filesChanged) {
|
|
|
4096
5210
|
};
|
|
4097
5211
|
}
|
|
4098
5212
|
async function reconcileTagsWhileLocked(input, page, tags, comment) {
|
|
4099
|
-
const schemaPath =
|
|
5213
|
+
const schemaPath = join26(input.vault, "SCHEMA.md");
|
|
4100
5214
|
const current = await readSchema(schemaPath);
|
|
4101
5215
|
if (!current.result.ok) return { exitCode: current.exitCode, result: current.result };
|
|
4102
5216
|
const next = reconcileTaxonomyDocument(current.result.data, { tags, comment });
|
|
@@ -4107,7 +5221,7 @@ async function reconcileTagsWhileLocked(input, page, tags, comment) {
|
|
|
4107
5221
|
}
|
|
4108
5222
|
let verifiedText;
|
|
4109
5223
|
try {
|
|
4110
|
-
verifiedText = await
|
|
5224
|
+
verifiedText = await readFile14(schemaPath, "utf8");
|
|
4111
5225
|
} catch (error) {
|
|
4112
5226
|
return {
|
|
4113
5227
|
exitCode: ExitCode.WRITE_FAILED,
|
|
@@ -4133,7 +5247,7 @@ async function runTagReconcile(input) {
|
|
|
4133
5247
|
const comment = taxonomyCommentForPage(page.data, date, input.reason);
|
|
4134
5248
|
if (!comment.ok) return { exitCode: ExitCode.SCHEME_REJECTED, result: comment };
|
|
4135
5249
|
if (!input.write) {
|
|
4136
|
-
const schema = await readSchema(
|
|
5250
|
+
const schema = await readSchema(join26(input.vault, "SCHEMA.md"));
|
|
4137
5251
|
if (!schema.result.ok) return { exitCode: schema.exitCode, result: schema.result };
|
|
4138
5252
|
const preview = reconcileTaxonomyDocument(schema.result.data, { tags, comment: comment.data });
|
|
4139
5253
|
return previewResult(page.data, tags, preview, true, []);
|
|
@@ -4185,22 +5299,9 @@ async function runTagReconcile(input) {
|
|
|
4185
5299
|
}
|
|
4186
5300
|
|
|
4187
5301
|
// src/commands/sync.ts
|
|
4188
|
-
import { existsSync as
|
|
4189
|
-
import { join as
|
|
4190
|
-
import { execFileSync as
|
|
4191
|
-
|
|
4192
|
-
// src/utils/git.ts
|
|
4193
|
-
import { execFileSync } from "child_process";
|
|
4194
|
-
function git(cwd, args) {
|
|
4195
|
-
try {
|
|
4196
|
-
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
4197
|
-
} catch {
|
|
4198
|
-
return "";
|
|
4199
|
-
}
|
|
4200
|
-
}
|
|
4201
|
-
function gitStrict(cwd, args) {
|
|
4202
|
-
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
4203
|
-
}
|
|
5302
|
+
import { existsSync as existsSync6 } from "fs";
|
|
5303
|
+
import { join as join27 } from "path";
|
|
5304
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
4204
5305
|
|
|
4205
5306
|
// src/utils/vault-git-pathspec.ts
|
|
4206
5307
|
var VAULT_GENERATED_COMMIT_PATHS = [
|
|
@@ -4241,7 +5342,7 @@ function isTrackedNotePath(path) {
|
|
|
4241
5342
|
}
|
|
4242
5343
|
function refHasPath(vault, ref, path) {
|
|
4243
5344
|
try {
|
|
4244
|
-
|
|
5345
|
+
execFileSync3("git", ["cat-file", "-e", `${ref}:${path}`], {
|
|
4245
5346
|
cwd: vault,
|
|
4246
5347
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4247
5348
|
});
|
|
@@ -4253,7 +5354,7 @@ function refHasPath(vault, ref, path) {
|
|
|
4253
5354
|
function runSyncStatus(input) {
|
|
4254
5355
|
const vault = input.vault;
|
|
4255
5356
|
const includeStashes = input.includeStashes ?? false;
|
|
4256
|
-
if (!
|
|
5357
|
+
if (!existsSync6(join27(vault, ".git"))) {
|
|
4257
5358
|
return {
|
|
4258
5359
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4259
5360
|
result: ok({
|
|
@@ -4360,7 +5461,7 @@ function runSyncStatus(input) {
|
|
|
4360
5461
|
}
|
|
4361
5462
|
async function runSyncPush(input) {
|
|
4362
5463
|
const vault = input.vault;
|
|
4363
|
-
if (!
|
|
5464
|
+
if (!existsSync6(join27(vault, ".git"))) {
|
|
4364
5465
|
return {
|
|
4365
5466
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4366
5467
|
result: err("NOT_A_GIT_REPO", { path: vault })
|
|
@@ -4520,100 +5621,44 @@ function enableGitLongPathsOnWindows(vault) {
|
|
|
4520
5621
|
}
|
|
4521
5622
|
async function runSyncPull(input) {
|
|
4522
5623
|
const vault = input.vault;
|
|
4523
|
-
if (!
|
|
5624
|
+
if (!existsSync6(join27(vault, ".git"))) {
|
|
4524
5625
|
return {
|
|
4525
5626
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4526
5627
|
result: err("NOT_A_GIT_REPO", { path: vault })
|
|
4527
5628
|
};
|
|
4528
5629
|
}
|
|
4529
5630
|
enableGitLongPathsOnWindows(vault);
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
gitStrict(vault, ["fetch", "origin"]);
|
|
4533
|
-
fetched = true;
|
|
4534
|
-
} catch (e) {
|
|
5631
|
+
const remoteUrl = git(vault, ["remote", "get-url", "origin"]);
|
|
5632
|
+
if (!remoteUrl) {
|
|
4535
5633
|
return {
|
|
4536
5634
|
exitCode: ExitCode.SYNC_PULL_FAILED,
|
|
4537
|
-
result: err("
|
|
5635
|
+
result: err("GIT_PULL_FAILED", { message: "no remote configured (origin)" })
|
|
4538
5636
|
};
|
|
4539
5637
|
}
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
let autoResolved = 0;
|
|
4544
|
-
try {
|
|
4545
|
-
const pullOutput = gitStrict(vault, ["pull", "--rebase", "origin", "HEAD"]);
|
|
4546
|
-
pulled = true;
|
|
4547
|
-
const fileMatch = pullOutput.match(/(\d+) file[s]? changed/);
|
|
4548
|
-
if (fileMatch) filesUpdated = parseInt(fileMatch[1], 10);
|
|
4549
|
-
} catch (e) {
|
|
4550
|
-
const errString = String(e);
|
|
4551
|
-
if (errString.includes("conflict")) {
|
|
4552
|
-
let inConflict = true;
|
|
4553
|
-
while (inConflict) {
|
|
4554
|
-
const stoppedSha = git(vault, ["rev-parse", "--verify", "REBASE_HEAD"]);
|
|
4555
|
-
let commitMsg = "";
|
|
4556
|
-
if (stoppedSha) {
|
|
4557
|
-
commitMsg = git(vault, ["log", "--format=%s", "-1", stoppedSha]);
|
|
4558
|
-
}
|
|
4559
|
-
const isArchiveOrSnapshot = commitMsg.startsWith("archive: moved") || commitMsg.startsWith("Snapshot ");
|
|
4560
|
-
const conflictedFiles = git(vault, ["diff", "--name-only", "--diff-filter=U"]);
|
|
4561
|
-
const conflictedList = conflictedFiles ? conflictedFiles.split("\n").filter((l) => l.trim().length > 0) : [];
|
|
4562
|
-
if (conflictedList.length === 0) {
|
|
4563
|
-
try {
|
|
4564
|
-
gitStrict(vault, ["rebase", "--continue"]);
|
|
4565
|
-
inConflict = true;
|
|
4566
|
-
} catch {
|
|
4567
|
-
inConflict = false;
|
|
4568
|
-
}
|
|
4569
|
-
continue;
|
|
4570
|
-
}
|
|
4571
|
-
if (isArchiveOrSnapshot) {
|
|
4572
|
-
for (const f of conflictedList) {
|
|
4573
|
-
try {
|
|
4574
|
-
gitStrict(vault, ["checkout", "--ours", f]);
|
|
4575
|
-
gitStrict(vault, ["add", f]);
|
|
4576
|
-
} catch {
|
|
4577
|
-
}
|
|
4578
|
-
}
|
|
4579
|
-
autoResolved += conflictedList.length;
|
|
4580
|
-
try {
|
|
4581
|
-
gitStrict(vault, ["rebase", "--continue"]);
|
|
4582
|
-
} catch (continueErr) {
|
|
4583
|
-
continue;
|
|
4584
|
-
}
|
|
4585
|
-
} else {
|
|
4586
|
-
conflicts = conflictedList.length;
|
|
4587
|
-
return {
|
|
4588
|
-
exitCode: ExitCode.SYNC_PULL_FAILED,
|
|
4589
|
-
result: ok({
|
|
4590
|
-
fetched,
|
|
4591
|
-
pulled: false,
|
|
4592
|
-
files_updated: 0,
|
|
4593
|
-
conflicts,
|
|
4594
|
-
auto_resolved: 0,
|
|
4595
|
-
lint_errors: 0,
|
|
4596
|
-
lint_warnings: 0,
|
|
4597
|
-
humanHint: `pull failed with ${conflicts} conflict(s) on non-archive commit "${commitMsg}" \u2014 resolve manually`
|
|
4598
|
-
})
|
|
4599
|
-
};
|
|
4600
|
-
}
|
|
4601
|
-
}
|
|
4602
|
-
if (autoResolved > 0) {
|
|
4603
|
-
const diffOutput = git(vault, ["diff", "--stat", "HEAD@{1}..HEAD"]);
|
|
4604
|
-
if (diffOutput) {
|
|
4605
|
-
const fileMatch = diffOutput.match(/(\d+) file[s]? changed/);
|
|
4606
|
-
if (fileMatch) filesUpdated = parseInt(fileMatch[1], 10);
|
|
4607
|
-
}
|
|
4608
|
-
pulled = true;
|
|
4609
|
-
conflicts = 0;
|
|
4610
|
-
}
|
|
4611
|
-
} else {
|
|
5638
|
+
const helper = await runVaultSyncPullHelper({ vault, remote: "origin", branch: "main" });
|
|
5639
|
+
if (!helper.ok) {
|
|
5640
|
+
if (helper.error === "PREFLIGHT_FAILED") {
|
|
4612
5641
|
return {
|
|
4613
|
-
exitCode: ExitCode.
|
|
4614
|
-
result: err("
|
|
5642
|
+
exitCode: ExitCode.PREFLIGHT_FAILED,
|
|
5643
|
+
result: err("PREFLIGHT_FAILED", helper.detail)
|
|
4615
5644
|
};
|
|
4616
5645
|
}
|
|
5646
|
+
return {
|
|
5647
|
+
exitCode: ExitCode.SYNC_PULL_FAILED,
|
|
5648
|
+
result: err("GIT_PULL_FAILED", helper.detail)
|
|
5649
|
+
};
|
|
5650
|
+
}
|
|
5651
|
+
const fetched = true;
|
|
5652
|
+
const pulled = helper.data.changed;
|
|
5653
|
+
const conflicts = 0;
|
|
5654
|
+
const autoResolved = 0;
|
|
5655
|
+
let filesUpdated = 0;
|
|
5656
|
+
if (helper.data.changed) {
|
|
5657
|
+
const diffOutput = git(vault, ["diff", "--stat", `${helper.data.before_oid}..${helper.data.after_oid}`]);
|
|
5658
|
+
if (diffOutput) {
|
|
5659
|
+
const fileMatch = diffOutput.match(/(\d+) file[s]? changed/);
|
|
5660
|
+
if (fileMatch) filesUpdated = parseInt(fileMatch[1], 10);
|
|
5661
|
+
}
|
|
4617
5662
|
}
|
|
4618
5663
|
const pathFix = await fixPathTooLong({ vault });
|
|
4619
5664
|
const pathFixCount = pathFix.result.ok ? pathFix.result.data.fixed.length : 0;
|
|
@@ -4626,8 +5671,7 @@ async function runSyncPull(input) {
|
|
|
4626
5671
|
}
|
|
4627
5672
|
const hintParts = [];
|
|
4628
5673
|
if (filesUpdated > 0) hintParts.push(`updated ${filesUpdated} file(s)`);
|
|
4629
|
-
else hintParts.push("already up to date");
|
|
4630
|
-
if (autoResolved > 0) hintParts.push(`${autoResolved} conflict(s) auto-resolved`);
|
|
5674
|
+
else hintParts.push(pulled ? "pulled via vault-sync helper" : "already up to date");
|
|
4631
5675
|
if (pathFixCount > 0) hintParts.push(`${pathFixCount} long path(s) fixed`);
|
|
4632
5676
|
if (lintErrors > 0) hintParts.push(`${lintErrors} lint error(s)`);
|
|
4633
5677
|
if (lintWarnings > 0) hintParts.push(`${lintWarnings} lint warning(s)`);
|
|
@@ -4692,7 +5736,7 @@ function runSyncPeers(input) {
|
|
|
4692
5736
|
}
|
|
4693
5737
|
function runSyncLock(input) {
|
|
4694
5738
|
const vault = input.vault;
|
|
4695
|
-
if (!
|
|
5739
|
+
if (!existsSync6(vault)) {
|
|
4696
5740
|
return {
|
|
4697
5741
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4698
5742
|
result: err("VAULT_PATH_INVALID", { path: vault })
|
|
@@ -4727,7 +5771,7 @@ function runSyncLock(input) {
|
|
|
4727
5771
|
}
|
|
4728
5772
|
function runSyncUnlock(input) {
|
|
4729
5773
|
const vault = input.vault;
|
|
4730
|
-
if (!
|
|
5774
|
+
if (!existsSync6(vault)) {
|
|
4731
5775
|
return {
|
|
4732
5776
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
4733
5777
|
result: err("VAULT_PATH_INVALID", { path: vault })
|
|
@@ -4759,9 +5803,53 @@ function runSyncUnlock(input) {
|
|
|
4759
5803
|
};
|
|
4760
5804
|
}
|
|
4761
5805
|
|
|
5806
|
+
// src/commands/sync-journal.ts
|
|
5807
|
+
function runSyncJournalList(input) {
|
|
5808
|
+
const byPhase = {};
|
|
5809
|
+
for (const opId of listJournalOpIds(input.vault)) {
|
|
5810
|
+
const fields = readJournal(input.vault, opId);
|
|
5811
|
+
const phase = fields?.phase ?? "unknown";
|
|
5812
|
+
byPhase[phase] = (byPhase[phase] ?? 0) + 1;
|
|
5813
|
+
}
|
|
5814
|
+
const review = listReviewRequiredOps(input.vault).map(({ opId, fields }) => ({
|
|
5815
|
+
operation_id: opId,
|
|
5816
|
+
reason: fields.reason,
|
|
5817
|
+
target_oid: fields.target_oid,
|
|
5818
|
+
original_head: fields.original_head
|
|
5819
|
+
}));
|
|
5820
|
+
const total = Object.values(byPhase).reduce((a, b) => a + b, 0);
|
|
5821
|
+
const hint = review.length === 0 ? `journals: ${total} total; no review-required handoffs` : `journals: ${total} total; ${review.length} review-required \u2014 if worktree clean: skillwiki sync journal clear-stale --dry-run`;
|
|
5822
|
+
return {
|
|
5823
|
+
exitCode: ExitCode.OK,
|
|
5824
|
+
result: ok({
|
|
5825
|
+
total,
|
|
5826
|
+
by_phase: byPhase,
|
|
5827
|
+
review_required: review,
|
|
5828
|
+
humanHint: hint
|
|
5829
|
+
})
|
|
5830
|
+
};
|
|
5831
|
+
}
|
|
5832
|
+
function runSyncJournalClearStale(input) {
|
|
5833
|
+
const { superseded, skipped } = supersedeStaleReviewRequiredJournals(input.vault, {
|
|
5834
|
+
dryRun: !!input.dryRun,
|
|
5835
|
+
by: input.dryRun ? "skillwiki-sync-journal-clear-stale-dry-run" : "skillwiki-sync-journal-clear-stale"
|
|
5836
|
+
});
|
|
5837
|
+
const mode = input.dryRun ? "dry-run" : "write";
|
|
5838
|
+
const hint = superseded.length === 0 ? `clear-stale (${mode}): nothing to supersede; skipped=${skipped.length}` : `clear-stale (${mode}): ${superseded.length} journal(s); skipped=${skipped.length}`;
|
|
5839
|
+
return {
|
|
5840
|
+
exitCode: ExitCode.OK,
|
|
5841
|
+
result: ok({
|
|
5842
|
+
dry_run: !!input.dryRun,
|
|
5843
|
+
superseded,
|
|
5844
|
+
skipped,
|
|
5845
|
+
humanHint: hint
|
|
5846
|
+
})
|
|
5847
|
+
};
|
|
5848
|
+
}
|
|
5849
|
+
|
|
4762
5850
|
// src/commands/backup.ts
|
|
4763
|
-
import { statSync as statSync2, readdirSync, readFileSync as
|
|
4764
|
-
import { join as
|
|
5851
|
+
import { statSync as statSync2, readdirSync, readFileSync as readFileSync11, mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
5852
|
+
import { join as join28, relative as relative2, dirname as dirname6 } from "path";
|
|
4765
5853
|
import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
|
|
4766
5854
|
|
|
4767
5855
|
// src/utils/s3-client.ts
|
|
@@ -4785,7 +5873,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node_
|
|
|
4785
5873
|
function* walkMarkdown(dir, base) {
|
|
4786
5874
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
4787
5875
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
4788
|
-
const full =
|
|
5876
|
+
const full = join28(dir, entry.name);
|
|
4789
5877
|
if (entry.isDirectory()) {
|
|
4790
5878
|
yield* walkMarkdown(full, base);
|
|
4791
5879
|
} else if (entry.name.endsWith(".md")) {
|
|
@@ -4808,7 +5896,7 @@ async function runBackupSync(input) {
|
|
|
4808
5896
|
let failed = 0;
|
|
4809
5897
|
const files = [...walkMarkdown(input.vault, input.vault)];
|
|
4810
5898
|
for (const relPath of files) {
|
|
4811
|
-
const absPath =
|
|
5899
|
+
const absPath = join28(input.vault, relPath);
|
|
4812
5900
|
const localStat = statSync2(absPath);
|
|
4813
5901
|
let needsUpload = true;
|
|
4814
5902
|
try {
|
|
@@ -4827,7 +5915,7 @@ async function runBackupSync(input) {
|
|
|
4827
5915
|
continue;
|
|
4828
5916
|
}
|
|
4829
5917
|
try {
|
|
4830
|
-
const body =
|
|
5918
|
+
const body = readFileSync11(absPath);
|
|
4831
5919
|
await client.send(new PutObjectCommand({ Bucket: input.bucket, Key: relPath, Body: body }));
|
|
4832
5920
|
uploaded++;
|
|
4833
5921
|
} catch {
|
|
@@ -4884,7 +5972,7 @@ async function runBackupRestore(input) {
|
|
|
4884
5972
|
const objects = list.Contents ?? [];
|
|
4885
5973
|
for (const obj of objects) {
|
|
4886
5974
|
if (!obj.Key) continue;
|
|
4887
|
-
const localPath =
|
|
5975
|
+
const localPath = join28(target, obj.Key);
|
|
4888
5976
|
try {
|
|
4889
5977
|
const localStat = statSync2(localPath);
|
|
4890
5978
|
if (obj.LastModified && localStat.mtime > obj.LastModified) {
|
|
@@ -4898,7 +5986,7 @@ async function runBackupRestore(input) {
|
|
|
4898
5986
|
const body = await resp.Body?.transformToByteArray();
|
|
4899
5987
|
if (body) {
|
|
4900
5988
|
mkdirSync2(dirname6(localPath), { recursive: true });
|
|
4901
|
-
|
|
5989
|
+
writeFileSync4(localPath, Buffer.from(body));
|
|
4902
5990
|
downloaded++;
|
|
4903
5991
|
}
|
|
4904
5992
|
} catch {
|
|
@@ -4930,11 +6018,11 @@ async function runBackupRestore(input) {
|
|
|
4930
6018
|
}
|
|
4931
6019
|
|
|
4932
6020
|
// src/commands/status.ts
|
|
4933
|
-
import { existsSync as
|
|
4934
|
-
import { readFile as
|
|
4935
|
-
import { join as
|
|
6021
|
+
import { existsSync as existsSync7, statSync as statSync3 } from "fs";
|
|
6022
|
+
import { readFile as readFile15 } from "fs/promises";
|
|
6023
|
+
import { join as join29 } from "path";
|
|
4936
6024
|
async function runStatus(input) {
|
|
4937
|
-
if (!
|
|
6025
|
+
if (!existsSync7(input.vault)) {
|
|
4938
6026
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
4939
6027
|
}
|
|
4940
6028
|
const scan = await scanVault(input.vault);
|
|
@@ -4959,7 +6047,7 @@ async function runStatus(input) {
|
|
|
4959
6047
|
const compound = scan.data.compound.length;
|
|
4960
6048
|
let schemaVersion = "v1";
|
|
4961
6049
|
try {
|
|
4962
|
-
const schemaContent = await
|
|
6050
|
+
const schemaContent = await readFile15(join29(input.vault, "SCHEMA.md"), "utf8");
|
|
4963
6051
|
const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
|
|
4964
6052
|
if (versionMatch) schemaVersion = versionMatch[1];
|
|
4965
6053
|
} catch {
|
|
@@ -5019,8 +6107,8 @@ async function runStatus(input) {
|
|
|
5019
6107
|
}
|
|
5020
6108
|
|
|
5021
6109
|
// src/commands/seed.ts
|
|
5022
|
-
import { mkdir as
|
|
5023
|
-
import { join as
|
|
6110
|
+
import { mkdir as mkdir11, writeFile as writeFile9, stat as stat4 } from "fs/promises";
|
|
6111
|
+
import { join as join30 } from "path";
|
|
5024
6112
|
var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
5025
6113
|
var EXAMPLE_PAGES = {
|
|
5026
6114
|
"entities/example-project.md": `---
|
|
@@ -5089,29 +6177,29 @@ Real sources are immutable after ingestion \u2014 never edit them.
|
|
|
5089
6177
|
`;
|
|
5090
6178
|
async function runSeed(input) {
|
|
5091
6179
|
try {
|
|
5092
|
-
await stat4(
|
|
6180
|
+
await stat4(join30(input.vault, "SCHEMA.md"));
|
|
5093
6181
|
} catch {
|
|
5094
6182
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
|
|
5095
6183
|
}
|
|
5096
6184
|
const created = [];
|
|
5097
6185
|
const skipped = [];
|
|
5098
6186
|
for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
|
|
5099
|
-
const absPath =
|
|
6187
|
+
const absPath = join30(input.vault, relPath);
|
|
5100
6188
|
try {
|
|
5101
6189
|
await stat4(absPath);
|
|
5102
6190
|
skipped.push(relPath);
|
|
5103
6191
|
} catch {
|
|
5104
|
-
await
|
|
6192
|
+
await mkdir11(join30(absPath, ".."), { recursive: true });
|
|
5105
6193
|
await writeFile9(absPath, content, "utf8");
|
|
5106
6194
|
created.push(relPath);
|
|
5107
6195
|
}
|
|
5108
6196
|
}
|
|
5109
|
-
const rawPath =
|
|
6197
|
+
const rawPath = join30(input.vault, "raw", "articles", "example-source.md");
|
|
5110
6198
|
try {
|
|
5111
6199
|
await stat4(rawPath);
|
|
5112
6200
|
skipped.push("raw/articles/example-source.md");
|
|
5113
6201
|
} catch {
|
|
5114
|
-
await
|
|
6202
|
+
await mkdir11(join30(rawPath, ".."), { recursive: true });
|
|
5115
6203
|
await writeFile9(rawPath, EXAMPLE_RAW, "utf8");
|
|
5116
6204
|
created.push("raw/articles/example-source.md");
|
|
5117
6205
|
}
|
|
@@ -5134,9 +6222,9 @@ async function runSeed(input) {
|
|
|
5134
6222
|
}
|
|
5135
6223
|
|
|
5136
6224
|
// src/commands/canvas.ts
|
|
5137
|
-
import { readFile as
|
|
5138
|
-
import { existsSync as
|
|
5139
|
-
import { join as
|
|
6225
|
+
import { readFile as readFile16, writeFile as writeFile10 } from "fs/promises";
|
|
6226
|
+
import { existsSync as existsSync8 } from "fs";
|
|
6227
|
+
import { join as join31 } from "path";
|
|
5140
6228
|
var NODE_WIDTH = 240;
|
|
5141
6229
|
var NODE_HEIGHT = 60;
|
|
5142
6230
|
var COLUMN_SPACING = 400;
|
|
@@ -5214,8 +6302,8 @@ function buildCanvasEdges(adjacency) {
|
|
|
5214
6302
|
return edges;
|
|
5215
6303
|
}
|
|
5216
6304
|
async function runCanvasGenerate(input) {
|
|
5217
|
-
const graphPath = input.graphPath ??
|
|
5218
|
-
if (!
|
|
6305
|
+
const graphPath = input.graphPath ?? join31(input.vault, ".skillwiki", "graph.json");
|
|
6306
|
+
if (!existsSync8(graphPath)) {
|
|
5219
6307
|
return {
|
|
5220
6308
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
5221
6309
|
result: err("FILE_NOT_FOUND", {
|
|
@@ -5226,7 +6314,7 @@ async function runCanvasGenerate(input) {
|
|
|
5226
6314
|
}
|
|
5227
6315
|
let raw;
|
|
5228
6316
|
try {
|
|
5229
|
-
raw = await
|
|
6317
|
+
raw = await readFile16(graphPath, "utf8");
|
|
5230
6318
|
} catch (e) {
|
|
5231
6319
|
return {
|
|
5232
6320
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
@@ -5252,7 +6340,7 @@ async function runCanvasGenerate(input) {
|
|
|
5252
6340
|
const nodes = buildCanvasNodes(paths);
|
|
5253
6341
|
const edges = buildCanvasEdges(graph.adjacency);
|
|
5254
6342
|
const canvas = { nodes, edges };
|
|
5255
|
-
const outPath =
|
|
6343
|
+
const outPath = join31(input.vault, "vault-graph.canvas");
|
|
5256
6344
|
try {
|
|
5257
6345
|
await writeFile10(outPath, JSON.stringify(canvas, null, 2));
|
|
5258
6346
|
} catch (e) {
|
|
@@ -5274,15 +6362,15 @@ written: ${outPath}`
|
|
|
5274
6362
|
}
|
|
5275
6363
|
|
|
5276
6364
|
// src/commands/fleet-health.ts
|
|
5277
|
-
import { existsSync as
|
|
6365
|
+
import { existsSync as existsSync9, readFileSync as readFileSync12 } from "fs";
|
|
5278
6366
|
import { execSync as nodeExecSync } from "child_process";
|
|
5279
6367
|
import { hostname as nodeHostname, platform as nodePlatform } from "os";
|
|
5280
|
-
import { join as
|
|
6368
|
+
import { join as join32 } from "path";
|
|
5281
6369
|
var SSH_TIMEOUT_MS = 15e3;
|
|
5282
6370
|
var TIMER_UNIT = "agent-memory-trends.timer";
|
|
5283
6371
|
var SERVICE_UNIT = "agent-memory-trends.service";
|
|
5284
6372
|
var SYSTEMD_SERVICE_FAILED_CLASS = "SYSTEMD_SERVICE_FAILED";
|
|
5285
|
-
function
|
|
6373
|
+
function defaultDeps2() {
|
|
5286
6374
|
return {
|
|
5287
6375
|
platform: () => nodePlatform(),
|
|
5288
6376
|
execSync: nodeExecSync
|
|
@@ -5374,9 +6462,9 @@ function applyServiceFailedOverlay(run, serviceFailed) {
|
|
|
5374
6462
|
function probeLocal(vaultPath, deps) {
|
|
5375
6463
|
const latestPath = satelliteLatestRunPath(vaultPath);
|
|
5376
6464
|
let parsed = null;
|
|
5377
|
-
if (
|
|
6465
|
+
if (existsSync9(latestPath)) {
|
|
5378
6466
|
try {
|
|
5379
|
-
const wire = readSatelliteLatestRunFromText(
|
|
6467
|
+
const wire = readSatelliteLatestRunFromText(readFileSync12(latestPath, "utf8"));
|
|
5380
6468
|
if (wire) {
|
|
5381
6469
|
parsed = {
|
|
5382
6470
|
status: wire.status,
|
|
@@ -5502,12 +6590,12 @@ function rowHealthy(reachable, timer, runUnhealthy, timerBad, platform2) {
|
|
|
5502
6590
|
return true;
|
|
5503
6591
|
}
|
|
5504
6592
|
async function runFleetHealth(input) {
|
|
5505
|
-
const deps = input.deps ??
|
|
6593
|
+
const deps = input.deps ?? defaultDeps2();
|
|
5506
6594
|
const env = input.env ?? process.env;
|
|
5507
6595
|
const home = input.home ?? env.HOME ?? "";
|
|
5508
6596
|
const osHostname = input.osHostname ?? env.HOSTNAME ?? nodeHostname();
|
|
5509
6597
|
const vault = input.vault ?? env.WIKI_PATH;
|
|
5510
|
-
const file = input.file ?? (vault ?
|
|
6598
|
+
const file = input.file ?? (vault ? join32(vault, FLEET_REL_PATH) : void 0);
|
|
5511
6599
|
if (!file) {
|
|
5512
6600
|
return {
|
|
5513
6601
|
exitCode: ExitCode.NO_VAULT_CONFIGURED,
|
|
@@ -5591,15 +6679,15 @@ async function runFleetHealth(input) {
|
|
|
5591
6679
|
}
|
|
5592
6680
|
|
|
5593
6681
|
// src/utils/auto-commit.ts
|
|
5594
|
-
import { existsSync as
|
|
5595
|
-
import { join as
|
|
6682
|
+
import { existsSync as existsSync10 } from "fs";
|
|
6683
|
+
import { join as join33 } from "path";
|
|
5596
6684
|
async function postCommit(vault, exitCode) {
|
|
5597
6685
|
if (exitCode !== 0) return;
|
|
5598
6686
|
const home = process.env.HOME ?? "";
|
|
5599
6687
|
const dotenv = await parseDotenvFile(configPath(home));
|
|
5600
6688
|
const autoCommit = process.env.AUTO_COMMIT ?? dotenv["AUTO_COMMIT"];
|
|
5601
6689
|
if (autoCommit === "false") return;
|
|
5602
|
-
if (!
|
|
6690
|
+
if (!existsSync10(join33(vault, ".git"))) return;
|
|
5603
6691
|
const lastOps = readLastOp(vault);
|
|
5604
6692
|
if (lastOps.length === 0) return;
|
|
5605
6693
|
const porcelain = git(vault, ["status", "--porcelain", "--", ...VAULT_COMMIT_PATHSPEC]);
|
|
@@ -5622,103 +6710,6 @@ async function postCommit(vault, exitCode) {
|
|
|
5622
6710
|
clearLastOp(vault);
|
|
5623
6711
|
}
|
|
5624
6712
|
|
|
5625
|
-
// src/utils/protected-vault-write-guard.ts
|
|
5626
|
-
import { readFileSync as readFileSync6 } from "fs";
|
|
5627
|
-
import { join as join26, resolve as resolvePath } from "path";
|
|
5628
|
-
async function guardProtectedVaultWrite(input) {
|
|
5629
|
-
const env = input.env ?? process.env;
|
|
5630
|
-
const home = input.home ?? process.env.HOME ?? "";
|
|
5631
|
-
const cwd = input.cwd ?? process.cwd();
|
|
5632
|
-
const liveVaultPath = await resolveLiveVaultPath({ env, home, cwd });
|
|
5633
|
-
const load = await loadFleetManifestAndHost({
|
|
5634
|
-
vault: liveVaultPath ?? input.vault,
|
|
5635
|
-
hostId: input.hostId,
|
|
5636
|
-
env,
|
|
5637
|
-
home,
|
|
5638
|
-
cwd,
|
|
5639
|
-
osHostname: input.osHostname ?? process.env.HOSTNAME,
|
|
5640
|
-
user: input.user ?? process.env.USER
|
|
5641
|
-
});
|
|
5642
|
-
if (!load?.hostId || load.identityStatus !== "known") {
|
|
5643
|
-
return { blocked: false };
|
|
5644
|
-
}
|
|
5645
|
-
const host = load.manifest.hosts[load.hostId];
|
|
5646
|
-
if (!host || host.role !== "snapshotter" || host.protected !== true) {
|
|
5647
|
-
return { blocked: false };
|
|
5648
|
-
}
|
|
5649
|
-
const snapshotWorktree = resolveSnapshotWorktree(home);
|
|
5650
|
-
const targetVault = resolvePath(input.vault);
|
|
5651
|
-
const canonicalLiveVault = liveVaultPath ? resolvePath(liveVaultPath) : void 0;
|
|
5652
|
-
const canonicalSnapshotWorktree = snapshotWorktree ? resolvePath(snapshotWorktree) : void 0;
|
|
5653
|
-
if (canonicalLiveVault && targetVault === canonicalLiveVault) {
|
|
5654
|
-
return { blocked: false };
|
|
5655
|
-
}
|
|
5656
|
-
if (canonicalSnapshotWorktree && targetVault === canonicalSnapshotWorktree) {
|
|
5657
|
-
return {
|
|
5658
|
-
blocked: true,
|
|
5659
|
-
exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED,
|
|
5660
|
-
result: err("PROTECTED_SNAPSHOTTER_WRITE_BLOCKED", {
|
|
5661
|
-
host_id: load.hostId,
|
|
5662
|
-
command: input.command,
|
|
5663
|
-
reason: `refusing mutation of snapshot worktree '${canonicalSnapshotWorktree}' on protected snapshotter host '${load.hostId}'`,
|
|
5664
|
-
guidance: canonicalLiveVault ? `Use the live vault path '${canonicalLiveVault}' for authoring. Keep snapshot worktrees read-only except explicit snapshot maintenance.` : "Use the live skillwiki vault path for authoring. Keep snapshot worktrees read-only except explicit snapshot maintenance."
|
|
5665
|
-
})
|
|
5666
|
-
};
|
|
5667
|
-
}
|
|
5668
|
-
if (canonicalLiveVault && targetVault !== canonicalLiveVault) {
|
|
5669
|
-
return {
|
|
5670
|
-
blocked: true,
|
|
5671
|
-
exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED,
|
|
5672
|
-
result: err("PROTECTED_SNAPSHOTTER_WRITE_BLOCKED", {
|
|
5673
|
-
host_id: load.hostId,
|
|
5674
|
-
command: input.command,
|
|
5675
|
-
reason: `refusing vault mutation outside the live vault path '${canonicalLiveVault}' on protected snapshotter host '${load.hostId}'`,
|
|
5676
|
-
guidance: "Use the resolved live skillwiki vault path for authoring. Keep alternate vault roots and snapshot worktrees read-only unless explicitly approved."
|
|
5677
|
-
})
|
|
5678
|
-
};
|
|
5679
|
-
}
|
|
5680
|
-
return { blocked: false };
|
|
5681
|
-
}
|
|
5682
|
-
async function resolveLiveVaultPath(input) {
|
|
5683
|
-
const resolved = await resolveRuntimePath({
|
|
5684
|
-
flag: void 0,
|
|
5685
|
-
envValue: input.env.WIKI_PATH,
|
|
5686
|
-
wikiEnv: input.env.WIKI,
|
|
5687
|
-
home: input.home,
|
|
5688
|
-
cwd: input.cwd
|
|
5689
|
-
});
|
|
5690
|
-
return resolved.ok ? resolved.data.path : void 0;
|
|
5691
|
-
}
|
|
5692
|
-
function resolveSnapshotWorktree(home) {
|
|
5693
|
-
const skillwikiEnv = join26(home, ".skillwiki", ".env");
|
|
5694
|
-
const explicitWorktree = readEnvKey(skillwikiEnv, ["vault_sync.snapshot_worktree"]);
|
|
5695
|
-
if (explicitWorktree) return explicitWorktree;
|
|
5696
|
-
const snapshotProfile = readEnvKey(skillwikiEnv, ["vault_sync.snapshot_profile"]);
|
|
5697
|
-
if (snapshotProfile) {
|
|
5698
|
-
const fromProfile = readEnvKey(snapshotProfile, ["WIKI_GIT_WORKTREE", "SNAPSHOT_WORKTREE", "GIT_DIR"]);
|
|
5699
|
-
if (fromProfile) return fromProfile;
|
|
5700
|
-
}
|
|
5701
|
-
return "/root/wiki-git";
|
|
5702
|
-
}
|
|
5703
|
-
function readEnvKey(path, keys) {
|
|
5704
|
-
try {
|
|
5705
|
-
const content = readFileSync6(path, "utf8");
|
|
5706
|
-
for (const line of content.split(/\r?\n/)) {
|
|
5707
|
-
const trimmed = line.trim();
|
|
5708
|
-
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
|
|
5709
|
-
const eq = trimmed.indexOf("=");
|
|
5710
|
-
if (eq <= 0) continue;
|
|
5711
|
-
const key = trimmed.slice(0, eq).trim();
|
|
5712
|
-
if (!keys.includes(key)) continue;
|
|
5713
|
-
const value = trimmed.slice(eq + 1).trim();
|
|
5714
|
-
if (value.length > 0) return value;
|
|
5715
|
-
}
|
|
5716
|
-
} catch {
|
|
5717
|
-
return void 0;
|
|
5718
|
-
}
|
|
5719
|
-
return void 0;
|
|
5720
|
-
}
|
|
5721
|
-
|
|
5722
6713
|
// src/cli.ts
|
|
5723
6714
|
var pkg = readCliPackageJson();
|
|
5724
6715
|
var program = new Command();
|
|
@@ -5745,6 +6736,28 @@ async function emitGuardedVaultWrite(vault, command, run, opts) {
|
|
|
5745
6736
|
}
|
|
5746
6737
|
return emit(await run(), vault, opts);
|
|
5747
6738
|
}
|
|
6739
|
+
async function emitManagedVaultWrite(vault, command, mutate, opts) {
|
|
6740
|
+
const guard = await guardProtectedVaultWrite({
|
|
6741
|
+
vault,
|
|
6742
|
+
command,
|
|
6743
|
+
env: process.env,
|
|
6744
|
+
home: process.env.HOME ?? "",
|
|
6745
|
+
cwd: process.cwd(),
|
|
6746
|
+
osHostname: process.env.HOSTNAME,
|
|
6747
|
+
user: process.env.USER
|
|
6748
|
+
});
|
|
6749
|
+
if (guard.blocked) {
|
|
6750
|
+
return emit({ exitCode: guard.exitCode, result: guard.result }, void 0, { postCommit: false });
|
|
6751
|
+
}
|
|
6752
|
+
const { runManagedWriteTransaction: runManagedWriteTransaction2 } = await import("./managed-write-preflight-PW4OOOMV.js");
|
|
6753
|
+
const run = await runManagedWriteTransaction2({
|
|
6754
|
+
vault,
|
|
6755
|
+
command,
|
|
6756
|
+
allowImmutableRecord: opts?.allowImmutableRecord === true,
|
|
6757
|
+
mutate: async (receipt) => await mutate(receipt)
|
|
6758
|
+
});
|
|
6759
|
+
return emit(run, vault, opts);
|
|
6760
|
+
}
|
|
5748
6761
|
program.command("hash <file>").description("compute SHA-256 hash of a vault page body").action(async (file) => emit(await runHash({ file })));
|
|
5749
6762
|
program.command("fetch-guard <url>").description("check if a URL passes fetch guard rules and sanitize secrets").action(async (url) => emit(await runFetchGuard({ url })));
|
|
5750
6763
|
program.command("validate <file>").description("validate vault page frontmatter against its detected schema").option("--apply", "auto-update vault index.md and log.md after successful validation", false).option("--vault <dir>", "vault root directory (required with --apply)").option("--wiki <name>", "wiki profile name").action(async (file, opts) => {
|
|
@@ -5755,12 +6768,12 @@ program.command("validate <file>").description("validate vault page frontmatter
|
|
|
5755
6768
|
else vault = v.vault;
|
|
5756
6769
|
}
|
|
5757
6770
|
if (opts.apply && vault) {
|
|
5758
|
-
return
|
|
6771
|
+
return emitManagedVaultWrite(vault, "validate --apply", () => runValidate({ file, apply: true, vault }), void 0);
|
|
5759
6772
|
}
|
|
5760
6773
|
emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
|
|
5761
6774
|
});
|
|
5762
6775
|
program.command("graph").description("graph subcommands").command("build <vault>").option("--out <path>", "graph output path (default: <vault>/.skillwiki/graph.json)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5763
|
-
const out = opts.out ??
|
|
6776
|
+
const out = opts.out ?? join34(vault, ".skillwiki", "graph.json");
|
|
5764
6777
|
return emitGuardedVaultWrite(vault, "graph build", () => runGraphBuild({ vault, out }));
|
|
5765
6778
|
});
|
|
5766
6779
|
var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
|
|
@@ -5893,6 +6906,12 @@ pageCmd.command("publish <draft> [vault]").description("preview or publish an un
|
|
|
5893
6906
|
{ postCommit: false }
|
|
5894
6907
|
);
|
|
5895
6908
|
});
|
|
6909
|
+
var indexCmd = program.command("index").description("root index projection and checks");
|
|
6910
|
+
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) => {
|
|
6911
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6912
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6913
|
+
else emit(await runIndexRebuild({ vault: v.vault, write: Boolean(opts.write) }), v.vault);
|
|
6914
|
+
});
|
|
5896
6915
|
program.command("index-check [vault]").description("verify index.md entries match actual vault pages").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5897
6916
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5898
6917
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
@@ -5945,12 +6964,47 @@ program.command("log-rotate [vault]").description("rotate or trim the vault log
|
|
|
5945
6964
|
program.command("log-append [vault]").description("append a single entry to the vault log under a short advisory lock").requiredOption("--content <text>", "log entry text to append").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5946
6965
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5947
6966
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
5948
|
-
else return
|
|
6967
|
+
else return emitManagedVaultWrite(
|
|
5949
6968
|
v.vault,
|
|
5950
6969
|
"log-append",
|
|
5951
6970
|
() => runLogAppend({ vault: v.vault, content: opts.content })
|
|
5952
6971
|
);
|
|
5953
6972
|
});
|
|
6973
|
+
var logCmd = program.command("log").description("immutable log events and projections");
|
|
6974
|
+
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) => {
|
|
6975
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6976
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6977
|
+
else if (opts.write) {
|
|
6978
|
+
return emitManagedVaultWrite(
|
|
6979
|
+
v.vault,
|
|
6980
|
+
"log materialize",
|
|
6981
|
+
() => runLogMaterialize({ vault: v.vault, write: true })
|
|
6982
|
+
);
|
|
6983
|
+
} else emit(await runLogMaterialize({ vault: v.vault, write: false }), v.vault);
|
|
6984
|
+
});
|
|
6985
|
+
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) => {
|
|
6986
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6987
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6988
|
+
else if (opts.write) {
|
|
6989
|
+
return emitManagedVaultWrite(
|
|
6990
|
+
v.vault,
|
|
6991
|
+
"log migrate-legacy",
|
|
6992
|
+
() => runLogMigrateLegacy({ vault: v.vault, write: true })
|
|
6993
|
+
);
|
|
6994
|
+
} else emit(await runLogMigrateLegacy({ vault: v.vault, write: false }), v.vault);
|
|
6995
|
+
});
|
|
6996
|
+
var projectionsCmd = program.command("projections").description("root index/log projection materialization");
|
|
6997
|
+
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) => {
|
|
6998
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6999
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
7000
|
+
else if (opts.write) {
|
|
7001
|
+
return emitManagedVaultWrite(
|
|
7002
|
+
v.vault,
|
|
7003
|
+
"projections materialize",
|
|
7004
|
+
() => runProjectionsMaterialize({ vault: v.vault, write: true })
|
|
7005
|
+
);
|
|
7006
|
+
} else emit(await runProjectionsMaterialize({ vault: v.vault, write: false }), v.vault);
|
|
7007
|
+
});
|
|
5954
7008
|
program.command("lint [vault]").description("run all vault health checks").option("--days <n>", "stale threshold", (s) => parseInt(s, 10), 90).option("--lines <n>", "pagesize threshold", (s) => parseInt(s, 10), 200).option("--log-threshold <n>", "log rotation threshold", (s) => parseInt(s, 10), 500).option("--fix", "auto-fix supported lint violations").option("--only <bucket>", "run only the specified lint bucket").option("--summary", "emit bounded bucket counts instead of full item arrays", false).option("--examples <n>", "example count per bucket in summary mode", (s) => parseInt(s, 10), 3).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
5955
7009
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
5956
7010
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
@@ -6045,7 +7099,7 @@ program.command("archive <page> [vault]").description("archive a typed-knowledge
|
|
|
6045
7099
|
remoteDelete: !!opts.remoteDelete,
|
|
6046
7100
|
maxRemoteDeletes: Number.parseInt(opts.maxRemoteDeletes, 10)
|
|
6047
7101
|
}), v.vault);
|
|
6048
|
-
else return
|
|
7102
|
+
else return emitManagedVaultWrite(
|
|
6049
7103
|
v.vault,
|
|
6050
7104
|
"archive",
|
|
6051
7105
|
() => runArchive({
|
|
@@ -6062,7 +7116,7 @@ program.command("archive <page> [vault]").description("archive a typed-knowledge
|
|
|
6062
7116
|
program.command("remove <page> [vault]").description("remove a vault path and write a delete-intent tombstone").option("--wiki <name>", "wiki profile name").option("--remote <remote>", "rclone remote root to prune the live path, for example seaweed-wiki:cloud/wiki").option("--remote-delete", "delete the live path from the remote after local remove", false).option("--max-remote-deletes <n>", "maximum remote object deletes allowed", "1").option("--reason <text>", "stored on the delete-intent tombstone").action(async (page, vault, opts) => {
|
|
6063
7117
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6064
7118
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6065
|
-
else return
|
|
7119
|
+
else return emitManagedVaultWrite(
|
|
6066
7120
|
v.vault,
|
|
6067
7121
|
"remove",
|
|
6068
7122
|
() => runRemove({
|
|
@@ -6235,6 +7289,35 @@ syncCmd.command("pull [vault]").description("pull remote vault changes and lint"
|
|
|
6235
7289
|
() => runSyncPull({ vault: v.vault })
|
|
6236
7290
|
);
|
|
6237
7291
|
});
|
|
7292
|
+
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) => {
|
|
7293
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
7294
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
7295
|
+
else {
|
|
7296
|
+
return emitGuardedVaultWrite(
|
|
7297
|
+
v.vault,
|
|
7298
|
+
"sync resolve-derived",
|
|
7299
|
+
() => runDerivedConflictResolution({ vault: v.vault, operationId: opts.operationId })
|
|
7300
|
+
);
|
|
7301
|
+
}
|
|
7302
|
+
});
|
|
7303
|
+
var syncJournalCmd = syncCmd.command("journal").description("inspect or clear vault-sync operation journals");
|
|
7304
|
+
syncJournalCmd.command("list [vault]").description("list vault-sync operation journals and review-required handoffs").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
7305
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
7306
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
7307
|
+
else emit(runSyncJournalList({ vault: v.vault }));
|
|
7308
|
+
});
|
|
7309
|
+
syncJournalCmd.command("clear-stale [vault]").description("supersede stale review-required journals when worktree is clean").option("--dry-run", "report what would be cleared without writing", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
7310
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
7311
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
7312
|
+
else if (opts.dryRun) emit(runSyncJournalClearStale({ vault: v.vault, dryRun: true }));
|
|
7313
|
+
else {
|
|
7314
|
+
return emitGuardedVaultWrite(
|
|
7315
|
+
v.vault,
|
|
7316
|
+
"sync journal clear-stale",
|
|
7317
|
+
async () => runSyncJournalClearStale({ vault: v.vault, dryRun: false })
|
|
7318
|
+
);
|
|
7319
|
+
}
|
|
7320
|
+
});
|
|
6238
7321
|
syncCmd.command("lock [vault]").description("acquire advisory lock on vault").option("--summary <text>", "lock description", "skillwiki sync").option("--ttl-minutes <n>", "lock time-to-live in minutes", "30").option("--force", "overwrite existing lock", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6239
7322
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6240
7323
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
@@ -6334,7 +7417,7 @@ program.command("observe [vault]").description("create a raw transcript observat
|
|
|
6334
7417
|
program.command("session-brief [vault]").description("render or refresh the bounded startup session brief").option("--project <slug>", "project slug, or auto for deterministic detection", "auto").option("--write", "write meta/latest-session-brief.md and local cache files", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
6335
7418
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
6336
7419
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
6337
|
-
else if (opts.write) return
|
|
7420
|
+
else if (opts.write) return emitManagedVaultWrite(
|
|
6338
7421
|
v.vault,
|
|
6339
7422
|
"session-brief --write",
|
|
6340
7423
|
() => runSessionBrief({
|