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 +54 -0
- package/package.json +1 -1
- package/src/commands/doctor.js +7 -0
- package/src/commands/init.js +5 -0
- package/src/commands/setup.js +5 -0
- package/src/lib/ui.js +178 -49
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
package/src/commands/doctor.js
CHANGED
|
@@ -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 ────────────────────────────
|
package/src/commands/init.js
CHANGED
package/src/commands/setup.js
CHANGED
|
@@ -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
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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(
|
|
86
|
-
console.log(c.cyan(
|
|
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(
|
|
109
|
+
console.log(c.cyan(`${indent}${BOX.v}`) + ` ${c.gray(centerAnsi(subtitle, innerWidth))} ` + c.cyan(BOX.v));
|
|
89
110
|
}
|
|
90
|
-
console.log(c.cyan(
|
|
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
|
-
|
|
100
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
171
|
+
console.log(`${indent}${c.bgRed(" ERROR ")} ${c.red(message)}`);
|
|
134
172
|
if (hint) {
|
|
135
|
-
console.log(
|
|
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
|
-
|
|
182
|
+
const indent = uiIndent();
|
|
183
|
+
console.log(`${indent}${c.bgYellow(" WARN ")} ${c.yellow(message)}`);
|
|
145
184
|
if (hint) {
|
|
146
|
-
console.log(
|
|
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(
|
|
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
|
|
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(
|
|
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 =
|
|
168
|
-
console.log(
|
|
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(
|
|
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
|
|
185
|
-
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
|
437
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|