Skip to content

Add a login command, and stop the CLI hanging when there is no terminal to prompt in - #146

Closed
jmadd wants to merge 4 commits into
masterfrom
jared/non-interactive-token-and-scan-prompts
Closed

Add a login command, and stop the CLI hanging when there is no terminal to prompt in#146
jmadd wants to merge 4 commits into
masterfrom
jared/non-interactive-token-and-scan-prompts

Conversation

@jmadd

@jmadd jmadd commented Jul 30, 2026

Copy link
Copy Markdown

Overview

Adds a login command, and stops the CLI hanging when there's nobody there to answer a prompt.

ditto-cli login — saves an API key and does nothing else:

npx -y @dittowords/cli@latest login

Onboarding needs a command it can hand to a person who only needs to get authenticated, and neither existing command works for that. pull follows the token with initProjectConfig and then writes files into the repo; scan uploads a whole codebase scan. Told "this just saves your key," someone running either gets an error about a missing ditto/config.yml, or side effects they didn't ask for.

login resolves a token through the existing initAPIToken path — validating one that's already saved, or collecting and saving a new one — reports where it ended up, and exits. Safe to rerun, so an agent can send someone to it without risking a clobber.

Non-interactive hangs. Two enquirer prompts had no non-interactive path, so running the CLI where nobody can type — an agent's tool call, CI — hung forever instead of ending:

  1. collectToken, reached by pull and scan whenever no key is saved
  2. the Open in browser? confirm after a successful scan

Both now check process.stdin.isTTY. The token prompt exits pointing at login; the scan confirm is skipped, so the run finishes right after printing the scan URL.

Three smaller fixes in the same path:

  • Mask the key prompt (type: "input""password"). The key was being echoed into the user's scrollback.
  • Open the API keys page instead of only printing it, using the open dependency scan already uses.
  • Build the URL from appContext.appHost instead of a hardcoded https://app.dittowords.com, so DITTO_APP_HOST no longer sends people to production.

__mocks__/open.js is new because open is ESM-only and jest can't transform it — four suites that transitively import collectToken fail without it.

Context

Came out of reviewing dittowords/ditto-app#9270, which rewrites the onboarding setup prompts to walk an agent through installing the MCP server and running a scan. A lot of that prompt exists to work around the hang: it appends < /dev/null to the scan command, and reads the API key out of the clipboard rather than letting the CLI ask for it, because the CLI's own prompt can't be answered from a tool call.

That workaround puts a credential one stray echo away from the agent's transcript, and it's enforced only by prose instructions telling the model not to print things. Handling the non-interactive case here removes the need for both, and login gives that prompt a single step to hand the person instead.

Before, with no key saved:

$ DITTO_CONFIG_FILE=/tmp/nope node bin/ditto.js pull < /dev/null
To get started, you'll need your Ditto API key. You can find this at: https://app.dittowords.com/developers/api-keys.
? What is your API key? ›
    (hangs indefinitely)

After:

$ DITTO_CONFIG_FILE=/tmp/nope node bin/ditto.js pull < /dev/null
Ditto needs an API key — a password that lets Ditto reach your workspace. There's no way to type one in here.

To set it up:
  1. Create a key at https://app.dittowords.com/developers/api-keys
  2. Open a terminal — the app on your computer where you type commands — and run:

       npx -y @dittowords/cli@latest login

     It'll ask for the key and remember it, so this is a one-time step.

Setting Ditto up for automation instead? Save your key as DITTO_TOKEN and Ditto will use that.

$ echo $?
2

The wording assumes as little as possible: it says what an API key is on first use, explains "terminal" inline, and puts DITTO_TOKEN behind an "if you're automating this" framing so it reads as not-for-you to everyone else.

Screenshots

n/a — terminal output is quoted above.

Test Plan

Testing successfully completed locally via:

  • yarn test — 49 suites, 352 tests pass
  • yarn build succeeds
  • --help lists login with its description
  • New login unit tests: exits 0, says where the key was saved, credits DITTO_TOKEN when the key came from there, stays quiet when there was no terminal
  • New collectToken unit tests, non-TTY path: quits without prompting, doesn't launch a browser, names the login command rather than whatever was run
  • New collectToken unit tests, TTY path: opens the keys page, still prompts when the browser can't be opened
  • pull with < /dev/null and no saved key — hangs on master, exits 2 with instructions on this branch (both runs quoted above)
  • login in a real terminal (driven through a pty) — key prompt is masked, a bad key is rejected live by /token-check, and no config file is written
  • login in a real terminal with a valid key — saves to ~/.config/ditto; rerunning validates the saved key and confirms without prompting again
  • scan in a real terminal — Open in browser? still appears and still opens
  • scan with < /dev/null and a valid DITTO_TOKEN — prints the scan URL and exits 0 without waiting

The last three need a real API key, so I haven't run them. The scan confirm guard in particular is only verified by reading and by the build — worth someone exercising it before this merges.

Follow-ups, not in this PR

  • Auth0 loopback flow. login is where it'd go. The MCP OAuth work (ditto-app d305ef9931) advertises Auth0 as the authorization server, and Auth0 already supports authorization code + PKCE against a 127.0.0.1 redirect — so the person could click Authorize and never handle the key at all. The blocker is that /token-check and /v1/* accept only Authorization: token <static-key>, not a bearer, so those routes need bearer support first.
  • DITTO_TOKEN vs DITTO_API_KEY. appContext reads DITTO_TOKEN; the legacy getTokenFromEnv reads DITTO_API_KEY. Worth confirming that split is deliberate.

Note: npx tsc --noEmit fails on tsconfig.json(10,25) with TS5095. That's pre-existing on master, unrelated to this change.

jmadd and others added 3 commits July 30, 2026 11:48
Agents run the CLI inside a non-interactive tool call, where enquirer has
no TTY to read from. Two prompts sat in that path and would wait forever:

- collectToken, reached by pull and scan whenever no key is saved
- the "Open in browser?" confirm after a successful scan

Both now check process.stdin.isTTY. The token prompt exits with the URL
and instructions to save a key in a real terminal or via DITTO_TOKEN; the
scan confirm is skipped, so the run ends after printing the scan URL.

Callers currently work around this by appending `< /dev/null` and piping
keys through clipboard readers, which puts a credential one stray echo
away from the agent's transcript. Fixing it here removes the need.

Also, while in this path:

- mask the key prompt (type: password), so it isn't echoed into scrollback
- open the API keys page rather than only printing it, using the `open`
  dependency already used by scan
- build the URL from appContext.appHost instead of a hardcoded host, so
  DITTO_APP_HOST no longer sends people to production

Adds __mocks__/open.js because `open` is ESM-only and jest can't
transform it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Set DITTO_TOKEN in your environment" assumes the reader knows what an
environment variable is, and "your own terminal" assumes they know what a
terminal is. Whoever hits this message is, by definition, not the person
who set the CLI up — often someone being walked through onboarding by an
agent.

Says what an API key is on first use, spells out the two steps, explains
"terminal" inline, and moves DITTO_TOKEN behind an "if you're automating
this" framing so it reads as not-for-you to everyone else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Whoever reads this message often never saw the command run — an agent
invoked it for them — so "run this same command yourself" points at
something they can't see. Rebuilds it from process.argv, on its own line so
it can be copied, and quotes any argument containing spaces.

Uses the npx form, matching CLI_INVOCATION in analyzeDirectories. Includes
-y for the same reason the TTY guard exists: without it npx can stop on its
own install prompt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jmadd
jmadd requested a review from jholiga July 30, 2026 16:14
Onboarding needs a command it can hand to a person who just needs to get
authenticated. Neither existing command works for that: pull follows the
token with initProjectConfig and then writes files into the repo, and scan
uploads a whole codebase scan. Told "this saves your key", someone running
either gets an error about a missing ditto/config.yml, or side effects they
didn't ask for.

  npx -y @dittowords/cli@latest login

Resolves a token through the existing initAPIToken path — validating one
that's already saved, or collecting and saving a new one — reports where it
ended up, and exits. Safe to rerun, so an agent can send someone to it
without risking a clobber. It's also where an Auth0 loopback flow would go
later, without touching pull or scan.

The no-terminal message now names login instead of reconstructing whatever
command was run, which reverts most of the previous commit. The smallest
safe thing to hand a person is the credential step alone — the agent can
retry the real work once the key is saved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jmadd jmadd changed the title Stop the CLI hanging when there's no terminal to prompt in Add a login command, and stop the CLI hanging when there is no terminal to prompt in Jul 30, 2026
@jmadd jmadd closed this Jul 31, 2026
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