From 8ca01f89d4df1ca942680a4619d69b90535b7056 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 1 Sep 2026 07:24:52 +0000 Subject: [PATCH] feat(coding-agent): silent background self-update on startup Check npm for a newer @southbag/code release on every startup and run the global package-manager update in the background with no output. Failures are ignored so startup stays seamless. Co-authored-by: Ingo Wolf --- packages/coding-agent/CHANGELOG.md | 4 + packages/coding-agent/src/main.ts | 2 + .../src/utils/startup-self-update.ts | 116 ++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 packages/coding-agent/src/utils/startup-self-update.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index cf0558a1..56fb86ee 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added silent background self-updates on every startup for global npm, pnpm, yarn, and bun installs. Pi checks npm for a newer release and updates in the background without blocking startup or showing output. + ### Removed - Removed interactive slash commands `/export`, `/import`, `/session`, `/changelog`, `/debug`, `/arminsayshi`, and `/dementedelves`. diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 6ad56a2b..c8c67411 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -44,6 +44,7 @@ import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.ts" import { handleConfigCommand, handlePackageCommand } from "./package-manager-cli.ts"; import { openBrowser } from "./utils/open-browser.ts"; import { isLocalPath, normalizePath, resolvePath } from "./utils/paths.ts"; +import { startSilentSelfUpdate } from "./utils/startup-self-update.ts"; const EXTENSION_LOAD_FAILURE_HINT = 'Hint: Start without extensions using "pi -ne".'; @@ -424,6 +425,7 @@ export async function main(args: string[], options?: MainOptions) { const bootstrapSettingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: false }); applyHttpProxySettings(bootstrapSettingsManager.getGlobalSettings().httpProxy); configureHttpDispatcher(); + startSilentSelfUpdate(bootstrapSettingsManager.getGlobalSettings().npmCommand); if (await handlePackageCommand(args, { extensionFactories: options?.extensionFactories })) { const exitCode = process.exitCode ?? 0; diff --git a/packages/coding-agent/src/utils/startup-self-update.ts b/packages/coding-agent/src/utils/startup-self-update.ts new file mode 100644 index 00000000..6007b2e3 --- /dev/null +++ b/packages/coding-agent/src/utils/startup-self-update.ts @@ -0,0 +1,116 @@ +import { compare, rcompare, valid } from "semver"; +import { + detectInstallMethod, + getPackageDir, + getSelfUpdateCommand, + PACKAGE_NAME, + type SelfUpdateCommand, + VERSION, +} from "../config.ts"; +import { spawnProcess, spawnProcessSync } from "./child-process.ts"; +import { cleanupWindowsSelfUpdateQuarantine, quarantineWindowsNativeDependencies } from "./windows-self-update.ts"; + +const VERSION_CHECK_TIMEOUT_MS = 10000; + +function getNpmCommand(npmCommand?: string[]): { command: string; args: string[] } { + if (!npmCommand || npmCommand.length === 0) { + return { command: "npm", args: [] }; + } + const [command, ...args] = npmCommand; + return { command: command || "npm", args }; +} + +export function isNewerPublishedVersion(latestVersion: string, currentVersion: string): boolean { + const latest = valid(latestVersion.trim()); + const current = valid(currentVersion.trim()); + if (latest && current) { + return compare(latest, current) > 0; + } + return latestVersion.trim() !== currentVersion.trim(); +} + +function parseNpmViewVersionOutput(raw: string): string | undefined { + const trimmed = raw.trim(); + if (!trimmed) { + return undefined; + } + try { + const parsed = JSON.parse(trimmed) as unknown; + if (typeof parsed === "string") { + return parsed; + } + if (Array.isArray(parsed)) { + const versions = parsed.filter((value): value is string => typeof value === "string" && value.length > 0); + return versions.sort(rcompare)[0]; + } + } catch { + return valid(trimmed) ?? undefined; + } + return undefined; +} + +function getLatestPublishedVersion(packageName: string, npmCommand?: string[]): string | undefined { + const npm = getNpmCommand(npmCommand); + const result = spawnProcessSync(npm.command, [...npm.args, "view", packageName, "version", "--json"], { + encoding: "utf-8", + stdio: ["ignore", "pipe", "pipe"], + timeout: VERSION_CHECK_TIMEOUT_MS, + }); + if (result.status !== 0) { + return undefined; + } + return parseNpmViewVersionOutput(result.stdout || result.stderr || ""); +} + +async function runSilentCommand(command: string, args: string[]): Promise { + await new Promise((resolve, reject) => { + const child = spawnProcess(command, args, { stdio: "ignore" }); + child.once("error", reject); + child.once("close", (code) => { + if (code === 0) { + resolve(); + return; + } + reject(new Error(`${command} ${args.join(" ")} exited with code ${code ?? "unknown"}`)); + }); + }); +} + +async function runSilentSelfUpdate(command: SelfUpdateCommand): Promise { + const installMethod = detectInstallMethod(); + if (process.platform === "win32" && installMethod === "npm") { + const packageDir = getPackageDir(); + cleanupWindowsSelfUpdateQuarantine(packageDir); + quarantineWindowsNativeDependencies(packageDir); + } + + for (const step of command.steps ?? [command]) { + await runSilentCommand(step.command, step.args); + } +} + +async function runSilentSelfUpdateIfNeeded(npmCommand?: string[]): Promise { + const latestVersion = getLatestPublishedVersion(PACKAGE_NAME, npmCommand); + if (!latestVersion || !isNewerPublishedVersion(latestVersion, VERSION)) { + return; + } + + const updateTarget = { + packageName: PACKAGE_NAME, + installSpec: `${PACKAGE_NAME}@${latestVersion}`, + }; + const selfUpdateCommand = getSelfUpdateCommand(PACKAGE_NAME, npmCommand, updateTarget); + if (!selfUpdateCommand) { + return; + } + + await runSilentSelfUpdate(selfUpdateCommand); +} + +/** + * Check npm for a newer pi release and update global package-manager installs in the background. + * Failures are ignored so startup stays seamless. + */ +export function startSilentSelfUpdate(npmCommand?: string[]): void { + void runSilentSelfUpdateIfNeeded(npmCommand).catch(() => {}); +}