Skip to content

feat: interactive gap-fill prompts for missing command input (Phase 1) - #772

Open
vscheuber wants to merge 4 commits into
mainfrom
feat/interactive-prompts
Open

vscheuber wants to merge 4 commits into
mainfrom
feat/interactive-prompts

Conversation

@vscheuber

Copy link
Copy Markdown
Contributor

Summary

Implements Phase 1 of INTERACTIVE-COMMANDS-PLAN.md (analysis doc on the rockcarver workspace root): on an interactive session, missing mandatory options and connection arguments are now prompted for before the action body runs. Every non-interactive surface (CI, scripts, MCP, e2e) behaves byte-identically to before — same messages, same exit codes, enforced by a structural gate, not convention.

frodo idm schema object describe on a TTY, before → after:

before:  error: required option '-o, --managed-object <type>' not specified
after:   ? Choose a connection profile:      (saved profiles, single skips silently)
         ? Enter a value for -o, --managed-object <type>: alpha_user

What's in the box

  • Prompt gate (PromptGateState.ts, unit-tested): prompts fire only when ALL of FRODO_NO_PROMPT unset, FRODO_TEST !== '1', stdin+stdout are TTYs, and setNeverPrompt() hasn't been called. Outside the gate the resolution pass degrades to exactly the old error behavior.
  • Prompts (PromptGate.ts): promptInput/promptPassword hand-built on @inquirer/core, matching the established hand-built pattern (EscapableSelectPrompt, JourneyDebugPrompt): Escape/empty-Enter resolve '' so callers keep their existing missing-input error paths (packaged prompts throw ExitPromptError instead). readline-buffer based (paste-safe); the completed line is mirrored from the preceding keystroke because rl.line is already consumed when Enter's keypress fires (verified against @inquirer/core 12.0.3 in an isolated PTY harness).
  • Resolution pass (ResolveInteractiveInputs.ts, wired via an async preAction hook — commander's hook chain awaits returned promises):
    • Host picker: saved connection profiles as an escapableSelect dropdown; single profile skipped silently (the runInteractivePreferredCredentialPicker convention); "Enter a host manually…" item; free-text when no profiles exist.
    • Username/password prompts only when nothing can answer: suppressed when a saved profile carries a usable credential (user+password, svcacct, or amster — frodo-lib falls back to it anyway); a credentials-less profile entry still prompts, prefilled with its saved username; --browser/--device suppress them (their flow IS the interactive part) but not the host prompt.
    • Deferred mandatory options: FrodoStubCommand.missingMandatoryOptionValue is reimplemented verbatim from commander (its typings omit the method) as the override point; FrodoCommand defers violations to the pass instead of throwing — which makes all 57 existing makeOptionMandatory() call sites prompt-capable with zero syntax change. Options with choices() get a dropdown.
  • --no-prompt on every FrodoCommand (Runtime options group) plus the FRODO_NO_PROMPT env var; frodo mcp server start --transport stdio calls setNeverPrompt() as belt-and-braces (stdio IS the protocol channel).
  • Dependency move: inquirer packages devDependencies → dependencies (production prompt surface now).

Two subtle bugs found and fixed during live PTY verification

  1. rl.line is already consumed when Enter's keypress fires — the first cut read it there and silently discarded every answer as "(no input)". Fixed by mirroring the buffer into prompt state on every keystroke.
  2. Positional injection must write both command.processedArgs (what commander's action wrapper slices positional parameters from) and command.args (what handleDefaultArgsAndOpts iterates to copy into frodo-lib state) — verified against commander's _processArguments (which never writes back) and this repo's shim.

Verification

All flows verified live in a real PTY (via the repo's generic test/utils/shell_pty_driver.py):

Flow Result
Mandatory-option prompt → value reaches action + frodo-lib state ✅ (error progressed to real connection attempt)
Host dropdown, aliases, "Enter a host manually…", free text ✅
Single-profile silent skip ✅
Username prefill from profile; credentials-less profile still prompts ✅
Password masked echo ✅
Escape abort → commander's byte-identical error + exit 1 ✅
--no-prompt / FRODO_NO_PROMPT on a TTY ✅
Enum dropdown from choices() (real resolver, standalone harness) ✅
Non-TTY: byte-identical error + exit code ✅
Bare frodo conn unchanged (no double-prompt with its own picker) ✅
Gate unit tests ✅ 4/4
Full suite (638 suites, replay mode) ✅ 8486 snapshots; 4 PTY-under-load flakes investigated: all pass in isolation/together, untouched by this diff (no shell/PTY code imports the new modules)

Notes / follow-ups

  • @inquirer/select rode along to dependencies with the others but is still imported nowhere (pre-existing) — keep-vs-drop is a separate decision.
  • Phases 1b (sub-command pickers), 2 (the 158-command one-of cohort), 3 (tenant-backed pickers) are specced in the plan doc, not started.

🤖 Generated with Claude Code

vscheuber and others added 4 commits October 9, 2026 22:37
Implements Phase 1 of INTERACTIVE-COMMANDS-PLAN.md: on an interactive
session, missing mandatory options and connection arguments are now
prompted for before the action body runs; every non-interactive surface
(CI, scripts, MCP, e2e) behaves byte-identically to before.

- PromptGateState: pure prompt gate (FRODO_NO_PROMPT / FRODO_TEST /
  TTYs / setNeverPrompt kill switch), unit-testable, 4 unit tests.
- PromptGate: promptInput/promptPassword hand-built on @inquirer/core,
  matching the established hand-built prompt pattern (Escape/empty-Enter
  resolve '' so callers keep their existing missing-input error paths).
  readline-buffer based (paste-safe); the completed line is mirrored
  from the preceding keystroke because rl.line is already consumed when
  Enter's keypress fires (verified against @inquirer/core 12.0.3).
- ResolveInteractiveInputs: preAction resolution pass. Host picker from
  saved connection profiles (single profile skipped silently, dropdown
  with "Enter a host manually..." otherwise, free text when none);
  username/password prompts only when no saved profile can supply a
  usable credential; deferred mandatory-option prompts (dropdown when
  the option carries choices()).
- FrodoStubCommand reimplements missingMandatoryOptionValue verbatim
  from commander (typings omit it) as the override point; FrodoCommand
  defers violations to the resolution pass instead of throwing, which
  makes all 57 existing makeOptionMandatory() sites prompt-capable with
  zero syntax change. Non-interactive sessions re-throw commander's
  exact message/code.
- Injection writes both processedArgs (action parameter source) and
  command.args (handleDefaultArgsAndOpts source) - the two surfaces
  commander/this repo read, verified against both.
- --no-prompt on every FrodoCommand (Runtime group); MCP stdio server
  start calls setNeverPrompt() as belt-and-braces.
- inquirer packages moved devDependencies -> dependencies (production
  prompt surface now).

All flows verified live in a PTY (host dropdown, single-profile skip,
username prefill, password masking, escape abort, --no-prompt,
FRODO_NO_PROMPT, enum dropdown, non-TTY error equivalence, bare
frodo conn unchanged).

Co-Authored-By: Claude Code <noreply@anthropic.com>
…eclined

Two issues found after opening the PR (CI Test(26) caught the first):

1. tsdown auto-externalizes packages listed in package.json `dependencies`
   unless named in deps.alwaysBundle. Moving the inquirer packages from
   devDependencies to dependencies therefore left require("@inquirer/core")
   external in the SEA bundle, where no node_modules exists to resolve it
   (ERR_UNKNOWN_BUILTIN_MODULE at runtime; my earlier local SEA build
   predated the dependency move and masked it). Both tsdown configs now
   alwaysBundle the three inquirer packages. Verified: rebuilt SEA bundle
   has zero external inquirer requires and the full PTY prompt chain works
   against the fresh binary.

2. An @inquirer/core prompt rejects with ExitPromptError on stdin EOF
   (Ctrl-D) and Ctrl-C. For gap-fill prompts both mean "no value
   supplied" and now degrade to the declined result (''), so the caller
   falls through to its pre-existing missing-input error instead of
   dumping a raw stack trace.

Also removes the failed baseline-worktree experiment's leftovers (the
symlinked-node_modules worktree could not run the e2e harness at all -
623/632 suites failing is a broken environment, not a signal).

Co-Authored-By: Claude Code <noreply@anthropic.com>
…sign

Follow-up to the dependency move in b122fba: in this repo, every
bundled runtime package (commander, tinyrainbow, @inquirer/confirm, and
now promptInput/promptPassword) deliberately lives in devDependencies --
that is what makes tsdown inline it and keeps the published package
zero-dep (empty dependencies field, empty npm audit --omit=dev surface,
no install weight). Declaring the inquirer packages as `dependencies`
followed conventional npm metadata but inverted this repo's convention:
tsdown auto-externalizes declared deps (needing deps.alwaysBundle to
force inline, per aa31f40), consumers would install three packages
whose code is also inlined, and npm audit would start scanning the
inquirer tree again.

Reverted: packages back to devDependencies, alwaysBundle dropped from
both tsdown configs, placement convention documented in both configs so
the next dep addition doesn't repeat the round trip. Verified: fresh
dist + SEA bundles have zero external @InQuirer requires (18 inlined
regions), SEA binary runs, full PTY prompt chain re-verified against the
fresh binary, npm audit --omit=dev = 0.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Maintainers and contributors need to know that in this repo "imported at
runtime" does NOT mean "declare as dependencies": the published package
is zero-dep by design, bundled runtime packages live in devDependencies
(tsdown inlines those), and anything declared under dependencies is
externalized - which breaks the SEA binary (no node_modules to resolve
against) and reopens the npm audit surface we keep empty. Hit for real
with the @inquirer/* packages in this PR.

- BUILD-ENV.md: new §2.2.1 (rule, tsup->tsdown option mapping, SEA
  failure mode, stale-binary gotcha, verification one-liner) + §11
  history row.
- CONTRIBUTE.md: callout in the build section pointing contributors at
  §2.2.1 before they add a package.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant