import { z } from 'zod'; import { type APIClient, APIResponseSchema } from '@agentuity/api'; import { ProjectResponseError } from './util.ts'; export const Resources = z.object({ memory: z.string().default('500Mi').describe('The memory requirements'), cpu: z.string().default('500m').describe('The CPU requirements'), disk: z.string().default('500Mi').describe('The disk requirements'), }); export const Mode = z.object({ type: z .enum(['on-demand', 'provisioned']) .default('on-demand') .describe('on-demand or provisioned'), idle: z.string().optional().describe('Idle timeout duration when on-demand (e.g., "10m", "1h")'), }); export const ProjectBuildConfig = z.object({ timeout: z.string().optional().describe('Build execution timeout (e.g. "30m")'), resources: z .object({ memory: z.string().optional().describe('Build sandbox memory (e.g. "4Gi")'), cpu: z.string().optional().describe('Build sandbox CPU (e.g. "2")'), disk: z.string().optional().describe('Build sandbox disk (e.g. "4Gi")'), }) .optional() .describe('Build sandbox resource limits'), }); export const DeploymentConfig = z.object({ resources: Resources.optional().describe('the resource requirements for your deployed project'), mode: Mode.optional().describe('the provisioning mode for the project'), dependencies: z .array(z.string().describe('APT dependencies to install prior to launching your project')) .optional(), domains: z.array(z.string().describe('the custom domain')).optional(), }); const BaseFileFields = { filename: z.string().describe('the relative path for the file'), version: z.string().describe('the SHA256 content of the file'), }; export const BuildEvalSchema = z.object({ ...BaseFileFields, id: z.string().describe('the unique calculated id for the eval'), identifier: z.string().describe('the unique id for eval for the project across deployments'), name: z.string().describe('the name of the eval'), description: z.string().optional().describe('the eval description'), agentIdentifier: z.string().describe('the identifier of the agent'), projectId: z.string().describe('the project id'), }); const BaseAgentFields = { ...BaseFileFields, id: z.string().describe('the unique calculated id for the agent'), agentId: z.string().describe('the unique id for agent for the project across deployments'), projectId: z.string().describe('the project id'), name: z.string().describe('the name of the agent'), description: z.string().optional().describe('the agent description'), evals: z.array(BuildEvalSchema).optional().describe('the evals for the agent'), schema: z .object({ input: z.string().optional().describe('JSON schema for input (stringified JSON)'), output: z.string().optional().describe('JSON schema for output (stringified JSON)'), }) .optional() .describe('input and output JSON schemas for the agent'), }; export const BuildAgentSchema = z.object({ ...BaseAgentFields, }); /** * Launch process definition — describes how to start the application. * Produced by the buildpack pipeline for any framework. */ export const LaunchProcessSchema = z.object({ type: z.string().describe('process type (e.g., web, worker)'), command: z.string().describe('command to execute'), default: z.boolean().describe('whether this is the default process'), workingDirectory: z.string().optional().describe('working directory relative to app root'), }); export const LaunchMetadataSchema = z.object({ processes: z.array(LaunchProcessSchema).describe('application processes'), framework: z.object({ name: z.string().describe('detected framework name'), version: z.string().optional().describe('framework version'), }), runtime: z.object({ name: z.string().describe('runtime binary (node, bun, deno)'), port: z.number().optional().describe('port the app listens on'), }), build: z.object({ date: z.string().describe('build timestamp'), duration: z.number().describe('build duration in ms'), }), }); export type LaunchMetadata = z.infer; export const BuildMetadataSchema = z.object({ routes: z .array( z.object({ id: z.string().describe('the unique calculated id for the route'), filename: z.string().describe('the relative path for the file'), path: z.string().describe('the route path'), method: z.enum(['get', 'post', 'put', 'delete', 'patch']).describe('the HTTP method'), version: z.string().describe('the SHA256 content of the file'), type: z.enum(['api', 'sms', 'email', 'cron', 'websocket', 'sse', 'stream']), agentIds: z .array(z.string()) .optional() .describe('the agent ids associated with this route'), config: z .record(z.string(), z.unknown()) .optional() .describe('type specific configuration'), schema: z .object({ input: z.string().optional().describe('JSON schema for input (stringified JSON)'), output: z .string() .optional() .describe('JSON schema for output (stringified JSON)'), }) .optional() .describe('input and output JSON schemas for the route'), }) ) .default([]) .describe('routes — only present for Agentuity native apps'), agents: z .array(BuildAgentSchema) .default([]) .describe('agents — only present for Agentuity native apps'), assets: z.array( z.object({ filename: z.string().describe('the relative path for the file'), kind: z.string().describe('the type of asset'), contentType: z.string().describe('the content-type for the file'), contentEncoding: z.string().optional().describe('the content-encoding for the file'), size: z.number().describe('the size in bytes for the file'), }) ), project: z.object({ id: z.string().describe('the project id'), name: z.string().describe('the name of the project (from package.json)'), version: z.string().optional().describe('the version of the project (from package.json)'), description: z .string() .optional() .describe('the description of the project (from package.json)'), keywords: z.array(z.string()).optional().describe('the keywords from package.json'), orgId: z.string().describe('the organization id for the project'), }), deployment: z.intersection( DeploymentConfig, z.object({ id: z.string().describe('the deployment id'), date: z.string().describe('the date the deployment was created in UTC format'), git: z .object({ repo: z.string().optional().describe('the repository name'), commit: z.string().optional().describe('the git commit sha'), message: z.string().optional().describe('the git commit message'), branch: z.string().optional().describe('the git branch'), tags: z.array(z.string()).optional().describe('the tags for the current branch'), pr: z.string().optional().describe('the pull request number'), provider: z.string().optional().describe('the CI provider'), trigger: z .string() .default('cli') .optional() .describe('the trigger that caused the build'), url: z .string() .url() .optional() .describe('the url to the commit for the CI provider'), buildUrl: z .string() .url() .optional() .describe('the url to the build for the CI provider'), event: z .enum(['pull_request', 'push', 'manual', 'workflow']) .default('manual') .optional() .describe( 'The type of Git-related event that triggered the deployment: pull_request (A pull request or merge request was opened, updated, or merged), push (A commit was pushed directly to a branch), manual (A deployment was triggered manually via CLI or a button), workflow (A deployment was triggered by an automated workflow, such as a CI pipeline)' ), pull_request: z .object({ number: z.number(), url: z.string().optional(), }) .optional() .describe( 'This is only present when the deployment was triggered via a pull request.' ), }) .optional() .describe('git commit information'), build: z.object({ bun: z.string().describe('the version of bun that was used to build the deployment'), agentuity: z.string().describe('the version of the agentuity runtime'), arch: z.string().describe('the machine architecture'), platform: z.string().describe('the machine os platform'), }), source: z.enum(['github', 'cli', 'managed']).optional(), channel: z.enum(['edge', 'stable', 'commit']).optional(), rollout_org_ids: z.array(z.string()).optional(), }) ), launch: LaunchMetadataSchema.optional().describe( 'launch metadata — how to start the application (processes, framework, runtime)' ), }); export type BuildMetadata = z.infer; export const CreateProjectDeploymentSchema = z.object({ id: z.string().describe('the unique id for the deployment'), orgId: z.string().describe('the organization id'), publicKey: z.string().describe('the public key to use for encrypting the deployment'), buildLogsStreamURL: z .string() .optional() .describe('the URL for streaming build logs (PUT to write, GET to read)'), }); export const CreateProjectDeploymentResponseSchema = APIResponseSchema( CreateProjectDeploymentSchema ); type CreateProjectDeploymentPayload = z.infer; export type Deployment = z.infer; /** * Create a new project deployment * * @param client * @param projectId * @returns */ export async function projectDeploymentCreate( client: APIClient, projectId: string, deploymentConfig?: z.infer ): Promise { const resp = await client.request( 'POST', `/cli/deploy/2/start/${projectId}`, CreateProjectDeploymentResponseSchema, deploymentConfig ?? {} ); if (resp.success) { return resp.data; } throw new ProjectResponseError({ message: resp.message }); } export const DeploymentInstructionsSchema = z.object({ deployment: z.string().describe('the url for uploading the encrypted deployment archive'), assets: z .record( z.string().describe('the asset id'), z.string().describe('the url for the asset upload') ) .describe('the upload metadata for public assets'), }); export const DeploymentInstructionsResponseSchema = APIResponseSchema(DeploymentInstructionsSchema); type DeploymentInstructionsResponse = z.infer; export type DeploymentInstructions = z.infer; /** * Update the deployment with the build metadata * * @param client * @param deploymentId * @returns */ export async function projectDeploymentUpdate( client: APIClient, deploymentId: string, deployment: BuildMetadata, signal?: AbortSignal ): Promise { const resp = await client.request( 'PUT', `/cli/deploy/2/start/${deploymentId}`, DeploymentInstructionsResponseSchema, deployment, BuildMetadataSchema, signal ); if (resp.success) { return resp.data; } throw new ProjectResponseError({ message: resp.message }); } export const DeploymentCompleteSchema = z.object({ streamId: z.string().optional().describe('the stream id for warmup logs'), rolloutId: z .string() .optional() .describe('Genesis managed rollout id when source deploy triggered fan-out'), publicUrls: z .object({ latest: z.string().url().describe('the public url for the latest deployment'), deployment: z.string().url().describe('the public url for this deployment'), custom: z.array(z.string().describe('the custom domain')), vanityDeployment: z .string() .url() .nullable() .optional() .describe('the vanity url for this deployment'), vanityProject: z .string() .url() .nullable() .optional() .describe('the vanity url for the latest deployment'), }) .describe('the map of public urls'), }); export const DeploymentCompleteResponseSchema = APIResponseSchema(DeploymentCompleteSchema); type DeploymentCompleteResponse = z.infer; export type DeploymentComplete = z.infer; export const DeploymentStateValue = z.enum([ 'pending', 'building', 'deploying', 'failed', 'completed', ]); export type DeploymentState = z.infer; export const DeploymentStatusSchema = z.object({ state: DeploymentStateValue.describe('the current deployment state'), }); export const DeploymentStatusResponseSchema = APIResponseSchema(DeploymentStatusSchema); type DeploymentStatusResponse = z.infer; export type DeploymentStatusResult = z.infer; /** * Complete the deployment once build is uploaded * * @param client * @param deploymentId * @returns */ export async function projectDeploymentComplete( client: APIClient, deploymentId: string, signal?: AbortSignal ): Promise { const resp = await client.request( 'POST', `/cli/deploy/2/complete/${deploymentId}`, DeploymentCompleteResponseSchema, undefined, undefined, signal ); if (resp.success) { return resp.data; } throw new ProjectResponseError({ message: resp.message || 'Deployment completion failed with unknown error', }); } /** * Get the current provisioning status of a deployment * * @param client * @param deploymentId * @returns */ export async function projectDeploymentStatus( client: APIClient, deploymentId: string, signal?: AbortSignal ): Promise { const resp = await client.request( 'GET', `/cli/deploy/2/status/${deploymentId}`, DeploymentStatusResponseSchema, undefined, undefined, signal ); if (resp.success) { return resp.data; } throw new ProjectResponseError({ message: resp.message }); } export const ClientDiagnosticsErrorSchema = z.object({ type: z.enum(['file', 'general']), scope: z.enum(['typescript', 'ast', 'build', 'bundler', 'validation', 'deploy']), path: z.string().optional(), line: z.number().optional(), column: z.number().optional(), message: z.string(), code: z.string().optional(), }); export const ClientDiagnosticsTimingSchema = z.object({ name: z.string(), startedAt: z.string(), completedAt: z.string(), durationMs: z.number(), }); export const ClientDiagnosticsSchema = z.object({ success: z.boolean(), errors: z.array(ClientDiagnosticsErrorSchema), warnings: z.array(ClientDiagnosticsErrorSchema), diagnostics: z.array(ClientDiagnosticsTimingSchema), error: z.string().optional(), }); export const DeploymentFailPayloadSchema = z.object({ error: z.string().optional(), diagnostics: ClientDiagnosticsSchema.optional(), }); export type ClientDiagnosticsError = z.infer; export type ClientDiagnosticsTiming = z.infer; export type ClientDiagnostics = z.infer; export type DeploymentFailPayload = z.infer; export const DeploymentFailResponseSchema = z.object({ state: z.literal('failed'), }); export const DeploymentFailAPIResponseSchema = APIResponseSchema(DeploymentFailResponseSchema); type DeploymentFailResponse = z.infer; /** * Report a deployment failure from the client * * @param client * @param deploymentId * @param payload - Error message and/or structured diagnostics * @returns */ export async function projectDeploymentFail( client: APIClient, deploymentId: string, payload: DeploymentFailPayload ): Promise { const resp = await client.request( 'POST', `/cli/deploy/2/fail/${deploymentId}`, DeploymentFailAPIResponseSchema, payload, DeploymentFailPayloadSchema ); if (!resp.success) { throw new ProjectResponseError({ message: resp.message }); } }