skillwiki 0.9.63 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/chunk-5TBIMLTZ.js +343 -0
  2. package/dist/chunk-C5OLZRRM.js +357 -0
  3. package/dist/{chunk-TUFQZ5K4.js → chunk-DR7KFHNH.js} +792 -1983
  4. package/dist/chunk-IZABIE44.js +647 -0
  5. package/dist/chunk-S5ABQCXQ.js +580 -0
  6. package/dist/cli.js +1540 -550
  7. package/dist/index-projection-ERFX76U5.js +10 -0
  8. package/dist/managed-write-preflight-SILEUQEV.js +11 -0
  9. package/dist/skillwiki-mcp.js +4 -1
  10. package/dist/vault-sync/scripts/lib/conflict-markers.sh +69 -0
  11. package/dist/vault-sync/scripts/lib/delete-intent.sh +74 -0
  12. package/dist/vault-sync/scripts/lib/fleet.sh +103 -0
  13. package/dist/vault-sync/scripts/lib/git-case.sh +71 -0
  14. package/dist/vault-sync/scripts/lib/git-materialization.sh +264 -0
  15. package/dist/vault-sync/scripts/lib/git-operation-journal.sh +469 -0
  16. package/dist/vault-sync/scripts/lib/git-rebase-state.sh +180 -0
  17. package/dist/vault-sync/scripts/lib/lockfile.sh +70 -0
  18. package/dist/vault-sync/scripts/lib/managed-write-lock.sh +80 -0
  19. package/dist/vault-sync/scripts/lib/platform.sh +184 -0
  20. package/dist/vault-sync/scripts/lib/runtime-manifest.sh +223 -0
  21. package/dist/vault-sync/scripts/wiki-fetch-notify.sh +207 -0
  22. package/dist/vault-sync/scripts/wiki-fuse-refresh.sh +405 -0
  23. package/dist/vault-sync/scripts/wiki-pull-with-auto-resolve.sh +631 -0
  24. package/dist/vault-sync/scripts/wiki-push.sh +364 -0
  25. package/dist/vault-sync/scripts/wiki-snapshot.sh +587 -0
  26. package/package.json +2 -2
  27. package/skills/.claude-plugin/plugin.json +1 -1
  28. package/skills/.codex-plugin/plugin.json +1 -1
  29. package/skills/README.md +13 -0
  30. package/skills/package.json +1 -1
  31. package/skills/proj-work/SKILL.md +3 -0
  32. package/skills/skills/proj-work/SKILL.md +3 -0
  33. package/skills/skills/using-skillwiki/SKILL.md +13 -0
  34. package/skills/skills/wiki-crystallize/SKILL.md +3 -0
  35. package/skills/using-skillwiki/SKILL.md +13 -0
  36. package/skills/wiki-crystallize/SKILL.md +3 -0
@@ -0,0 +1,580 @@
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
+ export {
566
+ CONFIG_KEYS,
567
+ isValidWikiProfileKey,
568
+ profileKey,
569
+ parseDotenvText,
570
+ parseDotenvFile,
571
+ writeDotenv,
572
+ FLEET_REL_PATH,
573
+ runFleetValidate,
574
+ runFleetContext,
575
+ loadFleetManifestAndHost,
576
+ snapshotterAliasForLocalHost,
577
+ satelliteGateFromFleetLoad,
578
+ loadFleetManifest,
579
+ resolveFleetHostId
580
+ };