mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-04-28 06:19:43 +00:00
470 lines
13 KiB
TypeScript
470 lines
13 KiB
TypeScript
import chalk from "chalk";
|
|
import { spawn } from "child_process";
|
|
import { selectConfig } from "./cli/config-selector.js";
|
|
import {
|
|
APP_NAME,
|
|
getAgentDir,
|
|
getSelfUpdateCommand,
|
|
getSelfUpdateUnavailableInstruction,
|
|
PACKAGE_NAME,
|
|
} from "./config.js";
|
|
import { DefaultPackageManager } from "./core/package-manager.js";
|
|
import { SettingsManager } from "./core/settings-manager.js";
|
|
|
|
export type PackageCommand = "install" | "remove" | "update" | "list";
|
|
|
|
type UpdateTarget = { type: "all" } | { type: "self" } | { type: "extensions"; source?: string };
|
|
|
|
interface PackageCommandOptions {
|
|
command: PackageCommand;
|
|
source?: string;
|
|
updateTarget?: UpdateTarget;
|
|
local: boolean;
|
|
help: boolean;
|
|
invalidOption?: string;
|
|
invalidArgument?: string;
|
|
missingOptionValue?: string;
|
|
conflictingOptions?: 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|self|pi] [--self] [--extensions] [--extension <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 pi and installed packages.
|
|
|
|
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;
|
|
|
|
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 invalidArgument: string | undefined;
|
|
let missingOptionValue: string | undefined;
|
|
let conflictingOptions: string | undefined;
|
|
let source: string | undefined;
|
|
let selfFlag = false;
|
|
let extensionsFlag = false;
|
|
let extensionFlagSource: string | undefined;
|
|
|
|
for (let index = 0; index < rest.length; index++) {
|
|
const arg = rest[index];
|
|
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 === "--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("-")) {
|
|
invalidOption = invalidOption ?? arg;
|
|
continue;
|
|
}
|
|
|
|
if (!source) {
|
|
source = arg;
|
|
} else {
|
|
invalidArgument = invalidArgument ?? arg;
|
|
}
|
|
}
|
|
|
|
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> {
|
|
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;
|
|
}
|
|
|
|
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;
|
|
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;
|
|
}
|
|
|
|
if (options.command === "update" && options.updateTarget?.type === "self" && !canSelfUpdate()) {
|
|
printSelfUpdateUnavailable();
|
|
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": {
|
|
const target = options.updateTarget ?? { type: "all" };
|
|
if (updateTargetIncludesExtensions(target)) {
|
|
const updateSource = target.type === "extensions" ? target.source : undefined;
|
|
await packageManager.update(updateSource);
|
|
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;
|
|
}
|
|
}
|
|
} 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;
|
|
}
|
|
}
|