opencode-auto-agent 1.1.0 → 1.2.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/README.md +23 -5
- package/bin/cli.js +275 -51
- package/package.json +1 -1
- package/src/commands/doctor.js +337 -54
- package/src/commands/init.js +337 -114
- package/src/commands/run.js +182 -61
- package/src/commands/setup.js +321 -21
- package/src/lib/constants.js +90 -14
- package/src/lib/models.js +172 -0
- package/src/lib/prerequisites.js +151 -0
- package/src/lib/ui.js +446 -0
- package/templates/sample-config.json +47 -8
package/src/lib/ui.js
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal UI utilities — zero-dep, inspired by OpenCode's TUI aesthetic.
|
|
3
|
+
*
|
|
4
|
+
* Provides box-drawing, ANSI colors, spinners, and interactive prompts
|
|
5
|
+
* that work on Node 18+ with raw stdin.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createInterface } from "node:readline";
|
|
9
|
+
import { stdin, stdout } from "node:process";
|
|
10
|
+
|
|
11
|
+
// ── ANSI color helpers ─────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
const ESC = "\x1b[";
|
|
14
|
+
|
|
15
|
+
export const c = {
|
|
16
|
+
reset: (s) => `${ESC}0m${s}${ESC}0m`,
|
|
17
|
+
bold: (s) => `${ESC}1m${s}${ESC}0m`,
|
|
18
|
+
dim: (s) => `${ESC}2m${s}${ESC}0m`,
|
|
19
|
+
italic: (s) => `${ESC}3m${s}${ESC}0m`,
|
|
20
|
+
underline:(s) => `${ESC}4m${s}${ESC}0m`,
|
|
21
|
+
|
|
22
|
+
// Theme colors (OpenCode-inspired)
|
|
23
|
+
cyan: (s) => `${ESC}36m${s}${ESC}0m`,
|
|
24
|
+
green: (s) => `${ESC}32m${s}${ESC}0m`,
|
|
25
|
+
yellow: (s) => `${ESC}33m${s}${ESC}0m`,
|
|
26
|
+
red: (s) => `${ESC}31m${s}${ESC}0m`,
|
|
27
|
+
magenta: (s) => `${ESC}35m${s}${ESC}0m`,
|
|
28
|
+
blue: (s) => `${ESC}34m${s}${ESC}0m`,
|
|
29
|
+
white: (s) => `${ESC}37m${s}${ESC}0m`,
|
|
30
|
+
gray: (s) => `${ESC}90m${s}${ESC}0m`,
|
|
31
|
+
|
|
32
|
+
// Bright variants
|
|
33
|
+
brightCyan: (s) => `${ESC}96m${s}${ESC}0m`,
|
|
34
|
+
brightGreen: (s) => `${ESC}92m${s}${ESC}0m`,
|
|
35
|
+
brightYellow: (s) => `${ESC}93m${s}${ESC}0m`,
|
|
36
|
+
brightRed: (s) => `${ESC}91m${s}${ESC}0m`,
|
|
37
|
+
brightMagenta: (s) => `${ESC}95m${s}${ESC}0m`,
|
|
38
|
+
|
|
39
|
+
// Background
|
|
40
|
+
bgCyan: (s) => `${ESC}46m${ESC}30m${s}${ESC}0m`,
|
|
41
|
+
bgGreen: (s) => `${ESC}42m${ESC}30m${s}${ESC}0m`,
|
|
42
|
+
bgYellow: (s) => `${ESC}43m${ESC}30m${s}${ESC}0m`,
|
|
43
|
+
bgRed: (s) => `${ESC}41m${ESC}37m${s}${ESC}0m`,
|
|
44
|
+
bgBlue: (s) => `${ESC}44m${ESC}37m${s}${ESC}0m`,
|
|
45
|
+
bgMagenta: (s) => `${ESC}45m${ESC}37m${s}${ESC}0m`,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ── Box-drawing characters ─────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
const BOX = {
|
|
51
|
+
tl: "\u256d", tr: "\u256e", bl: "\u2570", br: "\u256f",
|
|
52
|
+
h: "\u2500", v: "\u2502",
|
|
53
|
+
cross: "\u253c",
|
|
54
|
+
tee_r: "\u251c", tee_l: "\u2524",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// ── Layout helpers ─────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get terminal width, fallback to 80.
|
|
61
|
+
*/
|
|
62
|
+
function termWidth() {
|
|
63
|
+
return stdout.columns || 80;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Draw a horizontal rule.
|
|
68
|
+
*/
|
|
69
|
+
export function hr(char = BOX.h, width) {
|
|
70
|
+
const w = width || termWidth();
|
|
71
|
+
console.log(c.gray(char.repeat(w)));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Print a styled header banner (OpenCode-style).
|
|
76
|
+
*/
|
|
77
|
+
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
|
+
};
|
|
83
|
+
|
|
84
|
+
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));
|
|
87
|
+
if (subtitle) {
|
|
88
|
+
console.log(c.cyan(` ${BOX.v}`) + " " + c.gray(pad(subtitle, w - 6)) + c.cyan(BOX.v));
|
|
89
|
+
}
|
|
90
|
+
console.log(c.cyan(` ${BOX.bl}${BOX.h.repeat(w - 4)}${BOX.br}`));
|
|
91
|
+
console.log();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Print a section header.
|
|
96
|
+
*/
|
|
97
|
+
export function section(title) {
|
|
98
|
+
console.log();
|
|
99
|
+
console.log(` ${c.bold(c.cyan(title))}`);
|
|
100
|
+
console.log(` ${c.gray(BOX.h.repeat(stripAnsi(title).length + 2))}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Print a status line with icon.
|
|
105
|
+
*/
|
|
106
|
+
export function status(icon, label, detail) {
|
|
107
|
+
const icons = {
|
|
108
|
+
pass: c.green("\u2713"),
|
|
109
|
+
fail: c.red("\u2717"),
|
|
110
|
+
warn: c.yellow("\u26A0"),
|
|
111
|
+
info: c.cyan("\u2139"),
|
|
112
|
+
arrow: c.cyan("\u25B6"),
|
|
113
|
+
dot: c.gray("\u2022"),
|
|
114
|
+
star: c.yellow("\u2605"),
|
|
115
|
+
};
|
|
116
|
+
const i = icons[icon] || icon;
|
|
117
|
+
const d = detail ? c.gray(` ${detail}`) : "";
|
|
118
|
+
console.log(` ${i} ${label}${d}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Print a key-value pair.
|
|
123
|
+
*/
|
|
124
|
+
export function kv(key, value) {
|
|
125
|
+
console.log(` ${c.gray(key + ":")} ${c.white(String(value))}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Print an error block.
|
|
130
|
+
*/
|
|
131
|
+
export function error(message, hint) {
|
|
132
|
+
console.log();
|
|
133
|
+
console.log(` ${c.bgRed(" ERROR ")} ${c.red(message)}`);
|
|
134
|
+
if (hint) {
|
|
135
|
+
console.log(` ${c.gray(hint)}`);
|
|
136
|
+
}
|
|
137
|
+
console.log();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Print a warning block.
|
|
142
|
+
*/
|
|
143
|
+
export function warn(message, hint) {
|
|
144
|
+
console.log(` ${c.bgYellow(" WARN ")} ${c.yellow(message)}`);
|
|
145
|
+
if (hint) {
|
|
146
|
+
console.log(` ${c.gray(hint)}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Print a success block.
|
|
152
|
+
*/
|
|
153
|
+
export function success(message) {
|
|
154
|
+
console.log();
|
|
155
|
+
console.log(` ${c.bgGreen(" OK ")} ${c.green(message)}`);
|
|
156
|
+
console.log();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Print a boxed list of items.
|
|
161
|
+
*/
|
|
162
|
+
export function boxList(title, items) {
|
|
163
|
+
const w = Math.min(termWidth(), 68);
|
|
164
|
+
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)}`));
|
|
166
|
+
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));
|
|
169
|
+
}
|
|
170
|
+
console.log(c.gray(` ${BOX.bl}${BOX.h.repeat(w - 2)}${BOX.br}`));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Spinner ────────────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
const SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
176
|
+
|
|
177
|
+
export function createSpinner(text) {
|
|
178
|
+
let i = 0;
|
|
179
|
+
let interval = null;
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
start() {
|
|
183
|
+
interval = setInterval(() => {
|
|
184
|
+
const frame = c.cyan(SPINNER_FRAMES[i % SPINNER_FRAMES.length]);
|
|
185
|
+
stdout.write(`\r ${frame} ${text}`);
|
|
186
|
+
i++;
|
|
187
|
+
}, 80);
|
|
188
|
+
},
|
|
189
|
+
stop(finalText) {
|
|
190
|
+
if (interval) clearInterval(interval);
|
|
191
|
+
stdout.write(`\r ${c.green("\u2713")} ${finalText || text}\n`);
|
|
192
|
+
},
|
|
193
|
+
fail(finalText) {
|
|
194
|
+
if (interval) clearInterval(interval);
|
|
195
|
+
stdout.write(`\r ${c.red("\u2717")} ${finalText || text}\n`);
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── Interactive prompts (zero-dep) ─────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Ask a yes/no confirmation.
|
|
204
|
+
*/
|
|
205
|
+
export async function confirm(question, defaultYes = true) {
|
|
206
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
207
|
+
const hint = defaultYes ? "Y/n" : "y/N";
|
|
208
|
+
return new Promise((resolve) => {
|
|
209
|
+
rl.question(` ${c.cyan("?")} ${question} ${c.gray(`(${hint})`)} `, (answer) => {
|
|
210
|
+
rl.close();
|
|
211
|
+
const a = answer.trim().toLowerCase();
|
|
212
|
+
if (a === "") resolve(defaultYes);
|
|
213
|
+
else resolve(a === "y" || a === "yes");
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Ask for text input.
|
|
220
|
+
*/
|
|
221
|
+
export async function input(question, defaultValue = "") {
|
|
222
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
223
|
+
const hint = defaultValue ? c.gray(` (${defaultValue})`) : "";
|
|
224
|
+
return new Promise((resolve) => {
|
|
225
|
+
rl.question(` ${c.cyan("?")} ${question}${hint} `, (answer) => {
|
|
226
|
+
rl.close();
|
|
227
|
+
resolve(answer.trim() || defaultValue);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Interactive single-select menu with arrow-key navigation.
|
|
234
|
+
* Returns the selected item object.
|
|
235
|
+
*
|
|
236
|
+
* @param {string} title - Prompt title
|
|
237
|
+
* @param {Array<{label: string, value: any, hint?: string}>} items
|
|
238
|
+
* @returns {Promise<{label: string, value: any}>}
|
|
239
|
+
*/
|
|
240
|
+
export async function select(title, items) {
|
|
241
|
+
if (!items.length) throw new Error("select: no items provided");
|
|
242
|
+
|
|
243
|
+
return new Promise((resolve) => {
|
|
244
|
+
let cursor = 0;
|
|
245
|
+
|
|
246
|
+
const render = () => {
|
|
247
|
+
// Move cursor up to overwrite previous render
|
|
248
|
+
if (cursor !== -1) {
|
|
249
|
+
stdout.write(`\x1b[${items.length + 1}A`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
stdout.write(` ${c.cyan("?")} ${c.bold(title)}\n`);
|
|
253
|
+
for (let i = 0; i < items.length; i++) {
|
|
254
|
+
const item = items[i];
|
|
255
|
+
const selected = i === cursor;
|
|
256
|
+
const pointer = selected ? c.cyan("\u25B6") : " ";
|
|
257
|
+
const label = selected ? c.cyan(item.label) : c.white(item.label);
|
|
258
|
+
const hint = item.hint ? c.gray(` ${item.hint}`) : "";
|
|
259
|
+
stdout.write(` ${pointer} ${label}${hint}\n`);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// Initial render
|
|
264
|
+
stdout.write(` ${c.cyan("?")} ${c.bold(title)}\n`);
|
|
265
|
+
for (let i = 0; i < items.length; i++) {
|
|
266
|
+
const item = items[i];
|
|
267
|
+
const selected = i === cursor;
|
|
268
|
+
const pointer = selected ? c.cyan("\u25B6") : " ";
|
|
269
|
+
const label = selected ? c.cyan(item.label) : c.white(item.label);
|
|
270
|
+
const hint = item.hint ? c.gray(` ${item.hint}`) : "";
|
|
271
|
+
stdout.write(` ${pointer} ${label}${hint}\n`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
stdin.setRawMode(true);
|
|
275
|
+
stdin.resume();
|
|
276
|
+
stdin.setEncoding("utf8");
|
|
277
|
+
|
|
278
|
+
const onData = (key) => {
|
|
279
|
+
// Ctrl+C
|
|
280
|
+
if (key === "\x03") {
|
|
281
|
+
stdin.setRawMode(false);
|
|
282
|
+
stdin.removeListener("data", onData);
|
|
283
|
+
stdin.pause();
|
|
284
|
+
process.exit(130);
|
|
285
|
+
}
|
|
286
|
+
// Enter
|
|
287
|
+
if (key === "\r" || key === "\n") {
|
|
288
|
+
stdin.setRawMode(false);
|
|
289
|
+
stdin.removeListener("data", onData);
|
|
290
|
+
stdin.pause();
|
|
291
|
+
// Clear and reprint final selection
|
|
292
|
+
stdout.write(`\x1b[${items.length + 1}A`);
|
|
293
|
+
stdout.write(` ${c.green("\u2713")} ${c.bold(title)} ${c.cyan(items[cursor].label)}\n`);
|
|
294
|
+
for (let i = 0; i < items.length; i++) {
|
|
295
|
+
stdout.write(`\x1b[2K\n`); // clear each line
|
|
296
|
+
}
|
|
297
|
+
stdout.write(`\x1b[${items.length}A`); // move back up
|
|
298
|
+
resolve(items[cursor]);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
// Arrow up / k
|
|
302
|
+
if (key === "\x1b[A" || key === "k") {
|
|
303
|
+
cursor = (cursor - 1 + items.length) % items.length;
|
|
304
|
+
render();
|
|
305
|
+
}
|
|
306
|
+
// Arrow down / j
|
|
307
|
+
if (key === "\x1b[B" || key === "j") {
|
|
308
|
+
cursor = (cursor + 1) % items.length;
|
|
309
|
+
render();
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
stdin.on("data", onData);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Interactive multi-select menu with space to toggle and arrow keys to navigate.
|
|
319
|
+
* Returns array of selected item objects.
|
|
320
|
+
*
|
|
321
|
+
* @param {string} title - Prompt title
|
|
322
|
+
* @param {Array<{label: string, value: any, hint?: string, selected?: boolean}>} items
|
|
323
|
+
* @returns {Promise<Array<{label: string, value: any}>>}
|
|
324
|
+
*/
|
|
325
|
+
export async function multiSelect(title, items) {
|
|
326
|
+
if (!items.length) throw new Error("multiSelect: no items provided");
|
|
327
|
+
|
|
328
|
+
return new Promise((resolve) => {
|
|
329
|
+
let cursor = 0;
|
|
330
|
+
const selected = new Set(
|
|
331
|
+
items.filter((it) => it.selected).map((_, i) => i)
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
const render = () => {
|
|
335
|
+
stdout.write(`\x1b[${items.length + 2}A`);
|
|
336
|
+
stdout.write(` ${c.cyan("?")} ${c.bold(title)} ${c.gray("(space to toggle, enter to confirm)")}\n`);
|
|
337
|
+
for (let i = 0; i < items.length; i++) {
|
|
338
|
+
const item = items[i];
|
|
339
|
+
const isCursor = i === cursor;
|
|
340
|
+
const isSelected = selected.has(i);
|
|
341
|
+
const pointer = isCursor ? c.cyan("\u25B6") : " ";
|
|
342
|
+
const check = isSelected ? c.green("\u25C9") : c.gray("\u25CB");
|
|
343
|
+
const label = isCursor ? c.cyan(item.label) : c.white(item.label);
|
|
344
|
+
const hint = item.hint ? c.gray(` ${item.hint}`) : "";
|
|
345
|
+
stdout.write(`\x1b[2K ${pointer} ${check} ${label}${hint}\n`);
|
|
346
|
+
}
|
|
347
|
+
stdout.write("\x1b[2K\n");
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
// Initial render
|
|
351
|
+
stdout.write(` ${c.cyan("?")} ${c.bold(title)} ${c.gray("(space to toggle, enter to confirm)")}\n`);
|
|
352
|
+
for (let i = 0; i < items.length; i++) {
|
|
353
|
+
const item = items[i];
|
|
354
|
+
const isCursor = i === cursor;
|
|
355
|
+
const isSelected = selected.has(i);
|
|
356
|
+
const pointer = isCursor ? c.cyan("\u25B6") : " ";
|
|
357
|
+
const check = isSelected ? c.green("\u25C9") : c.gray("\u25CB");
|
|
358
|
+
const label = isCursor ? c.cyan(item.label) : c.white(item.label);
|
|
359
|
+
const hint = item.hint ? c.gray(` ${item.hint}`) : "";
|
|
360
|
+
stdout.write(` ${pointer} ${check} ${label}${hint}\n`);
|
|
361
|
+
}
|
|
362
|
+
stdout.write("\n");
|
|
363
|
+
|
|
364
|
+
stdin.setRawMode(true);
|
|
365
|
+
stdin.resume();
|
|
366
|
+
stdin.setEncoding("utf8");
|
|
367
|
+
|
|
368
|
+
const onData = (key) => {
|
|
369
|
+
if (key === "\x03") {
|
|
370
|
+
stdin.setRawMode(false);
|
|
371
|
+
stdin.removeListener("data", onData);
|
|
372
|
+
stdin.pause();
|
|
373
|
+
process.exit(130);
|
|
374
|
+
}
|
|
375
|
+
if (key === "\r" || key === "\n") {
|
|
376
|
+
stdin.setRawMode(false);
|
|
377
|
+
stdin.removeListener("data", onData);
|
|
378
|
+
stdin.pause();
|
|
379
|
+
const result = [...selected].map((i) => items[i]);
|
|
380
|
+
// Clear and show final
|
|
381
|
+
stdout.write(`\x1b[${items.length + 2}A`);
|
|
382
|
+
stdout.write(`\x1b[2K ${c.green("\u2713")} ${c.bold(title)} ${c.cyan(result.map((r) => r.label).join(", "))}\n`);
|
|
383
|
+
for (let i = 0; i < items.length + 1; i++) {
|
|
384
|
+
stdout.write(`\x1b[2K\n`);
|
|
385
|
+
}
|
|
386
|
+
stdout.write(`\x1b[${items.length + 1}A`);
|
|
387
|
+
resolve(result);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (key === " ") {
|
|
391
|
+
if (selected.has(cursor)) selected.delete(cursor);
|
|
392
|
+
else selected.add(cursor);
|
|
393
|
+
render();
|
|
394
|
+
}
|
|
395
|
+
if (key === "\x1b[A" || key === "k") {
|
|
396
|
+
cursor = (cursor - 1 + items.length) % items.length;
|
|
397
|
+
render();
|
|
398
|
+
}
|
|
399
|
+
if (key === "\x1b[B" || key === "j") {
|
|
400
|
+
cursor = (cursor + 1) % items.length;
|
|
401
|
+
render();
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
stdin.on("data", onData);
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Display a table with columns.
|
|
411
|
+
* @param {string[]} headers
|
|
412
|
+
* @param {string[][]} rows
|
|
413
|
+
*/
|
|
414
|
+
export function table(headers, rows) {
|
|
415
|
+
const colWidths = headers.map((h, i) => {
|
|
416
|
+
const maxData = rows.reduce((max, row) => Math.max(max, stripAnsi(String(row[i] || "")).length), 0);
|
|
417
|
+
return Math.max(stripAnsi(h).length, maxData) + 2;
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const headerLine = headers.map((h, i) => c.bold(h.padEnd(colWidths[i]))).join(c.gray(" \u2502 "));
|
|
421
|
+
console.log(` ${headerLine}`);
|
|
422
|
+
console.log(` ${colWidths.map((w) => c.gray(BOX.h.repeat(w))).join(c.gray("\u2500\u253c\u2500"))}`);
|
|
423
|
+
|
|
424
|
+
for (const row of rows) {
|
|
425
|
+
const line = row.map((cell, i) => String(cell || "").padEnd(colWidths[i])).join(c.gray(" \u2502 "));
|
|
426
|
+
console.log(` ${line}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ── Utilities ──────────────────────────────────────────────────────────────
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Strip ANSI escape codes from a string.
|
|
434
|
+
*/
|
|
435
|
+
export function stripAnsi(str) {
|
|
436
|
+
// eslint-disable-next-line no-control-regex
|
|
437
|
+
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Truncate a string to maxLen, adding ellipsis if needed.
|
|
442
|
+
*/
|
|
443
|
+
export function truncate(str, maxLen = 40) {
|
|
444
|
+
if (str.length <= maxLen) return str;
|
|
445
|
+
return str.slice(0, maxLen - 1) + "\u2026";
|
|
446
|
+
}
|
|
@@ -1,19 +1,58 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$schema": "https://github.com/
|
|
3
|
-
"version": "0.
|
|
2
|
+
"$schema": "https://github.com/opencode-auto-agent/config-schema",
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"preset": "java",
|
|
5
5
|
"docsPath": "./Docs",
|
|
6
6
|
"tasksPath": "./PRD.md",
|
|
7
7
|
"outputPath": "./",
|
|
8
8
|
"engine": "opencode",
|
|
9
|
+
"models": {
|
|
10
|
+
"orchestrator": "google/gemini-3-pro",
|
|
11
|
+
"planner": "google/gemini-3-pro",
|
|
12
|
+
"developer": "openai/gpt-5.1-codex",
|
|
13
|
+
"qa": "anthropic/claude-sonnet-4-5",
|
|
14
|
+
"reviewer": "anthropic/claude-sonnet-4-5",
|
|
15
|
+
"docs": "anthropic/claude-sonnet-4-5"
|
|
16
|
+
},
|
|
9
17
|
"agents": {
|
|
10
|
-
"orchestrator": {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
18
|
+
"orchestrator": {
|
|
19
|
+
"enabled": true,
|
|
20
|
+
"model": "google/gemini-3-pro",
|
|
21
|
+
"mode": "primary"
|
|
22
|
+
},
|
|
23
|
+
"planner": {
|
|
24
|
+
"enabled": true,
|
|
25
|
+
"model": "google/gemini-3-pro",
|
|
26
|
+
"mode": "subagent"
|
|
27
|
+
},
|
|
28
|
+
"developer": {
|
|
29
|
+
"enabled": true,
|
|
30
|
+
"model": "openai/gpt-5.1-codex",
|
|
31
|
+
"mode": "subagent"
|
|
32
|
+
},
|
|
33
|
+
"qa": {
|
|
34
|
+
"enabled": true,
|
|
35
|
+
"model": "anthropic/claude-sonnet-4-5",
|
|
36
|
+
"mode": "subagent"
|
|
37
|
+
},
|
|
38
|
+
"reviewer": {
|
|
39
|
+
"enabled": true,
|
|
40
|
+
"model": "anthropic/claude-sonnet-4-5",
|
|
41
|
+
"mode": "subagent"
|
|
42
|
+
},
|
|
43
|
+
"docs": {
|
|
44
|
+
"enabled": true,
|
|
45
|
+
"model": "anthropic/claude-sonnet-4-5",
|
|
46
|
+
"mode": "subagent"
|
|
47
|
+
}
|
|
16
48
|
},
|
|
49
|
+
"rules": [
|
|
50
|
+
".opencode/rules/universal.md"
|
|
51
|
+
],
|
|
52
|
+
"instructions": [
|
|
53
|
+
"AGENTS.md",
|
|
54
|
+
".opencode/rules/universal.md"
|
|
55
|
+
],
|
|
17
56
|
"orchestrator": {
|
|
18
57
|
"workflowSteps": ["plan", "implement", "test", "verify"],
|
|
19
58
|
"requirePlanApproval": false
|