From 620e2c2e599120873027a58568dcd3b0b0a8eaed Mon Sep 17 00:00:00 2001 From: Davy <95214375+thedavidweng@users.noreply.github.com> Date: Sun, 7 Jun 2026 21:07:29 -0700 Subject: [PATCH 1/5] docs: add Homebrew installation instructions Add Homebrew as an installation option for macOS/Linux users in both English and Chinese READMEs. Closes #130 --- README.md | 6 ++++++ README.zh-CN.md | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/README.md b/README.md index e3e17adb8c..d17d143e34 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,12 @@ Install with the official script. No Node.js required. curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash ``` +- **Homebrew (macOS/Linux)**: + +```sh +brew install kimi-code +``` + - **Windows (PowerShell)**: ```powershell diff --git a/README.zh-CN.md b/README.zh-CN.md index cf583f9b85..e60fd158e5 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -22,6 +22,12 @@ Kimi Code CLI 是一个运行在终端里的 AI 编程 agent,可以帮你读 curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash ``` +- **Homebrew(macOS / Linux)**: + +```sh +brew install kimi-code +``` + - **Windows(PowerShell)**: ```powershell From 9dbcd3e44bd4c482086e7b47c903bb4c4eab2505 Mon Sep 17 00:00:00 2001 From: Davy <95214375+thedavidweng@users.noreply.github.com> Date: Sun, 7 Jun 2026 21:30:46 -0700 Subject: [PATCH 2/5] feat(cli): detect Homebrew installs and use brew upgrade for updates When kimi-code is installed via Homebrew, the update system now detects the installation source and uses 'brew upgrade kimi-code' instead of falling back to 'npm install -g'. This prevents duplicate installations when Homebrew users receive update prompts. --- apps/kimi-code/src/cli/update/preflight.ts | 8 +++++ apps/kimi-code/src/cli/update/source.ts | 4 +++ apps/kimi-code/src/cli/update/types.ts | 1 + .../test/cli/update/preflight.test.ts | 33 +++++++++++++++++++ apps/kimi-code/test/cli/update/source.test.ts | 30 +++++++++++++++++ 5 files changed, 76 insertions(+) diff --git a/apps/kimi-code/src/cli/update/preflight.ts b/apps/kimi-code/src/cli/update/preflight.ts index 3a6460b582..8579dd706a 100644 --- a/apps/kimi-code/src/cli/update/preflight.ts +++ b/apps/kimi-code/src/cli/update/preflight.ts @@ -68,6 +68,8 @@ export function installCommandFor( return `yarn global add ${NPM_PACKAGE_NAME}@${version}`; case 'bun-global': return `bun add -g ${NPM_PACKAGE_NAME}@${version}`; + case 'homebrew': + return 'brew upgrade kimi-code'; case 'native': return platform === 'win32' ? NATIVE_INSTALL_COMMAND_WIN : NATIVE_INSTALL_COMMAND_UNIX; case 'unsupported': @@ -81,6 +83,7 @@ export function canAutoInstall(source: InstallSource, platform: NodeJS.Platform) case 'pnpm-global': case 'yarn-global': case 'bun-global': + case 'homebrew': return true; case 'native': return platform !== 'win32'; @@ -108,6 +111,8 @@ export function spawnForSource( return { cmd: withCmdSuffix('yarn', platform), args: ['global', 'add', `${NPM_PACKAGE_NAME}@${version}`] }; case 'bun-global': return { cmd: bunCommand(platform), args: ['add', '-g', `${NPM_PACKAGE_NAME}@${version}`] }; + case 'homebrew': + return { cmd: 'brew', args: ['upgrade', 'kimi-code'] }; case 'native': // `curl … | bash` reports only the trailing bash's exit status, so a // failed download (curl can't connect → empty stdin → bash exits 0) @@ -138,6 +143,9 @@ export function renderManualUpdateMessage( case 'bun-global': sourceDesc = source; break; + case 'homebrew': + sourceDesc = 'homebrew'; + break; case 'native': sourceDesc = 'native (windows). Auto-update is not supported on this platform.'; break; diff --git a/apps/kimi-code/src/cli/update/source.ts b/apps/kimi-code/src/cli/update/source.ts index 4b544d140a..cc0e41acfc 100644 --- a/apps/kimi-code/src/cli/update/source.ts +++ b/apps/kimi-code/src/cli/update/source.ts @@ -40,6 +40,7 @@ export function detectNativeInstall(): boolean { const PNPM_PATH_SEGMENT = 'pnpm/global/'; const YARN_PATH_SEGMENTS = ['.config/yarn/global/', '/.yarn/global/']; const BUN_PATH_SEGMENT = '.bun/install/global/'; +const HOMEBREW_PATH_SEGMENTS = ['/cellar/', '/homebrew/']; function normalizeForHeuristic(filePath: string): string { return filePath.replaceAll('\\', '/').toLowerCase(); @@ -57,6 +58,9 @@ export function classifyByPathHeuristic(packageRoot: string): InstallSource | nu if (normalized.includes(seg)) return 'yarn-global'; } if (normalized.includes(BUN_PATH_SEGMENT)) return 'bun-global'; + for (const seg of HOMEBREW_PATH_SEGMENTS) { + if (normalized.includes(seg)) return 'homebrew'; + } return null; } diff --git a/apps/kimi-code/src/cli/update/types.ts b/apps/kimi-code/src/cli/update/types.ts index abdcc8f689..ff4c93c1da 100644 --- a/apps/kimi-code/src/cli/update/types.ts +++ b/apps/kimi-code/src/cli/update/types.ts @@ -8,6 +8,7 @@ export type InstallSource = | 'pnpm-global' | 'yarn-global' | 'bun-global' + | 'homebrew' | 'native' | 'unsupported'; diff --git a/apps/kimi-code/test/cli/update/preflight.test.ts b/apps/kimi-code/test/cli/update/preflight.test.ts index d4bd6d6ba9..5036f4ef39 100644 --- a/apps/kimi-code/test/cli/update/preflight.test.ts +++ b/apps/kimi-code/test/cli/update/preflight.test.ts @@ -397,6 +397,39 @@ describe('runUpdatePreflight', () => { ); }); + it('homebrew: spawns brew upgrade kimi-code', async () => { + disableAutoInstall(); + mocks.readUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.refreshUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.detectInstallSource.mockResolvedValue('homebrew'); + mocks.promptForInstallChoice.mockResolvedValue('install'); + mockSpawnExit(0); + const { options } = captureOutput(); + await runUpdatePreflight('0.4.0', options); + expect(mocks.spawn).toHaveBeenCalledWith( + 'brew', + ['upgrade', 'kimi-code'], + { stdio: 'inherit' }, + ); + }); + + it('homebrew: starts automatic background update', async () => { + mocks.readUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.readUpdateInstallState.mockResolvedValue(installState()); + mocks.refreshUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.detectInstallSource.mockResolvedValue('homebrew'); + mockSpawnExit(0); + const { options } = captureOutput(); + + await expect(runUpdatePreflight('0.4.0', options)).resolves.toBe('continue'); + expect(promptForInstallChoice).not.toHaveBeenCalled(); + expect(mocks.spawn).toHaveBeenCalledWith( + 'brew', + ['upgrade', 'kimi-code'], + { detached: true, stdio: 'ignore' }, + ); + }); + it('native on darwin: spawns bash -c with pipefail-guarded curl|bash', async () => { disableAutoInstall(); mocks.readUpdateCache.mockResolvedValue(cacheWith('0.5.0')); diff --git a/apps/kimi-code/test/cli/update/source.test.ts b/apps/kimi-code/test/cli/update/source.test.ts index 20bba5cd15..8850451977 100644 --- a/apps/kimi-code/test/cli/update/source.test.ts +++ b/apps/kimi-code/test/cli/update/source.test.ts @@ -47,6 +47,24 @@ describe('classifyByPathHeuristic', () => { ).toBe('bun-global'); }); + it('detects homebrew on macOS (Cellar path)', () => { + expect( + classifyByPathHeuristic('/opt/homebrew/Cellar/kimi-code/0.5.0/libexec/lib/node_modules/@moonshot-ai/kimi-code'), + ).toBe('homebrew'); + }); + + it('detects homebrew on macOS (homebrew prefix path)', () => { + expect( + classifyByPathHeuristic('/usr/local/Cellar/kimi-code/0.5.0/libexec/lib/node_modules/@moonshot-ai/kimi-code'), + ).toBe('homebrew'); + }); + + it('detects homebrew on Linux (Linuxbrew)', () => { + expect( + classifyByPathHeuristic('/home/linuxbrew/.linuxbrew/Cellar/kimi-code/0.5.0/libexec/lib/node_modules/@moonshot-ai/kimi-code'), + ).toBe('homebrew'); + }); + it('returns null for an unknown layout', () => { expect(classifyByPathHeuristic('/Users/me/dev/@moonshot-ai/kimi-code')).toBeNull(); }); @@ -112,6 +130,18 @@ describe('detectInstallSource', () => { ).resolves.toBe('npm-global'); }); + it('returns homebrew when packageRoot matches Cellar heuristic', async () => { + await expect( + detectInstallSource({ + getPackageRoot: () => + '/opt/homebrew/Cellar/kimi-code/0.5.0/libexec/lib/node_modules/@moonshot-ai/kimi-code', + getGlobalPrefix: async () => '/usr/local', + detectNative: () => false, + platform: 'darwin', + }), + ).resolves.toBe('homebrew'); + }); + it('returns native when SEA isSea() is true (highest priority)', async () => { await expect( detectInstallSource({ From 487e653e3428ad08782865e0b9105b8f542a4528 Mon Sep 17 00:00:00 2001 From: Davy <95214375+thedavidweng@users.noreply.github.com> Date: Sun, 7 Jun 2026 21:31:03 -0700 Subject: [PATCH 3/5] chore: add changeset for Homebrew update detection --- .changeset/homebrew-update-detection.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/homebrew-update-detection.md diff --git a/.changeset/homebrew-update-detection.md b/.changeset/homebrew-update-detection.md new file mode 100644 index 0000000000..b9c8d6c8ff --- /dev/null +++ b/.changeset/homebrew-update-detection.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": minor +--- + +Detect Homebrew installations and use `brew upgrade kimi-code` for updates instead of falling back to npm. From 7c54031b2ff57e465218e03fd1a7fa57a0923e95 Mon Sep 17 00:00:00 2001 From: Davy <95214375+thedavidweng@users.noreply.github.com> Date: Sun, 7 Jun 2026 21:58:44 -0700 Subject: [PATCH 4/5] fix(cli): tighten Homebrew detection and disable auto-update - Only match /cellar/ path segment (not /homebrew/) to avoid false positives on Apple Silicon where npm global installs live under /opt/homebrew/lib/node_modules/ - Disable background auto-update for Homebrew: brew upgrade may mutate dependents silently and the formula can lag behind CDN releases --- apps/kimi-code/src/cli/update/preflight.ts | 5 +++- apps/kimi-code/src/cli/update/source.ts | 9 +++--- .../test/cli/update/preflight.test.ts | 30 +++---------------- apps/kimi-code/test/cli/update/source.test.ts | 10 +++---- 4 files changed, 18 insertions(+), 36 deletions(-) diff --git a/apps/kimi-code/src/cli/update/preflight.ts b/apps/kimi-code/src/cli/update/preflight.ts index 8579dd706a..3d28b56096 100644 --- a/apps/kimi-code/src/cli/update/preflight.ts +++ b/apps/kimi-code/src/cli/update/preflight.ts @@ -83,8 +83,11 @@ export function canAutoInstall(source: InstallSource, platform: NodeJS.Platform) case 'pnpm-global': case 'yarn-global': case 'bun-global': - case 'homebrew': return true; + case 'homebrew': + // Homebrew upgrade may mutate other dependents and the formula can lag + // behind the CDN release — prompt the user to run `brew upgrade` manually. + return false; case 'native': return platform !== 'win32'; case 'unsupported': diff --git a/apps/kimi-code/src/cli/update/source.ts b/apps/kimi-code/src/cli/update/source.ts index cc0e41acfc..7d6904b673 100644 --- a/apps/kimi-code/src/cli/update/source.ts +++ b/apps/kimi-code/src/cli/update/source.ts @@ -40,7 +40,10 @@ export function detectNativeInstall(): boolean { const PNPM_PATH_SEGMENT = 'pnpm/global/'; const YARN_PATH_SEGMENTS = ['.config/yarn/global/', '/.yarn/global/']; const BUN_PATH_SEGMENT = '.bun/install/global/'; -const HOMEBREW_PATH_SEGMENTS = ['/cellar/', '/homebrew/']; +// Homebrew installs formulae under its Cellar directory. Avoid matching the +// broader /homebrew/ prefix — on Apple Silicon, npm itself lives under +// /opt/homebrew/, so `npm install -g` paths also contain /homebrew/. +const HOMEBREW_PATH_SEGMENT = '/cellar/'; function normalizeForHeuristic(filePath: string): string { return filePath.replaceAll('\\', '/').toLowerCase(); @@ -58,9 +61,7 @@ export function classifyByPathHeuristic(packageRoot: string): InstallSource | nu if (normalized.includes(seg)) return 'yarn-global'; } if (normalized.includes(BUN_PATH_SEGMENT)) return 'bun-global'; - for (const seg of HOMEBREW_PATH_SEGMENTS) { - if (normalized.includes(seg)) return 'homebrew'; - } + if (normalized.includes(HOMEBREW_PATH_SEGMENT)) return 'homebrew'; return null; } diff --git a/apps/kimi-code/test/cli/update/preflight.test.ts b/apps/kimi-code/test/cli/update/preflight.test.ts index 5036f4ef39..33c4d42a37 100644 --- a/apps/kimi-code/test/cli/update/preflight.test.ts +++ b/apps/kimi-code/test/cli/update/preflight.test.ts @@ -397,37 +397,15 @@ describe('runUpdatePreflight', () => { ); }); - it('homebrew: spawns brew upgrade kimi-code', async () => { - disableAutoInstall(); - mocks.readUpdateCache.mockResolvedValue(cacheWith('0.5.0')); - mocks.refreshUpdateCache.mockResolvedValue(cacheWith('0.5.0')); - mocks.detectInstallSource.mockResolvedValue('homebrew'); - mocks.promptForInstallChoice.mockResolvedValue('install'); - mockSpawnExit(0); - const { options } = captureOutput(); - await runUpdatePreflight('0.4.0', options); - expect(mocks.spawn).toHaveBeenCalledWith( - 'brew', - ['upgrade', 'kimi-code'], - { stdio: 'inherit' }, - ); - }); - - it('homebrew: starts automatic background update', async () => { + it('homebrew: prints manual brew upgrade command, does not spawn', async () => { mocks.readUpdateCache.mockResolvedValue(cacheWith('0.5.0')); - mocks.readUpdateInstallState.mockResolvedValue(installState()); mocks.refreshUpdateCache.mockResolvedValue(cacheWith('0.5.0')); mocks.detectInstallSource.mockResolvedValue('homebrew'); - mockSpawnExit(0); - const { options } = captureOutput(); - + const { stdout, options } = captureOutput(); await expect(runUpdatePreflight('0.4.0', options)).resolves.toBe('continue'); + expect(stdout.join('')).toContain('brew upgrade kimi-code'); expect(promptForInstallChoice).not.toHaveBeenCalled(); - expect(mocks.spawn).toHaveBeenCalledWith( - 'brew', - ['upgrade', 'kimi-code'], - { detached: true, stdio: 'ignore' }, - ); + expect(mocks.spawn).not.toHaveBeenCalled(); }); it('native on darwin: spawns bash -c with pipefail-guarded curl|bash', async () => { diff --git a/apps/kimi-code/test/cli/update/source.test.ts b/apps/kimi-code/test/cli/update/source.test.ts index 8850451977..dd88d32c3c 100644 --- a/apps/kimi-code/test/cli/update/source.test.ts +++ b/apps/kimi-code/test/cli/update/source.test.ts @@ -53,16 +53,16 @@ describe('classifyByPathHeuristic', () => { ).toBe('homebrew'); }); - it('detects homebrew on macOS (homebrew prefix path)', () => { + it('detects homebrew on Linux (Linuxbrew)', () => { expect( - classifyByPathHeuristic('/usr/local/Cellar/kimi-code/0.5.0/libexec/lib/node_modules/@moonshot-ai/kimi-code'), + classifyByPathHeuristic('/home/linuxbrew/.linuxbrew/Cellar/kimi-code/0.5.0/libexec/lib/node_modules/@moonshot-ai/kimi-code'), ).toBe('homebrew'); }); - it('detects homebrew on Linux (Linuxbrew)', () => { + it('does not treat npm-global under Homebrew prefix as homebrew', () => { expect( - classifyByPathHeuristic('/home/linuxbrew/.linuxbrew/Cellar/kimi-code/0.5.0/libexec/lib/node_modules/@moonshot-ai/kimi-code'), - ).toBe('homebrew'); + classifyByPathHeuristic('/opt/homebrew/lib/node_modules/@moonshot-ai/kimi-code'), + ).toBeNull(); }); it('returns null for an unknown layout', () => { From 027f156391885a2e02f35361ab05fc2c979c817e Mon Sep 17 00:00:00 2001 From: Davy <95214375+thedavidweng@users.noreply.github.com> Date: Mon, 8 Jun 2026 00:47:04 -0700 Subject: [PATCH 5/5] fix(cli): add homebrew to InstallSource Zod schema Keeps the persistence schema in sync with the TypeScript type. Currently harmless since Homebrew auto-install is disabled, but prevents a silent state reset if it is ever enabled later. --- apps/kimi-code/src/cli/update/install-state.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/kimi-code/src/cli/update/install-state.ts b/apps/kimi-code/src/cli/update/install-state.ts index f9e10225e6..7edc1b675b 100644 --- a/apps/kimi-code/src/cli/update/install-state.ts +++ b/apps/kimi-code/src/cli/update/install-state.ts @@ -10,6 +10,7 @@ const InstallSourceSchema: z.ZodType = z.enum([ 'pnpm-global', 'yarn-global', 'bun-global', + 'homebrew', 'native', 'unsupported', ]);