import { intro, log } from "@clack/prompts" import { blue } from "ansis" import { setTimeout } from "node:timers/promises" import open from "open" import { apiClient } from "./api-client" import { env } from "./env" import { dimmed } from "./io" import { CLI_PREFIX, output, requireSession, sessionIntro } from "../utils" const DEPLOYMENT_POLL_INTERVAL_MS = 2_000 export type DeploymentState = Awaited< ReturnType > /** Prints the user session or organization API-key identity for a command. */ export async function deploymentSessionIntro() { if (env.AUTOMATE_AX_API_KEY) { output.normal(() => intro(`${CLI_PREFIX} ${dimmed("Organization API key")}`), ) return } const session = await requireSession() output.normal(() => sessionIntro(session)) } /** Error returned when a human must finish parameters or accounts. */ export class DeploymentSetupRequiredError extends Error { readonly code = "DEPLOYMENT_SETUP_REQUIRED" readonly data: { authorizationUrl: string deploymentId: string resumeCommand: string unresolvedParameters: { automationId: string name: string }[] unsatisfiedRequirements: { binding: string serviceId: string }[] } readonly status = 409 /** * Creates an actionable error for a deployment awaiting authorization. * * @param deployment - Awaiting deployment and focused browser handoff. */ constructor(deployment: DeploymentState) { const authorization = deployment.authorization! const resumeCommand = `automate deployment authorize ${deployment.id}` super( `Project setup is required for deployment ${deployment.id}. Open Parameters & Accounts, then resume with \`${resumeCommand}\`.`, ) this.name = "DeploymentSetupRequiredError" this.data = { authorizationUrl: authorization.url, deploymentId: deployment.id, resumeCommand, unresolvedParameters: deployment.automations.flatMap( ({ id: automationId, parameterIssues }) => (parameterIssues ?? []).map((name) => ({ automationId, name })), ), unsatisfiedRequirements: authorization.requirements.flatMap( ({ binding, satisfied, serviceId }) => satisfied ? [] : [{ binding, serviceId }], ), } } } /** * Polls until deployment completion or optionally required authorization. * * @param deploymentId - Deployment to observe. * @param options - Polling stop behavior. * @param options.returnForAuthorization - Return when human authorization is * required instead of waiting through it. */ export async function pollDeployment( deploymentId: string, options: { returnForAuthorization: boolean }, ) { while (true) { const deployment = await apiClient.deployment.get({ deploymentId }) if (deployment.status === "succeeded") return deployment if ( deployment.status === "awaiting_authorization" && options.returnForAuthorization ) { return deployment } if (deployment.status === "failed") { throw new Error( deployment.failure?.message ?? `Deployment ${deployment.id} failed.`, ) } if (deployment.status === "cancelled") { throw new Error(`Deployment ${deployment.id} was cancelled.`) } await setTimeout(DEPLOYMENT_POLL_INTERVAL_MS) } } /** * Opens or prints the focused Parameters & Accounts handoff. * * @param deployment - Deployment awaiting authorization. * @param options - Browser handoff behavior. * @param options.openBrowser - Open the URL instead of printing it only. */ export async function openDeploymentAuthorization( deployment: DeploymentState, options: { openBrowser: boolean }, ) { const authorization = deployment.authorization if (!authorization) { throw new Error("Project setup is not awaiting parameters or accounts.") } output.normal(() => log.info( `Finish project setup in your browser:\n${blue.underline.link(authorization.url, "Open Parameters & Accounts")}`, ), ) if (options.openBrowser) { await open(authorization.url, { wait: false }).catch(() => null) } }