Port basecamp-cli's onboarding flow: setup wizard, agent setup, login/logout - #228
Port basecamp-cli's onboarding flow: setup wizard, agent setup, login/logout#228jeremy wants to merge 13 commits into
Conversation
- config: persisted onboarded flag (global file only — a repository's .hey/config.json can neither suppress nor force the wizard), config set/show support, and the HEY_NONINTERACTIVE escape hatch - auth: LoginOptions.Logger so callers can route OAuth progress output; nil keeps today's os.Stderr prints byte for byte - cmd: stdin/stdout terminal checks become seam variables behind interactiveStdio(), which also honors HEY_NONINTERACTIVE; success, warning and muted styles for wizard output - tui: Confirm prompt on bubbletea v2 (renders to stderr so stdout stays data) and the wordmark hoisted from root.go for reuse - root: runTUI seam so routing is testable without a terminal
- internal/harness: agent registry with Claude Code and Codex detection, plugin/skill health checks (hey@37signals in installed_plugins.json, ~/.claude/skills/hey link, Codex skill copy under $CODEX_HOME|~/.codex). Codex is skill-presence only until a native .codex-plugin ships. - skill install: extracted installSkillFiles/linkSkillToClaude helpers (with a copy fallback where symlinks are unavailable — but never over a populated real directory), a Codex skill copy when Codex is detected, and an .installed-version stamp so staleness is observable - doctor: replaces the cwd-relative .claude-plugin check with the baseline-skill check plus per-detected-agent diagnostics - skill refresh: a PersistentPostRunE hook that re-syncs installed skill copies once per release-version change (never on dev builds, never installing anything new), repairing a broken Claude symlink on the way
Bare `hey` at an interactive terminal now routes on auth state: logged
out runs the setup wizard (welcome, OAuth sign-in, a look at the linked
accounts, coding-agent setup, summary) and stops there; logged in opens
the TUI as before. Once onboarded, a later logged-out run gets the lite
wizard (sign-in only). Pipes, cron and machine flags keep getting help.
- hey setup: the full wizard, always. With --json it never prompts; not
signed in without a terminal reports status incomplete and points at
hey auth login, so the piped installer can never hang on a browser.
- hey setup claude|codex: install the skill and connect one agent;
structured {plugin_installed, agent_detected, errors, manual_commands}
when piped. Claude refuses to fabricate ~/.claude on a machine that
has no Claude Code — detection must stay honest.
- hey setup agents: the installer's non-interactive path, HEY_SETUP_AGENT
(claude|codex|all|none, unset = auto-detect a single agent, ambiguity
connects nobody), flat envelope.
- hey login / hey logout: top-level shortcuts for auth login/logout;
OAuth login greets by identity and nudges toward hey setup <agent>
when a detected agent is unhealthy (never setup agents).
- requireAuth: at an interactive terminal a logged-out data command asks
"Not logged in. Sign in now?" and continues after OAuth; declined,
piped or machine-output runs get Error: Not logged in / Run: hey auth
login, exit 3.
- .surface gains login, logout and setup agents|claude|codex (additions
only)
…smoke README gains a Getting started section (bare hey signs you in, shows your linked accounts, offers agent setup; hey setup reruns it; login/logout shortcuts; piped runs never prompt) and the Agent Skill section becomes AI agent integration (setup claude|codex|agents, skill install, doctor, HEY_SETUP_AGENT, HEY_NONINTERACTIVE). AGENTS.md describes the routing, the requireAuth prompt, internal/harness, the onboarded flag, and the .surface/help-literal reminders for new commands. The embedded skill mentions hey setup and the shortcuts.
After a verified install, install.sh runs `hey setup` when stdin and stdout are a terminal; otherwise (CI, piped input, coding-agent shells, HEY_SKIP_SETUP=1) it runs the non-interactive `hey setup agents` and prints next steps. post_install_setup capability-probes `setup --help` so the hosted script stays correct against older release binaries: without `setup agents` it connects only an explicitly selected agent (HEY_SETUP_AGENT), capability-checked per agent so an unadvertised id is never handed to an old parent as a stray arg, and otherwise installs the shared skill. Every real call carries HEY_NO_KEYRING=1 per command so a locked headless keychain cannot wedge the installer; the help probe stays bare. install.ps1 mirrors this with Invoke-PostInstallSetup and a Test-InteractiveSession gate, restoring HEY_NO_KEYRING on the way out. installer.bats replaces the old "no interactive setup" pin with the ported contract: stub binaries logging argv, new/old binary dispatch, selector fallbacks, the keyring belt, and the ps1 helper evaluated from its AST.
interactiveStdio() honored the escape hatch, but the wizard branched on styled output alone: on a real PTY with HEY_NONINTERACTIVE=1 it still ran tui.Confirm (eating a keystroke and emitting Bubble Tea control traffic) and, logged out, parked in the six-minute OAuth wait — exactly what the README promised it would not do. Sign-in now runs only when it can be seen through: stdin is a terminal and HEY_NONINTERACTIVE is off (deliberately looser than interactiveStdio() so `hey setup --json` from a terminal still signs in with progress on stderr). The agent-setup prompt runs only under interactiveStdio(); otherwise the wizard proceeds with the prompt's default answer, matching the machine-mode contract. The prompt moves behind a confirmAgentSetup seam so the boundary is testable, and AGENTS.md's routing paragraph catches up with the hey tui contract.
The installer's non-TTY handoff runs `hey setup agents` from whatever directory curl was piped in — possibly a repository with an untrusted .hey/config.json — and the trust gate was failing it before the skill ever installed, a failure install.sh's best-effort `|| true` then swallowed without a trace. setup agents|claude|codex only touch local agent files, never the effective server or account, so they now skip the gate the same way skill/upgrade/version do. The wizard itself signs in against the effective server and stays gated.
After a symlink failure the copy fallback leaves a real, populated directory at ~/.claude/skills/hey, and the next install died trying to os.Remove it before ever reaching the fallback again. Removal now recognizes exactly the shape our fallback writes — a plain directory holding only SKILL.md and its version stamp — and replaces it; any other populated directory is user content and still refused, as the existing regression pins. The symlink call moves behind a seam so two consecutive fallback installs run as a real test.
Shape was standing in for ownership: any directory holding only a SKILL.md matched the copy-fallback profile and could be RemoveAll'd, so a user's hand-authored ~/.claude/skills/hey was deleted and replaced by a symlink. The release refresh was broader still — it rewrote every existing Claude/Codex SKILL.md at the known paths without proving hey-cli wrote them, so merely running any command on a new release clobbered a custom skill. Every hey-cli install now stamps its directory with a .managed-by-hey-cli marker (the copy fallback claims its freshly created directory up front, so a partial copy stays recognizable and replaceable). Replacement requires the marker AND the known-files allowlist; automatic refresh skips any location whose directory lacks the marker, treating foreign skills as finished state — the sentinel still advances, no retry loop — and stamps the baseline version only when that directory is ours. Pre-marker installs are adopted the next time hey skill install or the setup wizard runs. Boundary tests pin both adversarial cases: an unmarked directory holding only SKILL.md is refused and preserved by install, and a synthetic release refresh leaves unmarked baseline/Claude/Codex skills byte-for-byte alone.
The post-upgrade refresh treated every dangling ~/.claude/skills/hey symlink as ours and relinked it to the baseline, so a user's link to a temporarily unmounted volume was silently destroyed by the first command run on a new release — even with a genuinely marked baseline beside it. Repair now requires provenance on both ends: os.Readlink must equal the exact canonical relative target hey-cli writes, and the baseline it points at must carry the ownership marker. Every other broken link is preserved. The old test blessed an arbitrary dangling target; it is replaced by the provenance table (canonical over marked, canonical over unmarked, foreign target, absolute spelling of the same place) plus end-to-end pins that a foreign broken link survives the refresh and our canonical link passes through it intact.
With provenance required on both ends the repair could never fire: a canonical link over a marked baseline is by definition healthy, and a missing baseline has no marker. Safety was correct, but a function named repair that cannot repair misstates what the refresh does. Refresh now touches files it owns and nothing else; the end-to-end pins stay — a foreign dangling link survives, and our canonical link passes through intact.
Sensitive Change Detection (shadow mode)This PR modifies control-plane files:
|
There was a problem hiding this comment.
Pull request overview
Ports the onboarding flow, agent integration setup, authentication shortcuts, and installer handoff into hey-cli.
Changes:
- Adds first-run setup and interactive authentication flows.
- Adds Claude Code/Codex setup, diagnostics, and skill refresh.
- Updates installers, documentation, and test coverage.
Tip
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.
Reviewed changes
Copilot reviewed 44 out of 44 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
.surface |
Adds new command surfaces. |
AGENTS.md |
Documents onboarding architecture. |
README.md |
Documents setup and agent integration. |
skills/hey/SKILL.md |
Adds setup command guidance. |
scripts/install.sh |
Adds setup handoff on Unix. |
scripts/install.ps1 |
Adds setup handoff on Windows. |
tests/e2e/installer.bats |
Tests installer setup behavior. |
tests/smoke/auth_test.go |
Tests login/logout shortcuts. |
internal/auth/auth.go |
Adds configurable OAuth progress logging. |
internal/auth/auth_test.go |
Tests OAuth logging. |
internal/config/config.go |
Adds onboarding and noninteractive state. |
internal/config/config_test.go |
Tests new configuration behavior. |
internal/harness/harness.go |
Defines integration health results. |
internal/harness/agent.go |
Adds the agent registry. |
internal/harness/agent_test.go |
Tests registry behavior. |
internal/harness/claude.go |
Adds Claude detection and checks. |
internal/harness/claude_test.go |
Tests Claude integration checks. |
internal/harness/codex.go |
Adds Codex detection and checks. |
internal/harness/codex_test.go |
Tests Codex integration checks. |
internal/tui/brand.go |
Extracts the HEY wordmark. |
internal/tui/prompt.go |
Adds confirmation prompts. |
internal/tui/prompt_test.go |
Tests prompt behavior. |
internal/cmd/auth.go |
Adds shortcuts, greetings, and agent nudges. |
internal/cmd/auth_commands_test.go |
Tests authentication changes. |
internal/cmd/config.go |
Exposes onboarding configuration. |
internal/cmd/doctor.go |
Adds agent diagnostics. |
internal/cmd/formatting.go |
Adds prompt-aware terminal detection. |
internal/cmd/help_test.go |
Updates setup help expectations. |
internal/cmd/local_config_trust.go |
Exempts local-only setup commands. |
internal/cmd/local_config_trust_test.go |
Tests trust-gate exemptions. |
internal/cmd/login.go |
Adds the login shortcut. |
internal/cmd/logout.go |
Adds the logout shortcut. |
internal/cmd/root.go |
Routes onboarding and auth prompts. |
internal/cmd/root_test.go |
Tests root routing and authentication. |
internal/cmd/setup.go |
Implements the setup wizard. |
internal/cmd/setup_test.go |
Tests wizard behavior. |
internal/cmd/setup_agent.go |
Implements per-agent setup. |
internal/cmd/setup_agents.go |
Implements noninteractive agent setup. |
internal/cmd/setup_agents_test.go |
Tests agent selection and setup. |
internal/cmd/skill_install.go |
Extends managed skill installation. |
internal/cmd/skill_install_test.go |
Tests skill replacement safety. |
internal/cmd/skill_refresh.go |
Adds release-based skill refresh. |
internal/cmd/skill_refresh_test.go |
Tests safe skill refresh. |
internal/cmd/tui.go |
Routes TUI through a test seam. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e89a59b472
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Review of #228 surfaced that ownership was enforced at removal and refresh but not at write: installSkillFiles and installSkillToCodex overwrote an unmarked user-authored skill and then claimed it — reachable automatically through the installer's setup agents handoff — and removeExistingSkillLink's bare os.Remove deleted any user file or symlink before the directory check ever ran. Rather than patch each site, ownership now lives at the single layer that writes: - claimSkillDir gates every skill write (baseline, Codex, copy fallback): create-and-mark a missing or empty directory, accept a marked one, refuse anything else — including symlinks, which would land the write somewhere never inspected. - removeExistingSkillLink starts from Lstat: only our canonical symlink or a marked, allowlisted copy directory is removed; a foreign link, regular file, or unmarked directory errors. - refresh writes only marked, non-symlinked regular files in non-symlinked directories, and skips entirely when ConfigDir() is empty rather than dropping its sentinel in the cwd. - setup codex mirrors the Claude guard: it never fabricates ~/.codex and then counts its own creation as detection. - a styled hey setup <agent> verdicts from a fresh health snapshot and exits nonzero (setup_incomplete) instead of telling the user to start a session against an unconnected integration. - commands that never touch the server (setup agents|claude|codex, skill, …) skip legacy-credential migration, so the installer's HEY_NO_KEYRING=1 handoff can no longer move old config.json tokens into plaintext credentials.json. - doctor warns on an unmanaged baseline skill with the adoption path; pre-marker hey-cli installs adopt via hey skill install. Regression tests pin each refusal: unmarked baseline/Codex skills survive explicit install and setup agents byte-for-byte and unclaimed, foreign links and regular files are refused, refresh never writes through symlinks, the sentinel never lands in the cwd, and legacy credentials migrate for auth status but not for setup agents.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dfcf241e36
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- Narrow the credential-migration exemption to commands that neither use credentials nor rewrite the global config (setup subcommands, skill). The previous predicate exempted all of config, whose Save* helpers decode config.json into a struct with no credential fields — so `hey config set` on a legacy install deleted embedded tokens instead of migrating them. Regression test pins that config set migrates before it rewrites. - The wizard no longer reports a complete, signed-in setup over stored credentials HEY rejects: an auth-coded identity failure becomes a "Stored sign-in rejected" issue with a hey auth login hint (other failures keep the best-effort greeting, so offline setup still works). - setup agents counts an agent connected only when its handler succeeded AND health checks pass. Presence checks alone flipped a refused install (unmanaged skill at the canonical path) back to plugin_installed: true under a "connected" summary. - The refresh sentinel records the active Codex home alongside the version, so switching CODEX_HOME mid-release triggers one rescan instead of leaving the other home's marked skill stale until the next release.
Ports basecamp-cli's onboarding flow so a first run of
heyis seamless instead ofError: not logged in.Behavior
heyat an interactive terminal, logged out → the setup wizard (welcome → OAuth sign-in → linked-account greeting → coding-agent setup → summary). Once onboarded, a later logged-out run gets the lite wizard (sign-in only). Every other bareheyprints help, preserving main'shey tuicontract (#8d3e675).hey setupalways runs the full wizard.--jsonnever prompts; logged out without a terminal reportsstatus: incompletewith ahey auth loginbreadcrumb, so the piped installer can never hang on a browser.hey setup claude|codex|agentsbacked by a newinternal/harnessagent registry. Claude gets thehey@37signalsplugin frombasecamp/claude-pluginsplus a skill link; Codex gets the skill only until a.codex-pluginships.setup agentsis the installer's non-interactive path:HEY_SETUP_AGENT=claude|codex|all|none, ambiguity connects nobody.hey login/hey logoutshortcuts; OAuth login greets by identity and nudges towardhey setup <agent>when a detected agent is unhealthy.requireAuth()at an interactive terminal asks "Not logged in. Sign in now?" and continues after OAuth; piped, declined or machine-output runs getError: Not logged in/Run: hey auth login, exit 3.hey doctorgains baseline-skill and per-agent diagnostics; aPersistentPostRunEhook re-syncs installed skill copies once per release version.hey setupon a TTY and tohey setup agentsotherwise (HEY_SKIP_SETUP,HEY_SETUP_AGENT, per-commandHEY_NO_KEYRING=1);install.ps1mirrors it.HEY_NONINTERACTIVE=1disables every prompt, including on a PTY.Safety properties worth reviewing
~/.claude(hey setup claudeon a machine without Claude refuses)..managed-by-hey-climarker. Replacement requires the marker plus a known-files allowlist; the automatic refresh skips anything unmarked and never touches symlinks. A user-authored~/.claude/skills/hey— even one that is a singleSKILL.md— is preserved byte-for-byte.setup agents|claude|codexskip the local-config trust gate (they never touch the server); the wizard itself stays gated.Verification
make check, fullgo test ./..., all 40 installer bats (incl. the pwsh AST-extracted ones),.surfaceadditions only. Manual matrix and PTY probes against the built binary are in the commit messages. Not yet verified: live OAuth andmake test-smoke— the dev server atapp.hey.localhost:3003was unreachable throughout; that is the final merge gate.Follow-ups deliberately out of scope: wordmark animation,
.codex-plugin+ native Codex plugin install, a quick-start JSON envelope for non-TTY barehey.Summary by cubic
Ports basecamp-cli’s onboarding so first run is seamless instead of “Not logged in,” and hardens skill installs and refreshes to never overwrite user content.
Bare
heyat an interactive terminal routes on auth: logged out → setup wizard (welcome → OAuth → linked accounts → agent setup → summary); logged in → TUI. Non-interactive runs never prompt; setHEY_NONINTERACTIVE=1to disable prompts everywhere.hey setupalways runs the full wizard;--jsonnever prompts. When stored credentials are rejected by HEY, the wizard reports “Stored sign-in rejected” with ahey auth loginhint.hey setup agents|claude|codexruns non-interactively. It installs the baseline HEY agent skill and connects selected or detected agents. An agent counts as connected only if its handler succeeded and health checks pass. It never fabricates~/.claudeor~/.codex.hey login/hey logoutare top‑level shortcuts forhey auth login/logout. OAuth login greets by identity and nudges towardhey setup <agent>when a detected agent is unhealthy.requireAuth()at an interactive terminal offers “Not logged in. Sign in now?” and continues after OAuth; piped, declined, or machine‑output runs fail with “Not logged in” (exit 3) and ahey auth loginhint.hey doctorchecks the baseline skill and per‑agent health viainternal/harness. It warns when the baseline skill directory is not managed by the CLI and suggests adoption. A persistent post‑run hook refreshes installed skill copies once per CLI version and Codex home.Installers hand off to
hey setupon a TTY and tohey setup agentsotherwise. They respectHEY_SKIP_SETUP,HEY_SETUP_AGENT, and setHEY_NO_KEYRING=1on invoked commands.install.ps1mirrors this.Config adds a global
onboardedflag (hey config set onboarded=true|false)..surfacegainslogin,logout, andsetup agents|claude|codex.Safety and rollout
.managed-by-hey-climarker before writing. It refuses to write into unmarked or symlinked directories.setup agents|claude|codex) skip the local‑config trust gate; the wizard stays gated.setup <agent>exits non‑zero when the agent remains unhealthy after setup.hey skillskip it;hey config setnow migrates legacy credentials before rewriting the config.Written for commit 8a18480. Summary will update on new commits.