Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
57 changes: 45 additions & 12 deletions src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
38 changes: 38 additions & 0 deletions test/cli.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading