Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions plugins/codex/scripts/codex-companion.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -140,14 +140,25 @@ function normalizeArgv(argv) {

function parseCommandInput(argv, config = {}) {
return parseArgs(normalizeArgv(argv), {
rejectUnknownOptions: true,
...config,
booleanOptions: ["help", ...(config.booleanOptions ?? [])],
aliasMap: {
C: "cwd",
h: "help",
...(config.aliasMap ?? {})
}
});
}

function maybePrintCommandHelp(options) {
if (!options.help) {
return false;
}
printUsage();
return true;
}

function resolveCommandCwd(options = {}) {
return options.cwd ? path.resolve(process.cwd(), options.cwd) : process.cwd();
}
Expand Down Expand Up @@ -217,6 +228,9 @@ async function handleSetup(argv) {
valueOptions: ["cwd"],
booleanOptions: ["json", "enable-review-gate", "disable-review-gate"]
});
if (maybePrintCommandHelp(options)) {
return;
}

if (options["enable-review-gate"] && options["disable-review-gate"]) {
throw new Error("Choose either --enable-review-gate or --disable-review-gate.");
Expand Down Expand Up @@ -717,6 +731,9 @@ async function handleReviewCommand(argv, config) {
m: "model"
}
});
if (maybePrintCommandHelp(options)) {
return;
}

const cwd = resolveCommandCwd(options);
const workspaceRoot = resolveCommandWorkspace(options);
Expand Down Expand Up @@ -767,6 +784,9 @@ async function handleTask(argv) {
m: "model"
}
});
if (maybePrintCommandHelp(options)) {
return;
}

const cwd = resolveCommandCwd(options);
const workspaceRoot = resolveCommandWorkspace(options);
Expand Down Expand Up @@ -827,6 +847,9 @@ async function handleTransfer(argv) {
valueOptions: ["cwd", "source"],
booleanOptions: ["json"]
});
if (maybePrintCommandHelp(options)) {
return;
}

const cwd = resolveCommandCwd(options);
const { payload, rendered } = await executeTransfer(cwd, {
Expand Down Expand Up @@ -885,6 +908,9 @@ async function handleStatus(argv) {
valueOptions: ["cwd", "timeout-ms", "poll-interval-ms"],
booleanOptions: ["json", "all", "wait"]
});
if (maybePrintCommandHelp(options)) {
return;
}

const cwd = resolveCommandCwd(options);
const reference = positionals[0] ?? "";
Expand Down Expand Up @@ -912,6 +938,9 @@ function handleResult(argv) {
valueOptions: ["cwd"],
booleanOptions: ["json"]
});
if (maybePrintCommandHelp(options)) {
return;
}

const cwd = resolveCommandCwd(options);
const reference = positionals[0] ?? "";
Expand All @@ -930,6 +959,9 @@ function handleTaskResumeCandidate(argv) {
valueOptions: ["cwd"],
booleanOptions: ["json"]
});
if (maybePrintCommandHelp(options)) {
return;
}

const cwd = resolveCommandCwd(options);
const workspaceRoot = resolveCommandWorkspace(options);
Expand Down Expand Up @@ -965,6 +997,9 @@ async function handleCancel(argv) {
valueOptions: ["cwd"],
booleanOptions: ["json"]
});
if (maybePrintCommandHelp(options)) {
return;
}

const cwd = resolveCommandCwd(options);
const reference = positionals[0] ?? "";
Expand Down
9 changes: 9 additions & 0 deletions plugins/codex/scripts/lib/args.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export function parseArgs(argv, config = {}) {
const valueOptions = new Set(config.valueOptions ?? []);
const booleanOptions = new Set(config.booleanOptions ?? []);
const aliasMap = config.aliasMap ?? {};
const rejectUnknownOptions = Boolean(config.rejectUnknownOptions);
const options = {};
const positionals = [];
let passthrough = false;
Expand Down Expand Up @@ -45,6 +46,10 @@ export function parseArgs(argv, config = {}) {
continue;
}

if (rejectUnknownOptions) {
throw new Error(`Unknown option: --${rawKey}`);
}

positionals.push(token);
continue;
}
Expand All @@ -67,6 +72,10 @@ export function parseArgs(argv, config = {}) {
continue;
}

