Skip to content

feat(mcp): full MCP-conducted Builder onboarding — iOS + android + tail, gate flipped (PR 2)#2492

Open
WcaleNieWolny wants to merge 127 commits into
mainfrom
wolny/mcp-full-onboarding
Open

feat(mcp): full MCP-conducted Builder onboarding — iOS + android + tail, gate flipped (PR 2)#2492
WcaleNieWolny wants to merge 127 commits into
mainfrom
wolny/mcp-full-onboarding

Conversation

@WcaleNieWolny

@WcaleNieWolny WcaleNieWolny commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

What

PR 2 of the MCP/TUI onboarding integration (stacked on #PR1-branch wolny/mcp-tui-integration): the MCP now conducts the COMPLETE Builder onboarding for both platforms — every journey the TUI can do is completable through the three MCP tools — and __CAPGO_MCP_ONBOARDING__ is flipped ON, shipping the tools in the release bundle (+141 KB, +4.8%).

How it's built

The MCP is a thin driver over the SAME shared headless engines the TUI uses (ios/flow.ts, android/flow.ts, tail/flow.ts) — no duplicated flow logic:

  • iOS create-new, granular — decideIos mirrors decideAndroid: total resume + auto effects + structured views; verify-app gate is multi-turn (verifyAction/verifyAppId); cert-limit revoke picker, duplicate-profile recovery, structured error screen (retry/restart/exit/email-support + corrected-key re-collect).
  • iOS import-existing — capability-gated setup fork (macOS + scan deps), identity/profile pickers, no-match recovery + Developer-Portal walkthrough, manual .mobileprovision path input, Keychain export inside one call with the user pre-warned about the macOS dialog.
  • Post-save tail, both platforms — CI secrets (gh + glab, overwrite gates, not-ready retry with re-detection), GitHub Actions workflow write behind an explicit preview consent gate ('view' returns the YAML in the result), .env export (0600, overwrite gates), package-manager/build-script pickers with lockfile recommendation.
  • Cross-call state — mcp/session-state.ts (process-local carried registry; restart degrades to engine self-heal, pinned by tests) + mcp/tail-progress.ts (slim whitelist-only disk persistence: markers + non-secret prefs; the resume routers do the rest).
  • Both data-safety gates (credentials-exist, backup/cancel) work on iOS too, including dual-platform projects.
  • Secrets discipline: carried secrets (p8/p12/passwords/CAPGO_TOKEN) never serialize into tool results or progress files — enforced by a redaction helper asserted on every test walk + verify-tree no-leak markers.

Found by the live AI e2e (fixed here)

  1. iOS credentials-exist gate bounced to platform-select on dual-platform projects.
  2. A server restart at the unanswered GitHub-Actions consent gate silently dropped the user's with-workflow choice (resume skipped the gate).
  3. The terminal build-complete result carried no evidence for the upload it claimed — the Codex judge refused to take the agent's word for it. It now surfaces the upload summary / workflow path / .env path, like the TUI screen.

Tested

  • Private suites (cli-mcp-tests, overlay): test-mcp-onboarding 87 · ios-flow 23 · tail 28 · ios-import 13 · explain-coverage 5 (authored explanation for all 63 interactive states, 90 reachable inventoried) · schema-gate 6 · session-state 14 · android-flow 111. Engine↔schema parity 43/43 keys; strict one-answer-per-call gates everywhere.
  • Hermetic e2e tree (no LLM): 20/20 paths, 429 assertions, on-disk artifact + no-leak checks.
  • LIVE Codex runs (gpt-5.5 actor + judge, isolated CODEX_HOME): 20/20 paths PASS, zero flakes — full android (keystore generate/import incl. JKS edges, SA-import + 3-way validation recovery, OAuth fire-and-poll incl. token-reject, both GCP arms, credentials gates, full build + tail), full iOS (create-new happy through the tail, verify-app wrong-build-id + autofix, cert-limit revoke, both gate arms).
  • capgo chain: full 97-segment bun run test green on the flipped bundle; tsc clean; dev-gate test now POSITIVELY asserts the 3 tools ship while dev markers stay forbidden.
  • TUI untouched: the frozen TUI E2E suite runs 79/79 journeys green against this branch's dist; zero e2e-tui files changed (requirement).

Notes for review

  • The MCP build phase keeps the C2/D2 contract (Terminal-launch/agent hand-off + BuildOutputRecord polling); the tail enters after a confirmed build. requesting-build never runs in-process.
  • Deliberate exclusions from 'every journey' (documented): ai-analysis-* (the conducting agent reads logs itself — no AI-calling-AI), self-update re-exec, native file-picker dialogs (manual path inputs instead), the TUI-only resume-prompt fork (MCP resumes silently).
  • The private submodule pin stays at the PR-1 commit (CI's TUI preview workflow is unaffected); the MCP suites + 20-path tree live at cli-mcp-tests main c3e682b and run locally via run-tests.sh.

Summary by CodeRabbit

Release Notes

  • New Features

    • Enabled MCP onboarding support for cloud build workflows
    • Added cloud build management with status monitoring, log streaming, and cancellation capabilities
    • Enhanced iOS and Android onboarding flows with improved error recovery and resume handling
    • Strengthened build credential security with stricter file permissions and validation
  • Bug Fixes

    • Improved OAuth session state handling for more reliable sign-in flows
    • Enhanced platform selection and progress tracking during onboarding
  • Tests

    • Added comprehensive test coverage for onboarding flows, OAuth operations, and platform selection

…ship test hooks from the bundle + CI leak guard

Proven on the real CLI build: a dev-gated branch importing a src/__dev__/ module
is fully DCE'd from dist/index.js (marker absent). Apple-API spoofing for AI tests
(PR 2) will live in src/__dev__/ behind this gate, physically absent from the NPM build.
…ndroidOnboardingProgress

Brings the platform-agnostic onboarding engine (mcp/*, flow.ts) + its additive shared
deps (keystore/oauth-google/oauth-scopes/output-record/schemas) onto main. Unifies the
data model: main's step set (incl. the 4 TUI-only ai-analysis-* + resume-prompt) plus
the 3 MCP markers (activePlatform, keystorePasswordGenerated, keystorePasswordManual);
flow.ts KIND_TABLE covers the TUI-only steps. main's android/ui/app.tsx kept verbatim
(TUI render-only rewrite is a later plan). Behavior is moved/renamed only — NOT assumed
correct; the android audit is Plan 2.

Verified: typecheck 0 errors; engine 87 unit + e2e hermetic gate PASS; main's onboarding
tests still pass. Covers plan Tasks 2-3 (one green commit; they don't split cleanly).
…r-cancel gate (credentials-exist + backing-up) to match main — closes a data-safety gap
… engine (persistAndStep derives next via getAndroidResumeStep; behavior-preserving + dev mismatch guard) + sequencing smoke test
…rough the shared runAndroidEffect (behavior-preserving; logs kept)
…source-of-truth (remove redundant state mirrors; conservative)
saving-credentials now stashes the CI-secret entries (createCiSecretEntries)
in transient before routing to ask-build, mirroring the Ink TUI's
doSaveCredentials. Adds the additive post-save tail transient fields to
AndroidStepCtx and the rebuildTailCredentials/tailCiSecretEntries helpers the
later tail steps reuse to re-derive the same entries from progress.
detecting-ci-secrets discovers CI destinations and persists the single chosen
target (GitHub → ask-github-actions-setup, GitLab → ask-ci-secrets, many →
target-select). checking-ci-secrets resolves the GitHub repo label + existing
keys and gates on confirm-secrets-push / confirm-ci-secret-overwrite.
uploading-ci-secrets pushes the entries and branches with-workflow →
pick-package-manager vs build-complete. Routing mirrors the Ink TUI tail.
…ep(s)

exporting-env writes the 0o600 .env (defaultExportPath fallback) and routes an
existing file to confirm-env-export-overwrite (persisting the resolved path);
overwrite-and-export-env re-exports with overwrite=true. writing-workflow-file
generates + writes .github/workflows/capgo-build.yml (overwrite) using the
recorded build-script choice + secret keys. All finish at build-complete,
mirroring the Ink TUI tail.
requesting-build fires capgo build request through the pre-bound
requestBuildInternal dep (driver owns apikey/logger/silent). A successful build
with pending CI entries routes to detecting-ci-secrets to offer the secret
push; with no entries it finishes at build-complete. A failed build with a
captured log surfaces the AI job id and routes to the TUI-only
ai-analysis-prompt; otherwise build-complete. Mirrors the Ink TUI tail.
… transient (parity with TUI; no rebuild, no secrets on disk)
…ient/deps surface + additive persisted fields
…ning/mobileprovision types (not placeholders)
Base automatically changed from wolny/mcp-tui-integration to main June 13, 2026 11:18
…der-setup prompt

Discoverability fixes for MCP clients (e.g. Codex) that explore-first instead
of reaching for the onboarding tool:

- start_capgo_builder_onboarding description is now imperative + anti-explore:
  'ALWAYS call this FIRST ... Do NOT inspect the repo, read config files, or
  web-search to do this yourself', with more trigger words (set up/configure/
  connect/enable/troubleshoot · Capgo Builder/native builds/cloud builds/signing)
- add an MCP prompt 'capgo-builder-setup' (server.prompt, optional-chained so the
  2-tool test mock is unaffected) — clients that support prompts surface it as a
  slash command; invoking it injects a message that kicks off the tool-driven flow
  so the user never has to name the tool
The MCP server was constructed with only {name, version}, so clients (Codex,
Claude Code) got zero connect-time guidance on when to reach for the onboarding
tools and fell back to inspecting the repo themselves.

Add buildServerInstructions(): always describes the general Capgo Cloud tools,
and — only when the onboarding flag is on (same gate as tool registration) —
appends a steer to call start_capgo_builder_onboarding first. Kept under the
512-char cap some clients (Codex) apply; pinned by test-mcp-instructions.
… squash-merge)

PR-1 (#2495) was squash-merged into main, breaking shared-commit ancestry, so this merge surfaced ~20 add/add conflicts on the MCP/onboarding files even though PR-2 is a content superset of PR-1. Resolved by taking PR-2's version for all onboarding/MCP source + tests (verified: the only main-only commits touching them were the PR-1 squash + release bumps — no separate fix to fold in), keeping main's auto-merged package.json version (8.4.0) plus PR-2's test scripts, and pinning the cli-mcp-tests submodule to PR-2's c511d08.

Also folds in the pre-existing dead-code gate fix (PR-2's own exports, never in main): delete unused clearTailParked; knip-ignore the test-only exports clearAllSessions / MCP_ONLY_STATES / MCP_UNREACHABLE_STEPS (consumed only by the private cli-mcp-tests suite knip can't see).

Gates green post-merge: typecheck, lint, knip, build, instructions test, + 6 targeted onboarding/MCP tests.
Codex (a strict MCP client) dropped the connection with "Transport closed"
the moment it called start_capgo_builder_onboarding. Root cause: sdk.listApps()
called listAppInternal(opts, false) — silent=false — so clack intro/log UI
('List apps in Capgo', 'Apps not found', …) was written to stdout, the same
stream the stdio transport frames JSON-RPC on. The non-JSON bytes corrupt the
framing and the client closes the transport. (This also affected the existing
capgo_list_apps tool; lenient clients just tolerated the garbage.)

- sdk.listApps: pass silent=true (every other SDK method already does). The SDK
  is a programmatic API and must never write to stdout.
- Defense-in-depth: installMcpStdoutGuard() routes ALL ambient stdout writes to
  stderr and hands the transport a dedicated stream to the real stdout, so no
  stray clack/console output from any future tool or dependency can corrupt the
  protocol mid-flow. Wired into startMcpServer.
- Tests: test-mcp-stdout-guard pins both halves of the guard contract.

Verified against the real server: start_capgo_builder_onboarding now emits only
valid JSON-RPC on stdout (0 pollution) and returns its result.
…ogress

A fresh start_capgo_builder_onboarding silently resumed whichever platform had
leftover progress on disk: decideStart/decideAdvance called activePlatform(facts),
which scanned the ios/android progress files (android-checked-first). Two problems:

  1. On a dual-platform project, a fresh start skipped the platform picker and
     dropped the user straight into (e.g.) the iOS credentials-replace gate — they
     were never asked which platform they wanted. (Reported from a live Codex run.)
  2. The MCP server is long-lived per client, so platform is session state — but
     reading it from shared disk means two concurrent sessions onboarding the same
     app (one iOS, one Android) would clobber each other (the iOS session would get
     'android' the moment the Android session wrote any progress).

This matches what the TUI already does: platform comes from the user's pick (or
single-platform auto-detect), never from leftover progress; disk only resumes the
STEP within the chosen platform.

- session-state.ts: track the chosen platform in the process-local registry
  (getSessionPlatform / setSessionPlatform). Process-local ⇒ concurrent sessions
  are isolated; a restart loses it and the flow re-asks the picker.
- engine.ts: decideStart / decideAdvance / checkBuild resolve platform from session
  memory; commit it on the picker answer / explicit {platform} / single-platform
  auto-route. runStart clears it so a fresh start always re-offers the picker.
  Removed the disk-scanning activePlatform() helper (no longer reachable).
- test-mcp-platform-select: pins ask-on-fresh-start, android-pick-ignores-iOS-disk,
  bare-next_step-uses-session, and start-re-asks.

NOTE: private cli-mcp-tests cases that seed progress and call runStart/runAdvance({})
expecting a silent disk resume encode the OLD behavior and must be updated to pick a
platform first.
…, not listApps

isAppRegistered called sdk.listApps() — an unfiltered apps.select() that times out on
large databases (full-table RBAC RLS scan; see app-list fix PR #2497). On timeout it
false-negated, so the engine tried to re-register an app the user OWNS, and addApp's
'already exists' was rendered as a bogus 'already exists and is not in your account'
conflict — blocking onboarding before the platform picker even appeared.

- sdk: add appHasAccess(appId) — a targeted existence+permission check via
  checkAppExistsAndHasPermissionOrgErr (the same one app set / channel / bundle use):
  single-app, no full-table scan, RBAC-correct, silent (no stdout on the MCP channel).
- isAppRegistered now uses sdk.appHasAccess: owned app -> true -> skip registration ->
  proceed; missing -> register; genuinely-not-yours -> real conflict.

Verified end-to-end against the real backend: start_capgo_builder_onboarding on an owned
app now reaches platform-select instead of the false app-id-conflict.
…ign-in explanation

The google-sign-in explanation told the user to 'approve the permissions Google shows'
without naming them. List the exact scopes Capgo requests so the user knows what they're
granting before approving:
  - https://www.googleapis.com/auth/cloud-platform   (create the GCP service account)
  - https://www.googleapis.com/auth/androidpublisher (invite it to Play Console, release-only)
  - https://www.googleapis.com/auth/userinfo.email   (identify the signed-in account)
Plus the local/revocable trust framing (tokens never reach Capgo servers; revoke at
myaccount.google.com/permissions).

test-mcp-explain-scopes pins this AND ties it to OAUTH_SCOPES_FOR_ONBOARDING, so a scope
added to the flow but left undocumented fails the build instead of under-disclosing.
…(never stuck)

If the OAuth browser never opened, was closed, or the flow stalled on 'still waiting',
the MCP only re-rendered 'still waiting' forever — there was no way to reopen it, so the
user got stuck (Capgo saw the sign-in as in progress but never reopened the browser).

Add a reopenSignIn input: at google-sign-in it clears the in-flight OAuth session and
begins a fresh flow (a new browser window). Threaded through the schema (so the MCP SDK
doesn't strip the field), the engine OnboardingInput type, decideAndroid (clear-before-
poll → re-begin), and drive. The 'opened browser' and 'still waiting' gates now tell the
agent it can call next_step({ reopenSignIn: true }) to reopen.

test-mcp-oauth-reopen pins it: reopen clears + re-begins the session, direct (decideAndroid)
and end-to-end (runAdvance → schema + drive threading).
… directly; fix cross-platform gate misroute

Picking iOS then saying 'android' had no clean recovery: the agent improvised a cancel,
and the engine misrouted the iOS credentials-exist answer into a leftover ANDROID
progress file on disk (the shared gate was disambiguated by disk shape, not the session).

- start_capgo_builder_onboarding now accepts an optional platform ('ios'|'android'):
  it commits to that platform and skips the picker — handles 'set up Capgo Builder for
  iOS' up front AND switching after a wrong pick (it resets the session platform, so it
  subsumes a cancel->start). runStart(deps, platform?) threads it through.
- The SHARED credentials-exist gate is disambiguated by the SESSION platform now, not a
  disk heuristic, so a stray answer can't misroute into a leftover other-platform file.
- ONBOARDING_RULES tells the agent to switch via start({platform}) instead of cancelling
  the current step or answering it with the other platform in mind.

Verified end-to-end on the real backend: start({platform:'ios'}) -> iOS, then
start({platform:'android'}) -> Android, a clean switch despite both platforms having
leftover progress on disk. test-mcp-platform-select pins start(platform) + the switch.
…_step

The first cloud build is run only by the start_capgo_build tool (build-job
registry + capgo_build_wait/capgo_build_logs). next_step({ runBuild: true })
is the wrong tool and is now rejected with a corrective that points at
start_capgo_build (state build-use-start-tool, kind error), keeping the
appid-unsafe and credentials-not-ready guards. runBuild:false still skips.
checkBuild stays record-only (reads the finished BuildOutputRecord, enters
the CI tail). Removes the dead buildHandoffResult / build-run-handoff
terminal-command path and terminal-launch.ts; updates MCP_ONLY_STATES and
explanations.ts for the new state. Also carries the resume-prompt session
state (resumeResolvedFor) that engine.ts depends on.
A failed build must not be escaped by restarting onboarding or fake-advancing.
The rules already said so, but only in the JSON blob — the prominent prose
didn't, and start_capgo_builder_onboarding's description even invited calling it
to "troubleshoot cloud builds". Strengthen the build-failed result's summary +
next.instruction to state the required-gate rule, forbid
start_capgo_builder_onboarding / capgo_builder_onboarding_next_step, and name the
only retry path (start_capgo_build, after the user agrees — no self-edit/rebuild).
Add matching carve-outs to the start/next_step tool descriptions.

Driven by the build-fail-no-escape agentic e2e: Codex previously escaped via
start/next_step (and self-edited files); with this guidance it holds the line
(retries via start_capgo_build, stays truthful, refuses to fake completion).
…clearer keystore UX

Three keystore-onboarding UX fixes:
- Random password was generated but only surfaced ~2 steps later (folded into
  the Google-Play question), so users often never saw it and were locked out of
  their own keystore. Surface it AT keystore-new-cn — the moment the user picks
  "random" — via context.keystorePassword + an explicit instruction to relay it.
- When the user has no keystore and picks "generate", keystore-new-alias now
  reassures them a keystore FILE will be created and saved to disk on their
  machine (they will receive one; it won't disappear; they keep full control).
- Passwords in the chat are fine: drop the "never paste secrets in the chat"
  friction from ONBOARDING_RULES and the manual-password gates, and strongly tell
  the AI it is 100% OK for the user to paste keystore passwords directly in chat.
  File-based credentials (.p8, service-account JSON) are still collected as paths.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
cli/test/test-mcp.mjs (1)

66-73: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Assert the full onboarding MCP runtime contract (include explain tool).

This test validates runtime listTools, but it currently checks only two onboarding tools. Per the shipped contract, capgo_builder_onboarding_explain should also be required; otherwise a registration regression can slip through.

Suggested fix
   const requiredTools = [
     'capgo_list_apps',
     'capgo_upload_bundle',
     'capgo_update_channel',
     'capgo_get_stats',
     'start_capgo_builder_onboarding',
     'capgo_builder_onboarding_next_step',
+    'capgo_builder_onboarding_explain',
   ]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/test/test-mcp.mjs` around lines 66 - 73, The `requiredTools` array in the
test is missing the `capgo_builder_onboarding_explain` tool, which is part of
the shipped onboarding MCP runtime contract. Add
`capgo_builder_onboarding_explain` to the `requiredTools` array to ensure the
test validates all required onboarding tools and prevent registration
regressions from going undetected.
cli/src/build/onboarding/ios/flow.ts (1)

995-1033: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Forward detectPackageManager through toTailDeps.

Line 469 adds detectPackageManager to IosEffectDeps, but the adapter at Line 995-1033 does not pass it into the shared tail deps. This silently drops iOS lockfile-based package-manager recommendation behavior in tail routing.

Suggested patch
 function toTailDeps(deps: IosEffectDeps): TailEffectDeps<OnboardingProgress> {
   return {
@@
     exportCredentialsToEnv: deps.exportCredentialsToEnv,
     defaultExportPath: deps.defaultExportPath,
+    detectPackageManager: deps.detectPackageManager,
     generateWorkflow: deps.generateWorkflow,
     writeWorkflowFile: deps.writeWorkflowFile,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/build/onboarding/ios/flow.ts` around lines 995 - 1033, The toTailDeps
function does not forward the detectPackageManager property from deps to the
returned TailEffectDeps object, even though detectPackageManager was added to
IosEffectDeps. Add detectPackageManager: deps.detectPackageManager to the object
returned by toTailDeps, following the same pattern as other forwarded properties
like getPackageScripts and findProjectType, to ensure the iOS lockfile-based
package-manager recommendation behavior is preserved in tail routing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/src/build/onboarding/mcp/build-job.ts`:
- Around line 185-192: The status check in the build job record evaluation is
comparing against the wrong string value. The code at line 188 checks if
rec.status === 'success', but the actual status value written by
writeBuildOutputRecord in request.ts is 'succeeded'. Change the string
comparison from 'success' to 'succeeded' to match the actual status value that
is written to the build record. This will fix the issue where builds that
succeed without an output URL are incorrectly reported as failed instead of
completed.

In `@cli/src/build/onboarding/mcp/step-input.ts`:
- Around line 330-349: The validation logic in the verify step input handler is
missing a required check: when verifyAction is set to 'pick', verifyAppId must
be provided and cannot be undefined or null. Add a new validation condition
after the existing verifyAppId checks that returns an error with an appropriate
message if verifyAction equals 'pick' but verifyAppId is undefined or null. This
ensures incomplete verify-app answers are caught at the gate rather than
deferred downstream.

In `@cli/test/test-android-oauth.mjs`:
- Around line 294-304: When the session is closed in the finally block at the
session.close() call, the session.result promise may reject if the close
operation fails. Currently, this rejection is never observed, which can cause an
unhandled promise rejection that intermittently fails CI. In the finally block
where session.close() is called, also await or handle the session.result promise
rejection (using a catch clause or similar error handling) to ensure all
rejections from closing the session are properly handled and do not become
unhandled rejections.

---

Outside diff comments:
In `@cli/src/build/onboarding/ios/flow.ts`:
- Around line 995-1033: The toTailDeps function does not forward the
detectPackageManager property from deps to the returned TailEffectDeps object,
even though detectPackageManager was added to IosEffectDeps. Add
detectPackageManager: deps.detectPackageManager to the object returned by
toTailDeps, following the same pattern as other forwarded properties like
getPackageScripts and findProjectType, to ensure the iOS lockfile-based
package-manager recommendation behavior is preserved in tail routing.

In `@cli/test/test-mcp.mjs`:
- Around line 66-73: The `requiredTools` array in the test is missing the
`capgo_builder_onboarding_explain` tool, which is part of the shipped onboarding
MCP runtime contract. Add `capgo_builder_onboarding_explain` to the
`requiredTools` array to ensure the test validates all required onboarding tools
and prevent registration regressions from going undetected.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 2b00d8f9-a5ff-4e03-99a1-e7a604a51555

📥 Commits

Reviewing files that changed from the base of the PR and between a412c78 and c32ba29.

📒 Files selected for processing (39)
  • cli/build.mjs
  • cli/package.json
  • cli/src/build/onboarding/android/flow.ts
  • cli/src/build/onboarding/android/progress.ts
  • cli/src/build/onboarding/ios/flow.ts
  • cli/src/build/onboarding/ios/progress.ts
  • cli/src/build/onboarding/mcp/app-id-validation.ts
  • cli/src/build/onboarding/mcp/build-job.ts
  • cli/src/build/onboarding/mcp/build-tools.ts
  • cli/src/build/onboarding/mcp/contract.ts
  • cli/src/build/onboarding/mcp/engine.ts
  • cli/src/build/onboarding/mcp/explanations.ts
  • cli/src/build/onboarding/mcp/oauth-session.ts
  • cli/src/build/onboarding/mcp/onboarding-tools.ts
  • cli/src/build/onboarding/mcp/session-state.ts
  • cli/src/build/onboarding/mcp/step-input.ts
  • cli/src/build/onboarding/mcp/tail-progress.ts
  • cli/src/build/onboarding/mcp/terminal-launch.ts
  • cli/src/build/output-record.ts
  • cli/src/dev-flag.d.ts
  • cli/src/mcp/instructions.ts
  • cli/src/mcp/server.ts
  • cli/src/mcp/stdout-guard.ts
  • cli/src/schemas/onboarding.ts
  • cli/src/sdk.ts
  • cli/src/support/contact-support.ts
  • cli/test/test-android-keystore.mjs
  • cli/test/test-android-oauth.mjs
  • cli/test/test-android-tail-routing.mjs
  • cli/test/test-dev-gate-stripped.mjs
  • cli/test/test-init-guardrails.mjs
  • cli/test/test-mcp-explain-scopes.mjs
  • cli/test/test-mcp-instructions.mjs
  • cli/test/test-mcp-oauth-reopen.mjs
  • cli/test/test-mcp-platform-select.mjs
  • cli/test/test-mcp-stdout-guard.mjs
  • cli/test/test-mcp.mjs
  • knip.json
  • private/cli-mcp-tests
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • Cap-go/capacitor-updater (manual)
💤 Files with no reviewable changes (1)
  • cli/src/build/onboarding/mcp/terminal-launch.ts

Comment on lines +185 to +192
if (rec) {
if (rec.jobId)
entry.cloudJobId = rec.jobId
const done = rec.status === 'success' || Boolean(rec.outputUrl)
if (done)
return { ...base, status: 'completed', outputUrl: rec.outputUrl ?? undefined, qrCodeAscii: rec.qrCodeAscii ?? undefined }
return { ...base, status: 'failed', error: `The build did not succeed (status: ${rec.status}).` }
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Build status check uses wrong value: 'success' vs 'succeeded'

Line 188 checks rec.status === 'success', but writeBuildOutputRecord (called from request.ts) writes status: finalStatus where finalStatus is 'succeeded' (see context snippet 1, line 1984). The check will never match.

Currently this is masked because Boolean(rec.outputUrl) is also checked, so builds with --output-upload work. However, if a build succeeds without an output URL (e.g., --output-upload not passed), the status check fails and deriveStatus incorrectly reports 'failed'.

Proposed fix
-    const done = rec.status === 'success' || Boolean(rec.outputUrl)
+    const done = rec.status === 'succeeded' || Boolean(rec.outputUrl)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (rec) {
if (rec.jobId)
entry.cloudJobId = rec.jobId
const done = rec.status === 'success' || Boolean(rec.outputUrl)
if (done)
return { ...base, status: 'completed', outputUrl: rec.outputUrl ?? undefined, qrCodeAscii: rec.qrCodeAscii ?? undefined }
return { ...base, status: 'failed', error: `The build did not succeed (status: ${rec.status}).` }
}
if (rec) {
if (rec.jobId)
entry.cloudJobId = rec.jobId
const done = rec.status === 'succeeded' || Boolean(rec.outputUrl)
if (done)
return { ...base, status: 'completed', outputUrl: rec.outputUrl ?? undefined, qrCodeAscii: rec.qrCodeAscii ?? undefined }
return { ...base, status: 'failed', error: `The build did not succeed (status: ${rec.status}).` }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/build/onboarding/mcp/build-job.ts` around lines 185 - 192, The status
check in the build job record evaluation is comparing against the wrong string
value. The code at line 188 checks if rec.status === 'success', but the actual
status value written by writeBuildOutputRecord in request.ts is 'succeeded'.
Change the string comparison from 'success' to 'succeeded' to match the actual
status value that is written to the build record. This will fix the issue where
builds that succeed without an output URL are incorrectly reported as failed
instead of completed.

Comment on lines +330 to +349
if (verify.length > 0) {
if (currentStep !== 'verify-app') {
return {
ok: false,
message: 'verifyAction answers only the verify-app step, which is not the current step. Answer the current step instead.',
}
}
if (input.verifyAction === undefined || input.verifyAction === null) {
return {
ok: false,
message: 'verifyAppId is only valid together with verifyAction: "pick". Call next_step with verifyAction (and verifyAppId only when picking an app).',
}
}
if (input.verifyAppId !== undefined && input.verifyAppId !== null && input.verifyAction !== 'pick') {
return {
ok: false,
message: 'verifyAppId is only valid together with verifyAction: "pick" — remove verifyAppId, or use verifyAction: "pick".',
}
}
return { ok: true }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Enforce verifyAppId when verifyAction is pick.

Line 349 currently allows verifyAction: 'pick' without verifyAppId, which lets an incomplete verify-app answer pass the strict gate and defers failure downstream.

Proposed fix
   if (verify.length > 0) {
     if (currentStep !== 'verify-app') {
       return {
         ok: false,
         message: 'verifyAction answers only the verify-app step, which is not the current step. Answer the current step instead.',
       }
     }
@@
     if (input.verifyAppId !== undefined && input.verifyAppId !== null && input.verifyAction !== 'pick') {
       return {
         ok: false,
         message: 'verifyAppId is only valid together with verifyAction: "pick" — remove verifyAppId, or use verifyAction: "pick".',
       }
     }
+    if (input.verifyAction === 'pick' && (input.verifyAppId === undefined || input.verifyAppId === null || String(input.verifyAppId).trim().length === 0)) {
+      return {
+        ok: false,
+        message: 'verifyAction: "pick" requires verifyAppId. Provide the selected app bundle id in verifyAppId.',
+      }
+    }
     return { ok: true }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (verify.length > 0) {
if (currentStep !== 'verify-app') {
return {
ok: false,
message: 'verifyAction answers only the verify-app step, which is not the current step. Answer the current step instead.',
}
}
if (input.verifyAction === undefined || input.verifyAction === null) {
return {
ok: false,
message: 'verifyAppId is only valid together with verifyAction: "pick". Call next_step with verifyAction (and verifyAppId only when picking an app).',
}
}
if (input.verifyAppId !== undefined && input.verifyAppId !== null && input.verifyAction !== 'pick') {
return {
ok: false,
message: 'verifyAppId is only valid together with verifyAction: "pick" — remove verifyAppId, or use verifyAction: "pick".',
}
}
return { ok: true }
if (verify.length > 0) {
if (currentStep !== 'verify-app') {
return {
ok: false,
message: 'verifyAction answers only the verify-app step, which is not the current step. Answer the current step instead.',
}
}
if (input.verifyAction === undefined || input.verifyAction === null) {
return {
ok: false,
message: 'verifyAppId is only valid together with verifyAction: "pick". Call next_step with verifyAction (and verifyAppId only when picking an app).',
}
}
if (input.verifyAppId !== undefined && input.verifyAppId !== null && input.verifyAction !== 'pick') {
return {
ok: false,
message: 'verifyAppId is only valid together with verifyAction: "pick" — remove verifyAppId, or use verifyAction: "pick".',
}
}
if (input.verifyAction === 'pick' && (input.verifyAppId === undefined || input.verifyAppId === null || String(input.verifyAppId).trim().length === 0)) {
return {
ok: false,
message: 'verifyAction: "pick" requires verifyAppId. Provide the selected app bundle id in verifyAppId.',
}
}
return { ok: true }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/build/onboarding/mcp/step-input.ts` around lines 330 - 349, The
validation logic in the verify step input handler is missing a required check:
when verifyAction is set to 'pick', verifyAppId must be provided and cannot be
undefined or null. Add a new validation condition after the existing verifyAppId
checks that returns an error with an appropriate message if verifyAction equals
'pick' but verifyAppId is undefined or null. This ensures incomplete verify-app
answers are caught at the gate rather than deferred downstream.

Comment on lines +294 to +304
const session = await startOAuthFlow(config, { onAuthUrl: () => {}, onStatus: () => {} })
try {
const parsedUrl = new URL(session.authUrl)
assertEquals(parsedUrl.searchParams.get('client_id'), 'my-client-id.apps.googleusercontent.com')
const scope = parsedUrl.searchParams.get('scope')
assert(scope.includes('openid'), 'scope must include openid')
assert(scope.includes('userinfo.email'), 'scope must include userinfo.email')
}
finally {
session.close()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle session.result rejection in the forced-close path.

At Line 303, this test closes the session but never observes session.result. If close causes result to reject, that can surface as an unhandled rejection and intermittently fail CI.

Suggested fix
 await test('startOAuthFlow authUrl contains clientId and expected scopes', async () => {
   const { startOAuthFlow } = await importOAuth()
   const config = {
     clientId: 'my-client-id.apps.googleusercontent.com',
     scopes: ['openid', 'https://www.googleapis.com/auth/userinfo.email'],
   }
   const session = await startOAuthFlow(config, { onAuthUrl: () => {}, onStatus: () => {} })
+  const resultHandled = session.result.catch(() => {})
   try {
     const parsedUrl = new URL(session.authUrl)
     assertEquals(parsedUrl.searchParams.get('client_id'), 'my-client-id.apps.googleusercontent.com')
     const scope = parsedUrl.searchParams.get('scope')
     assert(scope.includes('openid'), 'scope must include openid')
     assert(scope.includes('userinfo.email'), 'scope must include userinfo.email')
   }
   finally {
     session.close()
+    await resultHandled
   }
 })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const session = await startOAuthFlow(config, { onAuthUrl: () => {}, onStatus: () => {} })
try {
const parsedUrl = new URL(session.authUrl)
assertEquals(parsedUrl.searchParams.get('client_id'), 'my-client-id.apps.googleusercontent.com')
const scope = parsedUrl.searchParams.get('scope')
assert(scope.includes('openid'), 'scope must include openid')
assert(scope.includes('userinfo.email'), 'scope must include userinfo.email')
}
finally {
session.close()
}
const session = await startOAuthFlow(config, { onAuthUrl: () => {}, onStatus: () => {} })
const resultHandled = session.result.catch(() => {})
try {
const parsedUrl = new URL(session.authUrl)
assertEquals(parsedUrl.searchParams.get('client_id'), 'my-client-id.apps.googleusercontent.com')
const scope = parsedUrl.searchParams.get('scope')
assert(scope.includes('openid'), 'scope must include openid')
assert(scope.includes('userinfo.email'), 'scope must include userinfo.email')
}
finally {
session.close()
await resultHandled
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/test/test-android-oauth.mjs` around lines 294 - 304, When the session is
closed in the finally block at the session.close() call, the session.result
promise may reject if the close operation fails. Currently, this rejection is
never observed, which can cause an unhandled promise rejection that
intermittently fails CI. In the finally block where session.close() is called,
also await or handle the session.result promise rejection (using a catch clause
or similar error handling) to ensure all rejections from closing the session are
properly handled and do not become unhandled rejections.

…d Play app

Generating a fresh keystore only makes sense for a first build. If the app was
already published to Google Play, signing a new build with a different keystore
makes Play REJECT the upload (signing-key mismatch) unless the user resets the
upload key in the Play Console (App integrity → App signing). The keystore "do
you already have one?" decision (keystore-method-select + keystore-explainer) now
spells this out and steers already-published apps to their existing keystore; the
explain-tool entry carries the same guidance.
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants