From e819fa0daccde2a98a21820f620ea5f3200404e1 Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Mon, 13 Apr 2026 19:45:21 +0200 Subject: [PATCH 01/11] Improve onboarding recovery flows --- package.json | 3 +- skills/native-builds/SKILL.md | 30 ++-- skills/usage/SKILL.md | 16 +- src/build/onboarding/recovery.ts | 101 +++++++++++++ src/build/onboarding/types.ts | 3 + src/build/onboarding/ui/app.tsx | 222 ++++++++++++++++++++++----- src/init/command.ts | 244 ++++++++++++++++++++++++++---- src/onboarding-support.ts | 94 ++++++++++++ src/runner-command.ts | 9 ++ test/test-onboarding-recovery.mjs | 84 ++++++++++ webdocs/build.mdx | 12 +- webdocs/init.mdx | 6 +- 12 files changed, 724 insertions(+), 100 deletions(-) create mode 100644 src/build/onboarding/recovery.ts create mode 100644 src/onboarding-support.ts create mode 100644 src/runner-command.ts create mode 100644 test/test-onboarding-recovery.mjs diff --git a/package.json b/package.json index 9445ab74..de985eb8 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "test:build-zip-filter": "bun test/test-build-zip-filter.mjs", "test:checksum": "bun test/test-checksum-algorithm.mjs", "test:ci-prompts": "bun test/test-ci-prompts.mjs", + "test:onboarding-recovery": "bun test/test-onboarding-recovery.mjs", "test:prompt-preferences": "bun test/test-prompt-preferences.mjs", "test:esm-sdk": "node test/test-sdk-esm.mjs", "test:mcp": "node test/test-mcp.mjs", @@ -79,7 +80,7 @@ "test:version-detection:setup": "./test/fixtures/setup-test-projects.sh", "test:platform-paths": "bun test/test-platform-paths.mjs", "test:payload-split": "bun test/test-payload-split.mjs", - "test": "bun run test:bundle && bun run test:functional && bun run test:semver && bun run test:version-edge-cases && bun run test:regex && bun run test:upload && bun run test:credentials && bun run test:credentials-validation && bun run test:build-zip-filter && bun run test:checksum && bun run test:ci-prompts && bun run test:prompt-preferences && bun run test:esm-sdk && bun run test:mcp && bun run test:version-detection && bun run test:platform-paths && bun run test:payload-split" + "test": "bun run test:bundle && bun run test:functional && bun run test:semver && bun run test:version-edge-cases && bun run test:regex && bun run test:upload && bun run test:credentials && bun run test:credentials-validation && bun run test:build-zip-filter && bun run test:checksum && bun run test:ci-prompts && bun run test:onboarding-recovery && bun run test:prompt-preferences && bun run test:esm-sdk && bun run test:mcp && bun run test:version-detection && bun run test:platform-paths && bun run test:payload-split" }, "devDependencies": { "@antfu/eslint-config": "^7.0.0", diff --git a/skills/native-builds/SKILL.md b/skills/native-builds/SKILL.md index 6a2b54b1..d3b250a3 100644 --- a/skills/native-builds/SKILL.md +++ b/skills/native-builds/SKILL.md @@ -13,8 +13,8 @@ Use this skill for Capgo Cloud native iOS and Android build workflows. - Interactive command that automates iOS certificate and provisioning profile creation. - Reduces iOS setup from ~10 manual steps to 1 manual step (creating an API key) + 1 command. -- Example: `npx @capgo/cli@latest build init` -- Backward compatibility: `npx @capgo/cli@latest build onboarding` still works. +- Example: `bunx @capgo/cli@latest build init` +- Backward compatibility: `bunx @capgo/cli@latest build onboarding` still works. - Notes: - Uses Ink (React for terminal) for the interactive UI, alongside the main `init` onboarding flow. - Requires running inside a Capacitor project directory with an `ios/` folder. @@ -24,6 +24,8 @@ Use this skill for Capgo Cloud native iOS and Android build workflows. - Progress persists in `~/.capgo-credentials/onboarding/.json` — safe to interrupt and resume. - Saves credentials to the same `~/.capgo-credentials/credentials.json` used by `build request`. - Optionally kicks off the first build at the end. + - If the native `ios/` folder is missing, onboarding can offer to run `cap add ios` automatically instead of exiting immediately. + - Unexpected failures now keep the user inside the recovery screen, show package-manager-aware commands, and save a support bundle under `~/.capgo-credentials/support/`. #### What it automates (iOS) @@ -71,7 +73,7 @@ interface BuildLogger { ### `build request [appId]` -- Example: `npx @capgo/cli@latest build request com.example.app --platform ios --path .` +- Example: `bunx @capgo/cli@latest build request com.example.app --platform ios --path .` - Notes: - Zips the current project directory and uploads it to Capgo for building. - Builds are processed for store distribution. @@ -129,7 +131,7 @@ Credentials are stored locally, either globally in `~/.capgo-credentials/credent - Example iOS flow: ```bash -npx @capgo/cli build credentials save --platform ios \ +bunx @capgo/cli build credentials save --platform ios \ --certificate ./cert.p12 --p12-password "password" \ --ios-provisioning-profile ./profile.mobileprovision \ --apple-key ./AuthKey.p8 --apple-key-id "KEY123" \ @@ -139,7 +141,7 @@ npx @capgo/cli build credentials save --platform ios \ - Example multi-target iOS flow: ```bash -npx @capgo/cli build credentials save --platform ios \ +bunx @capgo/cli build credentials save --platform ios \ --ios-provisioning-profile ./App.mobileprovision \ --ios-provisioning-profile com.example.widget=./Widget.mobileprovision ``` @@ -147,7 +149,7 @@ npx @capgo/cli build credentials save --platform ios \ - Example Android flow: ```bash -npx @capgo/cli build credentials save --platform android \ +bunx @capgo/cli build credentials save --platform android \ --keystore ./release.keystore --keystore-alias "my-key" \ --keystore-key-password "key-pass" \ --play-config ./service-account.json @@ -186,8 +188,8 @@ npx @capgo/cli build credentials save --platform android \ ### `build credentials list` - Examples: - - `npx @capgo/cli build credentials list` - - `npx @capgo/cli build credentials list --appId com.example.app` + - `bunx @capgo/cli build credentials list` + - `bunx @capgo/cli build credentials list --appId com.example.app` - Options: - `--appId ` - `--local` @@ -195,9 +197,9 @@ npx @capgo/cli build credentials save --platform android \ ### `build credentials clear` - Examples: - - `npx @capgo/cli build credentials clear` - - `npx @capgo/cli build credentials clear --local` - - `npx @capgo/cli build credentials clear --appId com.example.app --platform ios` + - `bunx @capgo/cli build credentials clear` + - `bunx @capgo/cli build credentials clear --local` + - `bunx @capgo/cli build credentials clear --appId com.example.app --platform ios` - Options: - `--appId ` - `--platform ` @@ -208,8 +210,8 @@ npx @capgo/cli build credentials save --platform android \ - Use to update specific credential fields without re-entering all data. - Platform is auto-detected from the supplied options. - Examples: - - `npx @capgo/cli build credentials update --ios-provisioning-profile ./new-profile.mobileprovision` - - `npx @capgo/cli build credentials update --local --keystore ./new-keystore.jks` + - `bunx @capgo/cli build credentials update --ios-provisioning-profile ./new-profile.mobileprovision` + - `bunx @capgo/cli build credentials update --local --keystore ./new-keystore.jks` - Core options: - `--appId ` - `--platform ` @@ -222,7 +224,7 @@ npx @capgo/cli build credentials save --platform android \ ### `build credentials migrate` -- Example: `npx @capgo/cli build credentials migrate --platform ios` +- Example: `bunx @capgo/cli build credentials migrate --platform ios` - Notes: - Converts `BUILD_PROVISION_PROFILE_BASE64` to `CAPGO_IOS_PROVISIONING_MAP`. - Discovers the main bundle ID from the Xcode project automatically. diff --git a/skills/usage/SKILL.md b/skills/usage/SKILL.md index 4608fa3b..085a27ea 100644 --- a/skills/usage/SKILL.md +++ b/skills/usage/SKILL.md @@ -16,7 +16,7 @@ TanStack Intent skills should stay focused and under the validator line limit, s ## Shared invocation rules -- Prefer `npx @capgo/cli@latest ...` in user-facing examples to match existing docs and CLI output. +- Prefer `bunx @capgo/cli@latest ...` in user-facing examples in this repo. - Many commands can infer `appId` and related config from the current Capacitor project. - Shared public flags commonly include `-a, --apikey ` and `--verbose` on commands that support verbose output. @@ -24,7 +24,7 @@ TanStack Intent skills should stay focused and under the validator line limit, s ### Project setup and diagnostics -- `init [apikey] [appId]`: guided first-time setup for Capgo in a Capacitor app. The interactive flow now runs as a real Ink-based fullscreen onboarding so it uses the same UI stack as `build init` (alias: `build onboarding`), with a persistent dashboard, phase roadmap, progress cards, shared log area, and resume support. When dependency auto-detection fails on macOS, the flow opens a native file picker for `package.json` before falling back to manual path entry. If the user reuses a pending app that was already created in the web onboarding flow, the CLI syncs that selected dashboard app ID back into `capacitor.config.*` before the remaining steps continue. Outside that reused pending-app path, the CLI keeps using the local Capacitor app ID. It can also offer a final `npx skills add https://github.com/Cap-go/capgo-skills -g -y` install step before the GitHub support prompt; if accepted, the support menu includes `Cap-go/capgo-skills` alongside the updater-only and all-Capgo choices. If iOS sync validation fails during onboarding, the CLI can offer to run a one-line native reset command, wait for you to type `ready` after a manual fix, and offer cancellation every third failed retry. +- `init [apikey] [appId]`: guided first-time setup for Capgo in a Capacitor app. The interactive flow now runs as a real Ink-based fullscreen onboarding so it uses the same UI stack as `build init` (alias: `build onboarding`), with a persistent dashboard, phase roadmap, progress cards, shared log area, and resume support. When dependency auto-detection fails on macOS, the flow opens a native file picker for `package.json` before falling back to manual path entry. If the user reuses a pending app that was already created in the web onboarding flow, the CLI syncs that selected dashboard app ID back into `capacitor.config.*` before the remaining steps continue. Outside that reused pending-app path, the CLI keeps using the local Capacitor app ID. It can also offer a final `bunx skills add https://github.com/Cap-go/capgo-skills -g -y` install step before the GitHub support prompt; if accepted, the support menu includes `Cap-go/capgo-skills` alongside the updater-only and all-Capgo choices. If native platforms are missing, the onboarding can offer to run `cap add` for you. If iOS sync validation fails during onboarding, the CLI can offer to run a one-line native reset command, wait for you to type `ready` after a manual fix, surface `doctor`, and save a support bundle before you leave the flow. - `login [apikey]`: store an API key locally. - `doctor`: inspect installation health and gather troubleshooting details. - `probe`: test whether the update endpoint would deliver an update. @@ -80,10 +80,10 @@ Load `skills/organization-management/SKILL.md` when working with: ## Common command examples ```bash -npx @capgo/cli@latest init YOUR_API_KEY com.example.app -npx @capgo/cli@latest login YOUR_API_KEY -npx @capgo/cli@latest doctor -npx @capgo/cli@latest probe --platform ios -npx @capgo/cli@latest app add com.example.app --name "My App" -npx @capgo/cli@latest star-all +bunx @capgo/cli@latest init YOUR_API_KEY com.example.app +bunx @capgo/cli@latest login YOUR_API_KEY +bunx @capgo/cli@latest doctor +bunx @capgo/cli@latest probe --platform ios +bunx @capgo/cli@latest app add com.example.app --name "My App" +bunx @capgo/cli@latest star-all ``` diff --git a/src/build/onboarding/recovery.ts b/src/build/onboarding/recovery.ts new file mode 100644 index 00000000..afd09203 --- /dev/null +++ b/src/build/onboarding/recovery.ts @@ -0,0 +1,101 @@ +import type { OnboardingStep } from './types.js' +import { formatRunnerCommand } from '../../runner-command.js' + +export interface BuildOnboardingRecoveryAdvice { + summary: string[] + commands: string[] + docs: string[] +} + +export function getBuildOnboardingRecoveryAdvice( + message: string, + step: OnboardingStep | null, + pmRunner: string, + appId: string, +): BuildOnboardingRecoveryAdvice { + const lower = message.toLowerCase() + const summary: string[] = [] + const commands = new Set() + const docs = new Set() + + const addIosCommand = formatRunnerCommand(pmRunner, ['cap', 'add', 'ios']) + const syncIosCommand = formatRunnerCommand(pmRunner, ['cap', 'sync', 'ios']) + const doctorCommand = formatRunnerCommand(pmRunner, ['@capgo/cli@latest', 'doctor']) + const buildInitCommand = formatRunnerCommand(pmRunner, ['@capgo/cli@latest', 'build', 'init']) + const buildRequestCommand = formatRunnerCommand(pmRunner, ['@capgo/cli@latest', 'build', 'request', appId, '--platform', 'ios']) + const loginCommand = formatRunnerCommand(pmRunner, ['@capgo/cli@latest', 'login']) + + if (step === 'no-platform' || step === 'adding-platform' || lower.includes('no ios/ directory')) { + summary.push('This project does not have a generated native iOS folder yet.') + summary.push('Create the iOS platform, then sync native sources before resuming onboarding.') + commands.add(addIosCommand) + commands.add(syncIosCommand) + } + + if (lower.includes('api key verification failed') || lower.includes('401') || lower.includes('403')) { + summary.push('Apple rejected the App Store Connect credentials.') + summary.push('Double-check the .p8 file, Key ID, Issuer ID, and that the key still has Admin or Developer access.') + docs.add('https://capgo.app/docs/cli/cloud-build/ios/') + docs.add('https://appstoreconnect.apple.com/access/integrations/api') + } + + if (lower.includes('fetch failed') || lower.includes('network') || lower.includes('etimedout') || lower.includes('enotfound') || lower.includes('econnreset')) { + summary.push('The CLI could not reach Apple or Capgo over the network.') + summary.push('Check VPN, proxy, firewall, and DNS settings, then retry from the saved step.') + commands.add(doctorCommand) + } + + if (lower.includes('429') || lower.includes('rate limit')) { + summary.push('Apple is rate-limiting the request right now.') + summary.push('Wait a minute, then retry from the saved step instead of restarting the whole flow.') + } + + if (lower.includes('certificate limit')) { + summary.push('Apple has reached the maximum number of active distribution certificates for this team.') + } + + if (lower.includes('duplicate profile')) { + summary.push('Apple still has conflicting provisioning profiles for this bundle identifier.') + summary.push('You can let onboarding delete the duplicates automatically, or clean them up in App Store Connect and resume.') + docs.add('https://appstoreconnect.apple.com/access/users') + } + + if (lower.includes('bundle') && lower.includes('identifier')) { + summary.push('Apple reported a bundle identifier conflict or bundle registration issue.') + summary.push(`Verify that ${appId} is the bundle ID you intend to build for in both Capgo and your Capacitor config.`) + commands.add(doctorCommand) + } + + if (lower.includes('file not found') || lower.includes('could not read file') || lower.includes('need .p8 file')) { + summary.push('The onboarding flow could not read the API key file from disk.') + summary.push('Re-select the .p8 file or move it somewhere stable before retrying.') + } + + if (lower.includes('no capgo api key found')) { + summary.push('Capgo login is missing, so the first cloud build cannot be requested automatically.') + commands.add(loginCommand) + commands.add(buildRequestCommand) + } + + if (lower.includes('credentials are saved')) { + summary.push('Your signing material is already saved locally, so you only need to re-run the build request.') + commands.add(buildRequestCommand) + } + + if (summary.length === 0) { + summary.push('The onboarding flow hit an unexpected error.') + summary.push('Retry the saved step first. If it still fails, capture diagnostics and keep the support bundle when you contact support.') + commands.add(doctorCommand) + commands.add(buildInitCommand) + docs.add('https://capgo.app/docs/cli/cloud-build/ios/') + } + else { + commands.add(buildInitCommand) + } + + return { + summary, + commands: Array.from(commands), + docs: Array.from(docs), + } +} diff --git a/src/build/onboarding/types.ts b/src/build/onboarding/types.ts index 3ea5b29d..6d0b3284 100644 --- a/src/build/onboarding/types.ts +++ b/src/build/onboarding/types.ts @@ -5,6 +5,7 @@ export type Platform = 'ios' | 'android' export type OnboardingStep = | 'welcome' | 'platform-select' + | 'adding-platform' | 'credentials-exist' | 'backing-up' | 'api-key-instructions' @@ -66,6 +67,7 @@ export interface OnboardingProgress { export const STEP_PROGRESS: Record = { 'welcome': 0, 'platform-select': 0, + 'adding-platform': 0, 'credentials-exist': 0, 'backing-up': 0, 'api-key-instructions': 5, @@ -92,6 +94,7 @@ export function getPhaseLabel(step: OnboardingStep): string { switch (step) { case 'welcome': case 'platform-select': + case 'adding-platform': case 'credentials-exist': case 'backing-up': return '' diff --git a/src/build/onboarding/ui/app.tsx b/src/build/onboarding/ui/app.tsx index 6bbef33a..387a87de 100644 --- a/src/build/onboarding/ui/app.tsx +++ b/src/build/onboarding/ui/app.tsx @@ -2,6 +2,7 @@ import type { FC } from 'react' import type { BuildLogger } from '../../request.js' import type { ApiKeyData, CertificateData, OnboardingProgress, OnboardingStep, ProfileData } from '../types.js' import { handleCustomMsg } from '../../qr.js' +import { spawn } from 'node:child_process' import { Buffer } from 'node:buffer' import { existsSync } from 'node:fs' import { copyFile, readFile } from 'node:fs/promises' @@ -13,13 +14,16 @@ import { Box, Newline, Text, useApp, useInput, useStdout } from 'ink' import open from 'open' // src/build/onboarding/ui/app.tsx import React, { useCallback, useEffect, useRef, useState } from 'react' -import { findSavedKey } from '../../../utils.js' +import { writeOnboardingSupportBundle } from '../../../onboarding-support.js' +import { formatRunnerCommand, splitRunnerCommand } from '../../../runner-command.js' +import { findSavedKey, getPMAndCommand } from '../../../utils.js' import { loadSavedCredentials, updateSavedCredentials } from '../../credentials.js' import { requestBuildInternal } from '../../request.js' import { CertificateLimitError, createCertificate, createProfile, deleteProfile, DuplicateProfileError, ensureBundleId, generateJwt, revokeCertificate, verifyApiKey } from '../apple-api.js' import { createP12, DEFAULT_P12_PASSWORD, generateCsr } from '../csr.js' import { canUseFilePicker, openFilePicker } from '../file-picker.js' import { deleteProgress, getResumeStep, loadProgress, saveProgress } from '../progress.js' +import { getBuildOnboardingRecoveryAdvice } from '../recovery.js' import { getPhaseLabel, @@ -36,6 +40,36 @@ interface AppProps { iosDir: string } +async function runRunnerCommand(runner: string, args: string[]): Promise<{ success: boolean, output: string[] }> { + const { command, args: runnerArgs } = splitRunnerCommand(runner) + + return new Promise((resolve) => { + const child = spawn(command, [...runnerArgs, ...args], { + stdio: ['ignore', 'pipe', 'pipe'], + }) + const output: string[] = [] + + const append = (chunk: Buffer | string) => { + const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8') + for (const rawLine of text.split(/\r?\n/)) { + const line = rawLine.replace(/\r/g, '').trim() + if (line) + output.push(line) + } + } + + child.stdout?.on('data', append) + child.stderr?.on('data', append) + child.once('error', (error) => { + output.push(error.message) + resolve({ success: false, output }) + }) + child.once('close', (code) => { + resolve({ success: code === 0, output }) + }) + }) +} + const OnboardingApp: FC = ({ appId, initialProgress, iosDir }) => { const { exit } = useApp() const startStep = getResumeStep(initialProgress) @@ -90,11 +124,20 @@ const OnboardingApp: FC = ({ appId, initialProgress, iosDir }) => { const [profileData, setProfileData] = useState(initialProgress?.completedSteps.profileCreated || null) const [buildUrl, setBuildUrl] = useState('') const [buildOutput, setBuildOutput] = useState([]) + const [supportBundlePath, setSupportBundlePath] = useState(null) const addLog = useCallback((text: string, color = 'green') => { setLog(prev => [...prev, { text, color }]) }, []) + const pm = getPMAndCommand() + const addIosCommand = formatRunnerCommand(pm.runner, ['cap', 'add', 'ios']) + const syncIosCommand = formatRunnerCommand(pm.runner, ['cap', 'sync', 'ios']) + const doctorCommand = formatRunnerCommand(pm.runner, ['@capgo/cli@latest', 'doctor']) + const buildInitCommand = formatRunnerCommand(pm.runner, ['@capgo/cli@latest', 'build', 'init']) + const buildRequestCommand = formatRunnerCommand(pm.runner, ['@capgo/cli@latest', 'build', 'request', appId, '--platform', 'ios']) + const loginCommand = formatRunnerCommand(pm.runner, ['@capgo/cli@latest', 'login']) + const exitOnboarding = useCallback((message?: string) => { if (exitRequestedRef.current) return @@ -200,19 +243,30 @@ const OnboardingApp: FC = ({ appId, initialProgress, iosDir }) => { return } const message = err instanceof Error ? err.message : String(err) - if (retryCount === 0) { - setError(message) - setRetryStep(failedStep) - setRetryCount(1) - setStep('error') - } - else { - // Second failure — exit - addLog(`✖ ${message}`, 'red') - addLog('Run `capgo build init` to retry from where you left off.', 'yellow') - setTimeout(() => exitOnboarding(), 100) + const nextRetryCount = retryCount + 1 + const bundlePath = writeOnboardingSupportBundle({ + kind: 'build-init', + appId, + currentStep: failedStep, + packageManager: pm.pm, + cwd: process.cwd(), + error: message, + commands: [buildInitCommand, doctorCommand], + docs: ['https://capgo.app/docs/cli/cloud-build/ios/'], + logs: [ + ...log.slice(-12).map(entry => entry.text), + ...buildOutput.slice(-12), + ], + }) + setSupportBundlePath(bundlePath) + setError(message) + setRetryStep(failedStep) + setRetryCount(nextRetryCount) + if (nextRetryCount > 1) { + addLog(`⚠ Attempt ${nextRetryCount} failed. Recovery steps and a support bundle are available below.`, 'yellow') } - }, [retryCount, addLog, exitOnboarding]) + setStep('error') + }, [retryCount, addLog, appId, buildInitCommand, buildOutput, doctorCommand, log, pm.pm]) // ── Credential save logic ── @@ -291,10 +345,28 @@ const OnboardingApp: FC = ({ appId, initialProgress, iosDir }) => { } if (step === 'no-platform') { - setTimeout(() => { - if (!cancelled) - exit() - }, 2000) + pickerOpenedRef.current = false + } + + if (step === 'adding-platform') { + ;(async () => { + const result = await runRunnerCommand(pm.runner, ['cap', 'add', 'ios']) + if (cancelled) + return + + if (result.success && existsSync(join(process.cwd(), iosDir))) { + addLog(`✔ Native iOS platform created with ${addIosCommand}`) + setError(null) + setRetryCount(0) + setStep('platform-select') + return + } + + const detail = result.output.length > 0 + ? `\n${result.output.slice(-6).join('\n')}` + : '' + handleError(new Error(`Could not add the iOS platform automatically.${detail}`), 'adding-platform') + })() } if (step === 'p8-method-select' && !pickerOpenedRef.current) { @@ -325,9 +397,7 @@ const OnboardingApp: FC = ({ appId, initialProgress, iosDir }) => { catch (err) { if (cancelled) return - setError(`Could not read file: ${err instanceof Error ? err.message : String(err)}`) - setRetryStep('api-key-instructions') - setStep('error') + handleError(new Error(`Could not read file: ${err instanceof Error ? err.message : String(err)}`), 'api-key-instructions') } })() } @@ -519,7 +589,7 @@ const OnboardingApp: FC = ({ appId, initialProgress, iosDir }) => { } if (!capgoKey) { setBuildOutput(prev => [...prev, '⚠ No Capgo API key found.']) - setBuildOutput(prev => [...prev, 'Run `capgo login` first, then `capgo build request`.']) + setBuildOutput(prev => [...prev, `Run \`${loginCommand}\` first, then \`${buildRequestCommand}\`.`]) setStep('build-complete') return } @@ -574,7 +644,7 @@ const OnboardingApp: FC = ({ appId, initialProgress, iosDir }) => { // Build failure is non-fatal — credentials are saved if (!cancelled) { setBuildOutput(prev => [...prev, `⚠ ${err instanceof Error ? err.message : String(err)}`]) - setBuildOutput(prev => [...prev, 'Your credentials are saved. Run `npx @capgo/cli@latest build request --platform ios` to try again.']) + setBuildOutput(prev => [...prev, `Your credentials are saved. Run \`${buildRequestCommand}\` to try again.`]) setStep('build-complete') } } @@ -603,9 +673,12 @@ const OnboardingApp: FC = ({ appId, initialProgress, iosDir }) => { const progress = STEP_PROGRESS[step] ?? 0 const phaseLabel = getPhaseLabel(step) - const showProgress = step !== 'welcome' && step !== 'platform-select' && step !== 'error' && step !== 'build-complete' && step !== 'requesting-build' + const showProgress = step !== 'welcome' && step !== 'platform-select' && step !== 'adding-platform' && step !== 'no-platform' && step !== 'error' && step !== 'build-complete' && step !== 'requesting-build' const showHeader = step !== 'requesting-build' const showLog = step !== 'requesting-build' && step !== 'build-complete' + const recoveryAdvice = error + ? getBuildOnboardingRecoveryAdvice(error, retryStep, pm.runner, appId) + : null return ( @@ -678,14 +751,44 @@ const OnboardingApp: FC = ({ appId, initialProgress, iosDir }) => { {/* No platform directory */} {step === 'no-platform' && ( - + - - Run - npx cap add ios - {' '} - first, then re-run onboarding. - + This onboarding flow needs a generated native iOS project before credentials can be created. + + {`Suggested commands: ${addIosCommand} && ${syncIosCommand}`} + +