@ouro.bot/cli 0.1.0-alpha.315 → 0.1.0-alpha.316

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 CHANGED
@@ -1,6 +1,12 @@
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.316",
6
+ "changes": [
7
+ "feat(daemon): unified progress TUI for ouro up lifecycle. New UpProgress accumulated-checklist renderer replaces disconnected writeStdout calls with a cohesive progress display showing completed phases with checkmarks and active phase with spinner. In-place ANSI overwrite in TTY mode, static lines in non-TTY (CI/pipes). Wired into daemon.up handler for all phases: update check, system setup, agent updates, bundle pruning, daemon start. Emits nerves events for each phase completion."
8
+ ]
9
+ },
4
10
  {
5
11
  "version": "0.1.0-alpha.315",
6
12
  "changes": [
@@ -75,6 +75,7 @@ const interactive_repair_1 = require("./interactive-repair");
75
75
  const agentic_repair_1 = require("./agentic-repair");
76
76
  const startup_tui_1 = require("./startup-tui");
77
77
  const stale_bundle_prune_1 = require("./stale-bundle-prune");
78
+ const up_progress_1 = require("./up-progress");
78
79
  // ── ensureDaemonRunning ──
79
80
  async function ensureDaemonRunning(deps) {
80
81
  const alive = await deps.checkSocketAlive(deps.socketPath);
@@ -814,19 +815,19 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
814
815
  }
815
816
  }
816
817
  const linkedVersionBeforeUp = deps.getCurrentCliVersion?.() ?? null;
818
+ const progress = new up_progress_1.UpProgress({ write: deps.writeStdout, isTTY: false });
817
819
  // ── versioned CLI update check ──
818
820
  if (deps.checkForCliUpdate) {
819
- deps.writeStdout("checking for updates...");
821
+ progress.startPhase("update check");
820
822
  let pendingReExec = false;
821
823
  try {
822
824
  const updateResult = await deps.checkForCliUpdate();
823
825
  if (updateResult.available && updateResult.latestVersion) {
824
- deps.writeStdout(`installing ${updateResult.latestVersion}...`);
825
826
  /* v8 ignore next -- fallback: getCurrentCliVersion always injected in tests @preserve */
826
827
  const currentVersion = linkedVersionBeforeUp ?? "unknown";
827
828
  await deps.installCliVersion(updateResult.latestVersion);
828
829
  deps.activateCliVersion(updateResult.latestVersion);
829
- deps.writeStdout(`ouro updated to ${updateResult.latestVersion} (was ${currentVersion})`);
830
+ progress.completePhase("update check", `installed ${updateResult.latestVersion}`);
830
831
  const changelogCommand = (0, ouro_version_manager_1.buildChangelogCommand)(currentVersion, updateResult.latestVersion);
831
832
  /* v8 ignore next -- buildChangelogCommand is non-null when an actual newer version is installed @preserve */
832
833
  if (changelogCommand) {
@@ -847,13 +848,16 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
847
848
  }
848
849
  /* v8 ignore stop */
849
850
  if (pendingReExec) {
851
+ progress.end();
850
852
  deps.reExecFromNewVersion(args);
851
853
  }
852
854
  else {
853
- deps.writeStdout("up to date.");
855
+ progress.completePhase("update check", "up to date");
854
856
  }
855
857
  }
858
+ progress.startPhase("system setup");
856
859
  await performSystemSetup(deps);
860
+ progress.completePhase("system setup");
857
861
  // Track whether we've already printed the "ouro updated to" message
858
862
  // this turn so the bundle-meta-fallback path below doesn't double-print.
859
863
  // There are three independent paths that can detect "the binary just
@@ -919,15 +923,18 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
919
923
  const to = updateSummary.updated[0].to;
920
924
  const fromStr = from ? ` (was ${from})` : "";
921
925
  const count = agents.length;
922
- deps.writeStdout(`updated ${count} agent${count === 1 ? "" : "s"} to runtime ${to}${fromStr}`);
926
+ progress.startPhase("agent updates");
927
+ progress.completePhase("agent updates", `${count} agent${count === 1 ? "" : "s"} to runtime ${to}${fromStr}`);
923
928
  }
924
929
  // ── stale bundle pruning ──
925
930
  const prunedBundles = (0, stale_bundle_prune_1.pruneStaleEphemeralBundles)({ bundlesRoot: deps.bundlesRoot });
926
- for (const name of prunedBundles) {
927
- deps.writeStdout(`pruned stale bundle: ${name}`);
931
+ if (prunedBundles.length > 0) {
932
+ progress.startPhase("bundle cleanup");
933
+ progress.completePhase("bundle cleanup", `pruned ${prunedBundles.length} stale bundle${prunedBundles.length === 1 ? "" : "s"}`);
928
934
  }
929
- deps.writeStdout("starting daemon...");
935
+ progress.startPhase("starting daemon");
930
936
  const daemonResult = await ensureDaemonRunning(deps);
937
+ progress.end();
931
938
  deps.writeStdout(daemonResult.message);
932
939
  // Interactive repair for degraded agents (Unit 5) — skipped by --no-repair (Unit 6)
933
940
  if (daemonResult.stability?.degraded && daemonResult.stability.degraded.length > 0) {
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ /**
3
+ * UpProgress — accumulated-checklist progress renderer for `ouro up`.
4
+ *
5
+ * Displays completed phases with checkmarks, the current phase with a
6
+ * spinner and elapsed time, and pending phases as plain text. Uses ANSI
7
+ * cursor control for in-place overwriting in TTY mode, and falls back to
8
+ * static line-per-phase output in non-TTY mode.
9
+ *
10
+ * The caller drives animation by calling `render(now)` on a setInterval.
11
+ * This module owns no timers.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.UpProgress = void 0;
15
+ const runtime_1 = require("../../nerves/runtime");
16
+ // ── ANSI constants (shared with startup-tui.ts pattern) ──
17
+ const SPINNER_FRAMES = "\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F";
18
+ const RESET = "\x1b[0m";
19
+ const BOLD = "\x1b[1m";
20
+ const DIM = "\x1b[2m";
21
+ const GREEN = "\x1b[38;2;46;204;64m";
22
+ // ── UpProgress class ──
23
+ class UpProgress {
24
+ write;
25
+ isTTY;
26
+ completed = [];
27
+ currentPhase = null;
28
+ prevLineCount = 0;
29
+ ended = false;
30
+ constructor(options) {
31
+ /* v8 ignore next -- thin wrapper: raw process.stdout.write for ANSI cursor control @preserve */
32
+ this.write = options?.write ?? ((text) => process.stdout.write(text));
33
+ /* v8 ignore next -- thin wrapper: real isTTY check injected for testability @preserve */
34
+ this.isTTY = options?.isTTY ?? (process.stdout.isTTY === true);
35
+ }
36
+ /**
37
+ * Begin a new phase with spinner. If a phase is already active, it is
38
+ * auto-completed (no detail text).
39
+ */
40
+ startPhase(label) {
41
+ if (this.currentPhase) {
42
+ this.completePhase(this.currentPhase.label);
43
+ }
44
+ this.currentPhase = { label, startedAt: Date.now() };
45
+ }
46
+ /**
47
+ * Mark the current phase as done. In non-TTY mode, immediately writes
48
+ * a static line. Emits a nerves event for observability.
49
+ */
50
+ completePhase(label, detail) {
51
+ if (!this.currentPhase) {
52
+ return;
53
+ }
54
+ const elapsedMs = Date.now() - this.currentPhase.startedAt;
55
+ this.completed.push({ label, detail });
56
+ this.currentPhase = null;
57
+ (0, runtime_1.emitNervesEvent)({
58
+ component: "daemon",
59
+ event: "daemon.up_phase_complete",
60
+ message: `phase complete: ${label}`,
61
+ meta: { phase: label, detail: detail ?? null, elapsedMs },
62
+ });
63
+ if (!this.isTTY) {
64
+ const detailStr = detail ? ` \u2014 ${detail}` : "";
65
+ this.write(` \u2713 ${label}${detailStr}\n`);
66
+ }
67
+ }
68
+ /**
69
+ * Build an ANSI string for in-place terminal display. Returns empty
70
+ * string in non-TTY mode (output is written eagerly in completePhase).
71
+ */
72
+ render(now) {
73
+ if (!this.isTTY) {
74
+ return "";
75
+ }
76
+ const lines = [];
77
+ // Completed phases
78
+ for (const phase of this.completed) {
79
+ const detailStr = phase.detail ? ` ${DIM}\u2014 ${phase.detail}${RESET}` : "";
80
+ lines.push(` ${GREEN}\u2713${RESET} ${phase.label}${detailStr}`);
81
+ }
82
+ // Current phase with spinner
83
+ if (this.currentPhase) {
84
+ const elapsed = now - this.currentPhase.startedAt;
85
+ const elapsedSec = (elapsed / 1000).toFixed(1);
86
+ const frameIndex = Math.floor(elapsed / 80) % SPINNER_FRAMES.length;
87
+ const spinner = SPINNER_FRAMES[frameIndex];
88
+ lines.push(` ${BOLD}${spinner}${RESET} ${this.currentPhase.label} ${DIM}(${elapsedSec}s)${RESET}`);
89
+ }
90
+ let output = "";
91
+ if (this.prevLineCount > 0) {
92
+ output += `\x1b[${this.prevLineCount}A`;
93
+ }
94
+ for (const line of lines) {
95
+ output += `\x1b[2K${line}\n`;
96
+ }
97
+ // Clear any leftover lines from previous render that are no longer needed
98
+ if (lines.length < this.prevLineCount) {
99
+ for (let i = 0; i < this.prevLineCount - lines.length; i++) {
100
+ output += `\x1b[2K\n`;
101
+ }
102
+ }
103
+ this.prevLineCount = lines.length;
104
+ return output;
105
+ }
106
+ /**
107
+ * Finalize the progress display. Clears the current phase (if any) and
108
+ * writes the final checklist state. Idempotent.
109
+ */
110
+ end() {
111
+ if (this.ended) {
112
+ return;
113
+ }
114
+ this.ended = true;
115
+ if (this.currentPhase) {
116
+ this.currentPhase = null;
117
+ }
118
+ if (this.isTTY) {
119
+ const output = this.render(Date.now());
120
+ if (output) {
121
+ this.write(output);
122
+ }
123
+ }
124
+ }
125
+ }
126
+ exports.UpProgress = UpProgress;
@@ -253,6 +253,16 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
253
253
  // PageUp/PageDown: suppress (no text insertion, no action)
254
254
  if (key.pageUp || key.pageDown)
255
255
  return;
256
+ // Alt+Enter (single data event): Ink checks `return: input === '\r'` before
257
+ // stripping the \x1b prefix, so key.return is false when the terminal sends
258
+ // \x1b\r as one chunk. Detect via the stripped inputChar instead.
259
+ if (inputChar === "\r" && key.meta) {
260
+ lastEscTime.current = 0;
261
+ const before = inputRef.current.slice(0, cursorRef.current);
262
+ const after = inputRef.current.slice(cursorRef.current);
263
+ updateInput(before + "\n" + after, cursorRef.current + 1);
264
+ return;
265
+ }
256
266
  if (key.return) {
257
267
  // Alt+Enter: detect via key.meta OR recent ESC (within 50ms — Ink splits \x1b\r)
258
268
  const recentEsc = (Date.now() - lastEscTime.current) < 50;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.315",
3
+ "version": "0.1.0-alpha.316",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",
@@ -26,6 +26,7 @@
26
26
  "teams": "tsc && node dist/senses/teams-entry.js --agent ouroboros",
27
27
  "bluebubbles": "tsc && node dist/senses/bluebubbles/entry.js --agent ouroboros",
28
28
  "test": "vitest run",
29
+ "test:outlook-ui": "npm test --prefix packages/outlook-ui",
29
30
  "test:coverage:vitest": "vitest run --coverage",
30
31
  "test:coverage": "node scripts/run-coverage-gate.cjs",
31
32
  "build": "tsc && (cd packages/outlook-ui && npm install --ignore-scripts 2>/dev/null && npm run build && cp -r dist ../../dist/outlook-ui) || echo 'outlook-ui build skipped'",
@@ -50,11 +51,19 @@
50
51
  "url": "https://github.com/ouroborosbot/ouroboros"
51
52
  },
52
53
  "devDependencies": {
54
+ "@testing-library/react": "^16.3.2",
53
55
  "@types/semver": "^7.7.1",
54
56
  "@vitest/coverage-v8": "^4.0.18",
55
57
  "eslint": "^10.0.2",
58
+ "jsdom": "^29.0.2",
56
59
  "typescript": "^5.7.0",
57
60
  "typescript-eslint": "^8.56.1",
58
61
  "vitest": "^4.0.18"
62
+ },
63
+ "overrides": {
64
+ "@testing-library/react": {
65
+ "react": "$react",
66
+ "@types/react": "$@types/react"
67
+ }
59
68
  }
60
69
  }