mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-21 05:02:14 +00:00
fix(coding-agent): add package subcommand help and friendly errors (#1347)
* fix(coding-agent): add package subcommand help and friendly errors * refactor(coding-agent): simplify package command parsing and dispatch * fix(coding-agent): add plain git URL examples to install help
This commit is contained in:
parent
2ae668823c
commit
f9161c4d4e
3 changed files with 241 additions and 81 deletions
|
|
@ -185,6 +185,7 @@ ${chalk.bold("Commands:")}
|
||||||
${APP_NAME} update [source] Update installed extensions (skips pinned sources)
|
${APP_NAME} update [source] Update installed extensions (skips pinned sources)
|
||||||
${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/update/list
|
||||||
|
|
||||||
${chalk.bold("Options:")}
|
${chalk.bold("Options:")}
|
||||||
--provider <name> Provider name (default: google)
|
--provider <name> Provider name (default: google)
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import { selectConfig } from "./cli/config-selector.js";
|
||||||
import { processFileArguments } from "./cli/file-processor.js";
|
import { processFileArguments } from "./cli/file-processor.js";
|
||||||
import { listModels } from "./cli/list-models.js";
|
import { listModels } from "./cli/list-models.js";
|
||||||
import { selectSession } from "./cli/session-picker.js";
|
import { selectSession } from "./cli/session-picker.js";
|
||||||
import { getAgentDir, getModelsPath, VERSION } from "./config.js";
|
import { APP_NAME, getAgentDir, getModelsPath, VERSION } from "./config.js";
|
||||||
import { AuthStorage } from "./core/auth-storage.js";
|
import { AuthStorage } from "./core/auth-storage.js";
|
||||||
import { DEFAULT_THINKING_LEVEL } from "./core/defaults.js";
|
import { DEFAULT_THINKING_LEVEL } from "./core/defaults.js";
|
||||||
import { exportFromFile } from "./core/export-html/index.js";
|
import { exportFromFile } from "./core/export-html/index.js";
|
||||||
|
|
@ -61,6 +61,74 @@ interface PackageCommandOptions {
|
||||||
command: PackageCommand;
|
command: PackageCommand;
|
||||||
source?: string;
|
source?: string;
|
||||||
local: boolean;
|
local: boolean;
|
||||||
|
help: boolean;
|
||||||
|
invalidOption?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 https://github.com/user/repo
|
||||||
|
${APP_NAME} install 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.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-l, --local Remove from project settings (.pi/settings.json)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
${APP_NAME} remove 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 {
|
function parsePackageCommand(args: string[]): PackageCommandOptions | undefined {
|
||||||
|
|
@ -70,16 +138,36 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
let local = false;
|
let local = false;
|
||||||
const sources: string[] = [];
|
let help = false;
|
||||||
|
let invalidOption: string | undefined;
|
||||||
|
let source: string | undefined;
|
||||||
|
|
||||||
for (const arg of rest) {
|
for (const arg of rest) {
|
||||||
if (arg === "-l" || arg === "--local") {
|
if (arg === "-h" || arg === "--help") {
|
||||||
local = true;
|
help = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
sources.push(arg);
|
|
||||||
|
if (arg === "-l" || arg === "--local") {
|
||||||
|
if (command === "install" || command === "remove") {
|
||||||
|
local = true;
|
||||||
|
} else {
|
||||||
|
invalidOption = invalidOption ?? arg;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { command, source: sources[0], local };
|
if (arg.startsWith("-")) {
|
||||||
|
invalidOption = invalidOption ?? arg;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!source) {
|
||||||
|
source = arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { command, source, local, help, invalidOption };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handlePackageCommand(args: string[]): Promise<boolean> {
|
async function handlePackageCommand(args: string[]): Promise<boolean> {
|
||||||
|
|
@ -88,47 +176,58 @@ async function handlePackageCommand(args: string[]): Promise<boolean> {
|
||||||
return false;
|
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 cwd = process.cwd();
|
||||||
const agentDir = getAgentDir();
|
const agentDir = getAgentDir();
|
||||||
const settingsManager = SettingsManager.create(cwd, agentDir);
|
const settingsManager = SettingsManager.create(cwd, agentDir);
|
||||||
const packageManager = new DefaultPackageManager({ cwd, agentDir, settingsManager });
|
const packageManager = new DefaultPackageManager({ cwd, agentDir, settingsManager });
|
||||||
|
|
||||||
// Set up progress callback for CLI feedback
|
|
||||||
packageManager.setProgressCallback((event) => {
|
packageManager.setProgressCallback((event) => {
|
||||||
if (event.type === "start") {
|
if (event.type === "start") {
|
||||||
process.stdout.write(chalk.dim(`${event.message}\n`));
|
process.stdout.write(chalk.dim(`${event.message}\n`));
|
||||||
} else if (event.type === "error") {
|
|
||||||
console.error(chalk.red(`Error: ${event.message}`));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (options.command === "install") {
|
try {
|
||||||
if (!options.source) {
|
switch (options.command) {
|
||||||
console.error(chalk.red("Missing install source."));
|
case "install":
|
||||||
process.exit(1);
|
await packageManager.install(source!, { local: options.local });
|
||||||
}
|
packageManager.addSourceToSettings(source!, { local: options.local });
|
||||||
await packageManager.install(options.source, { local: options.local });
|
console.log(chalk.green(`Installed ${source}`));
|
||||||
packageManager.addSourceToSettings(options.source, { local: options.local });
|
|
||||||
console.log(chalk.green(`Installed ${options.source}`));
|
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
|
|
||||||
if (options.command === "remove") {
|
case "remove": {
|
||||||
if (!options.source) {
|
await packageManager.remove(source!, { local: options.local });
|
||||||
console.error(chalk.red("Missing remove source."));
|
const removed = packageManager.removeSourceFromSettings(source!, { local: options.local });
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
await packageManager.remove(options.source, { local: options.local });
|
|
||||||
const removed = packageManager.removeSourceFromSettings(options.source, { local: options.local });
|
|
||||||
if (!removed) {
|
if (!removed) {
|
||||||
console.error(chalk.red(`No matching package found for ${options.source}`));
|
console.error(chalk.red(`No matching package found for ${source}`));
|
||||||
process.exit(1);
|
process.exitCode = 1;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
console.log(chalk.green(`Removed ${options.source}`));
|
console.log(chalk.green(`Removed ${source}`));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.command === "list") {
|
case "list": {
|
||||||
const globalSettings = settingsManager.getGlobalSettings();
|
const globalSettings = settingsManager.getGlobalSettings();
|
||||||
const projectSettings = settingsManager.getProjectSettings();
|
const projectSettings = settingsManager.getProjectSettings();
|
||||||
const globalPackages = globalSettings.packages ?? [];
|
const globalPackages = globalSettings.packages ?? [];
|
||||||
|
|
@ -144,7 +243,6 @@ async function handlePackageCommand(args: string[]): Promise<boolean> {
|
||||||
const filtered = typeof pkg === "object";
|
const filtered = typeof pkg === "object";
|
||||||
const display = filtered ? `${source} (filtered)` : source;
|
const display = filtered ? `${source} (filtered)` : source;
|
||||||
console.log(` ${display}`);
|
console.log(` ${display}`);
|
||||||
// Show resolved path
|
|
||||||
const path = packageManager.getInstalledPath(source, scope);
|
const path = packageManager.getInstalledPath(source, scope);
|
||||||
if (path) {
|
if (path) {
|
||||||
console.log(chalk.dim(` ${path}`));
|
console.log(chalk.dim(` ${path}`));
|
||||||
|
|
@ -169,13 +267,21 @@ async function handlePackageCommand(args: string[]): Promise<boolean> {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await packageManager.update(options.source);
|
case "update":
|
||||||
if (options.source) {
|
await packageManager.update(source);
|
||||||
console.log(chalk.green(`Updated ${options.source}`));
|
if (source) {
|
||||||
|
console.log(chalk.green(`Updated ${source}`));
|
||||||
} else {
|
} else {
|
||||||
console.log(chalk.green("Updated packages"));
|
console.log(chalk.green("Updated packages"));
|
||||||
}
|
}
|
||||||
return true;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function prepareInitialMessage(
|
async function prepareInitialMessage(
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { mkdirSync, readFileSync, realpathSync, rmSync } from "node:fs";
|
import { mkdirSync, readFileSync, realpathSync, rmSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { ENV_AGENT_DIR } from "../src/config.js";
|
import { ENV_AGENT_DIR } from "../src/config.js";
|
||||||
import { main } from "../src/main.js";
|
import { main } from "../src/main.js";
|
||||||
|
|
||||||
|
|
@ -12,6 +12,7 @@ describe("package commands", () => {
|
||||||
let packageDir: string;
|
let packageDir: string;
|
||||||
let originalCwd: string;
|
let originalCwd: string;
|
||||||
let originalAgentDir: string | undefined;
|
let originalAgentDir: string | undefined;
|
||||||
|
let originalExitCode: typeof process.exitCode;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
tempDir = join(tmpdir(), `pi-package-commands-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
tempDir = join(tmpdir(), `pi-package-commands-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
||||||
|
|
@ -24,12 +25,15 @@ describe("package commands", () => {
|
||||||
|
|
||||||
originalCwd = process.cwd();
|
originalCwd = process.cwd();
|
||||||
originalAgentDir = process.env[ENV_AGENT_DIR];
|
originalAgentDir = process.env[ENV_AGENT_DIR];
|
||||||
|
originalExitCode = process.exitCode;
|
||||||
|
process.exitCode = undefined;
|
||||||
process.env[ENV_AGENT_DIR] = agentDir;
|
process.env[ENV_AGENT_DIR] = agentDir;
|
||||||
process.chdir(projectDir);
|
process.chdir(projectDir);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
process.chdir(originalCwd);
|
process.chdir(originalCwd);
|
||||||
|
process.exitCode = originalExitCode;
|
||||||
if (originalAgentDir === undefined) {
|
if (originalAgentDir === undefined) {
|
||||||
delete process.env[ENV_AGENT_DIR];
|
delete process.env[ENV_AGENT_DIR];
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -64,4 +68,53 @@ describe("package commands", () => {
|
||||||
const removedSettings = JSON.parse(readFileSync(settingsPath, "utf-8")) as { packages?: string[] };
|
const removedSettings = JSON.parse(readFileSync(settingsPath, "utf-8")) as { packages?: string[] };
|
||||||
expect(removedSettings.packages ?? []).toHaveLength(0);
|
expect(removedSettings.packages ?? []).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows install subcommand help", async () => {
|
||||||
|
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||||
|
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await expect(main(["install", "--help"])).resolves.toBeUndefined();
|
||||||
|
|
||||||
|
const stdout = logSpy.mock.calls.map(([message]) => String(message)).join("\n");
|
||||||
|
expect(stdout).toContain("Usage:");
|
||||||
|
expect(stdout).toContain("pi install <source> [-l]");
|
||||||
|
expect(errorSpy).not.toHaveBeenCalled();
|
||||||
|
expect(process.exitCode).toBeUndefined();
|
||||||
|
} finally {
|
||||||
|
logSpy.mockRestore();
|
||||||
|
errorSpy.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows a friendly error for unknown install options", async () => {
|
||||||
|
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await expect(main(["install", "--unknown"])).resolves.toBeUndefined();
|
||||||
|
|
||||||
|
const stderr = errorSpy.mock.calls.map(([message]) => String(message)).join("\n");
|
||||||
|
expect(stderr).toContain('Unknown option --unknown for "install".');
|
||||||
|
expect(stderr).toContain('Use "pi --help" or "pi install <source> [-l]".');
|
||||||
|
expect(process.exitCode).toBe(1);
|
||||||
|
} finally {
|
||||||
|
errorSpy.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows a friendly error for missing install source", async () => {
|
||||||
|
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await expect(main(["install"])).resolves.toBeUndefined();
|
||||||
|
|
||||||
|
const stderr = errorSpy.mock.calls.map(([message]) => String(message)).join("\n");
|
||||||
|
expect(stderr).toContain("Missing install source.");
|
||||||
|
expect(stderr).toContain("Usage: pi install <source> [-l]");
|
||||||
|
expect(stderr).not.toContain("at ");
|
||||||
|
expect(process.exitCode).toBe(1);
|
||||||
|
} finally {
|
||||||
|
errorSpy.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue