@ouro.bot/cli 0.1.0-alpha.444 → 0.1.0-alpha.446

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.
@@ -4,9 +4,12 @@ exports.stripAnsi = stripAnsi;
4
4
  exports.visibleLength = visibleLength;
5
5
  exports.padAnsi = padAnsi;
6
6
  exports.wrapPlain = wrapPlain;
7
+ exports.renderOverwriteFrame = renderOverwriteFrame;
7
8
  exports.renderOuroMasthead = renderOuroMasthead;
8
9
  exports.formatActionActorLabel = formatActionActorLabel;
10
+ exports.renderTerminalWizard = renderTerminalWizard;
9
11
  exports.renderTerminalBoard = renderTerminalBoard;
12
+ exports.renderTerminalGuide = renderTerminalGuide;
10
13
  exports.renderTerminalOperation = renderTerminalOperation;
11
14
  const runtime_1 = require("../../nerves/runtime");
12
15
  const RESET = "\x1b[0m";
@@ -16,6 +19,7 @@ const SCALE = "\x1b[38;2;45;148;71m";
16
19
  const GLOW = "\x1b[38;2;74;227;108m";
17
20
  const BONE = "\x1b[38;2;237;242;238m";
18
21
  const MIST = "\x1b[38;2;154;174;159m";
