Skip to content

Commit 8b0d90d

Browse files
riderxCopilot
andauthored
Implement Ink-based onboarding experience (#549)
* feat(init): implement Ink-based onboarding experience with interactive prompts and progress tracking - Added onboarding steps and UI components for a guided setup process. - Introduced runtime management for session state, logging, and prompts. - Created reusable prompt components for confirmation, text input, and selection. - Enhanced user experience with progress indicators and session management. - Updated documentation to reflect new onboarding features and functionality. * feat(init): enhance onboarding process with framework detection and Capacitor initialization * feat(init): rename `build onboarding` to `build init` and enhance onboarding flow with improved error handling and file picker integration Co-authored-by: Copilot <copilot@github.com> * feat(init): improve apikey handling in initApp function with fallback to saved key * feat(init): enhance onboarding and prompt handling with improved type safety and error management * feat(init): enhance web directory validation for Next.js and Nuxt.js in setup process --------- Co-authored-by: Copilot <copilot@github.com>
1 parent f0204d2 commit 8b0d90d

File tree

14 files changed

+1737
-296
lines changed

14 files changed

+1737
-296
lines changed

skills/native-builds/SKILL.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ Use this skill for Capgo Cloud native iOS and Android build workflows.
99

1010
## Onboarding (automated iOS setup)
1111

12-
### `build onboarding`
12+
### `build init` (alias: `build onboarding`)
1313

1414
- Interactive command that automates iOS certificate and provisioning profile creation.
1515
- Reduces iOS setup from ~10 manual steps to 1 manual step (creating an API key) + 1 command.
16-
- Example: `npx @capgo/cli@latest build onboarding`
16+
- Example: `npx @capgo/cli@latest build init`
17+
- Backward compatibility: `npx @capgo/cli@latest build onboarding` still works.
1718
- Notes:
18-
- Uses Ink (React for terminal) for the interactive UI — only command that uses Ink; all other commands use `@clack/prompts`.
19+
- Uses Ink (React for terminal) for the interactive UI, alongside the main `init` onboarding flow.
1920
- Requires running inside a Capacitor project directory with an `ios/` folder.
2021
- The user creates ONE App Store Connect API key (.p8 file), then the CLI handles everything else.
2122
- On macOS, offers a native file picker dialog for .p8 selection.

skills/usage/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ TanStack Intent skills should stay focused and under the validator line limit, s
2424

2525
### Project setup and diagnostics
2626

27-
- `init [apikey] [appId]`: guided first-time setup for Capgo in a Capacitor app. In interactive use, onboarding 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.
27+
- `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.
2828
- `login [apikey]`: store an API key locally.
2929
- `doctor`: inspect installation health and gather troubleshooting details.
3030
- `probe`: test whether the update endpoint would deliver an update.

src/build/onboarding/command.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import { getPlatformDirFromCapacitorConfig } from '../platform-paths.js'
88
import { loadProgress } from './progress.js'
99
import OnboardingApp from './ui/app.js'
1010

11-
export async function onboardingCommand(): Promise<void> {
11+
export async function onboardingBuilderCommand(): Promise<void> {
1212
// Ink requires an interactive terminal — fail fast in CI/pipes
1313
if (!process.stdin.isTTY || !process.stdout.isTTY) {
14-
console.error('Error: `build onboarding` requires an interactive terminal.')
14+
console.error('Error: `build init` requires an interactive terminal.')
1515
console.error('It cannot run in CI, pipes, or non-TTY environments.')
1616
console.error('Use `build credentials save` for non-interactive credential setup.')
1717
process.exit(1)
Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,19 @@
11
// src/build/onboarding/file-picker.ts
22
import { execFile } from 'node:child_process'
3+
import { basename } from 'node:path'
34
import { platform } from 'node:process'
45

5-
/**
6-
* Returns true if we're on macOS and can use the native file picker.
7-
*/
8-
export function canUseFilePicker(): boolean {
9-
return platform === 'darwin'
10-
}
11-
12-
/**
13-
* Open the macOS native file picker dialog filtered to .p8 files.
14-
* Returns the selected file path, or null if the user cancelled.
15-
* Non-blocking — uses async execFile so Ink spinners keep animating.
16-
*/
17-
export function openFilePicker(): Promise<string | null> {
6+
function openMacFilePicker(script: string): Promise<string | null> {
187
if (!canUseFilePicker())
198
return Promise.resolve(null)
209

2110
return new Promise((resolve) => {
2211
execFile(
2312
'osascript',
24-
['-e', 'POSIX path of (choose file of type {"p8"} with prompt "Select your .p8 API key file")'],
13+
['-e', script],
2514
{ encoding: 'utf-8', timeout: 120000 },
2615
(err, stdout) => {
2716
if (err) {
28-
// User cancelled the dialog or osascript failed
2917
resolve(null)
3018
return
3119
}
@@ -35,3 +23,28 @@ export function openFilePicker(): Promise<string | null> {
3523
)
3624
})
3725
}
26+
27+
/**
28+
* Returns true if we're on macOS and can use the native file picker.
29+
*/
30+
export function canUseFilePicker(): boolean {
31+
return platform === 'darwin'
32+
}
33+
34+
/**
35+
* Open the macOS native file picker dialog filtered to .p8 files.
36+
* Returns the selected file path, or null if the user cancelled.
37+
* Non-blocking — uses async execFile so Ink spinners keep animating.
38+
*/
39+
export function openFilePicker(): Promise<string | null> {
40+
return openMacFilePicker('POSIX path of (choose file of type {"p8"} with prompt "Select your .p8 API key file")')
41+
}
42+
43+
export function openPackageJsonPicker(): Promise<string | null> {
44+
return openMacFilePicker('POSIX path of (choose file with prompt "Select your package.json file")')
45+
.then((selectedPath) => {
46+
if (!selectedPath)
47+
return null
48+
return basename(selectedPath).toLowerCase() === 'package.json' ? selectedPath : null
49+
})
50+
}

src/build/onboarding/ui/app.tsx

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,9 @@ const OnboardingApp: FC<AppProps> = ({ appId, initialProgress, iosDir }) => {
4949
const [existingCerts, setExistingCerts] = useState<Array<{ id: string, name: string, serialNumber: string, expirationDate: string }>>([])
5050
const [certToRevoke, setCertToRevoke] = useState<string | null>(null)
5151
const pickerOpenedRef = useRef(false)
52+
const exitRequestedRef = useRef(false)
5253
// overwriteConfirmedRef removed — credential check happens at start now
5354

54-
// Open browser on Ctrl+O (FilteredTextInput ignores ctrl keys, so no conflict)
55-
useInput((input, key) => {
56-
if (key.ctrl && input === 'o' && (step === 'api-key-instructions' || step === 'input-issuer-id')) {
57-
open('https://appstoreconnect.apple.com/access/integrations/api')
58-
}
59-
})
60-
6155
// Collected data — restore p8Path from progress if resuming
6256
const [p8Path, setP8Path] = useState(initialProgress?.p8Path || '')
6357
const [p8Content, _setP8Content] = useState('')
@@ -100,6 +94,27 @@ const OnboardingApp: FC<AppProps> = ({ appId, initialProgress, iosDir }) => {
10094
setLog(prev => [...prev, { text, color }])
10195
}, [])
10296

97+
const exitOnboarding = useCallback((message?: string) => {
98+
if (exitRequestedRef.current)
99+
return
100+
exitRequestedRef.current = true
101+
if (message)
102+
addLog(message, 'yellow')
103+
setTimeout(() => exit(), 50)
104+
}, [addLog, exit])
105+
106+
// Open browser on Ctrl+O (FilteredTextInput ignores ctrl keys, so no conflict)
107+
useInput((input, key) => {
108+
if (key.ctrl && input === 'c') {
109+
process.kill(process.pid, 'SIGINT')
110+
return
111+
}
112+
113+
if (key.ctrl && input === 'o' && (step === 'api-key-instructions' || step === 'input-issuer-id')) {
114+
open('https://appstoreconnect.apple.com/access/integrations/api')
115+
}
116+
})
117+
103118
/** Save partial progress so the user can resume mid-flow */
104119
const savePartialProgress = useCallback(async (updates: { p8Path?: string, keyId?: string, issuerId?: string }) => {
105120
const existing = await loadProgress(appId) || {
@@ -193,10 +208,10 @@ const OnboardingApp: FC<AppProps> = ({ appId, initialProgress, iosDir }) => {
193208
else {
194209
// Second failure — exit
195210
addLog(`✖ ${message}`, 'red')
196-
addLog('Run `capgo build onboarding` to retry from where you left off.', 'yellow')
197-
setTimeout(() => exit(), 100)
211+
addLog('Run `capgo build init` to retry from where you left off.', 'yellow')
212+
setTimeout(() => exitOnboarding(), 100)
198213
}
199-
}, [retryCount, addLog, exit])
214+
}, [retryCount, addLog, exitOnboarding])
200215

201216
// ── Credential save logic ──
202217

@@ -686,7 +701,7 @@ const OnboardingApp: FC<AppProps> = ({ appId, initialProgress, iosDir }) => {
686701
}
687702
else {
688703
addLog('Exiting onboarding.', 'yellow')
689-
setTimeout(() => exit(), 50)
704+
exitOnboarding()
690705
}
691706
}}
692707
/>
@@ -967,7 +982,7 @@ const OnboardingApp: FC<AppProps> = ({ appId, initialProgress, iosDir }) => {
967982
onChange={(value) => {
968983
if (value === '__exit__') {
969984
addLog('Exiting. Revoke a certificate manually, then re-run onboarding.', 'yellow')
970-
setTimeout(() => exit(), 50)
985+
exitOnboarding()
971986
}
972987
else {
973988
setCertToRevoke(value)
@@ -1012,7 +1027,7 @@ const OnboardingApp: FC<AppProps> = ({ appId, initialProgress, iosDir }) => {
10121027
}
10131028
else {
10141029
addLog('Exiting. Delete duplicate profiles manually in the Apple Developer Portal, then re-run onboarding.', 'yellow')
1015-
setTimeout(() => exit(), 50)
1030+
exitOnboarding()
10161031
}
10171032
}}
10181033
/>
@@ -1118,8 +1133,8 @@ const OnboardingApp: FC<AppProps> = ({ appId, initialProgress, iosDir }) => {
11181133
setStep('welcome')
11191134
}
11201135
else {
1121-
setError('Run `capgo build onboarding` to resume.')
1122-
setTimeout(() => exit(new Error('User exited onboarding')), 50)
1136+
setError('Run `capgo build init` to resume.')
1137+
exitOnboarding()
11231138
}
11241139
}}
11251140
/>

src/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { listApp } from './app/list'
1010
import { setApp } from './app/set'
1111
import { setSetting } from './app/setting'
1212
import { clearCredentialsCommand, listCredentialsCommand, migrateCredentialsCommand, saveCredentialsCommand, updateCredentialsCommand } from './build/credentials-command'
13-
import { onboardingCommand } from './build/onboarding/command'
13+
import { onboardingBuilderCommand } from './build/onboarding/command'
1414
import { requestBuildCommand } from './build/request'
1515
import { cleanupBundle } from './bundle/cleanup'
1616
import { checkCompatibility } from './bundle/compatibility'
@@ -731,9 +731,10 @@ const build = program
731731
npx @capgo/cli build credentials save --appId <your-app-id> --platform android`)
732732

733733
build
734-
.command('onboarding')
734+
.command('init')
735+
.alias('onboarding')
735736
.description('Set up iOS build credentials interactively (creates certificates and profiles automatically)')
736-
.action(onboardingCommand)
737+
.action(onboardingBuilderCommand)
737738

738739
build
739740
.command('request [appId]')

0 commit comments

Comments
 (0)