Skip to content
Open
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/non-interactive-upgrade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---

Add `-y, --yes` flag to `kimi upgrade` for non-interactive installs.
8 changes: 5 additions & 3 deletions apps/kimi-code/src/cli/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { registerProviderCommand } from './sub/provider';
export type MainCommandHandler = (opts: CLIOptions) => void;
export type MigrateCommandHandler = () => void;
export type PluginNodeRunnerHandler = (entry: string, args: readonly string[]) => void;
export type UpgradeCommandHandler = () => void | Promise<void>;
export type UpgradeCommandHandler = (yes?: boolean) => void | Promise<void>;

export function createProgram(
version: string,
Expand Down Expand Up @@ -84,8 +84,10 @@ export function createProgram(
program
.command('upgrade')
.description('Upgrade Kimi Code to the latest version.')
.action(async () => {
await onUpgrade();
.option('-y, --yes', 'Install the update without prompting.', false)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid reusing -y for the upgrade yes flag

When users run kimi upgrade -y, Commander parses -y as the root --yolo option because the root command already owns that short flag and Commander v13 treats parent options as usable after subcommands by default; the subcommand never sees its local yes value, and optsWithGlobals() then leaves opts.yes false. The advertised short form therefore still prompts (or falls back to the manual command in non-interactive runs). Use only the long flag, enable positional option parsing, or read the subcommand-local option without the parent merge.

Useful? React with 👍 / 👎.

.action(async function () {
const opts = this.optsWithGlobals<{ readonly yes?: boolean }>();
await onUpgrade(opts.yes === true);
});

program
Expand Down
60 changes: 34 additions & 26 deletions apps/kimi-code/src/cli/sub/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export interface UpgradeDeps {
readonly stdout: WritableLike;
readonly stderr: WritableLike;
readonly isInteractive: boolean;
readonly skipPrompt: boolean;
readonly track: UpgradeTrack;
readonly logger: UpgradeLogger;
}
Expand Down Expand Up @@ -86,7 +87,9 @@ export async function handleUpgrade(

const source = await deps.detectInstallSource().catch(() => 'unsupported' as const);
const installCommand = installCommandFor(source, target.version, deps.platform);
if (!canAutoInstall(source, deps.platform) || !deps.isInteractive) {
const autoInstallable = canAutoInstall(source, deps.platform);

if (!autoInstallable || (!deps.isInteractive && !deps.skipPrompt)) {
trackUpgradeEvent(deps.track, 'upgrade_command_manual_command', {
current_version: currentVersion,
target_version: target.version,
Expand All @@ -101,42 +104,46 @@ export async function handleUpgrade(
return 0;
}

trackUpgradeEvent(deps.track, 'upgrade_command_prompted', {
current_version: currentVersion,
target_version: target.version,
source,
});
logUpgradeInfo(deps.logger, 'manual upgrade prompted', {
currentVersion,
targetVersion: target.version,
source,
});
const choice = await deps.promptForInstallChoice({
currentVersion,
target,
installCommand,
installSource: source,
});
if (choice === 'skip') {
trackUpgradeEvent(deps.track, 'upgrade_command_skipped', {
if (!deps.skipPrompt) {
trackUpgradeEvent(deps.track, 'upgrade_command_prompted', {
current_version: currentVersion,
target_version: target.version,
source,
});
logUpgradeInfo(deps.logger, 'manual upgrade skipped', {
logUpgradeInfo(deps.logger, 'manual upgrade prompted', {
currentVersion,
targetVersion: target.version,
source,
});
return 0;
const choice = await deps.promptForInstallChoice({
currentVersion,
target,
installCommand,
installSource: source,
});
if (choice === 'skip') {
trackUpgradeEvent(deps.track, 'upgrade_command_skipped', {
current_version: currentVersion,
target_version: target.version,
source,
});
logUpgradeInfo(deps.logger, 'manual upgrade skipped', {
currentVersion,
targetVersion: target.version,
source,
});
return 0;
}
}

try {
trackUpgradeEvent(deps.track, 'upgrade_command_install_selected', {
current_version: currentVersion,
target_version: target.version,
source,
});
if (!deps.skipPrompt) {
trackUpgradeEvent(deps.track, 'upgrade_command_install_selected', {
current_version: currentVersion,
target_version: target.version,
source,
});
}
await deps.installUpdate(source, target.version, deps.platform);
trackUpgradeEvent(deps.track, 'upgrade_command_succeeded', {
current_version: currentVersion,
Expand Down Expand Up @@ -182,6 +189,7 @@ function createDefaultUpgradeDeps(overrides: Partial<UpgradeDeps>): UpgradeDeps
stdout: overrides.stdout ?? process.stdout,
stderr: overrides.stderr ?? process.stderr,
isInteractive: overrides.isInteractive ?? (process.stdin.isTTY && process.stdout.isTTY),
skipPrompt: overrides.skipPrompt ?? false,
track: overrides.track ?? trackTelemetry,
logger: overrides.logger ?? log,
};
Expand Down
8 changes: 4 additions & 4 deletions apps/kimi-code/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ async function handleMigrateCommand(version: string): Promise<void> {
await runShell(MIGRATE_CLI_OPTIONS, version, { migrateOnly: true });
}

export async function handleUpgradeCommand(version: string): Promise<void> {
export async function handleUpgradeCommand(version: string, yes = false): Promise<void> {
const telemetryBootstrap = createCliTelemetryBootstrap();
const telemetryClient: TelemetryClient = {
track,
Expand All @@ -94,7 +94,7 @@ export async function handleUpgradeCommand(version: string): Promise<void> {
version,
uiMode: CLI_UI_MODE,
});
exitCode = await handleUpgrade(version, { track, logger: log });
exitCode = await handleUpgrade(version, { track, logger: log, skipPrompt: yes });
} finally {
await shutdownTelemetry({ timeoutMs: CLI_SHUTDOWN_TIMEOUT_MS }).catch(() => {});
await harness.close().catch(() => {});
Expand Down Expand Up @@ -166,8 +166,8 @@ export function main(): void {
process.exit(1);
});
},
() => {
void handleUpgradeCommand(version).catch(async (error: unknown) => {
(yes) => {
void handleUpgradeCommand(version, yes).catch(async (error: unknown) => {
await logStartupFailure('upgrade', error);
process.stderr.write(formatStartupError(error, { operation: 'upgrade' }));
process.stderr.write(`See log: ${resolveGlobalLogPath(resolveKimiHome())}\n`);
Expand Down
24 changes: 24 additions & 0 deletions apps/kimi-code/test/cli/options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,30 @@ describe('CLI options parsing', () => {
expect(upgradeCalls).toBe(1);
});

it('routes upgrade --yes without calling the main action', () => {
let yesValue: boolean | undefined;
const program = createProgram(
'0.0.0',
() => {
throw new Error('main action should not run');
},
() => {},
() => {},
(yes) => {
yesValue = yes;
},
);
program.exitOverride();
program.configureOutput({
writeOut: () => {},
writeErr: () => {},
});

program.parse(['node', 'kimi', 'upgrade', '--yes']);

expect(yesValue).toBe(true);
});

it('registers the visible sub-commands', () => {
const program = createProgram(
'0.0.0',
Expand Down
32 changes: 32 additions & 0 deletions apps/kimi-code/test/cli/upgrade.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ function createDeps(overrides: {
readonly latest?: string | null;
readonly source?: InstallSource;
readonly isInteractive?: boolean;
readonly skipPrompt?: boolean;
readonly promptForInstallChoice?: () => Promise<InstallPromptChoiceValue>;
readonly installUpdate?: (source: InstallSource, version: string, platform: NodeJS.Platform) => Promise<void>;
} = {}) {
Expand All @@ -62,6 +63,7 @@ function createDeps(overrides: {
},
platform: 'darwin' as NodeJS.Platform,
isInteractive: overrides.isInteractive ?? true,
skipPrompt: overrides.skipPrompt ?? false,
};
}

Expand Down Expand Up @@ -165,6 +167,36 @@ describe('handleUpgrade', () => {
expect(stdout.join('')).toContain('To update manually, run: npm install -g @moonshot-ai/kimi-code@0.5.0');
});

it('installs without prompting when --yes is used and the source supports auto install', async () => {
const { stdout, writable } = captureOutput();
const deps = createDeps({ latest: '0.5.0', source: 'npm-global', skipPrompt: true });

await expect(handleUpgrade('0.4.0', { ...deps, ...writable })).resolves.toBe(0);

expect(deps.promptForInstallChoice).not.toHaveBeenCalled();
expect(deps.installUpdate).toHaveBeenCalledWith('npm-global', '0.5.0', 'darwin');
expect(deps.track).toHaveBeenCalledWith('upgrade_command_succeeded', expect.objectContaining({
target_version: '0.5.0',
source: 'npm-global',
}));
expect(stdout.join('')).toContain('Updated @moonshot-ai/kimi-code to 0.5.0');
});

it('prints the manual update command with --yes when the source cannot be auto-installed', async () => {
const { stdout, writable } = captureOutput();
const deps = createDeps({ latest: '0.5.0', source: 'unsupported', skipPrompt: true });

await expect(handleUpgrade('0.4.0', { ...deps, ...writable })).resolves.toBe(0);

expect(deps.promptForInstallChoice).not.toHaveBeenCalled();
expect(deps.installUpdate).not.toHaveBeenCalled();
expect(deps.track).toHaveBeenCalledWith('upgrade_command_manual_command', expect.objectContaining({
target_version: '0.5.0',
source: 'unsupported',
}));
expect(stdout.join('')).toContain('To update manually, run: npm install -g @moonshot-ai/kimi-code@0.5.0');
});

it('returns a failing exit code when the foreground install fails', async () => {
const { stderr, writable } = captureOutput();
const deps = createDeps({
Expand Down