diff --git a/CHANGELOG.md b/CHANGELOG.md index 006d191f2..31f1dee0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Extension bumped to 1.0.9 (Accessibility.enable allowlist + downloads permission ### 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. * **browser state** — add opt-in AX snapshot refs via `browser state --source ax`, including backend-node click resolution and role/name stale-ref recovery for the Phase 0 browser-agent runtime prototype. * **browser state** — AX snapshots now include same-origin iframe refs, and `browser state --compare-sources` prints DOM-vs-AX observation metrics for the Phase 1 default-source decision without dumping page contents. * **browser locators** — `browser find`, `browser click`, and `browser get text|value|attributes` now accept semantic locator flags (`--role`, `--name`, `--label`, `--text`, `--testid`) so agents can act on common controls without a separate state-ref lookup. diff --git a/src/cli.test.ts b/src/cli.test.ts index 18eeda650..e7a68b1f6 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -502,6 +502,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 [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 [options]', + positionals: [ + { name: 'contextId', required: true }, + { name: 'alias', required: true }, + ], + }); + } finally { + process.argv = argv; + } + }); }); describe('resolveBrowserVerifyInvocation', () => { diff --git a/src/cli.ts b/src/cli.ts index ef74647be..16726edc5 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2870,6 +2870,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') @@ -3064,6 +3066,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') @@ -3179,6 +3183,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') @@ -3260,6 +3266,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') @@ -3393,6 +3401,10 @@ cli({ const adapterGroups: RootAdapterGroups = { external: externalNames, apps, sites }; const adapterNameSet = new Set([...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())), });