From 10366ec7b7aa0461a5c8f105d04d85d0869c75a9 Mon Sep 17 00:00:00 2001 From: 7Sageer <7sageer@djwcb.cn> Date: Thu, 4 Jun 2026 16:55:27 +0800 Subject: [PATCH 1/4] docs: mention Windows Git Bash requirement --- README.md | 2 ++ README.zh-CN.md | 2 ++ apps/kimi-code/README.md | 2 ++ docs/en/guides/getting-started.md | 2 ++ docs/zh/guides/getting-started.md | 2 ++ 5 files changed, 10 insertions(+) diff --git a/README.md b/README.md index 501bf81994..7902de1314 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash irm https://code.kimi.com/kimi-code/install.ps1 | iex ``` +> On Windows, install [Git for Windows](https://gitforwindows.org/) before first launch because Kimi Code CLI uses the bundled Git Bash as its shell environment. If Git Bash is installed in a custom location, set `KIMI_SHELL_PATH` to the absolute path of `bash.exe`. + Then, run it with a new shell session: ```sh diff --git a/README.zh-CN.md b/README.zh-CN.md index 8e9146cb00..5a8714b058 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -28,6 +28,8 @@ curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash irm https://code.kimi.com/kimi-code/install.ps1 | iex ``` +> Windows 用户首次启动前还需要安装 [Git for Windows](https://gitforwindows.org/),Kimi Code CLI 会使用其中的 Git Bash 作为 Shell 环境。如果 Git Bash 安装在非标准路径,请把 `KIMI_SHELL_PATH` 设为 `bash.exe` 的绝对路径。 + 随后在新的终端会话中运行: ```sh diff --git a/apps/kimi-code/README.md b/apps/kimi-code/README.md index 79cd3ad910..7c92bc3a53 100644 --- a/apps/kimi-code/README.md +++ b/apps/kimi-code/README.md @@ -24,6 +24,8 @@ curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash irm https://code.kimi.com/kimi-code/install.ps1 | iex ``` +> On Windows, install [Git for Windows](https://gitforwindows.org/) before first launch because Kimi Code CLI uses the bundled Git Bash as its shell environment. If Git Bash is installed in a custom location, set `KIMI_SHELL_PATH` to the absolute path of `bash.exe`. + Then run it with a new Terminal session: ```sh diff --git a/docs/en/guides/getting-started.md b/docs/en/guides/getting-started.md index 13a8130711..fc19eee27b 100644 --- a/docs/en/guides/getting-started.md +++ b/docs/en/guides/getting-started.md @@ -30,6 +30,8 @@ curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash irm https://code.kimi.com/kimi-code/install.ps1 | iex ``` +> On Windows, install [Git for Windows](https://gitforwindows.org/) before first launch. Kimi Code CLI uses the bundled Git Bash as its shell environment; if Git Bash is installed in a custom location, set `KIMI_SHELL_PATH` to the absolute path of `bash.exe`. + The script automatically downloads the latest release, verifies the checksum, and places the `kimi` executable on your `PATH`. ### npm installation diff --git a/docs/zh/guides/getting-started.md b/docs/zh/guides/getting-started.md index 1865ca2e7c..3324543f59 100644 --- a/docs/zh/guides/getting-started.md +++ b/docs/zh/guides/getting-started.md @@ -30,6 +30,8 @@ curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash irm https://code.kimi.com/kimi-code/install.ps1 | iex ``` +> Windows 用户首次启动前还需要安装 [Git for Windows](https://gitforwindows.org/),Kimi Code CLI 会使用其中的 Git Bash 作为 Shell 环境。如果 Git Bash 安装在非标准路径,请把 `KIMI_SHELL_PATH` 设为 `bash.exe` 的绝对路径。 + 脚本会自动下载最新版本、校验 checksum,并把 `kimi` 可执行文件放到你的 `PATH` 中。 ### npm 安装 From 2fa7fcf4776da52f853cd52594bb7c342db46364 Mon Sep 17 00:00:00 2001 From: 7Sageer <7sageer@djwcb.cn> Date: Thu, 4 Jun 2026 17:00:26 +0800 Subject: [PATCH 2/4] docs: add Windows Git Bash changeset --- .changeset/windows-git-bash-docs.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/windows-git-bash-docs.md diff --git a/.changeset/windows-git-bash-docs.md b/.changeset/windows-git-bash-docs.md new file mode 100644 index 0000000000..8d7e00d7e7 --- /dev/null +++ b/.changeset/windows-git-bash-docs.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Document the Git Bash prerequisite for Windows installs. From 961f200aece6b78723d928ccbf89c50147d9d153 Mon Sep 17 00:00:00 2001 From: 7Sageer <7sageer@djwcb.cn> Date: Thu, 4 Jun 2026 19:57:31 +0800 Subject: [PATCH 3/4] fix: fail early when Git Bash is missing --- .changeset/windows-git-bash-preflight.md | 7 ++++ apps/kimi-code/src/cli/run-prompt.ts | 1 + apps/kimi-code/src/cli/run-shell.ts | 32 +++++++++--------- apps/kimi-code/test/cli/run-prompt.test.ts | 24 +++++++++++++ apps/kimi-code/test/cli/run-shell.test.ts | 39 ++++++++++++++++++++++ packages/agent-core/src/rpc/core-api.ts | 1 + packages/agent-core/src/rpc/core-impl.ts | 26 ++++++++++----- packages/node-sdk/src/kimi-harness.ts | 4 +++ packages/node-sdk/src/rpc.ts | 5 +++ 9 files changed, 115 insertions(+), 24 deletions(-) create mode 100644 .changeset/windows-git-bash-preflight.md diff --git a/.changeset/windows-git-bash-preflight.md b/.changeset/windows-git-bash-preflight.md new file mode 100644 index 0000000000..f58b78d14f --- /dev/null +++ b/.changeset/windows-git-bash-preflight.md @@ -0,0 +1,7 @@ +--- +"@moonshot-ai/agent-core": patch +"@moonshot-ai/kimi-code-sdk": patch +"@moonshot-ai/kimi-code": patch +--- + +Fail early when Git Bash is missing on Windows before starting CLI sessions. diff --git a/apps/kimi-code/src/cli/run-prompt.ts b/apps/kimi-code/src/cli/run-prompt.ts index c01b0015b2..56bf890b2e 100644 --- a/apps/kimi-code/src/cli/run-prompt.ts +++ b/apps/kimi-code/src/cli/run-prompt.ts @@ -109,6 +109,7 @@ export async function runPrompt( removeTerminationCleanup = installPromptTerminationCleanup(promptProcess, cleanupPromptRun); try { + await harness.checkRuntimeEnvironment(); await harness.ensureConfigFile(); const config = await harness.getConfig(); const { session, resumed, restorePermission, telemetryModel, goalModel } = diff --git a/apps/kimi-code/src/cli/run-shell.ts b/apps/kimi-code/src/cli/run-shell.ts index 4ec4330a57..3633f41cbd 100644 --- a/apps/kimi-code/src/cli/run-shell.ts +++ b/apps/kimi-code/src/cli/run-shell.ts @@ -34,21 +34,6 @@ export async function runShell( runOptions: { readonly migrateOnly?: boolean } = {}, ): Promise { const startedAt = Date.now(); - const configStartedAt = startedAt; - let tuiConfig: TuiConfig; - let configWarning: string | undefined; - try { - tuiConfig = await loadTuiConfig(); - } catch (error) { - if (!(error instanceof TuiConfigParseError)) throw error; - tuiConfig = error.fallback; - configWarning = error.message; - } - - // Resolve `theme = "auto"` against the live terminal once, before pi-tui - // grabs stdin. Explicit `dark` / `light` skip detection. - const resolvedTheme = tuiConfig.theme === 'auto' ? await detectTerminalTheme() : tuiConfig.theme; - const workDir = process.cwd(); const telemetryBootstrap = createCliTelemetryBootstrap(); const telemetryClient: TelemetryClient = { @@ -78,6 +63,23 @@ export async function runShell( platform: `${process.platform}/${process.arch}`, workDir, }); + await harness.checkRuntimeEnvironment(); + + const configStartedAt = Date.now(); + let tuiConfig: TuiConfig; + let configWarning: string | undefined; + try { + tuiConfig = await loadTuiConfig(); + } catch (error) { + if (!(error instanceof TuiConfigParseError)) throw error; + tuiConfig = error.fallback; + configWarning = error.message; + } + + // Resolve `theme = "auto"` against the live terminal once, before pi-tui + // grabs stdin. Explicit `dark` / `light` skip detection. + const resolvedTheme = tuiConfig.theme === 'auto' ? await detectTerminalTheme() : tuiConfig.theme; + await harness.ensureConfigFile(); const migrationPlan = await detectPendingMigration({ sourceHome: join(homedir(), '.kimi'), diff --git a/apps/kimi-code/test/cli/run-prompt.test.ts b/apps/kimi-code/test/cli/run-prompt.test.ts index 123c9920aa..0b426e4677 100644 --- a/apps/kimi-code/test/cli/run-prompt.test.ts +++ b/apps/kimi-code/test/cli/run-prompt.test.ts @@ -46,6 +46,7 @@ const mocks = vi.hoisted(() => { agentEvent, mainEvent, kimiHarnessConstructor: vi.fn(), + harnessCheckRuntimeEnvironment: vi.fn(async () => undefined), harnessEnsureConfigFile: vi.fn(), harnessGetConfig: vi.fn( async (): Promise<{ providers: {}; defaultModel?: string; telemetry: boolean }> => ({ @@ -89,6 +90,7 @@ vi.mock('@moonshot-ai/kimi-code-sdk', async (importOriginal) => { return { homeDir, auth: { getCachedAccessToken: mocks.harnessGetCachedAccessToken }, + checkRuntimeEnvironment: mocks.harnessCheckRuntimeEnvironment, ensureConfigFile: mocks.harnessEnsureConfigFile, getConfig: mocks.harnessGetConfig, getExperimentalFlags: mocks.harnessGetExperimentalFlags, @@ -187,6 +189,7 @@ describe('runPrompt', () => { mocks.resolveKimiHome.mockImplementation( (homeDir?: string) => homeDir ?? '/tmp/kimi-code-test-home', ); + mocks.harnessCheckRuntimeEnvironment.mockResolvedValue(undefined); mocks.harnessCreatesDeviceIdOnConstruction = false; }); @@ -199,6 +202,10 @@ describe('runPrompt', () => { expect(mocks.kimiHarnessConstructor).toHaveBeenCalledWith( expect.objectContaining({ skillDirs: ['/skills'], uiMode: 'print' }), ); + expect(mocks.harnessCheckRuntimeEnvironment).toHaveBeenCalledOnce(); + expect(mocks.harnessCheckRuntimeEnvironment.mock.invocationCallOrder[0]).toBeLessThan( + mocks.harnessEnsureConfigFile.mock.invocationCallOrder[0]!, + ); expect(mocks.harnessCreateSession).toHaveBeenCalledWith({ workDir: process.cwd(), model: 'k2', @@ -214,6 +221,23 @@ describe('runPrompt', () => { expect(mocks.harnessClose).toHaveBeenCalled(); }); + it('stops prompt startup when runtime environment check fails', async () => { + const stdout = writer(); + const stderr = writer(); + mocks.harnessCheckRuntimeEnvironment.mockRejectedValueOnce(new Error('Git Bash missing')); + + await expect(runPrompt(opts(), '1.2.3-test', { stdout, stderr })).rejects.toThrow( + 'Git Bash missing', + ); + + expect(mocks.harnessCheckRuntimeEnvironment).toHaveBeenCalledOnce(); + expect(mocks.harnessEnsureConfigFile).not.toHaveBeenCalled(); + expect(mocks.harnessGetConfig).not.toHaveBeenCalled(); + expect(mocks.harnessCreateSession).not.toHaveBeenCalled(); + expect(mocks.session.prompt).not.toHaveBeenCalled(); + expect(mocks.harnessClose).toHaveBeenCalledOnce(); + }); + it('uses the CLI model override when creating a fresh prompt session', async () => { await runPrompt(opts({ model: 'kimi-code/k2.5' }), '1.2.3-test', { stdout: { write: vi.fn(() => true) }, diff --git a/apps/kimi-code/test/cli/run-shell.test.ts b/apps/kimi-code/test/cli/run-shell.test.ts index c55a5e5904..027aeb85ff 100644 --- a/apps/kimi-code/test/cli/run-shell.test.ts +++ b/apps/kimi-code/test/cli/run-shell.test.ts @@ -31,6 +31,7 @@ const mocks = vi.hoisted(() => { loadTuiConfig: vi.fn(), detectTerminalTheme: vi.fn(), kimiHarnessConstructor: vi.fn(), + harnessCheckRuntimeEnvironment: vi.fn(async () => undefined), harnessEnsureConfigFile: vi.fn(), harnessGetConfig: vi.fn(async () => ({ providers: {}, @@ -81,6 +82,7 @@ vi.mock('@moonshot-ai/kimi-code-sdk', async (importOriginal) => { getCachedAccessToken: mocks.harnessGetCachedAccessToken, }, ensureConfigFile: mocks.harnessEnsureConfigFile, + checkRuntimeEnvironment: mocks.harnessCheckRuntimeEnvironment, getConfig: mocks.harnessGetConfig, close: mocks.harnessClose, track: mocks.harnessTrack, @@ -149,6 +151,7 @@ describe('runShell', () => { defaultModel: 'k2', telemetry: true, }); + mocks.harnessCheckRuntimeEnvironment.mockResolvedValue(undefined); mocks.tuiGetStartupMcpMs.mockResolvedValue(0); mocks.tuiGetCurrentSessionId.mockReturnValue(''); mocks.tuiHasSessionContent.mockReturnValue(false); @@ -191,6 +194,10 @@ describe('runShell', () => { }), }), ); + expect(mocks.harnessCheckRuntimeEnvironment).toHaveBeenCalledOnce(); + expect(mocks.harnessCheckRuntimeEnvironment.mock.invocationCallOrder[0]).toBeLessThan( + mocks.harnessEnsureConfigFile.mock.invocationCallOrder[0]!, + ); expect(mocks.harnessEnsureConfigFile).toHaveBeenCalledOnce(); expect(mocks.harnessEnsureConfigFile.mock.invocationCallOrder[0]).toBeLessThan( mocks.harnessGetConfig.mock.invocationCallOrder[0]!, @@ -244,6 +251,38 @@ describe('runShell', () => { }); }); + it('stops startup when runtime environment check fails', async () => { + mocks.loadTuiConfig.mockResolvedValue({ + theme: 'dark', + editorCommand: null, + notifications: { enabled: true, condition: 'unfocused' }, + }); + mocks.harnessCheckRuntimeEnvironment.mockRejectedValueOnce(new Error('Git Bash missing')); + + await expect( + runShell( + { + session: undefined, + continue: false, + yolo: false, + auto: false, + plan: false, + model: undefined, + outputFormat: undefined, + prompt: undefined, + skillsDirs: [], + }, + '1.2.3-test', + ), + ).rejects.toThrow('Git Bash missing'); + + expect(mocks.harnessCheckRuntimeEnvironment).toHaveBeenCalledOnce(); + expect(mocks.harnessEnsureConfigFile).not.toHaveBeenCalled(); + expect(mocks.harnessGetConfig).not.toHaveBeenCalled(); + expect(mocks.kimiTuiConstructor).not.toHaveBeenCalled(); + expect(mocks.tuiStart).not.toHaveBeenCalled(); + }); + it('tracks first launch when device id creation reports first launch', async () => { mocks.loadTuiConfig.mockResolvedValue({ theme: 'dark', diff --git a/packages/agent-core/src/rpc/core-api.ts b/packages/agent-core/src/rpc/core-api.ts index 76c4bbb89a..0f5f076624 100644 --- a/packages/agent-core/src/rpc/core-api.ts +++ b/packages/agent-core/src/rpc/core-api.ts @@ -357,6 +357,7 @@ type SessionAPIWithId = WithSessionId; export interface CoreAPI extends SessionAPIWithId { getCoreInfo: (payload: EmptyPayload) => CoreInfo; + checkRuntimeEnvironment: (payload: EmptyPayload) => void; getExperimentalFlags: (payload: EmptyPayload) => ExperimentalFlagMap; getKimiConfig: (payload: GetKimiConfigPayload) => KimiConfig; setKimiConfig: (payload: SetKimiConfigPayload) => KimiConfig; diff --git a/packages/agent-core/src/rpc/core-impl.ts b/packages/agent-core/src/rpc/core-impl.ts index 1ffd92b1d8..edd484fa55 100644 --- a/packages/agent-core/src/rpc/core-impl.ts +++ b/packages/agent-core/src/rpc/core-impl.ts @@ -122,7 +122,7 @@ export class KimiCore implements PromisableMethods { readonly sessions = new Map(); readonly telemetry: TelemetryClient; - private kaos: Promise; + private kaos: Promise | undefined; private runtime: ToolServices | undefined; private config: KimiConfig; private readonly runtimeOverride: ToolServices | undefined; @@ -146,12 +146,6 @@ export class KimiCore implements PromisableMethods { homeDir: this.homeDir, configPath: options.configPath, }); - this.kaos = LocalKaos.create().catch((error: unknown) => { - if (error instanceof KaosShellNotFoundError) { - throw new KimiError(ErrorCodes.SHELL_GIT_BASH_NOT_FOUND, error.message); - } - throw error; - }); this.runtimeOverride = options.runtime; this.runtime = options.runtime; this.kimiRequestHeaders = options.kimiRequestHeaders; @@ -175,6 +169,10 @@ export class KimiCore implements PromisableMethods { this.sdk = rpcClient(this); } + async checkRuntimeEnvironment(_: EmptyPayload): Promise { + await this.getKaos(); + } + async createSession(input: CreateSessionPayload): Promise { const options = input; const workDir = requiredWorkDir('createSession', options.workDir); @@ -204,7 +202,7 @@ export class KimiCore implements PromisableMethods { // ctor block throws, `session.close()` releases the sink (and mcp). const runtime = await this.resolveRuntime(config); const session = new Session({ - kaos: (await this.kaos).withCwd(workDir), + kaos: (await this.getKaos()).withCwd(workDir), toolServices: runtime, config, id, @@ -291,7 +289,7 @@ export class KimiCore implements PromisableMethods { const mcpConfig = this.mergePluginMcpConfig(withCallerMcp); const runtime = await this.resolveRuntime(config); const session = new Session({ - kaos: (await this.kaos).withCwd(summary.workDir), + kaos: (await this.getKaos()).withCwd(summary.workDir), toolServices: runtime, config, id: summary.id, @@ -728,6 +726,16 @@ export class KimiCore implements PromisableMethods { return runtime; } + private getKaos(): Promise { + this.kaos ??= LocalKaos.create().catch((error: unknown) => { + if (error instanceof KaosShellNotFoundError) { + throw new KimiError(ErrorCodes.SHELL_GIT_BASH_NOT_FOUND, error.message); + } + throw error; + }); + return this.kaos; + } + private resolveSessionSkillConfig(config: KimiConfig): SessionSkillConfig { const explicitDirs = this.skillDirs.length > 0 ? this.skillDirs : undefined; return { diff --git a/packages/node-sdk/src/kimi-harness.ts b/packages/node-sdk/src/kimi-harness.ts index 183de20295..590fb733bb 100644 --- a/packages/node-sdk/src/kimi-harness.ts +++ b/packages/node-sdk/src/kimi-harness.ts @@ -211,6 +211,10 @@ export class KimiHarness { await this.ensureConfigFileImpl(); } + async checkRuntimeEnvironment(): Promise { + await this.rpc.checkRuntimeEnvironment(); + } + async setConfig(patch: KimiConfigPatch): Promise { return this.rpc.setConfig(patch); } diff --git a/packages/node-sdk/src/rpc.ts b/packages/node-sdk/src/rpc.ts index 86cc6dd1e9..79aefa9701 100644 --- a/packages/node-sdk/src/rpc.ts +++ b/packages/node-sdk/src/rpc.ts @@ -99,6 +99,11 @@ export abstract class SDKRpcClientBase { protected abstract getRpc(): Promise; + async checkRuntimeEnvironment(): Promise { + const rpc = await this.getRpc(); + return rpc.checkRuntimeEnvironment({}); + } + async createSession(input: CreateSessionOptions): Promise { const rpc = await this.getRpc(); const { planMode, ...coreInput } = input; From dee8790ba1c476b9fe42c837394a411f913246b9 Mon Sep 17 00:00:00 2001 From: 7Sageer <7sageer@djwcb.cn> Date: Thu, 4 Jun 2026 20:13:27 +0800 Subject: [PATCH 4/4] test: update goal prompt harness mock --- apps/kimi-code/test/cli/goal-prompt.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/kimi-code/test/cli/goal-prompt.test.ts b/apps/kimi-code/test/cli/goal-prompt.test.ts index b7966a5a38..5373c04a10 100644 --- a/apps/kimi-code/test/cli/goal-prompt.test.ts +++ b/apps/kimi-code/test/cli/goal-prompt.test.ts @@ -112,6 +112,7 @@ const mocks = vi.hoisted(() => { mainEvent, experimentalFeatures: [{ id: 'goal_command', enabled: true }], sessions: [] as Array<{ readonly id: string; readonly workDir: string }>, + harnessCheckRuntimeEnvironment: vi.fn(async () => undefined), }; }); @@ -122,6 +123,7 @@ vi.mock('@moonshot-ai/kimi-code-sdk', async (importOriginal) => { createKimiHarness: () => ({ homeDir: '/tmp/kimi-goal-home', auth: { getCachedAccessToken: vi.fn() }, + checkRuntimeEnvironment: mocks.harnessCheckRuntimeEnvironment, ensureConfigFile: vi.fn(), getConfig: vi.fn(async () => ({ providers: {}, defaultModel: 'k2', telemetry: true })), getExperimentalFeatures: vi.fn(async () => mocks.experimentalFeatures),