Files
pi/packages/coding-agent/src/package-manager-cli.ts
T
Mario Zechner 9f9277ccdd refactor(coding-agent): replace AgentSessionRuntimeHost with closure-based AgentSessionRuntime
- Replace AgentSessionRuntimeHost and bootstrap abstractions with AgentSessionRuntime
- Runtime creation is now closure-based via CreateAgentSessionRuntimeFactory
- Factory closes over process-global fixed inputs, recreates cwd-bound services per effective cwd
- Session config (model, thinking, tools, scoped models) re-resolved per target cwd
- CLI resource paths resolved once at startup as absolute paths
- Swap lifecycle: teardown old, create next, apply next (hard fail on creation error)
- Unified diagnostics model (info/warning/error) for args, services, session resolution, resources
- No logging or process exits inside creation/parsing logic
- Removed session_directory support
- Removed session_switch and session_fork extension events (use session_start with reason)
- Moved package/config CLI to package-manager-cli.ts
- Fixed theme init for --resume session picker
- Fixed flaky reftable footer test (content-based polling)
- Fixed silent drop of unknown single-dash CLI flags
- Added error diagnostics for missing explicit CLI resource paths
- Updated SDK docs, examples, plans, exports, tests, changelog

fixes #2753
2026-04-03 20:14:12 +02:00

271 lines
7.1 KiB
TypeScript

