diff --git a/.changeset/windows-git-bash-preflight.md b/.changeset/windows-git-bash-preflight.md deleted file mode 100644 index f58b78d14..000000000 --- a/.changeset/windows-git-bash-preflight.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@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 3fcd5fb1a..7d41e6246 100644 --- a/apps/kimi-code/src/cli/run-prompt.ts +++ b/apps/kimi-code/src/cli/run-prompt.ts @@ -110,7 +110,6 @@ 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 3633f41cb..4ec4330a5 100644 --- a/apps/kimi-code/src/cli/run-shell.ts +++ b/apps/kimi-code/src/cli/run-shell.ts @@ -34,6 +34,21 @@ 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 = { @@ -63,23 +78,6 @@ 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/goal-prompt.test.ts b/apps/kimi-code/test/cli/goal-prompt.test.ts index 5373c04a1..b7966a5a3 100644 --- a/apps/kimi-code/test/cli/goal-prompt.test.ts +++ b/apps/kimi-code/test/cli/goal-prompt.test.ts @@ -112,7 +112,6 @@ 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), }; }); @@ -123,7 +122,6 @@ 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), diff --git a/apps/kimi-code/test/cli/run-prompt.test.ts b/apps/kimi-code/test/cli/run-prompt.test.ts index 713231ee3..9a26c5a7f 100644 --- a/apps/kimi-code/test/cli/run-prompt.test.ts +++ b/apps/kimi-code/test/cli/run-prompt.test.ts @@ -46,7 +46,6 @@ 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 }> => ({ @@ -90,7 +89,6 @@ vi.mock('@moonshot-ai/kimi-code-sdk', async (importOriginal) => { return { homeDir, auth: { getCachedAccessToken: mocks.harnessGetCachedAccessToken }, - checkRuntimeEnvironment: mocks.harnessCheckRuntimeEnvironment, ensureConfigFile: mocks.harnessEnsureConfigFile, getConfig: mocks.harnessGetConfig, getExperimentalFeatures: mocks.harnessGetExperimentalFeatures, @@ -189,7 +187,6 @@ describe('runPrompt', () => { mocks.resolveKimiHome.mockImplementation( (homeDir?: string) => homeDir ?? '/tmp/kimi-code-test-home', ); - mocks.harnessCheckRuntimeEnvironment.mockResolvedValue(undefined); mocks.harnessCreatesDeviceIdOnConstruction = false; }); @@ -202,10 +199,6 @@ 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', @@ -221,23 +214,6 @@ 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 027aeb85f..c55a5e590 100644 --- a/apps/kimi-code/test/cli/run-shell.test.ts +++ b/apps/kimi-code/test/cli/run-shell.test.ts @@ -31,7 +31,6 @@ 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: {}, @@ -82,7 +81,6 @@ 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, @@ -151,7 +149,6 @@ describe('runShell', () => { defaultModel: 'k2', telemetry: true, }); - mocks.harnessCheckRuntimeEnvironment.mockResolvedValue(undefined); mocks.tuiGetStartupMcpMs.mockResolvedValue(0); mocks.tuiGetCurrentSessionId.mockReturnValue(''); mocks.tuiHasSessionContent.mockReturnValue(false); @@ -194,10 +191,6 @@ 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]!, @@ -251,38 +244,6 @@ 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 b5942c73e..da186c721 100644 --- a/packages/agent-core/src/rpc/core-api.ts +++ b/packages/agent-core/src/rpc/core-api.ts @@ -357,7 +357,6 @@ type SessionAPIWithId = WithSessionId; export interface CoreAPI extends SessionAPIWithId { getCoreInfo: (payload: EmptyPayload) => CoreInfo; - checkRuntimeEnvironment: (payload: EmptyPayload) => void; getExperimentalFeatures: (payload: EmptyPayload) => readonly ExperimentalFeatureState[]; 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 48fb9bacf..61cb01131 100644 --- a/packages/agent-core/src/rpc/core-impl.ts +++ b/packages/agent-core/src/rpc/core-impl.ts @@ -123,7 +123,7 @@ export class KimiCore implements PromisableMethods { readonly sessions = new Map(); readonly telemetry: TelemetryClient; - private kaos: Promise | undefined; + private kaos: Promise; private runtime: ToolServices | undefined; private config: KimiConfig; private readonly runtimeOverride: ToolServices | undefined; @@ -148,6 +148,12 @@ 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; @@ -176,10 +182,6 @@ 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); @@ -209,7 +211,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.getKaos()).withCwd(workDir), + kaos: (await this.kaos).withCwd(workDir), toolServices: runtime, config, id, @@ -297,7 +299,7 @@ export class KimiCore implements PromisableMethods { const mcpConfig = this.mergePluginMcpConfig(withCallerMcp); const runtime = await this.resolveRuntime(config); const session = new Session({ - kaos: (await this.getKaos()).withCwd(summary.workDir), + kaos: (await this.kaos).withCwd(summary.workDir), toolServices: runtime, config, id: summary.id, @@ -735,16 +737,6 @@ 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 47780b855..86175843c 100644 --- a/packages/node-sdk/src/kimi-harness.ts +++ b/packages/node-sdk/src/kimi-harness.ts @@ -210,10 +210,6 @@ 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 ddd384f9c..c3a220ea8 100644 --- a/packages/node-sdk/src/rpc.ts +++ b/packages/node-sdk/src/rpc.ts @@ -99,11 +99,6 @@ 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;