Skip to content
Merged
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
30 changes: 30 additions & 0 deletions devlog/_plan/261003_cli_help_ux/000_plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,33 @@ required hosted evidence. The loop goal remains active.
Next direction: consume020 to make root/family navigation concise while retaining
wp1's full reference and compatibility behavior. No service/runtime changes are
inferred from the unrelated verification investigation.

wp2 P revalidated020 against de02ff1d00. Accepted architect CLI-UX-02 amendments:
entry canonical/declared-child presentation fields, curated context link, updated
root/full tests, incidental old error-banner behavior, and both provider usage
consumers consolidated with an explicit successful-help output sink. Same
architect final reflection ALIGNED; no execution/parser/JSON scope expansion.
wp2 independent A review PASS atde02ff1d00; both provider usage consumers,
transient sink/entry metadata, test migrations and draft limitations reviewed.
Main clarified that92is a measured full-reference baseline, not a permanent
assertion; visible registry coverage protects future command additions instead.
## wp2 Done (implementation scope; PR remains draft)

Root help is26logical lines/max80columns, with standard usage, common tasks,
registry-backed descriptions and the full-reference escape. Family/alias links,
curated context topic and provider single-owner help are implemented. Both
provider output channels and appended-help fallback remain correct.

Five meaningful RED tests preceded135focused passes. Changed-import verification
initially exposed a complete-reference test still reading compact help; preserving
all assertions and switching its renderer fixed it. Final:1,097pass/1skip/0fail
across46files. Typecheck, structure, skill surface, privacy, layout18tests and
docs561pages/77,929links pass.18real CLI QA scenarios and a40x24PTY capture confirm
outputs, exits, no ANSI dependency and no state writes. PTY, child and temp home
were closed/removed. Independent reviewer inspected16/16files:PASS.

The full local suite's prior four failures remain recorded in011; no full-green
or merge-readiness claim. PR6498's current head has23successful checks and8
workflow-policy skips, including a successful CI aggregate and format gate.
Next direction: consume030 for concise contextual recovery, keeping all wp1/wp2
behavior. Publication uses ordinary dependent draft PRs; no merge.
63 changes: 63 additions & 0 deletions devlog/_plan/261003_cli_help_ux/020_help_navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ codex/cli-ux-help-foundation, head codex/cli-ux-navigation.
- MODIFY `tests/cli/cli-registry.test.ts` and `cli-help.test.ts`: coverage checks
render full reference instead of assuming the root template contains everything;
move export-count assertion to full view. Keep every public command reachable.
- MODIFY `tests/cli/cli-help-paths.test.ts`: move its 92-line/equivalence checks
to explicit full-reference forms; treat92as baseline observation, not a
permanent reference-length assertion. Default root has independent compact
assertions. Keep all nested/context/safety/JSON cases unchanged.
- NEW `tests/cli/cli-help-navigation.test.ts` plus both layout registrations:
compact default agreement, public group validity, no hidden names, no self-loop,
examples resolve and no ANSI/control dependence.
Expand All @@ -47,3 +51,62 @@ widths; no command token is clipped, and redirected NO_COLOR output is complete.

Run focused navigation/path/head/help/registry/capabilities tests, typecheck,
test:changed, structure/skill surface/privacy checks and docs build.

## P revalidation after wp1

Previous D: wp1 explicit/full help is implemented; broader full-suite failure is
recorded, draft readiness remains pending in wp4. This layer retains that behavior.

CLI-UX-02 architect amendments accepted:
- Entry resolution gains transient canonical identity and declared descendants,
created in help-catalog from existing registry/capability data and consumed only
in help.ts. No serialization/deserialization: N/A, process-local presentation.
Preserve exact registry alias usage/details before canonical navigation.
- Curated models-context is a separate topic link, not a fabricated capability row.
- Preserve capability/prefix/context/unavailable resolution and flag-help fallback.
- Full forms remain identical and preserve registry coverage/recovery variants;
92lines is the observed baseline, not a permanent count that blocks new commands.
Compact root differs and is <=28lines.
Update cli-help-paths root/full checks and rename unknown-root test's "full
banner" wording to "root banner". wp2 retains its old exit1/stdout+stderr
contract; concise stderr-only recovery remains wp3.
- Compact root references registry names/summaries without silently truncating
text.28logical lines is hard;80columns is a readability target.

Proposed provider-help owner consolidation for reflection: MODIFY
`src/cli/provider.ts` to remove its duplicated PROVIDER_USAGE string and render
`printSubcommandUsage("provider")` in its existing no-args/help branch. Move the
existing command rows/examples into registry provider.details (one static owner),
retaining general preset/custom guidance and removing the self-loop. No command
handler, validation, credentials or execution grammar changes. Add exact human
output parity coverage for bare provider/help provider/provider --help, plus
existing cli-provider regression coverage. The root no-args provider path still
has its existing preflight; do not falsely claim it has the head-help bypass.

