From 28f79e6e228a11f3857216dd022f1a971d7983d6 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 22 Jul 2026 13:49:31 -0400 Subject: [PATCH] =?UTF-8?q?feat(#2505):=20Phase=205=20=E2=80=94=20Kimi=20v?= =?UTF-8?q?ariant=20install-time=20disambiguation=20(#2535)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(#2513): Phase 5 — Kimi variant install-time disambiguation (descriptions + mismatch warning) * fix #2513: use helpers.cleanup (Windows-EBUSY retry budget) not raw fs.rmSync * fix #2513: spawnSync captures stderr (execFileSync drops it on exit-0) * docs(changeset): backfill PR #2535 for Phase 5 (#2513) --- .../2505-5-kimi-variant-disambiguation.md | 6 ++ bin/install.js | 56 +++++++++++ tests/kimi-variant-disambiguation.test.cjs | 94 +++++++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 .changeset/2505-5-kimi-variant-disambiguation.md create mode 100644 tests/kimi-variant-disambiguation.test.cjs diff --git a/.changeset/2505-5-kimi-variant-disambiguation.md b/.changeset/2505-5-kimi-variant-disambiguation.md new file mode 100644 index 000000000..a42a5eaf4 --- /dev/null +++ b/.changeset/2505-5-kimi-variant-disambiguation.md @@ -0,0 +1,6 @@ +--- +type: Added +pr: 2535 +--- + +**The installer now distinguishes Kimi CLI (Python) from Kimi Code (Node) at install time** — running `--kimi` or `--kimi-code` prints a one-line description of each product, and if the selected variant doesn't match the detected `~/.kimi/config.toml` vs `~/.kimi-code/config.toml`, the installer warns with the correct `--kimi-code` / `--kimi` re-run command. Catches the "ran `--kimi --global` but actually on Kimi Code" mistake that produced inert YAMLs and empty agent-skills before the Phase 1 descriptor split. (#2513) diff --git a/bin/install.js b/bin/install.js index a83cfba7b..791942c50 100755 --- a/bin/install.js +++ b/bin/install.js @@ -658,6 +658,62 @@ function selectRuntimesFromArgs(runtimeArgs) { // Runtime selection - can be set by flags or interactive prompt let selectedRuntimes = selectRuntimesFromArgs(args); +// #2505 Phase 5: Kimi variant disambiguation (#2513). Kimi CLI (Python, ~/.kimi/) +// and Kimi Code (Node, ~/.kimi-code/) are two distinct Moonshot products that +// share the "kimi" brand. Probe for each product's config.toml and warn when +// the selected runtime doesn't match the detected install — catches the common +// "ran --kimi --global but actually on Kimi Code" mistake that produced inert +// YAMLs and empty agent-skills before the Phase 1 descriptor split. +function disambiguateKimiVariant(runtimes) { + const home = os.homedir(); + const hasKimiCli = fs.existsSync(path.join(home, '.kimi', 'config.toml')); + const hasKimiCode = fs.existsSync(path.join(home, '.kimi-code', 'config.toml')); + const notices = []; + if (runtimes.includes('kimi') && hasKimiCode && !hasKimiCli) { + notices.push({ + kind: 'wrong-variant', + selected: 'kimi', + detected: 'kimi-code', + message: `Detected ~/.kimi-code/config.toml (Kimi Code, Node CLI) but not ~/.kimi/config.toml (Kimi CLI, Python). You selected --kimi but appear to be on Kimi Code. Re-run with --kimi-code for a working install. (Kimi CLI = Python kimi-cli with named subagents; Kimi Code = Node CLI with coder/explore/plan built-ins only.)`, + }); + } + if (runtimes.includes('kimi-code') && hasKimiCli && !hasKimiCode) { + notices.push({ + kind: 'wrong-variant', + selected: 'kimi-code', + detected: 'kimi', + message: `Detected ~/.kimi/config.toml (Kimi CLI, Python) but not ~/.kimi-code/config.toml (Kimi Code, Node CLI). You selected --kimi-code but appear to be on Kimi CLI. Re-run with --kimi for a working install.`, + }); + } + // Distinct-entry descriptions when either Kimi variant is selected. + if (runtimes.includes('kimi')) { + notices.push({ + kind: 'description', + runtime: 'kimi', + message: 'Kimi CLI (Python kimi-cli): named subagents via YAML, config at ~/.kimi/, hooks via ~/.kimi/config.toml [[hooks]].', + }); + } + if (runtimes.includes('kimi-code')) { + notices.push({ + kind: 'description', + runtime: 'kimi-code', + message: 'Kimi Code (Node CLI): three built-in subagents (coder/explore/plan), Agent Skills at ~/.kimi-code/skills/, config at ~/.kimi-code/.', + }); + } + return notices; +} + +if (selectedRuntimes.includes('kimi') || selectedRuntimes.includes('kimi-code')) { + const kimiNotices = disambiguateKimiVariant(selectedRuntimes); + for (const notice of kimiNotices) { + if (notice.kind === 'wrong-variant') { + console.error(`${yellow}⚠ Kimi variant mismatch (${notice.selected} → ${notice.detected}).${reset} ${notice.message}`); + } else if (notice.kind === 'description') { + console.log(`${dim} ${notice.runtime}: ${notice.message}${reset}`); + } + } +} + // #1928: Google sunset Gemini CLI on 2026-06-18; Antigravity CLI is its // official successor. `--gemini` is no longer a valid runtime selector — // selectRuntimesFromArgs above no longer recognizes it, so it never lands in diff --git a/tests/kimi-variant-disambiguation.test.cjs b/tests/kimi-variant-disambiguation.test.cjs new file mode 100644 index 000000000..6b00290a4 --- /dev/null +++ b/tests/kimi-variant-disambiguation.test.cjs @@ -0,0 +1,94 @@ +// allow-test-rule: behavioral-subprocess-test — Phase 5 kimi-variant +// disambiguation is verified via install.js subprocess output capture, since +// the disambiguateKimiVariant function is inline in bin/install.js (not +// exported). The test sets a disposable HOME, creates the probe config files, +// and asserts on the printed notices. +process.env.GSD_TEST_MODE = '1'; + +const { test, describe, before, after } = require('node:test'); +const assert = require('node:assert/strict'); +const { spawnSync } = require('node:child_process'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { cleanup } = require('./helpers.cjs'); + +const INSTALL_JS = path.join(__dirname, '..', 'bin', 'install.js'); + +function makeDisposableHome() { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-kimi-disambig-')); + return { + dir, + cleanup() { cleanup(dir); }, + }; +} + +function runInstall(args, home) { + // spawnSync captures stdout AND stderr separately regardless of exit code + // (execFileSync drops stderr on success, which hid the console.error warnings). + const r = spawnSync('node', [INSTALL_JS, ...args], { + env: { ...process.env, HOME: home, GSD_TEST_MODE: '1' }, + encoding: 'utf8', + timeout: 15000, + }); + return { stdout: r.stdout || '', stderr: r.stderr || '', exit: r.status }; +} + +describe('Kimi variant disambiguation (#2505 Phase 5 / #2513)', () => { + let home; + before(() => { home = makeDisposableHome(); }); + after(() => home.cleanup()); + + test('--kimi prints the Kimi CLI (Python) description', () => { + const r = runInstall(['--kimi', '--help'], home.dir); + const combined = r.stdout + r.stderr; + assert.match(combined, /Kimi CLI \(Python kimi-cli\).*named subagents.*~\/\.kimi\//); + }); + + test('--kimi-code prints the Kimi Code (Node CLI) description', () => { + const r = runInstall(['--kimi-code', '--help'], home.dir); + const combined = r.stdout + r.stderr; + assert.match(combined, /Kimi Code \(Node CLI\).*coder\/explore\/plan.*~\/\.kimi-code\//); + }); + + test('--kimi warns when only ~/.kimi-code/config.toml exists', () => { + const h = makeDisposableHome(); + try { + fs.mkdirSync(path.join(h.dir, '.kimi-code'), { recursive: true }); + fs.writeFileSync(path.join(h.dir, '.kimi-code', 'config.toml'), '# kimi-code'); + const r = runInstall(['--kimi', '--help'], h.dir); + const combined = r.stdout + r.stderr; + assert.match(combined, /variant mismatch/i); + assert.match(combined, /Detected ~\/\.kimi-code\/config\.toml/); + assert.match(combined, /Re-run with --kimi-code/); + } finally { + h.cleanup(); + } + }); + + test('--kimi-code warns when only ~/.kimi/config.toml exists', () => { + const h = makeDisposableHome(); + try { + fs.mkdirSync(path.join(h.dir, '.kimi'), { recursive: true }); + fs.writeFileSync(path.join(h.dir, '.kimi', 'config.toml'), '# kimi-cli'); + const r = runInstall(['--kimi-code', '--help'], h.dir); + const combined = r.stdout + r.stderr; + assert.match(combined, /variant mismatch/i); + assert.match(combined, /Detected ~\/\.kimi\/config\.toml/); + assert.match(combined, /Re-run with --kimi/); + } finally { + h.cleanup(); + } + }); + + test('no warning when neither config exists (fresh install)', () => { + const h = makeDisposableHome(); + try { + const r = runInstall(['--kimi', '--help'], h.dir); + const combined = r.stdout + r.stderr; + assert.doesNotMatch(combined, /variant mismatch/i); + } finally { + h.cleanup(); + } + }); +});