feat(coding-agent): Add built-in update command (#3680)
This commit is contained in:
@@ -381,7 +381,10 @@ pi install ssh://git@github.com/user/repo@v1 # tag or commit
|
|||||||
pi remove npm:@foo/pi-tools
|
pi remove npm:@foo/pi-tools
|
||||||
pi uninstall npm:@foo/pi-tools # alias for remove
|
pi uninstall npm:@foo/pi-tools # alias for remove
|
||||||
pi list
|
pi list
|
||||||
pi update # skips pinned packages
|
pi update # update pi and packages (skips pinned packages)
|
||||||
|
pi update --extensions # update packages only
|
||||||
|
pi update --self # update pi only
|
||||||
|
pi update npm:@foo/pi-tools # update one package
|
||||||
pi config # enable/disable extensions, skills, prompts, themes
|
pi config # enable/disable extensions, skills, prompts, themes
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -476,7 +479,10 @@ pi [options] [@files...] [messages...]
|
|||||||
pi install <source> [-l] # Install package, -l for project-local
|
pi install <source> [-l] # Install package, -l for project-local
|
||||||
pi remove <source> [-l] # Remove package
|
pi remove <source> [-l] # Remove package
|
||||||
pi uninstall <source> [-l] # Alias for remove
|
pi uninstall <source> [-l] # Alias for remove
|
||||||
pi update [source] # Update packages (skips pinned)
|
pi update [source|self|pi] # Update pi and packages (skips pinned packages)
|
||||||
|
pi update --extensions # Update packages only
|
||||||
|
pi update --self # Update pi only
|
||||||
|
pi update --extension <src> # Update one package
|
||||||
pi list # List installed packages
|
pi list # List installed packages
|
||||||
pi config # Enable/disable package resources
|
pi config # Enable/disable package resources
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -27,8 +27,12 @@ pi install /absolute/path/to/package
|
|||||||
pi install ./relative/path/to/package
|
pi install ./relative/path/to/package
|
||||||
|
|
||||||
pi remove npm:@foo/bar
|
pi remove npm:@foo/bar
|
||||||
pi list # show installed packages from settings
|
pi list # show installed packages from settings
|
||||||
pi update # update all non-pinned packages
|
pi update # update pi and all non-pinned packages
|
||||||
|
pi update --extensions # update all non-pinned packages only
|
||||||
|
pi update --self # update pi only
|
||||||
|
pi update npm:@foo/bar # update one package
|
||||||
|
pi update --extension npm:@foo/bar
|
||||||
```
|
```
|
||||||
|
|
||||||
By default, `install` and `remove` write to global settings (`~/.pi/agent/settings.json`). Use `-l` to write to project settings (`.pi/settings.json`) instead. Project settings can be shared with your team, and pi installs any missing packages automatically on startup.
|
By default, `install` and `remove` write to global settings (`~/.pi/agent/settings.json`). Use `-l` to write to project settings (`.pi/settings.json`) instead. Project settings can be shared with your team, and pi installs any missing packages automatically on startup.
|
||||||
@@ -51,7 +55,7 @@ npm:@scope/pkg@1.2.3
|
|||||||
npm:pkg
|
npm:pkg
|
||||||
```
|
```
|
||||||
|
|
||||||
- Versioned specs are pinned and skipped by `pi update`.
|
- Versioned specs are pinned and skipped by package updates (`pi update`, `pi update --extensions`).
|
||||||
- Global installs use `npm install -g`.
|
- Global installs use `npm install -g`.
|
||||||
- Project installs go under `.pi/npm/`.
|
- Project installs go under `.pi/npm/`.
|
||||||
- Set `npmCommand` in `settings.json` to pin npm package lookup and install operations to a specific wrapper command such as `mise` or `asdf`.
|
- Set `npmCommand` in `settings.json` to pin npm package lookup and install operations to a specific wrapper command such as `mise` or `asdf`.
|
||||||
@@ -78,7 +82,7 @@ ssh://git@github.com/user/repo@v1
|
|||||||
- HTTPS and SSH URLs are both supported.
|
- HTTPS and SSH URLs are both supported.
|
||||||
- SSH URLs use your configured SSH keys automatically (respects `~/.ssh/config`).
|
- SSH URLs use your configured SSH keys automatically (respects `~/.ssh/config`).
|
||||||
- For non-interactive runs (for example CI), you can set `GIT_TERMINAL_PROMPT=0` to disable credential prompts and set `GIT_SSH_COMMAND` (for example `ssh -o BatchMode=yes -o ConnectTimeout=5`) to fail fast.
|
- For non-interactive runs (for example CI), you can set `GIT_TERMINAL_PROMPT=0` to disable credential prompts and set `GIT_SSH_COMMAND` (for example `ssh -o BatchMode=yes -o ConnectTimeout=5`) to fail fast.
|
||||||
- Refs pin the package and skip `pi update`.
|
- Refs pin the package and skip package updates (`pi update`, `pi update --extensions`).
|
||||||
- Cloned to `~/.pi/agent/git/<host>/<path>` (global) or `.pi/git/<host>/<path>` (project).
|
- Cloned to `~/.pi/agent/git/<host>/<path>` (global) or `.pi/git/<host>/<path>` (project).
|
||||||
- Runs `npm install` after clone or pull if `package.json` exists.
|
- Runs `npm install` after clone or pull if `package.json` exists.
|
||||||
|
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ ${chalk.bold("Commands:")}
|
|||||||
${APP_NAME} install <source> [-l] Install extension source and add to settings
|
${APP_NAME} install <source> [-l] Install extension source and add to settings
|
||||||
${APP_NAME} remove <source> [-l] Remove extension source from settings
|
${APP_NAME} remove <source> [-l] Remove extension source from settings
|
||||||
${APP_NAME} uninstall <source> [-l] Alias for remove
|
${APP_NAME} uninstall <source> [-l] Alias for remove
|
||||||
${APP_NAME} update [source] Update installed extensions (skips pinned sources)
|
${APP_NAME} update [source|self|pi] Update pi and installed extensions
|
||||||
${APP_NAME} list List installed extensions from settings
|
${APP_NAME} list List installed extensions from settings
|
||||||
${APP_NAME} config Open TUI to enable/disable package resources
|
${APP_NAME} config Open TUI to enable/disable package resources
|
||||||
${APP_NAME} <command> --help Show help for install/remove/uninstall/update/list
|
${APP_NAME} <command> --help Show help for install/remove/uninstall/update/list
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { spawnSync } from "child_process";
|
||||||
import { existsSync, readFileSync } from "fs";
|
import { existsSync, readFileSync } from "fs";
|
||||||
import { homedir } from "os";
|
import { homedir } from "os";
|
||||||
import { dirname, join, resolve } from "path";
|
import { dirname, join, resolve, sep } from "path";
|
||||||
import { fileURLToPath } from "url";
|
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 type InstallMethod = "bun-binary" | "npm" | "pnpm" | "yarn" | "bun" | "unknown";
|
||||||
|
|
||||||
|
export interface SelfUpdateCommand {
|
||||||
|
command: string;
|
||||||
|
args: string[];
|
||||||
|
display: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function detectInstallMethod(): InstallMethod {
|
export function detectInstallMethod(): InstallMethod {
|
||||||
if (isBunBinary) {
|
if (isBunBinary) {
|
||||||
return "bun-binary";
|
return "bun-binary";
|
||||||
@@ -49,24 +56,125 @@ export function detectInstallMethod(): InstallMethod {
|
|||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUpdateInstruction(packageName: string): string {
|
function getSelfUpdateCommandForMethod(method: InstallMethod, packageName: string): SelfUpdateCommand | undefined {
|
||||||
const method = detectInstallMethod();
|
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case "bun-binary":
|
case "bun-binary":
|
||||||
return `Download from: https://github.com/badlogic/pi-mono/releases/latest`;
|
return undefined;
|
||||||
case "pnpm":
|
case "pnpm":
|
||||||
return `Run: pnpm install -g ${packageName}`;
|
return {
|
||||||
|
command: "pnpm",
|
||||||
|
args: ["install", "-g", packageName],
|
||||||
|
display: `pnpm install -g ${packageName}`,
|
||||||
|
};
|
||||||
case "yarn":
|
case "yarn":
|
||||||
return `Run: yarn global add ${packageName}`;
|
return {
|
||||||
|
command: "yarn",
|
||||||
|
args: ["global", "add", packageName],
|
||||||
|
display: `yarn global add ${packageName}`,
|
||||||
|
};
|
||||||
case "bun":
|
case "bun":
|
||||||
return `Run: bun install -g ${packageName}`;
|
return {
|
||||||
|
command: "bun",
|
||||||
|
args: ["install", "-g", packageName],
|
||||||
|
display: `bun install -g ${packageName}`,
|
||||||
|
};
|
||||||
case "npm":
|
case "npm":
|
||||||
return `Run: npm install -g ${packageName}`;
|
return {
|
||||||
default:
|
command: "npm",
|
||||||
return `Run: npm install -g ${packageName}`;
|
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)
|
// Package Asset Paths (shipped with executable)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -182,13 +290,23 @@ export function getBundledInteractiveAssetPath(name: string): string {
|
|||||||
// App Config (from package.json piConfig)
|
// 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;
|
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_NAME: string = piConfigName || "pi";
|
||||||
export const APP_TITLE: string = piConfigName ? APP_NAME : "π";
|
export const APP_TITLE: string = piConfigName ? APP_NAME : "π";
|
||||||
export const CONFIG_DIR_NAME: string = pkg.piConfig?.configDir || ".pi";
|
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
|
// e.g., PI_CODING_AGENT_DIR or TAU_CODING_AGENT_DIR
|
||||||
export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;
|
export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ import {
|
|||||||
getDebugLogPath,
|
getDebugLogPath,
|
||||||
getDocsPath,
|
getDocsPath,
|
||||||
getShareViewerUrl,
|
getShareViewerUrl,
|
||||||
getUpdateInstruction,
|
|
||||||
VERSION,
|
VERSION,
|
||||||
} from "../../config.js";
|
} from "../../config.js";
|
||||||
import { type AgentSession, type AgentSessionEvent, parseSkillBlock } from "../../core/agent-session.js";
|
import { type AgentSession, type AgentSessionEvent, parseSkillBlock } from "../../core/agent-session.js";
|
||||||
@@ -3515,8 +3514,8 @@ export class InteractiveMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showNewVersionNotification(newVersion: string): void {
|
showNewVersionNotification(newVersion: string): void {
|
||||||
const action = theme.fg("accent", getUpdateInstruction("@mariozechner/pi-coding-agent"));
|
const action = theme.fg("accent", `${APP_NAME} update`);
|
||||||
const updateInstruction = theme.fg("muted", `New version ${newVersion} is available. `) + action;
|
const updateInstruction = theme.fg("muted", `New version ${newVersion} is available. Run `) + action;
|
||||||
const changelogUrl = theme.fg(
|
const changelogUrl = theme.fg(
|
||||||
"accent",
|
"accent",
|
||||||
"https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md",
|
"https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md",
|
||||||
|
|||||||
@@ -1,17 +1,30 @@
|
|||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
|
import { spawn } from "child_process";
|
||||||
import { selectConfig } from "./cli/config-selector.js";
|
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 { DefaultPackageManager } from "./core/package-manager.js";
|
||||||
import { SettingsManager } from "./core/settings-manager.js";
|
import { SettingsManager } from "./core/settings-manager.js";
|
||||||
|
|
||||||
export type PackageCommand = "install" | "remove" | "update" | "list";
|
export type PackageCommand = "install" | "remove" | "update" | "list";
|
||||||
|
|
||||||
|
type UpdateTarget = { type: "all" } | { type: "self" } | { type: "extensions"; source?: string };
|
||||||
|
|
||||||
interface PackageCommandOptions {
|
interface PackageCommandOptions {
|
||||||
command: PackageCommand;
|
command: PackageCommand;
|
||||||
source?: string;
|
source?: string;
|
||||||
|
updateTarget?: UpdateTarget;
|
||||||
local: boolean;
|
local: boolean;
|
||||||
help: boolean;
|
help: boolean;
|
||||||
invalidOption?: string;
|
invalidOption?: string;
|
||||||
|
invalidArgument?: string;
|
||||||
|
missingOptionValue?: string;
|
||||||
|
conflictingOptions?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function reportSettingsErrors(settingsManager: SettingsManager, context: string): void {
|
function reportSettingsErrors(settingsManager: SettingsManager, context: string): void {
|
||||||
@@ -31,7 +44,7 @@ function getPackageCommandUsage(command: PackageCommand): string {
|
|||||||
case "remove":
|
case "remove":
|
||||||
return `${APP_NAME} remove <source> [-l]`;
|
return `${APP_NAME} remove <source> [-l]`;
|
||||||
case "update":
|
case "update":
|
||||||
return `${APP_NAME} update [source]`;
|
return `${APP_NAME} update [source|self|pi] [--self] [--extensions] [--extension <source>]`;
|
||||||
case "list":
|
case "list":
|
||||||
return `${APP_NAME} list`;
|
return `${APP_NAME} list`;
|
||||||
}
|
}
|
||||||
@@ -78,8 +91,17 @@ Examples:
|
|||||||
console.log(`${chalk.bold("Usage:")}
|
console.log(`${chalk.bold("Usage:")}
|
||||||
${getPackageCommandUsage("update")}
|
${getPackageCommandUsage("update")}
|
||||||
|
|
||||||
Update installed packages.
|
Update pi and installed packages.
|
||||||
If <source> is provided, only that package is updated.
|
|
||||||
|
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;
|
return;
|
||||||
|
|
||||||
@@ -108,9 +130,16 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined
|
|||||||
let local = false;
|
let local = false;
|
||||||
let help = false;
|
let help = false;
|
||||||
let invalidOption: string | undefined;
|
let invalidOption: string | undefined;
|
||||||
|
let invalidArgument: string | undefined;
|
||||||
|
let missingOptionValue: string | undefined;
|
||||||
|
let conflictingOptions: string | undefined;
|
||||||
let source: 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") {
|
if (arg === "-h" || arg === "--help") {
|
||||||
help = true;
|
help = true;
|
||||||
continue;
|
continue;
|
||||||
@@ -125,6 +154,43 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined
|
|||||||
continue;
|
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("-")) {
|
if (arg.startsWith("-")) {
|
||||||
invalidOption = invalidOption ?? arg;
|
invalidOption = invalidOption ?? arg;
|
||||||
continue;
|
continue;
|
||||||
@@ -132,10 +198,103 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined
|
|||||||
|
|
||||||
if (!source) {
|
if (!source) {
|
||||||
source = arg;
|
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> {
|
export async function handleConfigCommand(args: string[]): Promise<boolean> {
|
||||||
@@ -178,6 +337,27 @@ export async function handlePackageCommand(args: string[]): Promise<boolean> {
|
|||||||
return true;
|
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;
|
const source = options.source;
|
||||||
if ((options.command === "install" || options.command === "remove") && !source) {
|
if ((options.command === "install" || options.command === "remove") && !source) {
|
||||||
console.error(chalk.red(`Missing ${options.command} source.`));
|
console.error(chalk.red(`Missing ${options.command} source.`));
|
||||||
@@ -186,6 +366,12 @@ export async function handlePackageCommand(args: string[]): Promise<boolean> {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.command === "update" && options.updateTarget?.type === "self" && !canSelfUpdate()) {
|
||||||
|
printSelfUpdateUnavailable();
|
||||||
|
process.exitCode = 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
const agentDir = getAgentDir();
|
const agentDir = getAgentDir();
|
||||||
const settingsManager = SettingsManager.create(cwd, agentDir);
|
const settingsManager = SettingsManager.create(cwd, agentDir);
|
||||||
@@ -252,14 +438,28 @@ export async function handlePackageCommand(args: string[]): Promise<boolean> {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "update":
|
case "update": {
|
||||||
await packageManager.update(source);
|
const target = options.updateTarget ?? { type: "all" };
|
||||||
if (source) {
|
if (updateTargetIncludesExtensions(target)) {
|
||||||
console.log(chalk.green(`Updated ${source}`));
|
const updateSource = target.type === "extensions" ? target.source : undefined;
|
||||||
} else {
|
await packageManager.update(updateSource);
|
||||||
console.log(chalk.green("Updated packages"));
|
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;
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : "Unknown package command error";
|
const message = error instanceof Error ? error.message : "Unknown package command error";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { afterEach, describe, expect, test } from "vitest";
|
import { afterEach, describe, expect, test } from "vitest";
|
||||||
import { detectInstallMethod, getUpdateInstruction } from "../src/config.js";
|
import { detectInstallMethod, getSelfUpdateCommand, getUpdateInstruction } from "../src/config.js";
|
||||||
|
|
||||||
const execPathDescriptor = Object.getOwnPropertyDescriptor(process, "execPath");
|
const execPathDescriptor = Object.getOwnPropertyDescriptor(process, "execPath");
|
||||||
|
|
||||||
@@ -27,4 +27,14 @@ describe("detectInstallMethod", () => {
|
|||||||
"Run: pnpm install -g @mariozechner/pi-coding-agent",
|
"Run: pnpm install -g @mariozechner/pi-coding-agent",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("does not self-update unknown wrapper installs", () => {
|
||||||
|
setExecPath("/usr/local/bin/node");
|
||||||
|
|
||||||
|
expect(detectInstallMethod()).toBe("unknown");
|
||||||
|
expect(getSelfUpdateCommand("@mariozechner/pi-coding-agent")).toBeUndefined();
|
||||||
|
expect(getUpdateInstruction("@mariozechner/pi-coding-agent")).toBe(
|
||||||
|
"Update @mariozechner/pi-coding-agent using the package manager, wrapper, or source checkout that provides this installation.",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user