@capgo/cli
Version:
A CLI to upload to capgo servers
249 lines (248 loc) • 13.6 kB
TypeScript
import type { AndroidOnboardingProgress, AndroidOnboardingStep } from '../android/types.js';
import type { AndroidEffectDeps } from '../android/flow.js';
import type { IosEffectDeps, IosStepCtx, IosStepView } from '../ios/flow.js';
import type { OnboardingProgress, OnboardingStep } from '../types.js';
import type { NextStepResult, Platform } from './contract.js';
import type { BuildOutputRecord } from '../../output-record.js';
import { androidViewForStep } from '../android/flow.js';
import { brokerBegin, brokerClear, brokerPoll } from './broker-session.js';
/** Facts gathered during preflight; the pure deciders branch only on these. */
export interface PreflightFacts {
capacitorProject: boolean;
appId?: string;
platformsDetected: Platform[];
authenticated: boolean;
appRegistered: boolean;
androidProgress: AndroidOnboardingProgress | null;
iosProgress: OnboardingProgress | null;
}
/** User input carried into the flow via next_step. */
interface OnboardingInput {
platform?: string;
serviceAccountJsonPath?: string;
runBuild?: boolean;
checkBuild?: boolean;
keyId?: string;
issuerId?: string;
p8Path?: string;
serviceAccountMethod?: 'generate' | 'existing';
playDeveloperId?: string;
gcpProjectId?: string;
gcpProjectName?: string;
androidPackage?: string;
saMethodChoice?: 'retry' | 'save-anyway' | 'oauth';
/** Set true at google-sign-in to (re)open the browser for a fresh OAuth — recovery when the browser didn't open, was closed, or the sign-in stalled. */
reopenSignIn?: boolean;
/** At google-sign-in: open the broker sign-in link in the user's browser (true) or let them open it (false/omit). */
openSignInBrowser?: boolean;
/** The confirmation code the user reads off the broker success page — releases the token on poll. */
confirmCode?: string;
/** Answer to the resume prompt: 'continue' resumes the saved step, 'restart' wipes this platform's saved progress and begins again. */
resumeChoice?: 'continue' | 'restart';
credentialsExistChoice?: 'backup' | 'cancel';
keystoreMethod?: 'existing' | 'generate';
keystorePath?: string;
keystoreStorePassword?: string;
keystoreAlias?: string;
keystoreKeyPassword?: string;
keystoreNewAlias?: string;
keystorePasswordMethod?: 'random' | 'manual';
keystoreCommonName?: string;
/** Answer to the parked iOS verify-app gate (the TUI Select vocabulary). */
verifyAction?: 'pick' | 'create-new' | 'autofix' | 'continue' | 'recheck' | 'open' | 'reopen' | 'back' | 'cancel';
/** The picked App Store app's bundle id — only with verifyAction 'pick'. */
verifyAppId?: string;
/**
* Answer to the parked iOS cert-limit-prompt (S6b): the Apple resource id of
* the Distribution certificate to revoke, or '__exit__' (the engine's
* OPTION_CERT_LIMIT_EXIT sentinel) to stop.
*/
certToRevoke?: string;
/** Answer to the parked iOS duplicate-profile-prompt (S6b). */
duplicateProfileAction?: 'delete' | 'exit';
/**
* Answer to the parked iOS error recovery screen (S6b): 'retry' re-runs the
* failing step (carried.retryStep), 'restart' wipes progress + session and
* starts over, 'exit' stops, 'email-support' surfaces support instructions
* (MCP-only arm — no host-side opens).
*/
errorAction?: 'retry' | 'restart' | 'exit' | 'email-support';
/** Answer to the CI-secrets choice steps (target-select / ask / overwrite / push-confirm / setup / failed). */
ciSecretAction?: 'github' | 'gitlab' | 'skip' | 'yes' | 'no' | 'replace' | 'retry' | 'continue' | 'confirm' | 'cancel';
/** Answer to ask-github-actions-setup ('no' maps to the persisted setupMode 'declined'). */
githubActionsSetup?: 'with-workflow' | 'secrets-only' | 'no';
/** Answer to ask-export-env (yes/no) and confirm-env-export-overwrite (replace/skip). */
exportEnvAction?: 'yes' | 'no' | 'replace' | 'skip';
/** Custom .env target path — only together with exportEnvAction 'yes'. */
envExportPath?: string;
/** Answer to pick-package-manager. */
packageManager?: 'bun' | 'npm' | 'pnpm' | 'yarn';
/** Answer to pick-build-script: a script name, '__custom__', or '__skip__'. */
buildScript?: string;
/** Answer to pick-build-script-custom: the exact custom build command. */
buildScriptCustom?: string;
/** Answer to preview-workflow-file: write / view (returns the file text, re-asks) / cancel. */
workflowFileAction?: 'write' | 'view' | 'cancel';
/** Answer to the iOS setup-method fork: create fresh credentials via Apple, or import from this Mac's Keychain. */
setupMethod?: 'create-new' | 'import-existing';
/** Answer to import-distribution-mode ('__cancel__' switches to the create-new path). */
importDistribution?: 'app_store' | 'ad_hoc' | '__cancel__';
/** Answer to import-pick-identity: the chosen identity's SHA-1 (an option value), or '__cancel__' for create-new. */
identityChoice?: string;
/** Answer to import-pick-profile: the chosen profile's UUID (an option value), or '__back__' to re-pick the identity. */
profileChoice?: string;
/** Answer to the import-no-match-recovery hub. */
importRecoveryAction?: 'create' | 'provide-profile-path' | 'browser' | 'back';
/** Answer to import-portal-explanation (the manual Apple-portal walkthrough). */
portalAction?: 'use-create' | 'open-anyway' | 'use-file' | 'back';
/** Absolute path to a .mobileprovision file — answers import-provide-profile-path (the MCP's manual-path arm of the TUI's native picker). */
profilePath?: string;
/** Answer to import-export-warning: 'go' exports from the Keychain (the one macOS permission dialog), 'back' re-picks the profile, 'exit' stops. */
exportConfirm?: 'go' | 'back' | 'exit';
}
/** Decide the first/again step for a fresh or resumed session. */
export declare function decideStart(facts: PreflightFacts, progress: OnboardingProgress | null, deps: EngineDeps): Promise<NextStepResult>;
/**
* Map an interactive IosStepView into a NextStepResult — the iOS mirror of
* mapAndroidView. State names reuse the engine step ids; option values mirror
* the TUI Select values. Only NON-SECRET, view-derived data may appear here.
*/
export declare function mapIosView(view: IosStepView, facts: PreflightFacts, ctx?: IosStepCtx): NextStepResult;
export declare function decideIos(facts: PreflightFacts, deps: EngineDeps, opts?: {
verifyAction?: OnboardingInput['verifyAction'];
verifyAppId?: string;
certToRevoke?: string;
duplicateProfileAction?: 'delete' | 'exit';
errorAction?: 'retry' | 'restart' | 'exit' | 'email-support';
/** import-pick-identity answer: an identity SHA-1 or '__cancel__'. */
identityChoice?: string;
/** import-pick-profile answer: a profile UUID or '__back__'. */
profileChoice?: string;
/** import-no-match-recovery answer. */
importRecoveryAction?: 'create' | 'provide-profile-path' | 'browser' | 'back';
/** import-portal-explanation answer. */
portalAction?: 'use-create' | 'open-anyway' | 'use-file' | 'back';
/** import-provide-profile-path answer: the .mobileprovision path. */
profilePath?: string;
/** import-export-warning answer. */
exportConfirm?: 'go' | 'back' | 'exit';
/**
* S9-S11: the explicit tail step a validated tail answer routed to
* (drive() → applyMcpTailAnswer). Honored only while the slim tail
* progress carries credentialsSaved — the same guard as the tail park.
*/
tailNext?: OnboardingStep;
}): Promise<NextStepResult>;
export declare function mapAndroidView(view: ReturnType<typeof androidViewForStep>, facts: PreflightFacts, opts?: {
keystorePath?: string;
keystorePassword?: string;
}): NextStepResult;
export declare function decideAndroid(facts: PreflightFacts, deps: EngineDeps, opts?: {
signInProceed?: boolean;
/** Drop any in-flight Google OAuth session and (re)open the browser for a fresh
* sign-in — recovery for "still waiting" when the browser never opened / was closed. */
reopenSignIn?: boolean;
/** At google-sign-in: open the broker sign-in link in the user's browser (vs. letting them open it). */
openSignInBrowser?: boolean;
/** The confirmation code the user reads off the broker success page — released the token on the next poll. */
confirmCode?: string;
/**
* S9-S11: the explicit tail step a validated tail answer routed to
* (drive() → applyMcpTailAnswer). Honored only while the slim tail
* progress carries credentialsSaved — the same guard as the tail park.
*/
tailNext?: AndroidOnboardingStep;
}): Promise<NextStepResult>;
export declare function decideAdvance(facts: PreflightFacts, progress: OnboardingProgress | null, input: OnboardingInput | undefined, deps: EngineDeps): Promise<NextStepResult>;
export interface EngineDeps {
cwd: string;
hasSavedKey: () => boolean;
getAppId: () => Promise<string | undefined>;
detectPlatforms: () => Promise<Platform[]>;
isAppRegistered: (appId: string) => Promise<boolean>;
loadProgress: (appId: string) => Promise<OnboardingProgress | null>;
registerApp: (appId: string) => Promise<{
ok: true;
} | {
ok: false;
alreadyExists: boolean;
error: string;
}>;
loadAndroidProgress: (appId: string) => Promise<AndroidOnboardingProgress | null>;
readBuildRecord: (path: string) => Promise<BuildOutputRecord | null>;
buildRecordPath: (appId: string, platform: Platform) => string;
/**
* Remove a build record (and its QR png) left behind by an earlier build.
* Called by runBuild BEFORE the hand-off so checkBuild can never read a
* stale record as the new build's result (hostile-review 2026-06-12).
* Optional so legacy fixtures keep working; production wires
* removeBuildOutputRecord.
*/
clearBuildRecord?: (recordPath: string) => Promise<void>;
/**
* The shared iOS flow engine's IO deps (Apple API / CSR / fs / persistence),
* pre-bound by the driver (buildIosEffectDeps in onboarding-tools.ts for
* production; canned fakes in tests). decideIos threads the per-app carried
* session state in on every effect run. Optional so legacy fixtures that
* never enter the iOS path keep working — a missing helper inside surfaces
* as a caught effect error, never a crash.
*/
iosEffectDeps?: IosEffectDeps;
androidEffectDeps: AndroidEffectDeps;
/**
* Optional injectable broker OAuth session for testing. When provided, the engine uses these instead of the
* disk-persisted broker-session.ts functions. Production omits this and relies on the broker session.
*/
oauthSession?: {
begin: typeof brokerBegin;
poll: typeof brokerPoll;
clear: typeof brokerClear;
};
/**
* Write the generated/loaded Android keystore (.p12) to a file on disk so the
* user has a durable copy after onboarding. Called once when the keystore phase
* completes. Returns the absolute path of the written file.
*
* Optional — when omitted the keystore is kept in progress only (no file written).
* Omitting does not break the flow; the keystore data is always in _keystoreBase64.
*/
writeKeystoreFile?: (appId: string, base64: string, alias: string) => Promise<string>;
}
export declare function gatherFacts(deps: EngineDeps): Promise<PreflightFacts>;
export declare function runStart(deps: EngineDeps, platform?: Platform): Promise<NextStepResult>;
export declare function runAdvance(deps: EngineDeps, input?: OnboardingInput): Promise<NextStepResult>;
/**
* Read-only: determine the onboarding state the user is currently on, WITHOUT
* running any side effect. Mirrors the branch selection of decideStart/
* decideAndroid/decideIos (preflight → platform → resume step, with the same
* ask-build → build-ready / .p8-chain → ios-api-key name mapping) but never
* calls effects.
*/
export declare function resolveCurrentState(facts: PreflightFacts): string;
/** State names the MCP constructs directly that are NOT engine step ids. */
export declare const MCP_ONLY_STATES: readonly string[];
/**
* Engine step ids (present in the type tables) the MCP can NEVER emit as a
* state name:
* - TUI bootstrap / TUI-only screens: welcome, adding-platform,
* the AI build-debug sub-flow (decideIos reroutes its entry to
* 'build-failed'), the contact-support sub-flow (the MCP's error screen has
* the email-support arm instead), the native file pickers (the MCP collects
* paths as text), the google-sign-in-running spinner (the MCP parks on
* 'google-sign-in' via its OAuth session), and view-workflow-diff (the MCP
* folds the diff into preview-workflow-file's context — 'view' re-parks);
* - the .p8 input chain, collapsed into the single 'ios-api-key' gate;
* - 'ask-build', mapped to the shared 'build-ready' choice (decideBuildPhase);
* - 'requesting-build', never run over MCP (the C2/D2 handoff + checkBuild
* polling replace it).
*/
export declare const MCP_UNREACHABLE_STEPS: ReadonlySet<string>;
/**
* Read-only "explain the current step" entry point backing the
* capgo_builder_onboarding_explain tool. Gathers facts (read-only) and returns a
* plain-language explanation string. Never advances the flow or runs effects.
*/
export declare function explainOnboarding(deps: EngineDeps, input?: {
state?: string;
}): Promise<string>;
export {};