import chalk from "chalk";
import { selectConfig } from "./cli/config-selector.js";
import { APP_NAME, getAgentDir } from "./config.js";
import { DefaultPackageManager } from "./core/package-manager.js";
import { SettingsManager } from "./core/settings-manager.js";
export type PackageCommand = "install" | "remove" | "update" | "list";
interface PackageCommandOptions {
command: PackageCommand;
source?: string;
local: boolean;
help: boolean;
invalidOption?: string;
}
function reportSettingsErrors(settingsManager: SettingsManager, context: string): void {
const errors = settingsManager.drainErrors();
for (const { scope, error } of errors) {
console.error(chalk.yellow(`Warning (${context}, ${scope} settings): ${error.message}`));
if (error.stack) {
console.error(chalk.dim(error.stack));
}
}
}
function getPackageCommandUsage(command: PackageCommand): string {
switch (command) {
case "install":
return `${APP_NAME} install <source> [-l]`;
case "remove":
return `${APP_NAME} remove <source> [-l]`;
case "update":
return `${APP_NAME} update [source]`;
case "list":
return `${APP_NAME} list`;
}
}
function printPackageCommandHelp(command: PackageCommand): void {
switch (command) {
case "install":
console.log(`${chalk.bold("Usage:")}
${getPackageCommandUsage("install")}
Install a package and add it to settings.
Options:
-l, --local Install project-locally (.pi/settings.json)
Examples:
${APP_NAME} install npm:@foo/bar
${APP_NAME} install git:github.com/user/repo
${APP_NAME} install git:git@github.com:user/repo
${APP_NAME} install https://github.com/user/repo
${APP_NAME} install ssh://git@github.com/user/repo
${APP_NAME} install ./local/path
`);
return;
case "remove":
console.log(`${chalk.bold("Usage:")}
${getPackageCommandUsage("remove")}
Remove a package and its source from settings.
Alias: ${APP_NAME} uninstall <source> [-l]
Options:
-l, --local Remove from project settings (.pi/settings.json)
Examples:
${APP_NAME} remove npm:@foo/bar
${APP_NAME} uninstall npm:@foo/bar
`);
return;
case "update":
console.log(`${chalk.bold("Usage:")}
${getPackageCommandUsage("update")}
Update installed packages.
If <source> is provided, only that package is updated.
`);
return;
case "list":
console.log(`${chalk.bold("Usage:")}
${getPackageCommandUsage("list")}
List installed packages from user and project settings.
`);
return;
}
}
function parsePackageCommand(args: string[]): PackageCommandOptions | undefined {
const [rawCommand, ...rest] = args;
let command: PackageCommand | undefined;
if (rawCommand === "uninstall") {
command = "remove";
} else if (rawCommand === "install" || rawCommand === "remove" || rawCommand === "update" || rawCommand === "list") {
command = rawCommand;
}
if (!command) {
return undefined;
}
let local = false;
let help = false;
let invalidOption: string | undefined;
let source: string | undefined;
for (const arg of rest) {
if (arg === "-h" || arg === "--help") {
help = true;
continue;
}
if (arg === "-l" || arg === "--local") {
if (command === "install" || command === "remove") {
local = true;
} else {
invalidOption = invalidOption ?? arg;
}
continue;
}
if (arg.startsWith("-")) {
invalidOption = invalidOption ?? arg;
continue;
}
if (!source) {
source = arg;
}
}
return { command, source, local, help, invalidOption };
}
export async function handleConfigCommand(args: string[]): Promise<boolean> {
if (args[0] !== "config") {
return false;
}
const cwd = process.cwd();
const agentDir = getAgentDir();
const settingsManager = SettingsManager.create(cwd, agentDir);
reportSettingsErrors(settingsManager, "config command");
const packageManager = new DefaultPackageManager({ cwd, agentDir, settingsManager });
const resolvedPaths = await packageManager.resolve();
await selectConfig({
resolvedPaths,
settingsManager,
cwd,
agentDir,
});
process.exit(0);
}
export async function handlePackageCommand(args: string[]): Promise<boolean> {
const options = parsePackageCommand(args);
if (!options) {
return false;
}
if (options.help) {
printPackageCommandHelp(options.command);
return true;
}
if (options.invalidOption) {
console.error(chalk.red(`Unknown option ${options.invalidOption} for "${options.command}".`));
console.error(chalk.dim(`Use "${APP_NAME} --help" or "${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.`));
console.error(chalk.dim(`Usage: ${getPackageCommandUsage(options.command)}`));
process.exitCode = 1;
return true;
}
const cwd = process.cwd();
const agentDir = getAgentDir();
const settingsManager = SettingsManager.create(cwd, agentDir);
reportSettingsErrors(settingsManager, "package command");
const packageManager = new DefaultPackageManager({ cwd, agentDir, settingsManager });
packageManager.setProgressCallback((event) => {
if (event.type === "start") {
process.stdout.write(chalk.dim(`${event.message}\n`));
}
});
try {
switch (options.command) {
case "install":
await packageManager.installAndPersist(source!, { local: options.local });
console.log(chalk.green(`Installed ${source}`));
return true;
case "remove": {
const removed = await packageManager.removeAndPersist(source!, { local: options.local });
if (!removed) {
console.error(chalk.red(`No matching package found for ${source}`));
process.exitCode = 1;
return true;
}
console.log(chalk.green(`Removed ${source}`));
return true;
}
case "list": {
const configuredPackages = packageManager.listConfiguredPackages();
const userPackages = configuredPackages.filter((pkg) => pkg.scope === "user");
const projectPackages = configuredPackages.filter((pkg) => pkg.scope === "project");
if (configuredPackages.length === 0) {
console.log(chalk.dim("No packages installed."));
return true;
}
const formatPackage = (pkg: (typeof configuredPackages)[number]) => {
const display = pkg.filtered ? `${pkg.source} (filtered)` : pkg.source;
console.log(` ${display}`);
if (pkg.installedPath) {
console.log(chalk.dim(` ${pkg.installedPath}`));
}
};
if (userPackages.length > 0) {
console.log(chalk.bold("User packages:"));
for (const pkg of userPackages) {
formatPackage(pkg);
}
}
if (projectPackages.length > 0) {
if (userPackages.length > 0) console.log();
console.log(chalk.bold("Project packages:"));
for (const pkg of projectPackages) {
formatPackage(pkg);
}
}
return true;
}
case "update":
await packageManager.update(source);
if (source) {
console.log(chalk.green(`Updated ${source}`));
} else {
console.log(chalk.green("Updated packages"));
}
return true;
}
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown package command error";
console.error(chalk.red(`Error: ${message}`));
process.exitCode = 1;
return true;
}
}