From b5ac5a3d679405125b6cf9962111ae9b0655e89e Mon Sep 17 00:00:00 2001 From: Nur Ad-Duja Date: Mon, 27 Jul 2026 17:23:01 +0700 Subject: [PATCH] feat(configure): add tool target picker --- README.md | 4 ++++ package.json | 2 +- src/cli.js | 57 ++++++++++++++++++++++++++++++++++++++---------- test/cli.test.js | 38 ++++++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 01a693b..e292239 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,10 @@ kenari configure Node.js 18 or newer is required. +When Claude Code and Codex are both installed, `kenari configure` asks which +tool to configure and defaults to `Both`. If only one is installed, Kenari +selects it automatically. Non-interactive use must name `claude` or `codex`. + ## Commands ```text diff --git a/package.json b/package.json index 12171d3..3412502 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kenarihq/cli", - "version": "0.3.2", + "version": "0.4.0", "description": "Official kenari CLI. Route Claude Code and Codex models per process.", "license": "MIT", "type": "module", diff --git a/src/cli.js b/src/cli.js index 30f72b2..c28045e 100644 --- a/src/cli.js +++ b/src/cli.js @@ -234,24 +234,56 @@ function printRoutingSummary(tool, roles) { } } +function detectedTools() { + return registry.filter((adapter) => { + try { + resolveBinary(adapter.id, { exclude: [process.argv[1]] }); + return true; + } catch { + return adapter.detect().installed; + } + }).map((adapter) => adapter.id); +} + +export async function chooseConfigureTools(installed, choose = pickNumber) { + if (installed.length === 1) return [...installed]; + const options = registry.map((adapter) => ({ + label: adapter.name, + tools: [adapter.id], + })); + options.push({ label: 'Both', tools: registry.map((adapter) => adapter.id) }); + const selected = await choose( + 'Configure which tool?', + options.map((option) => option.label), + options.length - 1, + ); + return options[selected].tools; +} + async function cmdConfigure(argv) { const { flags, rest } = parseFlags(argv); const requested = rest[0]; if (requested) assertTool(requested); - const tools = requested - ? [requested] - : registry.filter((adapter) => { - try { - resolveBinary(adapter.id, { exclude: [process.argv[1]] }); - return true; - } catch { - return adapter.detect().installed; - } - }).map((adapter) => adapter.id); - if (!tools.length) throw new KenariError('no supported tools installed (looked for Claude Code and Codex)'); + let tools; + if (requested) { + tools = [requested]; + } else { + if (flags.yes) { + throw new KenariError('--yes requires an explicit tool: kenari configure claude|codex'); + } + if (!isTTY()) { + throw new KenariError('non-interactive configuration requires an explicit tool'); + } + const installed = detectedTools(); + if (!installed.length) { + console.log('warning: Claude Code and Codex were not detected'); + } + tools = await chooseConfigureTools(installed); + } for (const tool of tools) await prepareMigration(tool); let config = loadConfig() || { version: 2, tools: {} }; + const summaries = []; for (const tool of tools) { const current = getToolConfig(config, tool)?.roles || null; let roles; @@ -277,8 +309,9 @@ async function cmdConfigure(argv) { version: 2, tools: { ...config.tools, [tool]: { roles } }, }; - printRoutingSummary(tool, roles); + summaries.push([tool, roles]); } + for (const [tool, roles] of summaries) printRoutingSummary(tool, roles); await withLock(() => saveConfig(config)); console.log('ok: routing configuration saved'); return 0; diff --git a/test/cli.test.js b/test/cli.test.js index 4bd2f56..c763d0a 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -112,6 +112,44 @@ test('non-interactive automation rejects partial roles', async () => { assert.match(logs(), /missing --review, --subagents/); }); +test('configure target picker defaults to both and maps every choice', async () => { + const { chooseConfigureTools } = await import('../src/cli.js'); + const expected = [ + ['claude'], + ['codex'], + ['claude', 'codex'], + ]; + for (let selection = 0; selection < expected.length; selection += 1) { + const chosen = await chooseConfigureTools(['claude', 'codex'], async ( + title, + items, + defaultIndex, + ) => { + assert.equal(title, 'Configure which tool?'); + assert.deepEqual(items, ['Claude Code', 'Codex CLI', 'Both']); + assert.equal(defaultIndex, 2); + return selection; + }); + assert.deepEqual(chosen, expected[selection]); + } +}); + +test('configure target picker skips the prompt for one detected tool', async () => { + const { chooseConfigureTools } = await import('../src/cli.js'); + let prompted = false; + const chosen = await chooseConfigureTools(['codex'], async () => { + prompted = true; + return 0; + }); + assert.deepEqual(chosen, ['codex']); + assert.equal(prompted, false); +}); + +test('non-interactive configure requires an explicit tool', async () => { + assert.equal(await run('configure', '--yes'), 1); + assert.match(logs(), /--yes requires an explicit tool/); +}); + test('fixed route validates catalog and writes namespaced config', async () => { process.env.KENARI_BASE_URL = await stubCatalog(CATALOG); process.env.KENARI_ALLOW_HTTP = '1';