@ouro.bot/cli 0.1.0-alpha.417 → 0.1.0-alpha.418
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/changelog.json +12 -0
- package/dist/heart/daemon/cli-defaults.js +8 -2
- package/dist/heart/daemon/cli-exec.js +46 -22
- package/dist/heart/daemon/daemon-runtime-sync.js +6 -12
- package/dist/heart/daemon/startup-tui.js +30 -8
- package/dist/heart/daemon/up-progress.js +70 -13
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.418",
|
|
6
|
+
"changes": [
|
|
7
|
+
"`ouro up` now uses the live progress renderer in interactive terminals instead of forcing static non-TTY output, so long startup/check phases animate instead of leaving a blinking cursor.",
|
|
8
|
+
"Non-TTY and captured `ouro up` output now prints current phase starts and changed detail lines (`launching daemon process`, `waiting for daemon socket`, `daemon answered`) before completion, avoiding silent waits in logs and terminal sessions that cannot render spinners.",
|
|
9
|
+
"Daemon startup readiness is completed explicitly before provider checks begin; `ouro up` no longer marks `starting daemon` as done by accidentally auto-completing it when the next phase starts.",
|
|
10
|
+
"Daemon startup polling can now report progress into the parent `ouro up` checklist without rendering its own nested startup TUI, keeping startup output as one coherent surface.",
|
|
11
|
+
"Runtime drift restart messages now summarize drift categories such as `code path` or `managed agents` without printing raw worktree/package paths in normal CLI output.",
|
|
12
|
+
"Default CLI stdout now writes exactly one newline instead of using `console.log`, removing the extra blank lines caused when progress renderers already include newline-terminated output.",
|
|
13
|
+
"`@ouro.bot/cli` and the `ouro.bot` wrapper are version-synced for the `ouro up` progress polish release."
|
|
14
|
+
]
|
|
15
|
+
},
|
|
4
16
|
{
|
|
5
17
|
"version": "0.1.0-alpha.417",
|
|
6
18
|
"changes": [
|
|
@@ -89,9 +89,13 @@ function defaultStartDaemonProcess(socketPath) {
|
|
|
89
89
|
return Promise.resolve({ pid: child.pid ?? null });
|
|
90
90
|
}
|
|
91
91
|
function defaultWriteStdout(text) {
|
|
92
|
-
|
|
93
|
-
console.log(text);
|
|
92
|
+
process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
|
|
94
93
|
}
|
|
94
|
+
/* v8 ignore start -- thin terminal adapter around process stdout @preserve */
|
|
95
|
+
function defaultWriteRaw(text) {
|
|
96
|
+
process.stdout.write(text);
|
|
97
|
+
}
|
|
98
|
+
/* v8 ignore stop */
|
|
95
99
|
/**
|
|
96
100
|
* Read the runtimeVersion from the first .ouro bundle's bundle-meta.json.
|
|
97
101
|
* Returns undefined if none found or unreadable.
|
|
@@ -486,6 +490,8 @@ function createDefaultOuroCliDeps(socketPath = socket_client_1.DEFAULT_DAEMON_SO
|
|
|
486
490
|
sendCommand: socket_client_1.sendDaemonCommand,
|
|
487
491
|
startDaemonProcess: defaultStartDaemonProcess,
|
|
488
492
|
writeStdout: defaultWriteStdout,
|
|
493
|
+
writeRaw: defaultWriteRaw,
|
|
494
|
+
isTTY: process.stdout.isTTY === true,
|
|
489
495
|
checkSocketAlive: socket_client_1.checkDaemonSocketAlive,
|
|
490
496
|
cleanupStaleSocket: defaultCleanupStaleSocket,
|
|
491
497
|
fallbackPendingMessage: defaultFallbackPendingMessage,
|
|
@@ -179,6 +179,15 @@ function providerRepairCountSummary(count) {
|
|
|
179
179
|
return "ok";
|
|
180
180
|
return `${count} ${count === 1 ? "needs" : "need"} attention`;
|
|
181
181
|
}
|
|
182
|
+
function daemonProgressSummary(result) {
|
|
183
|
+
if (result.verifyStartupStatus === false)
|
|
184
|
+
return "not answering yet";
|
|
185
|
+
if (result.alreadyRunning)
|
|
186
|
+
return "already running";
|
|
187
|
+
if (result.message.includes("restarted"))
|
|
188
|
+
return "restarted and ready";
|
|
189
|
+
return "ready";
|
|
190
|
+
}
|
|
182
191
|
async function reportPostRepairProviderHealth(deps, repairedAgents, onProgress) {
|
|
183
192
|
const remainingDegraded = await checkAgentProviders(deps, repairedAgents, onProgress);
|
|
184
193
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -314,17 +323,19 @@ async function ensureDaemonRunning(deps, options = {}) {
|
|
|
314
323
|
sendCommand: deps.sendCommand,
|
|
315
324
|
socketPath: deps.socketPath,
|
|
316
325
|
daemonPid: runtimeResult.startedPid ?? null,
|
|
317
|
-
/* v8 ignore next -- thin wrapper:
|
|
318
|
-
writeRaw: (text) => process.stdout.write(text),
|
|
319
|
-
/* v8 ignore next -- thin wrapper: real stdout TTY detection injected
|
|
320
|
-
isTTY: process.stdout.isTTY === true,
|
|
321
|
-
/* v8 ignore next -- thin wrapper: real Date.now
|
|
322
|
-
now: () => Date.now(),
|
|
323
|
-
/* v8 ignore next -- thin wrapper: real setTimeout injected
|
|
324
|
-
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
326
|
+
/* v8 ignore next -- thin wrapper: real stdout fallback injected by default deps @preserve */
|
|
327
|
+
writeRaw: deps.writeRaw ?? ((text) => process.stdout.write(text)),
|
|
328
|
+
/* v8 ignore next -- thin wrapper: real stdout TTY detection injected by default deps @preserve */
|
|
329
|
+
isTTY: deps.isTTY ?? process.stdout.isTTY === true,
|
|
330
|
+
/* v8 ignore next -- thin wrapper: real Date.now fallback injected by default deps @preserve */
|
|
331
|
+
now: deps.now ?? (() => Date.now()),
|
|
332
|
+
/* v8 ignore next -- thin wrapper: real setTimeout fallback injected by default deps @preserve */
|
|
333
|
+
sleep: deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
325
334
|
/* v8 ignore start -- daemon log tail + pid check: reads real filesystem, tested via deployment @preserve */
|
|
326
335
|
readLatestDaemonEvent: readLatestDaemonStartupEvent,
|
|
327
336
|
/* v8 ignore stop */
|
|
337
|
+
onProgress: deps.reportDaemonStartupPhase,
|
|
338
|
+
render: !deps.reportDaemonStartupPhase,
|
|
328
339
|
});
|
|
329
340
|
return {
|
|
330
341
|
alreadyRunning: runtimeResult.alreadyRunning,
|
|
@@ -339,8 +350,8 @@ async function ensureDaemonRunning(deps, options = {}) {
|
|
|
339
350
|
};
|
|
340
351
|
let lastPid = null;
|
|
341
352
|
for (let attempt = 0; attempt <= retryLimit; attempt += 1) {
|
|
342
|
-
deps.reportDaemonStartupPhase?.("
|
|
343
|
-
deps.reportDaemonStartupPhase?.("waiting for daemon socket
|
|
353
|
+
deps.reportDaemonStartupPhase?.("launching daemon process");
|
|
354
|
+
deps.reportDaemonStartupPhase?.("waiting for daemon socket");
|
|
344
355
|
deps.cleanupStaleSocket(deps.socketPath);
|
|
345
356
|
const bootStartedAtMs = (deps.now ?? Date.now)();
|
|
346
357
|
const started = await deps.startDaemonProcess(deps.socketPath);
|
|
@@ -354,17 +365,19 @@ async function ensureDaemonRunning(deps, options = {}) {
|
|
|
354
365
|
sendCommand: deps.sendCommand,
|
|
355
366
|
socketPath: deps.socketPath,
|
|
356
367
|
daemonPid: lastPid,
|
|
357
|
-
/* v8 ignore next -- thin wrapper:
|
|
358
|
-
writeRaw: (text) => process.stdout.write(text),
|
|
359
|
-
/* v8 ignore next -- thin wrapper: real stdout TTY detection injected
|
|
360
|
-
isTTY: process.stdout.isTTY === true,
|
|
361
|
-
/* v8 ignore next -- thin wrapper: real Date.now
|
|
362
|
-
now: () => Date.now(),
|
|
363
|
-
/* v8 ignore next -- thin wrapper: real setTimeout injected
|
|
364
|
-
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
368
|
+
/* v8 ignore next -- thin wrapper: real stdout fallback injected by default deps @preserve */
|
|
369
|
+
writeRaw: deps.writeRaw ?? ((text) => process.stdout.write(text)),
|
|
370
|
+
/* v8 ignore next -- thin wrapper: real stdout TTY detection injected by default deps @preserve */
|
|
371
|
+
isTTY: deps.isTTY ?? process.stdout.isTTY === true,
|
|
372
|
+
/* v8 ignore next -- thin wrapper: real Date.now fallback injected by default deps @preserve */
|
|
373
|
+
now: deps.now ?? (() => Date.now()),
|
|
374
|
+
/* v8 ignore next -- thin wrapper: real setTimeout fallback injected by default deps @preserve */
|
|
375
|
+
sleep: deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
365
376
|
/* v8 ignore start -- daemon log tail + pid check: reads real filesystem, tested via deployment @preserve */
|
|
366
377
|
readLatestDaemonEvent: readLatestDaemonStartupEvent,
|
|
367
378
|
/* v8 ignore stop */
|
|
379
|
+
onProgress: deps.reportDaemonStartupPhase,
|
|
380
|
+
render: !deps.reportDaemonStartupPhase,
|
|
368
381
|
});
|
|
369
382
|
return {
|
|
370
383
|
alreadyRunning: false,
|
|
@@ -376,7 +389,7 @@ async function ensureDaemonRunning(deps, options = {}) {
|
|
|
376
389
|
if (!startupFailure.retryable || attempt >= retryLimit) {
|
|
377
390
|
break;
|
|
378
391
|
}
|
|
379
|
-
deps.reportDaemonStartupPhase?.("daemon startup lost stability;
|
|
392
|
+
deps.reportDaemonStartupPhase?.("daemon startup lost stability; retrying once");
|
|
380
393
|
}
|
|
381
394
|
return {
|
|
382
395
|
alreadyRunning: false,
|
|
@@ -449,7 +462,7 @@ async function waitForDaemonStartup(deps, options) {
|
|
|
449
462
|
if (!sawSocket) {
|
|
450
463
|
sawSocket = true;
|
|
451
464
|
stableSinceMs = now();
|
|
452
|
-
deps.reportDaemonStartupPhase?.("verifying daemon health
|
|
465
|
+
deps.reportDaemonStartupPhase?.("verifying daemon health");
|
|
453
466
|
}
|
|
454
467
|
if (!hasFreshCurrentBootHealthSignal(deps, options.bootStartedAtMs, options.pid)) {
|
|
455
468
|
continue;
|
|
@@ -2469,7 +2482,13 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
2469
2482
|
}
|
|
2470
2483
|
}
|
|
2471
2484
|
const linkedVersionBeforeUp = deps.getCurrentCliVersion?.() ?? null;
|
|
2472
|
-
const
|
|
2485
|
+
const outputIsTTY = deps.isTTY ?? process.stdout.isTTY === true;
|
|
2486
|
+
const progress = new up_progress_1.UpProgress({
|
|
2487
|
+
write: deps.writeRaw ?? deps.writeStdout,
|
|
2488
|
+
isTTY: outputIsTTY,
|
|
2489
|
+
now: deps.now ?? (() => Date.now()),
|
|
2490
|
+
autoRender: true,
|
|
2491
|
+
});
|
|
2473
2492
|
// ── versioned CLI update check ──
|
|
2474
2493
|
if (deps.checkForCliUpdate) {
|
|
2475
2494
|
progress.startPhase("update check");
|
|
@@ -2631,6 +2650,12 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
2631
2650
|
progress.announceStep?.(label);
|
|
2632
2651
|
},
|
|
2633
2652
|
}, { initialAlive: daemonAliveBeforeStart });
|
|
2653
|
+
progress.completePhase("starting daemon", daemonProgressSummary(daemonResult));
|
|
2654
|
+
if (daemonResult.verifyStartupStatus === false) {
|
|
2655
|
+
progress.end();
|
|
2656
|
+
deps.writeStdout(daemonResult.message);
|
|
2657
|
+
return daemonResult.message;
|
|
2658
|
+
}
|
|
2634
2659
|
if (!providerChecksAlreadyRun || daemonResult.alreadyRunning) {
|
|
2635
2660
|
progress.startPhase("provider checks");
|
|
2636
2661
|
const providerDegraded = await checkAlreadyRunningAgentProviders(deps, (msg) => progress.updateDetail(msg));
|
|
@@ -2638,7 +2663,6 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
2638
2663
|
progress.completePhase("provider checks", providerRepairCountSummary(providerDegraded.length));
|
|
2639
2664
|
}
|
|
2640
2665
|
progress.end();
|
|
2641
|
-
deps.writeStdout(daemonResult.message);
|
|
2642
2666
|
// Interactive repair for degraded agents (Unit 5) — skipped by --no-repair (Unit 6)
|
|
2643
2667
|
if (daemonResult.stability?.degraded && daemonResult.stability.degraded.length > 0) {
|
|
2644
2668
|
if (command.noRepair) {
|
|
@@ -83,14 +83,8 @@ function collectRuntimeDriftReasons(local, running) {
|
|
|
83
83
|
}
|
|
84
84
|
return reasons;
|
|
85
85
|
}
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
return `${reason.running.slice(0, 12)} -> ${reason.local.slice(0, 12)}`;
|
|
89
|
-
}
|
|
90
|
-
return `${reason.running} -> ${reason.local}`;
|
|
91
|
-
}
|
|
92
|
-
function formatRuntimeDriftSummary(reasons) {
|
|
93
|
-
return reasons.map((reason) => `${reason.label} ${formatRuntimeValue(reason)}`).join("; ");
|
|
86
|
+
function formatRuntimeDriftPublicSummary(reasons) {
|
|
87
|
+
return reasons.map((reason) => reason.label).join(", ");
|
|
94
88
|
}
|
|
95
89
|
async function ensureCurrentDaemonRuntime(deps) {
|
|
96
90
|
const localRuntime = normalizeRuntimeIdentity({
|
|
@@ -107,7 +101,7 @@ async function ensureCurrentDaemonRuntime(deps) {
|
|
|
107
101
|
let result;
|
|
108
102
|
if (driftReasons.length > 0) {
|
|
109
103
|
const includesVersionDrift = driftReasons.some((entry) => entry.key === "version");
|
|
110
|
-
const
|
|
104
|
+
const publicDriftSummary = formatRuntimeDriftPublicSummary(driftReasons);
|
|
111
105
|
try {
|
|
112
106
|
await deps.stopDaemon();
|
|
113
107
|
}
|
|
@@ -117,7 +111,7 @@ async function ensureCurrentDaemonRuntime(deps) {
|
|
|
117
111
|
alreadyRunning: true,
|
|
118
112
|
message: includesVersionDrift
|
|
119
113
|
? `daemon already running (${deps.socketPath}; could not replace stale daemon ${runningVersion} -> ${deps.localVersion}: ${reason})`
|
|
120
|
-
: `daemon already running (${deps.socketPath}; could not replace
|
|
114
|
+
: `daemon already running (${deps.socketPath}; could not replace runtime drift ${publicDriftSummary}: ${reason})`,
|
|
121
115
|
};
|
|
122
116
|
(0, runtime_1.emitNervesEvent)({
|
|
123
117
|
level: "warn",
|
|
@@ -148,12 +142,12 @@ async function ensureCurrentDaemonRuntime(deps) {
|
|
|
148
142
|
const pid = started.pid ?? "unknown";
|
|
149
143
|
const verified = await verifyDaemonStarted(deps);
|
|
150
144
|
/* v8 ignore next -- daemon liveness failure: requires real daemon crash timing @preserve */
|
|
151
|
-
const suffix = verified ? "" : "\ndaemon
|
|
145
|
+
const suffix = verified ? "" : "\ndaemon restart has not answered yet; check logs with `ouro logs` or run `ouro doctor`.";
|
|
152
146
|
result = {
|
|
153
147
|
alreadyRunning: false,
|
|
154
148
|
message: includesVersionDrift
|
|
155
149
|
? `restarted stale daemon ${runningVersion} -> ${deps.localVersion} (pid ${pid})${suffix}`
|
|
156
|
-
: `restarted
|
|
150
|
+
: `restarted daemon after runtime drift: ${publicDriftSummary} (pid ${pid})${suffix}`,
|
|
157
151
|
verifyStartupStatus: verified,
|
|
158
152
|
startedPid: started.pid ?? null,
|
|
159
153
|
};
|
|
@@ -134,6 +134,14 @@ async function pollDaemonStartup(deps) {
|
|
|
134
134
|
let prevLineCount = 0;
|
|
135
135
|
const isTTY = deps.isTTY ?? true;
|
|
136
136
|
const isAlive = deps.isProcessAlive ?? defaultIsProcessAlive;
|
|
137
|
+
const shouldRender = deps.render ?? true;
|
|
138
|
+
let lastProgress = null;
|
|
139
|
+
const reportProgress = (message) => {
|
|
140
|
+
if (!deps.onProgress || message === lastProgress)
|
|
141
|
+
return;
|
|
142
|
+
lastProgress = message;
|
|
143
|
+
deps.onProgress(message);
|
|
144
|
+
};
|
|
137
145
|
(0, runtime_1.emitNervesEvent)({
|
|
138
146
|
component: "daemon",
|
|
139
147
|
event: "daemon.startup_poll_start",
|
|
@@ -174,22 +182,30 @@ async function pollDaemonStartup(deps) {
|
|
|
174
182
|
}
|
|
175
183
|
// Show what the daemon is doing from its log
|
|
176
184
|
const latestEvent = deps.readLatestDaemonEvent?.() ?? null;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
185
|
+
reportProgress(latestEvent ?? "waiting for daemon");
|
|
186
|
+
if (shouldRender) {
|
|
187
|
+
const output = renderWaitingForDaemon(elapsed, latestEvent, prevLineCount, { isTTY });
|
|
188
|
+
deps.writeRaw(output);
|
|
189
|
+
prevLineCount = latestEvent ? 2 : 1;
|
|
190
|
+
}
|
|
180
191
|
}
|
|
181
192
|
if (payload) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
193
|
+
reportProgress(formatStartupProgressDetail(payload));
|
|
194
|
+
if (shouldRender) {
|
|
195
|
+
const output = renderStartupProgress(payload, elapsed, prevLineCount, { isTTY });
|
|
196
|
+
deps.writeRaw(output);
|
|
197
|
+
prevLineCount = payload.workers.length + 1;
|
|
198
|
+
}
|
|
185
199
|
const assessment = assessStability(payload, now);
|
|
186
200
|
if (assessment.resolved) {
|
|
187
201
|
const result = {
|
|
188
202
|
stable: assessment.stable,
|
|
189
203
|
degraded: assessment.degraded,
|
|
190
204
|
};
|
|
191
|
-
|
|
192
|
-
|
|
205
|
+
if (shouldRender) {
|
|
206
|
+
const summary = renderFinalSummary(result, isTTY);
|
|
207
|
+
deps.writeRaw(summary);
|
|
208
|
+
}
|
|
193
209
|
(0, runtime_1.emitNervesEvent)({
|
|
194
210
|
component: "daemon",
|
|
195
211
|
event: "daemon.startup_poll_end",
|
|
@@ -206,6 +222,12 @@ async function pollDaemonStartup(deps) {
|
|
|
206
222
|
await deps.sleep(POLL_INTERVAL_MS);
|
|
207
223
|
}
|
|
208
224
|
}
|
|
225
|
+
function formatStartupProgressDetail(payload) {
|
|
226
|
+
if (payload.workers.length === 0)
|
|
227
|
+
return "daemon answered";
|
|
228
|
+
const workers = payload.workers.map((worker) => `${worker.agent}/${worker.worker} ${worker.status}`).join(", ");
|
|
229
|
+
return `waiting for agents: ${workers}`;
|
|
230
|
+
}
|
|
209
231
|
function colorStatus(status) {
|
|
210
232
|
const statusColor = status === "running" ? GREEN
|
|
211
233
|
: status === "crashed" ? RED
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* cursor control for in-place overwriting in TTY mode, and falls back to
|
|
8
8
|
* static line-per-phase output in non-TTY mode.
|
|
9
9
|
*
|
|
10
|
-
* The caller
|
|
11
|
-
*
|
|
10
|
+
* The caller can drive animation by calling `render(now)`. In production CLI
|
|
11
|
+
* use, `autoRender` starts a short-lived timer while a TTY phase is active so
|
|
12
|
+
* long operations never leave a dead-looking cursor.
|
|
12
13
|
*/
|
|
13
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
15
|
exports.UpProgress = void 0;
|
|
@@ -23,15 +24,30 @@ const GREEN = "\x1b[38;2;46;204;64m";
|
|
|
23
24
|
class UpProgress {
|
|
24
25
|
write;
|
|
25
26
|
isTTY;
|
|
27
|
+
now;
|
|
28
|
+
autoRender;
|
|
29
|
+
renderIntervalMs;
|
|
30
|
+
setTimer;
|
|
31
|
+
clearTimer;
|
|
26
32
|
completed = [];
|
|
27
33
|
currentPhase = null;
|
|
34
|
+
currentDetail = null;
|
|
28
35
|
prevLineCount = 0;
|
|
29
36
|
ended = false;
|
|
37
|
+
renderTimer = null;
|
|
30
38
|
constructor(options) {
|
|
31
39
|
/* v8 ignore next -- thin wrapper: raw process.stdout.write for ANSI cursor control @preserve */
|
|
32
40
|
this.write = options?.write ?? ((text) => process.stdout.write(text));
|
|
33
41
|
/* v8 ignore next -- thin wrapper: real isTTY check injected for testability @preserve */
|
|
34
42
|
this.isTTY = options?.isTTY ?? (process.stdout.isTTY === true);
|
|
43
|
+
/* v8 ignore next -- thin wrapper: real Date.now injected for testability @preserve */
|
|
44
|
+
this.now = options?.now ?? (() => Date.now());
|
|
45
|
+
this.autoRender = options?.autoRender ?? false;
|
|
46
|
+
this.renderIntervalMs = options?.renderIntervalMs ?? 80;
|
|
47
|
+
/* v8 ignore start -- real timers are injected in tests when needed @preserve */
|
|
48
|
+
this.setTimer = options?.setInterval ?? ((callback, ms) => setInterval(callback, ms));
|
|
49
|
+
this.clearTimer = options?.clearInterval ?? ((handle) => clearInterval(handle));
|
|
50
|
+
/* v8 ignore stop */
|
|
35
51
|
}
|
|
36
52
|
/**
|
|
37
53
|
* Begin a new phase with spinner. If a phase is already active, it is
|
|
@@ -41,26 +57,44 @@ class UpProgress {
|
|
|
41
57
|
if (this.currentPhase) {
|
|
42
58
|
this.completePhase(this.currentPhase.label);
|
|
43
59
|
}
|
|
44
|
-
this.currentPhase = { label, startedAt:
|
|
60
|
+
this.currentPhase = { label, startedAt: this.now() };
|
|
61
|
+
this.currentDetail = null;
|
|
62
|
+
if (this.isTTY) {
|
|
63
|
+
this.ensureAutoRender();
|
|
64
|
+
this.flushRender();
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
this.write(` ... ${label}\n`);
|
|
68
|
+
}
|
|
45
69
|
}
|
|
46
70
|
/**
|
|
47
71
|
* Emit a one-line status breadcrumb in non-TTY mode without affecting the
|
|
48
72
|
* accumulated checklist state. Used for daemon startup sub-steps.
|
|
49
73
|
*/
|
|
50
74
|
announceStep(label) {
|
|
75
|
+
if (this.currentPhase) {
|
|
76
|
+
this.updateDetail(label);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
51
79
|
if (this.isTTY)
|
|
52
80
|
return;
|
|
53
|
-
this.write(label);
|
|
81
|
+
this.write(` ${label}\n`);
|
|
54
82
|
}
|
|
55
83
|
/**
|
|
56
84
|
* Update the sub-step detail on the current spinner phase. Rendered as
|
|
57
|
-
* "label (Xs) -- detail" in TTY mode.
|
|
58
|
-
*
|
|
85
|
+
* "label (Xs) -- detail" in TTY mode. In non-TTY mode, writes changed
|
|
86
|
+
* detail lines so long operations remain visible in logs and captured output.
|
|
59
87
|
*/
|
|
60
88
|
updateDetail(detail) {
|
|
61
|
-
if (!this.
|
|
89
|
+
if (!this.currentPhase || detail === this.currentDetail)
|
|
62
90
|
return;
|
|
91
|
+
this.currentDetail = detail;
|
|
63
92
|
this.currentPhase.detail = detail;
|
|
93
|
+
if (this.isTTY) {
|
|
94
|
+
this.flushRender();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this.write(` ${detail}\n`);
|
|
64
98
|
}
|
|
65
99
|
/**
|
|
66
100
|
* Mark the current phase as done. In non-TTY mode, immediately writes
|
|
@@ -70,16 +104,21 @@ class UpProgress {
|
|
|
70
104
|
if (!this.currentPhase) {
|
|
71
105
|
return;
|
|
72
106
|
}
|
|
73
|
-
const elapsedMs =
|
|
107
|
+
const elapsedMs = this.now() - this.currentPhase.startedAt;
|
|
74
108
|
this.completed.push({ label, detail });
|
|
75
109
|
this.currentPhase = null;
|
|
110
|
+
this.currentDetail = null;
|
|
111
|
+
this.stopAutoRender();
|
|
76
112
|
(0, runtime_1.emitNervesEvent)({
|
|
77
113
|
component: "daemon",
|
|
78
114
|
event: "daemon.up_phase_complete",
|
|
79
115
|
message: `phase complete: ${label}`,
|
|
80
116
|
meta: { phase: label, detail: detail ?? null, elapsedMs },
|
|
81
117
|
});
|
|
82
|
-
if (
|
|
118
|
+
if (this.isTTY) {
|
|
119
|
+
this.flushRender();
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
83
122
|
const detailStr = detail ? ` \u2014 ${detail}` : "";
|
|
84
123
|
this.write(` \u2713 ${label}${detailStr}\n`);
|
|
85
124
|
}
|
|
@@ -134,12 +173,30 @@ class UpProgress {
|
|
|
134
173
|
this.ended = true;
|
|
135
174
|
if (this.currentPhase) {
|
|
136
175
|
this.currentPhase = null;
|
|
176
|
+
this.currentDetail = null;
|
|
137
177
|
}
|
|
178
|
+
this.stopAutoRender();
|
|
138
179
|
if (this.isTTY) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
180
|
+
this.flushRender();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
ensureAutoRender() {
|
|
184
|
+
if (!this.autoRender || !this.isTTY || this.renderTimer !== null) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
this.renderTimer = this.setTimer(() => this.flushRender(), this.renderIntervalMs);
|
|
188
|
+
}
|
|
189
|
+
stopAutoRender() {
|
|
190
|
+
if (this.renderTimer === null) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
this.clearTimer(this.renderTimer);
|
|
194
|
+
this.renderTimer = null;
|
|
195
|
+
}
|
|
196
|
+
flushRender() {
|
|
197
|
+
const output = this.render(this.now());
|
|
198
|
+
if (output) {
|
|
199
|
+
this.write(output);
|
|
143
200
|
}
|
|
144
201
|
}
|
|
145
202
|
}
|