feat(coding-agent): Add built-in update command (#3680)

This commit is contained in:
Armin Ronacher
2026-04-25 13:09:33 +02:00
committed by GitHub
parent 0b271a2c4f
commit dcf2651631
7 changed files with 372 additions and 35 deletions

View File

@@ -1,17 +1,30 @@
import chalk from "chalk";
import { spawn } from "child_process";
import { selectConfig } from "./cli/config-selector.js";
import { APP_NAME, getAgentDir } from "./config.js";
import {
APP_NAME,
getAgentDir,
getSelfUpdateCommand,
getSelfUpdateUnavailableInstruction,
PACKAGE_NAME,
} from "./config.js";
import { DefaultPackageManager } from "./core/package-manager.js";
import { SettingsManager } from "./core/settings-manager.js";
export type PackageCommand = "install" | "remove" | "update" | "list";
type UpdateTarget = { type: "all" } | { type: "self" } | { type: "extensions"; source?: string };
interface PackageCommandOptions {
command: PackageCommand;
source?: string;
updateTarget?: UpdateTarget;
local: boolean;
help: boolean;
invalidOption?: string;
invalidArgument?: string;
missingOptionValue?: string;
conflictingOptions?: string;
}
function reportSettingsErrors(settingsManager: SettingsManager, context: string): void {
@@ -31,7 +44,7 @@ function getPackageCommandUsage(command: PackageCommand): string {
case "remove":
return `${APP_NAME} remove <source> [-l]`;
case "update":
return `${APP_NAME} update [source]`;
return `${APP_NAME} update [source|self|pi] [--self] [--extensions] [--extension <source>]`;
case "list":
return `${APP_NAME} list`;
}
@@ -78,8 +91,17 @@ Examples:
console.log(`${chalk.bold("Usage:")}
${getPackageCommandUsage("update")}
Update installed packages.
If <source> is provided, only that package is updated.
Update pi and installed packages.
Options:
--self Update pi only
--extensions Update installed packages only
--extension <source> Update one package only
Short forms:
${APP_NAME} update Update pi and all extensions
${APP_NAME} update <source> Update one package
${APP_NAME} update pi Update pi only (self works as alias to pi)
`);
return;
@@ -108,9 +130,16 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined
let local = false;
let help = false;
let invalidOption: string | undefined;
let invalidArgument: string | undefined;
let missingOptionValue: string | undefined;
let conflictingOptions: string | undefined;
let source: string | undefined;
let selfFlag = false;
let extensionsFlag = false;
let extensionFlagSource: string | undefined;
for (const arg of rest) {
for (let index = 0; index < rest.length; index++) {
const arg = rest[index];
if (arg === "-h" || arg === "--help") {
help = true;
continue;
@@ -125,6 +154,43 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined
continue;
}
if (arg === "--self") {
if (command === "update") {
selfFlag = true;
} else {
invalidOption = invalidOption ?? arg;
}
continue;
}
if (arg === "--extensions") {
if (command === "update") {
extensionsFlag = true;
} else {
invalidOption = invalidOption ?? arg;
}
continue;
}
if (arg === "--extension") {
if (command !== "update") {
invalidOption = invalidOption ?? arg;
continue;
}
const value = rest[index + 1];
if (!value || value.startsWith("-")) {
missingOptionValue = missingOptionValue ?? arg;
} else if (extensionFlagSource) {
conflictingOptions = conflictingOptions ?? "--extension can only be provided once";
index++;
} else {
extensionFlagSource = value;
index++;
}
continue;
}
if (arg.startsWith("-")) {
invalidOption = invalidOption ?? arg;
continue;
@@ -132,10 +198,103 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined
if (!source) {
source = arg;
} else {
invalidArgument = invalidArgument ?? arg;
}
}
return { command, source, local, help, invalidOption };
let updateTarget: UpdateTarget | undefined;
if (command === "update") {
if (extensionFlagSource) {
if (selfFlag || extensionsFlag) {
conflictingOptions = conflictingOptions ?? "--extension cannot be combined with --self or --extensions";
}
if (source) {
conflictingOptions = conflictingOptions ?? "--extension cannot be combined with a positional source";
}
updateTarget = { type: "extensions", source: extensionFlagSource };
} else if (source) {
const sourceIsSelf = source === "self" || source === "pi";
if (sourceIsSelf) {
updateTarget = extensionsFlag ? { type: "all" } : { type: "self" };
} else {
if (extensionsFlag || selfFlag) {
conflictingOptions =
conflictingOptions ?? "positional update targets cannot be combined with --self or --extensions";
}
updateTarget = { type: "extensions", source };
}
} else if (selfFlag && extensionsFlag) {
updateTarget = { type: "all" };
} else if (selfFlag) {
updateTarget = { type: "self" };
} else if (extensionsFlag) {
updateTarget = { type: "extensions" };
} else {
updateTarget = { type: "all" };
}
}
return {
command,
source,
updateTarget,
local,
help,
invalidOption,
invalidArgument,
missingOptionValue,
conflictingOptions,
};
}
function updateTargetIncludesSelf(target: UpdateTarget): boolean {
return target.type === "all" || target.type === "self";
}
function updateTargetIncludesExtensions(target: UpdateTarget): boolean {
return target.type === "all" || target.type === "extensions";
}
function canSelfUpdate(): boolean {
return getSelfUpdateCommand(PACKAGE_NAME) !== undefined;
}
function printSelfUpdateUnavailable(): void {
console.error(`error: ${APP_NAME} cannot self-update this installation.`);
console.error(getSelfUpdateUnavailableInstruction(PACKAGE_NAME));
const entrypoint = process.argv[1];
if (entrypoint) {
console.error("");
console.error(`Location of pi executable: ${entrypoint}`);
}
}
async function runSelfUpdate(): Promise<void> {
const command = getSelfUpdateCommand(PACKAGE_NAME);
if (!command) {
throw new Error(
`${APP_NAME} cannot self-update this installation. ${getSelfUpdateUnavailableInstruction(PACKAGE_NAME)}`,
);
}
console.log(chalk.dim(`Updating ${APP_NAME} with ${command.display}...`));
await new Promise<void>((resolve, reject) => {
const child = spawn(command.command, command.args, { stdio: "inherit" });
child.on("error", (error) => {
reject(error);
});
child.on("close", (code, signal) => {
if (code === 0) {
resolve();
} else if (signal) {
reject(new Error(`${command.display} terminated by signal ${signal}`));
} else {
reject(new Error(`${command.display} exited with code ${code ?? "unknown"}`));
}
});
});
}
export async function handleConfigCommand(args: string[]): Promise<boolean> {
@@ -178,6 +337,27 @@ export async function handlePackageCommand(args: string[]): Promise<boolean> {
return true;
}
if (options.missingOptionValue) {
console.error(chalk.red(`Missing value for ${options.missingOptionValue}.`));
console.error(chalk.dim(`Usage: ${getPackageCommandUsage(options.command)}`));
process.exitCode = 1;
return true;
}
if (options.invalidArgument) {
console.error(chalk.red(`Unexpected argument ${options.invalidArgument}.`));
console.error(chalk.dim(`Usage: ${getPackageCommandUsage(options.command)}`));
process.exitCode = 1;
return true;
}
if (options.conflictingOptions) {
console.error(chalk.red(options.conflictingOptions));
console.error(chalk.dim(`Usage: ${getPackageCommandUsage(options.command)}`));
process.exitCode = 1;
return true;
}
const source = options.source;
if ((options.command === "install" || options.command === "remove") && !source) {
console.error(chalk.red(`Missing ${options.command} source.`));
@@ -186,6 +366,12 @@ export async function handlePackageCommand(args: string[]): Promise<boolean> {
return true;
}
if (options.command === "update" && options.updateTarget?.type === "self" && !canSelfUpdate()) {
printSelfUpdateUnavailable();
process.exitCode = 1;
return true;
}
const cwd = process.cwd();
const agentDir = getAgentDir();
const settingsManager = SettingsManager.create(cwd, agentDir);
@@ -252,14 +438,28 @@ export async function handlePackageCommand(args: string[]): Promise<boolean> {
return true;
}
case "update":
await packageManager.update(source);
if (source) {
console.log(chalk.green(`Updated ${source}`));
} else {
console.log(chalk.green("Updated packages"));
case "update": {
const target = options.updateTarget ?? { type: "all" };
if (updateTargetIncludesExtensions(target)) {
const updateSource = target.type === "extensions" ? target.source : undefined;
await packageManager.update(updateSource);
if (updateSource) {
console.log(chalk.green(`Updated ${updateSource}`));
} else {
console.log(chalk.green("Updated packages"));
}
}
if (updateTargetIncludesSelf(target)) {
if (canSelfUpdate()) {
await runSelfUpdate();
console.log(chalk.green(`Updated ${APP_NAME}`));
} else {
printSelfUpdateUnavailable();
process.exitCode = 1;
}
}
return true;
}
}
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown package command error";