@younndai/lyt 0.9.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 (96) hide show
  1. package/LICENSE +200 -0
  2. package/NOTICE +23 -0
  3. package/README.md +117 -0
  4. package/dist/automator-bodies/arc-builder.d.ts +13 -0
  5. package/dist/automator-bodies/arc-builder.d.ts.map +1 -0
  6. package/dist/automator-bodies/arc-builder.js +34 -0
  7. package/dist/automator-bodies/arc-builder.js.map +1 -0
  8. package/dist/automator-bodies/index.d.ts +20 -0
  9. package/dist/automator-bodies/index.d.ts.map +1 -0
  10. package/dist/automator-bodies/index.js +32 -0
  11. package/dist/automator-bodies/index.js.map +1 -0
  12. package/dist/automator-bodies/lane-builder.d.ts +12 -0
  13. package/dist/automator-bodies/lane-builder.d.ts.map +1 -0
  14. package/dist/automator-bodies/lane-builder.js +34 -0
  15. package/dist/automator-bodies/lane-builder.js.map +1 -0
  16. package/dist/automator-bodies/metadata-filler.d.ts +34 -0
  17. package/dist/automator-bodies/metadata-filler.d.ts.map +1 -0
  18. package/dist/automator-bodies/metadata-filler.js +211 -0
  19. package/dist/automator-bodies/metadata-filler.js.map +1 -0
  20. package/dist/automator-run.d.ts +27 -0
  21. package/dist/automator-run.d.ts.map +1 -0
  22. package/dist/automator-run.js +137 -0
  23. package/dist/automator-run.js.map +1 -0
  24. package/dist/bench/graded-corpus.d.ts +5 -0
  25. package/dist/bench/graded-corpus.d.ts.map +1 -0
  26. package/dist/bench/graded-corpus.js +121 -0
  27. package/dist/bench/graded-corpus.js.map +1 -0
  28. package/dist/bench/invariant-corpus.d.ts +18 -0
  29. package/dist/bench/invariant-corpus.d.ts.map +1 -0
  30. package/dist/bench/invariant-corpus.js +181 -0
  31. package/dist/bench/invariant-corpus.js.map +1 -0
  32. package/dist/bench/ir-metrics.d.ts +33 -0
  33. package/dist/bench/ir-metrics.d.ts.map +1 -0
  34. package/dist/bench/ir-metrics.js +79 -0
  35. package/dist/bench/ir-metrics.js.map +1 -0
  36. package/dist/bench/latency-bench.d.ts +15 -0
  37. package/dist/bench/latency-bench.d.ts.map +1 -0
  38. package/dist/bench/latency-bench.js +68 -0
  39. package/dist/bench/latency-bench.js.map +1 -0
  40. package/dist/bench/latency-corpus.d.ts +4 -0
  41. package/dist/bench/latency-corpus.d.ts.map +1 -0
  42. package/dist/bench/latency-corpus.js +109 -0
  43. package/dist/bench/latency-corpus.js.map +1 -0
  44. package/dist/bench/pod-harness.d.ts +16 -0
  45. package/dist/bench/pod-harness.d.ts.map +1 -0
  46. package/dist/bench/pod-harness.js +127 -0
  47. package/dist/bench/pod-harness.js.map +1 -0
  48. package/dist/bench/run-bench.d.ts +20 -0
  49. package/dist/bench/run-bench.d.ts.map +1 -0
  50. package/dist/bench/run-bench.js +86 -0
  51. package/dist/bench/run-bench.js.map +1 -0
  52. package/dist/cli-automator-run.d.ts +7 -0
  53. package/dist/cli-automator-run.d.ts.map +1 -0
  54. package/dist/cli-automator-run.js +80 -0
  55. package/dist/cli-automator-run.js.map +1 -0
  56. package/dist/cli.d.ts +3 -0
  57. package/dist/cli.d.ts.map +1 -0
  58. package/dist/cli.js +161 -0
  59. package/dist/cli.js.map +1 -0
  60. package/dist/commands/bench.d.ts +3 -0
  61. package/dist/commands/bench.d.ts.map +1 -0
  62. package/dist/commands/bench.js +92 -0
  63. package/dist/commands/bench.js.map +1 -0
  64. package/dist/commands/capture.d.ts +4 -0
  65. package/dist/commands/capture.d.ts.map +1 -0
  66. package/dist/commands/capture.js +251 -0
  67. package/dist/commands/capture.js.map +1 -0
  68. package/dist/commands/init.d.ts +12 -0
  69. package/dist/commands/init.d.ts.map +1 -0
  70. package/dist/commands/init.js +741 -0
  71. package/dist/commands/init.js.map +1 -0
  72. package/dist/commands/primer.d.ts +3 -0
  73. package/dist/commands/primer.d.ts.map +1 -0
  74. package/dist/commands/primer.js +199 -0
  75. package/dist/commands/primer.js.map +1 -0
  76. package/dist/commands/reindex.d.ts +3 -0
  77. package/dist/commands/reindex.d.ts.map +1 -0
  78. package/dist/commands/reindex.js +105 -0
  79. package/dist/commands/reindex.js.map +1 -0
  80. package/dist/commands/search.d.ts +3 -0
  81. package/dist/commands/search.d.ts.map +1 -0
  82. package/dist/commands/search.js +224 -0
  83. package/dist/commands/search.js.map +1 -0
  84. package/dist/flows/heal.d.ts +29 -0
  85. package/dist/flows/heal.d.ts.map +1 -0
  86. package/dist/flows/heal.js +146 -0
  87. package/dist/flows/heal.js.map +1 -0
  88. package/dist/flows/init-bootstrap.d.ts +93 -0
  89. package/dist/flows/init-bootstrap.d.ts.map +1 -0
  90. package/dist/flows/init-bootstrap.js +561 -0
  91. package/dist/flows/init-bootstrap.js.map +1 -0
  92. package/dist/index.d.ts +11 -0
  93. package/dist/index.d.ts.map +1 -0
  94. package/dist/index.js +27 -0
  95. package/dist/index.js.map +1 -0
  96. package/package.json +81 -0
