// `rnx describe` — format the pruned visible semantic tree. import { maybeHint } from '../../hints' import { inspectDescribe, type DescribeDumpOptions } from './core' import { maybeWaitForStartedScreenTransitions, printJson, sleep, wantsJson, } from './shared' import type { WsBridge } from '../../ws-bridge' export interface DescribeOptions { bridge: WsBridge args: string[] positional: string[] } function flagValue(args: string[], name: string): string | null { const i = args.indexOf(name) return i >= 0 && i + 1 < args.length ? args[i + 1] : null } export async function runDescribeSubcommand(opts: DescribeOptions): Promise { const { bridge, args, positional } = opts const verbose = args.includes('--verbose') || args.includes('-v') const jsonOut = wantsJson(args) const verboseDescribeTransitions = verbose && !jsonOut const watch = args.includes('--watch') || args.includes('-w') const watchInterval = 1000 const compact = args.includes('--compact') const hideXy = args.includes('--no-xy') const testIdLike = flagValue(args, '--testid-like') const onlyGlob = flagValue(args, '--only') const subtreeRoot = flagValue(args, '--subtree') // bare positional still works — routes to --only if it looks globby, // otherwise preserves the old substring-match behavior as `filter`. guard // against a flag-looking token leaking in as a filter: an unrecognized // `--flag` that the arg parser didn't strip must never become a text filter // (that's how `describe --compact` used to match nothing). const positionalFilter = positional[1] && !positional[1].startsWith('-') ? positional[1] : undefined const bareIsGlob = positionalFilter ? /[*?]/.test(positionalFilter) : false const filter = !bareIsGlob && !onlyGlob ? positionalFilter : undefined const effectiveOnlyGlob = onlyGlob ?? (bareIsGlob ? positionalFilter : undefined) const runOnce = async () => { await maybeWaitForStartedScreenTransitions(bridge, { verbose: verboseDescribeTransitions, }) const dumpOpts: DescribeDumpOptions = { describe: true, verbose, filter: filter || '', testIdLike: testIdLike || undefined, onlyGlob: effectiveOnlyGlob || undefined, subtreeRoot: subtreeRoot || undefined, compact, hideXy, // no terminal to fit, so nothing gets clipped on the way out fullText: jsonOut, } const result = await inspectDescribe(bridge, dumpOpts) const tree = result?.tree if (jsonOut) { printJson({ tree: tree ?? '' }) return } if (typeof tree === 'string' && tree.startsWith('__SUBTREE_NOT_FOUND__:')) { const missing = tree.slice('__SUBTREE_NOT_FOUND__:'.length) console.log(` subtree root not found: ${missing}`) maybeHint('subtree-root-not-found', missing) return } if (!tree) { const nodeCount = result?.nodeCount ?? 0 console.log(' no matching nodes found') const anyFilterActive = filter || testIdLike || effectiveOnlyGlob || subtreeRoot if (!anyFilterActive && nodeCount < 10) { maybeHint('app-still-loading', nodeCount) } return } console.log(tree) const anyFilterActive = filter || testIdLike || effectiveOnlyGlob || subtreeRoot const searchFilterActive = filter || testIdLike || effectiveOnlyGlob if (searchFilterActive && !watch) { maybeHint('describe-filter-context') } // when no filter was active and the tree got long, nudge once per // session toward the narrowing flags — this is exactly the case the // user flagged where "which row is the swap sheet" got buried under // 100+ lines of home content. if (!anyFilterActive && !watch) { const lineCount = tree.split('\n').length if (lineCount >= 80) { maybeHint('describe-use-filters') } } } if (watch) { console.log(' watching... (Ctrl+C to stop)\n') while (true) { console.clear() await runOnce() await sleep(watchInterval) } } else { await runOnce() } }