opencode-auto-agent 1.2.0 → 1.3.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/package.json +3 -3
- package/src/lib/models.test.js +42 -0
- package/src/lib/ui.js +104 -49
- package/src/lib/ui.test.js +26 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-auto-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
17
|
"dev": "node bin/cli.js",
|
|
18
|
-
"test": "node --test src/**/*.test.js
|
|
19
|
-
"verify": "node bin/cli.js help"
|
|
18
|
+
"test": "node --test src/**/*.test.js",
|
|
19
|
+
"verify": "npm run test && node bin/cli.js help"
|
|
20
20
|
},
|
|
21
21
|
"keywords": [
|
|
22
22
|
"ai",
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { parseModelsOutput, findModel, isValidModel } from "./models.js";
|
|
5
|
+
|
|
6
|
+
test("parseModelsOutput parses slash-style model lines", () => {
|
|
7
|
+
const raw = [
|
|
8
|
+
"openai/gpt-5.1-codex",
|
|
9
|
+
"anthropic/claude-sonnet-4-5 Fast and reliable",
|
|
10
|
+
"google/gemini-3-pro",
|
|
11
|
+
].join("\n");
|
|
12
|
+
|
|
13
|
+
const models = parseModelsOutput(raw);
|
|
14
|
+
assert.equal(models.length, 3);
|
|
15
|
+
assert.equal(models[0].id, "openai/gpt-5.1-codex");
|
|
16
|
+
assert.equal(models[1].provider, "anthropic");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("parseModelsOutput parses table-like provider/model lines", () => {
|
|
20
|
+
const raw = [
|
|
21
|
+
"provider model",
|
|
22
|
+
"openai gpt-5.1-codex",
|
|
23
|
+
"google gemini-3-pro",
|
|
24
|
+
].join("\n");
|
|
25
|
+
|
|
26
|
+
const models = parseModelsOutput(raw);
|
|
27
|
+
assert.equal(models.length, 2);
|
|
28
|
+
assert.equal(models[0].id, "openai/gpt-5.1-codex");
|
|
29
|
+
assert.equal(models[1].id, "google/gemini-3-pro");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("findModel falls back from exact to model-name match", () => {
|
|
33
|
+
const models = [
|
|
34
|
+
{ id: "openrouter/gpt-5.1-codex", provider: "openrouter", model: "gpt-5.1-codex" },
|
|
35
|
+
{ id: "google/gemini-3-pro", provider: "google", model: "gemini-3-pro" },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const found = findModel(models, "openai/gpt-5.1-codex");
|
|
39
|
+
assert.ok(found);
|
|
40
|
+
assert.equal(found.id, "openrouter/gpt-5.1-codex");
|
|
41
|
+
assert.equal(isValidModel(models, "openai/gpt-5.1-codex"), true);
|
|
42
|
+
});
|
package/src/lib/ui.js
CHANGED
|
@@ -240,36 +240,33 @@ export async function input(question, defaultValue = "") {
|
|
|
240
240
|
export async function select(title, items) {
|
|
241
241
|
if (!items.length) throw new Error("select: no items provided");
|
|
242
242
|
|
|
243
|
+
if (!supportsInteractiveMenu()) {
|
|
244
|
+
return selectFallback(title, items);
|
|
245
|
+
}
|
|
246
|
+
|
|
243
247
|
return new Promise((resolve) => {
|
|
244
248
|
let cursor = 0;
|
|
249
|
+
const totalLines = items.length + 1;
|
|
250
|
+
let hasRendered = false;
|
|
245
251
|
|
|
246
252
|
const render = () => {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
stdout.write(`\x1b[${items.length + 1}A`);
|
|
253
|
+
if (hasRendered) {
|
|
254
|
+
stdout.write(`\x1b[${totalLines}A`);
|
|
250
255
|
}
|
|
251
256
|
|
|
252
|
-
stdout.write(
|
|
257
|
+
stdout.write(`\x1b[2K ${c.cyan("?")} ${c.bold(title)}\n`);
|
|
253
258
|
for (let i = 0; i < items.length; i++) {
|
|
254
259
|
const item = items[i];
|
|
255
|
-
const
|
|
256
|
-
const pointer =
|
|
257
|
-
const label =
|
|
260
|
+
const isSelected = i === cursor;
|
|
261
|
+
const pointer = isSelected ? c.cyan("\u25B6") : " ";
|
|
262
|
+
const label = isSelected ? c.cyan(item.label) : c.white(item.label);
|
|
258
263
|
const hint = item.hint ? c.gray(` ${item.hint}`) : "";
|
|
259
|
-
stdout.write(
|
|
264
|
+
stdout.write(`\x1b[2K ${pointer} ${label}${hint}\n`);
|
|
260
265
|
}
|
|
266
|
+
hasRendered = true;
|
|
261
267
|
};
|
|
262
268
|
|
|
263
|
-
|
|
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
|
-
}
|
|
269
|
+
render();
|
|
273
270
|
|
|
274
271
|
stdin.setRawMode(true);
|
|
275
272
|
stdin.resume();
|
|
@@ -288,13 +285,12 @@ export async function select(title, items) {
|
|
|
288
285
|
stdin.setRawMode(false);
|
|
289
286
|
stdin.removeListener("data", onData);
|
|
290
287
|
stdin.pause();
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
for (let i = 0; i < items.length; i++) {
|
|
295
|
-
stdout.write(`\x1b[2K\n`); // clear each line
|
|
288
|
+
stdout.write(`\x1b[${totalLines}A`);
|
|
289
|
+
for (let i = 0; i < totalLines; i++) {
|
|
290
|
+
stdout.write("\x1b[2K\n");
|
|
296
291
|
}
|
|
297
|
-
stdout.write(`\x1b[${
|
|
292
|
+
stdout.write(`\x1b[${totalLines}A`);
|
|
293
|
+
stdout.write(`\x1b[2K ${c.green("\u2713")} ${c.bold(title)} ${c.cyan(items[cursor].label)}\n`);
|
|
298
294
|
resolve(items[cursor]);
|
|
299
295
|
return;
|
|
300
296
|
}
|
|
@@ -325,15 +321,22 @@ export async function select(title, items) {
|
|
|
325
321
|
export async function multiSelect(title, items) {
|
|
326
322
|
if (!items.length) throw new Error("multiSelect: no items provided");
|
|
327
323
|
|
|
324
|
+
if (!supportsInteractiveMenu()) {
|
|
325
|
+
return multiSelectFallback(title, items);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
328
|
return new Promise((resolve) => {
|
|
329
329
|
let cursor = 0;
|
|
330
|
-
const selected =
|
|
331
|
-
|
|
332
|
-
|
|
330
|
+
const selected = buildSelectedIndexSet(items);
|
|
331
|
+
const totalLines = items.length + 2;
|
|
332
|
+
let hasRendered = false;
|
|
333
333
|
|
|
334
334
|
const render = () => {
|
|
335
|
-
|
|
336
|
-
|
|
335
|
+
if (hasRendered) {
|
|
336
|
+
stdout.write(`\x1b[${totalLines}A`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
stdout.write(`\x1b[2K ${c.cyan("?")} ${c.bold(title)} ${c.gray("(space to toggle, enter to confirm)")}\n`);
|
|
337
340
|
for (let i = 0; i < items.length; i++) {
|
|
338
341
|
const item = items[i];
|
|
339
342
|
const isCursor = i === cursor;
|
|
@@ -345,21 +348,10 @@ export async function multiSelect(title, items) {
|
|
|
345
348
|
stdout.write(`\x1b[2K ${pointer} ${check} ${label}${hint}\n`);
|
|
346
349
|
}
|
|
347
350
|
stdout.write("\x1b[2K\n");
|
|
351
|
+
hasRendered = true;
|
|
348
352
|
};
|
|
349
353
|
|
|
350
|
-
|
|
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");
|
|
354
|
+
render();
|
|
363
355
|
|
|
364
356
|
stdin.setRawMode(true);
|
|
365
357
|
stdin.resume();
|
|
@@ -376,14 +368,14 @@ export async function multiSelect(title, items) {
|
|
|
376
368
|
stdin.setRawMode(false);
|
|
377
369
|
stdin.removeListener("data", onData);
|
|
378
370
|
stdin.pause();
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
stdout.write(`\x1b[${
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
stdout.write(`\x1b[2K\n`);
|
|
371
|
+
const orderedIndexes = [...selected].sort((a, b) => a - b);
|
|
372
|
+
const result = orderedIndexes.map((i) => items[i]);
|
|
373
|
+
stdout.write(`\x1b[${totalLines}A`);
|
|
374
|
+
for (let i = 0; i < totalLines; i++) {
|
|
375
|
+
stdout.write("\x1b[2K\n");
|
|
385
376
|
}
|
|
386
|
-
stdout.write(`\x1b[${
|
|
377
|
+
stdout.write(`\x1b[${totalLines}A`);
|
|
378
|
+
stdout.write(`\x1b[2K ${c.green("\u2713")} ${c.bold(title)} ${c.cyan(result.map((r) => r.label).join(", "))}\n`);
|
|
387
379
|
resolve(result);
|
|
388
380
|
return;
|
|
389
381
|
}
|
|
@@ -444,3 +436,66 @@ export function truncate(str, maxLen = 40) {
|
|
|
444
436
|
if (str.length <= maxLen) return str;
|
|
445
437
|
return str.slice(0, maxLen - 1) + "\u2026";
|
|
446
438
|
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Build selected index set preserving original item indexes.
|
|
442
|
+
* @param {Array<{selected?: boolean}>} items
|
|
443
|
+
* @returns {Set<number>}
|
|
444
|
+
*/
|
|
445
|
+
export function buildSelectedIndexSet(items) {
|
|
446
|
+
const indexes = new Set();
|
|
447
|
+
for (let i = 0; i < items.length; i++) {
|
|
448
|
+
if (items[i]?.selected) indexes.add(i);
|
|
449
|
+
}
|
|
450
|
+
return indexes;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function supportsInteractiveMenu() {
|
|
454
|
+
return Boolean(stdin.isTTY && stdout.isTTY && typeof stdin.setRawMode === "function");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function selectFallback(title, items) {
|
|
458
|
+
console.log(` ${c.cyan("?")} ${c.bold(title)}`);
|
|
459
|
+
for (let i = 0; i < items.length; i++) {
|
|
460
|
+
const item = items[i];
|
|
461
|
+
const hint = item.hint ? ` ${c.gray(item.hint)}` : "";
|
|
462
|
+
console.log(` ${String(i + 1).padStart(2, " ")}) ${item.label}${hint}`);
|
|
463
|
+
}
|
|
464
|
+
const answer = await input("Choose an option number", "1");
|
|
465
|
+
const idx = clampIndex(parseInt(answer, 10) - 1, items.length);
|
|
466
|
+
console.log(` ${c.green("\u2713")} ${c.bold(title)} ${c.cyan(items[idx].label)}`);
|
|
467
|
+
return items[idx];
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function multiSelectFallback(title, items) {
|
|
471
|
+
console.log(` ${c.cyan("?")} ${c.bold(title)}`);
|
|
472
|
+
for (let i = 0; i < items.length; i++) {
|
|
473
|
+
const item = items[i];
|
|
474
|
+
const mark = item.selected ? "*" : " ";
|
|
475
|
+
const hint = item.hint ? ` ${c.gray(item.hint)}` : "";
|
|
476
|
+
console.log(` ${String(i + 1).padStart(2, " ")}) [${mark}] ${item.label}${hint}`);
|
|
477
|
+
}
|
|
478
|
+
const answer = await input("Choose option numbers (comma-separated)", "1");
|
|
479
|
+
const indexes = parseMultiNumberInput(answer, items.length);
|
|
480
|
+
const selected = indexes.map((i) => items[i]);
|
|
481
|
+
console.log(` ${c.green("\u2713")} ${c.bold(title)} ${c.cyan(selected.map((it) => it.label).join(", "))}`);
|
|
482
|
+
return selected;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function clampIndex(idx, length) {
|
|
486
|
+
if (Number.isNaN(idx) || idx < 0) return 0;
|
|
487
|
+
if (idx >= length) return length - 1;
|
|
488
|
+
return idx;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function parseMultiNumberInput(text, length) {
|
|
492
|
+
const indexes = new Set();
|
|
493
|
+
for (const rawPart of text.split(",")) {
|
|
494
|
+
const n = parseInt(rawPart.trim(), 10);
|
|
495
|
+
if (Number.isNaN(n)) continue;
|
|
496
|
+
const idx = n - 1;
|
|
497
|
+
if (idx >= 0 && idx < length) indexes.add(idx);
|
|
498
|
+
}
|
|
499
|
+
if (indexes.size === 0) indexes.add(0);
|
|
500
|
+
return [...indexes].sort((a, b) => a - b);
|
|
501
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { buildSelectedIndexSet } from "./ui.js";
|
|
5
|
+
|
|
6
|
+
test("buildSelectedIndexSet keeps original indexes", () => {
|
|
7
|
+
const items = [
|
|
8
|
+
{ label: "a", selected: false },
|
|
9
|
+
{ label: "b", selected: true },
|
|
10
|
+
{ label: "c", selected: false },
|
|
11
|
+
{ label: "d", selected: true },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const selected = buildSelectedIndexSet(items);
|
|
15
|
+
assert.deepEqual([...selected], [1, 3]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("buildSelectedIndexSet handles empty selection", () => {
|
|
19
|
+
const items = [
|
|
20
|
+
{ label: "a" },
|
|
21
|
+
{ label: "b", selected: false },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const selected = buildSelectedIndexSet(items);
|
|
25
|
+
assert.equal(selected.size, 0);
|
|
26
|
+
});
|