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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Features

* **help / browser** — `opencli browser --help -f yaml|json` now emits a structured, agent-ready index of all browser leaf commands (including nested `tab`, `get`, and `dialog` commands), their positionals, command options, namespace options, and root global options. Individual browser commands also support structured help, backed by a shared Commander option/argument spec extractor.
* **help / built-in namespaces** — `opencli daemon|plugin|adapter|profile --help -f yaml|json` now emit the same structured payload as `browser`. One agent call returns every leaf's positionals, options, descriptions, and global options — no per-leaf `--help` follow-ups needed. Original namespace descriptions are preserved through `applyRootSubcommandSummaries()` via a snapshot at namespace declaration time.

### Bug Fixes

Expand Down
108 changes: 108 additions & 0 deletions src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,114 @@ describe('createProgram root help descriptions', () => {
process.argv = argv;
}
});

it('renders daemon namespace structured help with leaves and global options', () => {
const argv = process.argv;
try {
const program = createProgram('', '');
const daemon = program.commands.find(cmd => cmd.name() === 'daemon')!;
expect(daemon).toBeTruthy();

process.argv = ['node', 'opencli', 'daemon', '--help', '-f', 'yaml'];
const data = yaml.load(daemon.helpInformation()) as any;

expect(data).toMatchObject({
namespace: 'daemon',
command: 'opencli daemon',
usage: 'opencli daemon <command> [args] [options]',
description: 'Manage the opencli daemon',
command_count: 3,
namespace_options: [],
structured_help: { usage: 'opencli daemon --help -f yaml' },
});
expect(data.commands.map((cmd: any) => cmd.name)).toEqual(['restart', 'status', 'stop']);
expect(data.global_options.map((option: any) => option.name)).toEqual(expect.arrayContaining(['version', 'profile']));
} finally {
process.argv = argv;
}
});

it('renders plugin namespace structured help with positional + option leaves', () => {
const argv = process.argv;
try {
const program = createProgram('', '');
const plugin = program.commands.find(cmd => cmd.name() === 'plugin')!;
expect(plugin).toBeTruthy();

process.argv = ['node', 'opencli', 'plugin', '--help', '-f', 'yaml'];
const data = yaml.load(plugin.helpInformation()) as any;

expect(data).toMatchObject({
namespace: 'plugin',
command: 'opencli plugin',
description: 'Manage opencli plugins',
namespace_options: [],
});
expect(data.commands.map((cmd: any) => cmd.name)).toEqual(['create', 'install', 'list', 'uninstall', 'update']);
const update = data.commands.find((cmd: any) => cmd.name === 'update');
expect(update).toMatchObject({
usage: 'opencli plugin update [name] [options]',
positionals: [{ name: 'name' }],
});
expect(update.command_options.map((option: any) => option.name)).toEqual(['all']);
} finally {
process.argv = argv;
}
});

it('renders adapter namespace structured help preserving original description after applyRootSubcommandSummaries', () => {
const argv = process.argv;
try {
const program = createProgram('', '');
const adapter = program.commands.find(cmd => cmd.name() === 'adapter')!;
expect(adapter).toBeTruthy();

process.argv = ['node', 'opencli', 'adapter', '--help', '-f', 'yaml'];
const data = yaml.load(adapter.helpInformation()) as any;

// applyRootSubcommandSummaries() rewrites .description() to a child-name listing;
// structured help must surface the original product description via the snapshot.
expect(data.description).toBe('Manage CLI adapters');
expect(data.commands.map((cmd: any) => cmd.name)).toEqual(['eject', 'reset', 'status']);
const reset = data.commands.find((cmd: any) => cmd.name === 'reset');
expect(reset).toMatchObject({
usage: 'opencli adapter reset [site] [options]',
positionals: [{ name: 'site' }],
});
expect(reset.command_options.map((option: any) => option.name)).toEqual(['all']);
} finally {
process.argv = argv;
}
});

it('renders profile namespace structured help including required positionals', () => {
const argv = process.argv;
try {
const program = createProgram('', '');
const profile = program.commands.find(cmd => cmd.name() === 'profile')!;
expect(profile).toBeTruthy();

process.argv = ['node', 'opencli', 'profile', '--help', '-f', 'yaml'];
const data = yaml.load(profile.helpInformation()) as any;

expect(data).toMatchObject({
namespace: 'profile',
description: 'Manage Browser Bridge Chrome profiles',
command_count: 3,
});
expect(data.commands.map((cmd: any) => cmd.name)).toEqual(['list', 'rename', 'use']);
const rename = data.commands.find((cmd: any) => cmd.name === 'rename');
expect(rename).toMatchObject({
usage: 'opencli profile rename <contextId> <alias> [options]',
positionals: [
{ name: 'contextId', required: true },
{ name: 'alias', required: true },
],
});
} finally {
process.argv = argv;
}
});
});

describe('resolveBrowserVerifyInvocation', () => {
Expand Down
12 changes: 12 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2260,6 +2260,8 @@ cli({
// ── Plugin management ──────────────────────────────────────────────────────

const pluginCmd = program.command('plugin').description('Manage opencli plugins');
// Snapshot before applyRootSubcommandSummaries() rewrites .description() to a child-name listing.
const originalPluginDescription = pluginCmd.description();

pluginCmd
.command('install')
Expand Down Expand Up @@ -2454,6 +2456,8 @@ cli({

// ── Built-in: adapter management ─────────────────────────────────────────
const adapterCmd = program.command('adapter').description('Manage CLI adapters');
// Snapshot before applyRootSubcommandSummaries() rewrites .description() to a child-name listing.
const originalAdapterDescription = adapterCmd.description();

adapterCmd
.command('status')
Expand Down Expand Up @@ -2569,6 +2573,8 @@ cli({

// ── Built-in: browser profile selection ──────────────────────────────────
const profileCmd = program.command('profile').description('Manage Browser Bridge Chrome profiles');
// Snapshot before applyRootSubcommandSummaries() rewrites .description() to a child-name listing.
const originalProfileDescription = profileCmd.description();

profileCmd
.command('list')
Expand Down Expand Up @@ -2650,6 +2656,8 @@ cli({

// ── Built-in: daemon ──────────────────────────────────────────────────────
const daemonCmd = program.command('daemon').description('Manage the opencli daemon');
// Snapshot before applyRootSubcommandSummaries() rewrites .description() to a child-name listing.
const originalDaemonDescription = daemonCmd.description();
daemonCmd
.command('status')
.description('Show daemon status')
Expand Down Expand Up @@ -2783,6 +2791,10 @@ cli({
const adapterGroups: RootAdapterGroups = { external: externalNames, apps, sites };
const adapterNameSet = new Set<string>([...externalNames, ...siteNames]);
installCommanderNamespaceStructuredHelp(browser, { globalCommand: program, description: originalBrowserDescription });
installCommanderNamespaceStructuredHelp(daemonCmd, { globalCommand: program, description: originalDaemonDescription });
installCommanderNamespaceStructuredHelp(pluginCmd, { globalCommand: program, description: originalPluginDescription });
installCommanderNamespaceStructuredHelp(adapterCmd, { globalCommand: program, description: originalAdapterDescription });
installCommanderNamespaceStructuredHelp(profileCmd, { globalCommand: program, description: originalProfileDescription });
program.configureHelp({
visibleCommands: (command) => command.commands.filter(child => command !== program || !adapterNameSet.has(child.name())),
});
Expand Down
Loading