/** * The `clustly deploy` wizard — step sequencing for build-plan slices 2.1+. * * Design: docs/designs/clustly-cli.md §2. This module owns the FLOW (auth check → find * workspace → confirm → init → later slices); all terminal interaction goes through the * injected WizardIO so the sequencing is unit-testable with a scripted fake. cli.ts provides * the real readline-backed IO. Slices 2.4–2.6 (secret scan, inventory, packing) and 3.x * (auth, upload) plug into the marked extension points. */ import { type CliFailure } from "./cli-failure"; import { type WorkspaceCandidate } from "./discovery"; import { type DryrunFailureCode } from "./dryrun"; import { type Inventory } from "./inventory"; import { type ClustlyManifest } from "./manifest"; import { type PackResult } from "./pack"; export interface WizardIO { say(message: string): void; /** Yes/no confirmation (Enter = yes). Never called in --ci mode. */ confirm(question: string): Promise; /** Yes/no where Enter = NO — for a step that binds this folder to something already live. */ confirmExplicit(question: string): Promise; /** Pick one of `choices` by index. Never called in --ci mode. */ select(question: string, choices: string[]): Promise; /** Hidden text entry (secret values — never echoed). Never called in --ci mode. */ askHidden(question: string): Promise; } /** Slice 3.4 · ship the bundle (commands/hosting.ts injects the real deployRelease). */ export type UploadStep = (input: { workspacePath: string; manifest: ClustlyManifest; bundle: PackResult; openclawVersion?: string; }) => Promise<{ agentId: string; releaseId: string; status: string; duplicate: boolean; }>; /** Slice 3.5 · the server-side secret store, scoped to this workspace's hosted agent * (the glue ensures the agent exists first — a first deploy registers it here, and the * later upload reuses the remembered id). */ export type SecretsStep = (input: { workspacePath: string; manifest: ClustlyManifest; }) => Promise<{ /** Names already set server-side. */ setNames: string[]; put: (name: string, value: string) => Promise; }>; /** Slice 3.6 · the sandbox-parity dry-run (docker) — absent when docker isn't available. */ export type SandboxStep = (input: { workspacePath: string; manifest: ClustlyManifest; tarPath: string; /** The vetted engine pin — openclaw framework only; node/python dry-runs need none. */ openclawVersion?: string; }) => Promise<{ ok: boolean; missingEnv: string[]; deliverable?: string; log: string; /** Failed branch: whose failure (dryrun.ts DryrunFailureCode) — decides the exit and the trace. */ code?: DryrunFailureCode; }>; export interface DeployOptions { cwd: string; home: string; explicitPath?: string; /** Non-interactive: requires an unambiguous workspace; any prompt becomes a failure. */ ci?: boolean; /** Stop before anything would leave the machine. */ dryRun?: boolean; /** Injectable for tests. */ scanRoots?: string[]; /** The release push — absent in dry-run/init and in wizard unit tests. */ upload?: UploadStep; /** The secret store — wired alongside upload; absent in dry-run/init. */ secrets?: SecretsStep; /** The sandbox dry-run — wired whenever docker is available (real deploys AND --dry-run). */ sandbox?: SandboxStep; /** Deploy preflight (2026-08-28): what already exists for this agent — the listings the * new release will serve (context: agent↔listing is 1:N) and the currently-held release, * if any, so the wizard can warn + confirm before a deploy replaces it in the queue. */ deployPreflight?: (workspacePath: string) => Promise<{ held: { releaseId: string; sinceIso: string | null; url: string; } | null; listings: Array<{ title: string; status: string; }>; }>; /** Injectable `openclaw --version` probe (defaults to the real one). */ detectOpenclaw?: () => string | undefined; /** Release-timeline poll (lifecycle releaseLogs) — wired on real deploys; the wizard * WATCHES the review after upload instead of dropping the seller at "scanning" with * homework (field feedback 2026-08-01). Absent in dry-run/unit tests. */ watchRelease?: (releaseId: string) => Promise<{ status: string; lines: Array<{ at: string | null; text: string; }>; }>; /** Web origin for seller-facing tracking links (resolveBases().webOrigin). */ webOrigin?: string; /** Test override for the watch poll interval. */ watchPollMs?: number; } export type DeployOutcome = { status: "confirmed"; workspace: WorkspaceCandidate; manifest: ClustlyManifest; inventory?: Inventory; bundle?: PackResult; releaseId?: string; } /** Why it stopped, as the catalog failure the glue renders and exits on (error-catalog.ts). */ | { status: "aborted"; failure: CliFailure; releaseId?: string; }; export declare function runDeploy(opts: DeployOptions, io: WizardIO): Promise; /** The builder-safe timeline line that means the human seat has it (release-logs vocab). * EXPORTED as shared policy: the web release page reads the same marker to decide it is * held, and pins itself to this constant so the two surfaces can never disagree about WHEN * a release is waiting on us. Only the marker is shared — each surface keeps its own voice. */ export declare const HUMAN_REVIEW_MARKER = "pending human review"; /** The ticker's label: the pipeline's OWN latest words, not an invented phase name — * pure and exported for the unit proof. */ export declare function tickerLabel(status: string, lines: ReadonlyArray<{ text: string; }>): string; /** The web page for ONE release — pure and exported for the unit proof. * * `/host` is ACCOUNT-scoped: it resolves the account's newest hosted agent and reads that * agent's single latest release, so a seller with two agents — or two deploys in flight — was * handed a link to a release they were not watching (field report 2026-08-02). Every * concurrent deploy needs its own url, and this watch already holds the release id. * * No trailing-slash handling: `resolveBases()` strips them from webOrigin. The id is * percent-encoded because it goes into a path segment. */ export declare function releaseUrl(webOrigin: string, releaseId: string): string; export declare function runInit(opts: DeployOptions, io: WizardIO): Promise;