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,6 +1,7 @@
import { spawnSync } from "child_process";
import { existsSync, readFileSync } from "fs";
import { homedir } from "os";
import { dirname, join, resolve } from "path";
import { dirname, join, resolve, sep } from "path";
import { fileURLToPath } from "url";
// =============================================================================
@@ -26,6 +27,12 @@ export const isBunRuntime = !!process.versions.bun;
export type InstallMethod = "bun-binary" | "npm" | "pnpm" | "yarn" | "bun" | "unknown";
export interface SelfUpdateCommand {
command: string;
args: string[];
display: string;
}
export function detectInstallMethod(): InstallMethod {
if (isBunBinary) {
return "bun-binary";
@@ -49,24 +56,125 @@ export function detectInstallMethod(): InstallMethod {
return "unknown";
}
export function getUpdateInstruction(packageName: string): string {
const method = detectInstallMethod();
function getSelfUpdateCommandForMethod(method: InstallMethod, packageName: string): SelfUpdateCommand | undefined {
switch (method) {
case "bun-binary":
return `Download from: https://github.com/badlogic/pi-mono/releases/latest`;
return undefined;
case "pnpm":
return `Run: pnpm install -g ${packageName}`;
return {
command: "pnpm",
args: ["install", "-g", packageName],
display: `pnpm install -g ${packageName}`,
};
case "yarn":
return `Run: yarn global add ${packageName}`;
return {
command: "yarn",
args: ["global", "add", packageName],
display: `yarn global add ${packageName}`,
};
case "bun":
return `Run: bun install -g ${packageName}`;
return {
command: "bun",
args: ["install", "-g", packageName],
display: `bun install -g ${packageName}`,
};
case "npm":
return `Run: npm install -g ${packageName}`;
default:
return `Run: npm install -g ${packageName}`;
return {
command: "npm",
args: ["install", "-g", packageName],
display: `npm install -g ${packageName}`,
};
case "unknown":
return undefined;
}
}
function readCommandOutput(command: string, args: string[]): string | undefined {
const result = spawnSync(command, args, {
encoding: "utf-8",
stdio: ["ignore", "pipe", "ignore"],
timeout: 2000,
});
if (result.status !== 0) return undefined;
const stdout = result.stdout.trim();
return stdout || undefined;
}
function getGlobalPackageRoots(method: InstallMethod): string[] {
switch (method) {
case "npm": {
const root = readCommandOutput("npm", ["root", "-g"]);
return root ? [root] : [];
}
case "pnpm": {
const root = readCommandOutput("pnpm", ["root", "-g"]);
return root ? [root, dirname(root)] : [];
}
case "yarn": {
const dir = readCommandOutput("yarn", ["global", "dir"]);
return dir ? [dir, join(dir, "node_modules")] : [];
}
case "bun": {
const bunBin = readCommandOutput("bun", ["pm", "bin", "-g"]);
const roots = [join(homedir(), ".bun", "install", "global", "node_modules")];
if (bunBin) {
roots.push(join(dirname(dirname(bunBin)), "install", "global", "node_modules"));
}
return roots;
}
case "bun-binary":
case "unknown":
return [];
}
}
function isManagedByGlobalPackageManager(method: InstallMethod): boolean {
let packageDir = resolve(getPackageDir());
if (process.platform === "win32") {
packageDir = packageDir.toLowerCase();
}
return getGlobalPackageRoots(method).some((root) => {
let normalizedRoot = resolve(root);
if (process.platform === "win32") {
normalizedRoot = normalizedRoot.toLowerCase();
}
return (
existsSync(normalizedRoot) &&
(packageDir === normalizedRoot ||
packageDir.startsWith(normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`))
);
});
}
export function getSelfUpdateCommand(packageName: string): SelfUpdateCommand | undefined {
const method = detectInstallMethod();
const command = getSelfUpdateCommandForMethod(method, packageName);
if (!command || !isManagedByGlobalPackageManager(method)) {
return undefined;
}
return command;
}
export function getSelfUpdateUnavailableInstruction(packageName: string): string {
const method = detectInstallMethod();
if (method === "bun-binary") {
return `Download from: https://github.com/badlogic/pi-mono/releases/latest`;
}
if (getSelfUpdateCommandForMethod(method, packageName)) {
return `This installation is not managed by a global ${method} install. Update it with the package manager, wrapper, or source checkout that provides it.`;
}
return `Update ${packageName} using the package manager, wrapper, or source checkout that provides this installation.`;
}
export function getUpdateInstruction(packageName: string): string {
const method = detectInstallMethod();
const command = getSelfUpdateCommandForMethod(method, packageName);
if (command) {
return `Run: ${command.display}`;
}
return getSelfUpdateUnavailableInstruction(packageName);
}
// =============================================================================
// Package Asset Paths (shipped with executable)
// =============================================================================
@@ -182,13 +290,23 @@ export function getBundledInteractiveAssetPath(name: string): string {
// App Config (from package.json piConfig)
// =============================================================================
const pkg = JSON.parse(readFileSync(getPackageJsonPath(), "utf-8"));
interface PackageJson {
name?: string;
version?: string;
piConfig?: {
name?: string;
configDir?: string;
};
}
const pkg = JSON.parse(readFileSync(getPackageJsonPath(), "utf-8")) as PackageJson;
const piConfigName: string | undefined = pkg.piConfig?.name;
export const PACKAGE_NAME: string = pkg.name || "@mariozechner/pi-coding-agent";
export const APP_NAME: string = piConfigName || "pi";
export const APP_TITLE: string = piConfigName ? APP_NAME : "π";
export const CONFIG_DIR_NAME: string = pkg.piConfig?.configDir || ".pi";
export const VERSION: string = pkg.version;
export const VERSION: string = pkg.version || "0.0.0";
// e.g., PI_CODING_AGENT_DIR or TAU_CODING_AGENT_DIR
export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;