{"version":3,"file":"repl-loop.mjs","names":[],"sources":["../../src/feature/repl-loop.ts"],"sourcesContent":["import type { StandardSchemaV1 } from '@standard-schema/spec';\nimport { buildReplCompleter, findCommandByName, getCommandRuntime } from '../core/commands.ts';\nimport { createTerminalReplSession } from '../core/default-runtime.ts';\nimport { REPL_SIGINT, type ReplSessionConfig } from '../core/runtime.ts';\nimport type { AnyPadroneCommand, PadroneEvalPreferences, PadroneReplPreferences } from '../types/index.ts';\nimport { getVersion } from '../util/utils.ts';\n\nexport type ReplDeps = {\n  existingCommand: AnyPadroneCommand;\n  evalCommand: (input: string, prefs?: PadroneEvalPreferences) => any;\n  replActiveRef: { value: boolean };\n};\n\n/**\n * Creates a REPL async iterable for running commands interactively.\n */\nexport function createReplIterator(deps: ReplDeps, options?: PadroneReplPreferences): AsyncIterable<any> & { drain: () => Promise<any> } {\n  const { existingCommand, evalCommand, replActiveRef } = deps;\n\n  if (replActiveRef.value) {\n    const runtime = getCommandRuntime(existingCommand);\n    runtime.error('REPL is already running. Nested REPL sessions are not supported.');\n    return (async function* () {})() as any;\n  }\n\n  const runtime = getCommandRuntime(existingCommand);\n\n  const programName = existingCommand.name || 'padrone';\n  const env = runtime.env();\n  const useAnsi = runtime.format === 'ansi' || (runtime.format === 'auto' && !env.NO_COLOR && !env.CI && runtime.terminal?.isTTY === true);\n\n  // Track command history for .history built-in\n  const commandHistory: string[] = [];\n\n  // Resolve the initial scope command from options.scope (command path like 'db' or 'db migrate')\n  const resolveScope = (scope: string): AnyPadroneCommand[] => {\n    const parts = scope.split(/\\s+/);\n    const stack: AnyPadroneCommand[] = [];\n    let current = existingCommand;\n    for (const part of parts) {\n      const found = findCommandByName(part, current.commands);\n      if (!found) break;\n      stack.push(found);\n      current = found;\n    }\n    return stack;\n  };\n\n  async function* replIterator() {\n    replActiveRef.value = true;\n    const showGreeting = options?.greeting !== false;\n    const showHint = options?.hint !== false;\n\n    // Empty line before greeting/hint block\n    if (showGreeting || showHint) runtime.output('');\n\n    // Greeting: default shows program title (or name) + version, like \"Welcome to My App v1.0.0\"\n    if (showGreeting) {\n      if (options?.greeting) {\n        runtime.output(options.greeting);\n      } else {\n        const displayName = existingCommand.title || programName;\n        const version = existingCommand.version ? await getVersion(existingCommand.version) : undefined;\n        const greeting = version ? `Welcome to ${displayName} v${version}` : `Welcome to ${displayName}`;\n        runtime.output(greeting);\n      }\n    }\n\n    // Hint: dimmed text below greeting\n    if (showHint) {\n      const hintText =\n        (typeof options?.hint === 'string' ? options.hint : undefined) ?? 'Type \".help\" for more information, \".exit\" to quit.';\n      runtime.output(useAnsi ? `\\x1b[2m${hintText}\\x1b[0m` : hintText);\n    }\n\n    // Empty line after greeting/hint block\n    if (showGreeting || showHint) runtime.output('');\n\n    // Scope stack for nested/contextual REPLs.\n    // `cd <subcommand>` pushes, `cd ..`/`..` pops. The scope path is prepended to all eval input.\n    const scopeStack: AnyPadroneCommand[] = options?.scope ? resolveScope(options.scope) : [];\n\n    const getScopeCommand = () => (scopeStack.length ? scopeStack[scopeStack.length - 1]! : existingCommand);\n    const getScopePath = () => scopeStack.map((c) => c.name).join(' ');\n\n    const buildPrompt = () => {\n      if (options?.prompt) return typeof options.prompt === 'function' ? options.prompt() : options.prompt;\n      const scopePath = getScopePath();\n      const label = scopePath ? `${programName}/${scopePath.replace(/ /g, '/')}` : programName;\n      return useAnsi ? `\\x1b[1m${label}\\x1b[0m ❯ ` : `${label} ❯ `;\n    };\n\n    // Build completer scoped to the current command\n    const buildScopedCompleter = () => {\n      const scopeCmd = getScopeCommand();\n      const inScope = scopeStack.length > 0;\n      return buildReplCompleter(scopeCmd, { inScope });\n    };\n\n    // Build session config with completer\n    const sessionConfig: ReplSessionConfig = { history: options?.history };\n    if (options?.completion !== false) {\n      sessionConfig.completer = buildScopedCompleter();\n    }\n\n    // If the runtime provides a custom readLine, use it (stateless, no history/completion).\n    // Otherwise, create a persistent terminal session with history + tab completion.\n    const session = runtime.readLine ? undefined : createTerminalReplSession(sessionConfig);\n    const questionFn = session ? (prompt: string) => session.question(prompt) : runtime.readLine!;\n\n    // Update the session's completer when scope changes\n    const updateCompleter = () => {\n      if (options?.completion === false) return;\n      const completer = buildScopedCompleter();\n      if (session) session.completer = completer;\n      sessionConfig.completer = completer;\n    };\n\n    // Track last SIGINT time for double Ctrl+C to exit\n    let lastSigintTime = 0;\n\n    try {\n      while (true) {\n        const promptStr = buildPrompt();\n        const input = await questionFn(promptStr);\n\n        // EOF (Ctrl+D, closed connection)\n        if (input === null) break;\n\n        // Handle Ctrl+C (SIGINT sentinel from terminal session)\n        if (input === REPL_SIGINT) {\n          const now = Date.now();\n          if (now - lastSigintTime < 2000) break; // Double Ctrl+C within 2s → exit\n          lastSigintTime = now;\n          runtime.output('(press Ctrl+C again to exit, or Ctrl+D)');\n          continue;\n        }\n\n        const trimmed = input.trim();\n        if (!trimmed) continue;\n\n        // Reset SIGINT timer on any real input\n        lastSigintTime = 0;\n\n        // Track command history for .history\n        commandHistory.push(trimmed);\n\n        // Dot-prefixed built-in REPL commands\n        if (trimmed === '.exit' || trimmed === '.quit') break;\n        if (trimmed === '.clear') {\n          runtime.output('\\x1B[2J\\x1B[H');\n          continue;\n        }\n        if (trimmed === '.help') {\n          const lines = [\n            'REPL Commands:',\n            '  .                 Execute the current scoped command',\n            '  .help             Print this help message',\n            '  .exit             Exit the REPL',\n            '  .clear            Clear the screen',\n            '  .history          Show command history',\n            '  .scope <cmd>      Scope into a subcommand',\n            '  .scope ..         Go up one scope level',\n          ];\n          lines.push(\n            '',\n            'Keybindings:',\n            '  Ctrl+C       Cancel current line (press twice to exit)',\n            '  Ctrl+D       Exit the REPL',\n            '  Up/Down      Navigate history',\n            '  Tab          Auto-complete',\n            '',\n            'Type \"help\" to see available commands.',\n          );\n          runtime.output(lines.join('\\n'));\n          continue;\n        }\n        if (trimmed === '.history') {\n          // Show all previous entries (excluding the .history command itself)\n          const entries = commandHistory.slice(0, -1);\n          if (entries.length === 0) {\n            runtime.output('No history.');\n          } else {\n            runtime.output(entries.map((entry, i) => `${i + 1}  ${entry}`).join('\\n'));\n          }\n          continue;\n        }\n\n        // `.scope <subcommand>` — scope the REPL to a command subtree\n        // `.scope ..` or `..` — go up one scope level\n        if (trimmed.startsWith('.scope ') || trimmed === '.scope') {\n          const target = trimmed.slice(6).trim();\n          if (target === '..' || target === '') {\n            if (scopeStack.length > 0) {\n              scopeStack.pop();\n              updateCompleter();\n            }\n          } else {\n            const scopeCmd = getScopeCommand();\n            const found = findCommandByName(target, scopeCmd.commands);\n            if (found) {\n              if (found.commands?.length) {\n                scopeStack.push(found);\n                updateCompleter();\n              } else {\n                runtime.error(`\"${target}\" has no subcommands to scope into.`);\n              }\n            } else {\n              runtime.error(`Unknown command: ${target}`);\n            }\n          }\n          continue;\n        }\n\n        // `..` shorthand for `.scope ..`\n        if (trimmed === '..') {\n          if (scopeStack.length > 0) {\n            scopeStack.pop();\n            updateCompleter();\n          }\n          continue;\n        }\n\n        // `.` (bare dot) — execute the current command (scoped or root)\n        let evalInput = trimmed;\n        if (trimmed === '.') {\n          evalInput = '';\n        }\n\n        const prefix = options?.outputPrefix;\n        const prefixLines = prefix\n          ? (text: string) =>\n              text\n                .split('\\n')\n                .map((l) => prefix + l)\n                .join('\\n')\n          : undefined;\n\n        // Temporarily patch runtime on all commands so handler output gets prefixed.\n        // Commands store parent refs from build time, so we patch each command directly.\n        const savedRuntimes: { cmd: AnyPadroneCommand; runtime: typeof existingCommand.runtime }[] = [];\n        if (prefixLines) {\n          const prefixedRuntime = {\n            ...existingCommand.runtime,\n            output: (...args: unknown[]) => {\n              const first = args[0];\n              runtime.output(typeof first === 'string' ? prefixLines(first) : first, ...args.slice(1));\n            },\n            error: (text: string) => runtime.error(prefixLines(text)),\n          };\n          const patchAll = (cmd: AnyPadroneCommand) => {\n            savedRuntimes.push({ cmd, runtime: cmd.runtime });\n            cmd.runtime = prefixedRuntime;\n            cmd.commands?.forEach(patchAll);\n          };\n          patchAll(existingCommand);\n        }\n\n        // Resolve before/after spacing from the shorthand or object form\n        const sp = options?.spacing;\n        const isSpacingObject = typeof sp === 'object' && sp !== null && !Array.isArray(sp);\n        const spacingBefore = isSpacingObject ? sp.before : sp;\n        const spacingAfter = isSpacingObject ? sp.after : sp;\n\n        const emitSpacingLine = (value: boolean | string) => {\n          if (typeof value === 'string') {\n            const sep = value.length === 1 ? value.repeat(runtime.terminal?.columns ?? 80) : value;\n            runtime.output(sep);\n          } else if (value) {\n            runtime.output('');\n          }\n        };\n        const emitSpacing = (value: typeof spacingBefore) => {\n          if (!value) return;\n          if (Array.isArray(value)) {\n            for (const line of value) emitSpacingLine(line);\n          } else {\n            emitSpacingLine(value);\n          }\n        };\n\n        emitSpacing(spacingBefore);\n\n        // Prepend scope path so evalCommand resolves relative to root\n        const scopePath = getScopePath();\n        const scopedInput = scopePath ? (evalInput ? `${scopePath} ${evalInput}` : scopePath) : evalInput;\n\n        try {\n          const replEvalPrefs: PadroneEvalPreferences | undefined = { caller: 'repl' };\n          const result = await evalCommand(scopedInput, replEvalPrefs);\n          if (result.error) {\n            const msg = result.error instanceof Error ? result.error.message : String(result.error);\n            runtime.error(prefixLines ? prefixLines(msg) : msg);\n          } else if (result.argsResult?.issues) {\n            const issueMessages = result.argsResult.issues\n              .map((i: StandardSchemaV1.Issue) => `  - ${i.path?.join('.') || 'root'}: ${i.message}`)\n              .join('\\n');\n            const msg = `Validation error:\\n${issueMessages}`;\n            runtime.error(prefixLines ? prefixLines(msg) : msg);\n          }\n          yield result as any;\n        } catch (err) {\n          const msg = err instanceof Error ? err.message : String(err);\n          runtime.error(prefixLines ? prefixLines(msg) : msg);\n        } finally {\n          for (const { cmd, runtime: saved } of savedRuntimes) cmd.runtime = saved;\n          emitSpacing(spacingAfter);\n        }\n      }\n    } finally {\n      replActiveRef.value = false;\n      session?.close();\n    }\n  }\n\n  const iterable = replIterator();\n  (iterable as any).drain = async () => {\n    try {\n      const results: any[] = [];\n      for await (const result of iterable) results.push(result);\n      return { value: results };\n    } catch (err) {\n      return { error: err };\n    }\n  };\n  return iterable as any;\n}\n"],"mappings":"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"}