Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/homebrew-update-detection.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/src/cli/update/install-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const InstallSourceSchema: z.ZodType<InstallSource> = z.enum([
'pnpm-global',
'yarn-global',
'bun-global',
'homebrew',
'native',
'unsupported',
]);
Expand Down
11 changes: 11 additions & 0 deletions apps/kimi-code/src/cli/update/preflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand All @@ -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':
Expand All @@ -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'] };
Comment thread
thedavidweng marked this conversation as resolved.
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)
Expand Down Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions apps/kimi-code/src/cli/update/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;
}

Expand Down
1 change: 1 addition & 0 deletions apps/kimi-code/src/cli/update/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type InstallSource =
| 'pnpm-global'
| 'yarn-global'
| 'bun-global'
| 'homebrew'
| 'native'
| 'unsupported';

Expand Down
11 changes: 11 additions & 0 deletions apps/kimi-code/test/cli/update/preflight.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down
30 changes: 30 additions & 0 deletions apps/kimi-code/test/cli/update/source.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand Down Expand Up @@ -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({
Expand Down