22
+ const ALERT = "\x1b[38;2;255;106;106m";
19
23
  const ANSI_RE = /\x1b\[[0-9;]*m/g;
20
24
  const MASTHEAD_WORD = "OUROBOROS";
21
25
  const CLASSIC_WORDMARK_GLYPHS = {
@@ -71,6 +75,25 @@ function boardWidth(columns) {
71
75
  const requested = columns ?? 88;
72
76
  return Math.max(58, Math.min(requested, 96));
73
77
  }
78
+ function renderOverwriteFrame(lines, prevLineCount, isTTY) {
79
+ if (!isTTY)
80
+ return `${lines.join("\n")}\n`;
81
+ let output = "";
82
+ if (prevLineCount > 0) {
83
+ output += `\x1b[${prevLineCount}A`;
84
+ }
85
+ for (const line of lines) {
86
+ output += `\x1b[2K${line}\n`;
87
+ }
88
+ const extraLineCount = Math.max(0, prevLineCount - lines.length);
89
+ for (let i = 0; i < extraLineCount; i++) {
90
+ output += "\x1b[2K\n";
91
+ }
92
+ if (extraLineCount > 0) {
93
+ output += `\x1b[${extraLineCount}A`;
94
+ }
95
+ return output;
96
+ }
74
97
  function renderPanelTTY(title, lines, width) {
75
98
  const innerWidth = Math.max(8, width - 4);
76
99
  const topPrefix = `╭─ ${title} `;
@@ -94,15 +117,18 @@ function renderPanelPlain(title, lines) {
94
117
  ];
95
118
  }
96
119
  function mastheadArt(columns) {
97
- if ((columns ?? 88) >= 74) {
98
- const rows = Array.from({ length: 5 }, () => []);
99
- for (const letter of MASTHEAD_WORD.split("")) {
100
- const glyph = CLASSIC_WORDMARK_GLYPHS[letter];
101
- for (const [index, line] of glyph.entries()) {
102
- rows[index].push(line);
103
- }
120
+ const rows = Array.from({ length: 5 }, () => []);
121
+ for (const letter of MASTHEAD_WORD.split("")) {
122
+ const glyph = CLASSIC_WORDMARK_GLYPHS[letter];
123
+ for (const [index, line] of glyph.entries()) {
124
+ rows[index].push(line);
104
125
  }
105
- return rows.map((row) => row.join(" "));
126
+ }
127
+ const classicWordmark = rows.map((row) => row.join(" "));
128
+ const availableColumns = columns ?? 88;
129
+ const classicWidth = Math.max(...classicWordmark.map((line) => line.length));
130
+ if (availableColumns >= classicWidth) {
131
+ return classicWordmark;
106
132
  }
107
133
  return [MASTHEAD_WORD];
108
134
  }
@@ -121,6 +147,154 @@ function renderOuroMasthead(options) {
121
147
  function formatActionActorLabel(actor) {
122
148
  return actor.replace(/-/g, " ");
123
149
  }
150
+ function formatWizardStatusLabel(status) {
151
+ return status;
152
+ }
153
+ function isQuietWizardStatus(status) {
154
+ return status === "ready" || status === "attached" || status === "not attached";
155
+ }
156
+ function renderWizardStatusBadge(status, isTTY) {
157
+ const symbol = status === "ready" || status === "attached"
158
+ ? "●"
159
+ : status === "not attached"
160
+ ? "◌"
161
+ : "◆";
162
+ const label = `${symbol} ${formatWizardStatusLabel(status)}`;
163
+ if (!isTTY)
164
+ return label;
165
+ if (status === "ready" || status === "attached")
166
+ return color(label, GLOW, true);
167
+ if (status === "not attached")
168
+ return color(label, MIST);
169
+ return color(label, ALERT, true);
170
+ }
171
+ function renderWizardActorBadge(actor, isTTY) {
172
+ const label = `[${formatActionActorLabel(actor)}]`;
173
+ if (!isTTY)
174
+ return label;
175
+ if (actor === "human-required")
176
+ return color(label, ALERT, true);
177
+ if (actor === "human-choice")
178
+ return color(label, SCALE, true);
179
+ return color(label, MIST);
180
+ }
181
+ function renderWizardRecommendedBadge(isTTY) {
182
+ if (!isTTY)
183
+ return "[recommended]";
184
+ return color("[recommended]", GLOW, true);
185
+ }
186
+ function wrapWizardDetailLines(lines, width, isTTY, tone) {
187
+ const rendered = [];
188
+ for (const line of lines) {
189
+ const wrapped = wrapPlain(line, width);
190
+ for (const segment of wrapped) {
191
+ rendered.push(isTTY ? color(segment, tone) : segment);
192
+ }
193
+ }
194
+ return rendered;
195
+ }
196
+ function renderWizardItem(item, width, isTTY) {
197
+ const badges = [
198
+ ...(item.status ? [renderWizardStatusBadge(item.status, isTTY)] : []),
199
+ ...(item.actor ? [renderWizardActorBadge(item.actor, isTTY)] : []),
200
+ ...(item.recommended ? [renderWizardRecommendedBadge(isTTY)] : []),
201
+ ];
202
+ const keyPrefix = item.key ? `${item.key}. ` : "";
203
+ const header = `${keyPrefix}${item.label}${badges.length > 0 ? ` ${badges.join(" ")}` : ""}`;
204
+ const detailWidth = Math.max(18, width - 6);
205
+ const detailTone = item.status && !isQuietWizardStatus(item.status) ? BONE : MIST;
206
+ const lines = [isTTY ? color(header, BONE, true) : header];
207
+ if (item.summary) {
208
+ lines.push(...wrapWizardDetailLines([item.summary], detailWidth, isTTY, detailTone));
209
+ }
210
+ if (item.detailLines && item.detailLines.length > 0) {
211
+ lines.push(...wrapWizardDetailLines(item.detailLines, detailWidth, isTTY, detailTone));
212
+ }
213
+ if (item.command) {
214
+ lines.push(...wrapWizardDetailLines([`run: ${item.command}`], detailWidth, isTTY, MIST));
215
+ }
216
+ return lines.flatMap((line, index) => index === 0 ? [line] : [` ${line}`]);
217
+ }
218
+ function renderWizardSectionTTY(section, width) {
219
+ const lines = [];
220
+ if (section.summary) {
221
+ lines.push(...wrapWizardDetailLines([section.summary], Math.max(18, width - 4), true, MIST));
222
+ }
223
+ for (const [index, item] of section.items.entries()) {
224
+ if (index > 0)
225
+ lines.push("");
226
+ lines.push(...renderWizardItem(item, width, true));
227
+ }
228
+ return renderOperationSectionTTY(section.title, lines, width);
229
+ }
230
+ function renderWizardSectionPlain(section, width) {
231
+ const lines = [];
232
+ if (section.summary) {
233
+ lines.push(...wrapWizardDetailLines([section.summary], Math.max(18, width - 4), false, MIST));
234
+ }
235
+ for (const [index, item] of section.items.entries()) {
236
+ if (index > 0)
237
+ lines.push("");
238
+ lines.push(...renderWizardItem(item, width, false));
239
+ }
240
+ return renderOperationSectionPlain(section.title, lines);
241
+ }
242
+ function renderTerminalWizard(options) {
243
+ if (!options.suppressEvent) {
244
+ (0, runtime_1.emitNervesEvent)({
245
+ component: "daemon",
246
+ event: "daemon.terminal_wizard_rendered",
247
+ message: "rendered shared terminal wizard",
248
+ meta: {
249
+ title: options.title,
250
+ sections: options.sections?.length ?? 0,
251
+ items: options.sections?.reduce((count, section) => count + section.items.length, 0) ?? 0,
252
+ hasNextStep: !!options.nextStep,
253
+ tty: options.isTTY,
254
+ },
255
+ });
256
+ }
257
+ const width = boardWidth(options.columns);
258
+ const blocks = [];
259
+ blocks.push(renderOuroMasthead({
260
+ isTTY: options.isTTY,
261
+ columns: width,
262
+ subtitle: options.masthead?.subtitle,
263
+ }).trimEnd());
264
+ const introLines = [
265
+ options.isTTY ? color(options.title, BONE, true) : options.title,
266
+ ...(options.summary
267
+ ? wrapPlain(options.summary, Math.max(20, width - 2)).map((line) => options.isTTY ? color(line, MIST) : line)
268
+ : []),
269
+ ];
270
+ blocks.push(introLines.join("\n"));
271
+ if (options.nextStep) {
272
+ const nextStepLines = [
273
+ options.isTTY ? color(options.nextStep.label, BONE, true) : options.nextStep.label,
274
+ ...(options.nextStep.detail
275
+ ? wrapPlain(options.nextStep.detail, Math.max(18, width - 4)).map((line) => options.isTTY ? color(line, MIST) : line)
276
+ : []),
277
+ ...(options.nextStep.command
278
+ ? wrapPlain(`run: ${options.nextStep.command}`, Math.max(18, width - 4)).map((line) => options.isTTY ? color(line, MIST) : line)
279
+ : []),
280
+ ];
281
+ blocks.push((options.isTTY
282
+ ? renderOperationSectionTTY("Recommended next step", nextStepLines, width)
283
+ : renderOperationSectionPlain("Recommended next step", nextStepLines)).join("\n"));
284
+ }
285
+ for (const section of options.sections ?? []) {
286
+ blocks.push((options.isTTY
287
+ ? renderWizardSectionTTY(section, width)
288
+ : renderWizardSectionPlain(section, width)).join("\n"));
289
+ }
290
+ if (options.footerLines && options.footerLines.length > 0) {
291
+ blocks.push(options.footerLines.map((line) => options.isTTY ? color(line, MIST) : line).join("\n"));
292
+ }
293
+ if (options.prompt) {
294
+ blocks.push(options.isTTY ? color(options.prompt, BONE, true) : options.prompt);
295
+ }
296
+ return `${blocks.join("\n\n")}\n`;
297
+ }
124
298
  function renderActionLine(action) {
125
299
  const chips = [`[${formatActionActorLabel(action.actor)}]`];
126
300
  if (action.recommended)
@@ -171,6 +345,56 @@ function renderTerminalBoard(options) {
171
345
  }
172
346
  return `${blocks.join("\n\n")}\n`;
173
347
  }
348
+ function renderTerminalGuide(options) {
349
+ if (!options.suppressEvent) {
350
+ (0, runtime_1.emitNervesEvent)({
351
+ component: "daemon",
352
+ event: "daemon.terminal_guide_rendered",
353
+ message: "rendered shared terminal guide",
354
+ meta: {
355
+ title: options.title,
356
+ sections: options.sections?.length ?? 0,
357
+ actions: options.actions?.length ?? 0,
358
+ tty: options.isTTY,
359
+ },
360
+ });
361
+ }
362
+ const width = boardWidth(options.columns);
363
+ const blocks = [];
364
+ blocks.push(renderOuroMasthead({
365
+ isTTY: options.isTTY,
366
+ columns: width,
367
+ subtitle: options.masthead?.subtitle,
368
+ }).trimEnd());
369
+ const introLines = [
370
+ options.isTTY ? color(options.title, BONE, true) : options.title,
371
+ ...(options.summary
372
+ ? wrapPlain(options.summary, Math.max(20, width - 2)).map((line) => options.isTTY ? color(line, MIST) : line)
373
+ : []),
374
+ ];
375
+ blocks.push(introLines.join("\n"));
376
+ for (const section of options.sections ?? []) {
377
+ const lines = section.lines.map((line) => options.isTTY ? color(line, BONE) : line);
378
+ blocks.push((options.isTTY
379
+ ? renderOperationSectionTTY(section.title, lines, width)
380
+ : renderOperationSectionPlain(section.title, lines)).join("\n"));
381
+ }
382
+ const actionList = options.actions ?? [];
383
+ if (actionList.length > 0) {
384
+ const lines = [];
385
+ for (const [index, action] of actionList.entries()) {
386
+ lines.push(options.isTTY ? color(`${index + 1}. ${renderActionLine(action)}`, BONE, true) : `${index + 1}. ${renderActionLine(action)}`);
387
+ lines.push(options.isTTY ? color(`run: ${action.command}`, MIST) : `run: ${action.command}`);
388
+ }
389
+ blocks.push((options.isTTY
390
+ ? renderOperationSectionTTY("Next moves", lines, width)
391
+ : renderOperationSectionPlain("Next moves", lines)).join("\n"));
392
+ }
393
+ if (options.prompt) {
394
+ blocks.push(options.isTTY ? color(options.prompt, BONE, true) : options.prompt);
395
+ }
396
+ return `${blocks.join("\n\n")}\n`;
397
+ }
174
398
  function formatOperationStep(step) {
175
399
  const marker = step.status === "done"
176
400
  ? "✓"
@@ -182,7 +406,58 @@ function formatOperationStep(step) {
182
406
  const detail = step.detail ? ` — ${step.detail}` : "";
183
407
  return `${marker} ${step.label}${detail}`;
184
408
  }
409
+ function operationMarkerTone(status) {
410
+ switch (status) {
411
+ case "done":
412
+ return GLOW;
413
+ case "active":
414
+ return BONE;
415
+ case "failed":
416
+ return ALERT;
417
+ case "pending":
418
+ default:
419
+ return MIST;
420
+ }
421
+ }
422
+ function renderOperationStepTTY(step) {
423
+ const marker = step.status === "done"
424
+ ? "✓"
425
+ : step.status === "failed"
426
+ ? "✗"
427
+ : step.status === "active"
428
+ ? "→"
429
+ : "○";
430
+ const label = color(step.label, step.status === "pending" ? MIST : BONE, step.status !== "pending");
431
+ const detail = step.detail ? ` ${color(`— ${step.detail}`, MIST)}` : "";
432
+ return `${color(marker, operationMarkerTone(step.status), true)} ${label}${detail}`;
433
+ }
434
+ function renderOperationSectionTTY(title, lines, width) {
435
+ const rule = "─".repeat(Math.max(8, width - title.length - 3));
436
+ return [
437
+ `${color("─ ", CANOPY)}${color(title, BONE, true)} ${color(rule, CANOPY)}`,
438
+ ...lines.map((line) => ` ${line}`),
439
+ ];
440
+ }
441
+ function renderOperationSectionPlain(title, lines) {
442
+ return [
443
+ title,
444
+ ...lines.map((line) => ` ${plainLine(line)}`),
445
+ ];
446
+ }
185
447
  function renderTerminalOperation(options) {
448
+ if (!options.suppressEvent) {
449
+ (0, runtime_1.emitNervesEvent)({
450
+ component: "daemon",
451
+ event: "daemon.terminal_operation_rendered",
452
+ message: "rendered terminal operation surface",
453
+ meta: {
454
+ title: options.title,
455
+ steps: options.steps?.length ?? 0,
456
+ hasCurrentStep: !!options.currentStep,
457
+ tty: options.isTTY,
458
+ },
459
+ });
460
+ }
186
461
  const steps = options.steps ?? [];
187
462
  const currentLines = options.currentStep
188
463
  ? [
@@ -191,25 +466,34 @@ function renderTerminalOperation(options) {
191
466
  ]
192
467
  : ["Standing by."];
193
468
  const progressLines = steps.length > 0
194
- ? steps.map((step) => formatOperationStep(step))
469
+ ? options.isTTY
470
+ ? steps.map((step) => renderOperationStepTTY(step))
471
+ : steps.map((step) => formatOperationStep(step))
195
472
  : ["No active steps yet."];
196
- return renderTerminalBoard({
473
+ const width = boardWidth(options.columns);
474
+ const blocks = [];
475
+ blocks.push(renderOuroMasthead({
197
476
  isTTY: options.isTTY,
198
- columns: options.columns,
199
- masthead: options.masthead,
200
- title: options.title,
201
- summary: options.summary,
202
- sections: [
203
- {
204
- title: options.currentTitle ?? "Right now",
205
- lines: currentLines,
206
- },
207
- {
208
- title: options.stepsTitle ?? "Progress",
209
- lines: progressLines,
210
- },
211
- ],
212
- prompt: options.prompt,
213
- suppressEvent: options.suppressEvent,
214
- });
477
+ columns: width,
478
+ subtitle: options.masthead?.subtitle,
479
+ }).trimEnd());
480
+ const introLines = [
481
+ options.isTTY ? color(options.title, BONE, true) : options.title,
482
+ ...(options.summary
483
+ ? wrapPlain(options.summary, Math.max(20, width - 2)).map((line) => options.isTTY ? color(line, MIST) : line)
484
+ : []),
485
+ ];
486
+ blocks.push(introLines.join("\n"));
487
+ const renderedSteps = options.isTTY
488
+ ? renderOperationSectionTTY(options.stepsTitle ?? "Checklist", progressLines, width)
489
+ : renderOperationSectionPlain(options.stepsTitle ?? "Checklist", progressLines);
490
+ const renderedCurrent = options.isTTY
491
+ ? renderOperationSectionTTY(options.currentTitle ?? "Current work", currentLines.map((line, index) => index === 0 ? color(line, BONE, true) : color(line, MIST)), width)
492
+ : renderOperationSectionPlain(options.currentTitle ?? "Current work", currentLines);
493
+ blocks.push(renderedSteps.join("\n"));
494
+ blocks.push(renderedCurrent.join("\n"));
495
+ if (options.prompt) {
496
+ blocks.push(options.isTTY ? color(options.prompt, BONE, true) : options.prompt);
497
+ }
498
+ return `${blocks.join("\n\n")}\n`;
215
499
  }
@@ -219,19 +219,7 @@ class UpProgress {
219
219
  return "";
220
220
  }
221
221
  const lines = this.renderLines(now);
222
- let output = "";
223
- if (this.prevLineCount > 0) {
224
- output += `\x1b[${this.prevLineCount}A`;
225
- }
226
- for (const line of lines) {
227
- output += `\x1b[2K${line}\n`;
228
- }
229
- // Clear any leftover lines from previous render that are no longer needed
230
- if (lines.length < this.prevLineCount) {
231
- for (let i = 0; i < this.prevLineCount - lines.length; i++) {
232
- output += `\x1b[2K\n`;
233
- }
234
- }
222
+ const output = (0, terminal_ui_1.renderOverwriteFrame)(lines, this.prevLineCount, true);
235
223
  this.prevLineCount = lines.length;
236
224
  return output;
237
225
  }
@@ -301,14 +289,8 @@ class UpProgress {
301
289
  }
302
290
  renderUpScreen(now) {
303
291
  const seenLabels = new Set();
304
- const steps = this.completed.map((phase) => {
305
- seenLabels.add(phase.label);
306
- return {
307
- label: this.renderUpStepLabel(phase.label),
308
- status: phase.status === "failure" ? "failed" : "done",
309
- detail: phase.detail,
310
- };
311
- });
292
+ const completedByLabel = new Map(this.completed.map((phase) => [phase.label, phase]));
293
+ const steps = [];
312
294
  let currentStepLabel = this.completed.some((phase) => phase.status === "failure")
313
295
  ? "Boot paused."
314
296
  : this.completed.length > 0
@@ -322,19 +304,44 @@ class UpProgress {
322
304
  const spinner = SPINNER_FRAMES[frameIndex];
323
305
  currentStepLabel = `${spinner} ${this.renderUpStepLabel(this.currentPhase.label)} (${elapsedSec}s)`;
324
306
  currentStepDetails = splitDetailLines(this.currentPhase.detail);
325
- steps.push({
326
- label: this.renderUpStepLabel(this.currentPhase.label),
327
- status: "active",
328
- });
329
- seenLabels.add(this.currentPhase.label);
330
307
  }
331
308
  for (const label of this.upPhasePlan) {
332
- if (!seenLabels.has(label)) {
309
+ seenLabels.add(label);
310
+ const completedPhase = completedByLabel.get(label);
311
+ if (completedPhase) {
312
+ steps.push({
313
+ label: this.renderUpStepLabel(label),
314
+ status: completedPhase.status === "failure" ? "failed" : "done",
315
+ detail: completedPhase.detail,
316
+ });
317
+ continue;
318
+ }
319
+ if (this.currentPhase?.label === label) {
333
320
  steps.push({
334
321
  label: this.renderUpStepLabel(label),
335
- status: "pending",
322
+ status: "active",
336
323
  });
324
+ continue;
337
325
  }
326
+ steps.push({
327
+ label: this.renderUpStepLabel(label),
328
+ status: "pending",
329
+ });
330
+ }
331
+ for (const phase of this.completed) {
332
+ if (!seenLabels.has(phase.label)) {
333
+ steps.push({
334
+ label: this.renderUpStepLabel(phase.label),
335
+ status: phase.status === "failure" ? "failed" : "done",
336
+ detail: phase.detail,
337
+ });
338
+ }
339
+ }
340
+ if (this.currentPhase && !seenLabels.has(this.currentPhase.label)) {
341
+ steps.push({
342
+ label: this.renderUpStepLabel(this.currentPhase.label),
343
+ status: "active",
344
+ });
338
345
  }
339
346
  return (0, terminal_ui_1.renderTerminalOperation)({
340
347
  isTTY: true,
@@ -342,8 +349,8 @@ class UpProgress {
342
349
  masthead: {
343
350
  subtitle: "Booting the local agent runtime.",
344
351
  },
345
- title: "Ouro boot checklist",
346
- summary: "Ouro will check for updates, prepare this machine, verify the providers your agents use right now, start the background service, and make sure it stays up.",
352
+ title: "Starting Ouro",
353
+ summary: "Ouro is bringing the local agent runtime online and will stop here if anything needs attention.",
347
354
  currentStep: {
348
355
  label: currentStepLabel,
349
356
  detailLines: currentStepDetails,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.444",
3
+ "version": "0.1.0-alpha.446",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",