opencode-auto-agent 1.3.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js CHANGED
@@ -15,6 +15,9 @@
15
15
 
16
16
  import { resolve } from "node:path";
17
17
  import { argv, exit, cwd } from "node:process";
18
+ import { existsSync, readFileSync } from "node:fs";
19
+
20
+ import { configureUI } from "../src/lib/ui.js";
18
21
 
19
22
  // ── Arg parsing (zero-dep) ─────────────────────────────────────────────────
20
23
  const args = argv.slice(2);
@@ -31,6 +34,8 @@ const flags = Object.fromEntries(
31
34
 
32
35
  const TARGET_DIR = resolve(flags.dir || cwd());
33
36
 
37
+ configureUI(loadUIOptions(TARGET_DIR, flags));
38
+
34
39
  // ── Command router ─────────────────────────────────────────────────────────
35
40
  async function main() {
36
41
  switch (command) {
@@ -183,6 +188,9 @@ async function showConfig(targetDir) {
183
188
  kv("Tasks path", config.tasksPath || "PRD.md");
184
189
  kv("Docs path", config.docsPath || "Docs");
185
190
  kv("Output path", config.outputPath || "./");
191
+ kv("UI theme", config.ui?.theme || "rich");
192
+ kv("UI compact", config.ui?.compact || "auto");
193
+ kv("UI animations", String(config.ui?.animations ?? true));
186
194
 
187
195
  if (config.models) {
188
196
  section("Model Assignments");
@@ -277,6 +285,9 @@ async function printHelp() {
277
285
  console.log(cmd("--dry-run", "Assemble context without launching Ralphy"));
278
286
  console.log(cmd("--models", "Reconfigure model assignments (with setup)"));
279
287
  console.log(cmd("--verbose", "Show extra detail (doctor, run)"));
288
+ console.log(cmd("--ui-theme=<rich|minimal>", "UI theme style"));
289
+ console.log(cmd("--ui-compact=<auto|on|off>", "Compact layout behavior"));
290
+ console.log(cmd("--ui-animations=<true|false>", "Enable animated UI cues"));
280
291
  console.log(cmd("--version, -v", "Show version"));
281
292
  console.log();
282
293
 
@@ -316,6 +327,49 @@ async function loadPkg() {
316
327
  }
317
328
  }
318
329
 
330
+ function loadUIOptions(targetDir, cliFlags) {
331
+ const configUi = readProjectUiConfig(targetDir);
332
+ const flagUi = parseUiFlags(cliFlags);
333
+ return { ...configUi, ...flagUi };
334
+ }
335
+
336
+ function readProjectUiConfig(targetDir) {
337
+ try {
338
+ const configPath = resolve(targetDir, ".opencode", "config.json");
339
+ if (!existsSync(configPath)) return {};
340
+ const parsed = JSON.parse(readFileSync(configPath, "utf8"));
341
+ if (!parsed?.ui || typeof parsed.ui !== "object") return {};
342
+ return {
343
+ theme: parsed.ui.theme,
344
+ compact: parsed.ui.compact,
345
+ animations: parsed.ui.animations,
346
+ };
347
+ } catch {
348
+ return {};
349
+ }
350
+ }
351
+
352
+ function parseUiFlags(cliFlags) {
353
+ const out = {};
354
+
355
+ if (cliFlags["ui-theme"]) out.theme = String(cliFlags["ui-theme"]);
356
+ if (cliFlags["ui-compact"]) out.compact = String(cliFlags["ui-compact"]);
357
+ if (cliFlags["ui-animations"] !== undefined) {
358
+ const val = parseBoolFlag(cliFlags["ui-animations"]);
359
+ if (val !== null) out.animations = val;
360
+ }
361
+
362
+ return out;
363
+ }
364
+
365
+ function parseBoolFlag(value) {
366
+ if (typeof value === "boolean") return value;
367
+ const v = String(value).trim().toLowerCase();
368
+ if (["1", "true", "yes", "y", "on"].includes(v)) return true;
369
+ if (["0", "false", "no", "n", "off"].includes(v)) return false;
370
+ return null;
371
+ }
372
+
319
373
  // ── Run ────────────────────────────────────────────────────────────────────
320
374
  main().catch((err) => {
321
375
  import("../src/lib/ui.js").then(({ error: uiError }) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-auto-agent",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "Scaffold and run an AI agent team powered by Ralphy + OpenCode. Presets for Java, Spring Boot, Next.js, and more.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -146,6 +146,13 @@ export async function doctor(targetDir, { verbose = false } = {}) {
146
146
  } else {
147
147
  softWarn("Docs directory", `${config.docsPath || "Docs"} not found (optional)`);
148
148
  }
149
+
150
+ if (config.ui && typeof config.ui === "object") {
151
+ const theme = config.ui.theme || "rich";
152
+ const compact = config.ui.compact || "auto";
153
+ const animations = config.ui.animations ?? true;
154
+ pass("UI config", `theme=${theme}, compact=${compact}, animations=${String(animations)}`);
155
+ }
149
156
  }
150
157
 
151
158
  // ── 4. Model validation against live discovery ────────────────────────────
@@ -272,6 +272,11 @@ function buildProjectConfig(preset, modelMapping) {
272
272
  workflowSteps: WORKFLOW_STEPS,
273
273
  requirePlanApproval: false,
274
274
  },
275
+ ui: {
276
+ theme: "rich",
277
+ compact: "auto",
278
+ animations: true,
279
+ },
275
280
  };
276
281
  }
277
282
 
@@ -196,6 +196,11 @@ export async function setup(targetDir, preset, { models: reconfigModels, nonInte
196
196
  config.preset = chosenPreset;
197
197
  config.models = modelMapping;
198
198
  config.version = SCHEMA_VERSION;
199
+ config.ui = config.ui || {
200
+ theme: "rich",
201
+ compact: "auto",
202
+ animations: true,
203
+ };
199
204
 
200
205
  // Update agents section
201
206
  if (!config.agents) config.agents = {};
package/src/lib/ui.js CHANGED
@@ -54,6 +54,22 @@ const BOX = {
54
54
  tee_r: "\u251c", tee_l: "\u2524",
55
55
  };
56
56
 
57
+ const UI_DEFAULTS = {
58
+ theme: "rich",
59
+ compact: "auto",
60
+ animations: true,
61
+ };
62
+
63
+ const uiState = { ...UI_DEFAULTS };
64
+ let sectionCounter = 0;
65
+
66
+ export function configureUI(options = {}) {
67
+ const next = { ...uiState, ...options };
68
+ uiState.theme = next.theme === "minimal" ? "minimal" : "rich";
69
+ uiState.compact = ["on", "off", "auto"].includes(next.compact) ? next.compact : "auto";
70
+ uiState.animations = Boolean(next.animations);
71
+ }
72
+
57
73
  // ── Layout helpers ─────────────────────────────────────────────────────────
58
74
 
59
75
  /**
@@ -75,19 +91,24 @@ export function hr(char = BOX.h, width) {
75
91
  * Print a styled header banner (OpenCode-style).
76
92
  */
77
93
  export function banner(title, subtitle) {
78
- const w = Math.min(termWidth(), 72);
79
- const pad = (s, len) => {
80
- const stripped = stripAnsi(s);
81
- return s + " ".repeat(Math.max(0, len - stripped.length));
82
- };
94
+ const indent = uiIndent();
95
+ if (uiState.theme === "minimal") {
96
+ console.log();
97
+ console.log(`${indent}${c.bold(title)}`);
98
+ if (subtitle) console.log(`${indent}${c.dim(subtitle)}`);
99
+ console.log();
100
+ return;
101
+ }
102
+
103
+ const innerWidth = Math.max(32, Math.min(termWidth() - 8, 72));
83
104
 
84
105
  console.log();
85
- console.log(c.cyan(` ${BOX.tl}${BOX.h.repeat(w - 4)}${BOX.tr}`));
86
- console.log(c.cyan(` ${BOX.v}`) + " " + c.bold(c.brightCyan(pad(title, w - 6))) + c.cyan(BOX.v));
106
+ console.log(c.cyan(`${indent}${BOX.tl}${BOX.h.repeat(innerWidth + 2)}${BOX.tr}`));
107
+ console.log(c.cyan(`${indent}${BOX.v}`) + ` ${c.bold(c.brightCyan(centerAnsi(title, innerWidth)))} ` + c.cyan(BOX.v));
87
108
  if (subtitle) {
88
- console.log(c.cyan(` ${BOX.v}`) + " " + c.gray(pad(subtitle, w - 6)) + c.cyan(BOX.v));
109
+ console.log(c.cyan(`${indent}${BOX.v}`) + ` ${c.gray(centerAnsi(subtitle, innerWidth))} ` + c.cyan(BOX.v));
89
110
  }
90
- console.log(c.cyan(` ${BOX.bl}${BOX.h.repeat(w - 4)}${BOX.br}`));
111
+ console.log(c.cyan(`${indent}${BOX.bl}${BOX.h.repeat(innerWidth + 2)}${BOX.br}`));
91
112
  console.log();
92
113
  }
93
114
 
@@ -95,9 +116,22 @@ export function banner(title, subtitle) {
95
116
  * Print a section header.
96
117
  */
97
118
  export function section(title) {
119
+ const indent = uiIndent();
120
+ if (uiState.theme === "minimal") {
121
+ console.log();
122
+ console.log(`${indent}${c.bold(title)}`);
123
+ console.log(`${indent}${"-".repeat(Math.max(4, stripAnsi(title).length))}`);
124
+ return;
125
+ }
126
+
127
+ const frame = ["◐", "◓", "◑", "◒"][sectionCounter++ % 4];
98
128
  console.log();
99
- console.log(` ${c.bold(c.cyan(title))}`);
100
- console.log(` ${c.gray(BOX.h.repeat(stripAnsi(title).length + 2))}`);
129
+ if (supportsAnimatedUi()) {
130
+ stdout.write(`\x1b[2K${indent}${c.gray(frame)} ${c.gray(title)}\r`);
131
+ }
132
+ const heading = `${c.brightCyan(frame)} ${c.bold(c.cyan(title))}`;
133
+ console.log(`${indent}${heading}`);
134
+ console.log(`${indent}${c.gray(BOX.h.repeat(stripAnsi(title).length + 2))}`);
101
135
  }
102
136
 
103
137
  /**
@@ -114,25 +148,29 @@ export function status(icon, label, detail) {
114
148
  star: c.yellow("\u2605"),
115
149
  };
116
150
  const i = icons[icon] || icon;
151
+ const indent = uiIndent();
152
+ const maxLabel = Math.max(12, Math.min(isCompactMode() ? 36 : 46, termWidth() - (isCompactMode() ? 18 : 26)));
153
+ const labelText = padAnsiEnd(truncateAnsi(label, maxLabel), maxLabel);
117
154
  const d = detail ? c.gray(` ${detail}`) : "";
118
- console.log(` ${i} ${label}${d}`);
155
+ console.log(`${indent}${i} ${labelText}${d}`);
119
156
  }
120
157
 
121
158
  /**
122
159
  * Print a key-value pair.
123
160
  */
124
161
  export function kv(key, value) {
125
- console.log(` ${c.gray(key + ":")} ${c.white(String(value))}`);
162
+ console.log(`${uiIndent()}${c.gray(key + ":")} ${c.white(String(value))}`);
126
163
  }
127
164
 
128
165
  /**
129
166
  * Print an error block.
130
167
  */
131
168
  export function error(message, hint) {
169
+ const indent = uiIndent();
132
170
  console.log();
133
- console.log(` ${c.bgRed(" ERROR ")} ${c.red(message)}`);
171
+ console.log(`${indent}${c.bgRed(" ERROR ")} ${c.red(message)}`);
134
172
  if (hint) {
135
- console.log(` ${c.gray(hint)}`);
173
+ console.log(`${indent}${" ".repeat(8)}${c.gray(hint)}`);
136
174
  }
137
175
  console.log();
138
176
  }
@@ -141,9 +179,10 @@ export function error(message, hint) {
141
179
  * Print a warning block.
142
180
  */
143
181
  export function warn(message, hint) {
144
- console.log(` ${c.bgYellow(" WARN ")} ${c.yellow(message)}`);
182
+ const indent = uiIndent();
183
+ console.log(`${indent}${c.bgYellow(" WARN ")} ${c.yellow(message)}`);
145
184
  if (hint) {
146
- console.log(` ${c.gray(hint)}`);
185
+ console.log(`${indent}${" ".repeat(7)}${c.gray(hint)}`);
147
186
  }
148
187
  }
149
188
 
@@ -151,8 +190,9 @@ export function warn(message, hint) {
151
190
  * Print a success block.
152
191
  */
153
192
  export function success(message) {
193
+ const indent = uiIndent();
154
194
  console.log();
155
- console.log(` ${c.bgGreen(" OK ")} ${c.green(message)}`);
195
+ console.log(`${indent}${c.bgGreen(" OK ")} ${c.green(message)}`);
156
196
  console.log();
157
197
  }
158
198
 
@@ -160,14 +200,26 @@ export function success(message) {
160
200
  * Print a boxed list of items.
161
201
  */
162
202
  export function boxList(title, items) {
163
- const w = Math.min(termWidth(), 68);
203
+ const indent = uiIndent();
204
+ if (uiState.theme === "minimal") {
205
+ console.log();
206
+ console.log(`${indent}${c.bold(title)}`);
207
+ for (const item of items) {
208
+ console.log(`${indent}- ${truncateAnsi(item, Math.max(20, termWidth() - indent.length - 4))}`);
209
+ }
210
+ return;
211
+ }
212
+
213
+ const innerWidth = Math.max(26, Math.min(termWidth() - 8, 68));
164
214
  console.log();
165
- console.log(c.gray(` ${BOX.tl}${BOX.h} ${c.bold(c.white(title))} ${c.gray(BOX.h.repeat(Math.max(0, w - stripAnsi(title).length - 6)))}${c.gray(BOX.tr)}`));
215
+ console.log(c.gray(`${indent}${BOX.tl}${BOX.h.repeat(innerWidth + 2)}${BOX.tr}`));
216
+ console.log(c.gray(`${indent}${BOX.v}`) + ` ${c.bold(c.white(centerAnsi(title, innerWidth)))} ` + c.gray(BOX.v));
217
+ console.log(c.gray(`${indent}${BOX.tee_r}${BOX.h.repeat(innerWidth + 2)}${BOX.tee_l}`));
166
218
  for (const item of items) {
167
- const line = ` ${c.gray(BOX.v)} ${item}`;
168
- console.log(line + " ".repeat(Math.max(0, w - stripAnsi(line).length + 2)) + c.gray(BOX.v));
219
+ const line = padAnsiEnd(truncateAnsi(item, innerWidth), innerWidth);
220
+ console.log(c.gray(`${indent}${BOX.v}`) + ` ${line} ` + c.gray(BOX.v));
169
221
  }
170
- console.log(c.gray(` ${BOX.bl}${BOX.h.repeat(w - 2)}${BOX.br}`));
222
+ console.log(c.gray(`${indent}${BOX.bl}${BOX.h.repeat(innerWidth + 2)}${BOX.br}`));
171
223
  }
172
224
 
173
225
  // ── Spinner ────────────────────────────────────────────────────────────────
@@ -177,22 +229,27 @@ const SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u283
177
229
  export function createSpinner(text) {
178
230
  let i = 0;
179
231
  let interval = null;
232
+ const colors = uiState.theme === "minimal"
233
+ ? [c.gray]
234
+ : [c.cyan, c.brightCyan, c.blue, c.magenta];
180
235
 
181
236
  return {
182
237
  start() {
183
238
  interval = setInterval(() => {
184
- const frame = c.cyan(SPINNER_FRAMES[i % SPINNER_FRAMES.length]);
185
- stdout.write(`\r ${frame} ${text}`);
239
+ const colorFn = colors[i % colors.length];
240
+ const frame = colorFn(SPINNER_FRAMES[i % SPINNER_FRAMES.length]);
241
+ const trail = supportsAnimatedUi() && uiState.theme !== "minimal" ? c.gray(" •") : "";
242
+ stdout.write(`\r${uiIndent()}${frame} ${text}${trail}`);
186
243
  i++;
187
244
  }, 80);
188
245
  },
189
246
  stop(finalText) {
190
247
  if (interval) clearInterval(interval);
191
- stdout.write(`\r ${c.green("\u2713")} ${finalText || text}\n`);
248
+ stdout.write(`\r${uiIndent()}${c.green("\u2713")} ${finalText || text}\n`);
192
249
  },
193
250
  fail(finalText) {
194
251
  if (interval) clearInterval(interval);
195
- stdout.write(`\r ${c.red("\u2717")} ${finalText || text}\n`);
252
+ stdout.write(`\r${uiIndent()}${c.red("\u2717")} ${finalText || text}\n`);
196
253
  },
197
254
  };
198
255
  }
@@ -206,7 +263,7 @@ export async function confirm(question, defaultYes = true) {
206
263
  const rl = createInterface({ input: stdin, output: stdout });
207
264
  const hint = defaultYes ? "Y/n" : "y/N";
208
265
  return new Promise((resolve) => {
209
- rl.question(` ${c.cyan("?")} ${question} ${c.gray(`(${hint})`)} `, (answer) => {
266
+ rl.question(`${uiIndent()}${c.cyan("?")} ${question} ${c.gray(`(${hint})`)} `, (answer) => {
210
267
  rl.close();
211
268
  const a = answer.trim().toLowerCase();
212
269
  if (a === "") resolve(defaultYes);
@@ -222,7 +279,7 @@ export async function input(question, defaultValue = "") {
222
279
  const rl = createInterface({ input: stdin, output: stdout });
223
280
  const hint = defaultValue ? c.gray(` (${defaultValue})`) : "";
224
281
  return new Promise((resolve) => {
225
- rl.question(` ${c.cyan("?")} ${question}${hint} `, (answer) => {
282
+ rl.question(`${uiIndent()}${c.cyan("?")} ${question}${hint} `, (answer) => {
226
283
  rl.close();
227
284
  resolve(answer.trim() || defaultValue);
228
285
  });
@@ -258,7 +315,7 @@ export async function select(title, items) {
258
315
  stdout.write(`\x1b[${totalLines}A`);
259
316
  }
260
317
 
261
- stdout.write(`\x1b[2K ${c.cyan("?")} ${c.bold(title)}\n`);
318
+ stdout.write(`\x1b[2K${uiIndent()}${c.cyan("?")} ${c.bold(title)}\n`);
262
319
  for (let i = 0; i < visible.length; i++) {
263
320
  const itemIndex = start + i;
264
321
  const item = visible[i];
@@ -266,13 +323,13 @@ export async function select(title, items) {
266
323
  const pointer = isSelected ? c.cyan("\u25B6") : " ";
267
324
  const label = isSelected ? c.cyan(item.label) : c.white(item.label);
268
325
  const hint = item.hint ? c.gray(` ${item.hint}`) : "";
269
- stdout.write(`\x1b[2K ${pointer} ${label}${hint}\n`);
326
+ stdout.write(`\x1b[2K${uiIndent()}${pointer} ${label}${hint}\n`);
270
327
  }
271
328
 
272
329
  const range = `${start + 1}-${end}`;
273
330
  const moreAbove = start > 0 ? c.gray("↑") : c.gray(" ");
274
331
  const moreBelow = end < items.length ? c.gray("↓") : c.gray(" ");
275
- stdout.write(`\x1b[2K ${c.gray(`[${range}/${items.length}] ${moreAbove}${moreBelow} arrows/jk + enter`)}` + "\n");
332
+ stdout.write(`\x1b[2K${uiIndent()}${c.gray(`[${range}/${items.length}] ${moreAbove}${moreBelow} arrows/jk + enter`)}` + "\n");
276
333
  hasRendered = true;
277
334
  };
278
335
 
@@ -300,7 +357,7 @@ export async function select(title, items) {
300
357
  stdout.write("\x1b[2K\n");
301
358
  }
302
359
  stdout.write(`\x1b[${totalLines}A`);
303
- stdout.write(`\x1b[2K ${c.green("\u2713")} ${c.bold(title)} ${c.cyan(items[cursor].label)}\n`);
360
+ stdout.write(`\x1b[2K${uiIndent()}${c.green("\u2713")} ${c.bold(title)} ${c.cyan(items[cursor].label)}\n`);
304
361
  resolve(items[cursor]);
305
362
  return;
306
363
  }
@@ -350,7 +407,7 @@ export async function multiSelect(title, items) {
350
407
  stdout.write(`\x1b[${totalLines}A`);
351
408
  }
352
409
 
353
- stdout.write(`\x1b[2K ${c.cyan("?")} ${c.bold(title)} ${c.gray("(space to toggle)")}\n`);
410
+ stdout.write(`\x1b[2K${uiIndent()}${c.cyan("?")} ${c.bold(title)} ${c.gray("(space to toggle)")}\n`);
354
411
  for (let i = 0; i < visible.length; i++) {
355
412
  const itemIndex = start + i;
356
413
  const item = visible[i];
@@ -360,13 +417,13 @@ export async function multiSelect(title, items) {
360
417
  const check = isSelected ? c.green("\u25C9") : c.gray("\u25CB");
361
418
  const label = isCursor ? c.cyan(item.label) : c.white(item.label);
362
419
  const hint = item.hint ? c.gray(` ${item.hint}`) : "";
363
- stdout.write(`\x1b[2K ${pointer} ${check} ${label}${hint}\n`);
420
+ stdout.write(`\x1b[2K${uiIndent()}${pointer} ${check} ${label}${hint}\n`);
364
421
  }
365
422
 
366
423
  const range = `${start + 1}-${end}`;
367
424
  const moreAbove = start > 0 ? c.gray("↑") : c.gray(" ");
368
425
  const moreBelow = end < items.length ? c.gray("↓") : c.gray(" ");
369
- stdout.write(`\x1b[2K ${c.gray(`[${range}/${items.length}] ${moreAbove}${moreBelow} arrows/jk + space + enter`)}` + "\n");
426
+ stdout.write(`\x1b[2K${uiIndent()}${c.gray(`[${range}/${items.length}] ${moreAbove}${moreBelow} arrows/jk + space + enter`)}` + "\n");
370
427
  hasRendered = true;
371
428
  };
372
429
 
@@ -394,7 +451,7 @@ export async function multiSelect(title, items) {
394
451
  stdout.write("\x1b[2K\n");
395
452
  }
396
453
  stdout.write(`\x1b[${totalLines}A`);
397
- stdout.write(`\x1b[2K ${c.green("\u2713")} ${c.bold(title)} ${c.cyan(result.map((r) => r.label).join(", "))}\n`);
454
+ stdout.write(`\x1b[2K${uiIndent()}${c.green("\u2713")} ${c.bold(title)} ${c.cyan(result.map((r) => r.label).join(", "))}\n`);
398
455
  resolve(result);
399
456
  return;
400
457
  }
@@ -423,18 +480,27 @@ export async function multiSelect(title, items) {
423
480
  * @param {string[][]} rows
424
481
  */
425
482
  export function table(headers, rows) {
483
+ const indent = uiIndent();
484
+ const compact = isCompactMode();
426
485
  const colWidths = headers.map((h, i) => {
427
486
  const maxData = rows.reduce((max, row) => Math.max(max, stripAnsi(String(row[i] || "")).length), 0);
428
- return Math.max(stripAnsi(h).length, maxData) + 2;
487
+ return Math.max(stripAnsi(h).length, maxData) + (compact ? 1 : 2);
429
488
  });
430
489
 
431
- const headerLine = headers.map((h, i) => c.bold(padAnsiEnd(h, colWidths[i]))).join(c.gray(" \u2502 "));
432
- console.log(` ${headerLine}`);
433
- console.log(` ${colWidths.map((w) => c.gray(BOX.h.repeat(w))).join(c.gray("\u2500\u253c\u2500"))}`);
490
+ fitColumnsToViewport(colWidths, headers, rows, compact);
491
+
492
+ const colSep = compact ? c.gray("\u2502") : c.gray(" \u2502 ");
493
+ const ruleSep = compact ? c.gray("\u253c") : c.gray("\u2500\u253c\u2500");
494
+
495
+ const headerLine = headers.map((h, i) => c.bold(padAnsiEnd(truncateAnsi(h, colWidths[i]), colWidths[i]))).join(colSep);
496
+ console.log(`${indent}${headerLine}`);
497
+ console.log(`${indent}${colWidths.map((w) => c.gray(BOX.h.repeat(w))).join(ruleSep)}`);
434
498
 
435
499
  for (const row of rows) {
436
- const line = row.map((cell, i) => padAnsiEnd(cell, colWidths[i])).join(c.gray(" \u2502 "));
437
- console.log(` ${line}`);
500
+ const line = row
501
+ .map((cell, i) => padAnsiEnd(truncateAnsi(cell, colWidths[i]), colWidths[i]))
502
+ .join(colSep);
503
+ console.log(`${indent}${line}`);
438
504
  }
439
505
  }
440
506
 
@@ -448,6 +514,22 @@ export function stripAnsi(str) {
448
514
  return str.replace(/\x1b\[[0-9;]*m/g, "");
449
515
  }
450
516
 
517
+ function centerAnsi(value, width) {
518
+ const text = String(value || "");
519
+ const visible = stripAnsi(text).length;
520
+ if (visible >= width) return text;
521
+ const left = Math.floor((width - visible) / 2);
522
+ const right = width - visible - left;
523
+ return " ".repeat(left) + text + " ".repeat(right);
524
+ }
525
+
526
+ function truncateAnsi(value, maxLen = 40) {
527
+ const text = String(value || "");
528
+ const plain = stripAnsi(text);
529
+ if (plain.length <= maxLen) return text;
530
+ return `${plain.slice(0, maxLen - 1)}…`;
531
+ }
532
+
451
533
  function padAnsiEnd(value, width) {
452
534
  const text = String(value || "");
453
535
  const visible = stripAnsi(text).length;
@@ -455,6 +537,53 @@ function padAnsiEnd(value, width) {
455
537
  return text + " ".repeat(width - visible);
456
538
  }
457
539
 
540
+ function fitColumnsToViewport(colWidths, headers, rows, compact) {
541
+ const sepWidth = compact ? 1 : 3;
542
+ const indentWidth = uiIndent().length;
543
+ const minCol = compact ? 6 : 8;
544
+ const maxLine = Math.max(32, termWidth() - indentWidth - 1);
545
+
546
+ const total = () => colWidths.reduce((sum, w) => sum + w, 0) + sepWidth * Math.max(0, colWidths.length - 1);
547
+ if (total() <= maxLine) return;
548
+
549
+ const priorities = headers.map((_, i) => i).sort((a, b) => {
550
+ const wa = colWidths[a];
551
+ const wb = colWidths[b];
552
+ return wb - wa;
553
+ });
554
+
555
+ while (total() > maxLine) {
556
+ let changed = false;
557
+ for (const idx of priorities) {
558
+ if (colWidths[idx] > minCol) {
559
+ colWidths[idx]--;
560
+ changed = true;
561
+ }
562
+ if (total() <= maxLine) break;
563
+ }
564
+ if (!changed) break;
565
+ }
566
+
567
+ if (total() > maxLine && rows.length > 0) {
568
+ const last = colWidths.length - 1;
569
+ colWidths[last] = Math.max(minCol, maxLine - (total() - colWidths[last]));
570
+ }
571
+ }
572
+
573
+ function isCompactMode() {
574
+ if (uiState.compact === "on") return true;
575
+ if (uiState.compact === "off") return false;
576
+ return termWidth() < 96;
577
+ }
578
+
579
+ function uiIndent() {
580
+ return isCompactMode() ? " " : " ";
581
+ }
582
+
583
+ function supportsAnimatedUi() {
584
+ return Boolean(uiState.animations && stdin.isTTY && stdout.isTTY);
585
+ }
586
+
458
587
  /**
459
588
  * Truncate a string to maxLen, adding ellipsis if needed.
460
589
  */
@@ -517,30 +646,30 @@ function supportsInteractiveMenu() {
517
646
  }
518
647
 
519
648
  async function selectFallback(title, items) {
520
- console.log(` ${c.cyan("?")} ${c.bold(title)}`);
649
+ console.log(`${uiIndent()}${c.cyan("?")} ${c.bold(title)}`);
521
650
  for (let i = 0; i < items.length; i++) {
522
651
  const item = items[i];
523
652
  const hint = item.hint ? ` ${c.gray(item.hint)}` : "";
524
- console.log(` ${String(i + 1).padStart(2, " ")}) ${item.label}${hint}`);
653
+ console.log(`${uiIndent()} ${String(i + 1).padStart(2, " ")}) ${item.label}${hint}`);
525
654
  }
526
655
  const answer = await input("Choose an option number", "1");
527
656
  const idx = clampIndex(parseInt(answer, 10) - 1, items.length);
528
- console.log(` ${c.green("\u2713")} ${c.bold(title)} ${c.cyan(items[idx].label)}`);
657
+ console.log(`${uiIndent()}${c.green("\u2713")} ${c.bold(title)} ${c.cyan(items[idx].label)}`);
529
658
  return items[idx];
530
659
  }
531
660
 
532
661
  async function multiSelectFallback(title, items) {
533
- console.log(` ${c.cyan("?")} ${c.bold(title)}`);
662
+ console.log(`${uiIndent()}${c.cyan("?")} ${c.bold(title)}`);
534
663
  for (let i = 0; i < items.length; i++) {
535
664
  const item = items[i];
536
665
  const mark = item.selected ? "*" : " ";
537
666
  const hint = item.hint ? ` ${c.gray(item.hint)}` : "";
538
- console.log(` ${String(i + 1).padStart(2, " ")}) [${mark}] ${item.label}${hint}`);
667
+ console.log(`${uiIndent()} ${String(i + 1).padStart(2, " ")}) [${mark}] ${item.label}${hint}`);
539
668
  }
540
669
  const answer = await input("Choose option numbers (comma-separated)", "1");
541
670
  const indexes = parseMultiNumberInput(answer, items.length);
542
671
  const selected = indexes.map((i) => items[i]);
543
- console.log(` ${c.green("\u2713")} ${c.bold(title)} ${c.cyan(selected.map((it) => it.label).join(", "))}`);
672
+ console.log(`${uiIndent()}${c.green("\u2713")} ${c.bold(title)} ${c.cyan(selected.map((it) => it.label).join(", "))}`);
544
673
  return selected;
545
674
  }
546
675