@capgo/cli
Version:
A CLI to upload to capgo servers
284 lines (283 loc) • 14.3 kB
TypeScript
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>>;