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
Closed
Add a login command, and stop the CLI hanging when there is no terminal to prompt in#146jmadd wants to merge 4 commits into
jmadd wants to merge 4 commits into
Conversation
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
Adds a
logincommand, 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:Onboarding needs a command it can hand to a person who only needs to get authenticated, and neither existing command works for that.
pullfollows the token withinitProjectConfigand then writes files into the repo;scanuploads a whole codebase scan. Told "this just saves your key," someone running either gets an error about a missingditto/config.yml, or side effects they didn't ask for.loginresolves a token through the existinginitAPITokenpath — 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:
collectToken, reached bypullandscanwhenever no key is savedOpen in browser?confirm after a successfulscanBoth now check
process.stdin.isTTY. The token prompt exits pointing atlogin; the scan confirm is skipped, so the run finishes right after printing the scan URL.Three smaller fixes in the same path:
type: "input"→"password"). The key was being echoed into the user's scrollback.opendependencyscanalready uses.appContext.appHostinstead of a hardcodedhttps://app.dittowords.com, soDITTO_APP_HOSTno longer sends people to production.__mocks__/open.jsis new becauseopenis ESM-only and jest can't transform it — four suites that transitively importcollectTokenfail 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/nullto 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
echoaway 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, andlogingives that prompt a single step to hand the person instead.Before, with no key saved:
After:
The wording assumes as little as possible: it says what an API key is on first use, explains "terminal" inline, and puts
DITTO_TOKENbehind 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 passyarn buildsucceeds--helplistsloginwith its descriptionloginunit tests: exits 0, says where the key was saved, creditsDITTO_TOKENwhen the key came from there, stays quiet when there was no terminalcollectTokenunit tests, non-TTY path: quits without prompting, doesn't launch a browser, names thelogincommand rather than whatever was runcollectTokenunit tests, TTY path: opens the keys page, still prompts when the browser can't be openedpullwith< /dev/nulland no saved key — hangs onmaster, exits 2 with instructions on this branch (both runs quoted above)loginin 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 writtenloginin a real terminal with a valid key — saves to~/.config/ditto; rerunning validates the saved key and confirms without prompting againscanin a real terminal —Open in browser?still appears and still opensscanwith< /dev/nulland a validDITTO_TOKEN— prints the scan URL and exits 0 without waitingThe last three need a real API key, so I haven't run them. The
scanconfirm 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
loginis where it'd go. The MCP OAuth work (ditto-appd305ef9931) advertises Auth0 as the authorization server, and Auth0 already supports authorization code + PKCE against a127.0.0.1redirect — so the person could click Authorize and never handle the key at all. The blocker is that/token-checkand/v1/*accept onlyAuthorization: token <static-key>, not a bearer, so those routes need bearer support first.DITTO_TOKENvsDITTO_API_KEY.appContextreadsDITTO_TOKEN; the legacygetTokenFromEnvreadsDITTO_API_KEY. Worth confirming that split is deliberate.Note:
npx tsc --noEmitfails ontsconfig.json(10,25)withTS5095. That's pre-existing onmaster, unrelated to this change.