Revert "fix: fail early when Git Bash is missing (#430)"

This reverts commit be0da5ff39.
This commit is contained in:
7Sageer 2026-06-04 20:19:56 +08:00 committed by GitHub
parent be0da5ff39
commit 028cbc4ec4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 24 additions and 117 deletions

View file

@ -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.

View file

@ -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 } =

View file

@ -34,6 +34,21 @@ export async function runShell(
runOptions: { readonly migrateOnly?: boolean } = {},
): Promise<void> {
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'),

View file

@ -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),

View file

@ -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) },

View file

@ -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',

View file

@ -357,7 +357,6 @@ type SessionAPIWithId = WithSessionId<SessionAPI>;
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;

View file

@ -123,7 +123,7 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
readonly sessions = new Map<string, Session>();
readonly telemetry: TelemetryClient;
private kaos: Promise<Kaos> | undefined;
private kaos: Promise<Kaos>;
private runtime: ToolServices | undefined;
private config: KimiConfig;
private readonly runtimeOverride: ToolServices | undefined;
@ -148,6 +148,12 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
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<CoreAPI> {
this.sdk = rpcClient(this);
}
async checkRuntimeEnvironment(_: EmptyPayload): Promise<void> {
await this.getKaos();
}
async createSession(input: CreateSessionPayload): Promise<SessionSummary> {
const options = input;
const workDir = requiredWorkDir('createSession', options.workDir);
@ -209,7 +211,7 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
// 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<CoreAPI> {
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<CoreAPI> {
return runtime;
}
private getKaos(): Promise<Kaos> {
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 {

View file

@ -210,10 +210,6 @@ export class KimiHarness {
await this.ensureConfigFileImpl();
}
async checkRuntimeEnvironment(): Promise<void> {
await this.rpc.checkRuntimeEnvironment();
}
async setConfig(patch: KimiConfigPatch): Promise<KimiConfig> {
return this.rpc.setConfig(patch);
}

View file

@ -99,11 +99,6 @@ export abstract class SDKRpcClientBase {
protected abstract getRpc(): Promise<ResolvedCoreAPI>;
async checkRuntimeEnvironment(): Promise<void> {
const rpc = await this.getRpc();
return rpc.checkRuntimeEnvironment({});
}
async createSession(input: CreateSessionOptions): Promise<SessionSummary> {
const rpc = await this.getRpc();
const { planMode, ...coreInput } = input;