import type { RnxBackendKind, RnxCommandInvocation, RnxParsedCommandInput, } from './command-contract' export type RnxBrowserCommandFamily = RnxCommandInvocation['family'] export interface RnxCommandRegistration { name: string node: boolean browserFamily: RnxBrowserCommandFamily | null } export interface RnxCommandAvailability { available: boolean reason: string | null } const NODE_COMMAND_NAMES = [ 'test', 'detox', 'maestro', 'record', 'film', 'storage', 'permissions', 'perf', 'screenshot', 'state', 'reset', 'camera', 'mode', 'debug', 'timeline', 'what-happened', 'shell', 'assert', 'device', 'open', 'ios', 'android', 'remote', 'preview', 'box', 'list', 'use', 'claim', 'close', 'compat', 'report-issue', 'desktop', 'login', 'logout', 'auth', 'cli', 'setup', 'serve', 'daemon', 'runtime', 'upgrade', 'version', 'skill', 'config', 'cleanup', 'app-fonts', 'agent-wrapper', 'agent', 'describe', 'find', 'get', 'do', 'wait', 'network', 'logs', ] satisfies string[] const BROWSER_FAMILIES = new Map([ ['web', 'preview'], ['ios', 'preview'], ['android', 'preview'], ['list', 'runtime'], ['use', 'runtime'], ['state', 'runtime'], ['describe', 'tree'], ['find', 'find'], ['get', 'runtime'], ['wait', 'wait'], ['do', 'action'], ['shell', 'action'], ['reset', 'action'], ['screenshot', 'screenshot'], ['logs', 'logs'], ['network', 'network'], ['storage', 'storage'], ['timeline', 'history'], ['what-happened', 'history'], ['assert', 'evidence'], ['debug', 'evidence'], ['perf', 'evidence'], ]) const registrations = [ ...new Set([...NODE_COMMAND_NAMES, 'web']), ].map((name) => ({ name, node: name !== 'web', browserFamily: BROWSER_FAMILIES.get(name) ?? null, })) export const RNX_COMMAND_REGISTRY: readonly RnxCommandRegistration[] = registrations export const RNX_COMMAND_NAMES = new Set(registrations.map((entry) => entry.name)) // retained for registry drift checks that compare the established node CLI // with its generated public docs. browser-only selectors have local help. export const RNX_NODE_COMMAND_NAMES = new Set(NODE_COMMAND_NAMES) const VALUE_FLAGS = new Set([ '--sim', '--session', '--tab', '--port', '-p', '--timeout', '--device', '--output', '-o', '--out', '--plan', '--level', '--filter', '--threshold', '--limit', '--since', '--kinds', ]) const BROWSER_SCREENSHOT_SUBCOMMANDS = new Set(['capture', 'ask', 'appearance', 'motion']) export function getRnxFirstPositional(args: readonly string[]): string | null { for (let index = 0; index < args.length; index++) { const arg = args[index] if (arg.startsWith('-')) { if (VALUE_FLAGS.has(arg)) index++ continue } return arg } return null } export function getRnxCommandRegistration(name: string): RnxCommandRegistration | null { return RNX_COMMAND_REGISTRY.find((entry) => entry.name === name) ?? null } export function getRnxCommandAvailability( command: string, args: readonly string[], backend: RnxBackendKind, ): RnxCommandAvailability { const registration = getRnxCommandRegistration(command) if (!registration) { return { available: false, reason: `unknown command: ${command}` } } if (backend === 'node') { return registration.node ? { available: true, reason: null } : { available: false, reason: 'rnx web is available in the browser backend, where it selects the existing web preview.', } } if (!registration.browserFamily) { return { available: false, reason: `rnx ${command} is unavailable in the browser backend; it requires the node backend.`, } } const subcommand = getRnxFirstPositional(args) if (command === 'shell' && subcommand && subcommand !== 'appearance') { return { available: false, reason: `rnx shell ${subcommand} is unavailable in the browser backend; Factory exposes no semantic operation for it.`, } } if ( command === 'shell' && subcommand === 'appearance' && args[1] !== undefined && args[1] !== 'light' && args[1] !== 'dark' ) { return { available: false, reason: 'rnx shell appearance supports light or dark in the browser backend.', } } if (command === 'storage' && subcommand === 'profile') { return { available: false, reason: 'rnx storage profile is unavailable in the browser backend; use rnx storage clear for the selected preview.', } } const screenshotSubcommand = command === 'screenshot' ? subcommand : null if ( screenshotSubcommand !== null && !BROWSER_SCREENSHOT_SUBCOMMANDS.has(screenshotSubcommand) ) { return { available: false, reason: `rnx screenshot ${screenshotSubcommand} is unavailable in the browser backend; it requires the node backend.`, } } if (command === 'timeline' && subcommand === 'dump') { return { available: false, reason: 'rnx timeline dump is unavailable in the browser backend because it writes to the host filesystem.', } } return { available: true, reason: null } } export function createRnxCommandInvocation( input: RnxParsedCommandInput, ): RnxCommandInvocation { const common = { command: input.command, args: [...input.args], globalFlags: { ...input.globalFlags }, } switch (input.command) { case 'web': return { ...common, family: 'preview', command: 'web', platform: 'web', selection: 'select-or-ensure-existing', } case 'ios': return { ...common, family: 'preview', command: 'ios', platform: 'ios', selection: 'select-or-ensure-existing', } case 'android': return { ...common, family: 'preview', command: 'android', platform: 'android', selection: 'select-or-ensure-existing', } case 'describe': return { ...common, family: 'tree' } case 'find': return { ...common, family: 'find', command: 'find' } case 'get': { const noun = getRnxFirstPositional(input.args) if (noun === 'tree' || noun === 'a11y') return { ...common, family: 'tree' } if (noun === 'layout') return { ...common, family: 'layout' } if (noun === 'requests') return { ...common, family: 'network' } if (noun === 'errors' || noun === 'warnings') return { ...common, family: 'logs' } if (noun === 'diagnosis') return { ...common, family: 'evidence' } return { ...common, family: 'runtime' } } case 'wait': return { ...common, family: 'wait', command: 'wait' } case 'do': case 'shell': case 'reset': case 'permissions': case 'camera': return { ...common, family: 'action' } case 'screenshot': return { ...common, family: 'screenshot', command: 'screenshot' } case 'logs': return { ...common, family: 'logs', command: 'logs' } case 'network': return { ...common, family: 'network', command: 'network' } case 'storage': return { ...common, family: 'storage', command: 'storage' } case 'timeline': return { ...common, family: 'history', command: 'timeline' } case 'what-happened': return { ...common, family: 'history', command: 'what-happened' } case 'assert': case 'debug': case 'perf': return { ...common, family: 'evidence' } default: return { ...common, family: 'runtime' } } }