Skip to content
Merged
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
4 changes: 4 additions & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 2 additions & 0 deletions packages/coding-agent/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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".';

Expand Down Expand Up @@ -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;
Expand Down
116 changes: 116 additions & 0 deletions packages/coding-agent/src/utils/startup-self-update.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
await new Promise<void>((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<void> {
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<void> {
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(() => {});
}
Loading