if (rejectUnknownOptions) {
throw new Error(`Unknown option: -${shortKey}`);
Comment on lines +75 to +76

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep short-flag text usable in task prompts

When task is invoked in the documented quoted raw-argument form, normalizeArgv re-splits the natural-language prompt before it reaches this parser, so any prompt that mentions a short CLI flag or negative-style token now fails here before Codex is dispatched. For example, task "investigate grep -R usage" exits with Unknown option: -R instead of sending that prompt, which is a common shape for debugging command-line behavior; the parser should either stop option parsing once prompt text starts or avoid rejecting single-dash tokens for prompt-taking commands.

Useful? React with 👍 / 👎.

}

positionals.push(token);
}

Expand Down
86 changes: 86 additions & 0 deletions tests/args.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import test from "node:test";
import assert from "node:assert/strict";
import { fileURLToPath } from "node:url";
import path from "node:path";

import { parseArgs } from "../plugins/codex/scripts/lib/args.mjs";
import { makeTempDir, run, initGitRepo } from "./helpers.mjs";
import { buildEnv, installFakeCodex } from "./fake-codex-fixture.mjs";
import fs from "node:fs";

const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const SCRIPT = path.join(ROOT, "plugins", "codex", "scripts", "codex-companion.mjs");

test("parseArgs rejects unknown long options when configured", () => {
const helped = parseArgs(["--help", "--cwd", "/tmp"], {
booleanOptions: ["help"],
valueOptions: ["cwd"],
rejectUnknownOptions: true
});
assert.equal(helped.options.help, true);
assert.equal(helped.options.cwd, "/tmp");
assert.deepEqual(helped.positionals, []);

assert.throws(
() =>
parseArgs(["--not-a-flag"], {
booleanOptions: ["json"],
rejectUnknownOptions: true
}),
/Unknown option: --not-a-flag/
);
});

test("parseArgs keeps unknown options as positionals by default", () => {
const { options, positionals } = parseArgs(["--not-a-flag", "hello"], {
booleanOptions: ["json"]
});
assert.deepEqual(options, {});
assert.deepEqual(positionals, ["--not-a-flag", "hello"]);
});

test("task --help prints usage and does not dispatch a Codex thread", () => {
const repo = makeTempDir();
const binDir = makeTempDir();
installFakeCodex(binDir);
initGitRepo(repo);
fs.writeFileSync(path.join(repo, "README.md"), "hello\n");

const result = run("node", [SCRIPT, "task", "--help", "--cwd", repo, "--json"], {
cwd: repo,
env: buildEnv(binDir)
});

assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /Usage:/);
assert.match(result.stdout, /codex-companion\.mjs task/);
assert.equal(result.stderr.trim(), "");

const fakeStatePath = path.join(binDir, "fake-codex-state.json");
if (fs.existsSync(fakeStatePath)) {
const state = JSON.parse(fs.readFileSync(fakeStatePath, "utf8"));
assert.equal((state.threads ?? []).length, 0);
}
});

test("task unknown --flag errors without dispatching a Codex thread", () => {
const repo = makeTempDir();
const binDir = makeTempDir();
installFakeCodex(binDir);
initGitRepo(repo);
fs.writeFileSync(path.join(repo, "README.md"), "hello\n");

const result = run("node", [SCRIPT, "task", "--not-a-real-flag", "--cwd", repo], {
cwd: repo,
env: buildEnv(binDir)
});

assert.notEqual(result.status, 0);
assert.match(result.stderr, /Unknown option: --not-a-real-flag/);

const fakeStatePath = path.join(binDir, "fake-codex-state.json");
if (fs.existsSync(fakeStatePath)) {
const state = JSON.parse(fs.readFileSync(fakeStatePath, "utf8"));
assert.equal((state.threads ?? []).length, 0);
}
});