@@ -0,0 +1,741 @@
1
+ /*
2
+ * Copyright 2026 MARLINK TRADING SRL (YounndAI)
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ // v1.B.4 — `lyt init [--auto | --custom | --discover] [--json]`.
17
+ //
18
+ // Top-level meta-CLI verb per OD-1 default + master-plan §v1.B.4:543.
19
+ // Composes the v1.B.4 initBootstrapFlow with mutually-exclusive flag
20
+ // validation + structured-error contract + readline/promises three-prompt
21
+ // walkthrough under --custom.
22
+ //
23
+ // Source: brief 2026-05-31-v1-b-4-lyt-init-bootstrap.md "What's to ship"
24
+ // Commit 1 + federation-design §5:228-234 (custom-init prompts) +
25
+ // commands/move.ts (closest CLI shape — readline/promises + mutually-
26
+ // exclusive flag validation + structured-error contract).
27
+ //
28
+ // Error contract (OD-9 + brief acceptance):
29
+ // --auto + --custom together → exit 2 + flag-conflict
30
+ // --custom under non-TTY (incl. --json) → exit 3 + custom-requires-tty
31
+ // re-init with ALL-failed integrity → exit 1 (matches v1.B.2 OD-6)
32
+ // otherwise → exit 0
33
+ import { Command } from "commander";
34
+ import { createInterface } from "node:readline/promises";
35
+ import { getHandleFromIdentity, materializePodLocal, readIdentityCache, reconcilePublishFlow, ReadlinePromptHandler, renderNextSteps, renderPodCard, runWizard, startSpinner, validateMeshName, } from "@younndai/lyt-vault";
36
+ import { initBootstrapFlow, probeFreshState, resolveLocalFirst, } from "../flows/init-bootstrap.js";
37
+ import { healPod, summarizeHeal } from "../flows/heal.js";
38
+ export function buildLytInitCommand() {
39
+ return new Command("init")
40
+ .description("First-init (no mesh registered): enters guided setup wizard. Re-init (mesh exists): idempotent bootstrap (use --auto to force non-interactive). Flags: --auto (force bootstrap), --custom (3-prompt walkthrough), --discover (read-only GH delta), --wizard (force wizard), --dry-run (with --wizard only), --json.")
41
+ .option("--auto", "Force non-interactive bootstrap (skips first-init wizard auto-route)")
42
+ .option("--custom", "Three-prompt walkthrough; conflicts with --auto; requires a TTY")
43
+ .option("--discover", "Read-only GH delta; surface accessible lyt-* repos not in local registry")
44
+ .option("--wizard", "Force the 12-phase setup wizard (detect/install Node + gh + agent runtime, gh auth login, install Lyt skills, install agent manual, cross-machine adopt-detect, create personal mesh + first vault + federation repo, pod-map vault, first-use demo). Conflicts with --auto/--custom/--discover/--json.")
45
+ .option("--dry-run", "Only valid with --wizard. Walks all 12 wizard phases without spawn invocations or filesystem writes.")
46
+ .option("--json", "Emit deterministic Lock 0.3 JSON")
47
+ .action(async (opts) => {
48
+ // v1.G.13 Gap 1 — no-flag fresh-state wizard auto-route. When the
49
+ // handler runs `lyt init` with NO mode flags AND the registry is
50
+ // empty (first-init), enter the wizard. Re-init state falls through
51
+ // to the existing --auto default. Non-TTY first-init errors out per
52
+ // the ratified default.
53
+ const noMode = opts.auto !== true &&
54
+ opts.custom !== true &&
55
+ opts.discover !== true &&
56
+ opts.wizard !== true &&
57
+ opts.json !== true &&
58
+ opts.dryRun !== true;
59
+ if (noMode) {
60
+ let isFresh;
61
+ try {
62
+ isFresh = await probeFreshState();
63
+ }
64
+ catch (err) {
65
+ // Probe failure (registry unreachable) — surface and exit.
66
+ const msg = err instanceof Error ? err.message : String(err);
67
+ emitError(false, { error: "first-init-probe-failed", message: msg });
68
+ process.exitCode = 1;
69
+ return;
70
+ }
71
+ if (isFresh) {
72
+ if (process.stdin.isTTY !== true) {
73
+ emitError(false, {
74
+ error: "first-init-requires-tty",
75
+ message: "lyt init: interactive required for first-init; use --auto for non-interactive bootstrap.",
76
+ });
77
+ process.exitCode = 3;
78
+ return;
79
+ }
80
+ // TTY + fresh → route through the existing --wizard branch by
81
+ // flipping the flag. Single code path for both explicit
82
+ // `--wizard` and the no-flag first-init auto-route.
83
+ opts.wizard = true;
84
+ }
85
+ // Otherwise (re-init state) fall through to existing --auto default.
86
+ }
87
+ // v1.G.4 — --wizard takes a dedicated branch; mutually exclusive
88
+ // with the existing auto/custom/discover/json modes (the wizard
89
+ // composes mesh+vault+federation init itself).
90
+ if (opts.wizard === true) {
91
+ if (opts.auto === true ||
92
+ opts.custom === true ||
93
+ opts.discover === true ||
94
+ opts.json === true) {
95
+ emitError(false, {
96
+ error: "flag-conflict",
97
+ message: "lyt init --wizard is mutually exclusive with --auto, --custom, --discover, and --json.",
98
+ });
99
+ process.exitCode = 2;
100
+ return;
101
+ }
102
+ if (opts.dryRun !== true && process.stdin.isTTY !== true) {
103
+ emitError(false, {
104
+ error: "wizard-requires-tty",
105
+ message: "lyt init --wizard requires an interactive terminal (or pass --dry-run for a non-TTY phase walk).",
106
+ });
107
+ process.exitCode = 3;
108
+ return;
109
+ }
110
+ // Under --dry-run, use a non-interactive default handler so the
111
+ // wizard can be smoke-tested without stdin. Real interactive runs
112
+ // use the readline-backed handler.
113
+ const handler = opts.dryRun === true ? makeDryRunDefaultsHandler() : new ReadlinePromptHandler();
114
+ try {
115
+ const result = await runWizard({
116
+ promptHandler: handler,
117
+ dryRun: opts.dryRun === true,
118
+ });
119
+ // F3 (console-DX): scannable setup-summary block — a header/footer rule
120
+ // + status glyphs (✓ done · ⊘ skipped · ✗ failed) replacing the flat
121
+ // `[ok] Phase N (name): msg` lines that had no visible start/end.
122
+ const summaryLines = [
123
+ "",
124
+ "── Setup summary ──────────────────────────────────",
125
+ ...result.phases.map((ph) => {
126
+ const glyph = ph.skipped === true ? "⊘" : ph.ok ? "✓" : "✗";
127
+ const num = String(ph.phase).padStart(2, " ");
128
+ return ` ${glyph} Phase ${num} ${ph.name} — ${ph.message}`;
129
+ }),
130
+ "───────────────────────────────────────────────────",
131
+ ];
132
+ // eslint-disable-next-line no-console
133
+ console.log(summaryLines.join("\n"));
134
+ if (result.status !== "completed") {
135
+ process.exitCode = 1;
136
+ }
137
+ }
138
+ catch (err) {
139
+ const msg = err instanceof Error ? err.message : String(err);
140
+ emitError(false, { error: "wizard-error", message: msg });
141
+ process.exitCode = 1;
142
+ }
143
+ finally {
144
+ handler.close?.();
145
+ }
146
+ return;
147
+ }
148
+ // --dry-run is only valid with --wizard (the wizard branch returned
149
+ // above; reaching here means --dry-run was passed without --wizard).
150
+ if (opts.dryRun === true) {
151
+ emitError(opts.json === true, {
152
+ error: "flag-conflict",
153
+ message: "lyt init: --dry-run is only valid in combination with --wizard.",
154
+ });
155
+ process.exitCode = 2;
156
+ return;
157
+ }
158
+ // Flag conflict: --auto + --custom.
159
+ if (opts.auto === true && opts.custom === true) {
160
+ emitError(opts.json === true, {
161
+ error: "flag-conflict",
162
+ message: "lyt init: --auto and --custom are mutually exclusive. Omit both (defaults to --auto), or pick one.",
163
+ });
164
+ process.exitCode = 2;
165
+ return;
166
+ }
167
+ // Flag conflict: --discover with prompts is nonsensical (read-only).
168
+ if (opts.discover === true && opts.custom === true) {
169
+ emitError(opts.json === true, {
170
+ error: "flag-conflict",
171
+ message: "lyt init: --discover and --custom are mutually exclusive (--discover is read-only).",
172
+ });
173
+ process.exitCode = 2;
174
+ return;
175
+ }
176
+ // --custom requires a TTY (incl. JSON mode where prompts make no sense).
177
+ if (opts.custom === true && (process.stdin.isTTY !== true || opts.json === true)) {
178
+ emitError(opts.json === true, {
179
+ error: "custom-requires-tty",
180
+ message: "lyt init --custom requires an interactive terminal and cannot be combined with --json.",
181
+ });
182
+ process.exitCode = 3;
183
+ return;
184
+ }
185
+ const mode = opts.discover === true ? "discover" : opts.custom === true ? "custom" : "auto";
186
+ // --custom three-prompt walkthrough (per OD-6 default + federation-
187
+ // design §5:228-234; main vault name SKIPPED per naming-convention).
188
+ let customOverrides;
189
+ if (mode === "custom") {
190
+ try {
191
+ customOverrides = await runCustomPrompts();
192
+ }
193
+ catch (err) {
194
+ const msg = err instanceof Error ? err.message : String(err);
195
+ emitError(opts.json === true, {
196
+ error: "custom-prompt-error",
197
+ message: msg,
198
+ });
199
+ process.exitCode = 1;
200
+ return;
201
+ }
202
+ }
203
+ // v1.GP F7-followup — phase-spanning init spinner. Drive a persistent
204
+ // spinner across the WHOLE bootstrap so the surrounding sync work (mesh
205
+ // forge, vault scaffold, libSQL writes, git init, pod.yon write)
206
+ // no longer runs with a dead/frozen indicator. Active only for the
207
+ // human-output FRESH/auto path: --json stays escape-code-free + the
208
+ // discovery/re-init branches are fast read-only paths with no silent
209
+ // gap to cover. The spinner's onPhase re-labels + yields to the event
210
+ // loop at each boundary; per-op gh/git spinners deep in the flow defer
211
+ // to it (single-spinner invariant in util/spinner.ts). Cursor restored
212
+ // on stop() AND on throw via the finally below.
213
+ const useSpinner = opts.json !== true && mode !== "discover";
214
+ const spinner = useSpinner ? startSpinner() : undefined;
215
+ const flowArgs = {
216
+ mode,
217
+ // W1.2 / OD-4 — heal on every `lyt init` bootstrap. The flow gates
218
+ // this to the fresh + re-init branches (discovery stays read-only),
219
+ // so a single `lyt init` re-aligns skills + agent manual + patterns.
220
+ // Wired ONLY at the command layer so flow/integration unit tests
221
+ // (which call initBootstrapFlow directly without `heal`) never write
222
+ // to the real ~/.claude / ~/.codex / ~/.agents.
223
+ heal: () => healPod(),
224
+ // Brief B (B.1) — materialize each vault into a publishable LOCAL state
225
+ // (git + initial commit + remote URL) and commit pod.yon, with push +
226
+ // gh-create HELD (push: false). Outward publish is the consented sync
227
+ // engine's job (B.2), triggered by the staged-HIL prompt (B.3). Wired
228
+ // ONLY here so flow/integration tests stay hermetic (no git subprocesses
229
+ // on temp vault dirs).
230
+ // D34 (OD-LOCALFIRST) — in a local-first context (no gh / provisional
231
+ // identity), hold the remote too (setRemote:false): the provisional
232
+ // handle must never land in a vault `origin` URL. Connect re-materializes
233
+ // with the real handle + setRemote:true.
234
+ materializePublish: (db) => materializePodLocal(db, { push: false, setRemote: !isLocalFirstContext() }),
235
+ ...(customOverrides !== undefined ? { customOverrides } : {}),
236
+ ...(spinner !== undefined
237
+ ? {
238
+ onPhase: async (op, label) => {
239
+ spinner.phase(op, label);
240
+ // Yield so the render interval fires AT the boundary — the
241
+ // label + elapsed visibly advance even though frames can't
242
+ // animate inside a single blocking sync call.
243
+ await new Promise((r) => setImmediate(r));
244
+ },
245
+ }
246
+ : {}),
247
+ };
248
+ try {
249
+ const result = await initBootstrapFlow(flowArgs);
250
+ // Stop the spanning spinner BEFORE printing the result/card so its
251
+ // teardown (clear-line + show-cursor) doesn't clobber the output.
252
+ spinner?.stop();
253
+ if (opts.json === true) {
254
+ emitJsonResult(result);
255
+ }
256
+ else {
257
+ emitHumanResult(result);
258
+ // W1.2 — surface the heal summary (skills/manual/patterns realign).
259
+ if (result.heal !== undefined) {
260
+ // eslint-disable-next-line no-console
261
+ console.log(summarizeHeal(result.heal));
262
+ }
263
+ // WS2 — render the pod card at the end of `lyt init --auto` too
264
+ // (previously wizard-only). Fresh branch only; the lyt-pod-map line
265
+ // is omitted because --auto does not generate a pod-map vault.
266
+ if (result.branch === "fresh") {
267
+ emitAutoPodCard(result);
268
+ }
269
+ else if (result.branch === "adopt" && result.adopt !== undefined) {
270
+ emitAdoptPodCard(result);
271
+ }
272
+ // Brief B (B.3) — staged-HIL publish prompt. After the honest
273
+ // (staged) card, ASK whether to publish now (default-Yes per OD-B2).
274
+ // On yes → the consented sync engine pushes pod + vaults. Outward
275
+ // effect ONLY behind this explicit consent.
276
+ await maybePromptAndPublish(result);
277
+ }
278
+ // Re-init with ALL-failed integrity → exit 1 (matches v1.B.2 OD-6
279
+ // skip-and-warn precedent + brief OD-4 default).
280
+ if (result.branch === "re-init" &&
281
+ result.integrityIssues !== undefined &&
282
+ result.integrityIssues.length > 0 &&
283
+ result.integrityIssues.every((i) => i.status !== "ok")) {
284
+ process.exitCode = 1;
285
+ }
286
+ // a review finding — adopt attempted but the clone failed: clean non-zero exit (the
287
+ // actionable error was already rendered by emitHumanResult/emitJsonResult).
288
+ if (result.branch === "adopt" && result.adoptError !== undefined) {
289
+ process.exitCode = 1;
290
+ }
291
+ }
292
+ catch (err) {
293
+ // Restore the cursor + clear the spinner line before the error path.
294
+ spinner?.stop();
295
+ const message = err instanceof Error ? err.message : String(err);
296
+ emitError(opts.json === true, {
297
+ error: "init-bootstrap-error",
298
+ message,
299
+ });
300
+ process.exitCode = 2;
301
+ }
302
+ });
303
+ }
304
+ async function runCustomPrompts() {
305
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
306
+ try {
307
+ // 1. Mesh name (default 'personal'; validateMeshName-gated).
308
+ let meshName = "personal";
309
+ while (true) {
310
+ const ans = (await rl.question("Mesh name [personal]: ")).trim();
311
+ const candidate = ans.length === 0 ? "personal" : ans;
312
+ try {
313
+ validateMeshName(candidate);
314
+ meshName = candidate;
315
+ break;
316
+ }
317
+ catch (err) {
318
+ const msg = err instanceof Error ? err.message : String(err);
319
+ // eslint-disable-next-line no-console
320
+ console.log(` ! ${msg}`);
321
+ // loop until valid OR user submits empty (defaults to 'personal').
322
+ }
323
+ }
324
+ // 2. Push target (default authenticated GH handle; accept handle or
325
+ // org:name; not structurally validated here — passed through to mesh
326
+ // init which validates).
327
+ let pushDefault;
328
+ try {
329
+ pushDefault = getHandleFromIdentity();
330
+ }
331
+ catch {
332
+ pushDefault = "";
333
+ }
334
+ const pushPrompt = pushDefault.length > 0
335
+ ? `Push target (handle or org:name) [${pushDefault}]: `
336
+ : "Push target (handle or org:name) []: ";
337
+ const pushAns = (await rl.question(pushPrompt)).trim();
338
+ const pushTarget = pushAns.length === 0 ? pushDefault : pushAns;
339
+ // Main vault name SKIPPED per OD-6 + naming-convention §The main vault
340
+ // is locked. Surface the lock as an informational line.
341
+ // eslint-disable-next-line no-console
342
+ console.log("Main vault name: 'main' (locked; cannot be changed)");
343
+ // 3. Starter content (default y).
344
+ const starterAns = (await rl.question("Include starter content? [Y/n]: ")).trim().toLowerCase();
345
+ const starterFigment = starterAns === "" || starterAns === "y" || starterAns === "yes";
346
+ return { meshName, pushTarget, starterFigment };
347
+ }
348
+ finally {
349
+ rl.close();
350
+ }
351
+ }
352
+ // a review finding (release review) — the honest adopt "expected" set: clone FAILURES only.
353
+ // Benign skips (tombstoned / already-registered) are excluded so a clean adopt of
354
+ // a pod that lists tombstoned vaults never reads as a partial restore. Shared by
355
+ // the human + --json emit (one classification, no drift) and unit-tested directly.
356
+ export function adoptCloneFailures(manifestSkipped) {
357
+ return manifestSkipped.filter((s) => s.reason !== "tombstoned" && s.reason !== "already-registered");
358
+ }
359
+ // Exported for the SC5 emit-shape test (Phase-D a review finding — the rendered adopt
360
+ // `--json` contract is pinned directly, not just the pure classifier).
361
+ export function emitJsonResult(res) {
362
+ // Lock 0.3 stable-key-ordered output (discriminated-union per `branch`).
363
+ const stable = {
364
+ branch: res.branch,
365
+ durationMs: res.durationMs,
366
+ };
367
+ if (res.meshAssignment !== undefined) {
368
+ stable["meshAssignment"] = {
369
+ meshRidHex: res.meshAssignment.meshRidHex,
370
+ meshName: res.meshAssignment.meshName,
371
+ meshAutoCreated: res.meshAssignment.meshAutoCreated,
372
+ };
373
+ }
374
+ if (res.federation !== undefined) {
375
+ stable["federation"] = {
376
+ handle: res.federation.handle,
377
+ fedRidHex: res.federation.fedRidHex,
378
+ branch: res.federation.branch,
379
+ localPath: res.federation.localPath,
380
+ federationYonPath: res.federation.federationYonPath,
381
+ remoteCreated: res.federation.remoteCreated,
382
+ pushed: res.federation.pushed,
383
+ };
384
+ }
385
+ if (res.integrityIssues !== undefined) {
386
+ stable["integrityIssues"] = res.integrityIssues.map((i) => ({
387
+ vaultName: i.vaultName,
388
+ status: i.status,
389
+ ...(i.error !== undefined ? { error: i.error } : {}),
390
+ }));
391
+ }
392
+ if (res.discoveredRepos !== undefined) {
393
+ stable["discoveredRepos"] = res.discoveredRepos.map((r) => ({
394
+ fullName: r.fullName,
395
+ kind: r.kind,
396
+ alreadyInRegistry: r.alreadyInRegistry,
397
+ }));
398
+ }
399
+ // ADOPT branch (V-A-11). Stable-keyed; `vaultsExpectedFromManifest` is the a review finding
400
+ // honest denominator (excludes tombstoned/already-registered), `skipped` lists
401
+ // only real clone failures, `partialRestore` is the SC8 honesty flag.
402
+ if (res.adopt !== undefined) {
403
+ const a = res.adopt;
404
+ const failures = adoptCloneFailures(a.manifestSkipped);
405
+ stable["adopt"] = {
406
+ podBranch: a.podBranch,
407
+ podHandle: a.podHandle,
408
+ podLocalPath: a.podLocalPath,
409
+ vaultsRecoveredFromManifest: a.vaultsRecoveredFromManifest,
410
+ vaultsExpectedFromManifest: a.vaultsRecoveredFromManifest + failures.length,
411
+ vaultsAcquired: a.vaultsAcquired,
412
+ firstVaultCreated: a.firstVaultCreated,
413
+ partialRestore: failures.length > 0,
414
+ skipped: failures.map((f) => ({ vaultName: f.vaultName, reason: f.reason })),
415
+ // (reconciledVaultPaths is carried once, at the top level — the cross-branch
416
+ // contract fresh/re-init also use; Phase-D a review finding dropped the nested copy.)
417
+ };
418
+ }
419
+ if (res.adoptError !== undefined) {
420
+ stable["adoptError"] = { reason: res.adoptError.reason };
421
+ }
422
+ // W1.2 release review fix-pass (R1-Minor) — the heal runs its filesystem
423
+ // side-effects under `--json` too; surface its outcome so an automation
424
+ // consumer (e.g. the deferred self-updater) can observe collision/divergent
425
+ // notes the handler is meant to see (D30.3/D30.4).
426
+ if (res.heal !== undefined) {
427
+ stable["heal"] = {
428
+ runtimes: res.heal.runtimes,
429
+ skills: res.heal.skills.results.map((r) => ({
430
+ skill: r.skill,
431
+ runtime: r.runtime,
432
+ status: r.status,
433
+ })),
434
+ manual: res.heal.manual.map((m) => ({ runtime: m.runtime, action: m.action })),
435
+ patterns: res.heal.patterns.entries.map((e) => ({ id: e.id, action: e.action })),
436
+ };
437
+ }
438
+ if (res.reconciledVaultPaths !== undefined) {
439
+ stable["reconciledVaultPaths"] = res.reconciledVaultPaths;
440
+ }
441
+ // Brief B (B.3) — surface the staged-vs-published posture. Under --json the
442
+ // run is non-interactive (no publish prompt), so state is always "staged":
443
+ // init materializes locally; publishing is the consented `lyt sync` step.
444
+ if (res.publish !== undefined && !res.publish.skipped) {
445
+ stable["publish"] = {
446
+ state: "staged",
447
+ vaultsMaterialized: res.publish.vaults.filter((v) => !v.skipped).length,
448
+ podCommitted: res.publish.podCommitted,
449
+ };
450
+ }
451
+ // eslint-disable-next-line no-console
452
+ console.log(JSON.stringify(stable, null, 2));
453
+ }
454
+ function emitHumanResult(res) {
455
+ if (res.branch === "fresh") {
456
+ // eslint-disable-next-line no-console
457
+ console.log(`Forged mesh '${res.meshAssignment?.meshName}' + scaffolded main vault.`);
458
+ if (res.federation !== undefined) {
459
+ // WS3 / D25 — bridge pod ↔ federation on first surface in --auto output.
460
+ // Brief C (F3) — honest staged-HIL text: the pod CONTAINER repo was
461
+ // CREATED on GitHub (remoteCreated) per D31 §4 two-tier consent; CONTENT
462
+ // is staged (unpushed) until `lyt sync`. "local-only" was misleading and
463
+ // pointed at the retired `lyt federation rebuild --push` verb.
464
+ const fed = res.federation;
465
+ const podPosture = fed.remoteCreated
466
+ ? "created on GitHub · content staged (unpushed) — run `lyt sync` to publish"
467
+ : fed.branch === "adopted"
468
+ ? "on GitHub (adopted) · content staged — run `lyt sync` to publish"
469
+ : `${fed.branch} — run \`lyt sync\` to publish`;
470
+ // eslint-disable-next-line no-console
471
+ console.log(` pod (federation): ${fed.remoteFullName} — the identity layer behind your pod (${podPosture})`);
472
+ // eslint-disable-next-line no-console
473
+ console.log(` path: ${res.federation.localPath}`);
474
+ }
475
+ else {
476
+ // eslint-disable-next-line no-console
477
+ console.warn(" pod (federation): skipped (no authenticated handle resolvable)");
478
+ }
479
+ return;
480
+ }
481
+ if (res.branch === "re-init") {
482
+ const issues = res.integrityIssues ?? [];
483
+ const ok = issues.filter((i) => i.status === "ok").length;
484
+ const failed = issues.filter((i) => i.status !== "ok");
485
+ // eslint-disable-next-line no-console
486
+ console.log(`Re-init: ${issues.length} vault${issues.length === 1 ? "" : "s"} checked; ${ok} ok, ${failed.length} with issues.`);
487
+ for (const f of failed) {
488
+ // eslint-disable-next-line no-console
489
+ console.warn(` ! ${f.vaultName} [${f.status}] ${f.error ?? ""}`);
490
+ }
491
+ if (failed.length === 0) {
492
+ // eslint-disable-next-line no-console
493
+ console.log(" integrity OK.");
494
+ }
495
+ return;
496
+ }
497
+ if (res.branch === "adopt") {
498
+ // a review finding — adopt failed (pod/vault clone threw). Render an AI-actionable error;
499
+ // the main flow sets a clean non-zero exit. Local state is left re-runnable.
500
+ if (res.adoptError !== undefined) {
501
+ // Phase-D a review finding — do NOT over-promise "registry empty / nothing scaffolded":
502
+ // that holds for the common pod-clone throw but not a rare post-recovery
503
+ // fault that leaves a partial registry. Point at `lyt doctor` for the
504
+ // half-set-up case instead of asserting an unconditional clean state.
505
+ // eslint-disable-next-line no-console
506
+ console.error(`Couldn't finish adopting your pod — ${res.adoptError.reason}\n` +
507
+ ` This is usually a network drop or a GitHub-credentials issue on a private repo.\n` +
508
+ ` • Check your login: gh auth status\n` +
509
+ ` • Then retry (re-running resumes): lyt init --auto\n` +
510
+ ` • If anything looks half-set-up: lyt doctor`);
511
+ return;
512
+ }
513
+ const a = res.adopt;
514
+ if (a === undefined)
515
+ return;
516
+ // a review finding — honest "expected" denominator EXCLUDES benign skips; only real clone
517
+ // failures count, so a clean adopt of a pod with tombstoned vaults never reads
518
+ // as a partial restore (shared classifier with the --json emit).
519
+ const failures = adoptCloneFailures(a.manifestSkipped);
520
+ const expected = a.vaultsRecoveredFromManifest + failures.length;
521
+ // eslint-disable-next-line no-console
522
+ console.log(`Adopted pod ${a.podHandle}/lyt-pod — restored ${a.vaultsRecoveredFromManifest}/${expected} vault(s); re-indexed ${a.reconciledVaultPaths.length}.`);
523
+ if (a.firstVaultCreated) {
524
+ // eslint-disable-next-line no-console
525
+ console.log(` pod had no vaults to restore — scaffolded ${a.primaryMeshName ?? "personal"}/main.`);
526
+ }
527
+ if (failures.length > 0) {
528
+ // MF4/SC8 — partial restore is LOUD (a 3-of-5 adopt is never reported as success).
529
+ // eslint-disable-next-line no-console
530
+ console.warn(` ! partial restore: ${failures.length} vault(s) did not clone:`);
531
+ for (const f of failures) {
532
+ // eslint-disable-next-line no-console
533
+ console.warn(` - ${f.vaultName}: ${f.reason}`);
534
+ }
535
+ // eslint-disable-next-line no-console
536
+ console.warn(" Re-run `lyt init --auto` to retry the missing vault(s).");
537
+ }
538
+ return;
539
+ }
540
+ // discovery
541
+ const repos = res.discoveredRepos ?? [];
542
+ // eslint-disable-next-line no-console
543
+ console.log(`Discovery: ${repos.length} lyt-* repo${repos.length === 1 ? "" : "s"} found.`);
544
+ for (const r of repos) {
545
+ const marker = r.alreadyInRegistry ? "[in registry]" : "[NEW]";
546
+ // eslint-disable-next-line no-console
547
+ console.log(` ${marker} ${r.fullName} (${r.kind})`);
548
+ }
549
+ }
550
+ // WS2 — render the end-of-init pod card on `lyt init --auto` (FRESH branch).
551
+ // Mirrors the wizard's emitPodCard sourcing but for the bootstrap result:
552
+ // pod repo full name + local path come from the federation chokepoint
553
+ // (res.federation), the mesh + main-vault row from res.meshAssignment. The
554
+ // lyt-pod-map line is OMITTED because `--auto` does not generate a pod-map
555
+ // vault (only the wizard's P11 does) — PodCardData simply leaves
556
+ // podMapVaultPath unset so renderPodCard skips that block. Best-effort: a
557
+ // missing federation (no handle) skips the card (the warn line already
558
+ // printed by emitHumanResult covers that case).
559
+ function emitAutoPodCard(res) {
560
+ const fed = res.federation;
561
+ const mesh = res.meshAssignment;
562
+ if (fed === undefined || mesh?.mainVaultPath === undefined) {
563
+ return;
564
+ }
565
+ const data = {
566
+ handle: fed.handle,
567
+ mesh: {
568
+ meshName: mesh.meshName,
569
+ vaultName: mesh.mainVaultName ?? `${mesh.meshName}/main`,
570
+ vaultPath: mesh.mainVaultPath,
571
+ },
572
+ podRepoFullName: fed.remoteFullName,
573
+ podLocalPath: fed.localPath,
574
+ hyperlinksEnabled: process.stdout.isTTY === true,
575
+ // Brief B (B.3) — init materializes LOCALLY (push held); the card is honest
576
+ // that the pod is staged, not published, and points at `lyt sync`. The HIL
577
+ // publish prompt (maybePromptAndPublish) runs after the card.
578
+ // D34 (OD-LOCALFIRST) — a no-gh / provisional pod is "local-only" (NOT
579
+ // connected), a stronger honesty than "staged" (which implies gh is wired).
580
+ publishState: isLocalFirstContext() ? "local-only" : "staged",
581
+ };
582
+ // eslint-disable-next-line no-console
583
+ console.log(renderPodCard(data));
584
+ // Brief C (F4) — `--auto` materializes locally (publishState "staged"), so the
585
+ // Next-steps lead with `lyt sync` whenever the pod isn't yet published.
586
+ // eslint-disable-next-line no-console
587
+ console.log(renderNextSteps({ unpublished: data.publishState !== "published" }));
588
+ // eslint-disable-next-line no-console
589
+ console.log("");
590
+ }
591
+ // V-A-11 — pod card for the ADOPT branch (a fresh machine that cloned an existing
592
+ // pod). Built from res.adopt (the federation/meshAssignment fields stay unset on
593
+ // adopt). publishState "staged": the pod + vaults came FROM GitHub, but the local
594
+ // registration/recovery commits are unpushed (noPush) → Next-steps nudge `lyt sync`
595
+ // to push them. A null primaryVaultPath (no vault recovered AND scaffold failed)
596
+ // skips the card — the human/json line already carried the outcome.
597
+ function emitAdoptPodCard(res) {
598
+ const a = res.adopt;
599
+ if (a === undefined || a.primaryVaultPath === null) {
600
+ return;
601
+ }
602
+ const meshName = a.primaryMeshName ?? "personal";
603
+ const data = {
604
+ handle: a.podHandle,
605
+ mesh: {
606
+ meshName,
607
+ vaultName: `${meshName}/main`,
608
+ vaultPath: a.primaryVaultPath,
609
+ },
610
+ podRepoFullName: `${a.podHandle}/lyt-pod`,
611
+ podLocalPath: a.podLocalPath,
612
+ hyperlinksEnabled: process.stdout.isTTY === true,
613
+ publishState: "staged",
614
+ };
615
+ // eslint-disable-next-line no-console
616
+ console.log(renderPodCard(data));
617
+ // eslint-disable-next-line no-console
618
+ console.log(renderNextSteps({ unpublished: true }));
619
+ // eslint-disable-next-line no-console
620
+ console.log("");
621
+ }
622
+ // Brief B (B.3) — the staged-HIL publish prompt. Runs after the honest card on
623
+ // the fresh + re-init human paths. Default-Yes ([Y/n]) per OD-B2: publishing is
624
+ // the expected end-state and the prompt itself is the explicit consent (no
625
+ // surprise push). On yes → the B.2 reconcile engine (push=true) does the outward
626
+ // gh-create + push, resumable via the outbox. Non-interactive (no TTY) leaves
627
+ // the pod staged + prints the honest `lyt sync` nudge (never blocks on stdin).
628
+ async function maybePromptAndPublish(res) {
629
+ const pub = res.publish;
630
+ if (pub === undefined || pub.skipped)
631
+ return; // no pod / nothing materialized
632
+ // D34 (OD-LOCALFIRST) — a local-first (no-gh / provisional) pod has no gh to
633
+ // publish to; the publish prompt would fail. Connect is `lyt sync`'s job (the
634
+ // self-heal). Nudge there instead of prompting to publish.
635
+ if (isLocalFirstContext()) {
636
+ // eslint-disable-next-line no-console
637
+ console.log("\nYour pod is local-only (not connected to GitHub). Run `lyt sync` to connect + back it up.");
638
+ return;
639
+ }
640
+ const vaultCount = pub.vaults.filter((v) => !v.skipped).length;
641
+ if (process.stdin.isTTY !== true) {
642
+ // eslint-disable-next-line no-console
643
+ console.log("\nYour pod is staged locally (not published). Run `lyt sync` to publish it to GitHub.");
644
+ return;
645
+ }
646
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
647
+ let yes;
648
+ try {
649
+ const ans = (await rl.question(`\nPublish your pod to GitHub now? (pushes your pod + ${vaultCount} vault repo(s)) [Y/n]: `))
650
+ .trim()
651
+ .toLowerCase();
652
+ // OD-B2 default-Yes: empty (Enter) or y/yes → publish.
653
+ yes = ans === "" || ans === "y" || ans === "yes";
654
+ }
655
+ finally {
656
+ rl.close();
657
+ }
658
+ if (!yes) {
659
+ // eslint-disable-next-line no-console
660
+ console.log("Staged. Run `lyt sync` when you're ready to publish to GitHub.");
661
+ return;
662
+ }
663
+ // eslint-disable-next-line no-console
664
+ console.log("Publishing your pod to GitHub…");
665
+ const result = await reconcilePublishFlow({ push: true });
666
+ if (result.skipped) {
667
+ // eslint-disable-next-line no-console
668
+ console.log(`Publish skipped — ${result.reason ?? "no pod"}.`);
669
+ return;
670
+ }
671
+ const pushed = result.vaultOutcomes.filter((o) => o.pushed).length;
672
+ if (result.ok) {
673
+ // eslint-disable-next-line no-console
674
+ console.log(`✓ Published to GitHub — ${pushed} vault repo(s) + your pod.`);
675
+ }
676
+ else {
677
+ // eslint-disable-next-line no-console
678
+ console.log(`⚠ Partial publish — ${pushed} vault(s) pushed; ${result.outboxRemaining} op(s) pending. Re-run \`lyt sync\` to finish (resumable, no data lost).`);
679
+ for (const o of result.vaultOutcomes) {
680
+ if (o.status === "conflict" || o.status === "failed") {
681
+ // eslint-disable-next-line no-console
682
+ console.log(` ${o.status}: ${o.vaultName} — ${o.message}`);
683
+ }
684
+ }
685
+ }
686
+ }
687
+ // Non-interactive defaults handler used by `lyt init --wizard --dry-run`
688
+ // so the smoke test (and CI sanity invocations) never block on stdin.
689
+ // Returns: ask → defaultValue (or "" if absent); confirm → defaultValue
690
+ // (or true); select → first option. Matches the runWizard contract.
691
+ function makeDryRunDefaultsHandler() {
692
+ return {
693
+ async ask(_question, defaultValue) {
694
+ return defaultValue ?? "";
695
+ },
696
+ async confirm(_question, defaultValue) {
697
+ return defaultValue ?? true;
698
+ },
699
+ async select(_question, options) {
700
+ return options[0].value;
701
+ },
702
+ };
703
+ }
704
+ // D34 (OD-LOCALFIRST) — true when init should stay LOCAL: no gh handle resolves
705
+ // (gh absent/unauthed) OR the cached identity is provisional (a local pod). In
706
+ // both cases the materialize pass holds vault remotes (setRemote:false) so the
707
+ // provisional handle never reaches a remote URL — connect wires the real one.
708
+ //
709
+ // release review fix-pass: read the cache DIRECTLY (no getIdentity / TTL /
710
+ // gh-refresh). A provisional cache → local-first regardless of its age (connect,
711
+ // not a silent init refresh, is where it reconciles); a gh-cli cache →
712
+ // connected. Only when there is NO cache do we probe gh (the genuine
713
+ // connected-fresh-vs-no-gh decision). Mirrors init-bootstrap's local-first
714
+ // trigger so the command + flow agree.
715
+ function isLocalFirstContext() {
716
+ // MF1 — the provisional-cache determination is the shared resolveLocalFirst
717
+ // predicate (kills the triplication: router + doFreshBranch + here). When a
718
+ // cache exists its verdict is authoritative; only a NO-cache machine probes gh.
719
+ const cached = readIdentityCache();
720
+ if (cached !== null)
721
+ return resolveLocalFirst(cached);
722
+ // No cache → local-first iff no gh handle resolves (gh absent/unauthed).
723
+ try {
724
+ getHandleFromIdentity();
725
+ return false;
726
+ }
727
+ catch {
728
+ return true;
729
+ }
730
+ }
731
+ function emitError(json, body) {
732
+ if (json) {
733
+ // eslint-disable-next-line no-console
734
+ console.error(JSON.stringify(body, null, 2));
735
+ }
736
+ else {
737
+ // eslint-disable-next-line no-console
738
+ console.error(`lyt init: ${String(body["message"] ?? body["error"])}`);
739
+ }
740
+ }
741
+ //# sourceMappingURL=init.js.map