diff --git a/.changeset/homebrew-update-detection.md b/.changeset/homebrew-update-detection.md new file mode 100644 index 000000000..b9c8d6c8f --- /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. diff --git a/README.md b/README.md index e3e17adb8..d17d143e3 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 cf583f9b8..e60fd158e 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 diff --git a/apps/kimi-code/src/cli/update/install-state.ts b/apps/kimi-code/src/cli/update/install-state.ts index f9e10225e..7edc1b675 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', ]); diff --git a/apps/kimi-code/src/cli/update/preflight.ts b/apps/kimi-code/src/cli/update/preflight.ts index 3a6460b58..3d28b5609 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': @@ -82,6 +84,10 @@ export function canAutoInstall(source: InstallSource, platform: NodeJS.Platform) case 'yarn-global': case 'bun-global': 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': @@ -108,6 +114,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 +146,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 4b544d140..7d6904b67 100644 --- a/apps/kimi-code/src/cli/update/source.ts +++ b/apps/kimi-code/src/cli/update/source.ts @@ -40,6 +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/'; +// 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(); @@ -57,6 +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'; + if (normalized.includes(HOMEBREW_PATH_SEGMENT)) 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 abdcc8f68..ff4c93c1d 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 d4bd6d6ba..33c4d42a3 100644 --- a/apps/kimi-code/test/cli/update/preflight.test.ts +++ b/apps/kimi-code/test/cli/update/preflight.test.ts @@ -397,6 +397,17 @@ describe('runUpdatePreflight', () => { ); }); + it('homebrew: prints manual brew upgrade command, does not spawn', async () => { + mocks.readUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.refreshUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.detectInstallSource.mockResolvedValue('homebrew'); + 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).not.toHaveBeenCalled(); + }); + 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 20bba5cd1..dd88d32c3 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 Linux (Linuxbrew)', () => { + expect( + classifyByPathHeuristic('/home/linuxbrew/.linuxbrew/Cellar/kimi-code/0.5.0/libexec/lib/node_modules/@moonshot-ai/kimi-code'), + ).toBe('homebrew'); + }); + + it('does not treat npm-global under Homebrew prefix as homebrew', () => { + expect( + classifyByPathHeuristic('/opt/homebrew/lib/node_modules/@moonshot-ai/kimi-code'), + ).toBeNull(); + }); + 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({