UNPKG

@capgo/cli

Version:
284 lines (283 loc) 14.3 kB
import type { BuildCredentials } from '../../../schemas/build.js'; import type { BuildLogger, BuildRequestOptions, BuildRequestResult } from '../../request.js'; import type { AsyncCommandRunner, CiSecretDiscovery, CiSecretEntry, CiSecretSetupAdvice, CiSecretTarget, CommandRunner } from '../ci-secrets.js'; import type { EnvExportOpts, EnvExportResult } from '../env-export.js'; import type { BuildScriptChoice, GeneratedWorkflow, PackageManager, WorkflowGeneratorOpts } from '../workflow-generator.js'; import type { WorkflowWriteOptions, WorkflowWriteResult } from '../workflow-writer.js'; export type TailStep = 'saving-credentials' | 'detecting-ci-secrets' | 'ci-secrets-setup' | 'ci-secrets-target-select' | 'ask-ci-secrets' | 'checking-ci-secrets' | 'confirm-ci-secret-overwrite' | 'uploading-ci-secrets' | 'ci-secrets-failed' | 'ask-github-actions-setup' | 'confirm-secrets-push' | 'ask-export-env' | 'exporting-env' | 'confirm-env-export-overwrite' | 'overwrite-and-export-env' | 'pick-package-manager' | 'pick-build-script' | 'pick-build-script-custom' | 'preview-workflow-file' | 'view-workflow-diff' | 'writing-workflow-file' | 'ask-build' | 'requesting-build' | 'build-complete'; export type TailStepKind = 'auto' | 'input' | 'choice' | 'done' | 'error'; export interface TailStepOption { value: string; label?: string; note?: string; } export interface TailStepView { step: string; kind: TailStepKind; title?: string; prompt?: string; collect?: string[]; options?: TailStepOption[]; message?: string; } /** * Runtime context for the tail views — the OPTIONAL transient data a driver * surfaces from a prior effect. Mirrors the tail subset of `AndroidStepCtx` so * the android engine can pass its ctx straight through. */ export interface TailStepCtx { ciSecretEntries?: CiSecretEntry[]; ciSecretTargets?: CiSecretTarget[]; ciSecretSetupAdvice?: CiSecretSetupAdvice[]; ciSecretRepoLabel?: string | null; detectedPackageManager?: string; availableScripts?: Record<string, string>; recommendedScript?: string | null; defaultEnvExportPath?: string; } export interface TailEffectProgress { appId: string; setupMode?: 'undecided' | 'with-workflow' | 'secrets-only' | 'declined'; ciSecretTarget?: CiSecretTarget | null; selectedPackageManager?: PackageManager | null; buildScriptChoice?: BuildScriptChoice | null; envExportTargetPath?: string; /** * Android-only marker that gates the random-password backup hint at * saving-credentials. DRIVER REQUIREMENT: the driver MUST set this on `progress` * when its keystore step auto-generated the store password (the bespoke android * tail only held this in React `randomPasswordGenerated` state, never persisted); * the engine reads it from `progress` and has no other source. Never set on iOS. */ keystorePasswordGenerated?: boolean; } export interface TailEffectDeps<P extends TailEffectProgress = TailEffectProgress> { /** Tags the saved-cred store, env-export filename and build/workflow platform. */ platform: 'ios' | 'android'; /** * Build the platform credential SHAPE written at `saving-credentials` (e.g. * ANDROID_KEYSTORE_FILE… on android). Throws on missing inputs — same guards * the android engine used inline. */ buildSavedCredentials: (progress: P) => Record<string, string> | Promise<Record<string, string>>; /** * Lossy fallback used when the driver did not thread the saved-credential map * through `carried` (crash-recovery resume). Returns {} when not rebuildable. */ rebuildTailCredentials: (progress: P) => Record<string, string>; /** * The platform's resume resolver — used by the `saving-credentials` self-heal * guard to detect a progress that should resume elsewhere. */ resumeStep: (progress: P) => string; updateSavedCredentials: (appId: string, platform: 'ios' | 'android', credentials: Record<string, string>) => Promise<void>; loadProgress: (appId: string) => Promise<P | null>; /** * Persist progress. NOTE: the POST-SAVE tail never calls this — saving-credentials * deletes progress.json and every later tail step runs purely from transient/ * carried (the bespoke android tail is in-memory-only), so persisting would * re-create the deleted file. Kept on the surface so drivers can still supply it * (and for symmetry with the pre-save engine), but unused by runTailEffect. */ saveProgress: (appId: string, progress: P) => Promise<void>; deleteProgress: (appId: string) => Promise<void>; createCiSecretEntries?: (credentials: Partial<BuildCredentials>, apiKey?: string) => CiSecretEntry[]; detectCiSecretTargets?: (runner?: CommandRunner) => CiSecretDiscovery; getCiSecretRepoLabelAsync?: (target: CiSecretTarget, runner?: AsyncCommandRunner) => Promise<string | null>; listExistingCiSecretKeysAsync?: (target: CiSecretTarget, keys: string[], runner?: AsyncCommandRunner) => Promise<string[]>; uploadCiSecretsAsync?: (target: CiSecretTarget, entries: CiSecretEntry[], existingKeys?: string[], runner?: AsyncCommandRunner, onProgress?: (current: number, total: number, keyName: string) => void) => Promise<void>; exportCredentialsToEnv?: (opts: EnvExportOpts) => EnvExportResult; defaultExportPath?: (appId: string, platform: 'ios' | 'android') => string; generateWorkflow?: (opts: WorkflowGeneratorOpts) => GeneratedWorkflow; writeWorkflowFile?: (opts: WorkflowGeneratorOpts, writeOptions?: WorkflowWriteOptions) => WorkflowWriteResult; requestBuildInternal?: (appId: string, options: BuildRequestOptions, silent?: boolean, logger?: BuildLogger) => Promise<BuildRequestResult>; /** * The streaming BuildLogger the TUI threads into requestBuildInternal (the 4th * arg). On android it streams every line into `setBuildOutput`; the engine just * forwards it. When absent, requestBuildInternal is called without a logger. */ logger?: BuildLogger; /** * The build VIEWER sink — DISTINCT from `onLog` (the side-log). The bespoke * android tail (app.tsx ~L1654-1740) writes the build header / blank+queued / * ⚠ failure / no-key UX / catch lines via `setBuildOutput` (a dedicated build * output pane), NOT via the side-log `addLog`. The shared engine forwards * those build-viewer lines here so the driver can route them to the right * sink. OPTIONAL — absent on iOS (and legacy callers), where the lines are * simply dropped and routing is unaffected. */ onBuildOutput?: (line: string) => void; /** * Resolves the Capgo API key the build request should use, mirroring the * android tail's CLI-flag-over-saved precedence (`apikey ?? findSavedKeySilent()`). * Returns undefined when no key is resolvable — in which case requesting-build * skips the build attempt and finishes at build-complete (the android no-key UX). * When this dep is ABSENT the engine falls back to the legacy empty-string apikey * so existing callers/tests that never resolved a key keep working. */ resolveApikey?: () => string | undefined; /** * Per-key upload progress, forwarded as the 5th arg of uploadCiSecretsAsync. * The android tail feeds this into `setCiSecretUploadProgress`. No-op when absent. */ onCiSecretUploadProgress?: (current: number, total: number, keyName: string) => void; /** * The 2-phase checking-ci-secrets status text ('Resolving GitHub repository…' * then 'Checking existing env vars in <repo>…'). The android tail feeds this into * `setCiSecretCheckPhase`. This is the ONLY sink for the check phases — they are * intentionally NOT surfaced via `onStatus`, which the driver routes to the * oauth/gcp panes (sending the check phases there would corrupt those). No-op * when absent. */ onCiSecretCheckPhase?: (phase: string) => void; /** * The ci-secrets-failed reason (repo-null / catch in checking-ci-secrets). The * android tail feeds this into `setCiSecretError`, which the CiSecretsFailedStep * renders. Also surfaced via `transient.ciSecretError`. OPTIONAL — no-op when * absent (the failed-step view falls back to its generic message). */ onCiSecretError?: (message: string) => void; /** Reads the project's package.json scripts map. */ getPackageScripts?: () => Record<string, string>; /** Detects the web-framework project type (best-effort; may resolve null). */ findProjectType?: (options?: { quiet?: boolean; }) => Promise<string | null>; /** Maps a detected project type to its recommended build script name. */ findBuildCommandForProjectType?: (projectType: string) => Promise<string | null>; /** * Workflow-file telemetry hook (e.g. 'workflow-file-written'). The android tail * calls `trackWorkflowEvent`. No-op when absent. */ trackWorkflowEvent?: (event: string, options?: { decision?: string; }) => void; /** * DRIVER-HELD transient tail state, threaded back into each effect. The TUI * resolves these ONCE (at `saving-credentials`) and keeps them in React state; * a headless driver mirrors that by capturing `TailEffectResult.transient` and * passing it back here on the NEXT effect. NEVER persisted to progress.json. */ carried?: { savedCredentials?: Record<string, string>; ciSecretEntries?: CiSecretEntry[]; ciSecretExistingKeys?: string[]; /** * Whether the workflow file did NOT exist when previewed (app.tsx's * `previewIsNew`, resolved at `preview-workflow-file` via existsSync — driver * state, never persisted). `writing-workflow-file` logs '✔ Wrote' vs * '✔ Overwrote' from it. Absent/undefined defaults to NEW ('Wrote'), matching * the bespoke React `useState(true)` default. */ workflowIsNew?: boolean; }; onStatus?: (message: string) => void; onLog?: (message: string, color?: string) => void; /** Internal-only diagnostic line → the support internal log (main PR #2406). Optional; no-op when absent. */ onInternalLog?: (line: string) => void; signal?: AbortSignal; } export interface TailEffectResult<P extends TailEffectProgress = TailEffectProgress> { /** Updated progress after the effect ran (matches what was persisted). */ progress: P; /** Explicit next step (a platform step id — string so each platform widens it). */ next?: string; /** Transient runtime data that lives in the driver but is NOT persisted. */ transient?: TailTransient; } /** The tail subset of a platform's transient ctx. Every field is optional. */ export interface TailTransient { ciSecretEntries?: CiSecretEntry[]; savedCredentials?: Record<string, string>; ciSecretTargets?: CiSecretTarget[]; ciSecretSetupAdvice?: CiSecretSetupAdvice[]; ciSecretRepoLabel?: string | null; ciSecretExistingKeys?: string[]; ciSecretUploadSummary?: string; envExportPath?: string; workflowFilePath?: string; buildUrl?: string; buildOutput?: string[]; aiJobId?: string; /** Workflow-builder script preload (resolved at uploading-ci-secrets, with-workflow). */ availableScripts?: Record<string, string>; recommendedScript?: string | null; /** Set when env-export found nothing to write or threw — routed to build-complete, never thrown. */ envExportError?: string; /** Set when requesting-build THREW — routed to build-complete, never thrown (app.tsx ~L1733). */ error?: string; /** Set on the ci-secrets-failed routes (repo-null / catch) so the failed-step view can render the reason. */ ciSecretError?: string; } export type TailInput = { step: 'ci-secrets-setup'; value: 'retry' | 'skip'; } | { step: 'ci-secrets-target-select'; ciSecretTarget: CiSecretTarget | null; } | { step: 'ask-ci-secrets'; value: 'yes' | 'no'; } | { step: 'confirm-ci-secret-overwrite'; value: 'replace' | 'skip'; } | { step: 'ci-secrets-failed'; value: 'retry' | 'continue'; } | { step: 'ask-github-actions-setup'; value: 'with-workflow' | 'secrets-only' | 'no'; } | { step: 'confirm-secrets-push'; value: 'confirm' | 'cancel'; } | { step: 'ask-export-env'; value: 'no'; } | { step: 'ask-export-env'; value: 'yes'; envExportTargetPath: string; } | { step: 'confirm-env-export-overwrite'; value: 'replace' | 'skip'; } | { step: 'pick-package-manager'; selectedPackageManager: PackageManager; } | { step: 'pick-build-script'; value: '__custom__'; } | { step: 'pick-build-script'; buildScriptChoice: BuildScriptChoice; } | { step: 'pick-build-script-custom'; command: string; } | { step: 'preview-workflow-file'; value: 'write' | 'view' | 'cancel'; } | { step: 'view-workflow-diff'; value: 'close'; } | { step: 'ask-build'; value: 'yes' | 'no'; }; /** * Pure: a UI-framework-neutral description of a tail step. Mirrors the matching * <Select>/prompt the TUI renders. Moved verbatim from `androidViewForStep`'s * tail cases — the android engine adapts the returned view to `AndroidStepView`. */ export declare function tailViewForStep(step: TailStep, progress: TailEffectProgress | null, ctx: TailStepCtx): TailStepView; /** * Pure state write for each tail choice/input step — returns a NEW progress * object (spread — never mutate). Navigation-only / spinner-gate inputs return * progress unchanged. Moved verbatim from `applyAndroidInput`'s tail reducers. */ export declare function applyTailInput<P extends TailEffectProgress>(step: TailStep, progress: P, input: TailInput): P; /** * Dispatches to the right tail effect handler. Moved verbatim from * `runAndroidEffect`'s tail cases. Platform-specific calls are parameterised via * `deps.platform` / `deps.buildSavedCredentials` / `deps.rebuildTailCredentials` * / `deps.resumeStep`; everything else is unchanged. */ export declare function runTailEffect<P extends TailEffectProgress>(step: TailStep, progress: P, deps: TailEffectDeps<P>): Promise<TailEffectResult<P>>;