feat(prompts): make CLI work in non-TTY shells (Claude Code, Cursor, CI)#67
feat(prompts): make CLI work in non-TTY shells (Claude Code, Cursor, CI)#67tonychang04 merged 2 commits intomainfrom
Conversation
Running the CLI from Claude Code / Cursor bash tools would crash immediately with `uv_tty_init EINVAL` because clack prompts require a raw-mode TTY that agent shells don't provide. Add a prompt abstraction (`src/lib/prompts.ts`) that branches on `isInteractive`: - TTY: delegate to clack (unchanged UX for humans) - Non-TTY: use a buffered LineReader over stdin — text becomes a line read, select becomes a numbered list, confirm becomes y/n Also update the OAuth flow to print the authorize URL prominently via stdout in non-TTY mode (and skip the clack spinner) so agents can surface it to users. Replace clack.text/select/confirm across all command entry points. 21 new unit tests cover the readline paths (validate retries, EOF cancel, shared reader across prompts). Bumps version to 0.1.50.
WalkthroughThis PR adds a new local prompt wrapper module ( Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/lib/auth.ts`:
- Around line 231-232: The current code writes human-readable OAuth progress
(e.g., the process.stdout.write that prints authUrl and other status messages
referencing authUrl) directly to stdout which can break --json/non-interactive
outputs; change those writes to either emit to stderr (process.stderr.write) or
gate them behind a check for interactive/non-json mode (detect the CLI's JSON
flag or an isInteractive/isTTY helper) so that when JSON mode is enabled the
auth messages are suppressed or routed to stderr instead; update every
occurrence of process.stdout.write/console.log in auth flows (the spots printing
authUrl and related status) to use the chosen approach and reuse a helper like
emitAuthMessage() that observes the JSON/interactive flag.
In `@src/lib/prompts.ts`:
- Around line 173-179: The prompt loop doesn’t guard against an empty
opts.options which yields an impossible range ("Enter number [1-0]") and causes
a dead loop; in the function that writes the menu and reads user input (use the
variables/methods opts.options, stdout.write, reader.readLine and the
surrounding select/prompt logic in src/lib/prompts.ts) add an early check for
opts.options.length === 0 and handle it by either throwing a clear error or
returning a sensible default before rendering the menu, and apply the same check
where the second prompt range is computed (the other occurrence around lines
185-186) so the prompt never asks for an invalid range.
- Around line 140-141: The password path is accidentally stripping valid
whitespace by routing non-TTY input through nonTtyText and then applying
.trim(); update the password-handling code (the function that calls nonTtyText
and later trims input) to preserve the raw value for secrets: when prompting for
a password/secret (identify by nonTtyText usage and the surrounding password
prompt function), stop trimming the returned string (remove or conditionalize
the .trim() call) and ensure nonTtyText itself does not trim input; only apply
trimming for non-secret text prompts.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d100090e-67d5-4728-85db-fe516c97c884
📒 Files selected for processing (16)
package.jsonsrc/commands/create.tssrc/commands/deployments/cancel.tssrc/commands/diagnose/index.tssrc/commands/functions/delete.tssrc/commands/login.tssrc/commands/projects/link.tssrc/commands/projects/list.tssrc/commands/schedules/delete.tssrc/commands/secrets/delete.tssrc/commands/storage/delete-bucket.tssrc/index.tssrc/lib/auth.tssrc/lib/credentials.tssrc/lib/prompts.test.tssrc/lib/prompts.ts
- auth.ts: route non-TTY OAuth progress messages (URL + status) to stderr instead of stdout, so they don't pollute --json stdout streams - prompts.ts: guard nonTtySelect against empty options list (prevents impossible "Enter number [1-0]:" infinite-loop) - prompts.ts: add `trim` option to nonTtyText (defaults true); password() calls with trim: false so leading/trailing spaces in secrets are preserved Two new tests cover the password-whitespace and empty-options paths.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/lib/prompts.ts`:
- Around line 187-191: The select-input parsing currently uses Number.parseInt
on raw which accepts inputs like "1abc"; tighten validation by first ensuring
raw.trim() matches a pure integer pattern (e.g., /^\d+$/) or that converting raw
to Number yields an integer and the string round-trips, then parse to n and
check range against opts.options.length; update the logic around the
raw/Number.parseInt usage (the block that sets const n and checks
Number.isInteger(n) and range) to reject malformed inputs instead of accepting
"1abc".
- Around line 42-48: The readLine method can have concurrent callers which
overwrite this.waiter and leave the first promise unresolved; to fix, add a
guard at the start of readLine that checks whether this.waiter (or a
pending-waiter flag) already exists and reject/throw a clear error for
concurrent calls (or alternatively convert this.waiter into an array of
resolvers if you want to support multiple waiters), and ensure the code that
resolves the waiter clears the waiter (or removes the resolver from the array)
after resolving; update the readLine implementation (and any code that
assigns/resolves this.waiter) accordingly so simultaneous calls are either
rejected immediately or properly queued.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 5e23d9a0-da8e-47d8-b07a-ddcaa230fd99
📒 Files selected for processing (3)
src/lib/auth.tssrc/lib/prompts.test.tssrc/lib/prompts.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/lib/auth.ts
Summary
uv_tty_init EINVALwhen run from agent shells (Claude Code, Cursor bash tools) or other non-TTY environmentssrc/lib/prompts.tsabstraction: clack in TTY, bufferedLineReader-backed readline in non-TTYstdout.writein non-TTY so agents can relay it to the humanWhat changed
src/lib/prompts.tswithtext/select/confirm/password, auto-branching on TTY detectionLineReaderbuffers parsed lines in a queue and returnsnullon stream close — fixes event-loss and EOF handling limitations ofreadline.promises.question()clack.text/select/confirmusages across:link,create,login,diagnose,projects/list,storage/delete-bucket,deployments/cancel,schedules/delete,secrets/delete,functions/delete,credentials.ts, and the root menuTo sign in, open this URL in your browser:\n\n <url>\nbeforeopen()attempt, skips the clack spinner (which rendered junk in non-TTY)src/lib/prompts.test.tsTest plan
node dist/index.js link < /dev/nullshows a numbered org list instead of crashingprintf "8\n1\n" | node dist/index.js linkcompletes end-to-end: org selected, project selected, config saved, skills installedAuthenticated as <email>and continuesnpm run lintpasses (67 tests, 4 files)npm run buildsucceedsBumps version to 0.1.50.
Note
Make CLI prompts work in non-TTY shells by adding stdin fallbacks
@clack/promptswith non-TTY fallbacks fortext,select,confirm, andpassword.y/ninput; text prompts read plain lines with validation.create,login,projects/link,diagnose, etc.) are migrated from directclack.*calls to this new module.Changes since #67 opened
nonTtySelectfrom accepting empty options lists [6c2d821]Macroscope summarized df3dbed.
Summary by CodeRabbit
New Features
Tests
Chores