feat(cli): Appflow → Capgo migration in build init#2566
Conversation
…g, app-password validator Self-contained modules for the Appflow->Capgo migration: - appflow/auth.ts: OAuth Authorization Code + PKCE login (ported from Ionic CLI; no @ionic/cli dep), token refresh, expiry check. - appflow/api.ts: GraphQL + REST client (dashboard headers), org/app/cert/distribution listing, signing/distribution download, and the Appflow->Capgo credential mapping. Records a REDACTED request/response trace (no secret values) for the support bundle. - ios/validate-app-password.ts: advisory authenticateForSession check (never throws/blocks). Claude-Session: https://claude.ai/code/session_01AZWP4JPPHfZ3CyECJesm3S
…gapfill/build views) appflow/types.ts (AppflowStep/Progress/scope) and appflow/flow.ts implementing PlatformFlow: auth/org/app/signing/distribution effects, scope-aware routing (both|ios|android), auto-select singletons, the no-signing recovery submenu, advisory non-blocking runValidations, step-6 distribution gap-fill + step-8 p8-conversion views, and the build handoff (skippable native + JS build). Claude-Session: https://claude.ai/code/session_01AZWP4JPPHfZ3CyECJesm3S
…r appflowFlow - Platform type gains 'appflow'; picker shows "Both, I'm migrating from Ionic Appflow" (list + cards, key 'a'/'3'); command.ts widened for the picker callback (detection stays ios/android). - flow/appflow-flow.ts registers appflowFlow alongside ios/android flows. Claude-Session: https://claude.ai/code/session_01AZWP4JPPHfZ3CyECJesm3S
- mcp/engine.ts: decideAppflow drives appflowFlow (bounded auto-loop like decideIos); picker gains the "Both, migrating from Appflow" option; ios/android pick offers a "migrating from Appflow?" gate that routes to the scoped migration; in-flight migration resumes on a bare next_step. - session-state.ts: process-local appflow progress + gate. - ui/shell.tsx + ui/appflow-app.tsx: generic neutral-StepView renderer mounts the appflow flow. - appflow/deps.ts: production AppflowEffectDeps (token cache, redacted internal-log trace, reused validators) + persistAppflowCredentials into the real Capgo credential store. Claude-Session: https://claude.ai/code/session_01AZWP4JPPHfZ3CyECJesm3S
…cross drivers Both drivers (TUI appflow-app + MCP decideAppflow) advance interactive steps via applyInput -> resumeStep and only run runEffect for 'auto' steps. Three consequences were unhandled: - `validate` was an 'info' step, so runValidations NEVER ran (dead advisory checks). Make `validate` an AUTO step that runs the checks, then transition to a new `validate-results` info view that surfaces pass/warn/skipped (never blocks). - step-6 gap-fill (ios/android-dist-gapfill) and step-8 p8 upgrade rendered as choices but had no applyInput/resume handling -> the choice was ignored and the graph could loop. Encode the full spec step order in getAppflowResumeStep (signing -> distribution -> gap-fill -> validate -> p8-upgrade -> handoff) and record the gap-fill / p8 decisions in applyAppflowInput. - fetch-distribution early-returned to the iOS gap-fill, abandoning Android. Fetch BOTH in-scope platforms, then route via getAppflowResumeStep. Also add the spec's optional iOS .p12 local check (advisory) to runValidations, wired in deps.ts by reusing the PKCS#12 unlock. Tests cover the new routing, validate-results, and p12 pass/warn/skipped. Claude-Session: https://claude.ai/code/session_01AZWP4JPPHfZ3CyECJesm3S
…migration Implements the spec's "Build + finish": on `handoff-build` -> 'build' the migration drives the existing platform-neutral tail (tail/flow.ts) inline instead of only deferring to `capgo build request`. Reuse is at the component level (the spec's "reuse, not a 100% merge"): a new appflow/tail.ts builds a TailEffectDeps from the already-migrated per-platform Capgo creds, reusing the SAME shared building blocks the iOS/Android drivers inject (ci-secrets, env-export, workflow generate/write, requestBuildInternal, package-script detection). No ios/android/tail internals changed. - types.ts: AppflowStep widened with the tail steps + build-platform-pick; tail-input fields (setupMode, ciSecretTarget, selectedPackageManager, buildScriptChoice, envExportTargetPath) + buildPlatform/builtPlatforms on AppflowProgress. - flow.ts: handoff 'build' routes into the tail (build-platform-pick when BOTH platforms migrated, else straight in); 'skip' finishes with creds persisted. Tail steps delegate to tailViewForStep/applyTailInput/runTailEffect; after a platform builds it offers the second, then done. Build failure reuses the tail's AI/email branch. - ui/appflow-app.tsx: threads tail deps + streams the build log via the bespoke FullscreenBuildOutput at requesting-build; generic renderer handles the rest. - MCP decideAppflow stays out of scope per the spec (still persists + routes to the native build path); all 98 mcp-onboarding tests pass. - test-appflow-tail.mjs (18 cases) covers build/skip routing, platform-first, and the saved-credential shaping. Registered in package.json. Claude-Session: https://claude.ai/code/session_01AZWP4JPPHfZ3CyECJesm3S
The step-6 iOS gap-fill 'generate' and step-8 'convert' choices now drive the existing standalone App Store Connect API-key helper (asc-key/helper.ts runAscKeyHelper) and capture the produced key into progress.ios as APPLE_KEY_ID / APPLE_ISSUER_ID / APPLE_KEY_CONTENT (base64) — the same fields the native iOS flow persists (ios/flow.ts:934) and the build-request path consumes (request.ts). On 'convert' the now-redundant app-specific password is dropped in favour of the .p8. Both steps render as neutral 'auto' StepViews (generic in both the TUI and MCP drivers) and run at most once; a missing/failed generator records an advisory note and continues (never blocks). Android service-account generation is NOT standalone-reusable: there is no "generate PLAY_CONFIG_JSON" function — the project/dev-id/package chain is interactive and welded into android/flow.ts's wizard. Rather than duplicate the protected flow, android-dist-gapfill 'generate' records intent and routes the user to the dedicated Android setup (advisory, non-blocking). See report to the user: the spec assumed 100% SA reuse, which the codebase does not currently expose. Claude-Session: https://claude.ai/code/session_01AZWP4JPPHfZ3CyECJesm3S
…tion A multi-engine hostile review surfaced that the mocked unit tests masked real end-to-end gaps. Fixes all 6 CRITICAL + 14 HIGH + bounded MEDIUM/LOW: CRITICAL - Org/app selection was non-functional: nothing fetched/populated the org+app lists. Add `fetch-orgs`/`fetch-apps` AUTO steps calling api.listOrgs/listApps with a 0->error / 1->auto-select / 2+->prompt contract; thread the auto effect's transient options into the choice view ctx in BOTH drivers (TUI + MCP). - Multi-cert apps livelocked: select-ios/android-cert choices were discarded and fetch-signing was never marked done on the prompt branch. Store the chosen tag, mark fetch-signing up front, re-enter via resolveCertTag, process both platforms. - api.ts rest()/gql() swallowed every HTTP/transport/parse error and returned undefined -> false "no signing exists". They now THROW AppflowApiError on non-2xx / GraphQL errors / parse failure (status + redacted snippet); the driver surfaces a real error instead of a fabricated empty result. HIGH - Distribution 2+ entries silently dropped -> add select-ios/android-dist prompts. - Mappers fabricated APPLE_APP_ID:"undefined" -> omit absent fields (setIfPresent), typed the `raw: any` shapes. - OAuth `state` generated but never validated -> waitForCode validates state before the token exchange and rejects mismatches (CSRF/code-injection); token-exchange error body surfaced; raw error= sanitized. - appflowHandoff swallowed a persist failure then reported success -> surface it; no-signing submenu go-back/email-support/abandon no longer silently advance. MEDIUM/LOW - auth timer/server leaks (clearTimeout + always-close + AbortSignal), atomic token write + secure dir perms, encodeURIComponent on REST path segments, sanitized MCP summary, validator distinguishes auth-reject vs unreachable, honest Android SA gap-fill (no fake "generate"), both-scope dropped-platform note surfaced. New test-appflow-fetch.mjs covers org/app fetch+auto-select+advance, multi-cert no-loop, API non-2xx throwing, and mapper field omission. typecheck/lint/all appflow suites/98 mcp-onboarding/build all green. Claude-Session: https://claude.ai/code/session_01AZWP4JPPHfZ3CyECJesm3S
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
Merging this PR will not alter performance
Comparing Footnotes
|
🧪 Builder onboarding TUI preview — ❌ failed▶ Open the interactive HTML report (zoomable journey tree + cast playback) Commit: 5743d7b · Job summary with the result table |
…ection The e2e suite surfaced that on a multi-cert (or multi-distribution) app, picking an entry stored the tag/id but left `fetch-signing`/`fetch-distribution` marked done — so getAppflowResumeStep skipped the fetch step and the CHOSEN credential was never downloaded (validate then saw "no credentials"). The select-* reducers now un-mark the fetch step so it re-runs and downloads the stored tag/id via resolveCertTag/resolveDistId. Regression test goes through applyInput -> resumeStep (the level the bug lived at). Claude-Session: https://claude.ai/code/session_01AZWP4JPPHfZ3CyECJesm3S
… decode
Real-usage feedback: the migration UI was bare (just a header + plain prompt +
basic Select), gave zero indication of which credentials were imported, the
service-account check failed with a confusing base64 parse error, and the Select
felt "unselectable".
- ui/appflow-app.tsx: bring the migration up to native-flow parity — a
"Step N of 8 · <title>" header + progress bar + divider, an accumulating
"Imported from Appflow" summary box (org, app, iOS cert, provisioning profile
bundle IDs, upload auth, Android keystore alias, Play service account), the
prompt in a rounded info box, and validation results as a colored ✓/⚠/· list.
Key the Select per step so it REMOUNTS — Ink's Select otherwise re-fires
onChange while mounted across a step change, which makes options feel
unselectable; also set visibleOptionCount.
- appflow/deps.ts: PLAY_CONFIG_JSON is stored base64 (the native android
convention), but the SA validator was handed the base64 as if it were raw
JSON ("Unexpected token 'e', \"eyJ0eXBlIj\"..."). Decode it first via a new
exported serviceAccountJsonBytes() helper (base64 -> JSON, passthrough if
already raw). Now the check parses the real SA and reports field-level results.
- test-appflow-sa-decode.mjs: regression test for the base64/raw decode contract.
typecheck/lint/9 appflow suites/98 mcp-onboarding/build all green.
Claude-Session: https://claude.ai/code/session_01AZWP4JPPHfZ3CyECJesm3S
…picker cards The single-platform migration gate used a bare @inkjs/ui Select. Reuse the platform picker's nice bordered boxes for the question + answers via a new generic CardChooser (generalizes PlatformCard + the ←/→/Enter driving + the list fallback for narrow terminals). The gate now shows a centered question + subtitle and two cards — "🔄 Yes, migrate" (default) / "🆕 No, set up <p> fresh" — matching the picker. cardKeyAction is the pure, unit-tested keypress mapping. typecheck/lint/platform-layout (17)/appflow suites/98 mcp-onboarding all green. Claude-Session: https://claude.ai/code/session_01AZWP4JPPHfZ3CyECJesm3S
…to No; analytics notice to bottom Three usage-feedback fixes for the platform picker + migration gate: - Picker arrows could never select the third (Appflow) card — platformKeyAction mapped ←→ iOS / →→ Android only, so arrows toggled two cards and skipped Appflow. Arrows now MOVE across all three cards (←/→ move ±1, clamped; 1/2/3 jump; a jumps to Appflow; Enter confirms). - The single-platform "migrating from Appflow?" gate now defaults to "No, set up <p> fresh" (left, selected) with "Yes, migrate" on the right — picking iOS/ Android directly most often means NOT migrating, so No is the safer default. - The analytics opt-out notice moved from the top (right under the header) to the BOTTOM of each shell screen, below the "← → choose · Enter confirm" legend. platformKeyAction is now move/jump/confirm (unit-tested, 18 cases). typecheck/ lint/platform-layout/98 mcp-onboarding/build all green. Claude-Session: https://claude.ai/code/session_01AZWP4JPPHfZ3CyECJesm3S
|



