Skip to content

feat(cli): Appflow → Capgo migration in build init#2566

Draft
WcaleNieWolny wants to merge 14 commits into
mainfrom
feat/appflow-migration-init
Draft

feat(cli): Appflow → Capgo migration in build init#2566
WcaleNieWolny wants to merge 14 commits into
mainfrom
feat/appflow-migration-init

Conversation

@WcaleNieWolny

Copy link
Copy Markdown
Contributor

Appflow → Capgo migration in capgo build init

Adds a "Both, I'm migrating from Ionic Appflow" option to the capgo build init platform 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.md
Plan: docs/superpowers/plans/2026-06-22-appflow-migration.md

What it does

  • Auth (appflow/auth.ts): loopback PKCE login (ported from the Ionic CLI, no @ionic/cli dep), token cache + refresh, OAuth state validated against CSRF/code-injection.
  • API + mapping (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.
  • Flow (appflow/flow.ts): a PlatformFlow driven 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.
  • Validation (advisory, surfaced, NEVER blocks): Android SA Google-Play probe, keystore local unlock, iOS app-specific-password authenticateForSession, iOS .p12 local open.
  • Gap-fill: missing iOS upload destination → generate an App Store Connect .p8 API key via the existing ASC key helper; step-8 offers app-specific-password → .p8 upgrade. (Android service-account generation is routed to the guided Android setup — see Known limitations.)
  • Build + finish (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. No ios/, android/, or tail/ 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 the fix(onboarding): resolve hostile-review findings commit.

Known limitations (spec vs codebase)

  • 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 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.
  • The MCP surface persists creds and routes the build to 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

  • Unit/contract: auth (PKCE, state, refresh, timer/server cleanup), api mapping (data-URI strip, bundleId, provisioning map, field omission), app-password validator, flow step-graph (resume, auto-select, gap-fill/p8 routing, multi-cert no-loop), org/app fetch + populate, API non-2xx throws, tail reuse, engine routing. All test/test-appflow-*.mjs green + 98 mcp-onboarding.
  • bun run typecheck, bun run lint, bun run build all green.
  • E2E (in 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.
  • Manual: run a real Appflow migration end to end against a live account.

https://claude.ai/code/session_01AZWP4JPPHfZ3CyECJesm3S

…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
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 08b266d5-dcd2-46e4-9b12-fd3ed99d597f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@codspeed-hq

codspeed-hq Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 43 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing feat/appflow-migration-init (756ee3f) with main (a52498a)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

🧪 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
@sonarqubecloud

Copy link
Copy Markdown

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant