@ouro.bot/cli 0.1.0-alpha.417 → 0.1.0-alpha.419
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/README.md +2 -1
- package/changelog.json +23 -0
- package/dist/heart/daemon/cli-defaults.js +8 -2
- package/dist/heart/daemon/cli-exec.js +189 -84
- 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 +93 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -99,6 +99,7 @@ Task docs do not live in this repo anymore. Planning and doing docs live in the
|
|
|
99
99
|
- Vault unlock material is local machine state. Prefer macOS Keychain, Windows DPAPI, or Linux Secret Service; plaintext fallback is allowed only by explicit human choice.
|
|
100
100
|
- New vault unlock secrets are confirmed before use and rejected if they do not meet the minimum strength requirements.
|
|
101
101
|
- Provider and runtime credentials are loaded into process memory at startup/auth/unlock/refresh and reused. The remote vault is not queried for every model or sense request.
|
|
102
|
+
- Human-facing CLI commands that can wait on browser auth, vault IO, daemon startup, daemon restart, provider checks, or connector setup use a shared progress checklist. If a cursor may blink for more than a few seconds, the command should print or animate the current step instead of going quiet.
|
|
102
103
|
- CLI commands that mutate bundle config, such as vault setup or `ouro connect bluebubbles`, run bundle sync after the change when `sync.enabled` is true and report a compact `bundle sync:` line.
|
|
103
104
|
- The daemon discovers bundles dynamically from `~/AgentBundles`.
|
|
104
105
|
- `ouro status` reports version, last-updated time, discovered agents, senses, and workers.
|
|
@@ -120,7 +121,7 @@ ouro auth --agent <name>
|
|
|
120
121
|
ouro auth --agent <name> --provider <provider>
|
|
121
122
|
```
|
|
122
123
|
|
|
123
|
-
`ouro auth` stores credentials in the owning agent's vault. It does not switch a lane or write provider/model selection.
|
|
124
|
+
`ouro auth` stores credentials in the owning agent's vault. It does not switch a lane or write provider/model selection. The command shows progress while browser login, vault storage, refresh, and verification are happening.
|
|
124
125
|
|
|
125
126
|
When you want this machine to use a provider/model for a lane, use:
|
|
126
127
|
|
package/changelog.json
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
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.419",
|
|
6
|
+
"changes": [
|
|
7
|
+
"`ouro auth`, `ouro provider refresh`, `ouro connect perplexity`, and `ouro connect bluebubbles` now use the shared human CLI progress checklist so browser login, vault reads/writes, daemon reloads, and verification steps stay visible instead of leaving a blinking cursor.",
|
|
8
|
+
"`ouro connect perplexity` now introduces the flow before the hidden API-key prompt, shows vault storage and reload progress, keeps the key out of output, and ends with a compact next step for using Perplexity search.",
|
|
9
|
+
"`ouro connect bluebubbles` now introduces the local-machine attachment before prompts, shows machine runtime config and `agent.json` enablement progress, and keeps the app password out of output.",
|
|
10
|
+
"`ouro provider refresh` now shows vault-read progress and reload status, and refresh/auth/connect failure paths preserve the last visible progress context while still rethrowing actionable errors.",
|
|
11
|
+
"The CLI progress renderer is documented as a shared human-facing contract: long-running human flows should show current work, stay concise, and never print secrets.",
|
|
12
|
+
"`@ouro.bot/cli` and the `ouro.bot` wrapper are version-synced for the provider onboarding progress release."
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"version": "0.1.0-alpha.418",
|
|
17
|
+
"changes": [
|
|
18
|
+
"`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.",
|
|
19
|
+
"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.",
|
|
20
|
+
"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.",
|
|
21
|
+
"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.",
|
|
22
|
+
"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.",
|
|
23
|
+
"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.",
|
|
24
|
+
"`@ouro.bot/cli` and the `ouro.bot` wrapper are version-synced for the `ouro up` progress polish release."
|
|
25
|
+
]
|
|
26
|
+
},
|
|
4
27
|
{
|
|
5
28
|
"version": "0.1.0-alpha.417",
|
|
6
29
|
"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,25 @@ function providerRepairCountSummary(count) {
|
|
|
179
179
|
return "ok";
|
|
180
180
|
return `${count} ${count === 1 ? "needs" : "need"} attention`;
|
|
181
181
|
}
|
|
182
|
+
function createHumanCommandProgress(deps, commandName) {
|
|
183
|
+
return new up_progress_1.CommandProgress({
|
|
184
|
+
write: deps.writeRaw ?? deps.writeStdout,
|
|
185
|
+
isTTY: deps.isTTY ?? false,
|
|
186
|
+
now: deps.now ?? (() => Date.now()),
|
|
187
|
+
autoRender: true,
|
|
188
|
+
eventScope: "command",
|
|
189
|
+
commandName,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
function daemonProgressSummary(result) {
|
|
193
|
+
if (result.verifyStartupStatus === false)
|
|
194
|
+
return "not answering yet";
|
|
195
|
+
if (result.alreadyRunning)
|
|
196
|
+
return "already running";
|
|
197
|
+
if (result.message.includes("restarted"))
|
|
198
|
+
return "restarted and ready";
|
|
199
|
+
return "ready";
|
|
200
|
+
}
|
|
182
201
|
async function reportPostRepairProviderHealth(deps, repairedAgents, onProgress) {
|
|
183
202
|
const remainingDegraded = await checkAgentProviders(deps, repairedAgents, onProgress);
|
|
184
203
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -314,17 +333,19 @@ async function ensureDaemonRunning(deps, options = {}) {
|
|
|
314
333
|
sendCommand: deps.sendCommand,
|
|
315
334
|
socketPath: deps.socketPath,
|
|
316
335
|
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)),
|
|
336
|
+
/* v8 ignore next -- thin wrapper: real stdout fallback injected by default deps @preserve */
|
|
337
|
+
writeRaw: deps.writeRaw ?? ((text) => process.stdout.write(text)),
|
|
338
|
+
/* v8 ignore next -- thin wrapper: real stdout TTY detection injected by default deps @preserve */
|
|
339
|
+
isTTY: deps.isTTY ?? process.stdout.isTTY === true,
|
|
340
|
+
/* v8 ignore next -- thin wrapper: real Date.now fallback injected by default deps @preserve */
|
|
341
|
+
now: deps.now ?? (() => Date.now()),
|
|
342
|
+
/* v8 ignore next -- thin wrapper: real setTimeout fallback injected by default deps @preserve */
|
|
343
|
+
sleep: deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
325
344
|
/* v8 ignore start -- daemon log tail + pid check: reads real filesystem, tested via deployment @preserve */
|
|
326
345
|
readLatestDaemonEvent: readLatestDaemonStartupEvent,
|
|
327
346
|
/* v8 ignore stop */
|
|
347
|
+
onProgress: deps.reportDaemonStartupPhase,
|
|
348
|
+
render: !deps.reportDaemonStartupPhase,
|
|
328
349
|
});
|
|
329
350
|
return {
|
|
330
351
|
alreadyRunning: runtimeResult.alreadyRunning,
|
|
@@ -339,8 +360,8 @@ async function ensureDaemonRunning(deps, options = {}) {
|
|
|
339
360
|
};
|
|
340
361
|
let lastPid = null;
|
|
341
362
|
for (let attempt = 0; attempt <= retryLimit; attempt += 1) {
|
|
342
|
-
deps.reportDaemonStartupPhase?.("
|
|
343
|
-
deps.reportDaemonStartupPhase?.("waiting for daemon socket
|
|
363
|
+
deps.reportDaemonStartupPhase?.("launching daemon process");
|
|
364
|
+
deps.reportDaemonStartupPhase?.("waiting for daemon socket");
|
|
344
365
|
deps.cleanupStaleSocket(deps.socketPath);
|
|
345
366
|
const bootStartedAtMs = (deps.now ?? Date.now)();
|
|
346
367
|
const started = await deps.startDaemonProcess(deps.socketPath);
|
|
@@ -354,17 +375,19 @@ async function ensureDaemonRunning(deps, options = {}) {
|
|
|
354
375
|
sendCommand: deps.sendCommand,
|
|
355
376
|
socketPath: deps.socketPath,
|
|
356
377
|
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)),
|
|
378
|
+
/* v8 ignore next -- thin wrapper: real stdout fallback injected by default deps @preserve */
|
|
379
|
+
writeRaw: deps.writeRaw ?? ((text) => process.stdout.write(text)),
|
|
380
|
+
/* v8 ignore next -- thin wrapper: real stdout TTY detection injected by default deps @preserve */
|
|
381
|
+
isTTY: deps.isTTY ?? process.stdout.isTTY === true,
|
|
382
|
+
/* v8 ignore next -- thin wrapper: real Date.now fallback injected by default deps @preserve */
|
|
383
|
+
now: deps.now ?? (() => Date.now()),
|
|
384
|
+
/* v8 ignore next -- thin wrapper: real setTimeout fallback injected by default deps @preserve */
|
|
385
|
+
sleep: deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
365
386
|
/* v8 ignore start -- daemon log tail + pid check: reads real filesystem, tested via deployment @preserve */
|
|
366
387
|
readLatestDaemonEvent: readLatestDaemonStartupEvent,
|
|
367
388
|
/* v8 ignore stop */
|
|
389
|
+
onProgress: deps.reportDaemonStartupPhase,
|
|
390
|
+
render: !deps.reportDaemonStartupPhase,
|
|
368
391
|
});
|
|
369
392
|
return {
|
|
370
393
|
alreadyRunning: false,
|
|
@@ -376,7 +399,7 @@ async function ensureDaemonRunning(deps, options = {}) {
|
|
|
376
399
|
if (!startupFailure.retryable || attempt >= retryLimit) {
|
|
377
400
|
break;
|
|
378
401
|
}
|
|
379
|
-
deps.reportDaemonStartupPhase?.("daemon startup lost stability;
|
|
402
|
+
deps.reportDaemonStartupPhase?.("daemon startup lost stability; retrying once");
|
|
380
403
|
}
|
|
381
404
|
return {
|
|
382
405
|
alreadyRunning: false,
|
|
@@ -449,7 +472,7 @@ async function waitForDaemonStartup(deps, options) {
|
|
|
449
472
|
if (!sawSocket) {
|
|
450
473
|
sawSocket = true;
|
|
451
474
|
stableSinceMs = now();
|
|
452
|
-
deps.reportDaemonStartupPhase?.("verifying daemon health
|
|
475
|
+
deps.reportDaemonStartupPhase?.("verifying daemon health");
|
|
453
476
|
}
|
|
454
477
|
if (!hasFreshCurrentBootHealthSignal(deps, options.bootStartedAtMs, options.pid)) {
|
|
455
478
|
continue;
|
|
@@ -1129,18 +1152,35 @@ function runtimeScopeLabel(scope) {
|
|
|
1129
1152
|
}
|
|
1130
1153
|
async function storeRuntimeConfigKey(input) {
|
|
1131
1154
|
const machineId = input.scope === "machine" ? currentMachineId(input.deps) : undefined;
|
|
1155
|
+
input.onProgress?.("checking existing runtime config");
|
|
1132
1156
|
const current = input.scope === "machine"
|
|
1133
1157
|
? await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(input.agent, machineId, { preserveCachedOnFailure: true })
|
|
1134
1158
|
: await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(input.agent, { preserveCachedOnFailure: true });
|
|
1135
1159
|
if (!current.ok && current.reason !== "missing") {
|
|
1136
1160
|
throw new Error(`cannot read existing runtime credentials from ${current.itemPath}: ${current.error}`);
|
|
1137
1161
|
}
|
|
1162
|
+
input.onProgress?.(`storing ${input.key} in ${current.itemPath}`);
|
|
1138
1163
|
const nextConfig = setRuntimeConfigValue(current.ok ? current.config : {}, input.key, input.value);
|
|
1139
1164
|
const stored = input.scope === "machine"
|
|
1140
1165
|
? await (0, runtime_credentials_1.upsertMachineRuntimeCredentialConfig)(input.agent, machineId, nextConfig, providerCliNow(input.deps))
|
|
1141
1166
|
: await (0, runtime_credentials_1.upsertRuntimeCredentialConfig)(input.agent, nextConfig, providerCliNow(input.deps));
|
|
1167
|
+
input.onProgress?.(`stored ${input.key}; credential value was not printed`);
|
|
1142
1168
|
return { revision: stored.revision, itemPath: stored.itemPath, ...(machineId ? { machineId } : {}) };
|
|
1143
1169
|
}
|
|
1170
|
+
async function reloadRunningAgentAfterCredentialChange(agent, deps) {
|
|
1171
|
+
try {
|
|
1172
|
+
const alive = await deps.checkSocketAlive(deps.socketPath);
|
|
1173
|
+
if (!alive)
|
|
1174
|
+
return "daemon is not running; next `ouro up` will load it";
|
|
1175
|
+
const response = await deps.sendCommand(deps.socketPath, { kind: "agent.restart", agent });
|
|
1176
|
+
if (response.ok)
|
|
1177
|
+
return `restarted ${agent} so the running agent reloads credentials`;
|
|
1178
|
+
return `daemon restart skipped: ${response.error ?? response.message ?? "unknown daemon error"}`;
|
|
1179
|
+
}
|
|
1180
|
+
catch (error) {
|
|
1181
|
+
return `daemon restart skipped: ${error instanceof Error ? error.message : String(error)}`;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1144
1184
|
async function executeVaultConfigSet(command, deps) {
|
|
1145
1185
|
if (command.agent === "SerpentGuide") {
|
|
1146
1186
|
throw new Error("SerpentGuide does not have persistent runtime credentials. Store credentials in the hatchling agent vault.");
|
|
@@ -1255,18 +1295,16 @@ function enableAgentSense(agent, sense, deps) {
|
|
|
1255
1295
|
function connectMenu(agent) {
|
|
1256
1296
|
return [
|
|
1257
1297
|
`Connect ${agent}`,
|
|
1298
|
+
"Pick what this agent should be able to use.",
|
|
1258
1299
|
"",
|
|
1259
|
-
"Portable agent capabilities",
|
|
1260
1300
|
" 1. Perplexity search",
|
|
1261
|
-
"
|
|
1301
|
+
" Portable. Stores an API key in the agent vault.",
|
|
1262
1302
|
"",
|
|
1263
|
-
"This machine only",
|
|
1264
1303
|
" 2. BlueBubbles iMessage",
|
|
1265
|
-
"
|
|
1304
|
+
" This machine only. Connects a local Mac Messages bridge.",
|
|
1266
1305
|
"",
|
|
1267
|
-
"Model providers",
|
|
1268
1306
|
" 3. Provider auth",
|
|
1269
|
-
`
|
|
1307
|
+
` Model credentials: ouro auth --agent ${agent} --provider <provider>`,
|
|
1270
1308
|
"",
|
|
1271
1309
|
" 4. Cancel",
|
|
1272
1310
|
"",
|
|
@@ -1278,23 +1316,45 @@ async function executeConnectPerplexity(agent, deps) {
|
|
|
1278
1316
|
throw new Error("SerpentGuide has no persistent runtime credentials. Connect Perplexity on the hatchling agent instead.");
|
|
1279
1317
|
}
|
|
1280
1318
|
const promptSecret = requirePromptSecret(deps, "Perplexity API key entry");
|
|
1319
|
+
deps.writeStdout([
|
|
1320
|
+
`Connect Perplexity for ${agent}`,
|
|
1321
|
+
"The API key stays hidden while you type.",
|
|
1322
|
+
`Ouro stores it in ${agent}'s vault runtime/config item.`,
|
|
1323
|
+
].join("\n"));
|
|
1281
1324
|
const key = (await promptSecret("Perplexity API key: ")).trim();
|
|
1282
1325
|
if (!key)
|
|
1283
1326
|
throw new Error("Perplexity API key cannot be blank");
|
|
1284
|
-
const
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1327
|
+
const progress = createHumanCommandProgress(deps, "connect perplexity");
|
|
1328
|
+
let stored;
|
|
1329
|
+
let reload;
|
|
1330
|
+
try {
|
|
1331
|
+
progress.startPhase("saving Perplexity search");
|
|
1332
|
+
stored = await storeRuntimeConfigKey({
|
|
1333
|
+
agent,
|
|
1334
|
+
key: "integrations.perplexityApiKey",
|
|
1335
|
+
value: key,
|
|
1336
|
+
scope: "agent",
|
|
1337
|
+
deps,
|
|
1338
|
+
onProgress: (message) => progress.updateDetail(message),
|
|
1339
|
+
});
|
|
1340
|
+
progress.completePhase("saving Perplexity search", "secret stored");
|
|
1341
|
+
progress.startPhase(`reloading ${agent}`);
|
|
1342
|
+
reload = await reloadRunningAgentAfterCredentialChange(agent, deps);
|
|
1343
|
+
progress.completePhase(`reloading ${agent}`, reload);
|
|
1344
|
+
progress.end();
|
|
1345
|
+
}
|
|
1346
|
+
catch (error) {
|
|
1347
|
+
progress.end();
|
|
1348
|
+
throw error;
|
|
1349
|
+
}
|
|
1291
1350
|
const message = [
|
|
1292
1351
|
`Perplexity connected for ${agent}`,
|
|
1293
1352
|
"capability: Perplexity search",
|
|
1294
1353
|
`stored: ${stored.itemPath}`,
|
|
1354
|
+
`reload: ${reload}`,
|
|
1295
1355
|
"secret was not printed",
|
|
1296
1356
|
"",
|
|
1297
|
-
"Next: ask the agent to search
|
|
1357
|
+
"Next: ask the agent to search.",
|
|
1298
1358
|
].join("\n");
|
|
1299
1359
|
deps.writeStdout(message);
|
|
1300
1360
|
return message;
|
|
@@ -1305,6 +1365,11 @@ async function executeConnectBlueBubbles(agent, deps) {
|
|
|
1305
1365
|
}
|
|
1306
1366
|
const promptInput = requirePromptInput(deps, "BlueBubbles setup");
|
|
1307
1367
|
const promptSecret = requirePromptSecret(deps, "BlueBubbles password entry");
|
|
1368
|
+
deps.writeStdout([
|
|
1369
|
+
`Connect BlueBubbles for ${agent}`,
|
|
1370
|
+
"This is a local attachment for this machine.",
|
|
1371
|
+
"The app password stays hidden while you type.",
|
|
1372
|
+
].join("\n"));
|
|
1308
1373
|
const serverUrl = (await promptInput("BlueBubbles server URL for this machine: ")).trim();
|
|
1309
1374
|
if (!serverUrl)
|
|
1310
1375
|
throw new Error("BlueBubbles server URL cannot be blank");
|
|
@@ -1315,25 +1380,40 @@ async function executeConnectBlueBubbles(agent, deps) {
|
|
|
1315
1380
|
const webhookPath = normalizeWebhookPath(await promptInput("Local webhook path [/bluebubbles-webhook]: "), "/bluebubbles-webhook");
|
|
1316
1381
|
const requestTimeoutMs = parseOptionalPositiveInteger(await promptInput("Request timeout ms [30000]: "), 30000, "BlueBubbles request timeout");
|
|
1317
1382
|
const machineId = currentMachineId(deps);
|
|
1318
|
-
const
|
|
1319
|
-
|
|
1320
|
-
|
|
1383
|
+
const progress = createHumanCommandProgress(deps, "connect bluebubbles");
|
|
1384
|
+
let stored;
|
|
1385
|
+
try {
|
|
1386
|
+
progress.startPhase("saving BlueBubbles attachment");
|
|
1387
|
+
progress.updateDetail("checking existing machine runtime config");
|
|
1388
|
+
const current = await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(agent, machineId, { preserveCachedOnFailure: true });
|
|
1389
|
+
if (!current.ok && current.reason !== "missing") {
|
|
1390
|
+
progress.end();
|
|
1391
|
+
throw new Error(`cannot read existing machine runtime credentials from ${current.itemPath}: ${current.error}`);
|
|
1392
|
+
}
|
|
1393
|
+
const nextConfig = {
|
|
1394
|
+
...(current.ok ? current.config : {}),
|
|
1395
|
+
bluebubbles: {
|
|
1396
|
+
serverUrl,
|
|
1397
|
+
password,
|
|
1398
|
+
accountId: "default",
|
|
1399
|
+
},
|
|
1400
|
+
bluebubblesChannel: {
|
|
1401
|
+
port,
|
|
1402
|
+
webhookPath,
|
|
1403
|
+
requestTimeoutMs,
|
|
1404
|
+
},
|
|
1405
|
+
};
|
|
1406
|
+
progress.updateDetail("storing local machine config");
|
|
1407
|
+
stored = await (0, runtime_credentials_1.upsertMachineRuntimeCredentialConfig)(agent, machineId, nextConfig, providerCliNow(deps));
|
|
1408
|
+
progress.updateDetail("enabling BlueBubbles in agent.json");
|
|
1409
|
+
enableAgentSense(agent, "bluebubbles", deps);
|
|
1410
|
+
progress.completePhase("saving BlueBubbles attachment", "secret stored");
|
|
1411
|
+
progress.end();
|
|
1412
|
+
}
|
|
1413
|
+
catch (error) {
|
|
1414
|
+
progress.end();
|
|
1415
|
+
throw error;
|
|
1321
1416
|
}
|
|
1322
|
-
const nextConfig = {
|
|
1323
|
-
...(current.ok ? current.config : {}),
|
|
1324
|
-
bluebubbles: {
|
|
1325
|
-
serverUrl,
|
|
1326
|
-
password,
|
|
1327
|
-
accountId: "default",
|
|
1328
|
-
},
|
|
1329
|
-
bluebubblesChannel: {
|
|
1330
|
-
port,
|
|
1331
|
-
webhookPath,
|
|
1332
|
-
requestTimeoutMs,
|
|
1333
|
-
},
|
|
1334
|
-
};
|
|
1335
|
-
const stored = await (0, runtime_credentials_1.upsertMachineRuntimeCredentialConfig)(agent, machineId, nextConfig, providerCliNow(deps));
|
|
1336
|
-
enableAgentSense(agent, "bluebubbles", deps);
|
|
1337
1417
|
const message = appendBundleSyncSummary([
|
|
1338
1418
|
`BlueBubbles attached for ${agent} on this machine`,
|
|
1339
1419
|
`machine: ${machineId}`,
|
|
@@ -1635,38 +1715,40 @@ async function executeProviderRefresh(command, deps) {
|
|
|
1635
1715
|
deps.writeStdout(message);
|
|
1636
1716
|
return message;
|
|
1637
1717
|
}
|
|
1638
|
-
const
|
|
1718
|
+
const progress = createHumanCommandProgress(deps, "provider refresh");
|
|
1719
|
+
progress.startPhase("refreshing provider credentials");
|
|
1720
|
+
let pool;
|
|
1721
|
+
try {
|
|
1722
|
+
pool = await (0, provider_credentials_1.refreshProviderCredentialPool)(command.agent, {
|
|
1723
|
+
onProgress: (message) => progress.updateDetail(message),
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
catch (error) {
|
|
1727
|
+
progress.end();
|
|
1728
|
+
throw error;
|
|
1729
|
+
}
|
|
1639
1730
|
const lines = [];
|
|
1640
1731
|
if (pool.ok) {
|
|
1641
1732
|
const summary = (0, provider_credentials_1.summarizeProviderCredentialPool)(pool.pool);
|
|
1642
1733
|
lines.push(`refreshed provider credential snapshot for ${command.agent}`);
|
|
1643
1734
|
lines.push(`providers: ${summary.providers.map((provider) => provider.provider).join(", ") || "none"}`);
|
|
1735
|
+
progress.completePhase("refreshing provider credentials", summary.providers.map((provider) => provider.provider).join(", ") || "none");
|
|
1644
1736
|
}
|
|
1645
1737
|
else {
|
|
1738
|
+
progress.end();
|
|
1646
1739
|
lines.push(`provider credential refresh failed for ${command.agent}: ${pool.error}`);
|
|
1647
1740
|
lines.push((0, vault_unlock_1.vaultUnlockReplaceRecoverFix)(command.agent, "Then retry 'ouro provider refresh'."));
|
|
1648
1741
|
const message = lines.join("\n");
|
|
1649
1742
|
deps.writeStdout(message);
|
|
1650
1743
|
return message;
|
|
1651
1744
|
}
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
else {
|
|
1660
|
-
lines.push(`daemon restart skipped: ${response.error ?? response.message ?? "unknown daemon error"}`);
|
|
1661
|
-
}
|
|
1662
|
-
}
|
|
1663
|
-
else {
|
|
1664
|
-
lines.push("daemon is not running; the next start will load the refreshed snapshot");
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
catch (error) {
|
|
1668
|
-
lines.push(`daemon restart skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
1669
|
-
}
|
|
1745
|
+
progress.startPhase(`reloading ${command.agent}`);
|
|
1746
|
+
const reload = await reloadRunningAgentAfterCredentialChange(command.agent, deps);
|
|
1747
|
+
progress.completePhase(`reloading ${command.agent}`, reload);
|
|
1748
|
+
progress.end();
|
|
1749
|
+
lines.push(reload === "daemon is not running; next `ouro up` will load it"
|
|
1750
|
+
? "daemon is not running; the next start will load the refreshed snapshot"
|
|
1751
|
+
: reload);
|
|
1670
1752
|
const message = lines.join("\n");
|
|
1671
1753
|
deps.writeStdout(message);
|
|
1672
1754
|
return message;
|
|
@@ -2469,7 +2551,13 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
2469
2551
|
}
|
|
2470
2552
|
}
|
|
2471
2553
|
const linkedVersionBeforeUp = deps.getCurrentCliVersion?.() ?? null;
|
|
2472
|
-
const
|
|
2554
|
+
const outputIsTTY = deps.isTTY ?? process.stdout.isTTY === true;
|
|
2555
|
+
const progress = new up_progress_1.UpProgress({
|
|
2556
|
+
write: deps.writeRaw ?? deps.writeStdout,
|
|
2557
|
+
isTTY: outputIsTTY,
|
|
2558
|
+
now: deps.now ?? (() => Date.now()),
|
|
2559
|
+
autoRender: true,
|
|
2560
|
+
});
|
|
2473
2561
|
// ── versioned CLI update check ──
|
|
2474
2562
|
if (deps.checkForCliUpdate) {
|
|
2475
2563
|
progress.startPhase("update check");
|
|
@@ -2631,6 +2719,12 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
2631
2719
|
progress.announceStep?.(label);
|
|
2632
2720
|
},
|
|
2633
2721
|
}, { initialAlive: daemonAliveBeforeStart });
|
|
2722
|
+
progress.completePhase("starting daemon", daemonProgressSummary(daemonResult));
|
|
2723
|
+
if (daemonResult.verifyStartupStatus === false) {
|
|
2724
|
+
progress.end();
|
|
2725
|
+
deps.writeStdout(daemonResult.message);
|
|
2726
|
+
return daemonResult.message;
|
|
2727
|
+
}
|
|
2634
2728
|
if (!providerChecksAlreadyRun || daemonResult.alreadyRunning) {
|
|
2635
2729
|
progress.startPhase("provider checks");
|
|
2636
2730
|
const providerDegraded = await checkAlreadyRunningAgentProviders(deps, (msg) => progress.updateDetail(msg));
|
|
@@ -2638,7 +2732,6 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
2638
2732
|
progress.completePhase("provider checks", providerRepairCountSummary(providerDegraded.length));
|
|
2639
2733
|
}
|
|
2640
2734
|
progress.end();
|
|
2641
|
-
deps.writeStdout(daemonResult.message);
|
|
2642
2735
|
// Interactive repair for degraded agents (Unit 5) — skipped by --no-repair (Unit 6)
|
|
2643
2736
|
if (daemonResult.stability?.degraded && daemonResult.stability.degraded.length > 0) {
|
|
2644
2737
|
if (command.noRepair) {
|
|
@@ -3324,31 +3417,43 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
3324
3417
|
const provider = command.provider ?? (0, auth_flow_1.readAgentConfigForAgent)(command.agent, deps.bundlesRoot).config.humanFacing.provider;
|
|
3325
3418
|
/* v8 ignore next -- tests always inject runAuthFlow; default is for production @preserve */
|
|
3326
3419
|
const authRunner = deps.runAuthFlow ?? (await Promise.resolve().then(() => __importStar(require("../auth/auth-flow")))).runRuntimeAuthFlow;
|
|
3327
|
-
const
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3420
|
+
const progress = createHumanCommandProgress(deps, "auth");
|
|
3421
|
+
progress.startPhase(`authenticating ${provider}`);
|
|
3422
|
+
let result;
|
|
3423
|
+
try {
|
|
3424
|
+
result = await authRunner({
|
|
3425
|
+
agentName: command.agent,
|
|
3426
|
+
provider,
|
|
3427
|
+
promptInput: deps.promptInput,
|
|
3428
|
+
onProgress: (message) => progress.updateDetail(message),
|
|
3429
|
+
});
|
|
3430
|
+
}
|
|
3431
|
+
catch (error) {
|
|
3432
|
+
progress.end();
|
|
3433
|
+
throw error;
|
|
3434
|
+
}
|
|
3435
|
+
progress.completePhase(`authenticating ${provider}`, "credentials stored");
|
|
3333
3436
|
// Behavior: ouro auth stores credentials only — does NOT switch provider.
|
|
3334
3437
|
// Use `ouro auth switch` to change the active provider.
|
|
3335
|
-
deps.writeStdout(result.message);
|
|
3336
3438
|
// Verify the credentials actually work by pinging the provider
|
|
3337
3439
|
/* v8 ignore start -- integration: real API ping after auth @preserve */
|
|
3338
3440
|
try {
|
|
3339
|
-
|
|
3441
|
+
progress.startPhase(`verifying ${provider}`);
|
|
3340
3442
|
const credential = await readProviderCredentialRecord(command.agent, provider, deps);
|
|
3341
3443
|
const status = credential.ok
|
|
3342
3444
|
? await verifyProviderCredentials(provider, {
|
|
3343
3445
|
[provider]: { ...credential.record.config, ...credential.record.credentials },
|
|
3344
3446
|
})
|
|
3345
3447
|
: `stored but could not be re-read from vault (${credential.error})`;
|
|
3346
|
-
|
|
3448
|
+
progress.completePhase(`verifying ${provider}`, status);
|
|
3347
3449
|
}
|
|
3348
|
-
catch {
|
|
3349
|
-
// Verification failure is non-blocking — credentials were saved regardless
|
|
3450
|
+
catch (error) {
|
|
3451
|
+
// Verification failure is non-blocking — credentials were saved regardless.
|
|
3452
|
+
progress.completePhase(`verifying ${provider}`, `skipped (${error instanceof Error ? error.message : String(error)})`);
|
|
3350
3453
|
}
|
|
3351
3454
|
/* v8 ignore stop */
|
|
3455
|
+
progress.end();
|
|
3456
|
+
deps.writeStdout(result.message);
|
|
3352
3457
|
return result.message;
|
|
3353
3458
|
}
|
|
3354
3459
|
// ── auth verify (local, no daemon socket needed) ──
|
|
@@ -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
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* UpProgress — accumulated-checklist progress renderer
|
|
3
|
+
* UpProgress — accumulated-checklist progress renderer.
|
|
4
4
|
*
|
|
5
5
|
* Displays completed phases with checkmarks, the current phase with a
|
|
6
6
|
* spinner and elapsed time, and pending phases as plain text. Uses ANSI
|
|
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
|
-
exports.UpProgress = void 0;
|
|
15
|
+
exports.CommandProgress = exports.UpProgress = void 0;
|
|
15
16
|
const runtime_1 = require("../../nerves/runtime");
|
|
16
17
|
// ── ANSI constants (shared with startup-tui.ts pattern) ──
|
|
17
18
|
const SPINNER_FRAMES = "\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F";
|
|
@@ -23,15 +24,34 @@ 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;
|
|
32
|
+
eventScope;
|
|
33
|
+
commandName;
|
|
26
34
|
completed = [];
|
|
27
35
|
currentPhase = null;
|
|
36
|
+
currentDetail = null;
|
|
28
37
|
prevLineCount = 0;
|
|
29
38
|
ended = false;
|
|
39
|
+
renderTimer = null;
|
|
30
40
|
constructor(options) {
|
|
31
41
|
/* v8 ignore next -- thin wrapper: raw process.stdout.write for ANSI cursor control @preserve */
|
|
32
42
|
this.write = options?.write ?? ((text) => process.stdout.write(text));
|
|
33
43
|
/* v8 ignore next -- thin wrapper: real isTTY check injected for testability @preserve */
|
|
34
44
|
this.isTTY = options?.isTTY ?? (process.stdout.isTTY === true);
|
|
45
|
+
/* v8 ignore next -- thin wrapper: real Date.now injected for testability @preserve */
|
|
46
|
+
this.now = options?.now ?? (() => Date.now());
|
|
47
|
+
this.autoRender = options?.autoRender ?? false;
|
|
48
|
+
this.renderIntervalMs = options?.renderIntervalMs ?? 80;
|
|
49
|
+
/* v8 ignore start -- real timers are injected in tests when needed @preserve */
|
|
50
|
+
this.setTimer = options?.setInterval ?? ((callback, ms) => setInterval(callback, ms));
|
|
51
|
+
this.clearTimer = options?.clearInterval ?? ((handle) => clearInterval(handle));
|
|
52
|
+
/* v8 ignore stop */
|
|
53
|
+
this.eventScope = options?.eventScope ?? "up";
|
|
54
|
+
this.commandName = options?.commandName ?? null;
|
|
35
55
|
}
|
|
36
56
|
/**
|
|
37
57
|
* Begin a new phase with spinner. If a phase is already active, it is
|
|
@@ -41,26 +61,44 @@ class UpProgress {
|
|
|
41
61
|
if (this.currentPhase) {
|
|
42
62
|
this.completePhase(this.currentPhase.label);
|
|
43
63
|
}
|
|
44
|
-
this.currentPhase = { label, startedAt:
|
|
64
|
+
this.currentPhase = { label, startedAt: this.now() };
|
|
65
|
+
this.currentDetail = null;
|
|
66
|
+
if (this.isTTY) {
|
|
67
|
+
this.ensureAutoRender();
|
|
68
|
+
this.flushRender();
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
this.write(` ... ${label}\n`);
|
|
72
|
+
}
|
|
45
73
|
}
|
|
46
74
|
/**
|
|
47
75
|
* Emit a one-line status breadcrumb in non-TTY mode without affecting the
|
|
48
76
|
* accumulated checklist state. Used for daemon startup sub-steps.
|
|
49
77
|
*/
|
|
50
78
|
announceStep(label) {
|
|
79
|
+
if (this.currentPhase) {
|
|
80
|
+
this.updateDetail(label);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
51
83
|
if (this.isTTY)
|
|
52
84
|
return;
|
|
53
|
-
this.write(label);
|
|
85
|
+
this.write(` ${label}\n`);
|
|
54
86
|
}
|
|
55
87
|
/**
|
|
56
88
|
* Update the sub-step detail on the current spinner phase. Rendered as
|
|
57
|
-
* "label (Xs) -- detail" in TTY mode.
|
|
58
|
-
*
|
|
89
|
+
* "label (Xs) -- detail" in TTY mode. In non-TTY mode, writes changed
|
|
90
|
+
* detail lines so long operations remain visible in logs and captured output.
|
|
59
91
|
*/
|
|
60
92
|
updateDetail(detail) {
|
|
61
|
-
if (!this.
|
|
93
|
+
if (!this.currentPhase || detail === this.currentDetail)
|
|
62
94
|
return;
|
|
95
|
+
this.currentDetail = detail;
|
|
63
96
|
this.currentPhase.detail = detail;
|
|
97
|
+
if (this.isTTY) {
|
|
98
|
+
this.flushRender();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
this.write(` ${detail}\n`);
|
|
64
102
|
}
|
|
65
103
|
/**
|
|
66
104
|
* Mark the current phase as done. In non-TTY mode, immediately writes
|
|
@@ -70,16 +108,31 @@ class UpProgress {
|
|
|
70
108
|
if (!this.currentPhase) {
|
|
71
109
|
return;
|
|
72
110
|
}
|
|
73
|
-
const elapsedMs =
|
|
111
|
+
const elapsedMs = this.now() - this.currentPhase.startedAt;
|
|
74
112
|
this.completed.push({ label, detail });
|
|
75
113
|
this.currentPhase = null;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
114
|
+
this.currentDetail = null;
|
|
115
|
+
this.stopAutoRender();
|
|
116
|
+
if (this.eventScope === "command") {
|
|
117
|
+
(0, runtime_1.emitNervesEvent)({
|
|
118
|
+
component: "daemon",
|
|
119
|
+
event: "daemon.cli_progress_phase_complete",
|
|
120
|
+
message: `phase complete: ${label}`,
|
|
121
|
+
meta: { command: this.commandName, phase: label, detail: detail ?? null, elapsedMs },
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
(0, runtime_1.emitNervesEvent)({
|
|
126
|
+
component: "daemon",
|
|
127
|
+
event: "daemon.up_phase_complete",
|
|
128
|
+
message: `phase complete: ${label}`,
|
|
129
|
+
meta: { phase: label, detail: detail ?? null, elapsedMs },
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (this.isTTY) {
|
|
133
|
+
this.flushRender();
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
83
136
|
const detailStr = detail ? ` \u2014 ${detail}` : "";
|
|
84
137
|
this.write(` \u2713 ${label}${detailStr}\n`);
|
|
85
138
|
}
|
|
@@ -134,13 +187,32 @@ class UpProgress {
|
|
|
134
187
|
this.ended = true;
|
|
135
188
|
if (this.currentPhase) {
|
|
136
189
|
this.currentPhase = null;
|
|
190
|
+
this.currentDetail = null;
|
|
137
191
|
}
|
|
192
|
+
this.stopAutoRender();
|
|
138
193
|
if (this.isTTY) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
194
|
+
this.flushRender();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
ensureAutoRender() {
|
|
198
|
+
if (!this.autoRender || !this.isTTY || this.renderTimer !== null) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
this.renderTimer = this.setTimer(() => this.flushRender(), this.renderIntervalMs);
|
|
202
|
+
}
|
|
203
|
+
stopAutoRender() {
|
|
204
|
+
if (this.renderTimer === null) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
this.clearTimer(this.renderTimer);
|
|
208
|
+
this.renderTimer = null;
|
|
209
|
+
}
|
|
210
|
+
flushRender() {
|
|
211
|
+
const output = this.render(this.now());
|
|
212
|
+
if (output) {
|
|
213
|
+
this.write(output);
|
|
143
214
|
}
|
|
144
215
|
}
|
|
145
216
|
}
|
|
146
217
|
exports.UpProgress = UpProgress;
|
|
218
|
+
exports.CommandProgress = UpProgress;
|