Provider caller completion (architect correction): migrate BOTH consumers of
PROVIDER_USAGE. Successful no-args/help renders registry help to stdout and exits0.
The unknown provider action prints its existing diagnostic, then registry help to
stderr, and exits1. Add an optional transient `write` sink to the existing
printSubcommandUsage options, defaulting to console.log for successful rendering;
the provider error branch supplies console.error. Propagate it on parent fallback.
No persistent or machine schema fields. Add an isolated unknown-provider regression
asserting empty stdout, stderr usage and exit1; do not change command validation.

B output read: initialcompactroot24lines/max80columns. Main requested the
standard Usage header and concrete logs/usage registry summaries (retain alias
meaning) so common-task rows describe outcomes rather than only alias plumbing.
This is presentation copy within the existing metadata scope.

C coverage amendment: changed-import testing found the existing complete-client
help invariant in `tests/gui/integrations-invariants.test.ts` still calls compact
printUsage and expects a full command row. MODIFY that test to call
printFullUsage, retaining every client-id and `ocx integration client` assertion.
This is the same complete-reference coverage migration, not reduced coverage or
new client support. Add this exact file to the focused verification commands.

Hosted C follow-up: PR6500 test2/4 found a subprocess-only help consumer in
`tests/cli/cli-restore-back.test.ts` that import-graph selection did not discover.
MODIFY its help-documents-both-directions case to invoke `help --all` instead of
compact `help`; retain every restore/back assertion. Verify this file explicitly.
This follow-up belongs to the navigation layer and is committed before wp3's
source, with the parent ref advanced to retain a clean dependency chain.
20 changes: 20 additions & 0 deletions devlog/_plan/261003_cli_help_ux/021_verification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## wp2 Done (implementation scope; PR remains draft)

Root help is26logical lines/max80columns, with standard usage, common tasks,
registry-backed descriptions and the full-reference escape. Family/alias links,
curated context topic and provider single-owner help are implemented. Both
provider output channels and appended-help fallback remain correct.

Five meaningful RED tests preceded135focused passes. Changed-import verification
initially exposed a complete-reference test still reading compact help; preserving
all assertions and switching its renderer fixed it. Final:1,097pass/1skip/0fail
across46files. Typecheck, structure, skill surface, privacy, layout18tests and
docs561pages/77,929links pass.18real CLI QA scenarios and a40x24PTY capture confirm
outputs, exits, no ANSI dependency and no state writes. PTY, child and temp home
were closed/removed. Independent reviewer inspected16/16files:PASS.

The full local suite's prior four failures remain recorded in011; no full-green
or merge-readiness claim. PR6498's current head has23successful checks and8
workflow-policy skips, including a successful CI aggregate and format gate.
Next direction: consume030 for concise contextual recovery, keeping all wp1/wp2
behavior. Publication uses ordinary dependent draft PRs; no merge.
21 changes: 19 additions & 2 deletions docs-site/src/content/docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,23 @@ The opencodex CLI is `ocx`. It dispatches on the first command name, with docume
as `setup`/`init`, `restore`/`eject`, and `models`/`model` reaching the same operation. Unknown
commands and invalid command shapes are errors.

Run `ocx help` (or `ocx --help` / `ocx -h`) for the full top-level reference.
`ocx help --all` and `ocx --help --all` explicitly select the same full reference.
Run `ocx`, `ocx help`, `ocx --help`, or `ocx -h` for the same compact command index,
grouped into Start here, Common tasks, Explore, and More help.
Use `ocx help --all` or `ocx --help --all` for the full top-level reference, including
the detailed command variants omitted from the compact index.
Run `ocx help <command>`, `ocx <command> --help`, or `ocx <command> -h` for a command
registered in the help table; `ocx <command> help` also remains supported. Help and version
commands are read-only: they do not start, stop, install, uninstall, or rewrite Codex or
opencodex state.

## Nested help

Family help such as `ocx help models` preserves the family's usage and details, then
lists known declared child paths with summaries. These lists are marked as partial;
they do not enumerate every runtime operation. Alias help retains the alias's own usage
and details and points to its canonical family: `ocx help model` leads to `ocx help models`.
Models help also links the curated `models context` topic separately from declared capabilities.

Help accepts a path with more than one command word:

