feat(onboarding): shared engine for iOS + Android, TUI as render-only wrapper (PR 1)#2495
feat(onboarding): shared engine for iOS + Android, TUI as render-only wrapper (PR 1)#2495WcaleNieWolny wants to merge 90 commits into
Conversation
…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).
…stripped; shared engine still ships)
…non-empty) to match main; tighten gate
…gcp-project-create-name) to match main
…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)
…code now that the engine drives the flow
… android mapper (drop unsafe cast)
…oidEffect through the post-save tail)
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)
…e; android delegates
…mapIosViewToStepView)
…ient/deps surface + additive persisted fields
…ning/mobileprovision types (not placeholders)
…t-save tail through the shared tail module
…reserve appIdConfirmed/pendingAppIdNext; keep keyId cleared)
…ough the engine Route the iOS IMPORT credential EFFECT steps (import-scanning, import-validating-all-certs, import-checking-apple-cert, import-provide-profile-path, import-create-profile-only, import-compiling-helper, import-exporting) through the shared engine's runIosEffect via a new engine-driven import effect driver, mirroring the create-new effect driver. The driver wires the REAL macos-signing / apple-api / mobileprovision-parser helpers (token-adapted via getFreshToken; classify pre-binds the single team-wide cert fetch + SHA-1 index), threads the ephemeral selections from iosCarriedRef, mirrors importMatches/importProfiles/identityAvailability/profilePrefetch/ noMatchReason/certData/profileData/importedP12Password back into React state, applies redirectIfMismatch after import-scanning, and advances. The import CHOICE handlers (pickers, recovery hub, portal-explanation, export-warning) stay bespoke for now but their React-state writes bridge into the carried ref. Removes the 7 bespoke import effect bodies. Exports parseMobileprovisionBufferDetailed so the engine can parse the .mobileprovision bytes its readFile dep already loaded.
…e through the engine Convert the import-pick-identity / import-pick-profile choice handlers to pure engine resolvers: stash the resolved identity/profile into iosCarriedRef (+ keep the React mirror), run runIosEffect(step) for the next step. import-pick-identity's three-way routing (usable on-disk → pick-profile; no on-disk + ASC key → checking-apple-cert; otherwise → no-match-recovery with noMatchReason='no-profile-on-disk') and import-pick-profile's three validations (bundle/distribution/cert-trust → import-export-warning or error) now live in the engine; the handlers mirror the transient noMatchReason and surface resolver 'error' via handleError. import-distribution-mode persistence moves to applyIosInput (setupMethod + importDistribution, or clear on __cancel__), keeping the bespoke getImportEntryStep / import-pick-identity routing (the routing test's DIVERGE class). __cancel__ on the identity picker also persists the switch-to-create-new before re-driving, per the resolver contract.
…import verifying-key through the engine Convert the remaining import CHOICE handlers to engine resolvers: - import-no-match-recovery (5-way): stash recoveryAction + sticky noMatchReason into iosCarriedRef, run runIosEffect; engine routes browser/provide-profile-path/back/create(+/- ASC key). The no-ASC-key create branch persists pendingRecoveryAction via the engine; the React mirror stays in sync. Resets profilePickerOpened before the file picker. - import-portal-explanation: stash portalAction; engine routes use-create/ use-file/open-anyway (deps.openExternal + breadcrumb)/back. - import-export-warning: stash exportWarningAction; engine routes go (isHelperCached ? exporting : compiling-helper)/back; 'exit' keeps the bespoke exitOnboarding (the routing test's DIVERGE). import-mode verifying-key now routes through the SAME engine effect as create-new (verify + apiKeyVerified persist preserving setupMethod/ importDistribution), then OVERRIDES the engine's create-new 'next': pendingRecoveryAction resume → import-create-profile-only; plain import app_store → redirectIfMismatch(matches>0 ? import-validating-all-certs : import-pick-identity). Removes the bespoke verify body + the now-unused ApiKeyData import.
…doSaveCredentials/loginCommand/imports
…sResumeStep (gates + post-save tail)
Brings in 95 commits incl. #2397 (iOS remote App Store Connect app verification). verify-app replaces confirm-app-id; the feature is ported into the engine-driven TUI via the established verifying-key next-override pattern (engine port of verify-app itself follows). Our resume/routing tests updated to main's new verify-app invariant.
Port the PR #2397 verify-app step (remote App Store Connect verification) from the TUI driver into the iOS engine (ios/flow.ts): - runIosEffect('verify-app'): initial fetch (parallel listApps+listBundleIds, FRESH detectBundleIds re-detect, classifyAppVerification) with the four exits — exact-match (persists iosBundleIdOverride + iosBundleIdContextAppId via the returned progress, advances to carried.pendingVerifyNext ?? creating-certificate), fetch-failed / no-release-config pass-throughs, and the parked picker/gate states riding transient (verifyApps / verifyPath / verifyResult / verifyAttempt / …). - carried-driven gate RESOLVER (the cert-limit/duplicate pattern): pick / create-new / autofix (writeReleaseBundleId) / continue (fresh re-detect + evaluateGate escalation) / recheck (Path B re-poll + ask-before-reopen) / open-reopen (ensureBundleId + openExternal) / back / cancel (error exit sink). - verifying-key next: create-new → verify-app (was creating-certificate); import app_store → verify-app + transient.pendingVerifyNext (the matches>0 import continuation); import ad_hoc → the continuation directly; pendingRecoveryAction unchanged. pendingVerifyNext is EPHEMERAL (deps.carried) — never persisted, matching getResumeStep's fresh-mount fallback to creating-certificate. - New optional deps: listApps / listBundleIds / detectBundleIds / writeReleaseBundleId (the engine stays IO-free; pure helpers imported). - iosViewForStep('verify-app'): auto before classification; choice (picker / Path A / Path B / ask-reopen) once parked. applyIosInput: no-op (ephemeral). Tests: new test/test-ios-verify-app.mjs (33 cases: exits, gate paths incl. autofix + re-poll + cancel, pendingVerifyNext threading, view + reducer + ephemeral contracts), wired into the package.json chain; test-ios-create-new / test-ios-e2e / test-ios-import-export updated to the new verifying-key → verify-app routing (e2e create-new now traverses verify-app between verifying-key and creating-certificate).
…op driver overrides) Delete the two driver-side verify-app overrides now that the engine expresses the PR #2397 detour itself: - the import-mode verifying-key bespoke body (the setPendingVerifyNext + redirectIfMismatch('verify-app') / importTarget override) — verifying-key now runs through the engine driver on BOTH paths, with the import continuation arriving via transient.pendingVerifyNext → iosCarriedRef and the .p8 bytes falling back to the p8ContentRef mirror in carried; - the create-new driver's advance override (setPendingVerifyNext + forced 'verify-app') — the driver now uses the engine's next verbatim, wrapping the verifying-key advance in redirectIfMismatch (the SYNC FS bundle-id adopt) except on the pendingRecoveryAction resume (whose React mirror is cleared here, as before). verify-app itself joins IOS_ENGINE_CREATE_EFFECT_STEPS: the driver wires the real listApps / listBundleIds / detectIosBundleIds / writeReleaseBundleId / ensureBundleId / open deps, shows the loader during the initial fetch, mirrors the engine's verify* transient into the React render state, mirrors the persisted iosBundleIdOverride (setIosBundleId + setAppIdConfirmed), and fires the Shown / Result / Passed telemetry from the returned classification. The PARKED gate's render (loader / debug-differ warning box / picker / Path A / Path B / ask-reopen screens) is unchanged; its Select onChange handlers now record the pick into iosCarriedRef.verifyAction and re-drive the engine resolver via the new runVerifyGateAction (which also fires the Auto Fixed / Create App Opened / Gate Blocked / Passed events and clears the consumed action). Cancel stays a bespoke driver exit (telemetry + log + exitOnboarding). pendingVerifyNext React state, persistVerifyOverride, verifyFetchStartedRef and the bespoke verify-app effect are deleted; the restart reset now clears the carried verify-gate threading instead.
…y when the build fails The self-heal guard consulted the persisted-progress resume resolver BEFORE attempting to build the platform credentials. The iOS import payload (cert, profile, team id, p12 password) is ephemeral by design — it rides the driver's carried state and is never written to progress.json — so the persisted resume always pointed back to import-scanning at save time. The guard therefore diverted on every save, looping the import fork forever (export from keychain -> 'required input missing' -> re-import). Now the credential build runs first: if it succeeds we save regardless of what the persisted resolver says; only when it throws do we consult the resolver to route the user back to the genuinely missing step (and rethrow when the resolver insists we are already at saving-credentials). Tests: the two self-heal cases now inject genuinely-throwing builders, and two new regressions pin the live loop (shared engine + iOS tail handoff with the real persisted import shape + carried payload).
…e limit revoke picker to that pool createCertificate now POSTs certificateType DISTRIBUTION (the cross-platform 'Apple Distribution' type Xcode 11+ uses) instead of the deprecated IOS_DISTRIBUTION, whose separate per-team pool tends to be full of legacy certs — hitting Apple's limit even though the modern pool has room. When Apple still rejects on the limit, the revoke picker now lists ONLY the DISTRIBUTION pool (CertificateLimitError payload + the driver's listCertificates fallback): revoking a cert from another pool would not free a slot, and the old mixed-type list put the default cursor on the user's real Apple Distribution keychain cert — one Enter away from invalidating their production p12 for nothing. listDistributionCerts keeps querying BOTH types by default (the import flow must match local Keychain identities against the full ledger); a new optional types filter narrows it for the limit-recovery callers. UI copy follows: 'Creating Apple Distribution certificate…' spinner, 'Apple Distribution certificate limit reached (N existing)' header, engine view title named after the pool. New test/test-apple-api-cert-create.mjs (5 cases, global-fetch stub) pins the POST type, the scoped re-list, the empty-pool rethrow, and the both-types default; wired into the CI chain.
127 commits in, incl. the builder onboarding TUI preview CI (PR #2483: .gitmodules + private/cli-mcp-tests submodule @ 630a8d0 + workflow), simplified contact-support (PR #2406), streaming AI build analysis (PR #2438), and the self-update prompt. Conflict resolution (4 files): ours-as-base (engine-driven TUI) + main's new features ported in — - package.json: union of both test chains (main's 12 new segments + our 23) and script entries. - oauth-google.ts: kept our startOAuthFlow split; ported main's appendInternalLog breadcrumb into the open-browser catch. - ui/app.tsx + android/ui/app.tsx: discarded resurrected bespoke effect bodies (replaced by engine drivers on this branch); ported supaHost into the engine-built build request via a requestBuildInternal dep wrap; verified the auto-merged support flow, AI streaming and internal-log wiring cohere with the engine structure. Post-merge engine follow-ups: android KIND_TABLE gained the three support-* steps; main's internal-only appendInternalLog breadcrumbs that lived inside bespoke effect bodies were re-homed into the engine via a new optional onInternalLog dep (ios/flow.ts, tail/flow.ts, android/flow.ts; wired from both wizards; forwarded by both toTailDeps adapters). tsc clean; ios-tui-routing 39, ios-tui-render 60, tail-engine-shared 72, android-tail-engine 49, ios-e2e 12, ios-create-new 36, ios-verify-app 33, update-prompt 8, support + AI streaming suites, frame-fit 8/8 — all green. Root bun install --frozen-lockfile verified.
… (caught by the private E2E suite) 1. backing-up: the engine's success log dropped the backup destination path — main's bespoke logged '✔ Backup saved · <path>' (app.tsx:1473) so the user can find the backup. Restored the path detail. 2. verifying-key: the engine's catch echoed the multi-line verification- failure message into the log pane AND returned the error route — the error screen renders the same message, so the advice list painted twice on one frame. Main routes to handleError only; dropped the onLog echo (the onInternalLog breadcrumb stays). Both pinned by the private e2e-tui goldens (setup-method-select.txt / verifying-key__invalid.txt): 79/79 journeys green after this.
Suite-side counterpart of this branch: hermetic CAPGO_SKIP_UPDATE_PROMPT
(npm published 8.1.9 and the self-update prompt blocked every journey's
first frame, on main too), the documented resume-overwrite journey flip
(the credentials-exist gate this branch adds), and the pre-authorized
1-line cert-limit-prompt golden re-record ('iOS distribution' → 'Apple
Distribution', user-approved). 79/79 journeys + drift guard green against
this branch's dist.
The merged ErrorStep now delegates its options to buildHelpMenuOptions (support first, 'Try again', 'Exit') — the old bare 'Retry' label this assertion pinned no longer exists on either branch. Assert the new menu shape (support entry + renamed retry) per the TUI-is-main's-reference rule. 25/25.
Brings in 20 commits from main, most notably PR #2458 (precompiled, signed & notarized macOS keychain helper packages) and PR #2489 (multi-channel bundle upload), plus release chores (cli 8.3.0 / app 12.164.0) and frontend/backend changes that do not touch onboarding. Conflict resolutions (playbook: engine-driven structure is the base; main's new features are ported in): - cli/package.json: UNION. Kept our script entries (android-tail-*, ios-*, dev-gate-stripped, platform-flow-contract, tail-engine-shared, ...) plus main's `test:helper-dce` entry, and merged the `test` chain as the union of both sides' segments — main's `bun run test:helper-dce` is inserted right after `bun run build`, matching main's position. The chain otherwise keeps every segment from both sides. Version stays at main's 8.3.0. - cli/build.mjs: UNION of both sides' `define` blocks (CLI + SDK builds): kept our `globalThis.__CAPGO_DEV__` / `globalThis.__CAPGO_MCP_ONBOARDING__` release gates AND main's `__CAPGO_ALLOW_HELPER_ENV_OVERRIDE__` DCE gate for the CAPGO_KEYCHAIN_HELPER_PATH dev override (PR #2458). - cli/src/build/onboarding/ui/app.tsx: ours is the engine-driven thin wrapper; main's bespoke import-effect bodies (deleted on our branch, modified by PR #2458 on main) were dropped, and main's actual change — the removal of the import-compiling-helper step — was ported into our engine-driven structure instead (see below). - bun.lock: took main's cli version stamp; `bun install` then reconciled it to 8.3.0. - private/cli-mcp-tests (submodule): fast-forwarded to main's pointer 71db228 ("test(compat): pin cert golden + resume journey to capgo main") — our pointer 8a615f2 is its direct ancestor, and 71db228 matches the merged code's helper contract. The local frozen suite reconciliation stays with the orchestrator. Port of PR #2458's import-compiling-helper removal (main is the reference for its feature; our engine consumes main's new precompiled-helper mechanism): - macos-signing.ts took main's version wholesale (resolveHelperBinary + signature-verified precompiled helper; precompileSwiftHelper/isHelperCached deleted). types.ts / error-categories.ts / ui/steps/ios-import.tsx merged clean to main's contract (step union, STEP_PROGRESS, error category, and ImportCompilingHelperStep component all gone). - ios/flow.ts (ours-only engine file): removed the import-compiling-helper effect case, the precompileSwiftHelper/isHelperCached deps, the helperCompiled transient + carried idempotency guard, and rerouted the import-export-warning resolver's 'go' branch straight to import-exporting. Docs updated to record the PR #2458 contract. - ui/app.tsx: dropped the removed imports, the engine-deps wiring for the two deleted callbacks, the step from IOS_ENGINE_IMPORT_EFFECT_STEPS, and the isHelperCached arg on the export-warning runIosEffect call. - Tests updated to main's contract (each pinned the removed step): - test-ios-import-export.mjs: collapsed the three 'go' resolver variants into one unconditional 'go' -> import-exporting case, deleted the import-compiling-helper effect section and the "NOT cached" driver journey, dropped the two deps from makeDeps. - test-ios-e2e.mjs: journey (b) no longer detours through the compile step; dropped the deps mocks, the isHelperCached overrides, and the step's error-route case in (e). - test-ios-tui-routing.mjs: replaced the cached/NOT-cached parity pair with a single unconditional 'go' MATCH case. - test-ios-tui-render.mjs: removed the ImportCompilingHelperStep render test + its import (component deleted by #2458). - test-ios-resume.mjs: dropped the step from EPHEMERAL_PICKER_STEPS. - test-ios-tail-handoff.mjs: comment-only update. Gates: tsc --noEmit clean; bun run build clean; test:helper-dce green (entry + chain segment + cli/scripts/check-helper-dce.sh all survive); test-dev-gate-stripped green (MCP onboarding tools still ABSENT — the PR-1 gate stays off); full `bun run test` chain exit 0. TUI E2E (frozen private suite @ 68a181d vs this dist): 79 journeys — 78 pass, 1 flaky (support flow → view logs → cancel; pass-on-retry). Runtime drift guard: ios 40/65 covered / 25 excluded / 0 missing; android 42/65 covered / 23 excluded / 0 missing. Known follow-up (private repo, orchestrator-owned): test:e2e-tui:harness fails at harness-tests/test-drift.mjs hygiene — "UNCOVERED key 'import-compiling-helper' is not a STEP_PROGRESS step id" — because the suite's restored exclusion (private PR #4) is orphaned now that this merge ports the step's removal.
Drift-guard bookkeeping for the main merge: this branch now ports main's import-compiling-helper step removal (PR #2458), so the private suite's restored exclusion (#4) became orphaned — 1044e70 re-applies #3 exactly. Harness self-tests + the 79-journey suite green against this branch's dist (78 pass + 1 pty flake, pass-on-retry; drift 0 missing both platforms).
|
Warning Review limit reached
More reviews will be available in 58 minutes and 15 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more credits in the billing tab to continue. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (61)
Comment |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Merging this PR will not alter performance
Comparing Footnotes
|
| && progress._keystoreBase64 | ||
| && deps.writeKeystoreFile | ||
| ) { | ||
| keystoreFileWritten = true |
🧪 Builder onboarding TUI preview — ✅ passed▶ Open the interactive HTML report (zoomable journey tree + cast playback) Commit: 1db3bf6 · Job summary with the result table |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 73fe414168
ℹ️ 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".
| const recordPath = deps.buildRecordPath(buildAppId, buildPlatform) | ||
| const command = `npx @capgo/cli@latest build request ${buildAppId} --platform ${buildPlatform} --output-upload --output-record "${recordPath}"` |
There was a problem hiding this comment.
Clear stale build records before launching builds
When the same app/platform has a previous successful onboarding build, this deterministic temp path is reused and the new command only overwrites --output-record on a succeeded build (request.ts writes it only when finalStatus === 'succeeded'). If the next build fails or is still running, checkBuild reads the old record and reports the prior build as the current one, incorrectly completing onboarding. Remove or uniquely namespace the record before starting a new build.
Useful? React with 👍 / 👎.
…read, bundle-id resolution, error truthfulness BLOCKER: buildIosSavedCredentials re-reads the .p8 from the persisted progress.p8Path via the injected deps.readFile when the carried key content was lost (crash/restart), and REFUSES to save an app_store credential map without the ASC key — pre-fix the resume silently wrote credentials missing APPLE_KEY_CONTENT/APPLE_KEY_ID/APPLE_ISSUER_ID and declared success (3-engine hostile-review consensus; regression vs main's guards). buildSavedCredentials may now be async (tail/flow.ts awaits it). Engine findings: - resolveIosBundleId: override → detected Release bundle id → appId, threaded into the provisioning-map key, the tail rebuild, and all six runIosEffect call sites (bundle id ≠ Capgo appId) - resolveP8Content wraps readFile: a stale p8Path surfaces the NeedP8-style re-provide error instead of a raw ENOENT - import-portal open-anyway: success log carries the URL; openExternal failure logs the visit-manually fallback instead of fabricating success - tail saving-credentials self-heal divert surfaces the underlying builder error (guidance line + internal log) - requesting-build honors deps.signal (pre/post/catch abort bails; limitation vs request.ts internals documented) - android backing-up: only ENOENT keeps the benign note; real copy failures log a truthful warning - gcp-setup-running strips _oauthRefreshToken from persisted progress after a successful revoke - createCertificate/createProfile rethrow the ORIGINAL Apple error when the follow-up list call also fails - rebuildIosTailCredentials restores APPLE_KEY_ID/APPLE_ISSUER_ID from progress (keyId/issuerId ?? apiKeyVerified) - preloadWorkflowScripts failures route through onInternalLog - test-ios-import-discovery: prefetch fixture uses the RAW AscProfileSummary shape so the synthesizeProfileFromAscSummary mapping is actually exercised
…tale closures, tail readFile wire - .p8 submit handlers no longer rewrite every failure to 'File not found': classifyP8SubmitError (new ui/p8-error.ts) keeps the exact copy for ENOENT only; other errors reach handleError with the real message, and every failure lands in the internal log - the three [step]-keyed drivers (create-new / import / tail) call handleError through a ref (handleErrorRef), eliminating stale-closure routing on error - NeedP8Error hoisted to module scope (stable instanceof across renders) - tail driver carried.savedCredentials built via an explicit filtered conversion instead of a lying Record<string,string> cast - tail driver deps now thread readFile (mirrors the create driver) so the engine's crash-recovery .p8 re-read works through the TUI — verified by the private e2e crash-recovery journeys (xfail pin promoted) - error sinks: support-flow and restart deleteProgress catches log instead of swallowing; cert-limit/duplicate-profile Select onChange guarded against the @inkjs/ui re-fire with one-shot refs
| assertEquals(res.next, 'import-no-match-recovery', 'still bounces back to the recovery menu') | ||
| const opened = logs.find(l => /Opened Apple Developer Portal/.test(l.msg)) | ||
| assert(opened, 'the success breadcrumb still fires when openExternal succeeded') | ||
| assert(opened.msg.includes(PORTAL_PROFILES_URL), `the success line must include the URL that was opened (got: ${opened.msg})`) |
| assert(!logs.some(l => /Opened Apple Developer Portal/.test(l.msg)), 'must NOT claim the portal was opened when openExternal failed') | ||
| const warn = logs.find(l => /Could not open your browser/.test(l.msg)) | ||
| assert(warn, 'must log the could-not-open warning (verify-app sibling pattern)') | ||
| assert(warn.msg.includes(PORTAL_PROFILES_URL), `the warning must tell the user WHERE to go (got: ${warn.msg})`) |
|



What
The Builder onboarding mega-PR: both platforms' onboarding now runs on ONE shared headless engine, with the ink TUI as a render-only wrapper over the entire flow — including the post-save tail (CI secrets, env export, workflow file, build request). The MCP onboarding integration stays build-time gated OFF (
__CAPGO_MCP_ONBOARDING__= false; PR #2492 — stacked on this branch — completes and flips it).Architecture
flow/contract.ts(PlatformFlow/StepView) +ios/flow.ts+android/flow.ts+ the platform-neutraltail/flow.ts. Pure views/reducers/effects; zero ink imports.getIosResumeStep/android twin route gates (credentials-exist), the .p8 chain, verify-app, the import fork, and the marker-guarded tail.Deliberate behavior changes (3 — everything else is parity)
DISTRIBUTION-type certs instead of deprecatedIOS_DISTRIBUTION; the cert-limit revoke picker scopes to the pool that's actually full (the old mixed list defaulted the cursor onto the user's production cert).An adversarial audit verified these are the ONLY behavior changes: running the pre-change private TUI suite (which characterized main) against this branch's dist goes red on exactly those — nothing else. Test diff vs main is 100% additive (23 new files, +10.6k/-0).
Tested
test:helper-dcepreserved through the merge.Merge notes
1044e70.