Something went wrong. Try again.
more up to date at codeberg https://codeberg.org/cowie/tau.git
Something went wrong. Try again.
17 kB · 590 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591import { Markdown, type MarkdownTheme } from "@earendil-works/pi-tui";import chalk from "chalk";import { selectConfig } from "./cli/config-selector.ts";import { APP_NAME, detectInstallMethod, getAgentDir, getPackageDir, getSelfUpdateCommand, getSelfUpdateUnavailableInstruction, PACKAGE_NAME, type SelfUpdateCommand, VERSION,} from "./config.ts";import { DefaultPackageManager } from "./core/package-manager.ts";import { SettingsManager } from "./core/settings-manager.ts";import { spawnProcess } from "./utils/child-process.ts";import { getLatestPiRelease, isNewerPackageVersion } from "./utils/version-check.ts";import { cleanupWindowsSelfUpdateQuarantine, quarantineWindowsNativeDependencies,} from "./utils/windows-self-update.ts";
export type PackageCommand = "install" | "remove" | "update" | "list";
type UpdateTarget = { type: "all" } | { type: "self" } | { type: "extensions"; source?: string };
const SELF_UPDATE_NOTE_MARKDOWN_THEME: MarkdownTheme = { heading: (text) => chalk.bold(chalk.yellow(text)), link: (text) => chalk.cyan(text), linkUrl: (text) => chalk.dim(text), code: (text) => chalk.yellow(text), codeBlock: (text) => chalk.dim(text), codeBlockBorder: (text) => chalk.dim(text), quote: (text) => chalk.dim(text), quoteBorder: (text) => chalk.dim(text), hr: (text) => chalk.dim(text), listBullet: (text) => chalk.yellow(text), bold: (text) => chalk.bold(text), italic: (text) => chalk.italic(text), strikethrough: (text) => chalk.strikethrough(text), underline: (text) => chalk.underline(text),};
interface PackageCommandOptions { command: PackageCommand; source?: string; updateTarget?: UpdateTarget; local: boolean; force: 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>] [--force]`; 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 --force Reinstall pi even if the current version is latest
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 force = 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 === "--force") { if (command === "update") { force = 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, force, 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 printSelfUpdateUnavailable(npmCommand?: string[], updatePackageName = PACKAGE_NAME): void { console.error(`error: ${APP_NAME} cannot self-update this installation.`); console.error(getSelfUpdateUnavailableInstruction(PACKAGE_NAME, npmCommand, updatePackageName));
const entrypoint = process.argv[1]; if (entrypoint) { console.error(""); console.error(`Location of pi executable: ${entrypoint}`); }}
function printSelfUpdateFallback(command: SelfUpdateCommand): void { console.error(chalk.dim(`If this keeps failing, run this command yourself: ${command.display}`));}
function printSelfUpdateNote(note: string): void { const trimmedNote = note.trim(); if (!trimmedNote) { return; }
console.log(); console.log(chalk.bold(chalk.yellow("Update note"))); try { const width = Math.max(20, process.stdout.columns ?? 80); const renderedLines = new Markdown(trimmedNote, 0, 0, SELF_UPDATE_NOTE_MARKDOWN_THEME) .render(width) .map((line) => line.trimEnd()); console.log(renderedLines.join("\n")); } catch { console.log(trimmedNote); } console.log();}
interface SelfUpdatePlan { packageName: string; shouldRun: boolean; note?: string;}
async function getSelfUpdatePlan(force: boolean): Promise<SelfUpdatePlan> { if (force) { return { packageName: PACKAGE_NAME, shouldRun: true }; }
try { const latestRelease = await getLatestPiRelease(VERSION); const packageName = latestRelease?.packageName ?? PACKAGE_NAME; if (!latestRelease || packageName !== PACKAGE_NAME || isNewerPackageVersion(latestRelease.version, VERSION)) { return { packageName, shouldRun: true, ...(latestRelease?.note ? { note: latestRelease.note } : {}) }; } } catch { return { packageName: PACKAGE_NAME, shouldRun: true }; }
console.log(chalk.green(`${APP_NAME} is already up to date (v${VERSION})`)); return { packageName: PACKAGE_NAME, shouldRun: false };}
async function runSelfUpdate(command: SelfUpdateCommand): Promise<void> { console.log(chalk.dim(`Updating ${APP_NAME} with ${command.display}...`)); for (const step of command.steps ?? [command]) { await new Promise<void>((resolve, reject) => { const child = spawnProcess(step.command, step.args, { stdio: "inherit", }); child.on("error", (error) => { reject(error); }); child.on("close", (code, signal) => { if (code === 0) { resolve(); } else if (signal) { reject(new Error(`${step.display} terminated by signal ${signal}`)); } else { reject(new Error(`${step.display} exited with code ${code ?? "unknown"}`)); } }); }); }}
function prepareWindowsNpmSelfUpdate(): void { if (process.platform !== "win32") { return; }
const packageDir = getPackageDir(); cleanupWindowsSelfUpdateQuarantine(packageDir); quarantineWindowsNativeDependencies(packageDir);}
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; }
const cwd = process.cwd(); const agentDir = getAgentDir(); const settingsManager = SettingsManager.create(cwd, agentDir); reportSettingsErrors(settingsManager, "package command"); const selfUpdateNpmCommand = settingsManager.getGlobalSettings().npmCommand;
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)) { const selfUpdatePlan = await getSelfUpdatePlan(options.force); if (!selfUpdatePlan.shouldRun) { return true; } const installMethod = detectInstallMethod(); if (process.platform === "win32" && installMethod !== "npm" && installMethod !== "pnpm") { console.error( chalk.red(`${APP_NAME} self-update on Windows is only supported for npm and pnpm installs.`), ); console.error(chalk.dim(`Detected install method: ${installMethod}. Update ${APP_NAME} manually.`)); process.exitCode = 1; return true; } const selfUpdateCommand = getSelfUpdateCommand( PACKAGE_NAME, selfUpdateNpmCommand, selfUpdatePlan.packageName, ); if (!selfUpdateCommand) { printSelfUpdateUnavailable(selfUpdateNpmCommand, selfUpdatePlan.packageName); process.exitCode = 1; return true; } if (selfUpdatePlan.note) { printSelfUpdateNote(selfUpdatePlan.note); } try { if (installMethod === "npm") { prepareWindowsNpmSelfUpdate(); } await runSelfUpdate(selfUpdateCommand); } catch (error: unknown) { const message = error instanceof Error ? error.message : "Unknown package command error"; console.error(chalk.red(`Error: ${message}`)); printSelfUpdateFallback(selfUpdateCommand); process.exitCode = 1; return true; } console.log(chalk.green(`Updated ${APP_NAME}`)); } 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; }}