```bash
Expand Down Expand Up @@ -53,6 +61,15 @@ so `ocx claude -- --help` preserves the arguments for command dispatch.

## Command families

### `ocx provider`

`ocx provider`, `ocx help provider`, `ocx provider help`, `ocx provider --help`, and
`ocx provider -h` show the same provider help, with command syntax, examples, preset/custom
guidance, and declared topic pointers. Successful help prints to standard output and exits 0.
Bare `ocx provider` still follows ordinary command preflight; explicit head-help forms exit
before that preflight. An unknown provider action prints its diagnostic and provider help
to standard error, leaves standard output empty, and exits 1.

### `ocx alias`

`ocx alias list [--json]` shows effective user and built-in aliases. Use `ocx alias set <provider>[/<native-model-id>] <alias>` and `ocx alias rm <provider>[/<native-model-id>]` to edit them. Native model ids may contain additional slashes because the selector splits only at the first slash. Enable shipped defaults with `ocx alias defaults on|off [--provider <name>]`.
Expand Down
1 change: 1 addition & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@
"cli-headless-parity.test.ts": "cli",
"cli-help.test.ts": "cli",
"cli-help-paths.test.ts": "cli",
"cli-help-navigation.test.ts": "cli",
"cli-integer-options.test.ts": "cli",
"cli-init-port.test.ts": "cli",
"cli-json-contract.test.ts": "cli",
Expand Down
10 changes: 6 additions & 4 deletions src/cli/help-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { CAPABILITIES, type Capability } from "./capabilities";
import { CLI_COMMANDS, findCommand, type CliCommandEntry } from "./registry";

export type HelpResolution =
| { kind: "entry"; entry: CliCommandEntry }
| { kind: "entry"; entry: CliCommandEntry; canonicalName: string; children: readonly Capability[] }
| { kind: "capability"; path: string[]; capability: Capability }
| { kind: "prefix"; path: string[]; children: readonly Capability[] }
| { kind: "models-context"; path: string[] }
Expand All @@ -16,15 +16,17 @@ function startsWithPath(command: readonly string[], prefix: readonly string[]):
export function resolveHelpPath(requested: readonly string[]): HelpResolution {
const entry = requested[0] ? findCommand(requested[0]) : undefined;
if (!entry) return { kind: "unavailable", path: [...requested] };
if (requested.length === 1) return { kind: "entry", entry };

// Keep exact-name alias entries for root help; nested topics use the owner.
const canonical = CLI_COMMANDS.find(candidate => candidate.aliases?.includes(requested[0])) ?? entry;
const declared = CAPABILITIES.filter(capability => !findCommand(capability.command[0])?.hidden);
if (requested.length === 1) return {
kind: "entry", entry, canonicalName: canonical.name,
children: entry.hidden ? [] : declared.filter(candidate => candidate.command.length > 1 && candidate.command[0] === canonical.name),
};
const path = [canonical.name, ...requested.slice(1)];
if (path.length === 2 && path[0] === "models" && path[1] === "context") {
return { kind: "models-context", path };
}
const declared = CAPABILITIES.filter(capability => !findCommand(capability.command[0])?.hidden);
const capability = declared.find(candidate => candidate.command.length === path.length && startsWithPath(candidate.command, path));
if (capability) return { kind: "capability", path, capability };
const children = declared.filter(candidate => startsWithPath(candidate.command, path));
Expand Down
50 changes: 50 additions & 0 deletions src/cli/help-navigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { findCommand } from "./registry";

// Presentation owns selection/order only; names and summaries belong to registry.
const ACTION_GROUPS = [
{ title: "Start here", commands: ["setup", "start", "status"] },
{ title: "Common tasks", commands: ["doctor", "logs", "usage"] },
] as const;

const FAMILY_GROUPS = [
["provider", "account", "models", "alias"],
["combo", "route", "agent", "effort"],
["observe", "inspect", "access", "api"],
["config", "system", "integration"],
] as const;

function publicCommand(name: string) {
const entry = findCommand(name);
if (!entry || entry.hidden) throw new Error(`Invalid public help group command: ${name}`);
return entry;
}

function actionLines(name: string): string[] {
const entry = publicCommand(name);
const prefix = ` ocx ${entry.name}`.padEnd(16);
const lines: string[] = [];
let line = prefix;
for (const word of entry.summary.split(/\s+/)) {
if (line.trim() && line.length + word.length + 1 > 80 && line !== prefix) {
lines.push(line);
line = " ".repeat(prefix.length);
}
line += `${line.endsWith(" ") ? "" : " "}${word}`;
}
lines.push(line);
return lines;
}

export function renderRootHelp(): string {
const lines = ["opencodex (ocx) — Universal provider proxy for Codex", "Usage: ocx <command> [options]"];
for (const group of ACTION_GROUPS) {
lines.push("", `${group.title}:`, ...group.commands.flatMap(actionLines));
}
lines.push("", "Explore:");
for (const group of FAMILY_GROUPS) lines.push(` ${group.map(name => publicCommand(name).name).join(" ")}`);
lines.push("", "More help:",
" ocx help <command> Command usage and declared topics",
" ocx help --all Complete command reference",
" ocx capabilities --json Declared capabilities for scripts");
return lines.join("\n");
}
36 changes: 22 additions & 14 deletions src/cli/help.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { resolveHelpPath } from "./help-catalog";
import { MODELS_CONTEXT_DETAILS, MODELS_CONTEXT_USAGE } from "./help-models-context";
import { renderRootHelp } from "./help-navigation";
import { packageVersion as readPackageVersion } from "../lib/package-version";

/**
Expand All @@ -18,7 +19,7 @@ export function printVersion(): void {
}

export function printUsage(): void {
printFullUsage();
console.log(renderRootHelp());
}

export function printFullUsage(): void {
Expand Down Expand Up @@ -123,14 +124,15 @@ export function hasHelpFlag(values: string[]): boolean {
export function printSubcommandUsage(
name: string | undefined,
path?: readonly string[],
options: { fallbackToParent?: boolean } = {},
options: { fallbackToParent?: boolean; write?: (text: string) => void } = {},
): void {
const write = options.write ?? console.log;
const result = resolveHelpPath(path ?? (name ? [name] : []));
if (result.kind === "unavailable") {
// Appended flags may follow runtime operands. An explicit `help <path>`
// requests that exact detail instead, so only the CLI head enables fallback.
if (options.fallbackToParent && result.parent) {
printSubcommandUsage(result.parent[0], result.parent);
printSubcommandUsage(result.parent[0], result.parent, { write });
return;
}
if (result.parent) {
Expand All @@ -143,26 +145,32 @@ export function printSubcommandUsage(
process.exit(1);
}
if (result.kind === "entry") {
console.log(`Usage: ${result.entry.usage}\n\n${result.entry.summary}`);
if (result.entry.details?.length) console.log(`\n${result.entry.details.join("\n")}`);
write(`Usage: ${result.entry.usage}\n\n${result.entry.summary}`);
if (result.entry.details?.length) write(`\n${result.entry.details.join("\n")}`);
if (result.children.length) {
write("\nDeclared commands (incomplete):");
for (const child of result.children) write(` ocx help ${child.command.join(" ")} ${child.summary}`);
}
if (result.canonicalName === "models") write("\nContext cap help: ocx help models context");
if (result.entry.name !== result.canonicalName) write(`\nCanonical help: ocx help ${result.canonicalName}`);
return;
}
if (result.kind === "models-context") {
console.log(`Usage:\n${MODELS_CONTEXT_USAGE}\n\n${MODELS_CONTEXT_DETAILS.join("\n")}`);
write(`Usage:\n${MODELS_CONTEXT_USAGE}\n\n${MODELS_CONTEXT_DETAILS.join("\n")}`);
} else if (result.kind === "capability") {
const { capability } = result;
console.log(`Command: ocx ${result.path.join(" ")}\n\n${capability.summary}`);
write(`Command: ocx ${result.path.join(" ")}\n\n${capability.summary}`);
if (capability.flags.length) {
console.log("\nDeclared flags:");
write("\nDeclared flags:");
for (const flag of capability.flags) {
console.log(` ${flag.name}${flag.value && flag.value !== "boolean" ? ` <${flag.value}>` : ""}${flag.required ? " (required)" : ""} ${flag.summary}`);
write(` ${flag.name}${flag.value && flag.value !== "boolean" ? ` <${flag.value}>` : ""}${flag.required ? " (required)" : ""} ${flag.summary}`);
}
}
if (capability.details?.length) console.log(`\n${capability.details.join("\n")}`);
console.log("\nCapability metadata is incomplete; this is not the full operand grammar.");
if (capability.details?.length) write(`\n${capability.details.join("\n")}`);
write("\nCapability metadata is incomplete; this is not the full operand grammar.");
} else {
console.log(`Command group: ocx ${result.path.join(" ")}\n\nDeclared commands (incomplete):`);
for (const child of result.children) console.log(` ocx ${child.command.join(" ")} ${child.summary}`);
write(`Command group: ocx ${result.path.join(" ")}\n\nDeclared commands (incomplete):`);
for (const child of result.children) write(` ocx ${child.command.join(" ")} ${child.summary}`);
}
console.log(`\nParent help: ocx help ${result.path.slice(0, -1).join(" ")}`);
write(`\nParent help: ocx help ${result.path.slice(0, -1).join(" ")}`);
}
Loading
Loading