Merge pull request #2161: fix(cli): reject ambiguous extensions command
This commit is contained in:
@@ -180,6 +180,10 @@
|
||||
|
||||
- Added a structured memory runtime surface for extensions and UI integrations to query backend status, search memories, and save explicit memories across the configured memory backend.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed bare `omp extensions` being treated as a chat prompt instead of returning an actionable plugin-command error ([#2089](https://github.com/can1357/oh-my-pi/issues/2089)).
|
||||
|
||||
## [15.10.8] - 2026-06-09
|
||||
|
||||
### Added
|
||||
|
||||
@@ -38,6 +38,18 @@ export const commands: CommandEntry[] = [
|
||||
{ name: "search", load: () => import("./commands/web-search").then(m => m.default), aliases: ["q"] },
|
||||
];
|
||||
|
||||
const RESERVED_TOP_LEVEL_WORDS = new Map<string, string>([
|
||||
[
|
||||
"extensions",
|
||||
'`omp extensions` is not a management command. Use `omp plugin list` / `omp plugin install`, or run `omp launch extensions` if you meant to send "extensions" as a prompt.',
|
||||
],
|
||||
]);
|
||||
|
||||
export function reservedTopLevelWordMessage(first: string | undefined, argc = 1): string | undefined {
|
||||
if (argc !== 1 || !first || first.startsWith("-") || first.startsWith("@")) return undefined;
|
||||
return RESERVED_TOP_LEVEL_WORDS.get(first);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true when `first` matches a registered subcommand name or alias.
|
||||
*
|
||||
@@ -48,3 +60,20 @@ export function isSubcommand(first: string | undefined): boolean {
|
||||
if (!first || first.startsWith("-") || first.startsWith("@")) return false;
|
||||
return commands.some(entry => entry.name === first || entry.aliases?.includes(first));
|
||||
}
|
||||
|
||||
export type ResolvedCliArgv = { argv: string[] } | { error: string };
|
||||
|
||||
/**
|
||||
* Decide what the CLI runner should do with raw argv: reject bare reserved
|
||||
* management words, pass help/version through untouched, and route everything
|
||||
* that is not a known subcommand to `launch`.
|
||||
*/
|
||||
export function resolveCliArgv(argv: string[]): ResolvedCliArgv {
|
||||
const first = argv[0];
|
||||
const reservedMessage = reservedTopLevelWordMessage(first, argv.length);
|
||||
if (reservedMessage) return { error: reservedMessage };
|
||||
if (first === "--help" || first === "-h" || first === "--version" || first === "-v" || first === "help") {
|
||||
return { argv };
|
||||
}
|
||||
return { argv: isSubcommand(first) ? argv : ["launch", ...argv] };
|
||||
}
|
||||
|
||||
@@ -161,20 +161,19 @@ export async function runCli(argv: string[]): Promise<void> {
|
||||
if (await runWorkerEntrypoint(argv[0])) {
|
||||
return;
|
||||
}
|
||||
const [{ run }, { commands, isSubcommand }] = await Promise.all([
|
||||
const [{ run }, { commands, resolveCliArgv }] = await Promise.all([
|
||||
import("@oh-my-pi/pi-utils/cli"),
|
||||
import("./cli-commands"),
|
||||
]);
|
||||
// --help and --version are handled by run() directly, don't rewrite those.
|
||||
// Everything else that isn't a known subcommand routes to "launch".
|
||||
const first = argv[0];
|
||||
const runArgv =
|
||||
first === "--help" || first === "-h" || first === "--version" || first === "-v" || first === "help"
|
||||
? argv
|
||||
: isSubcommand(first)
|
||||
? argv
|
||||
: ["launch", ...argv];
|
||||
return run({ bin: APP_NAME, version: VERSION, argv: runArgv, commands, help: showHelp });
|
||||
const resolved = resolveCliArgv(argv);
|
||||
if ("error" in resolved) {
|
||||
process.stderr.write(`error: ${resolved.error}\n`);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
return run({ bin: APP_NAME, version: VERSION, argv: resolved.argv, commands, help: showHelp });
|
||||
}
|
||||
|
||||
// Floating call instead of top-level await: TLA forces `--bytecode` (CJS
|
||||
|
||||
@@ -15,13 +15,23 @@ import { describe, expect, test } from "bun:test";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { commands, isSubcommand, resolveCliArgv } from "@oh-my-pi/pi-coding-agent/cli-commands";
|
||||
import { looksLikeLocalPath } from "@oh-my-pi/pi-coding-agent/commands/install";
|
||||
|
||||
describe("install command is registered as a top-level subcommand", () => {
|
||||
test("CLI runner sees `install` as a known command", async () => {
|
||||
const cli = await import("@oh-my-pi/pi-coding-agent/cli-commands");
|
||||
expect(cli.commands.some(c => c.name === "install")).toBe(true);
|
||||
expect(cli.isSubcommand("install")).toBe(true);
|
||||
test("CLI runner sees `install` as a known command", () => {
|
||||
expect(commands.some(c => c.name === "install")).toBe(true);
|
||||
expect(isSubcommand("install")).toBe(true);
|
||||
});
|
||||
|
||||
test("CLI runner rejects only bare reserved management words", () => {
|
||||
expect(resolveCliArgv(["extensions"])).toEqual({
|
||||
error: '`omp extensions` is not a management command. Use `omp plugin list` / `omp plugin install`, or run `omp launch extensions` if you meant to send "extensions" as a prompt.',
|
||||
});
|
||||
expect(resolveCliArgv(["extensions", "are", "not", "loading"])).toEqual({
|
||||
argv: ["launch", "extensions", "are", "not", "loading"],
|
||||
});
|
||||
expect(resolveCliArgv(["launch", "extensions"])).toEqual({ argv: ["launch", "extensions"] });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user