@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.
- package/LICENSE +200 -0
- package/NOTICE +23 -0
- package/README.md +117 -0
- package/dist/automator-bodies/arc-builder.d.ts +13 -0
- package/dist/automator-bodies/arc-builder.d.ts.map +1 -0
- package/dist/automator-bodies/arc-builder.js +34 -0
- package/dist/automator-bodies/arc-builder.js.map +1 -0
- package/dist/automator-bodies/index.d.ts +20 -0
- package/dist/automator-bodies/index.d.ts.map +1 -0
- package/dist/automator-bodies/index.js +32 -0
- package/dist/automator-bodies/index.js.map +1 -0
- package/dist/automator-bodies/lane-builder.d.ts +12 -0
- package/dist/automator-bodies/lane-builder.d.ts.map +1 -0
- package/dist/automator-bodies/lane-builder.js +34 -0
- package/dist/automator-bodies/lane-builder.js.map +1 -0
- package/dist/automator-bodies/metadata-filler.d.ts +34 -0
- package/dist/automator-bodies/metadata-filler.d.ts.map +1 -0
- package/dist/automator-bodies/metadata-filler.js +211 -0
- package/dist/automator-bodies/metadata-filler.js.map +1 -0
- package/dist/automator-run.d.ts +27 -0
- package/dist/automator-run.d.ts.map +1 -0
- package/dist/automator-run.js +137 -0
- package/dist/automator-run.js.map +1 -0
- package/dist/bench/graded-corpus.d.ts +5 -0
- package/dist/bench/graded-corpus.d.ts.map +1 -0
- package/dist/bench/graded-corpus.js +121 -0
- package/dist/bench/graded-corpus.js.map +1 -0
- package/dist/bench/invariant-corpus.d.ts +18 -0
- package/dist/bench/invariant-corpus.d.ts.map +1 -0
- package/dist/bench/invariant-corpus.js +181 -0
- package/dist/bench/invariant-corpus.js.map +1 -0
- package/dist/bench/ir-metrics.d.ts +33 -0
- package/dist/bench/ir-metrics.d.ts.map +1 -0
- package/dist/bench/ir-metrics.js +79 -0
- package/dist/bench/ir-metrics.js.map +1 -0
- package/dist/bench/latency-bench.d.ts +15 -0
- package/dist/bench/latency-bench.d.ts.map +1 -0
- package/dist/bench/latency-bench.js +68 -0
- package/dist/bench/latency-bench.js.map +1 -0
- package/dist/bench/latency-corpus.d.ts +4 -0
- package/dist/bench/latency-corpus.d.ts.map +1 -0
- package/dist/bench/latency-corpus.js +109 -0
- package/dist/bench/latency-corpus.js.map +1 -0
- package/dist/bench/pod-harness.d.ts +16 -0
- package/dist/bench/pod-harness.d.ts.map +1 -0
- package/dist/bench/pod-harness.js +127 -0
- package/dist/bench/pod-harness.js.map +1 -0
- package/dist/bench/run-bench.d.ts +20 -0
- package/dist/bench/run-bench.d.ts.map +1 -0
- package/dist/bench/run-bench.js +86 -0
- package/dist/bench/run-bench.js.map +1 -0
- package/dist/cli-automator-run.d.ts +7 -0
- package/dist/cli-automator-run.d.ts.map +1 -0
- package/dist/cli-automator-run.js +80 -0
- package/dist/cli-automator-run.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +161 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/bench.d.ts +3 -0
- package/dist/commands/bench.d.ts.map +1 -0
- package/dist/commands/bench.js +92 -0
- package/dist/commands/bench.js.map +1 -0
- package/dist/commands/capture.d.ts +4 -0
- package/dist/commands/capture.d.ts.map +1 -0
- package/dist/commands/capture.js +251 -0
- package/dist/commands/capture.js.map +1 -0
- package/dist/commands/init.d.ts +12 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +741 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/primer.d.ts +3 -0
- package/dist/commands/primer.d.ts.map +1 -0
- package/dist/commands/primer.js +199 -0
- package/dist/commands/primer.js.map +1 -0
- package/dist/commands/reindex.d.ts +3 -0
- package/dist/commands/reindex.d.ts.map +1 -0
- package/dist/commands/reindex.js +105 -0
- package/dist/commands/reindex.js.map +1 -0
- package/dist/commands/search.d.ts +3 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/search.js +224 -0
- package/dist/commands/search.js.map +1 -0
- package/dist/flows/heal.d.ts +29 -0
- package/dist/flows/heal.d.ts.map +1 -0
- package/dist/flows/heal.js +146 -0
- package/dist/flows/heal.js.map +1 -0
- package/dist/flows/init-bootstrap.d.ts +93 -0
- package/dist/flows/init-bootstrap.d.ts.map +1 -0
- package/dist/flows/init-bootstrap.js +561 -0
- package/dist/flows/init-bootstrap.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- 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
|