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
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ExitCode,
|
|
4
|
+
FleetManifestSchema,
|
|
5
|
+
err,
|
|
6
|
+
ok
|
|
7
|
+
} from "./chunk-C5OLZRRM.js";
|
|
8
|
+
|
|
9
|
+
// src/utils/dotenv.ts
|
|
10
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
11
|
+
import { dirname } from "path";
|
|
12
|
+
var CONFIG_KEYS = [
|
|
13
|
+
"WIKI_PATH",
|
|
14
|
+
"WIKI_LANG",
|
|
15
|
+
"SKILLWIKI_HOST_ID",
|
|
16
|
+
"AUTO_COMMIT",
|
|
17
|
+
"BACKUP_ENDPOINT",
|
|
18
|
+
"BACKUP_BUCKET",
|
|
19
|
+
"BACKUP_REGION",
|
|
20
|
+
"BACKUP_ACCESS_KEY_ID",
|
|
21
|
+
"BACKUP_SECRET_ACCESS_KEY"
|
|
22
|
+
];
|
|
23
|
+
var _whitelist = new Set(CONFIG_KEYS);
|
|
24
|
+
var PROFILE_PATH_RE = /^WIKI_([A-Z][A-Z0-9_]{0,31})_PATH$/;
|
|
25
|
+
var PROFILE_LANG_RE = /^WIKI_([A-Z][A-Z0-9_]{0,31})_LANG$/;
|
|
26
|
+
var PROFILE_DEFAULT_RE = /^WIKI_DEFAULT$/;
|
|
27
|
+
function isValidWikiProfileKey(key) {
|
|
28
|
+
if (key === "WIKI_PATH" || key === "WIKI_LANG") return false;
|
|
29
|
+
return PROFILE_PATH_RE.test(key) || PROFILE_LANG_RE.test(key) || PROFILE_DEFAULT_RE.test(key);
|
|
30
|
+
}
|
|
31
|
+
function profileKey(name, suffix) {
|
|
32
|
+
return `WIKI_${name.toUpperCase().replace(/-/g, "_").replace(/[^A-Z0-9_]/g, "")}_${suffix}`;
|
|
33
|
+
}
|
|
34
|
+
function parseDotenvText(text) {
|
|
35
|
+
const out = {};
|
|
36
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
37
|
+
const line = rawLine.trim();
|
|
38
|
+
if (line.length === 0 || line.startsWith("#")) continue;
|
|
39
|
+
const eq = line.indexOf("=");
|
|
40
|
+
if (eq <= 0) continue;
|
|
41
|
+
const key = line.slice(0, eq).trim();
|
|
42
|
+
const value = line.slice(eq + 1).trim();
|
|
43
|
+
if (!_whitelist.has(key) && !isValidWikiProfileKey(key)) continue;
|
|
44
|
+
if (value.length === 0) continue;
|
|
45
|
+
out[key] = value;
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
async function parseDotenvFile(path) {
|
|
50
|
+
let text;
|
|
51
|
+
try {
|
|
52
|
+
text = await readFile(path, "utf8");
|
|
53
|
+
} catch {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
return parseDotenvText(text);
|
|
57
|
+
}
|
|
58
|
+
async function writeDotenv(filePath, entries, originalContent) {
|
|
59
|
+
const lines = originalContent !== void 0 ? updateLines(originalContent, entries) : freshLines(entries);
|
|
60
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
61
|
+
await writeFile(filePath, lines.join("\n") + "\n", "utf8");
|
|
62
|
+
}
|
|
63
|
+
function freshLines(entries) {
|
|
64
|
+
const out = [];
|
|
65
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
66
|
+
if (value !== void 0) out.push(`${key}=${value}`);
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
function updateLines(originalContent, entries) {
|
|
71
|
+
let rawLines = originalContent.split(/\r?\n/);
|
|
72
|
+
if (rawLines.length > 0 && rawLines[rawLines.length - 1] === "") {
|
|
73
|
+
rawLines = rawLines.slice(0, -1);
|
|
74
|
+
}
|
|
75
|
+
const keysToWrite = new Set(Object.keys(entries));
|
|
76
|
+
const out = [];
|
|
77
|
+
for (const line of rawLines) {
|
|
78
|
+
const trimmed = line.trim();
|
|
79
|
+
if (trimmed.length === 0 || trimmed.startsWith("#")) {
|
|
80
|
+
out.push(line);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const eq = trimmed.indexOf("=");
|
|
84
|
+
if (eq <= 0) {
|
|
85
|
+
out.push(line);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const key = trimmed.slice(0, eq).trim();
|
|
89
|
+
if (keysToWrite.has(key)) {
|
|
90
|
+
out.push(`${key}=${entries[key]}`);
|
|
91
|
+
keysToWrite.delete(key);
|
|
92
|
+
} else {
|
|
93
|
+
out.push(line);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (const key of keysToWrite) {
|
|
97
|
+
const value = entries[key];
|
|
98
|
+
if (value !== void 0) out.push(`${key}=${value}`);
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/commands/fleet.ts
|
|
104
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
105
|
+
import { hostname as nodeHostname, userInfo } from "os";
|
|
106
|
+
import { join } from "path";
|
|
107
|
+
import yaml from "js-yaml";
|
|
108
|
+
var FLEET_REL_PATH = join("projects", "llm-wiki", "architecture", "fleet.yaml");
|
|
109
|
+
async function runFleetValidate(input) {
|
|
110
|
+
const loaded = await loadFleetManifest(input.file);
|
|
111
|
+
if (!loaded.ok) {
|
|
112
|
+
if (loaded.error === "FILE_NOT_FOUND") {
|
|
113
|
+
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: input.file }) };
|
|
114
|
+
}
|
|
115
|
+
const errors = fleetLoadErrors(loaded);
|
|
116
|
+
return invalidFleet(errors);
|
|
117
|
+
}
|
|
118
|
+
const warnings = fleetWarnings(loaded.manifest);
|
|
119
|
+
const snapshotter = findSnapshotter(loaded.manifest);
|
|
120
|
+
return {
|
|
121
|
+
exitCode: ExitCode.OK,
|
|
122
|
+
result: ok({
|
|
123
|
+
valid: true,
|
|
124
|
+
errors: [],
|
|
125
|
+
warnings,
|
|
126
|
+
host_count: Object.keys(loaded.manifest.hosts).length,
|
|
127
|
+
snapshotter,
|
|
128
|
+
humanHint: `VALID fleet manifest (${Object.keys(loaded.manifest.hosts).length} hosts; snapshotter: ${snapshotter ?? "none"})`
|
|
129
|
+
})
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
async function runFleetContext(input) {
|
|
133
|
+
const env = input.env ?? process.env;
|
|
134
|
+
const home = input.home ?? env.HOME ?? "";
|
|
135
|
+
const cwd = input.cwd ?? process.cwd();
|
|
136
|
+
const osHostname = input.osHostname ?? safeEnvValue(env.HOSTNAME) ?? nodeHostname();
|
|
137
|
+
const user = input.user ?? safeEnvValue(env.USER) ?? safeUserName();
|
|
138
|
+
const vault = input.vault ?? safeEnvValue(env.WIKI_PATH);
|
|
139
|
+
const file = input.file ?? (vault ? join(vault, FLEET_REL_PATH) : void 0);
|
|
140
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
141
|
+
const loaded = file ? await loadFleetManifest(file) : { ok: false, error: "FILE_NOT_FOUND" };
|
|
142
|
+
if (!loaded.ok) {
|
|
143
|
+
const warnings = ["fleet manifest unavailable or invalid"];
|
|
144
|
+
const markdown2 = formatUnknownContext({
|
|
145
|
+
generatedAt,
|
|
146
|
+
osHostname,
|
|
147
|
+
user,
|
|
148
|
+
cwd,
|
|
149
|
+
vault,
|
|
150
|
+
reason: warnings[0],
|
|
151
|
+
trace: [],
|
|
152
|
+
warnings
|
|
153
|
+
});
|
|
154
|
+
return {
|
|
155
|
+
exitCode: ExitCode.OK,
|
|
156
|
+
result: ok({
|
|
157
|
+
manifest_loaded: false,
|
|
158
|
+
generated_at: generatedAt,
|
|
159
|
+
identity_status: "unknown",
|
|
160
|
+
resolver_trace: [],
|
|
161
|
+
warnings,
|
|
162
|
+
markdown: markdown2,
|
|
163
|
+
humanHint: markdown2
|
|
164
|
+
})
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const resolved = await resolveFleetHostId({
|
|
168
|
+
manifest: loaded.manifest,
|
|
169
|
+
hostId: input.hostId,
|
|
170
|
+
env,
|
|
171
|
+
home,
|
|
172
|
+
osHostname
|
|
173
|
+
});
|
|
174
|
+
if (resolved.hostId && !loaded.manifest.hosts[resolved.hostId]) {
|
|
175
|
+
const source = resolved.source ?? "unknown";
|
|
176
|
+
const warnings = [`resolved host id \`${resolved.hostId}\` from ${source} is not in fleet.yaml`];
|
|
177
|
+
const markdown2 = formatInvalidContext({
|
|
178
|
+
generatedAt,
|
|
179
|
+
hostId: resolved.hostId,
|
|
180
|
+
source,
|
|
181
|
+
osHostname,
|
|
182
|
+
user,
|
|
183
|
+
cwd,
|
|
184
|
+
vault,
|
|
185
|
+
trace: resolved.trace,
|
|
186
|
+
warnings
|
|
187
|
+
});
|
|
188
|
+
return {
|
|
189
|
+
exitCode: ExitCode.OK,
|
|
190
|
+
result: ok({
|
|
191
|
+
manifest_loaded: true,
|
|
192
|
+
host_id: resolved.hostId,
|
|
193
|
+
source: resolved.source,
|
|
194
|
+
generated_at: generatedAt,
|
|
195
|
+
identity_status: "invalid",
|
|
196
|
+
resolver_trace: resolved.trace,
|
|
197
|
+
warnings,
|
|
198
|
+
markdown: markdown2,
|
|
199
|
+
humanHint: markdown2
|
|
200
|
+
})
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
if (!resolved.hostId) {
|
|
204
|
+
const warnings = ["host identity is unresolved"];
|
|
205
|
+
const markdown2 = formatUnknownContext({
|
|
206
|
+
generatedAt,
|
|
207
|
+
osHostname,
|
|
208
|
+
user,
|
|
209
|
+
cwd,
|
|
210
|
+
vault,
|
|
211
|
+
reason: warnings[0],
|
|
212
|
+
trace: resolved.trace,
|
|
213
|
+
warnings
|
|
214
|
+
});
|
|
215
|
+
return {
|
|
216
|
+
exitCode: ExitCode.OK,
|
|
217
|
+
result: ok({
|
|
218
|
+
manifest_loaded: true,
|
|
219
|
+
generated_at: generatedAt,
|
|
220
|
+
identity_status: "unknown",
|
|
221
|
+
resolver_trace: resolved.trace,
|
|
222
|
+
warnings,
|
|
223
|
+
markdown: markdown2,
|
|
224
|
+
humanHint: markdown2
|
|
225
|
+
})
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
const markdown = formatKnownContext({
|
|
229
|
+
manifest: loaded.manifest,
|
|
230
|
+
hostId: resolved.hostId,
|
|
231
|
+
source: resolved.source,
|
|
232
|
+
generatedAt,
|
|
233
|
+
osHostname,
|
|
234
|
+
user,
|
|
235
|
+
cwd,
|
|
236
|
+
vault,
|
|
237
|
+
trace: resolved.trace
|
|
238
|
+
});
|
|
239
|
+
return {
|
|
240
|
+
exitCode: ExitCode.OK,
|
|
241
|
+
result: ok({
|
|
242
|
+
manifest_loaded: true,
|
|
243
|
+
host_id: resolved.hostId,
|
|
244
|
+
source: resolved.source,
|
|
245
|
+
generated_at: generatedAt,
|
|
246
|
+
identity_status: "known",
|
|
247
|
+
resolver_trace: resolved.trace,
|
|
248
|
+
warnings: [],
|
|
249
|
+
markdown,
|
|
250
|
+
humanHint: markdown
|
|
251
|
+
})
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function fleetContextEnv(input) {
|
|
255
|
+
const env = input.env ?? process.env;
|
|
256
|
+
const home = input.home ?? env.HOME ?? "";
|
|
257
|
+
const osHostname = input.osHostname ?? safeEnvValue(env.HOSTNAME) ?? nodeHostname();
|
|
258
|
+
const vault = input.vault ?? safeEnvValue(env.WIKI_PATH);
|
|
259
|
+
const file = input.file ?? (vault ? join(vault, FLEET_REL_PATH) : void 0);
|
|
260
|
+
return { env, home, osHostname, vault, file };
|
|
261
|
+
}
|
|
262
|
+
async function loadFleetManifestAndHost(input) {
|
|
263
|
+
const { env, home, osHostname, file } = fleetContextEnv(input);
|
|
264
|
+
if (!file) return null;
|
|
265
|
+
const loaded = await loadFleetManifest(file);
|
|
266
|
+
if (!loaded.ok) return null;
|
|
267
|
+
const resolved = await resolveFleetHostId({
|
|
268
|
+
manifest: loaded.manifest,
|
|
269
|
+
hostId: input.hostId,
|
|
270
|
+
env,
|
|
271
|
+
home,
|
|
272
|
+
osHostname
|
|
273
|
+
});
|
|
274
|
+
if (!resolved.hostId) {
|
|
275
|
+
return {
|
|
276
|
+
manifest: loaded.manifest,
|
|
277
|
+
hostId: void 0,
|
|
278
|
+
source: resolved.source,
|
|
279
|
+
warnings: ["host identity is unresolved"],
|
|
280
|
+
identityStatus: "unknown"
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
if (!loaded.manifest.hosts[resolved.hostId]) {
|
|
284
|
+
const source = resolved.source ?? "unknown";
|
|
285
|
+
return {
|
|
286
|
+
manifest: loaded.manifest,
|
|
287
|
+
hostId: resolved.hostId,
|
|
288
|
+
source: resolved.source,
|
|
289
|
+
warnings: [`resolved host id \`${resolved.hostId}\` from ${source} is not in fleet.yaml`],
|
|
290
|
+
identityStatus: "invalid"
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
manifest: loaded.manifest,
|
|
295
|
+
hostId: resolved.hostId,
|
|
296
|
+
source: resolved.source,
|
|
297
|
+
warnings: [],
|
|
298
|
+
identityStatus: "known"
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function snapshotterAliasForLocalHost(fleetLoad) {
|
|
302
|
+
if (!fleetLoad?.manifest || !fleetLoad.hostId) return void 0;
|
|
303
|
+
const snapshotterId = Object.entries(fleetLoad.manifest.hosts).find(([, h]) => h.role === "snapshotter")?.[0];
|
|
304
|
+
if (!snapshotterId) return void 0;
|
|
305
|
+
const profile = fleetLoad.manifest.hosts[snapshotterId]?.access?.from?.[fleetLoad.hostId];
|
|
306
|
+
if (!profile || profile.status !== "configured" && profile.status !== "local") return void 0;
|
|
307
|
+
const aliases = profile.ssh_aliases ?? [];
|
|
308
|
+
return aliases.length > 0 ? aliases[0] : void 0;
|
|
309
|
+
}
|
|
310
|
+
function satelliteGateFromFleetLoad(load) {
|
|
311
|
+
if (!load?.hostId) return { satelliteExpected: false };
|
|
312
|
+
const host = load.manifest.hosts[load.hostId];
|
|
313
|
+
if (!host) return { satelliteExpected: false };
|
|
314
|
+
return { satelliteExpected: host.maintenance?.skillwiki_satellite?.enabled === true };
|
|
315
|
+
}
|
|
316
|
+
async function loadFleetManifest(file) {
|
|
317
|
+
let text;
|
|
318
|
+
try {
|
|
319
|
+
text = await readFile2(file, "utf8");
|
|
320
|
+
} catch {
|
|
321
|
+
return { ok: false, error: "FILE_NOT_FOUND" };
|
|
322
|
+
}
|
|
323
|
+
let parsed;
|
|
324
|
+
try {
|
|
325
|
+
parsed = yaml.load(text, { schema: yaml.JSON_SCHEMA });
|
|
326
|
+
} catch (error) {
|
|
327
|
+
return { ok: false, error: "INVALID_YAML", detail: error instanceof Error ? error.message : String(error) };
|
|
328
|
+
}
|
|
329
|
+
const result = FleetManifestSchema.safeParse(parsed);
|
|
330
|
+
if (!result.success) {
|
|
331
|
+
return { ok: false, error: "INVALID_FLEET_MANIFEST", detail: result.error.issues };
|
|
332
|
+
}
|
|
333
|
+
return { ok: true, manifest: result.data };
|
|
334
|
+
}
|
|
335
|
+
function invalidFleet(errors) {
|
|
336
|
+
return {
|
|
337
|
+
exitCode: ExitCode.FLEET_MANIFEST_INVALID,
|
|
338
|
+
result: ok({
|
|
339
|
+
valid: false,
|
|
340
|
+
errors,
|
|
341
|
+
warnings: [],
|
|
342
|
+
host_count: 0,
|
|
343
|
+
humanHint: `INVALID fleet manifest
|
|
344
|
+
${errors.map((e) => ` ${e.path || "(root)"}: ${e.message}`).join("\n")}`
|
|
345
|
+
})
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function fleetLoadErrors(loaded) {
|
|
349
|
+
if (loaded.error === "INVALID_YAML") {
|
|
350
|
+
return [{ path: "", message: `invalid YAML: ${String(loaded.detail ?? "parse failed")}` }];
|
|
351
|
+
}
|
|
352
|
+
if (loaded.error === "INVALID_FLEET_MANIFEST" && Array.isArray(loaded.detail)) {
|
|
353
|
+
return loaded.detail.map((issue) => {
|
|
354
|
+
const zodIssue = issue;
|
|
355
|
+
return {
|
|
356
|
+
path: (zodIssue.path ?? []).join("."),
|
|
357
|
+
message: zodIssue.message ?? "invalid value"
|
|
358
|
+
};
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return [{ path: "", message: loaded.error }];
|
|
362
|
+
}
|
|
363
|
+
function fleetWarnings(manifest) {
|
|
364
|
+
const warnings = [];
|
|
365
|
+
for (const [id, host] of Object.entries(manifest.hosts)) {
|
|
366
|
+
if (host.role === "snapshotter" && host.protected !== true) {
|
|
367
|
+
warnings.push(`snapshotter host '${id}' is not protected=true`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return warnings;
|
|
371
|
+
}
|
|
372
|
+
function findSnapshotter(manifest) {
|
|
373
|
+
return Object.entries(manifest.hosts).find(([, host]) => host.role === "snapshotter")?.[0];
|
|
374
|
+
}
|
|
375
|
+
async function resolveFleetHostId(input) {
|
|
376
|
+
const trace = [];
|
|
377
|
+
if (input.hostId) {
|
|
378
|
+
trace.push({ source: "--host-id", status: "matched", value: input.hostId });
|
|
379
|
+
return { hostId: input.hostId, source: "host-id", trace };
|
|
380
|
+
}
|
|
381
|
+
trace.push({ source: "--host-id", status: "unset" });
|
|
382
|
+
if (input.env.SKILLWIKI_HOST_ID) {
|
|
383
|
+
trace.push({ source: "SKILLWIKI_HOST_ID", status: "matched", value: input.env.SKILLWIKI_HOST_ID });
|
|
384
|
+
return { hostId: input.env.SKILLWIKI_HOST_ID, source: "SKILLWIKI_HOST_ID", trace };
|
|
385
|
+
}
|
|
386
|
+
trace.push({ source: "SKILLWIKI_HOST_ID", status: "unset" });
|
|
387
|
+
if (input.env.AGENT_HOST_ID) {
|
|
388
|
+
trace.push({ source: "AGENT_HOST_ID", status: "matched", value: input.env.AGENT_HOST_ID });
|
|
389
|
+
return { hostId: input.env.AGENT_HOST_ID, source: "AGENT_HOST_ID", trace };
|
|
390
|
+
}
|
|
391
|
+
trace.push({ source: "AGENT_HOST_ID", status: "unset" });
|
|
392
|
+
if (input.home) {
|
|
393
|
+
const dotenv = await parseDotenvFile(join(input.home, ".skillwiki", ".env"));
|
|
394
|
+
if (dotenv.SKILLWIKI_HOST_ID) {
|
|
395
|
+
trace.push({ source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID", status: "matched", value: dotenv.SKILLWIKI_HOST_ID });
|
|
396
|
+
return { hostId: dotenv.SKILLWIKI_HOST_ID, source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID", trace };
|
|
397
|
+
}
|
|
398
|
+
trace.push({ source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID", status: "unset" });
|
|
399
|
+
} else {
|
|
400
|
+
trace.push({ source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID", status: "skipped" });
|
|
401
|
+
}
|
|
402
|
+
if (input.env.VS_HOSTNAME) {
|
|
403
|
+
trace.push({ source: "VS_HOSTNAME", status: "matched", value: input.env.VS_HOSTNAME });
|
|
404
|
+
return { hostId: input.env.VS_HOSTNAME, source: "VS_HOSTNAME", trace };
|
|
405
|
+
}
|
|
406
|
+
trace.push({ source: "VS_HOSTNAME", status: "unset" });
|
|
407
|
+
const hostname = input.osHostname.trim();
|
|
408
|
+
if (hostname) {
|
|
409
|
+
if (input.manifest.hosts[hostname]) {
|
|
410
|
+
trace.push({ source: "hostname", status: "matched", value: hostname });
|
|
411
|
+
return { hostId: hostname, source: "hostname", trace };
|
|
412
|
+
}
|
|
413
|
+
const byHostname = Object.entries(input.manifest.hosts).find(([, host]) => host.identity.hostnames.includes(hostname));
|
|
414
|
+
if (byHostname) {
|
|
415
|
+
trace.push({ source: "hostname", status: "matched", value: hostname });
|
|
416
|
+
return { hostId: byHostname[0], source: "hostname", trace };
|
|
417
|
+
}
|
|
418
|
+
trace.push({ source: "hostname", status: "unmatched", value: hostname });
|
|
419
|
+
} else {
|
|
420
|
+
trace.push({ source: "hostname", status: "unset" });
|
|
421
|
+
}
|
|
422
|
+
return { trace };
|
|
423
|
+
}
|
|
424
|
+
function formatKnownContext(input) {
|
|
425
|
+
const host = input.manifest.hosts[input.hostId];
|
|
426
|
+
const protectedValue = host.protected === true ? "true" : "false";
|
|
427
|
+
const writesTo = host.writes_to.join(", ");
|
|
428
|
+
const selfAliases = collectSelfAliases(input.manifest, input.hostId);
|
|
429
|
+
const outbound = collectOutboundAccess(input.manifest, input.hostId);
|
|
430
|
+
const maintenanceLines = formatMaintenanceLines(host);
|
|
431
|
+
const guidance = host.role === "snapshotter" && host.protected === true ? `this session is already on \`${input.hostId}\`; this is a protected snapshotter host. Live-vault authoring at the resolved \`skillwiki path\` is allowed here. Do not mutate snapshot worktrees or repo-local project workspaces from this session except explicitly approved snapshot maintenance. Keep release-validation workflows read-only when they are documented as such.` : input.hostId === "macos-dev" ? "use declared SSH aliases for remote work when needed; do not assume undeclared hosts have reciprocal SSH access." : `this session is already on \`${input.hostId}\`; do not SSH to self aliases unless the user explicitly asks. Do not assume outbound SSH to other fleet hosts is configured.`;
|
|
432
|
+
return [
|
|
433
|
+
"## Runtime Host Context",
|
|
434
|
+
"",
|
|
435
|
+
`- Context generated: \`${input.generatedAt}\``,
|
|
436
|
+
`- Current machine: \`${input.hostId}\`${input.source ? ` (source: \`${input.source}\`)` : ""}`,
|
|
437
|
+
"- Identity status: `known`",
|
|
438
|
+
`- Identity resolution: ${formatResolution(input.source, input.hostId)}`,
|
|
439
|
+
`- Resolver trace: ${formatTrace(input.trace)}`,
|
|
440
|
+
`- OS hostname: ${formatMaybe(input.osHostname)}`,
|
|
441
|
+
`- User: ${formatMaybe(input.user)}`,
|
|
442
|
+
`- Workspace: ${formatMaybe(input.cwd)}`,
|
|
443
|
+
`- Vault: ${formatMaybe(input.vault)}`,
|
|
444
|
+
"- Remote freshness: not checked by `fleet context`; run `sync status` or presync before host-sensitive work.",
|
|
445
|
+
`- Fleet role: \`${host.role}\`; protected: \`${protectedValue}\`; writes_to: \`${writesTo}\``,
|
|
446
|
+
...maintenanceLines,
|
|
447
|
+
`- Self SSH aliases known in fleet: ${formatList(selfAliases)}`,
|
|
448
|
+
`- Declared outbound SSH from this source: ${formatOutboundAccess(outbound)}`,
|
|
449
|
+
`- Guidance: ${guidance}`
|
|
450
|
+
].join("\n");
|
|
451
|
+
}
|
|
452
|
+
function formatUnknownContext(input) {
|
|
453
|
+
return [
|
|
454
|
+
"## Runtime Host Context",
|
|
455
|
+
"",
|
|
456
|
+
`- Context generated: \`${input.generatedAt}\``,
|
|
457
|
+
"- Current machine: unknown",
|
|
458
|
+
"- Identity status: `unknown`",
|
|
459
|
+
`- Resolver trace: ${formatTrace(input.trace)}`,
|
|
460
|
+
`- Warnings: ${formatWarnings(input.warnings)}`,
|
|
461
|
+
`- OS hostname: ${formatMaybe(input.osHostname)}`,
|
|
462
|
+
`- User: ${formatMaybe(input.user)}`,
|
|
463
|
+
`- Workspace: ${formatMaybe(input.cwd)}`,
|
|
464
|
+
`- Vault: ${formatMaybe(input.vault)}`,
|
|
465
|
+
"- Remote freshness: not checked by `fleet context`; run `sync status` or presync before host-sensitive work.",
|
|
466
|
+
"- Fleet role: unknown",
|
|
467
|
+
"- Self SSH aliases known in fleet: unknown",
|
|
468
|
+
"- Declared outbound SSH from this source: unknown",
|
|
469
|
+
`- Guidance: ${input.reason}; do not assume local vs remote role. Inspect runtime or ask before SSH/deploy/sync work.`
|
|
470
|
+
].join("\n");
|
|
471
|
+
}
|
|
472
|
+
function formatInvalidContext(input) {
|
|
473
|
+
return [
|
|
474
|
+
"## Runtime Host Context",
|
|
475
|
+
"",
|
|
476
|
+
`- Context generated: \`${input.generatedAt}\``,
|
|
477
|
+
"- Current machine: unknown",
|
|
478
|
+
"- Identity status: `invalid`",
|
|
479
|
+
`- Identity resolution: ${formatResolution(input.source, input.hostId)}`,
|
|
480
|
+
`- Resolver trace: ${formatTrace(input.trace)}`,
|
|
481
|
+
`- Warnings: ${formatWarnings(input.warnings)}`,
|
|
482
|
+
`- OS hostname: ${formatMaybe(input.osHostname)}`,
|
|
483
|
+
`- User: ${formatMaybe(input.user)}`,
|
|
484
|
+
`- Workspace: ${formatMaybe(input.cwd)}`,
|
|
485
|
+
`- Vault: ${formatMaybe(input.vault)}`,
|
|
486
|
+
"- Remote freshness: not checked by `fleet context`; run `sync status` or presync before host-sensitive work.",
|
|
487
|
+
"- Fleet role: unknown",
|
|
488
|
+
"- Self SSH aliases known in fleet: unknown",
|
|
489
|
+
"- Declared outbound SSH from this source: unknown",
|
|
490
|
+
`- Guidance: do not trust this identity; rerun with \`--host-id\` only if the user confirms \`${input.hostId}\` is the current fleet host id.`
|
|
491
|
+
].join("\n");
|
|
492
|
+
}
|
|
493
|
+
function collectSelfAliases(manifest, hostId) {
|
|
494
|
+
const aliases = [];
|
|
495
|
+
const host = manifest.hosts[hostId];
|
|
496
|
+
const access = host?.access?.from ?? {};
|
|
497
|
+
for (const profile of Object.values(access)) {
|
|
498
|
+
for (const alias of profile.ssh_aliases ?? []) aliases.push(alias);
|
|
499
|
+
}
|
|
500
|
+
return [...new Set(aliases)];
|
|
501
|
+
}
|
|
502
|
+
function collectOutboundAccess(manifest, sourceHostId) {
|
|
503
|
+
const hosts = [];
|
|
504
|
+
for (const [targetId, target] of Object.entries(manifest.hosts)) {
|
|
505
|
+
if (targetId === sourceHostId) continue;
|
|
506
|
+
const profile = target.access?.from?.[sourceHostId];
|
|
507
|
+
if (profile && (profile.status === "configured" || profile.status === "local")) {
|
|
508
|
+
hosts.push({
|
|
509
|
+
hostId: targetId,
|
|
510
|
+
sshAliases: [...new Set(profile.ssh_aliases ?? [])],
|
|
511
|
+
users: [...new Set(profile.users ?? [])]
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return hosts.sort((left, right) => left.hostId.localeCompare(right.hostId));
|
|
516
|
+
}
|
|
517
|
+
function formatMaintenanceLines(host) {
|
|
518
|
+
const satellite = host.maintenance?.skillwiki_satellite;
|
|
519
|
+
if (!satellite?.enabled) return [];
|
|
520
|
+
return [
|
|
521
|
+
`- Maintenance role: \`skillwiki satellite\`; user: \`${satellite.user}\`; ssh: \`${satellite.ssh_alias}\``,
|
|
522
|
+
`- Maintenance paths: maintenance vault: \`${satellite.vault_path}\`; repo: \`${satellite.repo_path}\`; scheduler: \`${satellite.scheduler}\`; jobs: ${formatList(satellite.jobs)}`
|
|
523
|
+
];
|
|
524
|
+
}
|
|
525
|
+
function formatOutboundAccess(values) {
|
|
526
|
+
if (values.length === 0) return "none";
|
|
527
|
+
return values.map((value) => {
|
|
528
|
+
const aliasPart = value.sshAliases.length > 0 ? ` via ${formatList(value.sshAliases)}` : " (no SSH aliases)";
|
|
529
|
+
const usersPart = value.users.length > 0 ? ` (users: ${formatList(value.users)})` : "";
|
|
530
|
+
return `\`${value.hostId}\`${aliasPart}${usersPart}`;
|
|
531
|
+
}).join("; ");
|
|
532
|
+
}
|
|
533
|
+
function formatResolution(source, hostId) {
|
|
534
|
+
return source ? `\`${source === "host-id" ? "--host-id" : source}\` -> \`${hostId}\`` : `unknown -> \`${hostId}\``;
|
|
535
|
+
}
|
|
536
|
+
function formatTrace(values) {
|
|
537
|
+
if (values.length === 0) return "not available";
|
|
538
|
+
return values.map((value) => {
|
|
539
|
+
const source = `\`${value.source}\``;
|
|
540
|
+
if (value.status === "matched") return `${source} matched \`${value.value ?? ""}\``;
|
|
541
|
+
if (value.status === "unmatched") return `${source} unmatched \`${value.value ?? ""}\``;
|
|
542
|
+
return `${source} ${value.status}`;
|
|
543
|
+
}).join("; ");
|
|
544
|
+
}
|
|
545
|
+
function formatWarnings(values) {
|
|
546
|
+
return values.length > 0 ? values.join("; ") : "none";
|
|
547
|
+
}
|
|
548
|
+
function formatList(values) {
|
|
549
|
+
return values.length > 0 ? values.map((v) => `\`${v}\``).join(", ") : "none";
|
|
550
|
+
}
|
|
551
|
+
function formatMaybe(value) {
|
|
552
|
+
return value && value.trim().length > 0 ? `\`${value}\`` : "unknown";
|
|
553
|
+
}
|
|
554
|
+
function safeEnvValue(value) {
|
|
555
|
+
return value && value.trim().length > 0 ? value : void 0;
|
|
556
|
+
}
|
|
557
|
+
function safeUserName() {
|
|
558
|
+
try {
|
|
559
|
+
return userInfo().username;
|
|
560
|
+
} catch {
|
|
561
|
+
return "";
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// src/utils/git.ts
|
|
566
|
+
import { execFileSync } from "child_process";
|
|
567
|
+
function git(cwd, args) {
|
|
568
|
+
try {
|
|
569
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
570
|
+
} catch {
|
|
571
|
+
return "";
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function gitStrict(cwd, args) {
|
|
575
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// src/utils/operation-journal.ts
|
|
579
|
+
import {
|
|
580
|
+
existsSync,
|
|
581
|
+
mkdirSync,
|
|
582
|
+
readdirSync,
|
|
583
|
+
readFileSync,
|
|
584
|
+
renameSync,
|
|
585
|
+
writeFileSync
|
|
586
|
+
} from "fs";
|
|
587
|
+
import { join as join2 } from "path";
|
|
588
|
+
function journalDir(vault) {
|
|
589
|
+
const gitPath = git(vault, ["rev-parse", "--git-path", "vault-sync/operations"]);
|
|
590
|
+
if (!gitPath) return null;
|
|
591
|
+
return gitPath.startsWith("/") ? gitPath : join2(vault, gitPath);
|
|
592
|
+
}
|
|
593
|
+
function parseJournalEnv(text) {
|
|
594
|
+
return Object.fromEntries(
|
|
595
|
+
text.split("\n").filter((line) => line.includes("=")).map((line) => {
|
|
596
|
+
const i = line.indexOf("=");
|
|
597
|
+
return [line.slice(0, i), line.slice(i + 1)];
|
|
598
|
+
})
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
function serializeJournalEnv(fields, preferredOrder = []) {
|
|
602
|
+
const keys = [...preferredOrder.filter((k) => k in fields), ...Object.keys(fields).filter((k) => !preferredOrder.includes(k))];
|
|
603
|
+
const seen = /* @__PURE__ */ new Set();
|
|
604
|
+
const lines = [];
|
|
605
|
+
for (const k of keys) {
|
|
606
|
+
if (seen.has(k)) continue;
|
|
607
|
+
seen.add(k);
|
|
608
|
+
lines.push(`${k}=${fields[k]}`);
|
|
609
|
+
}
|
|
610
|
+
return lines.join("\n") + "\n";
|
|
611
|
+
}
|
|
612
|
+
function readJournal(vault, opId) {
|
|
613
|
+
const dir = journalDir(vault);
|
|
614
|
+
if (!dir) return null;
|
|
615
|
+
const path = join2(dir, `${opId}.env`);
|
|
616
|
+
if (!existsSync(path)) return null;
|
|
617
|
+
try {
|
|
618
|
+
return parseJournalEnv(readFileSync(path, "utf8"));
|
|
619
|
+
} catch {
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
function writeJournal(vault, opId, fields) {
|
|
624
|
+
const dir = journalDir(vault);
|
|
625
|
+
if (!dir) return false;
|
|
626
|
+
try {
|
|
627
|
+
mkdirSync(dir, { recursive: true });
|
|
628
|
+
const path = join2(dir, `${opId}.env`);
|
|
629
|
+
const tmp = `${path}.tmp.${process.pid}`;
|
|
630
|
+
const order = [
|
|
631
|
+
"operation_id",
|
|
632
|
+
"phase",
|
|
633
|
+
"retry_count",
|
|
634
|
+
"original_branch",
|
|
635
|
+
"original_head",
|
|
636
|
+
"target_oid",
|
|
637
|
+
"owned_stash_oid",
|
|
638
|
+
"preservation_scope",
|
|
639
|
+
"lock_identity",
|
|
640
|
+
"helper_version",
|
|
641
|
+
"deployed_runtime_hash",
|
|
642
|
+
"conflict_identity",
|
|
643
|
+
"handoff",
|
|
644
|
+
"reason",
|
|
645
|
+
"prior_reason",
|
|
646
|
+
"superseded_at",
|
|
647
|
+
"cleared_reason",
|
|
648
|
+
"cleared_by",
|
|
649
|
+
"worktree_path",
|
|
650
|
+
"worktree_git_dir",
|
|
651
|
+
"inventory_path"
|
|
652
|
+
];
|
|
653
|
+
writeFileSync(tmp, serializeJournalEnv(fields, order), "utf8");
|
|
654
|
+
renameSync(tmp, path);
|
|
655
|
+
return true;
|
|
656
|
+
} catch {
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
function listJournalOpIds(vault) {
|
|
661
|
+
const dir = journalDir(vault);
|
|
662
|
+
if (!dir || !existsSync(dir)) return [];
|
|
663
|
+
try {
|
|
664
|
+
return readdirSync(dir).filter((f) => f.endsWith(".env")).map((f) => f.replace(/\.env$/, "")).sort();
|
|
665
|
+
} catch {
|
|
666
|
+
return [];
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
function listReviewRequiredOps(vault) {
|
|
670
|
+
const currentGitDir = git(vault, ["rev-parse", "--absolute-git-dir"]);
|
|
671
|
+
const out = [];
|
|
672
|
+
for (const opId of listJournalOpIds(vault)) {
|
|
673
|
+
const fields = readJournal(vault, opId);
|
|
674
|
+
if (!fields) continue;
|
|
675
|
+
if (fields.phase !== "review-required" || fields.handoff !== "1") continue;
|
|
676
|
+
const journalGitDir = fields.worktree_git_dir ?? "";
|
|
677
|
+
if (!journalGitDir || !currentGitDir || journalGitDir === currentGitDir) {
|
|
678
|
+
out.push({ opId, fields });
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return out;
|
|
682
|
+
}
|
|
683
|
+
function findReviewRequiredOp(vault) {
|
|
684
|
+
return listReviewRequiredOps(vault)[0]?.opId;
|
|
685
|
+
}
|
|
686
|
+
function hasUnmergedPaths(vault) {
|
|
687
|
+
const unmergedRaw = git(vault, ["diff", "--name-only", "--diff-filter=U"]);
|
|
688
|
+
return unmergedRaw ? unmergedRaw.split("\n").map((s) => s.trim()).filter(Boolean) : [];
|
|
689
|
+
}
|
|
690
|
+
function hasActiveGitSequencer(vault) {
|
|
691
|
+
const gitDir = git(vault, ["rev-parse", "--absolute-git-dir"]);
|
|
692
|
+
if (!gitDir) return false;
|
|
693
|
+
for (const m of ["MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD"]) {
|
|
694
|
+
if (existsSync(join2(gitDir, m))) return true;
|
|
695
|
+
}
|
|
696
|
+
if (existsSync(join2(gitDir, "rebase-merge")) || existsSync(join2(gitDir, "rebase-apply"))) {
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
function isWorktreeClean(vault) {
|
|
702
|
+
const porcelain = git(vault, ["status", "--porcelain"]);
|
|
703
|
+
return !porcelain || porcelain.trim() === "";
|
|
704
|
+
}
|
|
705
|
+
function canSupersedeJournal(vault, fields) {
|
|
706
|
+
if (hasUnmergedPaths(vault).length > 0) return false;
|
|
707
|
+
if (hasActiveGitSequencer(vault)) return false;
|
|
708
|
+
if (!isWorktreeClean(vault)) return false;
|
|
709
|
+
const target = fields.target_oid?.trim();
|
|
710
|
+
if (!target) return false;
|
|
711
|
+
const head = git(vault, ["rev-parse", "HEAD"]);
|
|
712
|
+
if (!head) return false;
|
|
713
|
+
return gitMergeBaseIsAncestor(vault, target, head);
|
|
714
|
+
}
|
|
715
|
+
function gitMergeBaseIsAncestor(vault, ancestor, tip) {
|
|
716
|
+
if (ancestor === tip) return true;
|
|
717
|
+
const mb = git(vault, ["merge-base", ancestor, tip]);
|
|
718
|
+
return mb !== "" && mb === ancestor;
|
|
719
|
+
}
|
|
720
|
+
function markJournalSuperseded(vault, opId, fields, by) {
|
|
721
|
+
const next = { ...fields };
|
|
722
|
+
if (next.reason && next.reason !== "superseded-stale-review-required") {
|
|
723
|
+
next.prior_reason = next.prior_reason || next.reason;
|
|
724
|
+
}
|
|
725
|
+
next.phase = "complete";
|
|
726
|
+
next.reason = "superseded-stale-review-required";
|
|
727
|
+
next.superseded_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
728
|
+
next.cleared_by = by;
|
|
729
|
+
next.cleared_reason = `operator-or-preflight ${next.superseded_at}`;
|
|
730
|
+
if (!next.handoff) next.handoff = "1";
|
|
731
|
+
if (!next.operation_id) next.operation_id = opId;
|
|
732
|
+
return writeJournal(vault, opId, next);
|
|
733
|
+
}
|
|
734
|
+
function supersedeStaleReviewRequiredJournals(vault, opts = {}) {
|
|
735
|
+
const by = opts.by ?? "skillwiki-preflight";
|
|
736
|
+
const superseded = [];
|
|
737
|
+
const skipped = [];
|
|
738
|
+
if (hasUnmergedPaths(vault).length > 0 || hasActiveGitSequencer(vault) || !isWorktreeClean(vault)) {
|
|
739
|
+
for (const { opId } of listReviewRequiredOps(vault)) skipped.push(opId);
|
|
740
|
+
return { superseded, skipped };
|
|
741
|
+
}
|
|
742
|
+
for (const { opId, fields } of listReviewRequiredOps(vault)) {
|
|
743
|
+
if (!canSupersedeJournal(vault, fields)) {
|
|
744
|
+
skipped.push(opId);
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
if (opts.dryRun) {
|
|
748
|
+
superseded.push(opId);
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
if (markJournalSuperseded(vault, opId, fields, by)) {
|
|
752
|
+
superseded.push(opId);
|
|
753
|
+
} else {
|
|
754
|
+
skipped.push(opId);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return { superseded, skipped };
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// src/utils/vault-sync-helper.ts
|
|
761
|
+
import { spawnSync } from "child_process";
|
|
762
|
+
import { existsSync as existsSync2 } from "fs";
|
|
763
|
+
import { homedir, platform } from "os";
|
|
764
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
765
|
+
import { fileURLToPath } from "url";
|
|
766
|
+
var HELPER_NAME = "wiki-pull-with-auto-resolve.sh";
|
|
767
|
+
function candidateHelperPaths(input = { vault: "" }) {
|
|
768
|
+
const env = input.env ?? process.env;
|
|
769
|
+
const paths = [];
|
|
770
|
+
if (input.helperPath) paths.push(input.helperPath);
|
|
771
|
+
if (env.SKILLWIKI_VAULT_SYNC_PULL_HELPER) paths.push(env.SKILLWIKI_VAULT_SYNC_PULL_HELPER);
|
|
772
|
+
let here = input.moduleDir;
|
|
773
|
+
if (!here) {
|
|
774
|
+
try {
|
|
775
|
+
here = dirname2(fileURLToPath(import.meta.url));
|
|
776
|
+
} catch {
|
|
777
|
+
here = void 0;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
if (here) {
|
|
781
|
+
paths.push(join3(here, "vault-sync", "scripts", HELPER_NAME));
|
|
782
|
+
paths.push(join3(here, "..", "vault-sync", "scripts", HELPER_NAME));
|
|
783
|
+
paths.push(join3(here, "..", "..", "vault-sync", "scripts", HELPER_NAME));
|
|
784
|
+
paths.push(join3(here, "..", "..", "..", "vault-sync", "scripts", HELPER_NAME));
|
|
785
|
+
}
|
|
786
|
+
const home = input.home ?? env.HOME ?? env.USERPROFILE ?? (() => {
|
|
787
|
+
try {
|
|
788
|
+
return homedir();
|
|
789
|
+
} catch {
|
|
790
|
+
return void 0;
|
|
791
|
+
}
|
|
792
|
+
})();
|
|
793
|
+
if (home) {
|
|
794
|
+
const xdg = env.XDG_DATA_HOME;
|
|
795
|
+
const isDarwin = platform() === "darwin";
|
|
796
|
+
if (isDarwin) {
|
|
797
|
+
paths.push(join3(home, "Library", "Application Support", "vault-sync", "bin", HELPER_NAME));
|
|
798
|
+
}
|
|
799
|
+
paths.push(join3(xdg || join3(home, ".local", "share"), "vault-sync", "bin", HELPER_NAME));
|
|
800
|
+
if (!isDarwin) {
|
|
801
|
+
paths.push(join3(home, "Library", "Application Support", "vault-sync", "bin", HELPER_NAME));
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return paths;
|
|
805
|
+
}
|
|
806
|
+
function resolveVaultSyncPullHelper(input) {
|
|
807
|
+
for (const p of candidateHelperPaths(input)) {
|
|
808
|
+
if (p && existsSync2(p)) return p;
|
|
809
|
+
}
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
async function runVaultSyncPullHelper(input) {
|
|
813
|
+
const helperPath = resolveVaultSyncPullHelper(input);
|
|
814
|
+
if (!helperPath) {
|
|
815
|
+
const tried = candidateHelperPaths(input).filter(Boolean);
|
|
816
|
+
return err("GIT_PULL_FAILED", {
|
|
817
|
+
message: "canonical vault-sync pull helper not found; run skillwiki doctor; install skillwiki@0.10.1+ or set SKILLWIKI_VAULT_SYNC_PULL_HELPER; host install: ~/Library/Application Support/vault-sync/bin or ~/.local/share/vault-sync/bin",
|
|
818
|
+
tried_paths: tried.slice(0, 12)
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
const remote = input.remote ?? "origin";
|
|
822
|
+
const branch = input.branch ?? "main";
|
|
823
|
+
const beforeOid = git(input.vault, ["rev-parse", "HEAD"]);
|
|
824
|
+
if (!beforeOid) {
|
|
825
|
+
return err("GIT_PULL_FAILED", { message: "could not read HEAD before pull" });
|
|
826
|
+
}
|
|
827
|
+
const env = {
|
|
828
|
+
...process.env,
|
|
829
|
+
...input.env ?? {},
|
|
830
|
+
WIKI_DIR: input.vault
|
|
831
|
+
};
|
|
832
|
+
if (input.lockToken) {
|
|
833
|
+
env.VAULT_SYNC_MANAGED_LOCK_TOKEN = input.lockToken;
|
|
834
|
+
}
|
|
835
|
+
const result = spawnSync("bash", [helperPath, remote, branch], {
|
|
836
|
+
env,
|
|
837
|
+
encoding: "utf8",
|
|
838
|
+
cwd: input.vault
|
|
839
|
+
});
|
|
840
|
+
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
841
|
+
const status = result.status ?? 1;
|
|
842
|
+
if (status === 2) {
|
|
843
|
+
return err("PREFLIGHT_FAILED", { reason: "existing-handoff", output, helper_path: helperPath });
|
|
844
|
+
}
|
|
845
|
+
if (status !== 0) {
|
|
846
|
+
return err("GIT_PULL_FAILED", {
|
|
847
|
+
message: result.error ? String(result.error) : `helper exited ${status}`,
|
|
848
|
+
output,
|
|
849
|
+
helper_path: helperPath
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
const afterOid = git(input.vault, ["rev-parse", "HEAD"]);
|
|
853
|
+
if (!afterOid) {
|
|
854
|
+
return err("GIT_PULL_FAILED", { message: "could not read HEAD after pull", helper_path: helperPath });
|
|
855
|
+
}
|
|
856
|
+
return ok({
|
|
857
|
+
before_oid: beforeOid,
|
|
858
|
+
after_oid: afterOid,
|
|
859
|
+
changed: beforeOid !== afterOid,
|
|
860
|
+
helper_path: helperPath
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
export {
|
|
865
|
+
CONFIG_KEYS,
|
|
866
|
+
isValidWikiProfileKey,
|
|
867
|
+
profileKey,
|
|
868
|
+
parseDotenvText,
|
|
869
|
+
parseDotenvFile,
|
|
870
|
+
writeDotenv,
|
|
871
|
+
FLEET_REL_PATH,
|
|
872
|
+
runFleetValidate,
|
|
873
|
+
runFleetContext,
|
|
874
|
+
loadFleetManifestAndHost,
|
|
875
|
+
snapshotterAliasForLocalHost,
|
|
876
|
+
satelliteGateFromFleetLoad,
|
|
877
|
+
loadFleetManifest,
|
|
878
|
+
resolveFleetHostId,
|
|
879
|
+
git,
|
|
880
|
+
gitStrict,
|
|
881
|
+
readJournal,
|
|
882
|
+
listJournalOpIds,
|
|
883
|
+
listReviewRequiredOps,
|
|
884
|
+
findReviewRequiredOp,
|
|
885
|
+
hasUnmergedPaths,
|
|
886
|
+
hasActiveGitSequencer,
|
|
887
|
+
supersedeStaleReviewRequiredJournals,
|
|
888
|
+
resolveVaultSyncPullHelper,
|
|
889
|
+
runVaultSyncPullHelper
|
|
890
|
+
};
|