Skip to content

feat(prompts): make CLI work in non-TTY shells (Claude Code, Cursor, CI)#67

Merged
tonychang04 merged 2 commits intomainfrom
feat/non-tty-prompts
Apr 14, 2026
Merged

feat(prompts): make CLI work in non-TTY shells (Claude Code, Cursor, CI)#67
tonychang04 merged 2 commits intomainfrom
feat/non-tty-prompts

Conversation

@tonychang04
Copy link
Copy Markdown
Contributor

@tonychang04 tonychang04 commented Apr 14, 2026

Summary

  • CLI no longer crashes with uv_tty_init EINVAL when run from agent shells (Claude Code, Cursor bash tools) or other non-TTY environments
  • Adds src/lib/prompts.ts abstraction: clack in TTY, buffered LineReader-backed readline in non-TTY
  • OAuth flow prints the authorize URL prominently via stdout.write in non-TTY so agents can relay it to the human

What changed

  • New src/lib/prompts.ts with text / select / confirm / password, auto-branching on TTY detection
  • Custom LineReader buffers parsed lines in a queue and returns null on stream close — fixes event-loss and EOF handling limitations of readline.promises.question()
  • Replaced clack.text/select/confirm usages across: link, create, login, diagnose, projects/list, storage/delete-bucket, deployments/cancel, schedules/delete, secrets/delete, functions/delete, credentials.ts, and the root menu
  • OAuth in non-TTY: prints To sign in, open this URL in your browser:\n\n <url>\n before open() attempt, skips the clack spinner (which rendered junk in non-TTY)
  • 21 new unit tests in src/lib/prompts.test.ts

Test plan

  • TTY users see the same clack UI as before (arrow-key dropdowns, boxes, spinners)
  • node dist/index.js link < /dev/null shows a numbered org list instead of crashing
  • printf "8\n1\n" | node dist/index.js link completes end-to-end: org selected, project selected, config saved, skills installed
  • Live OAuth test in non-TTY: URL printed, user clicks, callback fires, CLI prints Authenticated as <email> and continues
  • npm run lint passes (67 tests, 4 files)
  • npm run build succeeds

Bumps version to 0.1.50.

Note

Make CLI prompts work in non-TTY shells by adding stdin fallbacks

  • Introduces src/lib/prompts.ts, a new prompts module that wraps @clack/prompts with non-TTY fallbacks for text, select, confirm, and password.
  • In non-TTY environments, select prompts render a numbered list to stdout and read a numeric choice from stdin; confirm prompts accept y/n input; text prompts read plain lines with validation.
  • All interactive prompt calls across commands (create, login, projects/link, diagnose, etc.) are migrated from direct clack.* calls to this new module.
  • The OAuth login flow in src/lib/auth.ts detects non-interactive mode and prints the authorization URL and status as plain text instead of using clack spinners.
  • Behavioral Change: password input is unmasked in non-TTY contexts since terminal masking is unavailable.

Changes since #67 opened

  • Redirected OAuth PKCE flow informational messages to stderr in non-interactive mode [6c2d821]
  • Added optional whitespace preservation to non-interactive text input and enabled it for password input [6c2d821]
  • Added validation to prevent nonTtySelect from accepting empty options lists [6c2d821]
  • Added test coverage for whitespace preservation and empty options validation in non-TTY prompts [6c2d821]

Macroscope summarized df3dbed.

Summary by CodeRabbit

  • New Features

    • Unified interactive prompt system with robust non‑TTY behavior and consistent cancel handling.
    • Improved authentication output for non‑interactive terminals and scripts.
  • Tests

    • Added comprehensive tests for prompt utilities covering interactive and non‑interactive flows.
  • Chores

    • Package version bumped to 0.1.50.

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.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 14, 2026

Walkthrough

This PR adds a new local prompt wrapper module (src/lib/prompts.ts) with TTY and non‑TTY behaviors, replaces usages of @clack/prompts across CLI commands and libs with the new prompts API, adds tests for the prompts, and bumps package.json version to 0.1.50.

Changes

Cohort / File(s) Summary
New Prompt Infrastructure
src/lib/prompts.ts, src/lib/prompts.test.ts
Adds unified prompt module (interactive vs non‑TTY), LineReader, CANCEL symbol, isInteractive, isCancel, and text/select/confirm/password APIs; includes comprehensive non‑TTY unit tests.
Create / Project / Selection Flows
src/commands/create.ts, src/commands/projects/link.ts, src/commands/projects/list.ts, src/commands/diagnose/index.ts
Swapped clack.select/textprompts.select/text; removed as string casts by using typed returns; replaced clack.isCancelprompts.isCancel.
Login / Auth / Credentials / Core CLI
src/commands/login.ts, src/lib/auth.ts, src/lib/credentials.ts, src/index.ts
Replaced clack prompts with local prompts; added TTY-aware behavior in performOAuthLogin (suppress spinners and emit plaintext to stderr in non‑interactive mode); updated confirm/cancel checks.
Delete / Confirm Flows
src/commands/functions/delete.ts, src/commands/schedules/delete.ts, src/commands/secrets/delete.ts, src/commands/deployments/cancel.ts, src/commands/storage/delete-bucket.ts
Replaced clack.confirm and clack.isCancel with prompts.confirm and prompts.isCancel; preserved existing early-exit logic on cancel/negative confirmation.
Package Version
package.json
Bumped package version 0.1.490.1.50.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • jwfing
  • CarmenDou

Poem

🐰 I hopped through code with nimble paws,

swapped clack for prompts and fixed the flaws.
TTY or pipe, I listen and say,
"Yes, no, or cancel" — in either way.
A tiny version leap — 0.1.50 today!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: enabling CLI functionality in non-TTY shells by implementing a prompt abstraction that auto-branches between TTY and non-TTY modes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/non-tty-prompts

Comment @coderabbitai help to get the list of available commands and usage tips.

@tonychang04 tonychang04 requested review from CarmenDou and jwfing April 14, 2026 04:00
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 1690a5c and df3dbed.

📒 Files selected for processing (16)
  • package.json
  • src/commands/create.ts
  • src/commands/deployments/cancel.ts
  • src/commands/diagnose/index.ts
  • src/commands/functions/delete.ts
  • src/commands/login.ts
  • src/commands/projects/link.ts
  • src/commands/projects/list.ts
  • src/commands/schedules/delete.ts
  • src/commands/secrets/delete.ts
  • src/commands/storage/delete-bucket.ts
  • src/index.ts
  • src/lib/auth.ts
  • src/lib/credentials.ts
  • src/lib/prompts.test.ts
  • src/lib/prompts.ts

CarmenDou
CarmenDou previously approved these changes Apr 14, 2026
- 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.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between df3dbed and 6c2d821.

📒 Files selected for processing (3)
  • src/lib/auth.ts
  • src/lib/prompts.test.ts
  • src/lib/prompts.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/auth.ts

@tonychang04 tonychang04 requested a review from CarmenDou April 14, 2026 05:05
@tonychang04 tonychang04 merged commit 363759b into main Apr 14, 2026
3 checks passed
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.

2 participants