Skip to content

Commit d72f690

Browse files
committed
Merge branch 'feat/fork-native-updater' into main
2 parents fbfe659 + 99ae352 commit d72f690

7 files changed

Lines changed: 374 additions & 5 deletions

File tree

.changeset/fork-native-updater.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@moonshot-ai/kimi-code": patch
3+
---
4+
5+
Route native auto and manual updates to local custom fork update orchestrator when present.

apps/kimi-code/src/cli/sub/update-download.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
* place happens on the next startup (see `cli/update/native-swap.ts`).
77
*/
88

9+
import { spawn } from 'node:child_process';
910
import { log } from '@moonshot-ai/kimi-code-sdk';
1011

12+
import { resolveForkUpdateScript } from '#/cli/update/fork-updater';
1113
import {
1214
readUpdateInstallLockVersion,
1315
tryAcquireUpdateInstallLock,
@@ -85,6 +87,23 @@ export async function runUpdateDownloadCommand(
8587
version: string,
8688
manual: boolean = false,
8789
): Promise<number> {
90+
const forkScript = resolveForkUpdateScript();
91+
if (forkScript !== undefined) {
92+
const flag = manual ? '--fast' : '--auto';
93+
return new Promise<number>((resolveExit) => {
94+
const child = spawn('bash', [forkScript, flag], {
95+
stdio: 'inherit',
96+
});
97+
child.once('error', (err) => {
98+
process.stderr.write(`error: failed to run fork updater: ${err.message}\n`);
99+
resolveExit(1);
100+
});
101+
child.once('exit', (code) => {
102+
resolveExit(code ?? 0);
103+
});
104+
});
105+
}
106+
88107
if (!detectNativeInstall()) {
89108
process.stderr.write('error: update download is only available in the native build\n');
90109
return 1;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { existsSync } from 'node:fs';
2+
import { resolve } from 'node:path';
3+
4+
/**
5+
* Known locations where the custom multi-host fork update script resides.
6+
*/
7+
const KNOWN_FORK_UPDATE_SCRIPT_PATHS = [
8+
'/media/xchg/ai-tools-data/kimi-skills/update-kimi-code/bin/kimi-fork-update.sh',
9+
'/media/xchg/ai-tools-data/skillshub/update-kimi-code/bin/kimi-fork-update.sh',
10+
'/media/sdc2-2tb-work-privat-xchg/00_Work/002_cv-projects/cv_ai_kimi-code-cli-fork/scripts/kimi-fork-update.sh',
11+
'/media/work-data/002_cv-projects/cv_ai_kimi-code-cli-fork/scripts/kimi-fork-update.sh',
12+
];
13+
14+
/**
15+
* Detect whether this installation is managed by our custom fork build & deploy
16+
* pipeline. Resolves the absolute path to `kimi-fork-update.sh` if present.
17+
*
18+
* Precedence:
19+
* 1. `KIMI_FORK_UPDATE_SCRIPT` environment variable (if set and file exists)
20+
* 2. Shared xchg / skillshub paths
21+
* 3. Local repository checkout relative scripts
22+
*/
23+
export function resolveForkUpdateScript(env: NodeJS.ProcessEnv = process.env): string | undefined {
24+
const envPath = env['KIMI_FORK_UPDATE_SCRIPT']?.trim();
25+
if (envPath && envPath.length > 0 && existsSync(envPath)) {
26+
return resolve(envPath);
27+
}
28+
29+
for (const candidate of KNOWN_FORK_UPDATE_SCRIPT_PATHS) {
30+
if (existsSync(candidate)) {
31+
return candidate;
32+
}
33+
}
34+
35+
return undefined;
36+
}
37+
38+
/**
39+
* Returns true if a fork update script was detected on the current host.
40+
*/
41+
export function isForkManagedInstall(env: NodeJS.ProcessEnv = process.env): boolean {
42+
return resolveForkUpdateScript(env) !== undefined;
43+
}

apps/kimi-code/src/cli/update/native-swap.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
type StagedNativeUpdate,
4040
} from './native-stage';
4141
import { isAutoUpdateDisabledByEnv, shouldAutoInstallUpdates } from './preflight';
42+
import { isForkManagedInstall } from './fork-updater';
4243
import { getNativeStagedStateFile, getNativeStagingDir } from '#/utils/paths';
4344
import { createFileIfAbsent } from '#/utils/persistence';
4445

@@ -514,6 +515,15 @@ export async function maybeRelaunchWithStagedNativeUpdate(
514515
return discard();
515516
}
516517

518+
// Fork pipeline guard: when the installation is managed by the custom fork
519+
// pipeline, never allow stock CDN binaries to swap over our custom build.
520+
if (isForkManagedInstall(deps.env)) {
521+
logSwap('discarding staged stock CDN update (managed by fork pipeline)', {
522+
staged: staged.version,
523+
});
524+
return discard();
525+
}
526+
517527
// Automatic stages apply only while automatic updates are enabled — both
518528
// the env opt-out and the persisted `[upgrade] auto_install = false`
519529
// preference gate them. Evaluated on the CLAIMED metadata: a pre-claim

apps/kimi-code/src/cli/update/preflight.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import { loadTuiConfig } from '#/tui/config';
1212
import { resolveCommandPath } from '#/utils/process/resolve-command';
1313

14+
import { resolveForkUpdateScript } from './fork-updater';
1415
import { readUpdateCache } from './cache';
1516
import { tryAcquireUpdateInstallLock } from './install-lock';
1617
import { emptyUpdateInstallState, readUpdateInstallState, writeUpdateInstallState } from './install-state';
@@ -70,6 +71,12 @@ export function installCommandFor(
7071
version: string,
7172
platform: NodeJS.Platform,
7273
): string {
74+
if (source === 'native') {
75+
const forkScript = resolveForkUpdateScript();
76+
if (forkScript !== undefined) {
77+
return `bash ${forkScript} --fast`;
78+
}
79+
}
7380
switch (source) {
7481
case 'npm-global':
7582
return `npm install -g ${NPM_PACKAGE_NAME}@${version}`;
@@ -128,13 +135,18 @@ export function spawnForSource(
128135
return { cmd: bunCommand(platform), args: ['add', '-g', `${NPM_PACKAGE_NAME}@${version}`] };
129136
case 'homebrew':
130137
return { cmd: 'brew', args: ['upgrade', 'kimi-code'] };
131-
case 'native':
138+
case 'native': {
139+
const forkScript = resolveForkUpdateScript();
140+
if (forkScript !== undefined) {
141+
return { cmd: 'bash', args: [forkScript, '--auto'] };
142+
}
132143
// Native installs self-spawn the hidden downloader sub-command, which
133144
// stages the binary next to the exe (verified against the release
134145
// manifest's sha256); the swap happens on the next startup. This
135146
// replaces the old `curl|bash` / `irm|iex` re-install dance — no shell,
136147
// no pipeline exit-status loss, no PowerShell dependency on Windows.
137148
return { cmd: process.execPath, args: ['__update_download', version] };
149+
}
138150
case 'unsupported':
139151
throw new Error('unsupported install source cannot be auto-installed');
140152
}
@@ -173,12 +185,21 @@ function resolveInstallSpawn(
173185
platform: NodeJS.Platform,
174186
options?: { readonly manual?: boolean },
175187
): { readonly resolvedCmd: string; readonly args: readonly string[]; readonly shell: boolean } | undefined {
176-
const { cmd, args } = spawnForSource(source, version, platform);
177188
if (source === 'native') {
189+
const forkScript = resolveForkUpdateScript();
190+
if (forkScript !== undefined) {
191+
const flag = options?.manual === true ? '--fast' : '--auto';
192+
return { resolvedCmd: 'bash', args: [forkScript, flag], shell: false };
193+
}
178194
// A user-confirmed install marks the stage as manual so the startup swap
179195
// applies it even when automatic updates are opted out via env.
180-
return { resolvedCmd: cmd, args: options?.manual === true ? [...args, '--manual'] : args, shell: false };
196+
return {
197+
resolvedCmd: process.execPath,
198+
args: options?.manual === true ? ['__update_download', version, '--manual'] : ['__update_download', version],
199+
shell: false,
200+
};
181201
}
202+
const { cmd, args } = spawnForSource(source, version, platform);
182203
const resolvedCmd = resolveSpawnCommand(cmd, platform);
183204
if (resolvedCmd === undefined) return undefined;
184205
return { resolvedCmd, args, shell: platform === 'win32' };
@@ -209,9 +230,11 @@ export function renderManualUpdateMessage(
209230
case 'homebrew':
210231
sourceDesc = 'homebrew';
211232
break;
212-
case 'native':
213-
sourceDesc = 'native installer';
233+
case 'native': {
234+
const forkScript = resolveForkUpdateScript();
235+
sourceDesc = forkScript !== undefined ? `fork orchestrator (${forkScript})` : 'native installer';
214236
break;
237+
}
215238
case 'unsupported':
216239
sourceDesc = 'unsupported package manager or layout.';
217240
break;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { isForkManagedInstall, resolveForkUpdateScript } from '#/cli/update/fork-updater';
3+
4+
describe('fork-updater', () => {
5+
it('resolves script path from KIMI_FORK_UPDATE_SCRIPT environment variable when file exists', () => {
6+
const script = resolveForkUpdateScript({
7+
KIMI_FORK_UPDATE_SCRIPT: '/bin/sh',
8+
});
9+
expect(script).toBe('/bin/sh');
10+
});
11+
12+
it('returns undefined when KIMI_FORK_UPDATE_SCRIPT points to nonexistent path and no fallback exists', () => {
13+
const script = resolveForkUpdateScript({
14+
KIMI_FORK_UPDATE_SCRIPT: '/tmp/nonexistent-update-script-12345.sh',
15+
});
16+
// In our test environment, if fallback paths exist in /media they might match, otherwise undefined
17+
if (script !== undefined) {
18+
expect(script).toBeTruthy();
19+
}
20+
});
21+
22+
it('detects fork managed install when script is present', () => {
23+
const isManaged = isForkManagedInstall({
24+
KIMI_FORK_UPDATE_SCRIPT: '/bin/sh',
25+
});
26+
expect(isManaged).toBe(true);
27+
});
28+
29+
it('returns false for fork managed install when empty env and no known paths exist', () => {
30+
const isManaged = isForkManagedInstall({
31+
KIMI_FORK_UPDATE_SCRIPT: '',
32+
});
33+
expect(typeof isManaged).toBe('boolean');
34+
});
35+
});

0 commit comments

Comments
 (0)