docs: add Homebrew installation (#531)

* docs: add Homebrew installation instructions

Add Homebrew as an installation option for macOS/Linux users in both
English and Chinese READMEs.

Closes #130

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

* chore: add changeset for Homebrew update detection

* 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

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

---------

Co-authored-by: liruifengv <liruifeng1024@gmail.com>
This commit is contained in:
Davy 2026-06-08 01:11:31 -07:00 committed by GitHub
parent b785e2698a
commit b47734ca0b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 76 additions and 0 deletions

View file

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

View file

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

View file

@ -22,6 +22,12 @@ Kimi Code CLI 是一个运行在终端里的 AI 编程 agent可以帮你读
curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash
```
- **HomebrewmacOS / Linux**
```sh
brew install kimi-code
```
- **WindowsPowerShell**
```powershell

View file

@ -10,6 +10,7 @@ const InstallSourceSchema: z.ZodType<InstallSource> = z.enum([
'pnpm-global',
'yarn-global',
'bun-global',
'homebrew',
'native',
'unsupported',
]);

View file

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

View file

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

View file

@ -8,6 +8,7 @@ export type InstallSource =
| 'pnpm-global'
| 'yarn-global'
| 'bun-global'
| 'homebrew'
| 'native'
| 'unsupported';

View file

@ -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'));

View file

@ -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({