Appflow → Capgo migration in
capgo build initAdds a "Both, I'm migrating from Ionic Appflow" option to the
capgo build initplatform picker (and a "migrating from Appflow?" gate when picking iOS or Android alone). The migration signs the user in to Ionic/Appflow using the same secure browser OAuth + PKCE pathway the Ionic CLI uses, lets them pick an org + app, pulls that app's signing + distribution credentials out of Appflow, maps them to Capgo build credentials, validates them (advisory), and reuses the existing build/CI tail inline.Design principle: Appflow is a credential SOURCE, not a parallel onboarding. Only auth + org/app selection + credential fetch/map is new; everything downstream (gap-fill generation, validation, build, AI/email, CI/CD) reuses existing code.
Spec:
docs/superpowers/specs/2026-06-22-appflow-migration-build-init-design.mdPlan:
docs/superpowers/plans/2026-06-22-appflow-migration.mdWhat it does
appflow/auth.ts): loopback PKCE login (ported from the Ionic CLI, no@ionic/clidep), token cache + refresh, OAuthstatevalidated against CSRF/code-injection.appflow/api.ts): GraphQL (orgs/apps/certs) + REST (signing + distribution download) with dashboard headers; maps Appflow creds → Capgo keys (cert→BUILD_CERTIFICATE_BASE64/P12_PASSWORD, profiles→CAPGO_IOS_PROVISIONING_MAP, app-specific-password→FASTLANE_*, keystore→ANDROID_KEYSTORE_*, SA→PLAY_CONFIG_JSON). Every request/response is recorded to the internal support-log with secrets redacted.appflow/flow.ts): aPlatformFlowdriven identically by the Ink TUI and the MCP engine. Step graph: explain → auth → fetch-orgs → (select-org) → fetch-apps → (select-app) → fetch-signing (+ cert select on 2+) → fetch-distribution (+ dist select on 2+) → step-6 gap-fill → step-7 validate (advisory, never blocks) → step-8 p8 upgrade → handoff/build. Single-org/app/cert auto-selects.authenticateForSession, iOS.p12local open..p8API key via the existing ASC key helper; step-8 offers app-specific-password →.p8upgrade. (Android service-account generation is routed to the guided Android setup — see Known limitations.)appflow/tail.ts): on "build", reuses the existing build/CI tail inline (build-script pick, native build + streaming log, AI/email on failure, CI/CD secrets + workflow), with a skippable build and a build-platform-first choice when both platforms migrated. Noios/,android/, ortail/internals were modified.Hostile review
A multi-engine hostile review (Claude reviewer fleet + Codex) was run; all 6 CRITICAL + 14 HIGH findings + bounded MEDIUM/LOW were fixed (org/app fetch was non-functional, multi-cert livelock, API errors silently swallowed, mappers fabricating
"undefined", OAuth state unvalidated, dead submenu options, etc.). See thefix(onboarding): resolve hostile-review findingscommit.Known limitations (spec vs codebase)
generate PLAY_CONFIG_JSONfunction — the project/dev-id/package chain is interactive and welded into the Android wizard. Rather than duplicate protected code, the Android distribution gap-fill records intent and routes the user to the guided Android setup. The spec assumed 100% SA reuse, which the codebase does not currently expose. Flagged for a decision.start_capgo_build(the established build-phase contract); the full inline tail is the TUI path, per the spec's "MCP onboarding tool is out of scope".Test plan
test/test-appflow-*.mjsgreen + 98 mcp-onboarding.bun run typecheck,bun run lint,bun run buildall green.Cap-go/cli-mcp-tests): separate PR with recorded-fixture journeys (happy paths, auto-select, multi-cert/org/app, no-signing submenu, gap-fill, validate, p8 upgrade, build/skip). Linked when ready.https://claude.ai/code/session_01AZWP4JPPHfZ3CyECJesm3S