{"version":3,"file":"discovery.mjs","names":[],"sources":["../../src/codegen/discovery.ts"],"sourcesContent":["import { execFile } from 'node:child_process';\nimport { readFile } from 'node:fs/promises';\n\nimport { parseBashCompletions } from './parsers/bash.ts';\nimport { parseFishCompletions } from './parsers/fish.ts';\nimport { parseHelpOutput } from './parsers/help.ts';\nimport { mergeCommandMeta } from './parsers/merge.ts';\nimport { parseZshCompletions } from './parsers/zsh.ts';\nimport type { CommandMeta, GeneratorLogger } from './types.ts';\n\nexport type DiscoverySource = 'help' | 'completion' | 'bash' | 'fish' | 'zsh';\n\nexport interface DiscoveryOptions {\n  /** The command to discover (e.g. 'gh', 'docker', 'kubectl'). */\n  command: string;\n  /**\n   * Which parsing sources to use. Default: ['help'].\n   * Use `'completion'` to auto-detect the best shell completion source\n   * by probing `<cmd> completion <shell>` (bash → fish → zsh).\n   */\n  sources?: DiscoverySource[];\n  /** Max subcommand depth. 0 = root only, undefined = unlimited. */\n  depth?: number;\n  /** Delay in ms between help invocations. Default: 50 */\n  delay?: number;\n  /** Logger for progress reporting. */\n  log?: GeneratorLogger;\n  /** Timeout per help invocation in ms. Default: 10000 */\n  timeout?: number;\n}\n\nexport interface DiscoveryResult {\n  /** The discovered command tree. */\n  command: CommandMeta;\n  /** Number of help invocations made. */\n  invocations: number;\n  /** Errors encountered (non-fatal). */\n  warnings: string[];\n}\n\n/**\n * Discover CLI structure by running --help recursively and optionally\n * parsing shell completion scripts.\n */\nexport async function discoverCli(options: DiscoveryOptions): Promise<DiscoveryResult> {\n  const { command, sources: rawSources = ['help'], depth, delay = 50, log, timeout = 10000 } = options;\n\n  // Resolve 'completion' source by probing for the best available shell\n  const sources = await resolveSources(rawSources, command, timeout, log);\n\n  const warnings: string[] = [];\n  let invocations = 0;\n\n  const results: CommandMeta[] = [];\n\n  // Source 1: --help recursive crawl\n  if (sources.includes('help')) {\n    log?.info(`Discovering ${command} via --help...`);\n    const helpResult = await crawlHelp(command, [], {\n      depth,\n      delay,\n      timeout,\n      log,\n      onInvocation: () => {\n        invocations++;\n      },\n      onWarning: (msg) => {\n        warnings.push(msg);\n      },\n    });\n    results.push(helpResult);\n  }\n\n  // Source 2: Bash completions\n  if (sources.includes('bash')) {\n    log?.info(`Parsing bash completions for ${command}...`);\n    const bashText = await getCompletionScript(command, 'bash', timeout);\n    if (bashText) {\n      results.push(parseBashCompletions(bashText));\n    } else {\n      warnings.push('Could not obtain bash completion script');\n    }\n  }\n\n  // Source 3: Fish completions\n  if (sources.includes('fish')) {\n    log?.info(`Parsing fish completions for ${command}...`);\n    const fishText = await getCompletionScript(command, 'fish', timeout);\n    if (fishText) {\n      results.push(parseFishCompletions(fishText));\n    } else {\n      warnings.push('Could not obtain fish completion script');\n    }\n  }\n\n  // Source 4: Zsh completions\n  if (sources.includes('zsh')) {\n    log?.info(`Parsing zsh completions for ${command}...`);\n    const zshText = await getCompletionScript(command, 'zsh', timeout);\n    if (zshText) {\n      results.push(parseZshCompletions(zshText));\n    } else {\n      warnings.push('Could not obtain zsh completion script');\n    }\n  }\n\n  const merged = results.length > 0 ? mergeCommandMeta(...results) : { name: command };\n\n  // Ensure the root has the correct name\n  if (!merged.name) merged.name = command;\n\n  return { command: merged, invocations, warnings };\n}\n\ninterface CrawlOptions {\n  depth?: number;\n  delay: number;\n  timeout: number;\n  log?: GeneratorLogger;\n  onInvocation: () => void;\n  onWarning: (msg: string) => void;\n}\n\n/**\n * Breadth-first crawl of --help output.\n */\nasync function crawlHelp(command: string, prefixArgs: string[], options: CrawlOptions): Promise<CommandMeta> {\n  const fullCmd = [command, ...prefixArgs].join(' ');\n\n  options.onInvocation();\n  const helpText = await runHelp(command, prefixArgs, options.timeout);\n\n  if (!helpText) {\n    options.onWarning(`No help output from: ${fullCmd} --help`);\n    return { name: prefixArgs[prefixArgs.length - 1] || command };\n  }\n\n  const name = prefixArgs[prefixArgs.length - 1] || command;\n  const parsed = parseHelpOutput(helpText, { name });\n\n  options.log?.info(`  ${fullCmd}: ${parsed.subcommands?.length || 0} subcommands, ${parsed.arguments?.length || 0} options`);\n\n  // Recurse into subcommands breadth-first\n  const currentDepth = prefixArgs.length;\n  if (parsed.subcommands && parsed.subcommands.length > 0 && (options.depth === undefined || currentDepth < options.depth)) {\n    const resolvedSubs: CommandMeta[] = [];\n\n    for (const sub of parsed.subcommands) {\n      if (options.delay > 0) {\n        await sleep(options.delay);\n      }\n      const resolved = await crawlHelp(command, [...prefixArgs, sub.name], options);\n      // Preserve description from parent's subcommand list if child didn't have one\n      if (!resolved.description && sub.description) {\n        resolved.description = sub.description;\n      }\n      resolvedSubs.push(resolved);\n    }\n\n    parsed.subcommands = resolvedSubs;\n  }\n\n  return parsed;\n}\n\n/**\n * Run `<cmd> --help` or `<cmd> help` and return combined stdout+stderr.\n */\nasync function runHelp(command: string, args: string[], timeout: number): Promise<string | null> {\n  // Try --help first\n  let result = await runCommand(command, [...args, '--help'], timeout);\n  if (result) return result;\n\n  // Some CLIs use `help <cmd>` instead\n  if (args.length > 0) {\n    result = await runCommand(command, ['help', ...args], timeout);\n    if (result) return result;\n  }\n\n  return null;\n}\n\n/**\n * Run a command and return its combined output, or null on failure.\n */\nasync function runCommand(command: string, args: string[], timeout: number): Promise<string | null> {\n  try {\n    const { stdout, stderr } = await new Promise<{ stdout: string; stderr: string }>((resolve) => {\n      execFile(command, args, { timeout, maxBuffer: 10 * 1024 * 1024 }, (_error, stdout, stderr) => {\n        // Resolve even on non-zero exit — many CLIs exit non-zero on --help\n        resolve({ stdout: (stdout ?? '').trim(), stderr: (stderr ?? '').trim() });\n      });\n    });\n\n    // Some CLIs output help to stderr, some exit non-zero on --help\n    const combined = stdout || stderr;\n    if (!combined) return null;\n\n    // Basic sanity check: help text usually has some structure\n    if (combined.length < 10) return null;\n\n    return combined;\n  } catch {\n    return null;\n  }\n}\n\n/**\n * Try to get a shell completion script for a command.\n * Checks both `<cmd> completion <shell>` and well-known file paths.\n */\nasync function getCompletionScript(command: string, shell: 'bash' | 'fish' | 'zsh', timeout: number): Promise<string | null> {\n  // Try `<cmd> completion <shell>`\n  const completionArgs = ['completion', shell];\n  let result = await runCommand(command, completionArgs, timeout);\n  if (result) return result;\n\n  // Try `<cmd> completions <shell>`\n  result = await runCommand(command, ['completions', shell], timeout);\n  if (result) return result;\n\n  // Try reading from well-known paths\n  const paths =\n    shell === 'fish'\n      ? [`/usr/share/fish/vendor_completions.d/${command}.fish`, `/usr/local/share/fish/vendor_completions.d/${command}.fish`]\n      : shell === 'bash'\n        ? [\n            `/usr/share/bash-completion/completions/${command}`,\n            `/usr/local/share/bash-completion/completions/${command}`,\n            `/etc/bash_completion.d/${command}`,\n          ]\n        : [`/usr/share/zsh/site-functions/_${command}`, `/usr/local/share/zsh/site-functions/_${command}`];\n\n  for (const path of paths) {\n    try {\n      return await readFile(path, 'utf-8');\n    } catch {}\n  }\n\n  return null;\n}\n\n/**\n * Detect the best shell for completion parsing by probing the command.\n * Tries `<cmd> completion <shell>` for bash, fish, zsh (in that order).\n * Returns the shell name if successful, or null if no completion command exists.\n */\nexport async function detectCompletionShell(command: string, timeout = 5000): Promise<'bash' | 'fish' | 'zsh' | null> {\n  for (const shell of ['bash', 'fish', 'zsh'] as const) {\n    const result = await getCompletionScript(command, shell, timeout);\n    if (result) return shell;\n  }\n  return null;\n}\n\n/**\n * Resolve 'completion' entries in the sources array by probing for the best available shell.\n * Other sources are passed through unchanged.\n */\nasync function resolveSources(\n  sources: DiscoverySource[],\n  command: string,\n  timeout: number,\n  log?: GeneratorLogger,\n): Promise<DiscoverySource[]> {\n  if (!sources.includes('completion')) return sources;\n\n  log?.info(`Probing ${command} for completion command...`);\n  const shell = await detectCompletionShell(command, timeout);\n\n  return sources.flatMap((s) => {\n    if (s !== 'completion') return s;\n    if (shell) {\n      log?.info(`  Found ${shell} completion support`);\n      return shell;\n    }\n    log?.info('  No completion command found, skipping');\n    return [];\n  });\n}\n\nfunction sleep(ms: number): Promise<void> {\n  return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"],"mappings":"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"}