From 7d09e754517e28270d88e7370fe8d52bb539a60c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Ga=C5=84czarczyk?= Date: Wed, 4 Mar 2026 12:54:27 +0100 Subject: [PATCH 01/23] feat: add sandbox apps array API support --- src/api/openapi/types.gen.ts | 192 +++++++++++++++++++++-------------- src/api/openapi/zod.gen.ts | 102 ++++++++++++++----- src/core/buddy-api-client.ts | 51 ++++++++++ src/index.ts | 2 +- 4 files changed, 244 insertions(+), 103 deletions(-) diff --git a/src/api/openapi/types.gen.ts b/src/api/openapi/types.gen.ts index 58a0f7c..4a8cdee 100644 --- a/src/api/openapi/types.gen.ts +++ b/src/api/openapi/types.gen.ts @@ -1643,21 +1643,17 @@ export type UpdateSandboxRequest = { | "12x24" | "CUSTOM"; /** - * The commands to run during setup of the sandbox + * The commands to run on first boot of the sandbox */ - install_commands?: string; - /** - * The run command of the sandbox - */ - run_command?: string; + first_boot_commands?: string; /** * The application directory of the sandbox */ app_dir?: string; /** - * The application type of the sandbox (passed command or existent service eg. apache2) + * The list of app commands for the sandbox */ - app_type?: "CMD" | "SERVICE"; + apps?: Array; /** * The list of tags associated with the sandbox */ @@ -1951,6 +1947,12 @@ export type SandboxCommandsView = { commands?: Array; }; +export type SandboxAppView = { + id?: string; + command?: string; + app_status?: "NONE" | "RUNNING" | "ENDED" | "FAILED"; +}; + export type SandboxCommandView = { /** * API endpoint to GET this object @@ -1984,6 +1986,14 @@ export type SandboxCommandView = { * API endpoint URL to retrieve logs for this command */ logs_url?: string; + /** + * Command start date + */ + start_date?: string; + /** + * Command finish date + */ + finish_date?: string; }; export type SandboxCommandLog = { @@ -2158,21 +2168,17 @@ export type CreateNewSandboxRequest = { | "12x24" | "CUSTOM"; /** - * The commands to run during setup of the sandbox - */ - install_commands?: string; - /** - * The run command of the sandbox + * The commands to run on first boot of the sandbox */ - run_command?: string; + first_boot_commands?: string; /** * The application directory of the sandbox */ app_dir?: string; /** - * The application type of the sandbox (passed command or existent service eg. apache2) + * The list of app commands for the sandbox */ - app_type?: "CMD" | "SERVICE"; + apps?: Array; /** * The list of tags associated with the sandbox */ @@ -2222,21 +2228,17 @@ export type CreateFromSnapshotRequest = { | "12x24" | "CUSTOM"; /** - * The commands to run during setup of the sandbox - */ - install_commands?: string; - /** - * The run command of the sandbox + * The commands to run on first boot of the sandbox */ - run_command?: string; + first_boot_commands?: string; /** * The application directory of the sandbox */ app_dir?: string; /** - * The application type of the sandbox (passed command or existent service eg. apache2) + * The list of app commands for the sandbox */ - app_type?: "CMD" | "SERVICE"; + apps?: Array; /** * The list of tags associated with the sandbox */ @@ -2412,29 +2414,21 @@ export type SandboxResponse = { | "12x24" | "CUSTOM"; /** - * The commands to run during setup of the sandbox + * The commands to run on first boot of the sandbox */ - install_commands?: string; - /** - * The run command of the sandbox - */ - run_command?: string; + first_boot_commands?: string; /** * The application directory of the sandbox */ app_dir?: string; - /** - * The application type of the sandbox (passed command or existent service eg. apache2) - */ - app_type?: "CMD" | "SERVICE"; /** * The list of tags associated with the sandbox */ tags?: Array; /** - * The current application status of the sandbox + * The sandbox applications */ - app_status?: "NONE" | "RUNNING" | "ENDED" | "FAILED"; + apps?: Array; /** * The boot logs of the sandbox */ @@ -2442,7 +2436,7 @@ export type SandboxResponse = { /** * The current setup status of the sandbox */ - setup_status?: "INPROGRESS" | "SUCCESS" | "FAILED"; + setup_status?: "INPROGRESS" | "SUCCESS" | "FAILED" | "STALE"; /** * The tunnel endpoints of the sandbox */ @@ -3272,21 +3266,17 @@ export type UpdateSandboxRequestWritable = { | "12x24" | "CUSTOM"; /** - * The commands to run during setup of the sandbox - */ - install_commands?: string; - /** - * The run command of the sandbox + * The commands to run on first boot of the sandbox */ - run_command?: string; + first_boot_commands?: string; /** * The application directory of the sandbox */ app_dir?: string; /** - * The application type of the sandbox (passed command or existent service eg. apache2) + * The list of app commands for the sandbox */ - app_type?: "CMD" | "SERVICE"; + apps?: Array; /** * The list of tags associated with the sandbox */ @@ -3536,6 +3526,12 @@ export type SandboxCommandsViewWritable = { commands?: Array; }; +export type SandboxAppViewWritable = { + id?: string; + command?: string; + app_status?: "NONE" | "RUNNING" | "ENDED" | "FAILED"; +}; + export type SandboxCommandViewWritable = { /** * The ID of the command @@ -3561,6 +3557,14 @@ export type SandboxCommandViewWritable = { * API endpoint URL to retrieve logs for this command */ logs_url?: string; + /** + * Command start date + */ + start_date?: string; + /** + * Command finish date + */ + finish_date?: string; }; export type SandboxAppLogsViewWritable = { @@ -3666,21 +3670,17 @@ export type CreateNewSandboxRequestWritable = { | "12x24" | "CUSTOM"; /** - * The commands to run during setup of the sandbox + * The commands to run on first boot of the sandbox */ - install_commands?: string; - /** - * The run command of the sandbox - */ - run_command?: string; + first_boot_commands?: string; /** * The application directory of the sandbox */ app_dir?: string; /** - * The application type of the sandbox (passed command or existent service eg. apache2) + * The list of app commands for the sandbox */ - app_type?: "CMD" | "SERVICE"; + apps?: Array; /** * The list of tags associated with the sandbox */ @@ -3730,21 +3730,17 @@ export type CreateFromSnapshotRequestWritable = { | "12x24" | "CUSTOM"; /** - * The commands to run during setup of the sandbox + * The commands to run on first boot of the sandbox */ - install_commands?: string; - /** - * The run command of the sandbox - */ - run_command?: string; + first_boot_commands?: string; /** * The application directory of the sandbox */ app_dir?: string; /** - * The application type of the sandbox (passed command or existent service eg. apache2) + * The list of app commands for the sandbox */ - app_type?: "CMD" | "SERVICE"; + apps?: Array; /** * The list of tags associated with the sandbox */ @@ -3804,29 +3800,21 @@ export type SandboxResponseWritable = { | "12x24" | "CUSTOM"; /** - * The commands to run during setup of the sandbox - */ - install_commands?: string; - /** - * The run command of the sandbox + * The commands to run on first boot of the sandbox */ - run_command?: string; + first_boot_commands?: string; /** * The application directory of the sandbox */ app_dir?: string; - /** - * The application type of the sandbox (passed command or existent service eg. apache2) - */ - app_type?: "CMD" | "SERVICE"; /** * The list of tags associated with the sandbox */ tags?: Array; /** - * The current application status of the sandbox + * The sandbox applications */ - app_status?: "NONE" | "RUNNING" | "ENDED" | "FAILED"; + apps?: Array; /** * The boot logs of the sandbox */ @@ -3834,7 +3822,7 @@ export type SandboxResponseWritable = { /** * The current setup status of the sandbox */ - setup_status?: "INPROGRESS" | "SUCCESS" | "FAILED"; + setup_status?: "INPROGRESS" | "SUCCESS" | "FAILED" | "STALE"; /** * The tunnel endpoints of the sandbox */ @@ -4400,6 +4388,62 @@ export type GetSandboxAppLogsResponses = { export type GetSandboxAppLogsResponse = GetSandboxAppLogsResponses[keyof GetSandboxAppLogsResponses]; +export type StartSandboxAppData = { + body?: never; + path: { + workspace_domain: string; + sandbox_id: string; + app_id: string; + }; + query?: never; + url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/apps/{app_id}/start"; +}; + +export type StartSandboxAppResponses = { + 200: void; +}; + +export type StartSandboxAppResponse = + StartSandboxAppResponses[keyof StartSandboxAppResponses]; + +export type StopSandboxAppData = { + body?: never; + path: { + workspace_domain: string; + sandbox_id: string; + app_id: string; + }; + query?: never; + url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/apps/{app_id}/stop"; +}; + +export type StopSandboxAppResponses = { + 200: void; +}; + +export type StopSandboxAppResponse = + StopSandboxAppResponses[keyof StopSandboxAppResponses]; + +export type GetSandboxAppLogsByAppData = { + body?: never; + path: { + workspace_domain: string; + sandbox_id: string; + app_id: string; + }; + query?: { + cursor?: string; + }; + url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/apps/{app_id}/logs"; +}; + +export type GetSandboxAppLogsByAppResponses = { + 200: SandboxAppLogsView; +}; + +export type GetSandboxAppLogsByAppResponse = + GetSandboxAppLogsByAppResponses[keyof GetSandboxAppLogsByAppResponses]; + export type GetSandboxCommandsData = { body?: never; path: { diff --git a/src/api/openapi/zod.gen.ts b/src/api/openapi/zod.gen.ts index e718010..13eebed 100644 --- a/src/api/openapi/zod.gen.ts +++ b/src/api/openapi/zod.gen.ts @@ -1057,10 +1057,9 @@ export const zUpdateSandboxRequest = z.object({ "CUSTOM", ]), ), - install_commands: z.optional(z.string()), - run_command: z.optional(z.string()), + first_boot_commands: z.optional(z.string()), app_dir: z.optional(z.string()), - app_type: z.optional(z.enum(["CMD", "SERVICE"])), + apps: z.optional(z.array(z.string())), tags: z.optional(z.array(z.string())), endpoints: z.optional(z.array(zTunnelView)), variables: z.optional(z.array(zAddVariableInObjectRequest)), @@ -1103,6 +1102,12 @@ export const zSandboxContentView = z.object({ contents: z.optional(z.array(zSandboxContentItem)), }); +export const zSandboxAppView = z.object({ + id: z.optional(z.string()), + command: z.optional(z.string()), + app_status: z.optional(z.enum(["NONE", "RUNNING", "ENDED", "FAILED"])), +}); + export const zSandboxCommandView = z.object({ url: z.optional(z.string().readonly()), html_url: z.optional(z.string().readonly()), @@ -1121,6 +1126,8 @@ export const zSandboxCommandView = z.object({ }), ), logs_url: z.optional(z.string()), + start_date: z.optional(z.string()), + finish_date: z.optional(z.string()), }); export const zSandboxCommandsView = z.object({ @@ -1222,10 +1229,9 @@ export const zCreateNewSandboxRequest = z.object({ "CUSTOM", ]), ), - install_commands: z.optional(z.string()), - run_command: z.optional(z.string()), + first_boot_commands: z.optional(z.string()), app_dir: z.optional(z.string()), - app_type: z.optional(z.enum(["CMD", "SERVICE"])), + apps: z.optional(z.array(z.string())), tags: z.optional(z.array(z.string())), endpoints: z.optional(z.array(zTunnelView)), variables: z.optional(z.array(zAddVariableInObjectRequest)), @@ -1298,10 +1304,9 @@ export const zCreateFromSnapshotRequest = z.object({ "CUSTOM", ]), ), - install_commands: z.optional(z.string()), - run_command: z.optional(z.string()), + first_boot_commands: z.optional(z.string()), app_dir: z.optional(z.string()), - app_type: z.optional(z.enum(["CMD", "SERVICE"])), + apps: z.optional(z.array(z.string())), tags: z.optional(z.array(z.string())), endpoints: z.optional(z.array(zTunnelView)), variables: z.optional(z.array(zEnvironmentVariableView)), @@ -1347,14 +1352,12 @@ export const zSandboxResponse = z.object({ "CUSTOM", ]), ), - install_commands: z.optional(z.string()), - run_command: z.optional(z.string()), + first_boot_commands: z.optional(z.string()), app_dir: z.optional(z.string()), - app_type: z.optional(z.enum(["CMD", "SERVICE"])), tags: z.optional(z.array(z.string())), - app_status: z.optional(z.enum(["NONE", "RUNNING", "ENDED", "FAILED"])), + apps: z.optional(z.array(zSandboxAppView)), boot_logs: z.optional(z.array(z.string())), - setup_status: z.optional(z.enum(["INPROGRESS", "SUCCESS", "FAILED"])), + setup_status: z.optional(z.enum(["INPROGRESS", "SUCCESS", "FAILED", "STALE"])), endpoints: z.optional(z.array(zTunnelView)), project: z.optional(zProjectView), permissions: z.optional(zPermissionsView), @@ -1973,10 +1976,9 @@ export const zUpdateSandboxRequestWritable = z.object({ "CUSTOM", ]), ), - install_commands: z.optional(z.string()), - run_command: z.optional(z.string()), + first_boot_commands: z.optional(z.string()), app_dir: z.optional(z.string()), - app_type: z.optional(z.enum(["CMD", "SERVICE"])), + apps: z.optional(z.array(z.string())), tags: z.optional(z.array(z.string())), endpoints: z.optional(z.array(zTunnelViewWritable)), variables: z.optional(z.array(zAddVariableInObjectRequestWritable)), @@ -2013,6 +2015,12 @@ export const zSandboxContentViewWritable = z.object({ contents: z.optional(z.array(zSandboxContentItemWritable)), }); +export const zSandboxAppViewWritable = z.object({ + id: z.optional(z.string()), + command: z.optional(z.string()), + app_status: z.optional(z.enum(["NONE", "RUNNING", "ENDED", "FAILED"])), +}); + export const zSandboxCommandViewWritable = z.object({ id: z.optional(z.string()), command: z.optional(z.string()), @@ -2029,6 +2037,8 @@ export const zSandboxCommandViewWritable = z.object({ }), ), logs_url: z.optional(z.string()), + start_date: z.optional(z.string()), + finish_date: z.optional(z.string()), }); export const zSandboxCommandsViewWritable = z.object({ @@ -2104,10 +2114,9 @@ export const zCreateNewSandboxRequestWritable = z.object({ "CUSTOM", ]), ), - install_commands: z.optional(z.string()), - run_command: z.optional(z.string()), + first_boot_commands: z.optional(z.string()), app_dir: z.optional(z.string()), - app_type: z.optional(z.enum(["CMD", "SERVICE"])), + apps: z.optional(z.array(z.string())), tags: z.optional(z.array(z.string())), endpoints: z.optional(z.array(zTunnelViewWritable)), variables: z.optional(z.array(zAddVariableInObjectRequestWritable)), @@ -2135,10 +2144,9 @@ export const zCreateFromSnapshotRequestWritable = z.object({ "CUSTOM", ]), ), - install_commands: z.optional(z.string()), - run_command: z.optional(z.string()), + first_boot_commands: z.optional(z.string()), app_dir: z.optional(z.string()), - app_type: z.optional(z.enum(["CMD", "SERVICE"])), + apps: z.optional(z.array(z.string())), tags: z.optional(z.array(z.string())), endpoints: z.optional(z.array(zTunnelViewWritable)), variables: z.optional(z.array(zEnvironmentVariableView)), @@ -2176,14 +2184,12 @@ export const zSandboxResponseWritable = z.object({ "CUSTOM", ]), ), - install_commands: z.optional(z.string()), - run_command: z.optional(z.string()), + first_boot_commands: z.optional(z.string()), app_dir: z.optional(z.string()), - app_type: z.optional(z.enum(["CMD", "SERVICE"])), tags: z.optional(z.array(z.string())), - app_status: z.optional(z.enum(["NONE", "RUNNING", "ENDED", "FAILED"])), + apps: z.optional(z.array(zSandboxAppViewWritable)), boot_logs: z.optional(z.array(z.string())), - setup_status: z.optional(z.enum(["INPROGRESS", "SUCCESS", "FAILED"])), + setup_status: z.optional(z.enum(["INPROGRESS", "SUCCESS", "FAILED", "STALE"])), endpoints: z.optional(z.array(zTunnelViewWritable)), project: z.optional(zProjectViewWritable), permissions: z.optional(zPermissionsView), @@ -2531,6 +2537,46 @@ export const zGetSandboxAppLogsData = z.object({ export const zGetSandboxAppLogsResponse = zSandboxAppLogsView; +export const zStartSandboxAppData = z.object({ + body: z.optional(z.never()), + path: z.object({ + workspace_domain: z.string(), + sandbox_id: z.string(), + app_id: z.string(), + }), + query: z.optional(z.never()), +}); + +export const zStartSandboxAppResponse = z.void(); + +export const zStopSandboxAppData = z.object({ + body: z.optional(z.never()), + path: z.object({ + workspace_domain: z.string(), + sandbox_id: z.string(), + app_id: z.string(), + }), + query: z.optional(z.never()), +}); + +export const zStopSandboxAppResponse = z.void(); + +export const zGetSandboxAppLogsByAppData = z.object({ + body: z.optional(z.never()), + path: z.object({ + workspace_domain: z.string(), + sandbox_id: z.string(), + app_id: z.string(), + }), + query: z.optional( + z.object({ + cursor: z.optional(z.string()), + }), + ), +}); + +export const zGetSandboxAppLogsByAppResponse = zSandboxAppLogsView; + export const zGetSandboxCommandsData = z.object({ body: z.optional(z.never()), path: z.object({ diff --git a/src/core/buddy-api-client.ts b/src/core/buddy-api-client.ts index 2cb2843..607edca 100644 --- a/src/core/buddy-api-client.ts +++ b/src/core/buddy-api-client.ts @@ -23,6 +23,8 @@ import type { ExecuteSandboxCommandResponse, GetIdentifiersData, GetIdentifiersResponse, + GetSandboxAppLogsByAppData, + GetSandboxAppLogsByAppResponse, GetSandboxCommandData, GetSandboxCommandLogsData, GetSandboxCommandResponse, @@ -35,8 +37,12 @@ import type { RestartSandboxData, RestartSandboxResponse, SandboxCommandLog, + StartSandboxAppData, + StartSandboxAppResponse, StartSandboxData, StartSandboxResponse, + StopSandboxAppData, + StopSandboxAppResponse, StopSandboxData, StopSandboxResponse, TerminateSandboxCommandData, @@ -58,6 +64,8 @@ import { zExecuteSandboxCommandResponse, zGetIdentifiersData, zGetIdentifiersResponse, + zGetSandboxAppLogsByAppData, + zGetSandboxAppLogsByAppResponse, zGetSandboxCommandData, zGetSandboxCommandLogsData, zGetSandboxCommandResponse, @@ -70,8 +78,12 @@ import { zRestartSandboxData, zRestartSandboxResponse, zSandboxCommandLog, + zStartSandboxAppData, + zStartSandboxAppResponse, zStartSandboxData, zStartSandboxResponse, + zStopSandboxAppData, + zStopSandboxAppResponse, zStopSandboxData, zStopSandboxResponse, zTerminateSandboxCommandData, @@ -396,6 +408,45 @@ export class BuddyApiClient extends HttpClient { }); } + /** Start a sandbox app */ + async startSandboxApp( + data: ClientData, + ) { + return this.#requestWithValidation({ + method: "POST", + data, + url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/apps/{app_id}/start", + dataSchema: zStartSandboxAppData, + responseSchema: zStartSandboxAppResponse, + }); + } + + /** Stop a sandbox app */ + async stopSandboxApp( + data: ClientData, + ) { + return this.#requestWithValidation({ + method: "POST", + data, + url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/apps/{app_id}/stop", + dataSchema: zStopSandboxAppData, + responseSchema: zStopSandboxAppResponse, + }); + } + + /** Get logs for a specific sandbox app */ + async getSandboxAppLogs( + data: ClientData, + ) { + return this.#requestWithValidation({ + method: "GET", + data, + url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/apps/{app_id}/logs", + dataSchema: zGetSandboxAppLogsByAppData, + responseSchema: zGetSandboxAppLogsByAppResponse, + }); + } + /** Get sandbox content (list files/directories at a path) */ async getSandboxContent( data: ClientData, diff --git a/src/index.ts b/src/index.ts index 0a3fca2..80d91c7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -export type { SandboxIdView } from "@/api/openapi/types.gen"; +export type { SandboxAppView, SandboxIdView } from "@/api/openapi/types.gen"; export { BuddyApiClient } from "@/core/buddy-api-client"; export { Command } from "@/entity/command"; export type { FileInfo, GetFileSystemConfig } from "@/entity/filesystem"; From cb7e307ad1757abc63393e46c0ea2341e08d1140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Ga=C5=84czarczyk?= Date: Wed, 4 Mar 2026 14:22:09 +0100 Subject: [PATCH 02/23] feat: regenerate types from openapi spec and wire up app endpoints --- src/api/openapi/transformers.gen.ts | 67 ++++++++ src/api/openapi/types.gen.ts | 239 ++++++++++++++++------------ src/api/openapi/zod.gen.ts | 147 +++++++++++------ src/core/buddy-api-client.ts | 41 +++-- 4 files changed, 333 insertions(+), 161 deletions(-) diff --git a/src/api/openapi/transformers.gen.ts b/src/api/openapi/transformers.gen.ts index 540b788..edc8b77 100644 --- a/src/api/openapi/transformers.gen.ts +++ b/src/api/openapi/transformers.gen.ts @@ -6,9 +6,12 @@ import type { AddSandboxResponse, AddSandboxSnapshotResponse, CreateSandboxDirectoryResponse, + ExecuteSandboxCommandResponse, GetIntegrationResponse, GetIntegrationsResponse, GetProjectSnapshotsResponse, + GetSandboxCommandResponse, + GetSandboxCommandsResponse, GetSandboxContentResponse, GetSandboxResponse, GetSandboxSnapshotResponse, @@ -16,8 +19,11 @@ import type { GetWorkspaceMemberProjectsResponse, GetWorkspaceResponse, RestartSandboxResponse, + StartSandboxAppResponse, StartSandboxResponse, + StopSandboxAppResponse, StopSandboxResponse, + TerminateSandboxCommandResponse, UpdateIntegrationResponse, UpdateSandboxByYamlResponse, UpdateSandboxResponse, @@ -164,6 +170,67 @@ export const updateSandboxResponseTransformer = async ( return data; }; +export const startSandboxAppResponseTransformer = async ( + data: any, +): Promise => { + data = sandboxResponseSchemaResponseTransformer(data); + return data; +}; + +export const stopSandboxAppResponseTransformer = async ( + data: any, +): Promise => { + data = sandboxResponseSchemaResponseTransformer(data); + return data; +}; + +const sandboxCommandViewSchemaResponseTransformer = (data: any) => { + if (data.start_date) { + data.start_date = new Date(data.start_date); + } + if (data.finish_date) { + data.finish_date = new Date(data.finish_date); + } + return data; +}; + +const sandboxCommandsViewSchemaResponseTransformer = (data: any) => { + if (data.commands) { + data.commands = data.commands.map((item: any) => + sandboxCommandViewSchemaResponseTransformer(item), + ); + } + return data; +}; + +export const getSandboxCommandsResponseTransformer = async ( + data: any, +): Promise => { + data = sandboxCommandsViewSchemaResponseTransformer(data); + return data; +}; + +export const executeSandboxCommandResponseTransformer = async ( + data: any, +): Promise => { + data = sandboxCommandViewSchemaResponseTransformer(data); + return data; +}; + +export const getSandboxCommandResponseTransformer = async ( + data: any, +): Promise => { + data = sandboxCommandViewSchemaResponseTransformer(data); + return data; +}; + +export const terminateSandboxCommandResponseTransformer = async ( + data: any, +): Promise => { + data = sandboxCommandViewSchemaResponseTransformer(data); + return data; +}; + const sandboxContentItemSchemaResponseTransformer = (data: any) => { if (data.size) { data.size = BigInt(data.size.toString()); diff --git a/src/api/openapi/types.gen.ts b/src/api/openapi/types.gen.ts index 4a8cdee..147f2d5 100644 --- a/src/api/openapi/types.gen.ts +++ b/src/api/openapi/types.gen.ts @@ -932,7 +932,8 @@ export type IntegrationView = { | "BACKBLAZE" | "ONE_LOGIN" | "OKTA" - | "CONTENTFUL"; + | "CONTENTFUL" + | "JIRA"; /** * The authentication method used by the integration */ @@ -1261,7 +1262,8 @@ export type AddIntegrationRequest = { | "BACKBLAZE" | "ONE_LOGIN" | "OKTA" - | "CONTENTFUL"; + | "CONTENTFUL" + | "JIRA"; /** * The scope of the integration */ @@ -1306,6 +1308,10 @@ export type SandboxIdView = { | "RUNNING" | "STOPPED" | "RESTORING"; + /** + * The current setup status of the sandbox + */ + setup_status?: "INPROGRESS" | "SUCCESS" | "FAILED"; }; /** @@ -1385,7 +1391,8 @@ export type IntegrationIdView = { | "BACKBLAZE" | "ONE_LOGIN" | "OKTA" - | "CONTENTFUL"; + | "CONTENTFUL" + | "JIRA"; /** * The authentication method used by the integration */ @@ -1643,7 +1650,7 @@ export type UpdateSandboxRequest = { | "12x24" | "CUSTOM"; /** - * The commands to run on first boot of the sandbox + * The commands to run during first boot of the sandbox */ first_boot_commands?: string; /** @@ -1651,9 +1658,13 @@ export type UpdateSandboxRequest = { */ app_dir?: string; /** - * The list of app commands for the sandbox + * The list of apps (run commands) for the sandbox */ - apps?: Array; + apps?: Array; + /** + * The timeout in seconds after which the sandbox will be automatically stopped + */ + timeout?: number; /** * The list of tags associated with the sandbox */ @@ -1799,9 +1810,9 @@ export type HttpSettingsView = { /** * Custom HTTP headers to add to requests */ - request_headers?: Array<{ + request_headers?: { [key: string]: string; - }>; + }; /** * List of allowed User-Agent strings */ @@ -1813,9 +1824,9 @@ export type HttpSettingsView = { /** * Custom HTTP headers to add to responses */ - response_headers?: Array<{ + response_headers?: { [key: string]: string; - }>; + }; /** * Basic authentication username */ @@ -1875,6 +1886,21 @@ export type TunnelView = { endpoint_url?: string; }; +export type SandboxAppView = { + /** + * The auto-generated ID of the app + */ + id?: string; + /** + * The run command of the app + */ + command?: string; + /** + * The current status of the app + */ + app_status?: "NONE" | "RUNNING" | "ENDED" | "FAILED"; +}; + export type SandboxesView = { /** * API endpoint to GET this object @@ -1947,12 +1973,6 @@ export type SandboxCommandsView = { commands?: Array; }; -export type SandboxAppView = { - id?: string; - command?: string; - app_status?: "NONE" | "RUNNING" | "ENDED" | "FAILED"; -}; - export type SandboxCommandView = { /** * API endpoint to GET this object @@ -1983,17 +2003,17 @@ export type SandboxCommandView = { */ exit_code?: number; /** - * API endpoint URL to retrieve logs for this command + * Command execution start date */ - logs_url?: string; + start_date?: Date; /** - * Command start date + * Command execution finish date */ - start_date?: string; + finish_date?: Date; /** - * Command finish date + * API endpoint URL to retrieve logs for this command */ - finish_date?: string; + logs_url?: string; }; export type SandboxCommandLog = { @@ -2168,7 +2188,7 @@ export type CreateNewSandboxRequest = { | "12x24" | "CUSTOM"; /** - * The commands to run on first boot of the sandbox + * The commands to run during first boot of the sandbox */ first_boot_commands?: string; /** @@ -2176,7 +2196,7 @@ export type CreateNewSandboxRequest = { */ app_dir?: string; /** - * The list of app commands for the sandbox + * The list of apps (run commands) for the sandbox */ apps?: Array; /** @@ -2191,6 +2211,11 @@ export type CreateNewSandboxRequest = { * The environment variables of the sandbox */ variables?: Array; + /** + * The timeout in seconds after which the sandbox will be automatically stopped + */ + timeout?: number; + permissions?: PermissionsView; }; export type CreateFromSnapshotRequest = { @@ -2228,7 +2253,7 @@ export type CreateFromSnapshotRequest = { | "12x24" | "CUSTOM"; /** - * The commands to run on first boot of the sandbox + * The commands to run during first boot of the sandbox */ first_boot_commands?: string; /** @@ -2236,7 +2261,7 @@ export type CreateFromSnapshotRequest = { */ app_dir?: string; /** - * The list of app commands for the sandbox + * The list of apps (run commands) for the sandbox */ apps?: Array; /** @@ -2253,9 +2278,6 @@ export type CreateFromSnapshotRequest = { variables?: Array; }; -/** - * The list of variables you can use the action - */ export type EnvironmentVariableView = { /** * The ID of the variable @@ -2392,6 +2414,10 @@ export type SandboxResponse = { | "RUNNING" | "STOPPED" | "RESTORING"; + /** + * The current setup status of the sandbox + */ + setup_status?: "INPROGRESS" | "SUCCESS" | "FAILED" | "STALE"; /** * The operating system of the sandbox ["ubuntu:22.04", "ubuntu:24.04"] */ @@ -2414,7 +2440,7 @@ export type SandboxResponse = { | "12x24" | "CUSTOM"; /** - * The commands to run on first boot of the sandbox + * The commands to run during first boot of the sandbox */ first_boot_commands?: string; /** @@ -2422,21 +2448,21 @@ export type SandboxResponse = { */ app_dir?: string; /** - * The list of tags associated with the sandbox + * The list of apps (run commands) for the sandbox */ - tags?: Array; + apps?: Array; /** - * The sandbox applications + * The timeout in seconds after which the sandbox will be automatically stopped */ - apps?: Array; + timeout?: number; /** - * The boot logs of the sandbox + * The list of tags associated with the sandbox */ - boot_logs?: Array; + tags?: Array; /** - * The current setup status of the sandbox + * The boot logs of the sandbox */ - setup_status?: "INPROGRESS" | "SUCCESS" | "FAILED" | "STALE"; + boot_logs?: Array; /** * The tunnel endpoints of the sandbox */ @@ -2875,7 +2901,8 @@ export type IntegrationViewWritable = { | "BACKBLAZE" | "ONE_LOGIN" | "OKTA" - | "CONTENTFUL"; + | "CONTENTFUL" + | "JIRA"; /** * The authentication method used by the integration */ @@ -2990,6 +3017,10 @@ export type SandboxIdViewWritable = { | "RUNNING" | "STOPPED" | "RESTORING"; + /** + * The current setup status of the sandbox + */ + setup_status?: "INPROGRESS" | "SUCCESS" | "FAILED"; }; /** @@ -3061,7 +3092,8 @@ export type IntegrationIdViewWritable = { | "BACKBLAZE" | "ONE_LOGIN" | "OKTA" - | "CONTENTFUL"; + | "CONTENTFUL" + | "JIRA"; /** * The authentication method used by the integration */ @@ -3266,7 +3298,7 @@ export type UpdateSandboxRequestWritable = { | "12x24" | "CUSTOM"; /** - * The commands to run on first boot of the sandbox + * The commands to run during first boot of the sandbox */ first_boot_commands?: string; /** @@ -3274,9 +3306,13 @@ export type UpdateSandboxRequestWritable = { */ app_dir?: string; /** - * The list of app commands for the sandbox + * The list of apps (run commands) for the sandbox */ - apps?: Array; + apps?: Array; + /** + * The timeout in seconds after which the sandbox will be automatically stopped + */ + timeout?: number; /** * The list of tags associated with the sandbox */ @@ -3410,9 +3446,9 @@ export type HttpSettingsViewWritable = { /** * Custom HTTP headers to add to requests */ - request_headers?: Array<{ + request_headers?: { [key: string]: string; - }>; + }; /** * List of allowed User-Agent strings */ @@ -3424,9 +3460,9 @@ export type HttpSettingsViewWritable = { /** * Custom HTTP headers to add to responses */ - response_headers?: Array<{ + response_headers?: { [key: string]: string; - }>; + }; /** * Basic authentication username */ @@ -3526,12 +3562,6 @@ export type SandboxCommandsViewWritable = { commands?: Array; }; -export type SandboxAppViewWritable = { - id?: string; - command?: string; - app_status?: "NONE" | "RUNNING" | "ENDED" | "FAILED"; -}; - export type SandboxCommandViewWritable = { /** * The ID of the command @@ -3554,17 +3584,17 @@ export type SandboxCommandViewWritable = { */ exit_code?: number; /** - * API endpoint URL to retrieve logs for this command + * Command execution start date */ - logs_url?: string; + start_date?: Date; /** - * Command start date + * Command execution finish date */ - start_date?: string; + finish_date?: Date; /** - * Command finish date + * API endpoint URL to retrieve logs for this command */ - finish_date?: string; + logs_url?: string; }; export type SandboxAppLogsViewWritable = { @@ -3670,7 +3700,7 @@ export type CreateNewSandboxRequestWritable = { | "12x24" | "CUSTOM"; /** - * The commands to run on first boot of the sandbox + * The commands to run during first boot of the sandbox */ first_boot_commands?: string; /** @@ -3678,7 +3708,7 @@ export type CreateNewSandboxRequestWritable = { */ app_dir?: string; /** - * The list of app commands for the sandbox + * The list of apps (run commands) for the sandbox */ apps?: Array; /** @@ -3693,6 +3723,11 @@ export type CreateNewSandboxRequestWritable = { * The environment variables of the sandbox */ variables?: Array; + /** + * The timeout in seconds after which the sandbox will be automatically stopped + */ + timeout?: number; + permissions?: PermissionsView; }; export type CreateFromSnapshotRequestWritable = { @@ -3730,7 +3765,7 @@ export type CreateFromSnapshotRequestWritable = { | "12x24" | "CUSTOM"; /** - * The commands to run on first boot of the sandbox + * The commands to run during first boot of the sandbox */ first_boot_commands?: string; /** @@ -3738,7 +3773,7 @@ export type CreateFromSnapshotRequestWritable = { */ app_dir?: string; /** - * The list of app commands for the sandbox + * The list of apps (run commands) for the sandbox */ apps?: Array; /** @@ -3778,6 +3813,10 @@ export type SandboxResponseWritable = { | "RUNNING" | "STOPPED" | "RESTORING"; + /** + * The current setup status of the sandbox + */ + setup_status?: "INPROGRESS" | "SUCCESS" | "FAILED" | "STALE"; /** * The operating system of the sandbox ["ubuntu:22.04", "ubuntu:24.04"] */ @@ -3800,7 +3839,7 @@ export type SandboxResponseWritable = { | "12x24" | "CUSTOM"; /** - * The commands to run on first boot of the sandbox + * The commands to run during first boot of the sandbox */ first_boot_commands?: string; /** @@ -3808,21 +3847,21 @@ export type SandboxResponseWritable = { */ app_dir?: string; /** - * The list of tags associated with the sandbox + * The list of apps (run commands) for the sandbox */ - tags?: Array; + apps?: Array; /** - * The sandbox applications + * The timeout in seconds after which the sandbox will be automatically stopped */ - apps?: Array; + timeout?: number; /** - * The boot logs of the sandbox + * The list of tags associated with the sandbox */ - boot_logs?: Array; + tags?: Array; /** - * The current setup status of the sandbox + * The boot logs of the sandbox */ - setup_status?: "INPROGRESS" | "SUCCESS" | "FAILED" | "STALE"; + boot_logs?: Array; /** * The tunnel endpoints of the sandbox */ @@ -4360,7 +4399,7 @@ export type UpdateSandboxResponses = { export type UpdateSandboxResponse = UpdateSandboxResponses[keyof UpdateSandboxResponses]; -export type GetSandboxAppLogsData = { +export type GetSandboxAppLogsByIdData = { body?: never; path: { /** @@ -4371,6 +4410,10 @@ export type GetSandboxAppLogsData = { * The ID of the sandbox */ sandbox_id: string; + /** + * The ID of the app + */ + app_id: string; }; query?: { /** @@ -4378,21 +4421,30 @@ export type GetSandboxAppLogsData = { */ cursor?: string; }; - url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/app-logs"; + url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/apps/{app_id}/logs"; }; -export type GetSandboxAppLogsResponses = { +export type GetSandboxAppLogsByIdResponses = { 200: SandboxAppLogsView; }; -export type GetSandboxAppLogsResponse = - GetSandboxAppLogsResponses[keyof GetSandboxAppLogsResponses]; +export type GetSandboxAppLogsByIdResponse = + GetSandboxAppLogsByIdResponses[keyof GetSandboxAppLogsByIdResponses]; export type StartSandboxAppData = { body?: never; path: { + /** + * The human-readable ID of the workspace + */ workspace_domain: string; + /** + * The ID of the sandbox + */ sandbox_id: string; + /** + * The ID of the app + */ app_id: string; }; query?: never; @@ -4400,7 +4452,7 @@ export type StartSandboxAppData = { }; export type StartSandboxAppResponses = { - 200: void; + 200: SandboxResponse; }; export type StartSandboxAppResponse = @@ -4409,8 +4461,17 @@ export type StartSandboxAppResponse = export type StopSandboxAppData = { body?: never; path: { + /** + * The human-readable ID of the workspace + */ workspace_domain: string; + /** + * The ID of the sandbox + */ sandbox_id: string; + /** + * The ID of the app + */ app_id: string; }; query?: never; @@ -4418,32 +4479,12 @@ export type StopSandboxAppData = { }; export type StopSandboxAppResponses = { - 200: void; + 200: SandboxResponse; }; export type StopSandboxAppResponse = StopSandboxAppResponses[keyof StopSandboxAppResponses]; -export type GetSandboxAppLogsByAppData = { - body?: never; - path: { - workspace_domain: string; - sandbox_id: string; - app_id: string; - }; - query?: { - cursor?: string; - }; - url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/apps/{app_id}/logs"; -}; - -export type GetSandboxAppLogsByAppResponses = { - 200: SandboxAppLogsView; -}; - -export type GetSandboxAppLogsByAppResponse = - GetSandboxAppLogsByAppResponses[keyof GetSandboxAppLogsByAppResponses]; - export type GetSandboxCommandsData = { body?: never; path: { diff --git a/src/api/openapi/zod.gen.ts b/src/api/openapi/zod.gen.ts index 13eebed..54bee6c 100644 --- a/src/api/openapi/zod.gen.ts +++ b/src/api/openapi/zod.gen.ts @@ -556,6 +556,7 @@ export const zIntegrationView = z.object({ "ONE_LOGIN", "OKTA", "CONTENTFUL", + "JIRA", ]), ), auth_type: z.optional( @@ -731,6 +732,7 @@ export const zAddIntegrationRequest = z.object({ "ONE_LOGIN", "OKTA", "CONTENTFUL", + "JIRA", ]), scope: z.enum(["WORKSPACE", "PROJECT", "ENVIRONMENT"]), project_name: z.optional(z.string()), @@ -755,6 +757,7 @@ export const zSandboxIdView = z.object({ "RESTORING", ]), ), + setup_status: z.optional(z.enum(["INPROGRESS", "SUCCESS", "FAILED"])), }); /** @@ -818,6 +821,7 @@ export const zIntegrationIdView = z.object({ "ONE_LOGIN", "OKTA", "CONTENTFUL", + "JIRA", ]), ), auth_type: z.optional( @@ -995,10 +999,10 @@ export const zHttpSettingsView = z.object({ compression: z.optional(z.boolean()), http2: z.optional(z.boolean()), log_requests: z.optional(z.boolean()), - request_headers: z.optional(z.array(z.record(z.string(), z.string()))), + request_headers: z.optional(z.record(z.string(), z.string())), whitelist_user_agents: z.optional(z.array(z.string())), rewrite_host_header: z.optional(z.string()), - response_headers: z.optional(z.array(z.record(z.string(), z.string()))), + response_headers: z.optional(z.record(z.string(), z.string())), login: z.optional(z.string()), circuit_breaker: z.optional( z @@ -1037,6 +1041,12 @@ export const zTunnelView = z.object({ endpoint_url: z.optional(z.string()), }); +export const zSandboxAppView = z.object({ + id: z.optional(z.string()), + command: z.optional(z.string()), + app_status: z.optional(z.enum(["NONE", "RUNNING", "ENDED", "FAILED"])), +}); + export const zUpdateSandboxRequest = z.object({ name: z.optional(z.string()), identifier: z.optional(z.string()), @@ -1059,7 +1069,17 @@ export const zUpdateSandboxRequest = z.object({ ), first_boot_commands: z.optional(z.string()), app_dir: z.optional(z.string()), - apps: z.optional(z.array(z.string())), + apps: z.optional(z.array(zSandboxAppView)), + timeout: z.optional( + z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }), + ), tags: z.optional(z.array(z.string())), endpoints: z.optional(z.array(zTunnelView)), variables: z.optional(z.array(zAddVariableInObjectRequest)), @@ -1102,12 +1122,6 @@ export const zSandboxContentView = z.object({ contents: z.optional(z.array(zSandboxContentItem)), }); -export const zSandboxAppView = z.object({ - id: z.optional(z.string()), - command: z.optional(z.string()), - app_status: z.optional(z.enum(["NONE", "RUNNING", "ENDED", "FAILED"])), -}); - export const zSandboxCommandView = z.object({ url: z.optional(z.string().readonly()), html_url: z.optional(z.string().readonly()), @@ -1125,9 +1139,9 @@ export const zSandboxCommandView = z.object({ error: "Invalid value: Expected int32 to be <= 2147483647", }), ), + start_date: z.optional(z.iso.datetime()), + finish_date: z.optional(z.iso.datetime()), logs_url: z.optional(z.string()), - start_date: z.optional(z.string()), - finish_date: z.optional(z.string()), }); export const zSandboxCommandsView = z.object({ @@ -1235,11 +1249,19 @@ export const zCreateNewSandboxRequest = z.object({ tags: z.optional(z.array(z.string())), endpoints: z.optional(z.array(zTunnelView)), variables: z.optional(z.array(zAddVariableInObjectRequest)), + timeout: z.optional( + z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }), + ), + permissions: z.optional(zPermissionsView), }); -/** - * The list of variables you can use the action - */ export const zEnvironmentVariableView = z.object({ id: z.optional( z @@ -1334,6 +1356,9 @@ export const zSandboxResponse = z.object({ "RESTORING", ]), ), + setup_status: z.optional( + z.enum(["INPROGRESS", "SUCCESS", "FAILED", "STALE"]), + ), os: z.optional(z.string()), resources: z.optional( z.enum([ @@ -1354,10 +1379,19 @@ export const zSandboxResponse = z.object({ ), first_boot_commands: z.optional(z.string()), app_dir: z.optional(z.string()), - tags: z.optional(z.array(z.string())), apps: z.optional(z.array(zSandboxAppView)), + timeout: z.optional( + z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }), + ), + tags: z.optional(z.array(z.string())), boot_logs: z.optional(z.array(z.string())), - setup_status: z.optional(z.enum(["INPROGRESS", "SUCCESS", "FAILED", "STALE"])), endpoints: z.optional(z.array(zTunnelView)), project: z.optional(zProjectView), permissions: z.optional(zPermissionsView), @@ -1639,6 +1673,7 @@ export const zIntegrationViewWritable = z.object({ "ONE_LOGIN", "OKTA", "CONTENTFUL", + "JIRA", ]), ), auth_type: z.optional( @@ -1708,6 +1743,7 @@ export const zSandboxIdViewWritable = z.object({ "RESTORING", ]), ), + setup_status: z.optional(z.enum(["INPROGRESS", "SUCCESS", "FAILED"])), }); /** @@ -1769,6 +1805,7 @@ export const zIntegrationIdViewWritable = z.object({ "ONE_LOGIN", "OKTA", "CONTENTFUL", + "JIRA", ]), ), auth_type: z.optional( @@ -1914,10 +1951,10 @@ export const zHttpSettingsViewWritable = z.object({ compression: z.optional(z.boolean()), http2: z.optional(z.boolean()), log_requests: z.optional(z.boolean()), - request_headers: z.optional(z.array(z.record(z.string(), z.string()))), + request_headers: z.optional(z.record(z.string(), z.string())), whitelist_user_agents: z.optional(z.array(z.string())), rewrite_host_header: z.optional(z.string()), - response_headers: z.optional(z.array(z.record(z.string(), z.string()))), + response_headers: z.optional(z.record(z.string(), z.string())), login: z.optional(z.string()), password: z.optional(z.string()), tls_ca: z.optional(z.string()), @@ -1978,7 +2015,17 @@ export const zUpdateSandboxRequestWritable = z.object({ ), first_boot_commands: z.optional(z.string()), app_dir: z.optional(z.string()), - apps: z.optional(z.array(z.string())), + apps: z.optional(z.array(zSandboxAppView)), + timeout: z.optional( + z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }), + ), tags: z.optional(z.array(z.string())), endpoints: z.optional(z.array(zTunnelViewWritable)), variables: z.optional(z.array(zAddVariableInObjectRequestWritable)), @@ -2015,12 +2062,6 @@ export const zSandboxContentViewWritable = z.object({ contents: z.optional(z.array(zSandboxContentItemWritable)), }); -export const zSandboxAppViewWritable = z.object({ - id: z.optional(z.string()), - command: z.optional(z.string()), - app_status: z.optional(z.enum(["NONE", "RUNNING", "ENDED", "FAILED"])), -}); - export const zSandboxCommandViewWritable = z.object({ id: z.optional(z.string()), command: z.optional(z.string()), @@ -2036,9 +2077,9 @@ export const zSandboxCommandViewWritable = z.object({ error: "Invalid value: Expected int32 to be <= 2147483647", }), ), + start_date: z.optional(z.iso.datetime()), + finish_date: z.optional(z.iso.datetime()), logs_url: z.optional(z.string()), - start_date: z.optional(z.string()), - finish_date: z.optional(z.string()), }); export const zSandboxCommandsViewWritable = z.object({ @@ -2120,6 +2161,17 @@ export const zCreateNewSandboxRequestWritable = z.object({ tags: z.optional(z.array(z.string())), endpoints: z.optional(z.array(zTunnelViewWritable)), variables: z.optional(z.array(zAddVariableInObjectRequestWritable)), + timeout: z.optional( + z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }), + ), + permissions: z.optional(zPermissionsView), }); export const zCreateFromSnapshotRequestWritable = z.object({ @@ -2166,6 +2218,9 @@ export const zSandboxResponseWritable = z.object({ "RESTORING", ]), ), + setup_status: z.optional( + z.enum(["INPROGRESS", "SUCCESS", "FAILED", "STALE"]), + ), os: z.optional(z.string()), resources: z.optional( z.enum([ @@ -2186,10 +2241,19 @@ export const zSandboxResponseWritable = z.object({ ), first_boot_commands: z.optional(z.string()), app_dir: z.optional(z.string()), + apps: z.optional(z.array(zSandboxAppView)), + timeout: z.optional( + z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }), + ), tags: z.optional(z.array(z.string())), - apps: z.optional(z.array(zSandboxAppViewWritable)), boot_logs: z.optional(z.array(z.string())), - setup_status: z.optional(z.enum(["INPROGRESS", "SUCCESS", "FAILED", "STALE"])), endpoints: z.optional(z.array(zTunnelViewWritable)), project: z.optional(zProjectViewWritable), permissions: z.optional(zPermissionsView), @@ -2522,11 +2586,12 @@ export const zUpdateSandboxData = z.object({ export const zUpdateSandboxResponse = zSandboxResponse; -export const zGetSandboxAppLogsData = z.object({ +export const zGetSandboxAppLogsByIdData = z.object({ body: z.optional(z.never()), path: z.object({ workspace_domain: z.string(), sandbox_id: z.string(), + app_id: z.string(), }), query: z.optional( z.object({ @@ -2535,7 +2600,7 @@ export const zGetSandboxAppLogsData = z.object({ ), }); -export const zGetSandboxAppLogsResponse = zSandboxAppLogsView; +export const zGetSandboxAppLogsByIdResponse = zSandboxAppLogsView; export const zStartSandboxAppData = z.object({ body: z.optional(z.never()), @@ -2547,7 +2612,7 @@ export const zStartSandboxAppData = z.object({ query: z.optional(z.never()), }); -export const zStartSandboxAppResponse = z.void(); +export const zStartSandboxAppResponse = zSandboxResponse; export const zStopSandboxAppData = z.object({ body: z.optional(z.never()), @@ -2559,23 +2624,7 @@ export const zStopSandboxAppData = z.object({ query: z.optional(z.never()), }); -export const zStopSandboxAppResponse = z.void(); - -export const zGetSandboxAppLogsByAppData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - sandbox_id: z.string(), - app_id: z.string(), - }), - query: z.optional( - z.object({ - cursor: z.optional(z.string()), - }), - ), -}); - -export const zGetSandboxAppLogsByAppResponse = zSandboxAppLogsView; +export const zStopSandboxAppResponse = zSandboxResponse; export const zGetSandboxCommandsData = z.object({ body: z.optional(z.never()), diff --git a/src/core/buddy-api-client.ts b/src/core/buddy-api-client.ts index 607edca..f5ddc29 100644 --- a/src/core/buddy-api-client.ts +++ b/src/core/buddy-api-client.ts @@ -2,11 +2,16 @@ import { prettifyError, z } from "zod"; import { addSandboxResponseTransformer, createSandboxDirectoryResponseTransformer, + executeSandboxCommandResponseTransformer, + getSandboxCommandResponseTransformer, getSandboxContentResponseTransformer, getSandboxResponseTransformer, restartSandboxResponseTransformer, + startSandboxAppResponseTransformer, startSandboxResponseTransformer, + stopSandboxAppResponseTransformer, stopSandboxResponseTransformer, + terminateSandboxCommandResponseTransformer, uploadSandboxFileResponseTransformer, } from "@/api/openapi/transformers.gen"; import type { @@ -23,8 +28,8 @@ import type { ExecuteSandboxCommandResponse, GetIdentifiersData, GetIdentifiersResponse, - GetSandboxAppLogsByAppData, - GetSandboxAppLogsByAppResponse, + GetSandboxAppLogsByIdData, + GetSandboxAppLogsByIdResponse, GetSandboxCommandData, GetSandboxCommandLogsData, GetSandboxCommandResponse, @@ -64,8 +69,8 @@ import { zExecuteSandboxCommandResponse, zGetIdentifiersData, zGetIdentifiersResponse, - zGetSandboxAppLogsByAppData, - zGetSandboxAppLogsByAppResponse, + zGetSandboxAppLogsByIdData, + zGetSandboxAppLogsByIdResponse, zGetSandboxCommandData, zGetSandboxCommandLogsData, zGetSandboxCommandResponse, @@ -298,7 +303,9 @@ export class BuddyApiClient extends HttpClient { data, url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/commands", dataSchema: zExecuteSandboxCommandData, - responseSchema: zExecuteSandboxCommandResponse, + responseSchema: zExecuteSandboxCommandResponse.transform( + executeSandboxCommandResponseTransformer, + ), }); } @@ -311,7 +318,9 @@ export class BuddyApiClient extends HttpClient { data, url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/commands/{id}", dataSchema: zGetSandboxCommandData, - responseSchema: zGetSandboxCommandResponse, + responseSchema: zGetSandboxCommandResponse.transform( + getSandboxCommandResponseTransformer, + ), }); } @@ -324,7 +333,9 @@ export class BuddyApiClient extends HttpClient { data, url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/commands/{command_id}/terminate", dataSchema: zTerminateSandboxCommandData, - responseSchema: zTerminateSandboxCommandResponse, + responseSchema: zTerminateSandboxCommandResponse.transform( + terminateSandboxCommandResponseTransformer, + ), }); } @@ -417,7 +428,9 @@ export class BuddyApiClient extends HttpClient { data, url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/apps/{app_id}/start", dataSchema: zStartSandboxAppData, - responseSchema: zStartSandboxAppResponse, + responseSchema: zStartSandboxAppResponse.transform( + startSandboxAppResponseTransformer, + ), }); } @@ -430,20 +443,22 @@ export class BuddyApiClient extends HttpClient { data, url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/apps/{app_id}/stop", dataSchema: zStopSandboxAppData, - responseSchema: zStopSandboxAppResponse, + responseSchema: zStopSandboxAppResponse.transform( + stopSandboxAppResponseTransformer, + ), }); } /** Get logs for a specific sandbox app */ - async getSandboxAppLogs( + async getSandboxAppLogs( data: ClientData, ) { - return this.#requestWithValidation({ + return this.#requestWithValidation({ method: "GET", data, url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/apps/{app_id}/logs", - dataSchema: zGetSandboxAppLogsByAppData, - responseSchema: zGetSandboxAppLogsByAppResponse, + dataSchema: zGetSandboxAppLogsByIdData, + responseSchema: zGetSandboxAppLogsByIdResponse, }); } From c9529d39fb3f4e41c6e4fdbf5b4b1e7e1b0d65f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Ga=C5=84czarczyk?= Date: Wed, 4 Mar 2026 14:34:11 +0100 Subject: [PATCH 03/23] feat: add per-app methods to Sandbox entity and handle STALE setup status --- src/entity/sandbox.ts | 59 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/entity/sandbox.ts b/src/entity/sandbox.ts index d838d82..6170633 100644 --- a/src/entity/sandbox.ts +++ b/src/entity/sandbox.ts @@ -2,7 +2,9 @@ import type { Writable } from "node:stream"; import type { CreateNewSandboxRequestWritable, ExecuteSandboxCommandRequest, + GetSandboxAppLogsByIdResponse, GetSandboxResponse, + SandboxAppView, SandboxIdView, } from "@/api/openapi/types.gen"; import type { BuddyApiClient } from "@/core/buddy-api-client"; @@ -326,6 +328,12 @@ export class Sandbox { ); } + if (this.data.setup_status === "STALE") { + throw new Error( + `Sandbox ${sandboxId} setup is stale. The first_boot_commands were changed but not applied. Recreate the sandbox to apply them.`, + ); + } + await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); } }); @@ -497,6 +505,57 @@ export class Sandbox { }); } + /** + * Start a specific app within the sandbox + * + * Updates the internal sandbox data with the latest state after starting. + * @param appId - The auto-generated ID of the app (from sandbox.data.apps) + */ + async startApp(appId: NonNullable): Promise { + const sandboxId = this.initializedId; + return withErrorHandler("Failed to start app", async () => { + this.#sandboxData = await this.#client.startSandboxApp({ + path: { sandbox_id: sandboxId, app_id: appId }, + }); + }); + } + + /** + * Stop a specific app within the sandbox + * + * Updates the internal sandbox data with the latest state after stopping. + * @param appId - The auto-generated ID of the app (from sandbox.data.apps) + */ + async stopApp(appId: NonNullable): Promise { + const sandboxId = this.initializedId; + return withErrorHandler("Failed to stop app", async () => { + this.#sandboxData = await this.#client.stopSandboxApp({ + path: { sandbox_id: sandboxId, app_id: appId }, + }); + }); + } + + /** + * Get logs for a specific app within the sandbox + * + * Returns paginated log entries. Pass the returned cursor to fetch the next page. + * @param appId - The auto-generated ID of the app (from sandbox.data.apps) + * @param cursor - Pagination cursor from a previous response + * @returns Log entries and a cursor for the next page + */ + async getAppLogs( + appId: NonNullable, + cursor?: string, + ): Promise { + const sandboxId = this.initializedId; + return withErrorHandler("Failed to get app logs", async () => { + return this.#client.getSandboxAppLogs({ + path: { sandbox_id: sandboxId, app_id: appId }, + query: { cursor }, + }); + }); + } + private constructor( sandboxData: NonNullable, client: BuddyApiClient, From 75d25cc29335a54575705eee4f33293bbb6bcf38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Ga=C5=84czarczyk?= Date: Wed, 4 Mar 2026 14:34:24 +0100 Subject: [PATCH 04/23] test: add integration tests for sandbox app start/stop/logs --- tests/sandbox.test.ts | 61 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/tests/sandbox.test.ts b/tests/sandbox.test.ts index 835d39a..2005514 100644 --- a/tests/sandbox.test.ts +++ b/tests/sandbox.test.ts @@ -313,6 +313,67 @@ describe("Sandbox", () => { }); }); + describe("apps", () => { + let appSandbox: Sandbox; + + beforeAll(async () => { + appSandbox = await Sandbox.create({ + name: `test-apps-${Date.now()}`, + identifier: `test_apps_${Date.now()}`, + apps: ["echo 'app running' && sleep 3600"], + }); + await appSandbox.waitUntilRunning(); + }, 60_000); + + afterAll(async () => { + await appSandbox?.destroy(); + }, 30_000); + + it("should have apps in response", () => { + expect(appSandbox.data.apps).toBeDefined(); + expect(appSandbox.data.apps!.length).toBeGreaterThan(0); + }); + + it("should have app with id, command, and app_status", () => { + const apps = appSandbox.data.apps; + expect(apps).toBeDefined(); + expect(apps!.length).toBeGreaterThan(0); + + const app = apps![0]; + expect(app).toBeDefined(); + expect(app!.id).toBeDefined(); + expect(app!.command).toBeDefined(); + expect(["NONE", "RUNNING", "ENDED", "FAILED"]).toContain(app!.app_status); + }); + + it("should stop an app", async () => { + const appId = appSandbox.data.apps![0]!.id!; + await appSandbox.stopApp(appId); + + const app = appSandbox.data.apps?.find((a) => a.id === appId); + expect(app).toBeDefined(); + expect(["ENDED", "NONE"]).toContain(app!.app_status); + }); + + it("should start an app", async () => { + const appId = appSandbox.data.apps![0]!.id!; + await appSandbox.startApp(appId); + + const app = appSandbox.data.apps?.find((a) => a.id === appId); + expect(app).toBeDefined(); + expect(app!.app_status).toBe("RUNNING"); + }); + + it("should get app logs", async () => { + const appId = appSandbox.data.apps![0]!.id!; + const logs = await appSandbox.getAppLogs(appId); + + expect(logs).toBeDefined(); + expect(logs.logs).toBeDefined(); + expect(Array.isArray(logs.logs)).toBe(true); + }); + }); + describe("state management", () => { it("should stop the sandbox", async () => { await sandbox.stop(); From ae25cb02c351fc73aac03966c950c6ac9109d075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Ga=C5=84czarczyk?= Date: Wed, 4 Mar 2026 14:59:12 +0100 Subject: [PATCH 05/23] test: use multiple apps and verify independent start/stop behavior --- tests/sandbox.test.ts | 50 ++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/tests/sandbox.test.ts b/tests/sandbox.test.ts index 2005514..47ae373 100644 --- a/tests/sandbox.test.ts +++ b/tests/sandbox.test.ts @@ -320,7 +320,7 @@ describe("Sandbox", () => { appSandbox = await Sandbox.create({ name: `test-apps-${Date.now()}`, identifier: `test_apps_${Date.now()}`, - apps: ["echo 'app running' && sleep 3600"], + apps: ["echo 'app1 running' && sleep 3600", "echo 'app2 running' && sleep 3600"], }); await appSandbox.waitUntilRunning(); }, 60_000); @@ -329,33 +329,43 @@ describe("Sandbox", () => { await appSandbox?.destroy(); }, 30_000); - it("should have apps in response", () => { - expect(appSandbox.data.apps).toBeDefined(); - expect(appSandbox.data.apps!.length).toBeGreaterThan(0); - }); - - it("should have app with id, command, and app_status", () => { + it("should have multiple apps in response", () => { const apps = appSandbox.data.apps; expect(apps).toBeDefined(); - expect(apps!.length).toBeGreaterThan(0); + expect(apps!.length).toBe(2); - const app = apps![0]; - expect(app).toBeDefined(); - expect(app!.id).toBeDefined(); - expect(app!.command).toBeDefined(); - expect(["NONE", "RUNNING", "ENDED", "FAILED"]).toContain(app!.app_status); + for (const app of apps!) { + expect(app.id).toBeDefined(); + expect(app.command).toBeDefined(); + expect(["NONE", "RUNNING", "ENDED", "FAILED"]).toContain(app.app_status); + } }); - it("should stop an app", async () => { - const appId = appSandbox.data.apps![0]!.id!; - await appSandbox.stopApp(appId); + it("should have distinct app ids and commands", () => { + const apps = appSandbox.data.apps!; + const ids = apps.map((a) => a.id); + const commands = apps.map((a) => a.command); - const app = appSandbox.data.apps?.find((a) => a.id === appId); - expect(app).toBeDefined(); - expect(["ENDED", "NONE"]).toContain(app!.app_status); + expect(new Set(ids).size).toBe(2); + expect(new Set(commands).size).toBe(2); + }); + + it("should stop one app without affecting the other", async () => { + const apps = appSandbox.data.apps!; + const firstId = apps[0]!.id!; + const secondId = apps[1]!.id!; + + await appSandbox.stopApp(firstId); + + const stopped = appSandbox.data.apps?.find((a) => a.id === firstId); + const other = appSandbox.data.apps?.find((a) => a.id === secondId); + expect(stopped).toBeDefined(); + expect(["ENDED", "NONE"]).toContain(stopped!.app_status); + expect(other).toBeDefined(); + expect(other!.app_status).toBe("RUNNING"); }); - it("should start an app", async () => { + it("should start a stopped app", async () => { const appId = appSandbox.data.apps![0]!.id!; await appSandbox.startApp(appId); From 5ddc81432f734234491686a351c05a6a66cfe486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Ga=C5=84czarczyk?= Date: Wed, 4 Mar 2026 15:18:16 +0100 Subject: [PATCH 06/23] feat: add apps example demonstrating per-app start/stop/logs --- examples/package.json | 1 + examples/src/apps.ts | 70 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 examples/src/apps.ts diff --git a/examples/package.json b/examples/package.json index eec70f7..d2cc26d 100644 --- a/examples/package.json +++ b/examples/package.json @@ -10,6 +10,7 @@ "example:lifecycle": "NODE_OPTIONS='--enable-source-maps' dotenvx run -q -- tsx src/lifecycle.ts", "example:streaming": "NODE_OPTIONS='--enable-source-maps' dotenvx run -q -- tsx src/streaming.ts", "example:filesystem": "NODE_OPTIONS='--enable-source-maps' dotenvx run -q -- tsx src/filesystem.ts", + "example:apps": "NODE_OPTIONS='--enable-source-maps' dotenvx run -q -- tsx src/apps.ts", "example:error-handling": "NODE_OPTIONS='--enable-source-maps' dotenvx run -q -- tsx src/error-handling.ts", "example:all": "concurrently \"pnpm example:ping\" \"pnpm example:list\" \"pnpm example:lifecycle\" \"pnpm example:streaming\" \"pnpm example:filesystem\" \"pnpm example:error-handling\"" }, diff --git a/examples/src/apps.ts b/examples/src/apps.ts new file mode 100644 index 0000000..e436286 --- /dev/null +++ b/examples/src/apps.ts @@ -0,0 +1,70 @@ +import { Sandbox } from "@buddy-works/sandbox-sdk"; +import { log } from "@/shared/logger"; + +log("Sandbox Apps Example\n"); + +const identifier = "apps-demo-sandbox"; + +let sandbox: Sandbox; + +try { + sandbox = await Sandbox.getByIdentifier(identifier); + log(`Found existing sandbox with identifier: ${identifier}, deleting...`); + await sandbox.destroy(); +} catch { + // Sandbox doesn't exist, nothing to delete +} + +log(`Creating sandbox with 2 apps...`); +sandbox = await Sandbox.create({ + identifier, + name: "Apps Demo Sandbox", + os: "ubuntu:24.04", + first_boot_commands: "apt-get update && apt-get install -y curl", + apps: [ + "while true; do echo \"[app1] ping $(date)\"; sleep 2; done", + "while true; do echo \"[app2] pong $(date)\"; sleep 3; done", + ], +}); +log(`Created sandbox: ${sandbox.data.identifier} (${sandbox.data.html_url})`); +log(`Status: ${sandbox.data.status}, Setup: ${sandbox.data.setup_status}\n`); + +log("Apps:"); +for (const app of sandbox.data.apps ?? []) { + log(` ${app.id}: "${app.command}" -> ${app.app_status}`); +} + +const firstApp = sandbox.data.apps?.[0]; +const secondApp = sandbox.data.apps?.[1]; + +if (!firstApp?.id || !secondApp?.id) { + throw new Error("Expected 2 apps in sandbox"); +} + +log(`\nStopping the first app (${firstApp.id})...`); +await sandbox.stopApp(firstApp.id); +log("Apps after stopping the first one:"); +for (const app of sandbox.data.apps ?? []) { + log(` ${app.id}: ${app.app_status}`); +} + +log(`\nStarting the first app (${firstApp.id})...`); +await sandbox.startApp(firstApp.id); +log("Apps after starting the first one:"); +for (const app of sandbox.data.apps ?? []) { + log(` ${app.id}: ${app.app_status}`); +} + +log("\nWaiting a few seconds for log output..."); +await new Promise((resolve) => setTimeout(resolve, 5000)); + +log(`Getting logs for the second app (${secondApp.id})...`); +const logs = await sandbox.getAppLogs(secondApp.id); +log(`Logs (${logs.logs?.length ?? 0} entries):`); +for (const entry of logs.logs ?? []) { + log(` ${entry}`); +} + +log("\nCleaning up..."); +await sandbox.destroy(); +log("Sandbox deleted successfully"); From 1af0f82083e191569ee095f7a5c1f808631cb1ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Ga=C5=84czarczyk?= Date: Thu, 5 Mar 2026 08:14:22 +0100 Subject: [PATCH 07/23] docs: add apps section to README --- README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/README.md b/README.md index 863b86b..4390f56 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,34 @@ export BUDDY_PROJECT="your-project" export BUDDY_REGION="US" # Optional: US (default), EU, or AP ``` +## Apps + +Sandboxes can run multiple apps simultaneously. Each app is a long-running process defined by a command string. + +```typescript +const sandbox = await Sandbox.create({ + identifier: "my-sandbox", + name: "My Sandbox", + os: "ubuntu:24.04", + first_boot_commands: "apt-get update && apt-get install -y curl", + apps: ["node server.js", "python worker.py"], +}); + +// List apps +for (const app of sandbox.data.apps ?? []) { + console.log(`${app.id}: "${app.command}" -> ${app.app_status}`); +} + +// Control individual apps +const appId = sandbox.data.apps![0].id!; + +await sandbox.stopApp(appId); +await sandbox.startApp(appId); + +const { logs } = await sandbox.getAppLogs(appId); +console.log(logs); +``` + ## Regions Configure the API region: From 9704f2d8858558f23548d8d9dbe6a5950c919388 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Ga=C5=84czarczyk?= Date: Mon, 9 Mar 2026 12:45:11 +0100 Subject: [PATCH 08/23] fix: resolve lint warnings in app tests and example --- examples/src/apps.ts | 4 ++-- tests/sandbox.test.ts | 49 ++++++++++++++++++++++++++++++------------- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/examples/src/apps.ts b/examples/src/apps.ts index e436286..d7648e0 100644 --- a/examples/src/apps.ts +++ b/examples/src/apps.ts @@ -22,8 +22,8 @@ sandbox = await Sandbox.create({ os: "ubuntu:24.04", first_boot_commands: "apt-get update && apt-get install -y curl", apps: [ - "while true; do echo \"[app1] ping $(date)\"; sleep 2; done", - "while true; do echo \"[app2] pong $(date)\"; sleep 3; done", + 'while true; do echo "[app1] ping $(date)"; sleep 2; done', + 'while true; do echo "[app2] pong $(date)"; sleep 3; done', ], }); log(`Created sandbox: ${sandbox.data.identifier} (${sandbox.data.html_url})`); diff --git a/tests/sandbox.test.ts b/tests/sandbox.test.ts index 47ae373..a35017b 100644 --- a/tests/sandbox.test.ts +++ b/tests/sandbox.test.ts @@ -316,11 +316,30 @@ describe("Sandbox", () => { describe("apps", () => { let appSandbox: Sandbox; + function getApps() { + const apps = appSandbox.data.apps; + if (!apps || apps.length < 2) { + throw new Error("Expected at least 2 apps"); + } + return apps; + } + + function getAppId(index: number) { + const app = getApps()[index]; + if (!app?.id) { + throw new Error(`App at index ${index} has no id`); + } + return app.id; + } + beforeAll(async () => { appSandbox = await Sandbox.create({ name: `test-apps-${Date.now()}`, identifier: `test_apps_${Date.now()}`, - apps: ["echo 'app1 running' && sleep 3600", "echo 'app2 running' && sleep 3600"], + apps: [ + "echo 'app1 running' && sleep 3600", + "echo 'app2 running' && sleep 3600", + ], }); await appSandbox.waitUntilRunning(); }, 60_000); @@ -330,19 +349,20 @@ describe("Sandbox", () => { }, 30_000); it("should have multiple apps in response", () => { - const apps = appSandbox.data.apps; - expect(apps).toBeDefined(); - expect(apps!.length).toBe(2); + const apps = getApps(); + expect(apps.length).toBe(2); - for (const app of apps!) { + for (const app of apps) { expect(app.id).toBeDefined(); expect(app.command).toBeDefined(); - expect(["NONE", "RUNNING", "ENDED", "FAILED"]).toContain(app.app_status); + expect(["NONE", "RUNNING", "ENDED", "FAILED"]).toContain( + app.app_status, + ); } }); it("should have distinct app ids and commands", () => { - const apps = appSandbox.data.apps!; + const apps = getApps(); const ids = apps.map((a) => a.id); const commands = apps.map((a) => a.command); @@ -351,31 +371,30 @@ describe("Sandbox", () => { }); it("should stop one app without affecting the other", async () => { - const apps = appSandbox.data.apps!; - const firstId = apps[0]!.id!; - const secondId = apps[1]!.id!; + const firstId = getAppId(0); + const secondId = getAppId(1); await appSandbox.stopApp(firstId); const stopped = appSandbox.data.apps?.find((a) => a.id === firstId); const other = appSandbox.data.apps?.find((a) => a.id === secondId); expect(stopped).toBeDefined(); - expect(["ENDED", "NONE"]).toContain(stopped!.app_status); + expect(["ENDED", "NONE"]).toContain(stopped?.app_status); expect(other).toBeDefined(); - expect(other!.app_status).toBe("RUNNING"); + expect(other?.app_status).toBe("RUNNING"); }); it("should start a stopped app", async () => { - const appId = appSandbox.data.apps![0]!.id!; + const appId = getAppId(0); await appSandbox.startApp(appId); const app = appSandbox.data.apps?.find((a) => a.id === appId); expect(app).toBeDefined(); - expect(app!.app_status).toBe("RUNNING"); + expect(app?.app_status).toBe("RUNNING"); }); it("should get app logs", async () => { - const appId = appSandbox.data.apps![0]!.id!; + const appId = getAppId(0); const logs = await appSandbox.getAppLogs(appId); expect(logs).toBeDefined(); From a2b7572ef2ae2b5b1ca2c37afd08e7aa3baa7e74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Ga=C5=84czarczyk?= Date: Mon, 9 Mar 2026 14:37:48 +0100 Subject: [PATCH 09/23] chore: add apps example to example:all script --- examples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/package.json b/examples/package.json index d2cc26d..e401073 100644 --- a/examples/package.json +++ b/examples/package.json @@ -12,7 +12,7 @@ "example:filesystem": "NODE_OPTIONS='--enable-source-maps' dotenvx run -q -- tsx src/filesystem.ts", "example:apps": "NODE_OPTIONS='--enable-source-maps' dotenvx run -q -- tsx src/apps.ts", "example:error-handling": "NODE_OPTIONS='--enable-source-maps' dotenvx run -q -- tsx src/error-handling.ts", - "example:all": "concurrently \"pnpm example:ping\" \"pnpm example:list\" \"pnpm example:lifecycle\" \"pnpm example:streaming\" \"pnpm example:filesystem\" \"pnpm example:error-handling\"" + "example:all": "concurrently \"pnpm example:ping\" \"pnpm example:list\" \"pnpm example:lifecycle\" \"pnpm example:streaming\" \"pnpm example:filesystem\" \"pnpm example:apps\" \"pnpm example:error-handling\"" }, "keywords": [], "author": "", From df1dc399a9f6a8e1f1b12af2f05a5e935ce71f0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Ga=C5=84czarczyk?= Date: Tue, 10 Mar 2026 08:27:35 +0100 Subject: [PATCH 10/23] fix: increase afterAll destroy timeout from 30s to 60s --- tests/command.test.ts | 2 +- tests/create-options.test.ts | 2 +- tests/filesystem.test.ts | 2 +- tests/sandbox.test.ts | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/command.test.ts b/tests/command.test.ts index d2a13fe..7e14299 100644 --- a/tests/command.test.ts +++ b/tests/command.test.ts @@ -18,7 +18,7 @@ describe("Command", () => { afterAll(async () => { await sandbox?.destroy(); - }, 30_000); + }, 60_000); describe("data getter", () => { it("should return command response data", async () => { diff --git a/tests/create-options.test.ts b/tests/create-options.test.ts index 6b179f7..3aad4f0 100644 --- a/tests/create-options.test.ts +++ b/tests/create-options.test.ts @@ -11,7 +11,7 @@ describe("Sandbox.create options", () => { afterAll(async () => { // Clean up all created sandboxes await Promise.all(sandboxes.map((s) => s.destroy())); - }); + }, 60_000); it("should create sandbox with default options", async () => { const sandbox = await Sandbox.create({ diff --git a/tests/filesystem.test.ts b/tests/filesystem.test.ts index 3de0118..d458805 100644 --- a/tests/filesystem.test.ts +++ b/tests/filesystem.test.ts @@ -22,7 +22,7 @@ describe("FileSystem", () => { afterAll(async () => { await sandbox?.destroy(); - }, 30_000); + }, 60_000); describe("FileSystem.forSandbox()", () => { it("should create FileSystem instance for sandbox ID", () => { diff --git a/tests/sandbox.test.ts b/tests/sandbox.test.ts index a35017b..dfc860e 100644 --- a/tests/sandbox.test.ts +++ b/tests/sandbox.test.ts @@ -24,7 +24,7 @@ describe("Sandbox", () => { afterAll(async () => { await sandbox?.destroy(); - }, 30_000); + }, 60_000); describe("data properties", () => { it("should have id", () => { @@ -346,7 +346,7 @@ describe("Sandbox", () => { afterAll(async () => { await appSandbox?.destroy(); - }, 30_000); + }, 60_000); it("should have multiple apps in response", () => { const apps = getApps(); From 32c238a0d8d47126722162989524684dccbab5ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Ga=C5=84czarczyk?= Date: Wed, 11 Mar 2026 09:25:22 +0100 Subject: [PATCH 11/23] Revert "fix: increase afterAll destroy timeout from 30s to 60s" This reverts commit df1dc399a9f6a8e1f1b12af2f05a5e935ce71f0d. --- tests/command.test.ts | 2 +- tests/create-options.test.ts | 2 +- tests/filesystem.test.ts | 2 +- tests/sandbox.test.ts | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/command.test.ts b/tests/command.test.ts index 7e14299..d2a13fe 100644 --- a/tests/command.test.ts +++ b/tests/command.test.ts @@ -18,7 +18,7 @@ describe("Command", () => { afterAll(async () => { await sandbox?.destroy(); - }, 60_000); + }, 30_000); describe("data getter", () => { it("should return command response data", async () => { diff --git a/tests/create-options.test.ts b/tests/create-options.test.ts index 3aad4f0..6b179f7 100644 --- a/tests/create-options.test.ts +++ b/tests/create-options.test.ts @@ -11,7 +11,7 @@ describe("Sandbox.create options", () => { afterAll(async () => { // Clean up all created sandboxes await Promise.all(sandboxes.map((s) => s.destroy())); - }, 60_000); + }); it("should create sandbox with default options", async () => { const sandbox = await Sandbox.create({ diff --git a/tests/filesystem.test.ts b/tests/filesystem.test.ts index d458805..3de0118 100644 --- a/tests/filesystem.test.ts +++ b/tests/filesystem.test.ts @@ -22,7 +22,7 @@ describe("FileSystem", () => { afterAll(async () => { await sandbox?.destroy(); - }, 60_000); + }, 30_000); describe("FileSystem.forSandbox()", () => { it("should create FileSystem instance for sandbox ID", () => { diff --git a/tests/sandbox.test.ts b/tests/sandbox.test.ts index dfc860e..a35017b 100644 --- a/tests/sandbox.test.ts +++ b/tests/sandbox.test.ts @@ -24,7 +24,7 @@ describe("Sandbox", () => { afterAll(async () => { await sandbox?.destroy(); - }, 60_000); + }, 30_000); describe("data properties", () => { it("should have id", () => { @@ -346,7 +346,7 @@ describe("Sandbox", () => { afterAll(async () => { await appSandbox?.destroy(); - }, 60_000); + }, 30_000); it("should have multiple apps in response", () => { const apps = getApps(); From d9c67784d04966539a373d12205532f2027e7b91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Ga=C5=84czarczyk?= Date: Wed, 11 Mar 2026 09:34:58 +0100 Subject: [PATCH 12/23] bumped timeout for the ubuntu 22.04 sandbox startup --- tests/create-options.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/create-options.test.ts b/tests/create-options.test.ts index 6b179f7..90e69d8 100644 --- a/tests/create-options.test.ts +++ b/tests/create-options.test.ts @@ -44,7 +44,7 @@ describe("Sandbox.create options", () => { sandboxes.push(sandbox); expect(sandbox.data.os).toBe("ubuntu:22.04"); - }); + }, 120_000); it("should reject duplicate identifier", async () => { const identifier = `duplicate_test_${Date.now()}`; From d6108fce37d38d7dfa74292074e849876c3a5354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Ga=C5=84czarczyk?= Date: Wed, 11 Mar 2026 09:36:48 +0100 Subject: [PATCH 13/23] Revert "bumped timeout for the ubuntu 22.04 sandbox startup" This reverts commit d9c67784d04966539a373d12205532f2027e7b91. --- tests/create-options.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/create-options.test.ts b/tests/create-options.test.ts index 90e69d8..6b179f7 100644 --- a/tests/create-options.test.ts +++ b/tests/create-options.test.ts @@ -44,7 +44,7 @@ describe("Sandbox.create options", () => { sandboxes.push(sandbox); expect(sandbox.data.os).toBe("ubuntu:22.04"); - }, 120_000); + }); it("should reject duplicate identifier", async () => { const identifier = `duplicate_test_${Date.now()}`; From afc64628fbe520181b087e874789b9665b96bb00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Ga=C5=84czarczyk?= Date: Wed, 20 May 2026 09:24:51 +0200 Subject: [PATCH 14/23] Sandbox timeout + fetch (#9) * 0.1.5 - Sandbox apps (#8) * feat: add sandbox apps array API support * feat: regenerate types from openapi spec and wire up app endpoints * feat: add per-app methods to Sandbox entity and handle STALE setup status * test: add integration tests for sandbox app start/stop/logs * test: use multiple apps and verify independent start/stop behavior * feat: add apps example demonstrating per-app start/stop/logs * docs: add apps section to README * fix: resolve lint warnings in app tests and example * chore: add apps example to example:all script * fix: increase afterAll destroy timeout from 30s to 60s * Revert "fix: increase afterAll destroy timeout from 30s to 60s" This reverts commit df1dc399a9f6a8e1f1b12af2f05a5e935ce71f0d. * bumped timeout for the ubuntu 22.04 sandbox startup * Revert "bumped timeout for the ubuntu 22.04 sandbox startup" This reverts commit d9c67784d04966539a373d12205532f2027e7b91. * chore(release): bump version to 0.1.5 * chore: set pnpm minimumReleaseAge to 2 days * fix: patch openapi spec for missing String schema * fix: drop circular Target types from generated schemas * chore: bump @hey-api/openapi-ts to 0.97.2 * feat: regenerate openapi types with timeout and fetch fields * refactor: migrate BuddyApiClient to per-part body/path/query validation * test: add unit tests for timeout and fetch in addSandbox * test: add integration tests for Sandbox.create timeout and fetch * docs: document timeout and add fetch section to README * fix: drop circular Target types from generated schemas * test: increase ubuntu 22.04 sandbox creation timeout to 120s --- README.md | 22 + openapi-ts.config.ts | 22 +- package.json | 4 +- pnpm-lock.yaml | 205 +- pnpm-workspace.yaml | 2 + scripts/cleanup-schemas.ts | 29 +- src/api/openapi/transformers.gen.ts | 29 +- src/api/openapi/types.gen.ts | 927 ++++-- src/api/openapi/zod.gen.ts | 4081 ++++++++++++++------------- src/core/buddy-api-client.ts | 245 +- tests/buddy-api-client.test.ts | 73 + tests/create-options.test.ts | 34 + 12 files changed, 3263 insertions(+), 2410 deletions(-) diff --git a/README.md b/README.md index 4390f56..395b28c 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ const sandbox = await Sandbox.create({ os: "ubuntu:24.04", first_boot_commands: "apt-get update && apt-get install -y curl", apps: ["node server.js", "python worker.py"], + timeout: 600, // auto-stop after 10 minutes of inactivity }); // List apps @@ -73,6 +74,27 @@ const { logs } = await sandbox.getAppLogs(appId); console.log(logs); ``` +## Fetching repositories and artifacts + +Use `fetch` to clone repositories or download artifacts into the sandbox on +first boot. Each entry sets a `type` (`PROJECT_REPO`, `PUBLIC_REPO`, or +`ARTIFACT`) plus the fields relevant to it. + +```typescript +await Sandbox.create({ + identifier: "my-sandbox", + fetch: [ + { + type: "PUBLIC_REPO", + repository: "https://github.com/octocat/Hello-World", + ref: "master", + path: "/workspace/hello", + build_command: "echo built", + }, + ], +}); +``` + ## Regions Configure the API region: diff --git a/openapi-ts.config.ts b/openapi-ts.config.ts index 92adb7a..970bacc 100644 --- a/openapi-ts.config.ts +++ b/openapi-ts.config.ts @@ -6,8 +6,24 @@ if (!schemaUrl) { throw new Error("SCHEMA_URL environment variable is required"); } -export default defineConfig({ - input: schemaUrl, +// Spec has dangling `$ref: '#/components/schemas/String'` on the Crawl/VisualTest +// getToken endpoints; inject a placeholder so hey-api can resolve them. +const fetchPatchedSpec = async () => { + const response = await fetch(schemaUrl); + if (!response.ok) { + throw new Error(`Failed to fetch ${schemaUrl}: ${response.status}`); + } + const spec = (await response.json()) as { + components?: { schemas?: Record }; + }; + spec.components ??= {}; + spec.components.schemas ??= {}; + spec.components.schemas["String"] ??= { type: "string" }; + return spec; +}; + +export default defineConfig(async () => ({ + input: await fetchPatchedSpec(), output: { path: "src/api/openapi", postProcess: [ @@ -42,4 +58,4 @@ export default defineConfig({ exportFromIndex: false, }, ], -}); +})); diff --git a/package.json b/package.json index 40777d7..227d7c1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@buddy-works/sandbox-sdk", - "version": "0.1.4", + "version": "0.1.5", "type": "module", "description": "TypeScript SDK for managing sandboxes through the Buddy API", "main": "./dist/index.mjs", @@ -49,7 +49,7 @@ "devDependencies": { "@biomejs/biome": "2.3.11", "@dotenvx/dotenvx": "catalog:", - "@hey-api/openapi-ts": "0.90.3", + "@hey-api/openapi-ts": "0.97.2", "@tsconfig/node20": "catalog:", "@tsconfig/strictest": "catalog:", "@types/node": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ead93f..57706dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,8 +43,8 @@ importers: specifier: 'catalog:' version: 1.51.4 '@hey-api/openapi-ts': - specifier: 0.90.3 - version: 0.90.3(typescript@5.9.3) + specifier: 0.97.2 + version: 0.97.2(typescript@5.9.3) '@tsconfig/node20': specifier: 'catalog:' version: 20.1.8 @@ -359,22 +359,30 @@ packages: cpu: [x64] os: [win32] - '@hey-api/codegen-core@0.5.2': - resolution: {integrity: sha512-88cqrrB2cLXN8nMOHidQTcVOnZsJ5kebEbBefjMCifaUCwTA30ouSSWvTZqrOX4O104zjJyu7M8Gcv/NNYQuaA==} - engines: {node: '>=20.19.0'} - peerDependencies: - typescript: '>=5.5.3' + '@hey-api/codegen-core@0.8.1': + resolution: {integrity: sha512-Iciv2vUCJTW9lWM/ROvyZLblmcbYJHPuXfzb1SzeDVVn4xEXu2ilLU1pq3fn+09FZ/Y0P7VyvRE47UDU6om8xA==} + engines: {node: '>=22.13.0'} - '@hey-api/json-schema-ref-parser@1.2.2': - resolution: {integrity: sha512-oS+5yAdwnK20lSeFO1d53Ku+yaGCsY8PcrmSq2GtSs3bsBfRnHAbpPKSVzQcaxAOrzj5NB+f34WhZglVrNayBA==} - engines: {node: '>= 16'} + '@hey-api/json-schema-ref-parser@1.4.2': + resolution: {integrity: sha512-ZhCFSKI2ipZHEbgmtUHdyddvRU3wJ4elgCfYUC7T7hZa4EivSrVflTQf2w+v3TuaYxR1Y2V2kq3otqTttrrK8Q==} + engines: {node: '>=22.13.0'} - '@hey-api/openapi-ts@0.90.3': - resolution: {integrity: sha512-9UT347hVQaph/jwAJJI2l+istWDLw540PTIHUevq9xzRYnHAxC1Aprtv4CYoWkeFv9aMcMrmAbNH7ANPoxEK2A==} - engines: {node: '>=20.19.0'} + '@hey-api/openapi-ts@0.97.2': + resolution: {integrity: sha512-nA+y0/I5O9loQMeJKumi6BQ40/Y71N0hIMmXZ/I7rh8jEOzYxSxmf5a4TBEI2Ap4RAfZyh7RJzJfVzT98KUYQQ==} + engines: {node: '>=22.13.0'} hasBin: true peerDependencies: - typescript: '>=5.5.3' + typescript: '>=5.5.3 || >=6.0.0 || 6.0.1-rc' + + '@hey-api/shared@0.4.4': + resolution: {integrity: sha512-UZgaQNEdo/OSGLeNXhSv0VQTHQQm5Q2mHOuoYhFPJkNvLVrz7KZtGdKR8O4QPrhyblshxY+caJli08WKM0gREg==} + engines: {node: '>=22.13.0'} + + '@hey-api/spec-types@0.2.0': + resolution: {integrity: sha512-ibQ8Is7evMavzr8GNyJCcTg975d8DpaMUyLmOrQ85UBdy1l6t1KuRAwgChAbesJsIlNV6gjmlXruWyegDX18Fg==} + + '@hey-api/types@0.1.4': + resolution: {integrity: sha512-thWfawrDIP7wSI9ioT13I5soaaqB5vAPIiZmgD8PbeEVKNrkonc0N/Sjj97ezl7oQgusZmaNphGdMKipPO6IBg==} '@inquirer/ansi@1.0.2': resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} @@ -427,6 +435,10 @@ packages: '@jsdevtools/ono@7.1.3': resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} + '@lukeed/ms@2.0.2': + resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} + engines: {node: '>=8'} + '@mswjs/interceptors@0.40.0': resolution: {integrity: sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==} engines: {node: '>=18'} @@ -875,8 +887,8 @@ packages: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} - c12@3.3.3: - resolution: {integrity: sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q==} + c12@3.3.4: + resolution: {integrity: sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==} peerDependencies: magicast: '*' peerDependenciesMeta: @@ -899,9 +911,6 @@ packages: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} - citty@0.1.6: - resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} - cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} @@ -925,8 +934,8 @@ packages: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} - commander@14.0.2: - resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} concurrently@9.2.1: @@ -934,12 +943,8 @@ packages: engines: {node: '>=18'} hasBin: true - confbox@0.2.2: - resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} - - consola@3.4.2: - resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} - engines: {node: ^14.18.0 || >=16.10.0} + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} @@ -964,6 +969,9 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} @@ -971,6 +979,10 @@ packages: resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} engines: {node: '>=12'} + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + dts-resolver@2.1.3: resolution: {integrity: sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw==} engines: {node: '>=20.19.0'} @@ -1061,8 +1073,11 @@ packages: get-tsconfig@4.13.0: resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} - giget@2.0.0: - resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + + giget@3.2.0: + resolution: {integrity: sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==} hasBin: true glob-parent@5.1.2: @@ -1168,9 +1183,6 @@ packages: '@types/node': '>=18' typescript: '>=5.0.4 <7' - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1211,18 +1223,10 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - node-fetch-native@1.6.7: - resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} - npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} - nypm@0.6.2: - resolution: {integrity: sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==} - engines: {node: ^14.16.0 || >=16.10.0} - hasBin: true - object-treeify@1.1.33: resolution: {integrity: sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==} engines: {node: '>= 10'} @@ -1261,8 +1265,8 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - perfect-debounce@2.0.0: - resolution: {integrity: sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==} + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1292,8 +1296,8 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - rc9@2.1.2: - resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + rc9@3.0.1: + resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==} readdirp@5.0.0: resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} @@ -1357,6 +1361,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1820,33 +1829,56 @@ snapshots: '@esbuild/win32-x64@0.27.2': optional: true - '@hey-api/codegen-core@0.5.2(typescript@5.9.3)': + '@hey-api/codegen-core@0.8.1': dependencies: + '@hey-api/types': 0.1.4 ansi-colors: 4.1.3 + c12: 3.3.4 color-support: 1.1.3 - typescript: 5.9.3 + transitivePeerDependencies: + - magicast - '@hey-api/json-schema-ref-parser@1.2.2': + '@hey-api/json-schema-ref-parser@1.4.2': dependencies: '@jsdevtools/ono': 7.1.3 '@types/json-schema': 7.0.15 js-yaml: 4.1.1 - lodash: 4.17.21 - '@hey-api/openapi-ts@0.90.3(typescript@5.9.3)': + '@hey-api/openapi-ts@0.97.2(typescript@5.9.3)': dependencies: - '@hey-api/codegen-core': 0.5.2(typescript@5.9.3) - '@hey-api/json-schema-ref-parser': 1.2.2 + '@hey-api/codegen-core': 0.8.1 + '@hey-api/json-schema-ref-parser': 1.4.2 + '@hey-api/shared': 0.4.4 + '@hey-api/spec-types': 0.2.0 + '@hey-api/types': 0.1.4 + '@lukeed/ms': 2.0.2 ansi-colors: 4.1.3 - c12: 3.3.3 color-support: 1.1.3 - commander: 14.0.2 - open: 11.0.0 - semver: 7.7.3 + commander: 14.0.3 + get-tsconfig: 4.14.0 typescript: 5.9.3 transitivePeerDependencies: - magicast + '@hey-api/shared@0.4.4': + dependencies: + '@hey-api/codegen-core': 0.8.1 + '@hey-api/json-schema-ref-parser': 1.4.2 + '@hey-api/spec-types': 0.2.0 + '@hey-api/types': 0.1.4 + ansi-colors: 4.1.3 + cross-spawn: 7.0.6 + open: 11.0.0 + semver: 7.7.4 + transitivePeerDependencies: + - magicast + + '@hey-api/spec-types@0.2.0': + dependencies: + '@hey-api/types': 0.1.4 + + '@hey-api/types@0.1.4': {} + '@inquirer/ansi@1.0.2': {} '@inquirer/confirm@5.1.21(@types/node@20.19.29)': @@ -1891,6 +1923,8 @@ snapshots: '@jsdevtools/ono@7.1.3': {} + '@lukeed/ms@2.0.2': {} + '@mswjs/interceptors@0.40.0': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -2219,20 +2253,20 @@ snapshots: dependencies: run-applescript: 7.1.0 - c12@3.3.3: + c12@3.3.4: dependencies: chokidar: 5.0.0 - confbox: 0.2.2 - defu: 6.1.4 - dotenv: 17.2.3 + confbox: 0.2.4 + defu: 6.1.7 + dotenv: 17.4.2 exsolve: 1.0.8 - giget: 2.0.0 + giget: 3.2.0 jiti: 2.6.1 ohash: 2.0.11 pathe: 2.0.3 - perfect-debounce: 2.0.0 + perfect-debounce: 2.1.0 pkg-types: 2.3.0 - rc9: 2.1.2 + rc9: 3.0.1 cac@6.7.14: {} @@ -2247,10 +2281,6 @@ snapshots: dependencies: readdirp: 5.0.0 - citty@0.1.6: - dependencies: - consola: 3.4.2 - cli-width@4.1.0: {} cliui@8.0.1: @@ -2269,7 +2299,7 @@ snapshots: commander@11.1.0: {} - commander@14.0.2: {} + commander@14.0.3: {} concurrently@9.2.1: dependencies: @@ -2280,9 +2310,7 @@ snapshots: tree-kill: 1.2.2 yargs: 17.7.2 - confbox@0.2.2: {} - - consola@3.4.2: {} + confbox@0.2.4: {} cookie@1.1.1: {} @@ -2303,10 +2331,14 @@ snapshots: defu@6.1.4: {} + defu@6.1.7: {} + destr@2.0.5: {} dotenv@17.2.3: {} + dotenv@17.4.2: {} + dts-resolver@2.1.3(oxc-resolver@11.16.3): optionalDependencies: oxc-resolver: 11.16.3 @@ -2414,14 +2446,11 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - giget@2.0.0: + get-tsconfig@4.14.0: dependencies: - citty: 0.1.6 - consola: 3.4.2 - defu: 6.1.4 - node-fetch-native: 1.6.7 - nypm: 0.6.2 - pathe: 2.0.3 + resolve-pkg-maps: 1.0.0 + + giget@3.2.0: {} glob-parent@5.1.2: dependencies: @@ -2498,8 +2527,6 @@ snapshots: typescript: 5.9.3 zod: 4.3.5 - lodash@4.17.21: {} - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2546,20 +2573,10 @@ snapshots: nanoid@3.3.11: {} - node-fetch-native@1.6.7: {} - npm-run-path@4.0.1: dependencies: path-key: 3.1.1 - nypm@0.6.2: - dependencies: - citty: 0.1.6 - consola: 3.4.2 - pathe: 2.0.3 - pkg-types: 2.3.0 - tinyexec: 1.0.2 - object-treeify@1.1.33: {} obug@2.1.1: {} @@ -2614,7 +2631,7 @@ snapshots: pathe@2.0.3: {} - perfect-debounce@2.0.0: {} + perfect-debounce@2.1.0: {} picocolors@1.1.1: {} @@ -2624,7 +2641,7 @@ snapshots: pkg-types@2.3.0: dependencies: - confbox: 0.2.2 + confbox: 0.2.4 exsolve: 1.0.8 pathe: 2.0.3 @@ -2640,9 +2657,9 @@ snapshots: queue-microtask@1.2.3: {} - rc9@2.1.2: + rc9@3.0.1: dependencies: - defu: 6.1.4 + defu: 6.1.7 destr: 2.0.5 readdirp@5.0.0: {} @@ -2733,6 +2750,8 @@ snapshots: semver@7.7.3: {} + semver@7.7.4: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 899e4a2..2387aca 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,6 +2,8 @@ packages: - . - examples +minimumReleaseAge: 2880 + catalog: '@dotenvx/dotenvx': 1.51.4 '@tsconfig/node20': '20' diff --git a/scripts/cleanup-schemas.ts b/scripts/cleanup-schemas.ts index 4cecca6..a19a4d0 100644 --- a/scripts/cleanup-schemas.ts +++ b/scripts/cleanup-schemas.ts @@ -1,12 +1,37 @@ import { readFileSync, writeFileSync } from "node:fs"; -// Remove ClientOptions type from types.gen.ts (contains hardcoded base URL) const typesFile = "src/api/openapi/types.gen.ts"; +const zodFile = "src/api/openapi/zod.gen.ts"; + +const dropBlocks = (content: string, pattern: RegExp) => + content + .split(/\n\n/) + .filter((block) => !pattern.test(block)) + .join("\n\n"); + let typesContent = readFileSync(typesFile, "utf8"); + +// Remove ClientOptions type from types.gen.ts (contains hardcoded base URL) typesContent = typesContent.replace( /export type ClientOptions[\s\S]*?^};\n\n/m, "", ); + +typesContent = dropBlocks(typesContent, /^export type Target/m); +typesContent = typesContent.replace( + /Array/g, + "Array", +); + writeFileSync(typesFile, typesContent); -console.log("Removed ClientOptions from generated schemas"); +let zodContent = readFileSync(zodFile, "utf8"); +zodContent = dropBlocks(zodContent, /^export const zTarget/m); +zodContent = zodContent.replace( + /z\.array\(zTargetView(?:Writable)?\)/g, + "z.array(z.unknown())", +); + +writeFileSync(zodFile, zodContent); + +console.log("Cleaned up generated schemas"); diff --git a/src/api/openapi/transformers.gen.ts b/src/api/openapi/transformers.gen.ts index edc8b77..91964ed 100644 --- a/src/api/openapi/transformers.gen.ts +++ b/src/api/openapi/transformers.gen.ts @@ -44,6 +44,23 @@ export const getWorkspaceResponseTransformer = async ( return data; }; +const shortProjectViewSchemaResponseTransformer = (data: any) => { + if (data.create_date) { + data.create_date = new Date(data.create_date); + } + return data; +}; + +const targetViewSchemaResponseTransformer = (data: any) => { + if (data.project) { + data.project = shortProjectViewSchemaResponseTransformer(data.project); + } + if (data.pipeline) { + data.pipeline = shortPipelineViewSchemaResponseTransformer(data.pipeline); + } + return data; +}; + const pipelineEventViewSchemaResponseTransformer = (data: any) => { if (data.start_date) { data.start_date = new Date(data.start_date); @@ -51,6 +68,11 @@ const pipelineEventViewSchemaResponseTransformer = (data: any) => { if (data.delay) { data.delay = BigInt(data.delay.toString()); } + if (data.targets) { + data.targets = data.targets.map((item: any) => + targetViewSchemaResponseTransformer(item), + ); + } return data; }; @@ -112,13 +134,6 @@ export const updateIntegrationResponseTransformer = async ( return data; }; -const shortProjectViewSchemaResponseTransformer = (data: any) => { - if (data.create_date) { - data.create_date = new Date(data.create_date); - } - return data; -}; - const projectsViewSchemaResponseTransformer = (data: any) => { if (data.projects) { data.projects = data.projects.map((item: any) => diff --git a/src/api/openapi/types.gen.ts b/src/api/openapi/types.gen.ts index 147f2d5..4a83851 100644 --- a/src/api/openapi/types.gen.ts +++ b/src/api/openapi/types.gen.ts @@ -224,6 +224,10 @@ export type UpdateIntegrationRequest = { * The ATOP service URL */ atop_url?: string; + /** + * Set to `true` to disable the integration. Disabled integrations cannot be used in actions + */ + disabled?: boolean; }; export type GroupPermissionView = { @@ -237,6 +241,7 @@ export type GroupPermissionView = { access_level?: | "DENIED" | "READ_ONLY" + | "USE_ONLY" | "BLIND" | "RUN_ONLY" | "READ_WRITE" @@ -244,8 +249,7 @@ export type GroupPermissionView = { | "DEFAULT" | "ALLOWED" | "STAGE" - | "COMMIT" - | "USE_ONLY"; + | "COMMIT"; }; export type UserPermissionView = { @@ -259,6 +263,7 @@ export type UserPermissionView = { access_level?: | "DENIED" | "READ_ONLY" + | "USE_ONLY" | "BLIND" | "RUN_ONLY" | "READ_WRITE" @@ -266,8 +271,7 @@ export type UserPermissionView = { | "DEFAULT" | "ALLOWED" | "STAGE" - | "COMMIT" - | "USE_ONLY"; + | "COMMIT"; }; /** @@ -280,6 +284,7 @@ export type IntegrationPermissionsView = { others?: | "DENIED" | "READ_ONLY" + | "USE_ONLY" | "BLIND" | "RUN_ONLY" | "READ_WRITE" @@ -287,8 +292,7 @@ export type IntegrationPermissionsView = { | "DEFAULT" | "ALLOWED" | "STAGE" - | "COMMIT" - | "USE_ONLY"; + | "COMMIT"; /** * List of specific users with their access levels */ @@ -303,6 +307,7 @@ export type IntegrationPermissionsView = { admins?: | "DENIED" | "READ_ONLY" + | "USE_ONLY" | "BLIND" | "RUN_ONLY" | "READ_WRITE" @@ -310,8 +315,7 @@ export type IntegrationPermissionsView = { | "DEFAULT" | "ALLOWED" | "STAGE" - | "COMMIT" - | "USE_ONLY"; + | "COMMIT"; }; /** @@ -319,7 +323,7 @@ export type IntegrationPermissionsView = { */ export type PipelineIdView = { /** - * The unique identifier of the pipeline + * The unique ID of the pipeline */ id: number; }; @@ -577,13 +581,344 @@ export type PipelinePropertyView = { value?: string; }; -export type PipelinePkgContextView = { +/** + * MSSQL authentication credentials + */ +export type MssqlAuthView = { + /** + * Authentication method. Default: `PASSWORD` + */ + method?: "PASSWORD"; + /** + * The MSSQL username + */ + username: string; + /** + * The MSSQL password + */ + password: string; +}; + +/** + * MongoDB authentication credentials + */ +export type MongoAuthView = { + /** + * Authentication method. Default: `PASSWORD` + */ + method?: "PASSWORD"; + /** + * The MongoDB username + */ + username: string; + /** + * The MongoDB password + */ + password: string; +}; + +/** + * PostgreSQL authentication credentials + */ +export type PostgresqlAuthView = { + /** + * Authentication method. Default: `PASSWORD` + */ + method?: "PASSWORD"; + /** + * The PostgreSQL username + */ + username: string; + /** + * The PostgreSQL password + */ + password: string; +}; + +/** + * MySQL authentication credentials + */ +export type MysqlAuthView = { + /** + * Authentication method. Default: `PASSWORD` + */ + method?: "PASSWORD"; + /** + * The MySQL username + */ + username: string; + /** + * The MySQL password + */ + password: string; +}; + +/** + * Define proxy servers' authentication method using the following parameters + */ +export type SshAuthView = { + /** + * Authentication method + */ + method: + | "PASSWORD" + | "SSH_KEY" + | "ASSETS_KEY" + | "PROXY_CREDENTIALS" + | "PROXY_KEY"; + /** + * The username required to connect to the server + */ + username?: string; + /** + * The password required to connect to the server. Required for `PASSWORD` method + */ + password?: string; + /** + * Name of the variable containing the private key. Required for `ASSETS_KEY` method + */ + asset?: string; + /** + * Passphrase for the SSH key + */ + passphrase?: string; + /** + * The private SSH key. Required when method is `SSH_KEY` + */ + key?: string; + /** + * Path to the key on proxy server. Required for method `PROXY_KEY` + */ + key_path?: string; +}; + +/** + * Kubernetes cluster authentication method + */ +export type K8sAuthView = { + /** + * Authentication method + */ + method: "PASS" | "CERT" | "TOKEN"; + /** + * Username to the Kubernetes cluster. Required if the `auth.method` is `BASIC` + */ + username?: string; + /** + * Password to the Kubernetes cluster. Required if the `auth.method` is `BASIC` + */ + password?: string; + /** + * Kuberenetes certificate authority. Required if the `auth.method` is `CERT` + */ + certificate_authority?: string; + /** + * Kuberenetes client certificate. Required if the `auth.method` is `CERT` + */ + client_certificate?: string; + /** + * Kuberenetes client key. Required if the `auth.method` is `CERT` + */ + client_key?: string; + /** + * Token for the Kubernetes cluster. Required if the `auth.method` is `TOKEN` + */ + token?: string; +}; + +/** + * Authentication details + */ +export type GitAuthView = { + /** + * The authentication for Git + */ + method: "HTTP" | "SSH_KEY" | "ASSETS_KEY" | "CURRENT"; + /** + * Username required to connect to the Git repository. Required when method is `HTTP` + */ + username?: string; + /** + * Password required to connect to the Git repository. Required when method is `HTTP` + */ + password?: string; + /** + * Name of the variable containing the private key. Required when method is `ASSETS_KEY` + */ + asset?: string; + /** + * The private SSH key. Required when method is `SSH_KEY` + */ + key?: string; +}; + +/** + * Authentication details + */ +export type FtpAuthView = { + /** + * The username required to connect to the server + */ + username: string; + /** + * The password required to connect to the server + */ + password: string; +}; + +/** + * Defines how the target can be used (as deployment target, proxy, or both) + */ +export type UseAsView = { + /** + * Whether the target can be used as a deployment target (default: `true`) + */ + target?: boolean; + /** + * Whether the target can be used as a proxy (default: `true`) + */ + proxy?: boolean; +}; + +/** + * List of specific sandboxes allowed to use this target + */ +export type AllowedSandboxView = { + /** + * Project name + */ + project: string; + /** + * Sandbox identifier + */ + sandbox: string; + /** + * Access level for the allowed sandbox: `DENIED`, `READ_ONLY`, `READ_WRITE` + */ + access_level?: + | "DENIED" + | "READ_ONLY" + | "USE_ONLY" + | "BLIND" + | "RUN_ONLY" + | "READ_WRITE" + | "MANAGE" + | "DEFAULT" + | "ALLOWED" + | "STAGE" + | "COMMIT"; +}; + +/** + * List of specific pipelines allowed to use this target + */ +export type AllowedPipelineView = { + /** + * Project name + */ + project: string; + /** + * Pipeline identifier + */ + pipeline: string; + /** + * Access level for the allowed pipeline. Project repository targets: `DENIED`, `READ_ONLY`, `READ_WRITE`. Other targets: `DENIED`, `USE_ONLY` + */ + access_level?: + | "DENIED" + | "READ_ONLY" + | "USE_ONLY" + | "BLIND" + | "RUN_ONLY" + | "READ_WRITE" + | "MANAGE" + | "DEFAULT" + | "ALLOWED" + | "STAGE" + | "COMMIT"; +}; + +/** + * Access permissions configuration + */ +export type PermissionsView = { + /** + * Access level for other workspace members + */ + others?: + | "DENIED" + | "READ_ONLY" + | "USE_ONLY" + | "BLIND" + | "RUN_ONLY" + | "READ_WRITE" + | "MANAGE" + | "DEFAULT" + | "ALLOWED" + | "STAGE" + | "COMMIT"; + /** + * List of specific users with their access levels + */ + users?: Array; + /** + * List of user groups with their access levels + */ + groups?: Array; +}; + +/** + * Short representation of an environment object + */ +export type ShortEnvironmentView = { + /** + * API endpoint to GET this object + */ + readonly url?: string; + /** + * Web URL to view this object in Buddy.works + */ + readonly html_url?: string; + /** + * The name of the environment + */ + name?: string; + /** + * The human-readable identifier of the environment + */ + identifier?: string; + /** + * The ID of the environment + */ + id?: number; + /** + * The scope level of the environment + */ + scope?: "PROJECT" | "WORKSPACE" | "ANY"; +}; + +export type PipelineEnvironmentContextView = { + /** + * A human-readable ID of the environment. Alphanumeric characters, underscores, and hyphens (hyphens cannot appear at the start or end). + */ + identifier?: string; + /** + * The list of tags associated with the environment + */ + tags?: Array; + /** + * The scope of the environment + */ + scope?: "PROJECT" | "WORKSPACE" | "ANY"; +}; + +export type PipelineArtifactContextView = { /** - * A human-readable ID of package + * A human-readable ID of artifact */ identifier?: string; /** - * The scope of the package + * The scope of the artifact */ scope?: "WORKSPACE" | "PROJECT" | "ENVIRONMENT" | "ANY"; }; @@ -601,11 +936,16 @@ export type PipelineEventView = { | "DELETE_REF" | "PULL_REQUEST" | "SCHEDULE" - | "PUBLISH_PACKAGE_VERSION" - | "DELETE_PACKAGE_VERSION" + | "PUBLISH_ARTIFACT_VERSION" + | "DELETE_ARTIFACT_VERSION" | "WEBHOOK" | "EMAIL" - | "CREATE_PACKAGE_VERSION"; + | "CREATE_ARTIFACT_VERSION" + | "ENVIRONMENT_CREATE" + | "ENVIRONMENT_DELETE" + | "SANDBOX_CREATED" + | "SANDBOX_DELETED" + | "SANDBOX_TIMED_OUT"; /** * The list of refs (branches/tags) that trigger the pipeline for push/ref events */ @@ -619,9 +959,13 @@ export type PipelineEventView = { */ branches?: Array; /** - * The list of packages that trigger the pipeline + * The list of artifacts that trigger the pipeline + */ + artifacts?: Array; + /** + * The list of environments that trigger the pipeline */ - packages?: Array; + environments?: Array; /** * The start date for scheduled events (type `SCHEDULE`) */ @@ -650,6 +994,10 @@ export type PipelineEventView = { * The list of allowed email addresses that can trigger the pipeline via email (type `EMAIL`) */ whitelist?: Array; + /** + * The list of sandbox targets that trigger the pipeline for sandbox events (type `SANDBOX_CREATED`, `SANDBOX_DELETED`, `SANDBOX_TIMED_OUT`) + */ + targets?: Array; }; /** @@ -665,7 +1013,7 @@ export type ShortPipelineView = { */ readonly html_url?: string; /** - * The unique identifier of the pipeline + * The unique ID of the pipeline */ id?: number; /** @@ -773,6 +1121,10 @@ export type ShortPipelineView = { * If set to true, all refs will be fetched from the repository */ fetch_all_refs?: boolean; + /** + * If set to true, LFS files will be fetched from the external repository during pipeline runs + */ + fetch_lfs?: boolean; /** * If set to true, the pipeline will fail on environment preparation warnings */ @@ -855,6 +1207,40 @@ export type ShortPipelineView = { folder?: string; }; +/** + * Short representation of a project + */ +export type ShortProjectView = { + /** + * API endpoint to GET this object + */ + readonly url?: string; + /** + * Web URL to view this object in Buddy.works + */ + readonly html_url?: string; + /** + * The human-readable ID of the project + */ + name?: string; + /** + * The Name of the project + */ + display_name: string; + /** + * The status of the project + */ + status?: string; + /** + * Indicates if this is a public project + */ + access?: "PRIVATE" | "PUBLIC"; + /** + * The creation date of the project + */ + create_date?: Date; +}; + /** * The integration to use for authentication */ @@ -933,7 +1319,9 @@ export type IntegrationView = { | "ONE_LOGIN" | "OKTA" | "CONTENTFUL" - | "JIRA"; + | "JIRA" + | "NPM_REGISTRY" + | "ANTHROPIC"; /** * The authentication method used by the integration */ @@ -989,6 +1377,10 @@ export type IntegrationView = { * List of specific pipelines allowed to use this integration */ allowed_pipelines?: Array; + /** + * Set to `true` to disable the integration. Disabled integrations cannot be used in actions + */ + disabled?: boolean; }; export type IdsView = { @@ -1017,17 +1409,37 @@ export type IdsView = { */ environment_id?: string; /** - * The ID of the package + * The ID of the artifact */ - pkg_id?: string; + artifact_id?: string; /** - * The ID of the package version + * The ID of the artifact version */ - pkg_version_id?: string; + artifact_version_id?: string; /** * The ID of the sandbox */ sandbox_id?: string; + /** + * The ID of the unit test suite + */ + unit_test_suite_id?: string; + /** + * The ID of the visual test suite + */ + visual_test_suite_id?: string; + /** + * The ID of the crawl suite + */ + crawl_suite_id?: string; + /** + * The ID of the distribution + */ + distribution_id?: string; + /** + * The ID of the route + */ + route_id?: string; }; export type AddWorkspaceMemberRequest = { @@ -1209,6 +1621,10 @@ export type AddIntegrationRequest = { * The ATOP service URL */ atop_url?: string; + /** + * Set to `true` to disable the integration. Disabled integrations cannot be used in actions + */ + disabled?: boolean; /** * The type of integration */ @@ -1263,7 +1679,9 @@ export type AddIntegrationRequest = { | "ONE_LOGIN" | "OKTA" | "CONTENTFUL" - | "JIRA"; + | "JIRA" + | "NPM_REGISTRY" + | "ANTHROPIC"; /** * The scope of the integration */ @@ -1392,7 +1810,9 @@ export type IntegrationIdView = { | "ONE_LOGIN" | "OKTA" | "CONTENTFUL" - | "JIRA"; + | "JIRA" + | "NPM_REGISTRY" + | "ANTHROPIC"; /** * The authentication method used by the integration */ @@ -1513,114 +1933,51 @@ export type ProjectView = { */ custom_repo_url?: string; /** - * Username used to authorize access to the git repository. Required when adding the project integrated with custom git repository - */ - custom_repo_user?: string; - /** - * Password used to authorize access to the git repository. Required when adding the project integrated with custom git repository and the provided `custom_repo_url` is the HTTPS url - */ - custom_repo_pass?: string; - /** - * The ID of the private SSH key used to authorize access to the git repository. Required when adding the project integrated with private git server by SSH url - */ - custom_repo_ssh_key_id?: number; - created_by?: MemberView; - /** - * The HTTP repository URL - */ - http_repository?: string; - /** - * The SSH repository URL - */ - ssh_repository?: string; - /** - * The default branch name - */ - default_branch?: string; - integration?: IntegrationIdView; - /** - * Defines whether the submodules are fetched during the runs in this project - */ - fetch_submodules?: boolean; - /** - * Name of the key that will be used to authorize while fetching the submodules. Required when `fetch_submodules` is set to `true` - */ - fetch_submodules_env_key?: string; - /** - * Enables/disables pull requests in the project. Available only for projects synchronized with GitHub or GitHub Enterprise repository - */ - allow_pull_requests?: boolean; - /** - * If set to true, the default branch will be updated from GitHub/GitLab/Bitbucket. - */ - update_default_branch_from_external?: boolean; - /** - * If set to true, the project is created without any repository attached. - */ - without_repository?: boolean; -}; - -/** - * Access permissions configuration - */ -export type PermissionsView = { - /** - * Access level for other workspace members - */ - others?: - | "DENIED" - | "READ_ONLY" - | "BLIND" - | "RUN_ONLY" - | "READ_WRITE" - | "MANAGE" - | "DEFAULT" - | "ALLOWED" - | "STAGE" - | "COMMIT" - | "USE_ONLY"; - /** - * List of specific users with their access levels + * Username used to authorize access to the git repository. Required when adding the project integrated with custom git repository */ - users?: Array; + custom_repo_user?: string; /** - * List of user groups with their access levels + * Password used to authorize access to the git repository. Required when adding the project integrated with custom git repository and the provided `custom_repo_url` is the HTTPS url */ - groups?: Array; -}; - -/** - * Short representation of a project - */ -export type ShortProjectView = { + custom_repo_pass?: string; /** - * API endpoint to GET this object + * The ID of the private SSH key used to authorize access to the git repository. Required when adding the project integrated with private git server by SSH url */ - readonly url?: string; + custom_repo_ssh_key_id?: number; + created_by?: MemberView; /** - * Web URL to view this object in Buddy.works + * The HTTP repository URL */ - readonly html_url?: string; + http_repository?: string; /** - * The human-readable ID of the project + * The SSH repository URL */ - name?: string; + ssh_repository?: string; /** - * The Name of the project + * The default branch name */ - display_name: string; + default_branch?: string; + integration?: IntegrationIdView; /** - * The status of the project + * Defines whether the submodules are fetched during the runs in this project */ - status?: string; + fetch_submodules?: boolean; /** - * Indicates if this is a public project + * Name of the key that will be used to authorize while fetching the submodules. Required when `fetch_submodules` is set to `true` */ - access?: "PRIVATE" | "PUBLIC"; + fetch_submodules_env_key?: string; /** - * The creation date of the project + * Enables/disables pull requests in the project. Available only for projects synchronized with GitHub or GitHub Enterprise repository */ - create_date?: Date; + allow_pull_requests?: boolean; + /** + * If set to true, the default branch will be updated from GitHub/GitLab/Bitbucket. + */ + update_default_branch_from_external?: boolean; + /** + * If set to true, the project is created without any repository attached. + */ + without_repository?: boolean; }; export type UpdateSandboxRequest = { @@ -1661,6 +2018,10 @@ export type UpdateSandboxRequest = { * The list of apps (run commands) for the sandbox */ apps?: Array; + /** + * The list of items (repositories and artifacts) to fetch into the sandbox + */ + fetch?: Array; /** * The timeout in seconds after which the sandbox will be automatically stopped */ @@ -1725,15 +2086,15 @@ export type AddVariableInObjectRequest = { */ defaults?: string; /** - * Specifies where to copy the file on each run. Set if `type` is `SSH_KEY` + * Specifies where to copy the file on each run. Set if `type` is `FILE`, `SSH_KEY`, `SSH_PUBLIC_KEY`, `IOS_KEYCHAIN`, or `IOS_PROVISION_PROFILES` */ file_path?: string; /** - * File permission set on copy to a container on each run. Set if `type` is `SSH_KEY` + * File permission set on copy to a container on each run. Set if `type` is `FILE`, `SSH_KEY`, `SSH_PUBLIC_KEY`, `IOS_KEYCHAIN`, or `IOS_PROVISION_PROFILES` */ file_chmod?: string; /** - * Set if `type` is `SSH_KEY`. If it's `NONE`, the variable can be used as a parameter in an action. For `CONTAINER`, the given key is additionally copied to an action container on each run + * Set if `type` is `FILE`, `SSH_KEY`, `SSH_PUBLIC_KEY`, `IOS_KEYCHAIN`, or `IOS_PROVISION_PROFILES`. If it's `NONE`, the variable can be used as a parameter in an action. For `CONTAINER`, the given key is additionally copied to an action container on each run */ file_place?: "NONE" | "CONTAINER"; /** @@ -1745,9 +2106,13 @@ export type AddVariableInObjectRequest = { */ passphrase?: string; /** - * GPG key identifier + * Key identifier for iOS certificates, provisioning profiles, or GPG keys */ key_identifier?: string; + /** + * Set to `true` to disable the variable. Disabled variables are not injected anywhere + */ + disabled?: boolean; /** * The type of the added variable */ @@ -1886,6 +2251,33 @@ export type TunnelView = { endpoint_url?: string; }; +export type SandboxFetchView = { + /** + * The type of the fetch item: PROJECT_REPO, PUBLIC_REPO, or ARTIFACT + */ + type?: "PROJECT_REPO" | "PUBLIC_REPO" | "ARTIFACT"; + /** + * The URL of the git repository (for PUBLIC_REPO type) + */ + repository?: string; + /** + * The branch, tag, or commit to checkout. Defaults to the default branch + */ + ref?: string; + /** + * The target path where the item will be cloned/downloaded. Defaults to the app directory + */ + path?: string; + /** + * The command to run after fetching + */ + build_command?: string; + /** + * The artifact identifier in format pkg:version (for ARTIFACT type) + */ + artifact?: string; +}; + export type SandboxAppView = { /** * The auto-generated ID of the app @@ -2199,6 +2591,10 @@ export type CreateNewSandboxRequest = { * The list of apps (run commands) for the sandbox */ apps?: Array; + /** + * The list of items (repositories and artifacts) to fetch into the sandbox + */ + fetch?: Array; /** * The list of tags associated with the sandbox */ @@ -2327,15 +2723,15 @@ export type EnvironmentVariableView = { */ defaults?: string; /** - * Specifies where to copy the file on each run. Set if `type` is `SSH_KEY` + * Specifies where to copy the file on each run. Set if `type` is `FILE`, `SSH_KEY`, `SSH_PUBLIC_KEY`, `IOS_KEYCHAIN`, or `IOS_PROVISION_PROFILES` */ file_path?: string; /** - * File permission set on copy to a container on each run. Set if `type` is `SSH_KEY` + * File permission set on copy to a container on each run. Set if `type` is `FILE`, `SSH_KEY`, `SSH_PUBLIC_KEY`, `IOS_KEYCHAIN`, or `IOS_PROVISION_PROFILES` */ file_chmod?: string; /** - * Set if `type` is `SSH_KEY`. If it's `NONE`, the variable can be used as a parameter in an action. For `CONTAINER`, the given key is additionally copied to an action container on each run + * Set if `type` is `FILE`, `SSH_KEY`, `SSH_PUBLIC_KEY`, `IOS_KEYCHAIN`, or `IOS_PROVISION_PROFILES`. If it's `NONE`, the variable can be used as a parameter in an action. For `CONTAINER`, the given key is additionally copied to an action container on each run */ file_place?: "NONE" | "CONTAINER"; /** @@ -2363,9 +2759,13 @@ export type EnvironmentVariableView = { */ passphrase?: string; /** - * GPG key identifier + * Key identifier for iOS certificates, provisioning profiles, or GPG keys */ key_identifier?: string; + /** + * Set to `true` to disable the variable. Disabled variables are not injected anywhere + */ + disabled?: boolean; }; export type CloneSandboxRequest = { @@ -2451,6 +2851,10 @@ export type SandboxResponse = { * The list of apps (run commands) for the sandbox */ apps?: Array; + /** + * The list of items (repositories and artifacts) to fetch into the sandbox + */ + fetch?: Array; /** * The timeout in seconds after which the sandbox will be automatically stopped */ @@ -2467,6 +2871,14 @@ export type SandboxResponse = { * The tunnel endpoints of the sandbox */ endpoints?: Array; + /** + * The SSH hostname + */ + ssh_host?: string; + /** + * The SSH port + */ + ssh_port?: number; project?: ProjectView; permissions?: PermissionsView; /** @@ -2637,12 +3049,111 @@ export type IntegrationsViewWritable = { integrations?: Array; }; +/** + * Short representation of an environment object + */ +export type ShortEnvironmentViewWritable = { + /** + * The name of the environment + */ + name?: string; + /** + * The human-readable identifier of the environment + */ + identifier?: string; + /** + * The ID of the environment + */ + id?: number; + /** + * The scope level of the environment + */ + scope?: "PROJECT" | "WORKSPACE" | "ANY"; +}; + +/** + * The list of events that trigger the pipeline run + */ +export type PipelineEventViewWritable = { + /** + * The type of event that triggers the pipeline + */ + type?: + | "PUSH" + | "CREATE_REF" + | "DELETE_REF" + | "PULL_REQUEST" + | "SCHEDULE" + | "PUBLISH_ARTIFACT_VERSION" + | "DELETE_ARTIFACT_VERSION" + | "WEBHOOK" + | "EMAIL" + | "CREATE_ARTIFACT_VERSION" + | "ENVIRONMENT_CREATE" + | "ENVIRONMENT_DELETE" + | "SANDBOX_CREATED" + | "SANDBOX_DELETED" + | "SANDBOX_TIMED_OUT"; + /** + * The list of refs (branches/tags) that trigger the pipeline for push/ref events + */ + refs?: Array; + /** + * The list of pull request events that trigger the pipeline. Examples: `OPENED`, `EDITED`, `CLOSED`, `LABELED`, `UNLABELED`, `REVIEW_REQUESTED`, `REVIEW_REQUESTED_REMOVED`, `SYNCHRONIZED` + */ + events?: Array; + /** + * The list of branches for pull request events + */ + branches?: Array; + /** + * The list of artifacts that trigger the pipeline + */ + artifacts?: Array; + /** + * The list of environments that trigger the pipeline + */ + environments?: Array; + /** + * The start date for scheduled events (type `SCHEDULE`) + */ + start_date?: Date; + /** + * The delay in minutes between scheduled runs (type `SCHEDULE`) + */ + delay?: bigint; + /** + * The cron expression for scheduled (type `SCHEDULE`) events e.g., '0 9 * * 1-5' for weekdays at 9 AM + */ + cron?: string; + /** + * The timezone for scheduled events (type `SCHEDULE`) e.g., 'UTC', 'Europe/Warsaw' + */ + timezone?: string; + /** + * Whether TOTP (Time-based One-Time Password) is enabled for webhook events (type `WEBHOOK`) + */ + totp?: boolean; + /** + * The email subject prefix for email trigger events (type `EMAIL`) + */ + prefix?: string; + /** + * The list of allowed email addresses that can trigger the pipeline via email (type `EMAIL`) + */ + whitelist?: Array; + /** + * The list of sandbox targets that trigger the pipeline for sandbox events (type `SANDBOX_CREATED`, `SANDBOX_DELETED`, `SANDBOX_TIMED_OUT`) + */ + targets?: Array; +}; + /** * Short representation of a pipeline */ export type ShortPipelineViewWritable = { /** - * The unique identifier of the pipeline + * The unique ID of the pipeline */ id?: number; /** @@ -2668,7 +3179,7 @@ export type ShortPipelineViewWritable = { /** * The list of events that trigger the pipeline run */ - events?: Array; + events?: Array; /** * The loop configuration for the pipeline */ @@ -2750,6 +3261,10 @@ export type ShortPipelineViewWritable = { * If set to true, all refs will be fetched from the repository */ fetch_all_refs?: boolean; + /** + * If set to true, LFS files will be fetched from the external repository during pipeline runs + */ + fetch_lfs?: boolean; /** * If set to true, the pipeline will fail on environment preparation warnings */ @@ -2832,6 +3347,32 @@ export type ShortPipelineViewWritable = { folder?: string; }; +/** + * Short representation of a project + */ +export type ShortProjectViewWritable = { + /** + * The human-readable ID of the project + */ + name?: string; + /** + * The Name of the project + */ + display_name: string; + /** + * The status of the project + */ + status?: string; + /** + * Indicates if this is a public project + */ + access?: "PRIVATE" | "PUBLIC"; + /** + * The creation date of the project + */ + create_date?: Date; +}; + /** * The integration to use for authentication */ @@ -2902,7 +3443,9 @@ export type IntegrationViewWritable = { | "ONE_LOGIN" | "OKTA" | "CONTENTFUL" - | "JIRA"; + | "JIRA" + | "NPM_REGISTRY" + | "ANTHROPIC"; /** * The authentication method used by the integration */ @@ -2958,6 +3501,10 @@ export type IntegrationViewWritable = { * List of specific pipelines allowed to use this integration */ allowed_pipelines?: Array; + /** + * Set to `true` to disable the integration. Disabled integrations cannot be used in actions + */ + disabled?: boolean; }; export type IdsViewWritable = { @@ -2978,17 +3525,37 @@ export type IdsViewWritable = { */ environment_id?: string; /** - * The ID of the package + * The ID of the artifact */ - pkg_id?: string; + artifact_id?: string; /** - * The ID of the package version + * The ID of the artifact version */ - pkg_version_id?: string; + artifact_version_id?: string; /** * The ID of the sandbox */ sandbox_id?: string; + /** + * The ID of the unit test suite + */ + unit_test_suite_id?: string; + /** + * The ID of the visual test suite + */ + visual_test_suite_id?: string; + /** + * The ID of the crawl suite + */ + crawl_suite_id?: string; + /** + * The ID of the distribution + */ + distribution_id?: string; + /** + * The ID of the route + */ + route_id?: string; }; /** @@ -3093,7 +3660,9 @@ export type IntegrationIdViewWritable = { | "ONE_LOGIN" | "OKTA" | "CONTENTFUL" - | "JIRA"; + | "JIRA" + | "NPM_REGISTRY" + | "ANTHROPIC"; /** * The authentication method used by the integration */ @@ -3245,32 +3814,6 @@ export type ProjectViewWritable = { without_repository?: boolean; }; -/** - * Short representation of a project - */ -export type ShortProjectViewWritable = { - /** - * The human-readable ID of the project - */ - name?: string; - /** - * The Name of the project - */ - display_name: string; - /** - * The status of the project - */ - status?: string; - /** - * Indicates if this is a public project - */ - access?: "PRIVATE" | "PUBLIC"; - /** - * The creation date of the project - */ - create_date?: Date; -}; - export type UpdateSandboxRequestWritable = { /** * The name of the sandbox @@ -3309,6 +3852,10 @@ export type UpdateSandboxRequestWritable = { * The list of apps (run commands) for the sandbox */ apps?: Array; + /** + * The list of items (repositories and artifacts) to fetch into the sandbox + */ + fetch?: Array; /** * The timeout in seconds after which the sandbox will be automatically stopped */ @@ -3365,15 +3912,15 @@ export type AddVariableInObjectRequestWritable = { */ defaults?: string; /** - * Specifies where to copy the file on each run. Set if `type` is `SSH_KEY` + * Specifies where to copy the file on each run. Set if `type` is `FILE`, `SSH_KEY`, `SSH_PUBLIC_KEY`, `IOS_KEYCHAIN`, or `IOS_PROVISION_PROFILES` */ file_path?: string; /** - * File permission set on copy to a container on each run. Set if `type` is `SSH_KEY` + * File permission set on copy to a container on each run. Set if `type` is `FILE`, `SSH_KEY`, `SSH_PUBLIC_KEY`, `IOS_KEYCHAIN`, or `IOS_PROVISION_PROFILES` */ file_chmod?: string; /** - * Set if `type` is `SSH_KEY`. If it's `NONE`, the variable can be used as a parameter in an action. For `CONTAINER`, the given key is additionally copied to an action container on each run + * Set if `type` is `FILE`, `SSH_KEY`, `SSH_PUBLIC_KEY`, `IOS_KEYCHAIN`, or `IOS_PROVISION_PROFILES`. If it's `NONE`, the variable can be used as a parameter in an action. For `CONTAINER`, the given key is additionally copied to an action container on each run */ file_place?: "NONE" | "CONTAINER"; /** @@ -3385,9 +3932,13 @@ export type AddVariableInObjectRequestWritable = { */ passphrase?: string; /** - * GPG key identifier + * Key identifier for iOS certificates, provisioning profiles, or GPG keys */ key_identifier?: string; + /** + * Set to `true` to disable the variable. Disabled variables are not injected anywhere + */ + disabled?: boolean; /** * The type of the added variable */ @@ -3711,6 +4262,10 @@ export type CreateNewSandboxRequestWritable = { * The list of apps (run commands) for the sandbox */ apps?: Array; + /** + * The list of items (repositories and artifacts) to fetch into the sandbox + */ + fetch?: Array; /** * The list of tags associated with the sandbox */ @@ -3850,6 +4405,10 @@ export type SandboxResponseWritable = { * The list of apps (run commands) for the sandbox */ apps?: Array; + /** + * The list of items (repositories and artifacts) to fetch into the sandbox + */ + fetch?: Array; /** * The timeout in seconds after which the sandbox will be automatically stopped */ @@ -3866,6 +4425,14 @@ export type SandboxResponseWritable = { * The tunnel endpoints of the sandbox */ endpoints?: Array; + /** + * The SSH hostname + */ + ssh_host?: string; + /** + * The SSH port + */ + ssh_port?: number; project?: ProjectViewWritable; permissions?: PermissionsView; /** @@ -3971,17 +4538,45 @@ export type GetIdentifiersData = { */ environment?: string; /** - * The human-readable ID of the package + * The human-readable ID of the artifact */ - package?: string; + artifact?: string; /** - * The version of the package + * The version of the artifact */ - package_version?: string; + artifact_version?: string; /** * The human-readable ID of the sandbox */ sandbox?: string; + /** + * The human-readable ID of the unit test suite + */ + unit_test_suite?: string; + /** + * The human-readable ID of the visual test suite + */ + visual_test_suite?: string; + /** + * The human-readable ID of the crawl suite + */ + crawl_suite?: string; + /** + * The human-readable ID of the distribution + */ + distribution?: string; + /** + * The subdomain of the route. Resolved together with route_domain and route_path against the parent distribution. + */ + route_subdomain?: string; + /** + * The domain of the route. Required to resolve a route. + */ + route_domain?: string; + /** + * The path of the route. Resolved together with route_subdomain and route_domain against the parent distribution. + */ + route_path?: string; }; url: "/workspaces/{workspace_domain}/identifiers"; }; diff --git a/src/api/openapi/zod.gen.ts b/src/api/openapi/zod.gen.ts index 54bee6c..4f74e1b 100644 --- a/src/api/openapi/zod.gen.ts +++ b/src/api/openapi/zod.gen.ts @@ -1,52 +1,51 @@ // This file is auto-generated by @hey-api/openapi-ts -import { z } from "zod"; +import * as z from "zod"; export const zUpdateWorkspaceMemberRequest = z.object({ - admin: z.optional(z.boolean()), - auto_assign_to_new_projects: z.optional(z.boolean()), - auto_assign_permission_set_id: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), + admin: z.boolean().optional(), + auto_assign_to_new_projects: z.boolean().optional(), + auto_assign_permission_set_id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), }); export const zUpdateSsoRequest = z.object({ - type: z.optional(z.enum(["SAML", "OIDC"])), - sso_provider_type: z.optional( - z.enum(["OKTA", "ONE_LOGIN", "GOOGLE", "AZURE", "AWS", "CUSTOM"]), - ), - sso_url: z.optional(z.string()), - issuer: z.optional(z.string()), - certificate: z.optional(z.string()), - signature_method: z.optional(z.string()), - digest_method: z.optional(z.string()), - client_id: z.optional(z.string()), - client_secret: z.optional(z.string()), - require_sso_for_all_members: z.optional(z.boolean()), + type: z.enum(["SAML", "OIDC"]).optional(), + sso_provider_type: z + .enum(["OKTA", "ONE_LOGIN", "GOOGLE", "AZURE", "AWS", "CUSTOM"]) + .optional(), + sso_url: z.string().optional(), + issuer: z.string().optional(), + certificate: z.string().optional(), + signature_method: z.string().optional(), + digest_method: z.string().optional(), + client_id: z.string().optional(), + client_secret: z.string().optional(), + require_sso_for_all_members: z.boolean().optional(), }); export const zGroupPermissionView = z.object({ - id: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - access_level: z.optional( - z.enum([ + id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + access_level: z + .enum([ "DENIED", "READ_ONLY", + "USE_ONLY", "BLIND", "RUN_ONLY", "READ_WRITE", @@ -55,26 +54,25 @@ export const zGroupPermissionView = z.object({ "ALLOWED", "STAGE", "COMMIT", - "USE_ONLY", - ]), - ), + ]) + .optional(), }); export const zUserPermissionView = z.object({ - id: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - access_level: z.optional( - z.enum([ + id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + access_level: z + .enum([ "DENIED", "READ_ONLY", + "USE_ONLY", "BLIND", "RUN_ONLY", "READ_WRITE", @@ -83,19 +81,19 @@ export const zUserPermissionView = z.object({ "ALLOWED", "STAGE", "COMMIT", - "USE_ONLY", - ]), - ), + ]) + .optional(), }); /** * Permission settings defining who can use this integration */ export const zIntegrationPermissionsView = z.object({ - others: z.optional( - z.enum([ + others: z + .enum([ "DENIED", "READ_ONLY", + "USE_ONLY", "BLIND", "RUN_ONLY", "READ_WRITE", @@ -104,15 +102,15 @@ export const zIntegrationPermissionsView = z.object({ "ALLOWED", "STAGE", "COMMIT", - "USE_ONLY", - ]), - ), - users: z.optional(z.array(zUserPermissionView)), - groups: z.optional(z.array(zGroupPermissionView)), - admins: z.optional( - z.enum([ + ]) + .optional(), + users: z.array(zUserPermissionView).optional(), + groups: z.array(zGroupPermissionView).optional(), + admins: z + .enum([ "DENIED", "READ_ONLY", + "USE_ONLY", "BLIND", "RUN_ONLY", "READ_WRITE", @@ -121,9 +119,8 @@ export const zIntegrationPermissionsView = z.object({ "ALLOWED", "STAGE", "COMMIT", - "USE_ONLY", - ]), - ), + ]) + .optional(), }); /** @@ -141,54 +138,53 @@ export const zPipelineIdView = z.object({ }); export const zRoleAssumptionView = z.object({ - arn: z.optional(z.string()), - external_id: z.optional(z.string()), - duration: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), + arn: z.string().optional(), + external_id: z.string().optional(), + duration: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), }); export const zUpdateIntegrationRequest = z.object({ - identifier: z.optional(z.string()), + identifier: z.string().optional(), name: z.string(), - token: z.optional(z.string()), - email: z.optional(z.string()), - api_key: z.optional(z.string()), - access_key: z.optional(z.string()), - secret_key: z.optional(z.string()), - partner_token: z.optional(z.string()), - shop: z.optional(z.string()), - url: z.optional(z.string()), - chat_id: z.optional(z.string()), - git_hub_user_id: z.optional(z.string()), - git_hub_user_name: z.optional(z.string()), - username: z.optional(z.string()), - password: z.optional(z.string()), - app_id: z.optional(z.string()), - tenant_id: z.optional(z.string()), - client_id: z.optional(z.string()), - client_token: z.optional(z.string()), - server_id: z.optional(z.string()), - server_token: z.optional(z.string()), - key_id: z.optional(z.string()), - application_key: z.optional(z.string()), - host_url: z.optional(z.string()), - webhook_address: z.optional(z.string()), - slack_user_id: z.optional(z.string()), - region: z.optional(z.string()), - role_assumptions: z.optional(z.array(zRoleAssumptionView)), - all_pipelines_allowed: z.optional(z.boolean()), - allowed_pipelines: z.optional(z.array(zPipelineIdView)), - permissions: z.optional(zIntegrationPermissionsView), - auth_type: z.optional( - z.enum([ + token: z.string().optional(), + email: z.string().optional(), + api_key: z.string().optional(), + access_key: z.string().optional(), + secret_key: z.string().optional(), + partner_token: z.string().optional(), + shop: z.string().optional(), + url: z.string().optional(), + chat_id: z.string().optional(), + git_hub_user_id: z.string().optional(), + git_hub_user_name: z.string().optional(), + username: z.string().optional(), + password: z.string().optional(), + app_id: z.string().optional(), + tenant_id: z.string().optional(), + client_id: z.string().optional(), + client_token: z.string().optional(), + server_id: z.string().optional(), + server_token: z.string().optional(), + key_id: z.string().optional(), + application_key: z.string().optional(), + host_url: z.string().optional(), + webhook_address: z.string().optional(), + slack_user_id: z.string().optional(), + region: z.string().optional(), + role_assumptions: z.array(zRoleAssumptionView).optional(), + all_pipelines_allowed: z.boolean().optional(), + allowed_pipelines: z.array(zPipelineIdView).optional(), + permissions: zIntegrationPermissionsView.optional(), + auth_type: z + .enum([ "OAUTH", "TOKEN", "API_KEY", @@ -199,105 +195,101 @@ export const zUpdateIntegrationRequest = z.object({ "OIDC", "TRUSTED", "APP_RW", - ]), - ), - target_url: z.optional(z.string()), - refresh_token: z.optional(z.string()), - audience: z.optional(z.string()), - config: z.optional(z.string()), - google_project: z.optional(z.string()), - atop_url: z.optional(z.string()), + ]) + .optional(), + target_url: z.string().optional(), + refresh_token: z.string().optional(), + audience: z.string().optional(), + config: z.string().optional(), + google_project: z.string().optional(), + atop_url: z.string().optional(), + disabled: z.boolean().optional(), }); export const zShortWorkspaceView = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), - id: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - name: z.optional(z.string()), - domain: z.optional(z.string()), + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + name: z.string().optional(), + domain: z.string().optional(), }); export const zWorkspacesView = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), - workspaces: z.optional(z.array(zShortWorkspaceView)), + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + workspaces: z.array(zShortWorkspaceView).optional(), }); export const zWorkspaceMemberView = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), - id: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - name: z.optional(z.string()), - avatar_url: z.optional(z.string()), - email: z.optional(z.string()), - admin: z.optional(z.boolean()), - workspace_owner: z.optional(z.boolean()), - auto_assign_to_new_projects: z.optional(z.boolean()), - auto_assign_permission_set_id: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + name: z.string().optional(), + avatar_url: z.string().optional(), + email: z.string().optional(), + admin: z.boolean().optional(), + workspace_owner: z.boolean().optional(), + auto_assign_to_new_projects: z.boolean().optional(), + auto_assign_permission_set_id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), }); export const zWorkspaceMembersView = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), - members: z.optional(z.array(zWorkspaceMemberView)), + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + members: z.array(zWorkspaceMemberView).optional(), }); export const zWorkspaceView = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), - id: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - name: z.optional(z.string()), - domain: z.optional(z.string()), - owner_id: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - frozen: z.optional(z.boolean()), - create_date: z.optional(z.iso.datetime()), - default_pipeline_resource: z.optional( - z.enum([ + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + name: z.string().optional(), + domain: z.string().optional(), + owner_id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + frozen: z.boolean().optional(), + create_date: z.iso.datetime().optional(), + default_pipeline_resource: z + .enum([ "DEFAULT", "NANO", "SMALL", @@ -306,363 +298,326 @@ export const zWorkspaceView = z.object({ "XLARGE", "CUSTOM", "X2LARGE", - ]), - ), - sso_enabled: z.optional(z.boolean()), - public_pipelines_disabled: z.optional(z.boolean()), + ]) + .optional(), + sso_enabled: z.boolean().optional(), + public_pipelines_disabled: z.boolean().optional(), }); export const zSsoView = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), - type: z.optional(z.enum(["SAML", "OIDC"])), - sso_provider_type: z.optional( - z.enum(["OKTA", "ONE_LOGIN", "GOOGLE", "AZURE", "AWS", "CUSTOM"]), - ), - sso_url: z.optional(z.string()), - issuer: z.optional(z.string()), - certificate: z.optional(z.string()), - signature_method: z.optional(z.string()), - digest_method: z.optional(z.string()), - require_sso_for_all_members: z.optional(z.boolean()), + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + type: z.enum(["SAML", "OIDC"]).optional(), + sso_provider_type: z + .enum(["OKTA", "ONE_LOGIN", "GOOGLE", "AZURE", "AWS", "CUSTOM"]) + .optional(), + sso_url: z.string().optional(), + issuer: z.string().optional(), + certificate: z.string().optional(), + signature_method: z.string().optional(), + digest_method: z.string().optional(), + require_sso_for_all_members: z.boolean().optional(), }); /** * YAML pipeline definition configuration */ export const zYamlDefinitionView = z.object({ - path: z.optional(z.string()), - branch: z.optional(z.string()), - project: z.optional(z.string()), + path: z.string().optional(), + branch: z.string().optional(), + project: z.string().optional(), }); /** * The parameters passed to the remote pipeline definition */ export const zPipelinePropertyView = z.object({ - key: z.optional(z.string()), - value: z.optional(z.string()), + key: z.string().optional(), + value: z.string().optional(), }); -export const zPipelinePkgContextView = z.object({ - identifier: z.optional(z.string()), - scope: z.optional(z.enum(["WORKSPACE", "PROJECT", "ENVIRONMENT", "ANY"])), +/** + * MSSQL authentication credentials + */ +export const zMssqlAuthView = z.object({ + method: z.enum(["PASSWORD"]).optional(), + username: z.string(), + password: z.string(), }); /** - * The list of events that trigger the pipeline run + * MongoDB authentication credentials */ -export const zPipelineEventView = z.object({ - type: z.optional( - z.enum([ - "PUSH", - "CREATE_REF", - "DELETE_REF", - "PULL_REQUEST", - "SCHEDULE", - "PUBLISH_PACKAGE_VERSION", - "DELETE_PACKAGE_VERSION", - "WEBHOOK", - "EMAIL", - "CREATE_PACKAGE_VERSION", - ]), - ), - refs: z.optional(z.array(z.string())), - events: z.optional(z.array(z.string())), - branches: z.optional(z.array(z.string())), - packages: z.optional(z.array(zPipelinePkgContextView)), - start_date: z.optional(z.iso.datetime()), - delay: z.optional( - z.coerce - .bigint() - .min(BigInt("-9223372036854775808"), { - error: "Invalid value: Expected int64 to be >= -9223372036854775808", - }) - .max(BigInt("9223372036854775807"), { - error: "Invalid value: Expected int64 to be <= 9223372036854775807", - }), - ), - cron: z.optional(z.string()), - timezone: z.optional(z.string()), - totp: z.optional(z.boolean()), - prefix: z.optional(z.string()), - whitelist: z.optional(z.array(z.string())), +export const zMongoAuthView = z.object({ + method: z.enum(["PASSWORD"]).optional(), + username: z.string(), + password: z.string(), }); /** - * Short representation of a pipeline + * PostgreSQL authentication credentials */ -export const zShortPipelineView = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), - id: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - identifier: z.optional(z.string()), - name: z.optional(z.string()), - definition_source: z.optional(z.enum(["LOCAL", "REMOTE"])), - git_config_ref: z.optional(z.enum(["NONE", "DYNAMIC", "FIXED"])), - refs: z.optional(z.array(z.string())), - events: z.optional(z.array(zPipelineEventView)), - loop: z.optional(z.array(z.string())), - priority: z.optional(z.enum(["LOW", "NORMAL", "HIGH"])), - disabled: z.optional(z.boolean()), - disabled_reason: z.optional(z.string()), - last_execution_status: z.optional( - z.enum([ - "INPROGRESS", - "ENQUEUED", - "TERMINATED", - "SUCCESSFUL", - "FAILED", - "INITIAL", - "NOT_EXECUTED", - "SKIPPED", - "TERMINATING", - "WAITING_FOR_APPLY", - "WAITING_FOR_VARIABLES", - "WAITING_FOR_SETTABLE_VARIABLES", - "WAITING_FOR_VT_SESSION", - ]), - ), - last_execution_revision: z.optional(z.string()), - target_site_url: z.optional(z.string()), - execution_message_template: z.optional(z.string()), - create_date: z.optional(z.iso.datetime()), - always_from_scratch: z.optional(z.boolean()), - ignore_fail_on_project_status: z.optional(z.boolean()), - no_skip_to_most_recent: z.optional(z.boolean()), - terminate_stale_runs: z.optional(z.boolean()), - auto_clear_cache: z.optional(z.boolean()), - paused: z.optional(z.boolean()), - pause_on_repeated_failures: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - fetch_all_refs: z.optional(z.boolean()), - fail_on_prepare_env_warning: z.optional(z.boolean()), - concurrent_pipeline_runs: z.optional(z.boolean()), - clone_depth: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - do_not_create_commit_status: z.optional(z.boolean()), - stale: z.optional(z.boolean()), - waiting_for_push: z.optional(z.boolean()), - resources: z.optional( - z.enum([ +export const zPostgresqlAuthView = z.object({ + method: z.enum(["PASSWORD"]).optional(), + username: z.string(), + password: z.string(), +}); + +/** + * MySQL authentication credentials + */ +export const zMysqlAuthView = z.object({ + method: z.enum(["PASSWORD"]).optional(), + username: z.string(), + password: z.string(), +}); + +/** + * Define proxy servers' authentication method using the following parameters + */ +export const zSshAuthView = z.object({ + method: z.enum([ + "PASSWORD", + "SSH_KEY", + "ASSETS_KEY", + "PROXY_CREDENTIALS", + "PROXY_KEY", + ]), + username: z.string().optional(), + password: z.string().optional(), + asset: z.string().optional(), + passphrase: z.string().optional(), + key: z.string().optional(), + key_path: z.string().optional(), +}); + +/** + * Kubernetes cluster authentication method + */ +export const zK8sAuthView = z.object({ + method: z.enum(["PASS", "CERT", "TOKEN"]), + username: z.string().optional(), + password: z.string().optional(), + certificate_authority: z.string().optional(), + client_certificate: z.string().optional(), + client_key: z.string().optional(), + token: z.string().optional(), +}); + +/** + * Authentication details + */ +export const zGitAuthView = z.object({ + method: z.enum(["HTTP", "SSH_KEY", "ASSETS_KEY", "CURRENT"]), + username: z.string().optional(), + password: z.string().optional(), + asset: z.string().optional(), + key: z.string().optional(), +}); + +/** + * Authentication details + */ +export const zFtpAuthView = z.object({ + username: z.string(), + password: z.string(), +}); + +/** + * Defines how the target can be used (as deployment target, proxy, or both) + */ +export const zUseAsView = z.object({ + target: z.boolean().optional(), + proxy: z.boolean().optional(), +}); + +/** + * List of specific sandboxes allowed to use this target + */ +export const zAllowedSandboxView = z.object({ + project: z.string(), + sandbox: z.string(), + access_level: z + .enum([ + "DENIED", + "READ_ONLY", + "USE_ONLY", + "BLIND", + "RUN_ONLY", + "READ_WRITE", + "MANAGE", "DEFAULT", - "NANO", - "SMALL", - "MEDIUM", - "LARGE", - "XLARGE", - "CUSTOM", - "X2LARGE", - ]), - ), - remote_path: z.optional(z.string()), - remote_ref: z.optional(z.string()), - remote_project_name: z.optional(z.string()), - remote_parameters: z.optional(z.array(zPipelinePropertyView)), - git_config: z.optional(zYamlDefinitionView), - tags: z.optional(z.array(z.string())), - git_changeset_base: z.optional( - z.enum(["LATEST_RUN", "LATEST_RUN_MATCHING_REF", "PULL_REQUEST"]), - ), - filesystem_changeset_base: z.optional(z.enum(["DATE_MODIFIED", "CONTENTS"])), - cpu: z.optional(z.enum(["X64", "ARM", "X86"])), - description_required: z.optional(z.boolean()), - folder: z.optional(z.string()), + "ALLOWED", + "STAGE", + "COMMIT", + ]) + .optional(), }); /** - * The integration to use for authentication + * List of specific pipelines allowed to use this target */ -export const zIntegrationView = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), - identifier: z.optional(z.string()), - hash_id: z.optional(z.string()), - name: z.optional(z.string()), - type: z.optional( - z.enum([ - "GIT_HUB", - "BITBUCKET", - "GOOGLE", - "DIGITAL_OCEAN", - "SLACK", - "MODULUS", - "HEROKU", - "AMAZON", - "GIT_LAB", - "SHOPIFY", - "GIT_HUB_ENTERPRISE", - "GIT_LAB_ENTERPRISE", - "PUSHOVER", - "PUSHBULLET", - "RACKSPACE", - "CUSTOM", - "CLOUDFLARE", - "NEW_RELIC", - "SENTRY", - "ROLLBAR", - "DATADOG", - "DO_SPACES", - "HONEYBADGER", - "VULTR", - "SENTRY_ENTERPRISE", - "LOGGLY", - "HIP_CHAT", - "FIREBASE", - "TELEGRAM", - "AZURE", - "UPCLOUD", - "GHOST_INSPECTOR", - "NETLIFY", - "AZURE_CLOUD", - "MICROSOFT_TEAMS", - "GOOGLE_SERVICE_ACCOUNT", - "GOOGLE_PLAY_STORE", - "DOCKER_HUB", - "APP_STORE", - "GIT_HUB_APP", - "GIT_HUB_APP_ENTERPRISE", - "GIT_HUB_API", - "ATOP", - "SNYK", - "STACK_HAWK", - "BLACKFIRE", - "BACKBLAZE", - "ONE_LOGIN", - "OKTA", - "CONTENTFUL", - "JIRA", - ]), - ), - auth_type: z.optional( - z.enum([ - "OAUTH", - "TOKEN", - "API_KEY", - "APP", - "APP_SPRYKER", - "TOKEN_APP_EXTENSION", +export const zAllowedPipelineView = z.object({ + project: z.string(), + pipeline: z.string(), + access_level: z + .enum([ + "DENIED", + "READ_ONLY", + "USE_ONLY", + "BLIND", + "RUN_ONLY", + "READ_WRITE", + "MANAGE", "DEFAULT", - "OIDC", - "TRUSTED", - "APP_RW", - ]), - ), - scope: z.optional(z.enum(["WORKSPACE", "PROJECT", "ENVIRONMENT"])), - project_name: z.optional(z.string()), - app_id: z.optional(z.string()), - google_project: z.optional(z.string()), - host_url: z.optional(z.string()), - webhook_address: z.optional(z.string()), - audience: z.optional(z.string()), - atop_url: z.optional(z.string()), - permissions: z.optional(zIntegrationPermissionsView), - all_pipelines_allowed: z.optional(z.boolean()), - allowed_pipelines: z.optional(z.array(zShortPipelineView)), + "ALLOWED", + "STAGE", + "COMMIT", + ]) + .optional(), }); -export const zIntegrationsView = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), - integrations: z.optional(z.array(zIntegrationView)), +/** + * Access permissions configuration + */ +export const zPermissionsView = z.object({ + others: z + .enum([ + "DENIED", + "READ_ONLY", + "USE_ONLY", + "BLIND", + "RUN_ONLY", + "READ_WRITE", + "MANAGE", + "DEFAULT", + "ALLOWED", + "STAGE", + "COMMIT", + ]) + .optional(), + users: z.array(zUserPermissionView).optional(), + groups: z.array(zGroupPermissionView).optional(), +}); + +/** + * Short representation of an environment object + */ +export const zShortEnvironmentView = z.object({ + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + name: z.string().optional(), + identifier: z.string().optional(), + id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + scope: z.enum(["PROJECT", "WORKSPACE", "ANY"]).optional(), +}); + +export const zPipelineEnvironmentContextView = z.object({ + identifier: z.string().optional(), + tags: z.array(z.string()).optional(), + scope: z.enum(["PROJECT", "WORKSPACE", "ANY"]).optional(), +}); + +export const zPipelineArtifactContextView = z.object({ + identifier: z.string().optional(), + scope: z.enum(["WORKSPACE", "PROJECT", "ENVIRONMENT", "ANY"]).optional(), +}); + +/** + * Short representation of a project + */ +export const zShortProjectView = z.object({ + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + name: z.string().optional(), + display_name: z.string(), + status: z.string().optional(), + access: z.enum(["PRIVATE", "PUBLIC"]).optional(), + create_date: z.iso.datetime().optional(), }); export const zIdsView = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), - domain: z.optional(z.string()), - project_identifier: z.optional(z.string()), - pipeline_id: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - environment_id: z.optional(z.string()), - pkg_id: z.optional(z.string()), - pkg_version_id: z.optional(z.string()), - sandbox_id: z.optional(z.string()), + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + domain: z.string().optional(), + project_identifier: z.string().optional(), + pipeline_id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + environment_id: z.string().optional(), + artifact_id: z.string().optional(), + artifact_version_id: z.string().optional(), + sandbox_id: z.string().optional(), + unit_test_suite_id: z.string().optional(), + visual_test_suite_id: z.string().optional(), + crawl_suite_id: z.string().optional(), + distribution_id: z.string().optional(), + route_id: z.string().optional(), }); export const zAddWorkspaceMemberRequest = z.object({ - admin: z.optional(z.boolean()), - auto_assign_to_new_projects: z.optional(z.boolean()), - auto_assign_permission_set_id: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), + admin: z.boolean().optional(), + auto_assign_to_new_projects: z.boolean().optional(), + auto_assign_permission_set_id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), email: z.string(), }); export const zAddIntegrationRequest = z.object({ - identifier: z.optional(z.string()), + identifier: z.string().optional(), name: z.string(), - token: z.optional(z.string()), - email: z.optional(z.string()), - api_key: z.optional(z.string()), - access_key: z.optional(z.string()), - secret_key: z.optional(z.string()), - partner_token: z.optional(z.string()), - shop: z.optional(z.string()), - url: z.optional(z.string()), - chat_id: z.optional(z.string()), - git_hub_user_id: z.optional(z.string()), - git_hub_user_name: z.optional(z.string()), - username: z.optional(z.string()), - password: z.optional(z.string()), - app_id: z.optional(z.string()), - tenant_id: z.optional(z.string()), - client_id: z.optional(z.string()), - client_token: z.optional(z.string()), - server_id: z.optional(z.string()), - server_token: z.optional(z.string()), - key_id: z.optional(z.string()), - application_key: z.optional(z.string()), - host_url: z.optional(z.string()), - webhook_address: z.optional(z.string()), - slack_user_id: z.optional(z.string()), - region: z.optional(z.string()), - role_assumptions: z.optional(z.array(zRoleAssumptionView)), - all_pipelines_allowed: z.optional(z.boolean()), - allowed_pipelines: z.optional(z.array(zPipelineIdView)), - permissions: z.optional(zIntegrationPermissionsView), - auth_type: z.optional( - z.enum([ + token: z.string().optional(), + email: z.string().optional(), + api_key: z.string().optional(), + access_key: z.string().optional(), + secret_key: z.string().optional(), + partner_token: z.string().optional(), + shop: z.string().optional(), + url: z.string().optional(), + chat_id: z.string().optional(), + git_hub_user_id: z.string().optional(), + git_hub_user_name: z.string().optional(), + username: z.string().optional(), + password: z.string().optional(), + app_id: z.string().optional(), + tenant_id: z.string().optional(), + client_id: z.string().optional(), + client_token: z.string().optional(), + server_id: z.string().optional(), + server_token: z.string().optional(), + key_id: z.string().optional(), + application_key: z.string().optional(), + host_url: z.string().optional(), + webhook_address: z.string().optional(), + slack_user_id: z.string().optional(), + region: z.string().optional(), + role_assumptions: z.array(zRoleAssumptionView).optional(), + all_pipelines_allowed: z.boolean().optional(), + allowed_pipelines: z.array(zPipelineIdView).optional(), + permissions: zIntegrationPermissionsView.optional(), + auth_type: z + .enum([ "OAUTH", "TOKEN", "API_KEY", @@ -673,14 +628,15 @@ export const zAddIntegrationRequest = z.object({ "OIDC", "TRUSTED", "APP_RW", - ]), - ), - target_url: z.optional(z.string()), - refresh_token: z.optional(z.string()), - audience: z.optional(z.string()), - config: z.optional(z.string()), - google_project: z.optional(z.string()), - atop_url: z.optional(z.string()), + ]) + .optional(), + target_url: z.string().optional(), + refresh_token: z.string().optional(), + audience: z.string().optional(), + config: z.string().optional(), + google_project: z.string().optional(), + atop_url: z.string().optional(), + disabled: z.boolean().optional(), type: z.enum([ "GIT_HUB", "BITBUCKET", @@ -733,44 +689,39 @@ export const zAddIntegrationRequest = z.object({ "OKTA", "CONTENTFUL", "JIRA", + "NPM_REGISTRY", + "ANTHROPIC", ]), scope: z.enum(["WORKSPACE", "PROJECT", "ENVIRONMENT"]), - project_name: z.optional(z.string()), + project_name: z.string().optional(), }); /** * Sandbox reference */ export const zSandboxIdView = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), - id: z.optional(z.string()), - identifier: z.optional(z.string()), - name: z.optional(z.string()), - status: z.optional( - z.enum([ - "STARTING", - "STOPPING", - "FAILED", - "RUNNING", - "STOPPED", - "RESTORING", - ]), - ), - setup_status: z.optional(z.enum(["INPROGRESS", "SUCCESS", "FAILED"])), + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + id: z.string().optional(), + identifier: z.string().optional(), + name: z.string().optional(), + status: z + .enum(["STARTING", "STOPPING", "FAILED", "RUNNING", "STOPPED", "RESTORING"]) + .optional(), + setup_status: z.enum(["INPROGRESS", "SUCCESS", "FAILED"]).optional(), }); /** * Integration reference */ export const zIntegrationIdView = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), - identifier: z.optional(z.string()), - hash_id: z.optional(z.string()), - name: z.optional(z.string()), - type: z.optional( - z.enum([ + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + identifier: z.string().optional(), + hash_id: z.string().optional(), + name: z.string().optional(), + type: z + .enum([ "GIT_HUB", "BITBUCKET", "GOOGLE", @@ -822,153 +773,117 @@ export const zIntegrationIdView = z.object({ "OKTA", "CONTENTFUL", "JIRA", - ]), - ), - auth_type: z.optional( - z.enum([ + "NPM_REGISTRY", + "ANTHROPIC", + ]) + .optional(), + auth_type: z + .enum([ "OAUTH", "TOKEN", "API_KEY", "APP", - "APP_SPRYKER", - "TOKEN_APP_EXTENSION", - "DEFAULT", - "OIDC", - "TRUSTED", - "APP_RW", - ]), - ), - host_url: z.optional(z.string()), - webhook_address: z.optional(z.string()), - atop_url: z.optional(z.string()), - app_id: z.optional(z.string()), - google_project: z.optional(z.string()), - audience: z.optional(z.string()), -}); - -/** - * User/member reference - */ -export const zMemberView = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), - id: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - name: z.optional(z.string()), - avatar_url: z.optional(z.string()), - email: z.optional(z.string()), - admin: z.optional(z.boolean()), - workspace_owner: z.optional(z.boolean()), -}); - -export const zProjectView = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), - name: z.optional(z.string()), - display_name: z.string(), - status: z.optional(z.string()), - access: z.optional(z.enum(["PRIVATE", "PUBLIC"])), - create_date: z.optional(z.iso.datetime()), - external_project_id: z.optional(z.string()), - git_lab_project_id: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - custom_repo_url: z.optional(z.string()), - custom_repo_user: z.optional(z.string()), - custom_repo_pass: z.optional(z.string()), - custom_repo_ssh_key_id: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - created_by: z.optional(zMemberView), - http_repository: z.optional(z.string()), - ssh_repository: z.optional(z.string()), - default_branch: z.optional(z.string()), - integration: z.optional(zIntegrationIdView), - fetch_submodules: z.optional(z.boolean()), - fetch_submodules_env_key: z.optional(z.string()), - allow_pull_requests: z.optional(z.boolean()), - update_default_branch_from_external: z.optional(z.boolean()), - without_repository: z.optional(z.boolean()), -}); - -/** - * Access permissions configuration - */ -export const zPermissionsView = z.object({ - others: z.optional( - z.enum([ - "DENIED", - "READ_ONLY", - "BLIND", - "RUN_ONLY", - "READ_WRITE", - "MANAGE", + "APP_SPRYKER", + "TOKEN_APP_EXTENSION", "DEFAULT", - "ALLOWED", - "STAGE", - "COMMIT", - "USE_ONLY", - ]), - ), - users: z.optional(z.array(zUserPermissionView)), - groups: z.optional(z.array(zGroupPermissionView)), + "OIDC", + "TRUSTED", + "APP_RW", + ]) + .optional(), + host_url: z.string().optional(), + webhook_address: z.string().optional(), + atop_url: z.string().optional(), + app_id: z.string().optional(), + google_project: z.string().optional(), + audience: z.string().optional(), }); /** - * Short representation of a project + * User/member reference */ -export const zShortProjectView = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), - name: z.optional(z.string()), +export const zMemberView = z.object({ + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + name: z.string().optional(), + avatar_url: z.string().optional(), + email: z.string().optional(), + admin: z.boolean().optional(), + workspace_owner: z.boolean().optional(), +}); + +export const zProjectView = z.object({ + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + name: z.string().optional(), display_name: z.string(), - status: z.optional(z.string()), - access: z.optional(z.enum(["PRIVATE", "PUBLIC"])), - create_date: z.optional(z.iso.datetime()), + status: z.string().optional(), + access: z.enum(["PRIVATE", "PUBLIC"]).optional(), + create_date: z.iso.datetime().optional(), + external_project_id: z.string().optional(), + git_lab_project_id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + custom_repo_url: z.string().optional(), + custom_repo_user: z.string().optional(), + custom_repo_pass: z.string().optional(), + custom_repo_ssh_key_id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + created_by: zMemberView.optional(), + http_repository: z.string().optional(), + ssh_repository: z.string().optional(), + default_branch: z.string().optional(), + integration: zIntegrationIdView.optional(), + fetch_submodules: z.boolean().optional(), + fetch_submodules_env_key: z.string().optional(), + allow_pull_requests: z.boolean().optional(), + update_default_branch_from_external: z.boolean().optional(), + without_repository: z.boolean().optional(), }); /** * The environment variables of the sandbox */ export const zAddVariableInObjectRequest = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), key: z.string(), - value: z.optional(z.string()), - settable: z.optional(z.boolean()), - run_only_settable: z.optional(z.boolean()), - encrypted: z.optional(z.boolean()), - description: z.optional(z.string()), - init_path: z.optional(z.string()), - defaults: z.optional(z.string()), - file_path: z.optional(z.string()), - file_chmod: z.optional(z.string()), - file_place: z.optional(z.enum(["NONE", "CONTAINER"])), - password: z.optional(z.string()), - passphrase: z.optional(z.string()), - key_identifier: z.optional(z.string()), + value: z.string().optional(), + settable: z.boolean().optional(), + run_only_settable: z.boolean().optional(), + encrypted: z.boolean().optional(), + description: z.string().optional(), + init_path: z.string().optional(), + defaults: z.string().optional(), + file_path: z.string().optional(), + file_chmod: z.string().optional(), + file_place: z.enum(["NONE", "CONTAINER"]).optional(), + password: z.string().optional(), + passphrase: z.string().optional(), + key_identifier: z.string().optional(), + disabled: z.boolean().optional(), type: z.enum([ "VAR", "FILE", @@ -984,74 +899,81 @@ export const zAddVariableInObjectRequest = z.object({ * The TLS/SSL encryption settings of the tunnel */ export const zTlsSettingsView = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), - terminate_at: z.optional(z.enum(["REGION", "AGENT", "TARGET"])), + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + terminate_at: z.enum(["REGION", "AGENT", "TARGET"]).optional(), }); /** * The HTTP-specific settings of the tunnel */ export const zHttpSettingsView = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), - verify_certificate: z.optional(z.boolean()), - compression: z.optional(z.boolean()), - http2: z.optional(z.boolean()), - log_requests: z.optional(z.boolean()), - request_headers: z.optional(z.record(z.string(), z.string())), - whitelist_user_agents: z.optional(z.array(z.string())), - rewrite_host_header: z.optional(z.string()), - response_headers: z.optional(z.record(z.string(), z.string())), - login: z.optional(z.string()), - circuit_breaker: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - serve_path: z.optional(z.string()), - auth_type: z.optional(z.enum(["NONE", "BASIC", "BUDDY"])), + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + verify_certificate: z.boolean().optional(), + compression: z.boolean().optional(), + http2: z.boolean().optional(), + log_requests: z.boolean().optional(), + request_headers: z.record(z.string(), z.string()).optional(), + whitelist_user_agents: z.array(z.string()).optional(), + rewrite_host_header: z.string().optional(), + response_headers: z.record(z.string(), z.string()).optional(), + login: z.string().optional(), + circuit_breaker: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + serve_path: z.string().optional(), + auth_type: z.enum(["NONE", "BASIC", "BUDDY"]).optional(), }); export const zTunnelView = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), name: z.string(), endpoint: z.string(), type: z.enum(["TCP", "TLS", "HTTP", "SSH"]), region: z.enum(["US", "EU", "AS"]), - whitelist: z.optional(z.array(z.string())), - timeout: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - http: z.optional(zHttpSettingsView), - tls: z.optional(zTlsSettingsView), - endpoint_url: z.optional(z.string()), + whitelist: z.array(z.string()).optional(), + timeout: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + http: zHttpSettingsView.optional(), + tls: zTlsSettingsView.optional(), + endpoint_url: z.string().optional(), +}); + +export const zSandboxFetchView = z.object({ + type: z.enum(["PROJECT_REPO", "PUBLIC_REPO", "ARTIFACT"]).optional(), + repository: z.string().optional(), + ref: z.string().optional(), + path: z.string().optional(), + build_command: z.string().optional(), + artifact: z.string().optional(), }); export const zSandboxAppView = z.object({ - id: z.optional(z.string()), - command: z.optional(z.string()), - app_status: z.optional(z.enum(["NONE", "RUNNING", "ENDED", "FAILED"])), + id: z.string().optional(), + command: z.string().optional(), + app_status: z.enum(["NONE", "RUNNING", "ENDED", "FAILED"]).optional(), }); export const zUpdateSandboxRequest = z.object({ - name: z.optional(z.string()), - identifier: z.optional(z.string()), - resources: z.optional( - z.enum([ + name: z.string().optional(), + identifier: z.string().optional(), + resources: z + .enum([ "1x2", "2x4", "3x6", @@ -1065,169 +987,165 @@ export const zUpdateSandboxRequest = z.object({ "11x22", "12x24", "CUSTOM", - ]), - ), - first_boot_commands: z.optional(z.string()), - app_dir: z.optional(z.string()), - apps: z.optional(z.array(zSandboxAppView)), - timeout: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - tags: z.optional(z.array(z.string())), - endpoints: z.optional(z.array(zTunnelView)), - variables: z.optional(z.array(zAddVariableInObjectRequest)), - permissions: z.optional(zPermissionsView), + ]) + .optional(), + first_boot_commands: z.string().optional(), + app_dir: z.string().optional(), + apps: z.array(zSandboxAppView).optional(), + fetch: z.array(zSandboxFetchView).optional(), + timeout: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + tags: z.array(z.string()).optional(), + endpoints: z.array(zTunnelView).optional(), + variables: z.array(zAddVariableInObjectRequest).optional(), + permissions: zPermissionsView.optional(), }); export const zSandboxesView = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), - sandboxes: z.optional(z.array(zSandboxIdView)), + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + sandboxes: z.array(zSandboxIdView).optional(), }); /** * Content item in a sandbox */ export const zSandboxContentItem = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), - type: z.optional(z.enum(["FILE", "DIR"])), - name: z.optional(z.string()), - path: z.optional(z.string()), - size: z.optional( - z.coerce - .bigint() - .min(BigInt("-9223372036854775808"), { - error: "Invalid value: Expected int64 to be >= -9223372036854775808", - }) - .max(BigInt("9223372036854775807"), { - error: "Invalid value: Expected int64 to be <= 9223372036854775807", - }), - ), + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + type: z.enum(["FILE", "DIR"]).optional(), + name: z.string().optional(), + path: z.string().optional(), + size: z.coerce + .bigint() + .min(BigInt("-9223372036854775808"), { + error: "Invalid value: Expected int64 to be >= -9223372036854775808", + }) + .max(BigInt("9223372036854775807"), { + error: "Invalid value: Expected int64 to be <= 9223372036854775807", + }) + .optional(), }); /** * Sandbox content listing */ export const zSandboxContentView = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), - contents: z.optional(z.array(zSandboxContentItem)), + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + contents: z.array(zSandboxContentItem).optional(), }); export const zSandboxCommandView = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), - id: z.optional(z.string()), - command: z.optional(z.string()), - runtime: z.optional(z.enum(["BASH", "JAVASCRIPT", "TYPESCRIPT", "PYTHON"])), - status: z.optional(z.enum(["INPROGRESS", "SUCCESSFUL", "FAILED"])), - exit_code: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - start_date: z.optional(z.iso.datetime()), - finish_date: z.optional(z.iso.datetime()), - logs_url: z.optional(z.string()), + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + id: z.string().optional(), + command: z.string().optional(), + runtime: z.enum(["BASH", "JAVASCRIPT", "TYPESCRIPT", "PYTHON"]).optional(), + status: z.enum(["INPROGRESS", "SUCCESSFUL", "FAILED"]).optional(), + exit_code: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + start_date: z.iso.datetime().optional(), + finish_date: z.iso.datetime().optional(), + logs_url: z.string().optional(), }); export const zSandboxCommandsView = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), - commands: z.optional(z.array(zSandboxCommandView)), + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + commands: z.array(zSandboxCommandView).optional(), }); export const zSandboxCommandLog = z.object({ - type: z.optional(z.enum(["STDOUT", "STDERR"])), - data: z.optional(z.string()), + type: z.enum(["STDOUT", "STDERR"]).optional(), + data: z.string().optional(), }); export const zSandboxAppLogsView = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), - cursor: z.optional(z.string()), - logs: z.optional(z.array(z.string())), + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + cursor: z.string().optional(), + logs: z.array(z.string()).optional(), }); export const zShortSnapshotView = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), - id: z.optional(z.string()), - name: z.optional(z.string()), - size: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - status: z.optional(z.enum(["CREATING", "CREATED", "DELETING", "FAILED"])), - create_date: z.optional(z.iso.datetime()), + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + id: z.string().optional(), + name: z.string().optional(), + size: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + status: z.enum(["CREATING", "CREATED", "DELETING", "FAILED"]).optional(), + create_date: z.iso.datetime().optional(), }); export const zSnapshotsView = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), - snapshots: z.optional(z.array(zShortSnapshotView)), + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + snapshots: z.array(zShortSnapshotView).optional(), }); export const zExecuteSandboxCommandRequest = z.object({ command: z.string(), - runtime: z.optional(z.enum(["BASH", "JAVASCRIPT", "TYPESCRIPT", "PYTHON"])), + runtime: z.enum(["BASH", "JAVASCRIPT", "TYPESCRIPT", "PYTHON"]).optional(), }); export const zAddSnapshotRequest = z.object({ - name: z.optional(z.string()), + name: z.string().optional(), }); export const zSnapshotView = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), - id: z.optional(z.string()), - name: z.optional(z.string()), - size: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - status: z.optional(z.enum(["CREATING", "CREATED", "DELETING", "FAILED"])), - create_date: z.optional(z.iso.datetime()), - created_by: z.optional(zMemberView), + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + id: z.string().optional(), + name: z.string().optional(), + size: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + status: z.enum(["CREATING", "CREATED", "DELETING", "FAILED"]).optional(), + create_date: z.iso.datetime().optional(), + created_by: zMemberView.optional(), }); export const zSandboxYamlView = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), - yaml: z.optional(z.string()), + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + yaml: z.string().optional(), }); export const zCreateNewSandboxRequest = z.object({ name: z.string(), - identifier: z.optional(z.string()), + identifier: z.string().optional(), os: z.string(), - resources: z.optional( - z.enum([ + resources: z + .enum([ "1x2", "2x4", "3x6", @@ -1241,42 +1159,41 @@ export const zCreateNewSandboxRequest = z.object({ "11x22", "12x24", "CUSTOM", - ]), - ), - first_boot_commands: z.optional(z.string()), - app_dir: z.optional(z.string()), - apps: z.optional(z.array(z.string())), - tags: z.optional(z.array(z.string())), - endpoints: z.optional(z.array(zTunnelView)), - variables: z.optional(z.array(zAddVariableInObjectRequest)), - timeout: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - permissions: z.optional(zPermissionsView), + ]) + .optional(), + first_boot_commands: z.string().optional(), + app_dir: z.string().optional(), + apps: z.array(z.string()).optional(), + fetch: z.array(zSandboxFetchView).optional(), + tags: z.array(z.string()).optional(), + endpoints: z.array(zTunnelView).optional(), + variables: z.array(zAddVariableInObjectRequest).optional(), + timeout: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + permissions: zPermissionsView.optional(), }); export const zEnvironmentVariableView = z.object({ - id: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - key: z.optional(z.string()), - value: z.optional(z.string()), - type: z.optional( - z.enum([ + id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + key: z.string().optional(), + value: z.string().optional(), + type: z + .enum([ "VAR", "FILE", "SSH_KEY", @@ -1284,33 +1201,34 @@ export const zEnvironmentVariableView = z.object({ "IOS_PROVISION_PROFILES", "SSH_PUBLIC_KEY", "GPG_KEY", - ]), - ), - encrypted: z.optional(z.boolean()), - settable: z.optional(z.boolean()), - run_only_settable: z.optional(z.boolean()), - description: z.optional(z.string()), - init_path: z.optional(z.string()), - defaults: z.optional(z.string()), - file_path: z.optional(z.string()), - file_chmod: z.optional(z.string()), - file_place: z.optional(z.enum(["NONE", "CONTAINER"])), - binary: z.optional(z.boolean()), - public_value: z.optional(z.string()), - key_fingerprint: z.optional(z.string()), - checksum: z.optional(z.string()), - password: z.optional(z.string()), - passphrase: z.optional(z.string()), - key_identifier: z.optional(z.string()), + ]) + .optional(), + encrypted: z.boolean().optional(), + settable: z.boolean().optional(), + run_only_settable: z.boolean().optional(), + description: z.string().optional(), + init_path: z.string().optional(), + defaults: z.string().optional(), + file_path: z.string().optional(), + file_chmod: z.string().optional(), + file_place: z.enum(["NONE", "CONTAINER"]).optional(), + binary: z.boolean().optional(), + public_value: z.string().optional(), + key_fingerprint: z.string().optional(), + checksum: z.string().optional(), + password: z.string().optional(), + passphrase: z.string().optional(), + key_identifier: z.string().optional(), + disabled: z.boolean().optional(), }); export const zCreateFromSnapshotRequest = z.object({ snapshot_id: z.string(), name: z.string(), - identifier: z.optional(z.string()), - os: z.optional(z.string()), - resources: z.optional( - z.enum([ + identifier: z.string().optional(), + os: z.string().optional(), + resources: z + .enum([ "1x2", "2x4", "3x6", @@ -1324,44 +1242,35 @@ export const zCreateFromSnapshotRequest = z.object({ "11x22", "12x24", "CUSTOM", - ]), - ), - first_boot_commands: z.optional(z.string()), - app_dir: z.optional(z.string()), - apps: z.optional(z.array(z.string())), - tags: z.optional(z.array(z.string())), - endpoints: z.optional(z.array(zTunnelView)), - variables: z.optional(z.array(zEnvironmentVariableView)), + ]) + .optional(), + first_boot_commands: z.string().optional(), + app_dir: z.string().optional(), + apps: z.array(z.string()).optional(), + tags: z.array(z.string()).optional(), + endpoints: z.array(zTunnelView).optional(), + variables: z.array(zEnvironmentVariableView).optional(), }); export const zCloneSandboxRequest = z.object({ source_sandbox_id: z.string(), name: z.string(), - identifier: z.optional(z.string()), + identifier: z.string().optional(), }); export const zSandboxResponse = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), - id: z.optional(z.string()), - identifier: z.optional(z.string()), - name: z.optional(z.string()), - status: z.optional( - z.enum([ - "STARTING", - "STOPPING", - "FAILED", - "RUNNING", - "STOPPED", - "RESTORING", - ]), - ), - setup_status: z.optional( - z.enum(["INPROGRESS", "SUCCESS", "FAILED", "STALE"]), - ), - os: z.optional(z.string()), - resources: z.optional( - z.enum([ + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + id: z.string().optional(), + identifier: z.string().optional(), + name: z.string().optional(), + status: z + .enum(["STARTING", "STOPPING", "FAILED", "RUNNING", "STOPPED", "RESTORING"]) + .optional(), + setup_status: z.enum(["INPROGRESS", "SUCCESS", "FAILED", "STALE"]).optional(), + os: z.string().optional(), + resources: z + .enum([ "1x2", "2x4", "3x6", @@ -1375,114 +1284,119 @@ export const zSandboxResponse = z.object({ "11x22", "12x24", "CUSTOM", - ]), - ), - first_boot_commands: z.optional(z.string()), - app_dir: z.optional(z.string()), - apps: z.optional(z.array(zSandboxAppView)), - timeout: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - tags: z.optional(z.array(z.string())), - boot_logs: z.optional(z.array(z.string())), - endpoints: z.optional(z.array(zTunnelView)), - project: z.optional(zProjectView), - permissions: z.optional(zPermissionsView), - variables: z.optional(z.array(zEnvironmentVariableView)), + ]) + .optional(), + first_boot_commands: z.string().optional(), + app_dir: z.string().optional(), + apps: z.array(zSandboxAppView).optional(), + fetch: z.array(zSandboxFetchView).optional(), + timeout: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + tags: z.array(z.string()).optional(), + boot_logs: z.array(z.string()).optional(), + endpoints: z.array(zTunnelView).optional(), + ssh_host: z.string().optional(), + ssh_port: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + project: zProjectView.optional(), + permissions: zPermissionsView.optional(), + variables: z.array(zEnvironmentVariableView).optional(), }); export const zProjectsView = z.object({ - url: z.optional(z.string().readonly()), - html_url: z.optional(z.string().readonly()), - projects: z.optional(z.array(zShortProjectView)), + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + projects: z.array(zShortProjectView).optional(), }); export const zShortWorkspaceViewWritable = z.object({ - id: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - name: z.optional(z.string()), - domain: z.optional(z.string()), + id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + name: z.string().optional(), + domain: z.string().optional(), }); export const zWorkspacesViewWritable = z.object({ - workspaces: z.optional(z.array(zShortWorkspaceViewWritable)), + workspaces: z.array(zShortWorkspaceViewWritable).optional(), }); export const zWorkspaceMemberViewWritable = z.object({ - id: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - name: z.optional(z.string()), - avatar_url: z.optional(z.string()), - email: z.optional(z.string()), - admin: z.optional(z.boolean()), - workspace_owner: z.optional(z.boolean()), - auto_assign_to_new_projects: z.optional(z.boolean()), - auto_assign_permission_set_id: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), + id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + name: z.string().optional(), + avatar_url: z.string().optional(), + email: z.string().optional(), + admin: z.boolean().optional(), + workspace_owner: z.boolean().optional(), + auto_assign_to_new_projects: z.boolean().optional(), + auto_assign_permission_set_id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), }); export const zWorkspaceMembersViewWritable = z.object({ - members: z.optional(z.array(zWorkspaceMemberViewWritable)), + members: z.array(zWorkspaceMemberViewWritable).optional(), }); export const zWorkspaceViewWritable = z.object({ - id: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - name: z.optional(z.string()), - domain: z.optional(z.string()), - owner_id: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - frozen: z.optional(z.boolean()), - create_date: z.optional(z.iso.datetime()), - default_pipeline_resource: z.optional( - z.enum([ + id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + name: z.string().optional(), + domain: z.string().optional(), + owner_id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + frozen: z.boolean().optional(), + create_date: z.iso.datetime().optional(), + default_pipeline_resource: z + .enum([ "DEFAULT", "NANO", "SMALL", @@ -1491,270 +1405,99 @@ export const zWorkspaceViewWritable = z.object({ "XLARGE", "CUSTOM", "X2LARGE", - ]), - ), - sso_enabled: z.optional(z.boolean()), - public_pipelines_disabled: z.optional(z.boolean()), + ]) + .optional(), + sso_enabled: z.boolean().optional(), + public_pipelines_disabled: z.boolean().optional(), }); export const zSsoViewWritable = z.object({ - type: z.optional(z.enum(["SAML", "OIDC"])), - sso_provider_type: z.optional( - z.enum(["OKTA", "ONE_LOGIN", "GOOGLE", "AZURE", "AWS", "CUSTOM"]), - ), - sso_url: z.optional(z.string()), - issuer: z.optional(z.string()), - certificate: z.optional(z.string()), - signature_method: z.optional(z.string()), - digest_method: z.optional(z.string()), - require_sso_for_all_members: z.optional(z.boolean()), + type: z.enum(["SAML", "OIDC"]).optional(), + sso_provider_type: z + .enum(["OKTA", "ONE_LOGIN", "GOOGLE", "AZURE", "AWS", "CUSTOM"]) + .optional(), + sso_url: z.string().optional(), + issuer: z.string().optional(), + certificate: z.string().optional(), + signature_method: z.string().optional(), + digest_method: z.string().optional(), + require_sso_for_all_members: z.boolean().optional(), }); /** - * Short representation of a pipeline + * Short representation of an environment object */ -export const zShortPipelineViewWritable = z.object({ - id: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - identifier: z.optional(z.string()), - name: z.optional(z.string()), - definition_source: z.optional(z.enum(["LOCAL", "REMOTE"])), - git_config_ref: z.optional(z.enum(["NONE", "DYNAMIC", "FIXED"])), - refs: z.optional(z.array(z.string())), - events: z.optional(z.array(zPipelineEventView)), - loop: z.optional(z.array(z.string())), - priority: z.optional(z.enum(["LOW", "NORMAL", "HIGH"])), - disabled: z.optional(z.boolean()), - disabled_reason: z.optional(z.string()), - last_execution_status: z.optional( - z.enum([ - "INPROGRESS", - "ENQUEUED", - "TERMINATED", - "SUCCESSFUL", - "FAILED", - "INITIAL", - "NOT_EXECUTED", - "SKIPPED", - "TERMINATING", - "WAITING_FOR_APPLY", - "WAITING_FOR_VARIABLES", - "WAITING_FOR_SETTABLE_VARIABLES", - "WAITING_FOR_VT_SESSION", - ]), - ), - last_execution_revision: z.optional(z.string()), - target_site_url: z.optional(z.string()), - execution_message_template: z.optional(z.string()), - create_date: z.optional(z.iso.datetime()), - always_from_scratch: z.optional(z.boolean()), - ignore_fail_on_project_status: z.optional(z.boolean()), - no_skip_to_most_recent: z.optional(z.boolean()), - terminate_stale_runs: z.optional(z.boolean()), - auto_clear_cache: z.optional(z.boolean()), - paused: z.optional(z.boolean()), - pause_on_repeated_failures: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - fetch_all_refs: z.optional(z.boolean()), - fail_on_prepare_env_warning: z.optional(z.boolean()), - concurrent_pipeline_runs: z.optional(z.boolean()), - clone_depth: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - do_not_create_commit_status: z.optional(z.boolean()), - stale: z.optional(z.boolean()), - waiting_for_push: z.optional(z.boolean()), - resources: z.optional( - z.enum([ - "DEFAULT", - "NANO", - "SMALL", - "MEDIUM", - "LARGE", - "XLARGE", - "CUSTOM", - "X2LARGE", - ]), - ), - remote_path: z.optional(z.string()), - remote_ref: z.optional(z.string()), - remote_project_name: z.optional(z.string()), - remote_parameters: z.optional(z.array(zPipelinePropertyView)), - git_config: z.optional(zYamlDefinitionView), - tags: z.optional(z.array(z.string())), - git_changeset_base: z.optional( - z.enum(["LATEST_RUN", "LATEST_RUN_MATCHING_REF", "PULL_REQUEST"]), - ), - filesystem_changeset_base: z.optional(z.enum(["DATE_MODIFIED", "CONTENTS"])), - cpu: z.optional(z.enum(["X64", "ARM", "X86"])), - description_required: z.optional(z.boolean()), - folder: z.optional(z.string()), +export const zShortEnvironmentViewWritable = z.object({ + name: z.string().optional(), + identifier: z.string().optional(), + id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + scope: z.enum(["PROJECT", "WORKSPACE", "ANY"]).optional(), }); /** - * The integration to use for authentication + * Short representation of a project */ -export const zIntegrationViewWritable = z.object({ - identifier: z.optional(z.string()), - hash_id: z.optional(z.string()), - name: z.optional(z.string()), - type: z.optional( - z.enum([ - "GIT_HUB", - "BITBUCKET", - "GOOGLE", - "DIGITAL_OCEAN", - "SLACK", - "MODULUS", - "HEROKU", - "AMAZON", - "GIT_LAB", - "SHOPIFY", - "GIT_HUB_ENTERPRISE", - "GIT_LAB_ENTERPRISE", - "PUSHOVER", - "PUSHBULLET", - "RACKSPACE", - "CUSTOM", - "CLOUDFLARE", - "NEW_RELIC", - "SENTRY", - "ROLLBAR", - "DATADOG", - "DO_SPACES", - "HONEYBADGER", - "VULTR", - "SENTRY_ENTERPRISE", - "LOGGLY", - "HIP_CHAT", - "FIREBASE", - "TELEGRAM", - "AZURE", - "UPCLOUD", - "GHOST_INSPECTOR", - "NETLIFY", - "AZURE_CLOUD", - "MICROSOFT_TEAMS", - "GOOGLE_SERVICE_ACCOUNT", - "GOOGLE_PLAY_STORE", - "DOCKER_HUB", - "APP_STORE", - "GIT_HUB_APP", - "GIT_HUB_APP_ENTERPRISE", - "GIT_HUB_API", - "ATOP", - "SNYK", - "STACK_HAWK", - "BLACKFIRE", - "BACKBLAZE", - "ONE_LOGIN", - "OKTA", - "CONTENTFUL", - "JIRA", - ]), - ), - auth_type: z.optional( - z.enum([ - "OAUTH", - "TOKEN", - "API_KEY", - "APP", - "APP_SPRYKER", - "TOKEN_APP_EXTENSION", - "DEFAULT", - "OIDC", - "TRUSTED", - "APP_RW", - ]), - ), - scope: z.optional(z.enum(["WORKSPACE", "PROJECT", "ENVIRONMENT"])), - project_name: z.optional(z.string()), - app_id: z.optional(z.string()), - google_project: z.optional(z.string()), - host_url: z.optional(z.string()), - webhook_address: z.optional(z.string()), - audience: z.optional(z.string()), - atop_url: z.optional(z.string()), - permissions: z.optional(zIntegrationPermissionsView), - all_pipelines_allowed: z.optional(z.boolean()), - allowed_pipelines: z.optional(z.array(zShortPipelineViewWritable)), -}); - -export const zIntegrationsViewWritable = z.object({ - integrations: z.optional(z.array(zIntegrationViewWritable)), +export const zShortProjectViewWritable = z.object({ + name: z.string().optional(), + display_name: z.string(), + status: z.string().optional(), + access: z.enum(["PRIVATE", "PUBLIC"]).optional(), + create_date: z.iso.datetime().optional(), }); export const zIdsViewWritable = z.object({ - domain: z.optional(z.string()), - project_identifier: z.optional(z.string()), - pipeline_id: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - environment_id: z.optional(z.string()), - pkg_id: z.optional(z.string()), - pkg_version_id: z.optional(z.string()), - sandbox_id: z.optional(z.string()), + domain: z.string().optional(), + project_identifier: z.string().optional(), + pipeline_id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + environment_id: z.string().optional(), + artifact_id: z.string().optional(), + artifact_version_id: z.string().optional(), + sandbox_id: z.string().optional(), + unit_test_suite_id: z.string().optional(), + visual_test_suite_id: z.string().optional(), + crawl_suite_id: z.string().optional(), + distribution_id: z.string().optional(), + route_id: z.string().optional(), }); /** * Sandbox reference */ export const zSandboxIdViewWritable = z.object({ - id: z.optional(z.string()), - identifier: z.optional(z.string()), - name: z.optional(z.string()), - status: z.optional( - z.enum([ - "STARTING", - "STOPPING", - "FAILED", - "RUNNING", - "STOPPED", - "RESTORING", - ]), - ), - setup_status: z.optional(z.enum(["INPROGRESS", "SUCCESS", "FAILED"])), + id: z.string().optional(), + identifier: z.string().optional(), + name: z.string().optional(), + status: z + .enum(["STARTING", "STOPPING", "FAILED", "RUNNING", "STOPPED", "RESTORING"]) + .optional(), + setup_status: z.enum(["INPROGRESS", "SUCCESS", "FAILED"]).optional(), }); /** * Integration reference */ export const zIntegrationIdViewWritable = z.object({ - identifier: z.optional(z.string()), - hash_id: z.optional(z.string()), - name: z.optional(z.string()), - type: z.optional( - z.enum([ + identifier: z.string().optional(), + hash_id: z.string().optional(), + name: z.string().optional(), + type: z + .enum([ "GIT_HUB", "BITBUCKET", "GOOGLE", @@ -1806,10 +1549,12 @@ export const zIntegrationIdViewWritable = z.object({ "OKTA", "CONTENTFUL", "JIRA", - ]), - ), - auth_type: z.optional( - z.enum([ + "NPM_REGISTRY", + "ANTHROPIC", + ]) + .optional(), + auth_type: z + .enum([ "OAUTH", "TOKEN", "API_KEY", @@ -1820,88 +1565,74 @@ export const zIntegrationIdViewWritable = z.object({ "OIDC", "TRUSTED", "APP_RW", - ]), - ), - host_url: z.optional(z.string()), - webhook_address: z.optional(z.string()), - atop_url: z.optional(z.string()), - app_id: z.optional(z.string()), - google_project: z.optional(z.string()), - audience: z.optional(z.string()), + ]) + .optional(), + host_url: z.string().optional(), + webhook_address: z.string().optional(), + atop_url: z.string().optional(), + app_id: z.string().optional(), + google_project: z.string().optional(), + audience: z.string().optional(), }); /** * User/member reference */ export const zMemberViewWritable = z.object({ - id: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - name: z.optional(z.string()), - avatar_url: z.optional(z.string()), - email: z.optional(z.string()), - admin: z.optional(z.boolean()), - workspace_owner: z.optional(z.boolean()), + id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + name: z.string().optional(), + avatar_url: z.string().optional(), + email: z.string().optional(), + admin: z.boolean().optional(), + workspace_owner: z.boolean().optional(), }); export const zProjectViewWritable = z.object({ - name: z.optional(z.string()), - display_name: z.string(), - status: z.optional(z.string()), - access: z.optional(z.enum(["PRIVATE", "PUBLIC"])), - create_date: z.optional(z.iso.datetime()), - external_project_id: z.optional(z.string()), - git_lab_project_id: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - custom_repo_url: z.optional(z.string()), - custom_repo_user: z.optional(z.string()), - custom_repo_pass: z.optional(z.string()), - custom_repo_ssh_key_id: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - created_by: z.optional(zMemberViewWritable), - http_repository: z.optional(z.string()), - ssh_repository: z.optional(z.string()), - default_branch: z.optional(z.string()), - integration: z.optional(zIntegrationIdViewWritable), - fetch_submodules: z.optional(z.boolean()), - fetch_submodules_env_key: z.optional(z.string()), - allow_pull_requests: z.optional(z.boolean()), - update_default_branch_from_external: z.optional(z.boolean()), - without_repository: z.optional(z.boolean()), -}); - -/** - * Short representation of a project - */ -export const zShortProjectViewWritable = z.object({ - name: z.optional(z.string()), + name: z.string().optional(), display_name: z.string(), - status: z.optional(z.string()), - access: z.optional(z.enum(["PRIVATE", "PUBLIC"])), - create_date: z.optional(z.iso.datetime()), + status: z.string().optional(), + access: z.enum(["PRIVATE", "PUBLIC"]).optional(), + create_date: z.iso.datetime().optional(), + external_project_id: z.string().optional(), + git_lab_project_id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + custom_repo_url: z.string().optional(), + custom_repo_user: z.string().optional(), + custom_repo_pass: z.string().optional(), + custom_repo_ssh_key_id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + created_by: zMemberViewWritable.optional(), + http_repository: z.string().optional(), + ssh_repository: z.string().optional(), + default_branch: z.string().optional(), + integration: zIntegrationIdViewWritable.optional(), + fetch_submodules: z.boolean().optional(), + fetch_submodules_env_key: z.string().optional(), + allow_pull_requests: z.boolean().optional(), + update_default_branch_from_external: z.boolean().optional(), + without_repository: z.boolean().optional(), }); /** @@ -1909,19 +1640,20 @@ export const zShortProjectViewWritable = z.object({ */ export const zAddVariableInObjectRequestWritable = z.object({ key: z.string(), - value: z.optional(z.string()), - settable: z.optional(z.boolean()), - run_only_settable: z.optional(z.boolean()), - encrypted: z.optional(z.boolean()), - description: z.optional(z.string()), - init_path: z.optional(z.string()), - defaults: z.optional(z.string()), - file_path: z.optional(z.string()), - file_chmod: z.optional(z.string()), - file_place: z.optional(z.enum(["NONE", "CONTAINER"])), - password: z.optional(z.string()), - passphrase: z.optional(z.string()), - key_identifier: z.optional(z.string()), + value: z.string().optional(), + settable: z.boolean().optional(), + run_only_settable: z.boolean().optional(), + encrypted: z.boolean().optional(), + description: z.string().optional(), + init_path: z.string().optional(), + defaults: z.string().optional(), + file_path: z.string().optional(), + file_chmod: z.string().optional(), + file_place: z.enum(["NONE", "CONTAINER"]).optional(), + password: z.string().optional(), + passphrase: z.string().optional(), + key_identifier: z.string().optional(), + disabled: z.boolean().optional(), type: z.enum([ "VAR", "FILE", @@ -1937,39 +1669,38 @@ export const zAddVariableInObjectRequestWritable = z.object({ * The TLS/SSL encryption settings of the tunnel */ export const zTlsSettingsViewWritable = z.object({ - private_key: z.optional(z.string()), - certificate: z.optional(z.string()), - ca_certificate: z.optional(z.string()), - terminate_at: z.optional(z.enum(["REGION", "AGENT", "TARGET"])), + private_key: z.string().optional(), + certificate: z.string().optional(), + ca_certificate: z.string().optional(), + terminate_at: z.enum(["REGION", "AGENT", "TARGET"]).optional(), }); /** * The HTTP-specific settings of the tunnel */ export const zHttpSettingsViewWritable = z.object({ - verify_certificate: z.optional(z.boolean()), - compression: z.optional(z.boolean()), - http2: z.optional(z.boolean()), - log_requests: z.optional(z.boolean()), - request_headers: z.optional(z.record(z.string(), z.string())), - whitelist_user_agents: z.optional(z.array(z.string())), - rewrite_host_header: z.optional(z.string()), - response_headers: z.optional(z.record(z.string(), z.string())), - login: z.optional(z.string()), - password: z.optional(z.string()), - tls_ca: z.optional(z.string()), - circuit_breaker: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - serve_path: z.optional(z.string()), - auth_type: z.optional(z.enum(["NONE", "BASIC", "BUDDY"])), + verify_certificate: z.boolean().optional(), + compression: z.boolean().optional(), + http2: z.boolean().optional(), + log_requests: z.boolean().optional(), + request_headers: z.record(z.string(), z.string()).optional(), + whitelist_user_agents: z.array(z.string()).optional(), + rewrite_host_header: z.string().optional(), + response_headers: z.record(z.string(), z.string()).optional(), + login: z.string().optional(), + password: z.string().optional(), + tls_ca: z.string().optional(), + circuit_breaker: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + serve_path: z.string().optional(), + auth_type: z.enum(["NONE", "BASIC", "BUDDY"]).optional(), }); export const zTunnelViewWritable = z.object({ @@ -1977,27 +1708,26 @@ export const zTunnelViewWritable = z.object({ endpoint: z.string(), type: z.enum(["TCP", "TLS", "HTTP", "SSH"]), region: z.enum(["US", "EU", "AS"]), - whitelist: z.optional(z.array(z.string())), - timeout: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - http: z.optional(zHttpSettingsViewWritable), - tls: z.optional(zTlsSettingsViewWritable), - endpoint_url: z.optional(z.string()), + whitelist: z.array(z.string()).optional(), + timeout: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + http: zHttpSettingsViewWritable.optional(), + tls: zTlsSettingsViewWritable.optional(), + endpoint_url: z.string().optional(), }); export const zUpdateSandboxRequestWritable = z.object({ - name: z.optional(z.string()), - identifier: z.optional(z.string()), - resources: z.optional( - z.enum([ + name: z.string().optional(), + identifier: z.string().optional(), + resources: z + .enum([ "1x2", "2x4", "3x6", @@ -2011,135 +1741,131 @@ export const zUpdateSandboxRequestWritable = z.object({ "11x22", "12x24", "CUSTOM", - ]), - ), - first_boot_commands: z.optional(z.string()), - app_dir: z.optional(z.string()), - apps: z.optional(z.array(zSandboxAppView)), - timeout: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - tags: z.optional(z.array(z.string())), - endpoints: z.optional(z.array(zTunnelViewWritable)), - variables: z.optional(z.array(zAddVariableInObjectRequestWritable)), - permissions: z.optional(zPermissionsView), + ]) + .optional(), + first_boot_commands: z.string().optional(), + app_dir: z.string().optional(), + apps: z.array(zSandboxAppView).optional(), + fetch: z.array(zSandboxFetchView).optional(), + timeout: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + tags: z.array(z.string()).optional(), + endpoints: z.array(zTunnelViewWritable).optional(), + variables: z.array(zAddVariableInObjectRequestWritable).optional(), + permissions: zPermissionsView.optional(), }); export const zSandboxesViewWritable = z.object({ - sandboxes: z.optional(z.array(zSandboxIdViewWritable)), + sandboxes: z.array(zSandboxIdViewWritable).optional(), }); /** * Content item in a sandbox */ export const zSandboxContentItemWritable = z.object({ - type: z.optional(z.enum(["FILE", "DIR"])), - name: z.optional(z.string()), - path: z.optional(z.string()), - size: z.optional( - z.coerce - .bigint() - .min(BigInt("-9223372036854775808"), { - error: "Invalid value: Expected int64 to be >= -9223372036854775808", - }) - .max(BigInt("9223372036854775807"), { - error: "Invalid value: Expected int64 to be <= 9223372036854775807", - }), - ), + type: z.enum(["FILE", "DIR"]).optional(), + name: z.string().optional(), + path: z.string().optional(), + size: z.coerce + .bigint() + .min(BigInt("-9223372036854775808"), { + error: "Invalid value: Expected int64 to be >= -9223372036854775808", + }) + .max(BigInt("9223372036854775807"), { + error: "Invalid value: Expected int64 to be <= 9223372036854775807", + }) + .optional(), }); /** * Sandbox content listing */ export const zSandboxContentViewWritable = z.object({ - contents: z.optional(z.array(zSandboxContentItemWritable)), + contents: z.array(zSandboxContentItemWritable).optional(), }); export const zSandboxCommandViewWritable = z.object({ - id: z.optional(z.string()), - command: z.optional(z.string()), - runtime: z.optional(z.enum(["BASH", "JAVASCRIPT", "TYPESCRIPT", "PYTHON"])), - status: z.optional(z.enum(["INPROGRESS", "SUCCESSFUL", "FAILED"])), - exit_code: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - start_date: z.optional(z.iso.datetime()), - finish_date: z.optional(z.iso.datetime()), - logs_url: z.optional(z.string()), + id: z.string().optional(), + command: z.string().optional(), + runtime: z.enum(["BASH", "JAVASCRIPT", "TYPESCRIPT", "PYTHON"]).optional(), + status: z.enum(["INPROGRESS", "SUCCESSFUL", "FAILED"]).optional(), + exit_code: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + start_date: z.iso.datetime().optional(), + finish_date: z.iso.datetime().optional(), + logs_url: z.string().optional(), }); export const zSandboxCommandsViewWritable = z.object({ - commands: z.optional(z.array(zSandboxCommandViewWritable)), + commands: z.array(zSandboxCommandViewWritable).optional(), }); export const zSandboxAppLogsViewWritable = z.object({ - cursor: z.optional(z.string()), - logs: z.optional(z.array(z.string())), + cursor: z.string().optional(), + logs: z.array(z.string()).optional(), }); export const zShortSnapshotViewWritable = z.object({ - id: z.optional(z.string()), - name: z.optional(z.string()), - size: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - status: z.optional(z.enum(["CREATING", "CREATED", "DELETING", "FAILED"])), - create_date: z.optional(z.iso.datetime()), + id: z.string().optional(), + name: z.string().optional(), + size: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + status: z.enum(["CREATING", "CREATED", "DELETING", "FAILED"]).optional(), + create_date: z.iso.datetime().optional(), }); export const zSnapshotsViewWritable = z.object({ - snapshots: z.optional(z.array(zShortSnapshotViewWritable)), + snapshots: z.array(zShortSnapshotViewWritable).optional(), }); export const zSnapshotViewWritable = z.object({ - id: z.optional(z.string()), - name: z.optional(z.string()), - size: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - status: z.optional(z.enum(["CREATING", "CREATED", "DELETING", "FAILED"])), - create_date: z.optional(z.iso.datetime()), - created_by: z.optional(zMemberViewWritable), + id: z.string().optional(), + name: z.string().optional(), + size: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + status: z.enum(["CREATING", "CREATED", "DELETING", "FAILED"]).optional(), + create_date: z.iso.datetime().optional(), + created_by: zMemberViewWritable.optional(), }); export const zSandboxYamlViewWritable = z.object({ - yaml: z.optional(z.string()), + yaml: z.string().optional(), }); export const zCreateNewSandboxRequestWritable = z.object({ name: z.string(), - identifier: z.optional(z.string()), + identifier: z.string().optional(), os: z.string(), - resources: z.optional( - z.enum([ + resources: z + .enum([ "1x2", "2x4", "3x6", @@ -2153,34 +1879,68 @@ export const zCreateNewSandboxRequestWritable = z.object({ "11x22", "12x24", "CUSTOM", - ]), - ), - first_boot_commands: z.optional(z.string()), - app_dir: z.optional(z.string()), - apps: z.optional(z.array(z.string())), - tags: z.optional(z.array(z.string())), - endpoints: z.optional(z.array(zTunnelViewWritable)), - variables: z.optional(z.array(zAddVariableInObjectRequestWritable)), - timeout: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - permissions: z.optional(zPermissionsView), + ]) + .optional(), + first_boot_commands: z.string().optional(), + app_dir: z.string().optional(), + apps: z.array(z.string()).optional(), + fetch: z.array(zSandboxFetchView).optional(), + tags: z.array(z.string()).optional(), + endpoints: z.array(zTunnelViewWritable).optional(), + variables: z.array(zAddVariableInObjectRequestWritable).optional(), + timeout: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + permissions: zPermissionsView.optional(), }); export const zCreateFromSnapshotRequestWritable = z.object({ snapshot_id: z.string(), name: z.string(), - identifier: z.optional(z.string()), - os: z.optional(z.string()), - resources: z.optional( - z.enum([ + identifier: z.string().optional(), + os: z.string().optional(), + resources: z + .enum([ + "1x2", + "2x4", + "3x6", + "4x8", + "5x10", + "6x12", + "7x14", + "8x16", + "9x18", + "10x20", + "11x22", + "12x24", + "CUSTOM", + ]) + .optional(), + first_boot_commands: z.string().optional(), + app_dir: z.string().optional(), + apps: z.array(z.string()).optional(), + tags: z.array(z.string()).optional(), + endpoints: z.array(zTunnelViewWritable).optional(), + variables: z.array(zEnvironmentVariableView).optional(), +}); + +export const zSandboxResponseWritable = z.object({ + id: z.string().optional(), + identifier: z.string().optional(), + name: z.string().optional(), + status: z + .enum(["STARTING", "STOPPING", "FAILED", "RUNNING", "STOPPED", "RESTORING"]) + .optional(), + setup_status: z.enum(["INPROGRESS", "SUCCESS", "FAILED", "STALE"]).optional(), + os: z.string().optional(), + resources: z + .enum([ "1x2", "2x4", "3x6", @@ -2194,156 +1954,578 @@ export const zCreateFromSnapshotRequestWritable = z.object({ "11x22", "12x24", "CUSTOM", - ]), - ), - first_boot_commands: z.optional(z.string()), - app_dir: z.optional(z.string()), - apps: z.optional(z.array(z.string())), - tags: z.optional(z.array(z.string())), - endpoints: z.optional(z.array(zTunnelViewWritable)), - variables: z.optional(z.array(zEnvironmentVariableView)), + ]) + .optional(), + first_boot_commands: z.string().optional(), + app_dir: z.string().optional(), + apps: z.array(zSandboxAppView).optional(), + fetch: z.array(zSandboxFetchView).optional(), + timeout: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + tags: z.array(z.string()).optional(), + boot_logs: z.array(z.string()).optional(), + endpoints: z.array(zTunnelViewWritable).optional(), + ssh_host: z.string().optional(), + ssh_port: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + project: zProjectViewWritable.optional(), + permissions: zPermissionsView.optional(), + variables: z.array(zEnvironmentVariableView).optional(), +}); + +export const zProjectsViewWritable = z.object({ + projects: z.array(zShortProjectViewWritable).optional(), +}); + +export const zIntegrationsView = z.object({ + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + integrations: z.array(z.lazy((): any => zIntegrationView)).optional(), +}); + +/** + * The list of events that trigger the pipeline run + */ +export const zPipelineEventView = z.object({ + type: z + .enum([ + "PUSH", + "CREATE_REF", + "DELETE_REF", + "PULL_REQUEST", + "SCHEDULE", + "PUBLISH_ARTIFACT_VERSION", + "DELETE_ARTIFACT_VERSION", + "WEBHOOK", + "EMAIL", + "CREATE_ARTIFACT_VERSION", + "ENVIRONMENT_CREATE", + "ENVIRONMENT_DELETE", + "SANDBOX_CREATED", + "SANDBOX_DELETED", + "SANDBOX_TIMED_OUT", + ]) + .optional(), + refs: z.array(z.string()).optional(), + events: z.array(z.string()).optional(), + branches: z.array(z.string()).optional(), + artifacts: z.array(zPipelineArtifactContextView).optional(), + environments: z.array(zPipelineEnvironmentContextView).optional(), + start_date: z.iso.datetime().optional(), + delay: z.coerce + .bigint() + .min(BigInt("-9223372036854775808"), { + error: "Invalid value: Expected int64 to be >= -9223372036854775808", + }) + .max(BigInt("9223372036854775807"), { + error: "Invalid value: Expected int64 to be <= 9223372036854775807", + }) + .optional(), + cron: z.string().optional(), + timezone: z.string().optional(), + totp: z.boolean().optional(), + prefix: z.string().optional(), + whitelist: z.array(z.string()).optional(), + targets: z.array(z.unknown()).optional(), +}); + +/** + * Short representation of a pipeline + */ +export const zShortPipelineView = z.object({ + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + identifier: z.string().optional(), + name: z.string().optional(), + definition_source: z.enum(["LOCAL", "REMOTE"]).optional(), + git_config_ref: z.enum(["NONE", "DYNAMIC", "FIXED"]).optional(), + refs: z.array(z.string()).optional(), + events: z.array(zPipelineEventView).optional(), + loop: z.array(z.string()).optional(), + priority: z.enum(["LOW", "NORMAL", "HIGH"]).optional(), + disabled: z.boolean().optional(), + disabled_reason: z.string().optional(), + last_execution_status: z + .enum([ + "INPROGRESS", + "ENQUEUED", + "TERMINATED", + "SUCCESSFUL", + "FAILED", + "INITIAL", + "NOT_EXECUTED", + "SKIPPED", + "TERMINATING", + "WAITING_FOR_APPLY", + "WAITING_FOR_VARIABLES", + "WAITING_FOR_SETTABLE_VARIABLES", + "WAITING_FOR_VT_SESSION", + ]) + .optional(), + last_execution_revision: z.string().optional(), + target_site_url: z.string().optional(), + execution_message_template: z.string().optional(), + create_date: z.iso.datetime().optional(), + always_from_scratch: z.boolean().optional(), + ignore_fail_on_project_status: z.boolean().optional(), + no_skip_to_most_recent: z.boolean().optional(), + terminate_stale_runs: z.boolean().optional(), + auto_clear_cache: z.boolean().optional(), + paused: z.boolean().optional(), + pause_on_repeated_failures: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + fetch_all_refs: z.boolean().optional(), + fetch_lfs: z.boolean().optional(), + fail_on_prepare_env_warning: z.boolean().optional(), + concurrent_pipeline_runs: z.boolean().optional(), + clone_depth: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + do_not_create_commit_status: z.boolean().optional(), + stale: z.boolean().optional(), + waiting_for_push: z.boolean().optional(), + resources: z + .enum([ + "DEFAULT", + "NANO", + "SMALL", + "MEDIUM", + "LARGE", + "XLARGE", + "CUSTOM", + "X2LARGE", + ]) + .optional(), + remote_path: z.string().optional(), + remote_ref: z.string().optional(), + remote_project_name: z.string().optional(), + remote_parameters: z.array(zPipelinePropertyView).optional(), + git_config: zYamlDefinitionView.optional(), + tags: z.array(z.string()).optional(), + git_changeset_base: z + .enum(["LATEST_RUN", "LATEST_RUN_MATCHING_REF", "PULL_REQUEST"]) + .optional(), + filesystem_changeset_base: z.enum(["DATE_MODIFIED", "CONTENTS"]).optional(), + cpu: z.enum(["X64", "ARM", "X86"]).optional(), + description_required: z.boolean().optional(), + folder: z.string().optional(), +}); + +/** + * The integration to use for authentication + */ +export const zIntegrationView = z.object({ + url: z.string().readonly().optional(), + html_url: z.string().readonly().optional(), + identifier: z.string().optional(), + hash_id: z.string().optional(), + name: z.string().optional(), + type: z + .enum([ + "GIT_HUB", + "BITBUCKET", + "GOOGLE", + "DIGITAL_OCEAN", + "SLACK", + "MODULUS", + "HEROKU", + "AMAZON", + "GIT_LAB", + "SHOPIFY", + "GIT_HUB_ENTERPRISE", + "GIT_LAB_ENTERPRISE", + "PUSHOVER", + "PUSHBULLET", + "RACKSPACE", + "CUSTOM", + "CLOUDFLARE", + "NEW_RELIC", + "SENTRY", + "ROLLBAR", + "DATADOG", + "DO_SPACES", + "HONEYBADGER", + "VULTR", + "SENTRY_ENTERPRISE", + "LOGGLY", + "HIP_CHAT", + "FIREBASE", + "TELEGRAM", + "AZURE", + "UPCLOUD", + "GHOST_INSPECTOR", + "NETLIFY", + "AZURE_CLOUD", + "MICROSOFT_TEAMS", + "GOOGLE_SERVICE_ACCOUNT", + "GOOGLE_PLAY_STORE", + "DOCKER_HUB", + "APP_STORE", + "GIT_HUB_APP", + "GIT_HUB_APP_ENTERPRISE", + "GIT_HUB_API", + "ATOP", + "SNYK", + "STACK_HAWK", + "BLACKFIRE", + "BACKBLAZE", + "ONE_LOGIN", + "OKTA", + "CONTENTFUL", + "JIRA", + "NPM_REGISTRY", + "ANTHROPIC", + ]) + .optional(), + auth_type: z + .enum([ + "OAUTH", + "TOKEN", + "API_KEY", + "APP", + "APP_SPRYKER", + "TOKEN_APP_EXTENSION", + "DEFAULT", + "OIDC", + "TRUSTED", + "APP_RW", + ]) + .optional(), + scope: z.enum(["WORKSPACE", "PROJECT", "ENVIRONMENT"]).optional(), + project_name: z.string().optional(), + app_id: z.string().optional(), + google_project: z.string().optional(), + host_url: z.string().optional(), + webhook_address: z.string().optional(), + audience: z.string().optional(), + atop_url: z.string().optional(), + permissions: zIntegrationPermissionsView.optional(), + all_pipelines_allowed: z.boolean().optional(), + allowed_pipelines: z.array(zShortPipelineView).optional(), + disabled: z.boolean().optional(), +}); + +export const zIntegrationsViewWritable = z.object({ + integrations: z.array(z.lazy((): any => zIntegrationViewWritable)).optional(), +}); + +/** + * The list of events that trigger the pipeline run + */ +export const zPipelineEventViewWritable = z.object({ + type: z + .enum([ + "PUSH", + "CREATE_REF", + "DELETE_REF", + "PULL_REQUEST", + "SCHEDULE", + "PUBLISH_ARTIFACT_VERSION", + "DELETE_ARTIFACT_VERSION", + "WEBHOOK", + "EMAIL", + "CREATE_ARTIFACT_VERSION", + "ENVIRONMENT_CREATE", + "ENVIRONMENT_DELETE", + "SANDBOX_CREATED", + "SANDBOX_DELETED", + "SANDBOX_TIMED_OUT", + ]) + .optional(), + refs: z.array(z.string()).optional(), + events: z.array(z.string()).optional(), + branches: z.array(z.string()).optional(), + artifacts: z.array(zPipelineArtifactContextView).optional(), + environments: z.array(zPipelineEnvironmentContextView).optional(), + start_date: z.iso.datetime().optional(), + delay: z.coerce + .bigint() + .min(BigInt("-9223372036854775808"), { + error: "Invalid value: Expected int64 to be >= -9223372036854775808", + }) + .max(BigInt("9223372036854775807"), { + error: "Invalid value: Expected int64 to be <= 9223372036854775807", + }) + .optional(), + cron: z.string().optional(), + timezone: z.string().optional(), + totp: z.boolean().optional(), + prefix: z.string().optional(), + whitelist: z.array(z.string()).optional(), + targets: z.array(z.unknown()).optional(), }); -export const zSandboxResponseWritable = z.object({ - id: z.optional(z.string()), - identifier: z.optional(z.string()), - name: z.optional(z.string()), - status: z.optional( - z.enum([ - "STARTING", - "STOPPING", +/** + * Short representation of a pipeline + */ +export const zShortPipelineViewWritable = z.object({ + id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + identifier: z.string().optional(), + name: z.string().optional(), + definition_source: z.enum(["LOCAL", "REMOTE"]).optional(), + git_config_ref: z.enum(["NONE", "DYNAMIC", "FIXED"]).optional(), + refs: z.array(z.string()).optional(), + events: z.array(zPipelineEventViewWritable).optional(), + loop: z.array(z.string()).optional(), + priority: z.enum(["LOW", "NORMAL", "HIGH"]).optional(), + disabled: z.boolean().optional(), + disabled_reason: z.string().optional(), + last_execution_status: z + .enum([ + "INPROGRESS", + "ENQUEUED", + "TERMINATED", + "SUCCESSFUL", "FAILED", - "RUNNING", - "STOPPED", - "RESTORING", - ]), - ), - setup_status: z.optional( - z.enum(["INPROGRESS", "SUCCESS", "FAILED", "STALE"]), - ), - os: z.optional(z.string()), - resources: z.optional( - z.enum([ - "1x2", - "2x4", - "3x6", - "4x8", - "5x10", - "6x12", - "7x14", - "8x16", - "9x18", - "10x20", - "11x22", - "12x24", + "INITIAL", + "NOT_EXECUTED", + "SKIPPED", + "TERMINATING", + "WAITING_FOR_APPLY", + "WAITING_FOR_VARIABLES", + "WAITING_FOR_SETTABLE_VARIABLES", + "WAITING_FOR_VT_SESSION", + ]) + .optional(), + last_execution_revision: z.string().optional(), + target_site_url: z.string().optional(), + execution_message_template: z.string().optional(), + create_date: z.iso.datetime().optional(), + always_from_scratch: z.boolean().optional(), + ignore_fail_on_project_status: z.boolean().optional(), + no_skip_to_most_recent: z.boolean().optional(), + terminate_stale_runs: z.boolean().optional(), + auto_clear_cache: z.boolean().optional(), + paused: z.boolean().optional(), + pause_on_repeated_failures: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + fetch_all_refs: z.boolean().optional(), + fetch_lfs: z.boolean().optional(), + fail_on_prepare_env_warning: z.boolean().optional(), + concurrent_pipeline_runs: z.boolean().optional(), + clone_depth: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + do_not_create_commit_status: z.boolean().optional(), + stale: z.boolean().optional(), + waiting_for_push: z.boolean().optional(), + resources: z + .enum([ + "DEFAULT", + "NANO", + "SMALL", + "MEDIUM", + "LARGE", + "XLARGE", "CUSTOM", - ]), - ), - first_boot_commands: z.optional(z.string()), - app_dir: z.optional(z.string()), - apps: z.optional(z.array(zSandboxAppView)), - timeout: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - tags: z.optional(z.array(z.string())), - boot_logs: z.optional(z.array(z.string())), - endpoints: z.optional(z.array(zTunnelViewWritable)), - project: z.optional(zProjectViewWritable), - permissions: z.optional(zPermissionsView), - variables: z.optional(z.array(zEnvironmentVariableView)), -}); - -export const zProjectsViewWritable = z.object({ - projects: z.optional(z.array(zShortProjectViewWritable)), + "X2LARGE", + ]) + .optional(), + remote_path: z.string().optional(), + remote_ref: z.string().optional(), + remote_project_name: z.string().optional(), + remote_parameters: z.array(zPipelinePropertyView).optional(), + git_config: zYamlDefinitionView.optional(), + tags: z.array(z.string()).optional(), + git_changeset_base: z + .enum(["LATEST_RUN", "LATEST_RUN_MATCHING_REF", "PULL_REQUEST"]) + .optional(), + filesystem_changeset_base: z.enum(["DATE_MODIFIED", "CONTENTS"]).optional(), + cpu: z.enum(["X64", "ARM", "X86"]).optional(), + description_required: z.boolean().optional(), + folder: z.string().optional(), }); -export const zGetWorkspacesData = z.object({ - body: z.optional(z.never()), - path: z.optional(z.never()), - query: z.optional(z.never()), +/** + * The integration to use for authentication + */ +export const zIntegrationViewWritable = z.object({ + identifier: z.string().optional(), + hash_id: z.string().optional(), + name: z.string().optional(), + type: z + .enum([ + "GIT_HUB", + "BITBUCKET", + "GOOGLE", + "DIGITAL_OCEAN", + "SLACK", + "MODULUS", + "HEROKU", + "AMAZON", + "GIT_LAB", + "SHOPIFY", + "GIT_HUB_ENTERPRISE", + "GIT_LAB_ENTERPRISE", + "PUSHOVER", + "PUSHBULLET", + "RACKSPACE", + "CUSTOM", + "CLOUDFLARE", + "NEW_RELIC", + "SENTRY", + "ROLLBAR", + "DATADOG", + "DO_SPACES", + "HONEYBADGER", + "VULTR", + "SENTRY_ENTERPRISE", + "LOGGLY", + "HIP_CHAT", + "FIREBASE", + "TELEGRAM", + "AZURE", + "UPCLOUD", + "GHOST_INSPECTOR", + "NETLIFY", + "AZURE_CLOUD", + "MICROSOFT_TEAMS", + "GOOGLE_SERVICE_ACCOUNT", + "GOOGLE_PLAY_STORE", + "DOCKER_HUB", + "APP_STORE", + "GIT_HUB_APP", + "GIT_HUB_APP_ENTERPRISE", + "GIT_HUB_API", + "ATOP", + "SNYK", + "STACK_HAWK", + "BLACKFIRE", + "BACKBLAZE", + "ONE_LOGIN", + "OKTA", + "CONTENTFUL", + "JIRA", + "NPM_REGISTRY", + "ANTHROPIC", + ]) + .optional(), + auth_type: z + .enum([ + "OAUTH", + "TOKEN", + "API_KEY", + "APP", + "APP_SPRYKER", + "TOKEN_APP_EXTENSION", + "DEFAULT", + "OIDC", + "TRUSTED", + "APP_RW", + ]) + .optional(), + scope: z.enum(["WORKSPACE", "PROJECT", "ENVIRONMENT"]).optional(), + project_name: z.string().optional(), + app_id: z.string().optional(), + google_project: z.string().optional(), + host_url: z.string().optional(), + webhook_address: z.string().optional(), + audience: z.string().optional(), + atop_url: z.string().optional(), + permissions: zIntegrationPermissionsView.optional(), + all_pipelines_allowed: z.boolean().optional(), + allowed_pipelines: z.array(zShortPipelineViewWritable).optional(), + disabled: z.boolean().optional(), }); export const zGetWorkspacesResponse = zWorkspacesView; -export const zGetWorkspaceData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - }), - query: z.optional(z.never()), +export const zGetWorkspacePath = z.object({ + workspace_domain: z.string(), }); export const zGetWorkspaceResponse = zWorkspaceView; -export const zDisableSsoData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - }), - query: z.optional(z.never()), -}); - -export const zEnableSsoData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - }), - query: z.optional(z.never()), -}); - -export const zGetIdentifiersData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - }), - query: z.optional( - z.object({ - project: z.optional(z.string()), - pipeline: z.optional(z.string()), - environment: z.optional(z.string()), - package: z.optional(z.string()), - package_version: z.optional(z.string()), - sandbox: z.optional(z.string()), - }), - ), +export const zDisableSsoPath = z.object({ + workspace_domain: z.string(), }); -export const zGetIdentifiersResponse = zIdsView; - -export const zGetIntegrationsData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - }), - query: z.optional(z.never()), +export const zEnableSsoPath = z.object({ + workspace_domain: z.string(), }); -export const zGetIntegrationsResponse = zIntegrationsView; +export const zGetIdentifiersPath = z.object({ + workspace_domain: z.string(), +}); -export const zAddIntegrationData = z.object({ - body: z.optional(zAddIntegrationRequest), - path: z.object({ - workspace_domain: z.string(), - }), - query: z.optional(z.never()), +export const zGetIdentifiersQuery = z.object({ + project: z.string().optional(), + pipeline: z.string().optional(), + environment: z.string().optional(), + artifact: z.string().optional(), + artifact_version: z.string().optional(), + sandbox: z.string().optional(), + unit_test_suite: z.string().optional(), + visual_test_suite: z.string().optional(), + crawl_suite: z.string().optional(), + distribution: z.string().optional(), + route_subdomain: z.string().optional(), + route_domain: z.string().optional(), + route_path: z.string().optional(), }); -export const zAddIntegrationResponse = zIntegrationView; +export const zGetIdentifiersResponse = zIdsView; -export const zDeleteIntegrationData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - hash_id: z.string(), - }), - query: z.optional(z.never()), +export const zDeleteIntegrationPath = z.object({ + workspace_domain: z.string(), + hash_id: z.string(), }); /** @@ -2351,87 +2533,53 @@ export const zDeleteIntegrationData = z.object({ */ export const zDeleteIntegrationResponse = z.void(); -export const zGetIntegrationData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - hash_id: z.string(), - }), - query: z.optional(z.never()), -}); - -export const zGetIntegrationResponse = zIntegrationView; - -export const zUpdateIntegrationData = z.object({ - body: z.optional(zUpdateIntegrationRequest), - path: z.object({ - workspace_domain: z.string(), - hash_id: z.string(), - }), - query: z.optional(z.never()), +export const zGetWorkspaceMembersPath = z.object({ + workspace_domain: z.string(), }); -export const zUpdateIntegrationResponse = zIntegrationView; - -export const zGetWorkspaceMembersData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - }), - query: z.optional( - z.object({ - page: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - per_page: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - sort_by: z.optional(z.string()), - sort_direction: z.optional(z.string()), - }), - ), +export const zGetWorkspaceMembersQuery = z.object({ + page: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + per_page: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + sort_by: z.string().optional(), + sort_direction: z.string().optional(), }); export const zGetWorkspaceMembersResponse = zWorkspaceMembersView; -export const zAddWorkspaceMemberData = z.object({ - body: z.optional(zAddWorkspaceMemberRequest), - path: z.object({ - workspace_domain: z.string(), - }), - query: z.optional(z.never()), +export const zAddWorkspaceMemberBody = zAddWorkspaceMemberRequest; + +export const zAddWorkspaceMemberPath = z.object({ + workspace_domain: z.string(), }); export const zAddWorkspaceMemberResponse = zWorkspaceMemberView; -export const zDeleteWorkspaceMemberData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - id: z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - }), - query: z.optional(z.never()), +export const zDeleteWorkspaceMemberPath = z.object({ + workspace_domain: z.string(), + id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }), }); /** @@ -2439,124 +2587,104 @@ export const zDeleteWorkspaceMemberData = z.object({ */ export const zDeleteWorkspaceMemberResponse = z.void(); -export const zGetWorkspaceMemberData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - id: z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - }), - query: z.optional(z.never()), +export const zGetWorkspaceMemberPath = z.object({ + workspace_domain: z.string(), + id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }), }); export const zGetWorkspaceMemberResponse = zWorkspaceMemberView; -export const zUpdateWorkspaceMemberData = z.object({ - body: z.optional(zUpdateWorkspaceMemberRequest), - path: z.object({ - workspace_domain: z.string(), - id: z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - }), - query: z.optional(z.never()), +export const zUpdateWorkspaceMemberBody = zUpdateWorkspaceMemberRequest; + +export const zUpdateWorkspaceMemberPath = z.object({ + workspace_domain: z.string(), + id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }), }); export const zUpdateWorkspaceMemberResponse = zWorkspaceMemberView; -export const zGetWorkspaceMemberProjectsData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - user_id: z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - }), - query: z.optional( - z.object({ - page: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - per_page: z.optional( - z - .int() - .min(-2147483648, { - error: "Invalid value: Expected int32 to be >= -2147483648", - }) - .max(2147483647, { - error: "Invalid value: Expected int32 to be <= 2147483647", - }), - ), - status: z.optional(z.string()), - sort_by: z.optional(z.string()), - sort_direction: z.optional(z.string()), - all: z.optional(z.string()), +export const zGetWorkspaceMemberProjectsPath = z.object({ + workspace_domain: z.string(), + user_id: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", }), - ), +}); + +export const zGetWorkspaceMemberProjectsQuery = z.object({ + page: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + per_page: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + status: z.string().optional(), + sort_by: z.string().optional(), + sort_direction: z.string().optional(), + all: z.string().optional(), }); export const zGetWorkspaceMemberProjectsResponse = zProjectsView; -export const zGetSandboxesData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - }), - query: z.object({ - project_name: z.string(), - }), +export const zGetSandboxesPath = z.object({ + workspace_domain: z.string(), +}); + +export const zGetSandboxesQuery = z.object({ + project_name: z.string(), }); export const zGetSandboxesResponse = zSandboxesView; -export const zAddSandboxData = z.object({ - body: z.optional( - z.union([ - zCloneSandboxRequest, - zCreateFromSnapshotRequestWritable, - zCreateNewSandboxRequestWritable, - ]), - ), - path: z.object({ - workspace_domain: z.string(), - }), - query: z.object({ - project_name: z.string(), - }), +export const zAddSandboxBody = z.union([ + zCloneSandboxRequest, + zCreateFromSnapshotRequestWritable, + zCreateNewSandboxRequestWritable, +]); + +export const zAddSandboxPath = z.object({ + workspace_domain: z.string(), +}); + +export const zAddSandboxQuery = z.object({ + project_name: z.string(), }); export const zAddSandboxResponse = zSandboxResponse; -export const zDeleteSandboxData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - id: z.string(), - }), - query: z.optional(z.never()), +export const zDeleteSandboxPath = z.object({ + workspace_domain: z.string(), + id: z.string(), }); /** @@ -2564,138 +2692,98 @@ export const zDeleteSandboxData = z.object({ */ export const zDeleteSandboxResponse = z.void(); -export const zGetSandboxData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - id: z.string(), - }), - query: z.optional(z.never()), +export const zGetSandboxPath = z.object({ + workspace_domain: z.string(), + id: z.string(), }); export const zGetSandboxResponse = zSandboxResponse; -export const zUpdateSandboxData = z.object({ - body: z.optional(zUpdateSandboxRequestWritable), - path: z.object({ - workspace_domain: z.string(), - id: z.string(), - }), - query: z.optional(z.never()), +export const zUpdateSandboxBody = zUpdateSandboxRequestWritable; + +export const zUpdateSandboxPath = z.object({ + workspace_domain: z.string(), + id: z.string(), }); export const zUpdateSandboxResponse = zSandboxResponse; -export const zGetSandboxAppLogsByIdData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - sandbox_id: z.string(), - app_id: z.string(), - }), - query: z.optional( - z.object({ - cursor: z.optional(z.string()), - }), - ), +export const zGetSandboxAppLogsByIdPath = z.object({ + workspace_domain: z.string(), + sandbox_id: z.string(), + app_id: z.string(), +}); + +export const zGetSandboxAppLogsByIdQuery = z.object({ + cursor: z.string().optional(), }); export const zGetSandboxAppLogsByIdResponse = zSandboxAppLogsView; -export const zStartSandboxAppData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - sandbox_id: z.string(), - app_id: z.string(), - }), - query: z.optional(z.never()), +export const zStartSandboxAppPath = z.object({ + workspace_domain: z.string(), + sandbox_id: z.string(), + app_id: z.string(), }); export const zStartSandboxAppResponse = zSandboxResponse; -export const zStopSandboxAppData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - sandbox_id: z.string(), - app_id: z.string(), - }), - query: z.optional(z.never()), +export const zStopSandboxAppPath = z.object({ + workspace_domain: z.string(), + sandbox_id: z.string(), + app_id: z.string(), }); export const zStopSandboxAppResponse = zSandboxResponse; -export const zGetSandboxCommandsData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - sandbox_id: z.string(), - }), - query: z.optional(z.never()), +export const zGetSandboxCommandsPath = z.object({ + workspace_domain: z.string(), + sandbox_id: z.string(), }); export const zGetSandboxCommandsResponse = zSandboxCommandsView; -export const zExecuteSandboxCommandData = z.object({ - body: z.optional(zExecuteSandboxCommandRequest), - path: z.object({ - workspace_domain: z.string(), - sandbox_id: z.string(), - }), - query: z.optional(z.never()), +export const zExecuteSandboxCommandBody = zExecuteSandboxCommandRequest; + +export const zExecuteSandboxCommandPath = z.object({ + workspace_domain: z.string(), + sandbox_id: z.string(), }); export const zExecuteSandboxCommandResponse = zSandboxCommandView; -export const zGetSandboxCommandData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - sandbox_id: z.string(), - id: z.string(), - }), - query: z.optional(z.never()), +export const zGetSandboxCommandPath = z.object({ + workspace_domain: z.string(), + sandbox_id: z.string(), + id: z.string(), }); export const zGetSandboxCommandResponse = zSandboxCommandView; -export const zGetSandboxCommandLogsData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - sandbox_id: z.string(), - command_id: z.string(), - }), - query: z.optional( - z.object({ - follow: z.optional(z.boolean()), - }), - ), +export const zGetSandboxCommandLogsPath = z.object({ + workspace_domain: z.string(), + sandbox_id: z.string(), + command_id: z.string(), +}); + +export const zGetSandboxCommandLogsQuery = z.object({ + follow: z.boolean().optional(), }); export const zGetSandboxCommandLogsResponse = zSandboxCommandLog; -export const zTerminateSandboxCommandData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - sandbox_id: z.string(), - command_id: z.string(), - }), - query: z.optional(z.never()), +export const zTerminateSandboxCommandPath = z.object({ + workspace_domain: z.string(), + sandbox_id: z.string(), + command_id: z.string(), }); export const zTerminateSandboxCommandResponse = zSandboxCommandView; -export const zDeleteSandboxFileData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - sandbox_id: z.string(), - path: z.string().regex(/.*/), - }), - query: z.optional(z.never()), +export const zDeleteSandboxFilePath = z.object({ + workspace_domain: z.string(), + sandbox_id: z.string(), + path: z.string().regex(/.*/), }); /** @@ -2703,14 +2791,10 @@ export const zDeleteSandboxFileData = z.object({ */ export const zDeleteSandboxFileResponse = z.void(); -export const zGetSandboxContentData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - sandbox_id: z.string(), - path: z.string().regex(/.*/), - }), - query: z.optional(z.never()), +export const zGetSandboxContentPath = z.object({ + workspace_domain: z.string(), + sandbox_id: z.string(), + path: z.string().regex(/.*/), }); /** @@ -2718,14 +2802,10 @@ export const zGetSandboxContentData = z.object({ */ export const zGetSandboxContentResponse = zSandboxContentView; -export const zCreateSandboxDirectoryData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - sandbox_id: z.string(), - path: z.string().regex(/.*/), - }), - query: z.optional(z.never()), +export const zCreateSandboxDirectoryPath = z.object({ + workspace_domain: z.string(), + sandbox_id: z.string(), + path: z.string().regex(/.*/), }); /** @@ -2733,14 +2813,15 @@ export const zCreateSandboxDirectoryData = z.object({ */ export const zCreateSandboxDirectoryResponse = zSandboxContentItem; -export const zUploadSandboxFileData = z.object({ - body: z.optional(z.string()), - path: z.object({ - workspace_domain: z.string(), - sandbox_id: z.string(), - path: z.string().regex(/.*/), - }), - query: z.optional(z.never()), +/** + * File to upload + */ +export const zUploadSandboxFileBody = z.string(); + +export const zUploadSandboxFilePath = z.object({ + workspace_domain: z.string(), + sandbox_id: z.string(), + path: z.string().regex(/.*/), }); /** @@ -2748,14 +2829,10 @@ export const zUploadSandboxFileData = z.object({ */ export const zUploadSandboxFileResponse = zSandboxContentItem; -export const zDownloadSandboxContentData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - sandbox_id: z.string(), - path: z.string().regex(/.*/), - }), - query: z.optional(z.never()), +export const zDownloadSandboxContentPath = z.object({ + workspace_domain: z.string(), + sandbox_id: z.string(), + path: z.string().regex(/.*/), }); /** @@ -2763,47 +2840,33 @@ export const zDownloadSandboxContentData = z.object({ */ export const zDownloadSandboxContentResponse = z.string(); -export const zRestartSandboxData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - sandbox_id: z.string(), - }), - query: z.optional(z.never()), +export const zRestartSandboxPath = z.object({ + workspace_domain: z.string(), + sandbox_id: z.string(), }); export const zRestartSandboxResponse = zSandboxResponse; -export const zGetSandboxSnapshotsData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - sandbox_id: z.string(), - }), - query: z.optional(z.never()), +export const zGetSandboxSnapshotsPath = z.object({ + workspace_domain: z.string(), + sandbox_id: z.string(), }); export const zGetSandboxSnapshotsResponse = zSnapshotsView; -export const zAddSandboxSnapshotData = z.object({ - body: z.optional(zAddSnapshotRequest), - path: z.object({ - workspace_domain: z.string(), - sandbox_id: z.string(), - }), - query: z.optional(z.never()), +export const zAddSandboxSnapshotBody = zAddSnapshotRequest; + +export const zAddSandboxSnapshotPath = z.object({ + workspace_domain: z.string(), + sandbox_id: z.string(), }); export const zAddSandboxSnapshotResponse = zSnapshotView; -export const zDeleteSandboxSnapshotData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - sandbox_id: z.string(), - id: z.string(), - }), - query: z.optional(z.never()), +export const zDeleteSandboxSnapshotPath = z.object({ + workspace_domain: z.string(), + sandbox_id: z.string(), + id: z.string(), }); /** @@ -2811,81 +2874,57 @@ export const zDeleteSandboxSnapshotData = z.object({ */ export const zDeleteSandboxSnapshotResponse = z.void(); -export const zGetSandboxSnapshotData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - sandbox_id: z.string(), - id: z.string(), - }), - query: z.optional(z.never()), +export const zGetSandboxSnapshotPath = z.object({ + workspace_domain: z.string(), + sandbox_id: z.string(), + id: z.string(), }); export const zGetSandboxSnapshotResponse = zSnapshotView; -export const zStartSandboxData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - sandbox_id: z.string(), - }), - query: z.optional(z.never()), +export const zStartSandboxPath = z.object({ + workspace_domain: z.string(), + sandbox_id: z.string(), }); export const zStartSandboxResponse = zSandboxResponse; -export const zStopSandboxData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - sandbox_id: z.string(), - }), - query: z.optional(z.never()), +export const zStopSandboxPath = z.object({ + workspace_domain: z.string(), + sandbox_id: z.string(), }); export const zStopSandboxResponse = zSandboxResponse; -export const zGetSandboxYamlData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - sandbox_id: z.string(), - }), - query: z.optional(z.never()), +export const zGetSandboxYamlPath = z.object({ + workspace_domain: z.string(), + sandbox_id: z.string(), }); export const zGetSandboxYamlResponse = zSandboxYamlView; -export const zUpdateSandboxByYamlData = z.object({ - body: z.optional(zSandboxYamlViewWritable), - path: z.object({ - workspace_domain: z.string(), - sandbox_id: z.string(), - }), - query: z.optional(z.never()), +export const zUpdateSandboxByYamlBody = zSandboxYamlViewWritable; + +export const zUpdateSandboxByYamlPath = z.object({ + workspace_domain: z.string(), + sandbox_id: z.string(), }); export const zUpdateSandboxByYamlResponse = zSandboxResponse; -export const zGetProjectSnapshotsData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - }), - query: z.object({ - project_name: z.string(), - }), +export const zGetProjectSnapshotsPath = z.object({ + workspace_domain: z.string(), +}); + +export const zGetProjectSnapshotsQuery = z.object({ + project_name: z.string(), }); export const zGetProjectSnapshotsResponse = zSnapshotsView; -export const zDeleteSnapshotData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - id: z.string(), - }), - query: z.optional(z.never()), +export const zDeleteSnapshotPath = z.object({ + workspace_domain: z.string(), + id: z.string(), }); /** @@ -2893,34 +2932,58 @@ export const zDeleteSnapshotData = z.object({ */ export const zDeleteSnapshotResponse = z.void(); -export const zAddSandboxByYamlData = z.object({ - body: z.optional(zSandboxYamlViewWritable), - path: z.object({ - workspace_domain: z.string(), - }), - query: z.object({ - project_name: z.string(), - }), +export const zAddSandboxByYamlBody = zSandboxYamlViewWritable; + +export const zAddSandboxByYamlPath = z.object({ + workspace_domain: z.string(), +}); + +export const zAddSandboxByYamlQuery = z.object({ + project_name: z.string(), }); export const zAddSandboxByYamlResponse = zSandboxResponse; -export const zGetSsoData = z.object({ - body: z.optional(z.never()), - path: z.object({ - workspace_domain: z.string(), - }), - query: z.optional(z.never()), +export const zGetSsoPath = z.object({ + workspace_domain: z.string(), }); export const zGetSsoResponse = zSsoView; -export const zUpdateSsoData = z.object({ - body: z.optional(zUpdateSsoRequest), - path: z.object({ - workspace_domain: z.string(), - }), - query: z.optional(z.never()), +export const zUpdateSsoBody = zUpdateSsoRequest; + +export const zUpdateSsoPath = z.object({ + workspace_domain: z.string(), }); export const zUpdateSsoResponse = zSsoView; + +export const zGetIntegrationsPath = z.object({ + workspace_domain: z.string(), +}); + +export const zGetIntegrationsResponse = zIntegrationsView; + +export const zAddIntegrationBody = zAddIntegrationRequest; + +export const zAddIntegrationPath = z.object({ + workspace_domain: z.string(), +}); + +export const zAddIntegrationResponse = zIntegrationView; + +export const zGetIntegrationPath = z.object({ + workspace_domain: z.string(), + hash_id: z.string(), +}); + +export const zGetIntegrationResponse = zIntegrationView; + +export const zUpdateIntegrationBody = zUpdateIntegrationRequest; + +export const zUpdateIntegrationPath = z.object({ + workspace_domain: z.string(), + hash_id: z.string(), +}); + +export const zUpdateIntegrationResponse = zIntegrationView; diff --git a/src/core/buddy-api-client.ts b/src/core/buddy-api-client.ts index f5ddc29..c8e980d 100644 --- a/src/core/buddy-api-client.ts +++ b/src/core/buddy-api-client.ts @@ -1,4 +1,4 @@ -import { prettifyError, z } from "zod"; +import { prettifyError, type z } from "zod"; import { addSandboxResponseTransformer, createSandboxDirectoryResponseTransformer, @@ -56,44 +56,51 @@ import type { UploadSandboxFileResponse, } from "@/api/openapi/types.gen"; import { - zAddSandboxData, + zAddSandboxBody, + zAddSandboxPath, + zAddSandboxQuery, zAddSandboxResponse, - zCreateSandboxDirectoryData, + zCreateSandboxDirectoryPath, zCreateSandboxDirectoryResponse, - zDeleteSandboxData, - zDeleteSandboxFileData, + zDeleteSandboxFilePath, zDeleteSandboxFileResponse, + zDeleteSandboxPath, zDeleteSandboxResponse, - zDownloadSandboxContentData, - zExecuteSandboxCommandData, + zDownloadSandboxContentPath, + zExecuteSandboxCommandBody, + zExecuteSandboxCommandPath, zExecuteSandboxCommandResponse, - zGetIdentifiersData, + zGetIdentifiersPath, + zGetIdentifiersQuery, zGetIdentifiersResponse, - zGetSandboxAppLogsByIdData, + zGetSandboxAppLogsByIdPath, + zGetSandboxAppLogsByIdQuery, zGetSandboxAppLogsByIdResponse, - zGetSandboxCommandData, - zGetSandboxCommandLogsData, + zGetSandboxCommandLogsPath, + zGetSandboxCommandLogsQuery, + zGetSandboxCommandPath, zGetSandboxCommandResponse, - zGetSandboxContentData, + zGetSandboxContentPath, zGetSandboxContentResponse, - zGetSandboxData, - zGetSandboxesData, + zGetSandboxesPath, + zGetSandboxesQuery, zGetSandboxesResponse, + zGetSandboxPath, zGetSandboxResponse, - zRestartSandboxData, + zRestartSandboxPath, zRestartSandboxResponse, zSandboxCommandLog, - zStartSandboxAppData, + zStartSandboxAppPath, zStartSandboxAppResponse, - zStartSandboxData, + zStartSandboxPath, zStartSandboxResponse, - zStopSandboxAppData, + zStopSandboxAppPath, zStopSandboxAppResponse, - zStopSandboxData, + zStopSandboxPath, zStopSandboxResponse, - zTerminateSandboxCommandData, + zTerminateSandboxCommandPath, zTerminateSandboxCommandResponse, - zUploadSandboxFileData, + zUploadSandboxFilePath, zUploadSandboxFileResponse, } from "@/api/openapi/zod.gen"; import { @@ -160,71 +167,66 @@ export class BuddyApiClient extends HttpClient { return result.data; } - /** Check if a schema expects query parameters (not ZodNever) */ - #schemaExpectsQuery(schema: z.ZodObject<{ query: z.ZodType }>): boolean { - const querySchema = schema.shape.query; - if (querySchema instanceof z.ZodOptional) { - const inner = querySchema._def.innerType; - if (inner instanceof z.ZodNever) { - return false; - } - } - return true; - } - /** Execute an HTTP request with input/output validation */ async #requestWithValidation({ method, url, data, - dataSchema, + bodySchema, + pathSchema, + querySchema, responseSchema, skipRetry, }: { method: "GET" | "POST" | "DELETE"; url: DataUrl; data: ClientData; - dataSchema: z.ZodObject<{ - body: z.ZodType; - path: z.ZodType; - query: z.ZodType; - }>; + bodySchema?: z.ZodType; + pathSchema: z.ZodType; + querySchema?: z.ZodType; responseSchema: z.ZodType; skipRetry?: boolean; }): Promise { - const expectsQuery = this.#schemaExpectsQuery( - dataSchema as z.ZodObject<{ query: z.ZodType }>, - ); + const pathResult = await pathSchema.safeParseAsync({ + workspace_domain: this.workspace, + ...(data.path ?? {}), + }); + if (!pathResult.success) { + throw pathResult.error; + } + const validatedPath = pathResult.data as Record; - const fullData = { - body: data.body, - path: { - workspace_domain: this.workspace, - ...(data.path ?? {}), - }, - query: expectsQuery - ? { - project_name: this.project_name, - ...(data.query ?? {}), - } - : undefined, - }; + let validatedQuery: Record | undefined; + if (querySchema) { + const queryResult = await querySchema.safeParseAsync({ + project_name: this.project_name, + ...(data.query ?? {}), + }); + if (!queryResult.success) { + throw queryResult.error; + } + validatedQuery = queryResult.data as Record< + string, + string | number | boolean + >; + } - const result = await dataSchema.safeParseAsync(fullData); - if (!result.success) { - throw result.error; + let validatedBody: unknown = data.body; + if (bodySchema && data.body !== undefined) { + const bodyResult = await bodySchema.safeParseAsync(data.body); + if (!bodyResult.success) { + throw bodyResult.error; + } + validatedBody = bodyResult.data; } - const validatedData = result.data; const parameterizedUrl = this.#buildUrl({ url, - path: validatedData.path as Record, + path: validatedPath, }); const requestConfig: RequestConfig = { - queryParams: validatedData.query as - | Record - | undefined, + queryParams: validatedQuery, skipRetry, }; @@ -234,7 +236,7 @@ export class BuddyApiClient extends HttpClient { case "POST": { request = this.post( parameterizedUrl, - validatedData.body ?? {}, + validatedBody ?? {}, requestConfig, ); break; @@ -259,7 +261,9 @@ export class BuddyApiClient extends HttpClient { method: "POST", data, url: "/workspaces/{workspace_domain}/sandboxes", - dataSchema: zAddSandboxData, + bodySchema: zAddSandboxBody, + pathSchema: zAddSandboxPath, + querySchema: zAddSandboxQuery, responseSchema: zAddSandboxResponse.transform( addSandboxResponseTransformer, ), @@ -274,7 +278,7 @@ export class BuddyApiClient extends HttpClient { method: "GET", data, url: "/workspaces/{workspace_domain}/sandboxes/{id}", - dataSchema: zGetSandboxData, + pathSchema: zGetSandboxPath, responseSchema: zGetSandboxResponse.transform( getSandboxResponseTransformer, ), @@ -289,7 +293,8 @@ export class BuddyApiClient extends HttpClient { method: "GET", data, url: "/workspaces/{workspace_domain}/identifiers", - dataSchema: zGetIdentifiersData, + pathSchema: zGetIdentifiersPath, + querySchema: zGetIdentifiersQuery, responseSchema: zGetIdentifiersResponse, }); } @@ -302,7 +307,8 @@ export class BuddyApiClient extends HttpClient { method: "POST", data, url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/commands", - dataSchema: zExecuteSandboxCommandData, + bodySchema: zExecuteSandboxCommandBody, + pathSchema: zExecuteSandboxCommandPath, responseSchema: zExecuteSandboxCommandResponse.transform( executeSandboxCommandResponseTransformer, ), @@ -317,7 +323,7 @@ export class BuddyApiClient extends HttpClient { method: "GET", data, url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/commands/{id}", - dataSchema: zGetSandboxCommandData, + pathSchema: zGetSandboxCommandPath, responseSchema: zGetSandboxCommandResponse.transform( getSandboxCommandResponseTransformer, ), @@ -332,7 +338,7 @@ export class BuddyApiClient extends HttpClient { method: "POST", data, url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/commands/{command_id}/terminate", - dataSchema: zTerminateSandboxCommandData, + pathSchema: zTerminateSandboxCommandPath, responseSchema: zTerminateSandboxCommandResponse.transform( terminateSandboxCommandResponseTransformer, ), @@ -348,7 +354,7 @@ export class BuddyApiClient extends HttpClient { method: "DELETE", data, url: "/workspaces/{workspace_domain}/sandboxes/{id}", - dataSchema: zDeleteSandboxData, + pathSchema: zDeleteSandboxPath, responseSchema: zDeleteSandboxResponse, skipRetry: true, }); @@ -369,7 +375,8 @@ export class BuddyApiClient extends HttpClient { method: "GET", data, url: "/workspaces/{workspace_domain}/sandboxes", - dataSchema: zGetSandboxesData, + pathSchema: zGetSandboxesPath, + querySchema: zGetSandboxesQuery, responseSchema: zGetSandboxesResponse, }); } @@ -382,7 +389,7 @@ export class BuddyApiClient extends HttpClient { method: "POST", data, url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/start", - dataSchema: zStartSandboxData, + pathSchema: zStartSandboxPath, responseSchema: zStartSandboxResponse.transform( startSandboxResponseTransformer, ), @@ -397,7 +404,7 @@ export class BuddyApiClient extends HttpClient { method: "POST", data, url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/stop", - dataSchema: zStopSandboxData, + pathSchema: zStopSandboxPath, responseSchema: zStopSandboxResponse.transform( stopSandboxResponseTransformer, ), @@ -412,7 +419,7 @@ export class BuddyApiClient extends HttpClient { method: "POST", data, url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/restart", - dataSchema: zRestartSandboxData, + pathSchema: zRestartSandboxPath, responseSchema: zRestartSandboxResponse.transform( restartSandboxResponseTransformer, ), @@ -427,7 +434,7 @@ export class BuddyApiClient extends HttpClient { method: "POST", data, url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/apps/{app_id}/start", - dataSchema: zStartSandboxAppData, + pathSchema: zStartSandboxAppPath, responseSchema: zStartSandboxAppResponse.transform( startSandboxAppResponseTransformer, ), @@ -442,7 +449,7 @@ export class BuddyApiClient extends HttpClient { method: "POST", data, url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/apps/{app_id}/stop", - dataSchema: zStopSandboxAppData, + pathSchema: zStopSandboxAppPath, responseSchema: zStopSandboxAppResponse.transform( stopSandboxAppResponseTransformer, ), @@ -457,7 +464,8 @@ export class BuddyApiClient extends HttpClient { method: "GET", data, url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/apps/{app_id}/logs", - dataSchema: zGetSandboxAppLogsByIdData, + pathSchema: zGetSandboxAppLogsByIdPath, + querySchema: zGetSandboxAppLogsByIdQuery, responseSchema: zGetSandboxAppLogsByIdResponse, }); } @@ -470,7 +478,7 @@ export class BuddyApiClient extends HttpClient { method: "GET", data, url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/content/{path}", - dataSchema: zGetSandboxContentData, + pathSchema: zGetSandboxContentPath, responseSchema: zGetSandboxContentResponse.transform( getSandboxContentResponseTransformer, ), @@ -485,7 +493,7 @@ export class BuddyApiClient extends HttpClient { method: "DELETE", data, url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/content/{path}", - dataSchema: zDeleteSandboxFileData, + pathSchema: zDeleteSandboxFilePath, responseSchema: zDeleteSandboxFileResponse, }); } @@ -498,7 +506,7 @@ export class BuddyApiClient extends HttpClient { method: "POST", data, url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/content/{path}", - dataSchema: zCreateSandboxDirectoryData, + pathSchema: zCreateSandboxDirectoryPath, responseSchema: zCreateSandboxDirectoryResponse.transform( createSandboxDirectoryResponseTransformer, ), @@ -510,27 +518,17 @@ export class BuddyApiClient extends HttpClient { body: Blob | File; path: { sandbox_id: string; path: string }; }): Promise { - const fullData = { - body: data.body, - path: { - workspace_domain: this.workspace, - ...(data.path ?? {}), - }, - query: undefined, - }; - - const validationResult = await zUploadSandboxFileData.safeParseAsync({ - ...fullData, - body: undefined, // Skip body validation for binary data + const pathResult = await zUploadSandboxFilePath.safeParseAsync({ + workspace_domain: this.workspace, + ...data.path, }); - if (!validationResult.success) { - throw validationResult.error; + if (!pathResult.success) { + throw pathResult.error; } - const validatedData = validationResult.data; const parameterizedUrl = this.#buildUrl({ url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/content/upload/{path}", - path: validatedData.path, + path: pathResult.data, }); const url = new URL(parameterizedUrl, this.#apiUrl); @@ -590,25 +588,17 @@ export class BuddyApiClient extends HttpClient { async downloadSandboxContent(data: { path: { sandbox_id: string; path: string }; }): Promise<{ data: ArrayBuffer; filename: string }> { - const fullData = { - body: undefined, - path: { - workspace_domain: this.workspace, - ...data.path, - }, - query: undefined, - }; - - const validationResult = - await zDownloadSandboxContentData.safeParseAsync(fullData); - if (!validationResult.success) { - throw validationResult.error; + const pathResult = await zDownloadSandboxContentPath.safeParseAsync({ + workspace_domain: this.workspace, + ...data.path, + }); + if (!pathResult.success) { + throw pathResult.error; } - const validatedData = validationResult.data; const parameterizedUrl = this.#buildUrl({ url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/download/{path}", - path: validatedData.path, + path: pathResult.data, }); const url = new URL(parameterizedUrl, this.#apiUrl); @@ -658,30 +648,29 @@ export class BuddyApiClient extends HttpClient { async *streamCommandLogs( data: ClientData, ): AsyncGenerator { - const fullData = { - body: data.body, - path: { - workspace_domain: this.workspace, - ...(data.path ?? {}), - }, - query: data.query, - }; + const pathResult = await zGetSandboxCommandLogsPath.safeParseAsync({ + workspace_domain: this.workspace, + ...(data.path ?? {}), + }); + if (!pathResult.success) { + throw pathResult.error; + } - const validationResult = - await zGetSandboxCommandLogsData.safeParseAsync(fullData); - if (!validationResult.success) { - throw validationResult.error; + const queryResult = await zGetSandboxCommandLogsQuery.safeParseAsync( + data.query ?? {}, + ); + if (!queryResult.success) { + throw queryResult.error; } - const validatedData = validationResult.data; const parameterizedUrl = this.#buildUrl({ url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/commands/{command_id}/logs", - path: validatedData.path, + path: pathResult.data as Record, }); const url = new URL(parameterizedUrl, this.#apiUrl); - if (validatedData.query?.follow !== undefined) { - url.searchParams.set("follow", String(validatedData.query.follow)); + if (queryResult.data.follow !== undefined) { + url.searchParams.set("follow", String(queryResult.data.follow)); } const headers = { diff --git a/tests/buddy-api-client.test.ts b/tests/buddy-api-client.test.ts index 29ea7c1..a594ced 100644 --- a/tests/buddy-api-client.test.ts +++ b/tests/buddy-api-client.test.ts @@ -156,6 +156,79 @@ describe("BuddyApiClient", () => { expect(response?.id).toBe("new-sandbox-id"); expect(response?.name).toBe("New Sandbox"); }); + + it("should forward timeout in the request body", async () => { + let receivedBody: Record | undefined; + server.use( + http.post( + `${TEST_API_URL}/workspaces/${TEST_WORKSPACE}/sandboxes`, + async ({ request }) => { + receivedBody = (await request.json()) as Record; + return HttpResponse.json({ + id: "with-timeout", + name: receivedBody["name"], + identifier: receivedBody["identifier"], + os: receivedBody["os"], + timeout: receivedBody["timeout"], + status: "STARTING", + }); + }, + ), + ); + + const client = createClient(); + const response = await client.addSandbox({ + body: { + name: "Timeout Sandbox", + identifier: "timeout-sandbox", + os: "ubuntu:24.04", + timeout: 600, + }, + }); + + expect(receivedBody?.["timeout"]).toBe(600); + expect(response?.timeout).toBe(600); + }); + + it("should forward fetch items in the request body", async () => { + let receivedBody: Record | undefined; + const fetchItems = [ + { + type: "PUBLIC_REPO" as const, + repository: "https://github.com/octocat/Hello-World", + ref: "master", + }, + ]; + server.use( + http.post( + `${TEST_API_URL}/workspaces/${TEST_WORKSPACE}/sandboxes`, + async ({ request }) => { + receivedBody = (await request.json()) as Record; + return HttpResponse.json({ + id: "with-fetch", + name: receivedBody["name"], + identifier: receivedBody["identifier"], + os: receivedBody["os"], + fetch: receivedBody["fetch"], + status: "STARTING", + }); + }, + ), + ); + + const client = createClient(); + const response = await client.addSandbox({ + body: { + name: "Fetch Sandbox", + identifier: "fetch-sandbox", + os: "ubuntu:24.04", + fetch: fetchItems, + }, + }); + + expect(receivedBody?.["fetch"]).toEqual(fetchItems); + expect(response?.fetch).toEqual(fetchItems); + }); }); describe("deleteSandboxById", () => { diff --git a/tests/create-options.test.ts b/tests/create-options.test.ts index 6b179f7..5843b93 100644 --- a/tests/create-options.test.ts +++ b/tests/create-options.test.ts @@ -44,6 +44,40 @@ describe("Sandbox.create options", () => { sandboxes.push(sandbox); expect(sandbox.data.os).toBe("ubuntu:22.04"); + }, 120000); + + it("should create sandbox with custom timeout", async () => { + const sandbox = await Sandbox.create({ + identifier: `timeout_${Date.now()}`, + timeout: 600, + }); + sandboxes.push(sandbox); + + expect(sandbox.data.timeout).toBe(600); + }); + + it("should create sandbox with fetch (public repo)", async () => { + const sandbox = await Sandbox.create({ + identifier: `fetch_${Date.now()}`, + fetch: [ + { + type: "PUBLIC_REPO", + repository: "https://github.com/octocat/Hello-World", + ref: "master", + }, + ], + }); + sandboxes.push(sandbox); + + expect(sandbox.data.fetch).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: "PUBLIC_REPO", + repository: "https://github.com/octocat/Hello-World", + ref: "master", + }), + ]), + ); }); it("should reject duplicate identifier", async () => { From 019995cc3039527cacfba9ff50650c4561533c79 Mon Sep 17 00:00:00 2001 From: Buddy CI Date: Wed, 20 May 2026 09:21:15 +0000 Subject: [PATCH 15/23] chore(release): bump version to 0.1.6-rc.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 227d7c1..cc80945 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@buddy-works/sandbox-sdk", - "version": "0.1.5", + "version": "0.1.6-rc.0", "type": "module", "description": "TypeScript SDK for managing sandboxes through the Buddy API", "main": "./dist/index.mjs", From 92fcc0e464787be9b9ffedea1a9ab51683ef44fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Ga=C5=84czarczyk?= Date: Thu, 21 May 2026 12:33:27 +0200 Subject: [PATCH 16/23] Sandbox update and snapshots (#11) * feat: add PATCH support to HttpClient * feat: add updateSandbox and snapshot endpoints to BuddyApiClient * feat: add Snapshot entity class * feat: add update, snapshot and createFromSnapshot methods to Sandbox entity * feat: export Snapshot and related types from public API * test: add MSW unit tests for updateSandbox and snapshot endpoints * test: add integration tests for update, snapshot lifecycle, idempotency and data preservation * test: add directory download integration test * test: add error cases for createFromSnapshot, update on destroyed sandbox and getSnapshot 404 * test: add empirical update behavior probes * docs: document update and Snapshot entity in README * refactor: drop snapshot.restore in favor of Sandbox.createFromSnapshot * test: assert restored sandbox name and identifier match overrides * chore: simplify example:all with concurrently glob pattern * feat: add sandbox update example * feat: add snapshots example with create, restore and marker file verification * feat: add fetch example for cloning a public repo at first boot * test: add MSW unit tests for sandbox app start, stop and logs endpoints * refactor: dedupe sandbox create helpers and tighten Snapshot constructor * test: assert setup_status contract in update behavior probes * test: ensure sandbox is RUNNING before update and snapshots tests * chore: wrap examples in try/finally for guaranteed resource cleanup * feat: add static Sandbox.listSnapshots for listing all snapshots in a project * feat: add static Sandbox.deleteSnapshot for project-level deletion * test: cover project-level snapshot delete and harden cleanup * feat: add getSandboxCommands endpoint to BuddyApiClient * feat: add sandbox.listCommands and static Sandbox.clone * test: cover commands history, clone, and widen global teardown * fix: surface non-404 errors from getByIdentifier and remove stale identifier fallback * unit tests --- README.md | 51 +++++ examples/package.json | 5 +- examples/src/apps.ts | 101 ++++----- examples/src/error-handling.ts | 133 ++++++------ examples/src/fetch.ts | 51 +++++ examples/src/filesystem.ts | 203 ++++++++++--------- examples/src/lifecycle.ts | 89 ++++---- examples/src/ping.ts | 131 ++++++------ examples/src/snapshots.ts | 77 +++++++ examples/src/streaming.ts | 53 ++--- examples/src/update.ts | 54 +++++ package.json | 1 + pnpm-lock.yaml | 71 +++++++ src/core/buddy-api-client.ts | 194 +++++++++++++++++- src/core/http-client.ts | 9 + src/entity/sandbox.ts | 343 ++++++++++++++++++++++++++++--- src/entity/snapshot.ts | 122 +++++++++++ src/index.ts | 12 +- tests/buddy-api-client.test.ts | 360 +++++++++++++++++++++++++++++++++ tests/errors.test.ts | 45 ++++- tests/filesystem.test.ts | 26 +++ tests/global-teardown.ts | 7 +- tests/sandbox.test.ts | 209 +++++++++++++++++++ tests/update-behavior.test.ts | 204 +++++++++++++++++++ vitest.config.ts | 1 + 25 files changed, 2169 insertions(+), 383 deletions(-) create mode 100644 examples/src/fetch.ts create mode 100644 examples/src/snapshots.ts create mode 100644 examples/src/update.ts create mode 100644 src/entity/snapshot.ts create mode 100644 tests/update-behavior.test.ts diff --git a/README.md b/README.md index 395b28c..70284e6 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,57 @@ await Sandbox.create({ }); ``` +## Updating a sandbox + +Use `sandbox.update()` to change configuration after creation - `timeout`, `apps`, `endpoints`, `variables`, `tags`, etc. The internal state is updated with the API's response. + +```typescript +const sandbox = await Sandbox.getByIdentifier("my-sandbox"); + +await sandbox.update({ + timeout: 1200, + tags: ["staging", "feature-x"], +}); +``` + +## Snapshots + +Take point-in-time snapshots of a sandbox and restore from them later. Each snapshot is returned as a `Snapshot` instance with its own methods. + +```typescript +const sandbox = await Sandbox.getByIdentifier("my-sandbox"); + +// Create a snapshot. Returns immediately with status "CREATING". +const snapshot = await sandbox.createSnapshot({ name: "before-deploy" }); + +// Wait until the snapshot is "CREATED" before using it. +await snapshot.waitUntilReady(); + +// Create a new sandbox from the snapshot. +const restored = await Sandbox.createFromSnapshot(snapshot.id, { + name: "restored-sandbox", +}); + +// List all snapshots for this sandbox. +const snapshots = await sandbox.listSnapshots(); + +// Delete a snapshot. +await snapshot.delete(); +``` + +If you already have a snapshot ID from elsewhere (e.g. persisted in your own storage), get the entity directly: + +```typescript +const snapshot = await Sandbox.getSnapshotById(sandboxId, snapshotId); +``` + +Equivalent convenience methods on `Sandbox` are available when you only have an ID and don't want to fetch the `Snapshot` first: + +```typescript +await sandbox.waitForSnapshotReady(snapshotId); +await sandbox.deleteSnapshot(snapshotId); +``` + ## Regions Configure the API region: diff --git a/examples/package.json b/examples/package.json index e401073..5ab318d 100644 --- a/examples/package.json +++ b/examples/package.json @@ -12,7 +12,10 @@ "example:filesystem": "NODE_OPTIONS='--enable-source-maps' dotenvx run -q -- tsx src/filesystem.ts", "example:apps": "NODE_OPTIONS='--enable-source-maps' dotenvx run -q -- tsx src/apps.ts", "example:error-handling": "NODE_OPTIONS='--enable-source-maps' dotenvx run -q -- tsx src/error-handling.ts", - "example:all": "concurrently \"pnpm example:ping\" \"pnpm example:list\" \"pnpm example:lifecycle\" \"pnpm example:streaming\" \"pnpm example:filesystem\" \"pnpm example:apps\" \"pnpm example:error-handling\"" + "example:update": "NODE_OPTIONS='--enable-source-maps' dotenvx run -q -- tsx src/update.ts", + "example:snapshots": "NODE_OPTIONS='--enable-source-maps' dotenvx run -q -- tsx src/snapshots.ts", + "example:fetch": "NODE_OPTIONS='--enable-source-maps' dotenvx run -q -- tsx src/fetch.ts", + "example:all": "concurrently 'pnpm:example:*(!all)'" }, "keywords": [], "author": "", diff --git a/examples/src/apps.ts b/examples/src/apps.ts index d7648e0..db6144b 100644 --- a/examples/src/apps.ts +++ b/examples/src/apps.ts @@ -5,66 +5,69 @@ log("Sandbox Apps Example\n"); const identifier = "apps-demo-sandbox"; -let sandbox: Sandbox; - try { - sandbox = await Sandbox.getByIdentifier(identifier); + const existing = await Sandbox.getByIdentifier(identifier); log(`Found existing sandbox with identifier: ${identifier}, deleting...`); - await sandbox.destroy(); + await existing.destroy(); } catch { // Sandbox doesn't exist, nothing to delete } -log(`Creating sandbox with 2 apps...`); -sandbox = await Sandbox.create({ - identifier, - name: "Apps Demo Sandbox", - os: "ubuntu:24.04", - first_boot_commands: "apt-get update && apt-get install -y curl", - apps: [ - 'while true; do echo "[app1] ping $(date)"; sleep 2; done', - 'while true; do echo "[app2] pong $(date)"; sleep 3; done', - ], -}); -log(`Created sandbox: ${sandbox.data.identifier} (${sandbox.data.html_url})`); -log(`Status: ${sandbox.data.status}, Setup: ${sandbox.data.setup_status}\n`); +let sandbox: Sandbox | undefined; +try { + log(`Creating sandbox with 2 apps...`); + sandbox = await Sandbox.create({ + identifier, + name: "Apps Demo Sandbox", + os: "ubuntu:24.04", + first_boot_commands: "apt-get update && apt-get install -y curl", + apps: [ + 'while true; do echo "[app1] ping $(date)"; sleep 2; done', + 'while true; do echo "[app2] pong $(date)"; sleep 3; done', + ], + }); + log(`Created sandbox: ${sandbox.data.identifier} (${sandbox.data.html_url})`); + log(`Status: ${sandbox.data.status}, Setup: ${sandbox.data.setup_status}\n`); -log("Apps:"); -for (const app of sandbox.data.apps ?? []) { - log(` ${app.id}: "${app.command}" -> ${app.app_status}`); -} + log("Apps:"); + for (const app of sandbox.data.apps ?? []) { + log(` ${app.id}: "${app.command}" -> ${app.app_status}`); + } -const firstApp = sandbox.data.apps?.[0]; -const secondApp = sandbox.data.apps?.[1]; + const firstApp = sandbox.data.apps?.[0]; + const secondApp = sandbox.data.apps?.[1]; -if (!firstApp?.id || !secondApp?.id) { - throw new Error("Expected 2 apps in sandbox"); -} + if (!firstApp?.id || !secondApp?.id) { + throw new Error("Expected 2 apps in sandbox"); + } -log(`\nStopping the first app (${firstApp.id})...`); -await sandbox.stopApp(firstApp.id); -log("Apps after stopping the first one:"); -for (const app of sandbox.data.apps ?? []) { - log(` ${app.id}: ${app.app_status}`); -} + log(`\nStopping the first app (${firstApp.id})...`); + await sandbox.stopApp(firstApp.id); + log("Apps after stopping the first one:"); + for (const app of sandbox.data.apps ?? []) { + log(` ${app.id}: ${app.app_status}`); + } -log(`\nStarting the first app (${firstApp.id})...`); -await sandbox.startApp(firstApp.id); -log("Apps after starting the first one:"); -for (const app of sandbox.data.apps ?? []) { - log(` ${app.id}: ${app.app_status}`); -} + log(`\nStarting the first app (${firstApp.id})...`); + await sandbox.startApp(firstApp.id); + log("Apps after starting the first one:"); + for (const app of sandbox.data.apps ?? []) { + log(` ${app.id}: ${app.app_status}`); + } -log("\nWaiting a few seconds for log output..."); -await new Promise((resolve) => setTimeout(resolve, 5000)); + log("\nWaiting a few seconds for log output..."); + await new Promise((resolve) => setTimeout(resolve, 5000)); -log(`Getting logs for the second app (${secondApp.id})...`); -const logs = await sandbox.getAppLogs(secondApp.id); -log(`Logs (${logs.logs?.length ?? 0} entries):`); -for (const entry of logs.logs ?? []) { - log(` ${entry}`); + log(`Getting logs for the second app (${secondApp.id})...`); + const logs = await sandbox.getAppLogs(secondApp.id); + log(`Logs (${logs.logs?.length ?? 0} entries):`); + for (const entry of logs.logs ?? []) { + log(` ${entry}`); + } +} finally { + if (sandbox) { + log("\nCleaning up..."); + await sandbox.destroy().catch(() => undefined); + log("Sandbox deleted successfully"); + } } - -log("\nCleaning up..."); -await sandbox.destroy(); -log("Sandbox deleted successfully"); diff --git a/examples/src/error-handling.ts b/examples/src/error-handling.ts index 2654797..0d6f8dc 100644 --- a/examples/src/error-handling.ts +++ b/examples/src/error-handling.ts @@ -24,82 +24,83 @@ try { log(`Created sandbox: ${sandbox.data.identifier} (${sandbox.data.html_url})`); } -log("Starting sandbox..."); -await sandbox.start(); - -// Example 1: Basic exit code check -log("\n=== Example 1: Basic exit code check ==="); -log("Running: ls /nonexistent-directory\n"); - -const result1 = await sandbox.runCommand({ - command: "ls /nonexistent-directory", -}); - -if (result1.data.exit_code !== 0) { - log(`Command failed with exit code ${result1.data.exit_code}`); -} +try { + log("Starting sandbox..."); + await sandbox.start(); -// Example 2: Capture stderr for error details -log("\n=== Example 2: Capture stderr ==="); -log("Running: cat /file/that/does/not/exist\n"); + // Example 1: Basic exit code check + log("\n=== Example 1: Basic exit code check ==="); + log("Running: ls /nonexistent-directory\n"); -const result2 = await sandbox.runCommand({ - command: "cat /file/that/does/not/exist", - stdout: null, - stderr: null, -}); + const result1 = await sandbox.runCommand({ + command: "ls /nonexistent-directory", + }); -if (result2.data.exit_code !== 0) { - const errorOutput = await result2.stderr(); - log(`Command failed: ${errorOutput.trim()}`); -} + if (result1.data.exit_code !== 0) { + log(`Command failed with exit code ${result1.data.exit_code}`); + } -// Example 3: Throw on failure (wrapper pattern) -log("\n=== Example 3: Throw on failure ==="); -log("Running: false (always exits with 1)\n"); + // Example 2: Capture stderr for error details + log("\n=== Example 2: Capture stderr ==="); + log("Running: cat /file/that/does/not/exist\n"); -async function runOrThrow( - sandbox: Sandbox, - command: string, -): Promise>> { - const result = await sandbox.runCommand({ - command, + const result2 = await sandbox.runCommand({ + command: "cat /file/that/does/not/exist", stdout: null, stderr: null, }); - if (result.data.exit_code !== 0) { - const stderr = await result.stderr(); - throw new Error(`Command "${command}" failed: ${stderr.trim()}`); + + if (result2.data.exit_code !== 0) { + const errorOutput = await result2.stderr(); + log(`Command failed: ${errorOutput.trim()}`); } - return result; -} -try { - await runOrThrow(sandbox, "false"); -} catch (err) { - log(`Caught error: ${err instanceof Error ? err.message : err}`); -} + // Example 3: Throw on failure (wrapper pattern) + log("\n=== Example 3: Throw on failure ==="); + log("Running: false (always exits with 1)\n"); + + async function runOrThrow( + sandbox: Sandbox, + command: string, + ): Promise>> { + const result = await sandbox.runCommand({ + command, + stdout: null, + stderr: null, + }); + if (result.data.exit_code !== 0) { + const stderr = await result.stderr(); + throw new Error(`Command "${command}" failed: ${stderr.trim()}`); + } + return result; + } -// Example 4: Expected failures (like grep with no matches) -log("\n=== Example 4: Expected failures ==="); -log("Running: echo 'hello world' | grep 'nonexistent'\n"); - -const grepResult = await sandbox.runCommand({ - command: "echo 'hello world' | grep 'nonexistent'", - stdout: null, - stderr: null, -}); - -// grep returns 1 when no matches - this is expected, not an error -if (grepResult.data.exit_code === 1) { - log("No matches found (expected)"); -} else if (grepResult.data.exit_code === 0) { - log("Found matches"); -} else { - log(`Unexpected grep error: exit code ${grepResult.data.exit_code}`); -} + try { + await runOrThrow(sandbox, "false"); + } catch (err) { + log(`Caught error: ${err instanceof Error ? err.message : err}`); + } + + // Example 4: Expected failures (like grep with no matches) + log("\n=== Example 4: Expected failures ==="); + log("Running: echo 'hello world' | grep 'nonexistent'\n"); -log("\nStopping sandbox..."); -await sandbox.stop(); + const grepResult = await sandbox.runCommand({ + command: "echo 'hello world' | grep 'nonexistent'", + stdout: null, + stderr: null, + }); -log("Error handling example completed!"); + // grep returns 1 when no matches - this is expected, not an error + if (grepResult.data.exit_code === 1) { + log("No matches found (expected)"); + } else if (grepResult.data.exit_code === 0) { + log("Found matches"); + } else { + log(`Unexpected grep error: exit code ${grepResult.data.exit_code}`); + } +} finally { + log("\nStopping sandbox..."); + await sandbox.stop().catch(() => undefined); + log("Error handling example completed!"); +} diff --git a/examples/src/fetch.ts b/examples/src/fetch.ts new file mode 100644 index 0000000..fe2c949 --- /dev/null +++ b/examples/src/fetch.ts @@ -0,0 +1,51 @@ +import { Sandbox } from "@buddy-works/sandbox-sdk"; +import { log } from "@/shared/logger"; + +log("Fetch Example\n"); + +const identifier = "fetch-demo-sandbox"; + +try { + const existing = await Sandbox.getByIdentifier(identifier); + log(`Found existing sandbox with identifier: ${identifier}, deleting...`); + await existing.destroy(); +} catch { + // Sandbox doesn't exist, nothing to delete +} + +let sandbox: Sandbox | undefined; +try { + log("Creating sandbox that clones a public repository on first boot..."); + sandbox = await Sandbox.create({ + identifier, + name: "Fetch Demo Sandbox", + os: "ubuntu:24.04", + fetch: [ + { + type: "PUBLIC_REPO", + repository: "https://github.com/octocat/Hello-World", + ref: "master", + path: "/workspace/hello-world", + }, + ], + }); + log( + `Created sandbox: ${sandbox.data.identifier} (${sandbox.data.html_url})\n`, + ); + + log("Listing the cloned directory:"); + const ls = await sandbox.runCommand({ + command: "ls -la /workspace/hello-world", + stdout: null, + stderr: null, + }); + const finished = await ls.wait(); + const output = await finished.stdout(); + log(output); +} finally { + if (sandbox) { + log("Cleaning up..."); + await sandbox.destroy().catch(() => undefined); + log("Sandbox deleted successfully"); + } +} diff --git a/examples/src/filesystem.ts b/examples/src/filesystem.ts index 8d8d5fe..7f47e3e 100644 --- a/examples/src/filesystem.ts +++ b/examples/src/filesystem.ts @@ -24,110 +24,111 @@ try { log(`Created sandbox: ${sandbox.data.identifier} (${sandbox.data.html_url})`); } -await sandbox.start(); +try { + await sandbox.start(); + + log("\n=== Direct FileSystem Usage ==="); + log("Creating FileSystem directly from sandbox ID..."); + + const sandboxId = sandbox.initializedId; + + const directFs = FileSystem.forSandbox(sandboxId); + const homeFiles = await directFs.listFiles("/home"); + log(`Direct FileSystem found ${homeFiles.length} items in /home:`); + for (const file of homeFiles) { + log(` ${file.type === "DIR" ? "[DIR]" : "[FILE]"} ${file.name}`); + } + + log("\n=== Example 1: Listing Files ==="); + log("Listing contents of /etc directory (first 10 items)..."); + + const files = await sandbox.fs.listFiles("/etc"); + log(`Found ${files.length} items:`); + for (const file of files.slice(0, 10)) { + const typeIcon = file.type === "DIR" ? "[DIR]" : "[FILE]"; + const size = file.size ? ` (${file.size} bytes)` : ""; + log(` ${typeIcon} ${file.name}${size}`); + } + if (files.length > 10) { + log(` ... and ${files.length - 10} more`); + } + + log("\n=== Example 2: Creating Directories ==="); + log("Creating directory structure: /buddy/demo/data"); + + await sandbox.fs.createFolder("/buddy/demo"); + await sandbox.fs.createFolder("/buddy/demo/data"); + log("Directories created successfully"); + + const demoFiles = await sandbox.fs.listFiles("/buddy/demo"); + log(`Contents of /buddy/demo: ${demoFiles.map((f) => f.name).join(", ")}`); + + log("\n=== Example 3: Uploading Files ==="); + + const configContent = JSON.stringify( + { version: "1.0.0", name: "demo" }, + null, + 2, + ); + await sandbox.fs.uploadFile( + Buffer.from(configContent), + "/buddy/demo/config.json", + ); + log("Uploaded config.json from buffer"); + + await Promise.all([ + sandbox.fs.uploadFile( + Buffer.from("Hello from file 1!"), + "/buddy/demo/data/file1.txt", + ), + sandbox.fs.uploadFile( + Buffer.from("Hello from file 2!"), + "/buddy/demo/data/file2.txt", + ), + ]); + log("Uploaded 2 files to /buddy/demo/data/"); + + log("\n=== Example 4: Downloading Files ==="); + + const downloadedConfig = await sandbox.fs.downloadFile( + "/buddy/demo/config.json", + ); + log(`Downloaded config.json (${downloadedConfig.length} bytes):`); + log(` Content: ${downloadedConfig.toString()}`); -log("\n=== Direct FileSystem Usage ==="); -log("Creating FileSystem directly from sandbox ID..."); + const [file1Content, file2Content] = await Promise.all([ + sandbox.fs.downloadFile("/buddy/demo/data/file1.txt"), + sandbox.fs.downloadFile("/buddy/demo/data/file2.txt"), + ]); + log("Downloaded multiple files:"); + log(` file1.txt: "${file1Content.toString()}"`); + log(` file2.txt: "${file2Content.toString()}"`); -const sandboxId = sandbox.initializedId; + log("\n=== Example 5: Deleting Files ==="); -const directFs = FileSystem.forSandbox(sandboxId); -const homeFiles = await directFs.listFiles("/home"); -log(`Direct FileSystem found ${homeFiles.length} items in /home:`); -for (const file of homeFiles) { - log(` ${file.type === "DIR" ? "[DIR]" : "[FILE]"} ${file.name}`); -} + await sandbox.fs.deleteFile("/buddy/demo/data/file1.txt"); + log("Deleted file1.txt"); -log("\n=== Example 1: Listing Files ==="); -log("Listing contents of /etc directory (first 10 items)..."); + await sandbox.fs.deleteFile("/buddy/demo/data/file2.txt"); + log("Deleted file2.txt"); -const files = await sandbox.fs.listFiles("/etc"); -log(`Found ${files.length} items:`); -for (const file of files.slice(0, 10)) { - const typeIcon = file.type === "DIR" ? "[DIR]" : "[FILE]"; - const size = file.size ? ` (${file.size} bytes)` : ""; - log(` ${typeIcon} ${file.name}${size}`); -} -if (files.length > 10) { - log(` ... and ${files.length - 10} more`); -} + await sandbox.fs.deleteFile("/buddy/demo/data"); + log("Deleted /buddy/demo/data directory"); + + await sandbox.fs.deleteFile("/buddy/demo/config.json"); + log("Deleted config.json"); + + await sandbox.fs.deleteFile("/buddy/demo"); + log("Deleted /buddy/demo directory"); -log("\n=== Example 2: Creating Directories ==="); -log("Creating directory structure: /buddy/demo/data"); - -await sandbox.fs.createFolder("/buddy/demo"); -await sandbox.fs.createFolder("/buddy/demo/data"); -log("Directories created successfully"); - -const demoFiles = await sandbox.fs.listFiles("/buddy/demo"); -log(`Contents of /buddy/demo: ${demoFiles.map((f) => f.name).join(", ")}`); - -log("\n=== Example 3: Uploading Files ==="); - -const configContent = JSON.stringify( - { version: "1.0.0", name: "demo" }, - null, - 2, -); -await sandbox.fs.uploadFile( - Buffer.from(configContent), - "/buddy/demo/config.json", -); -log("Uploaded config.json from buffer"); - -await Promise.all([ - sandbox.fs.uploadFile( - Buffer.from("Hello from file 1!"), - "/buddy/demo/data/file1.txt", - ), - sandbox.fs.uploadFile( - Buffer.from("Hello from file 2!"), - "/buddy/demo/data/file2.txt", - ), -]); -log("Uploaded 2 files to /buddy/demo/data/"); - -log("\n=== Example 4: Downloading Files ==="); - -const downloadedConfig = await sandbox.fs.downloadFile( - "/buddy/demo/config.json", -); -log(`Downloaded config.json (${downloadedConfig.length} bytes):`); -log(` Content: ${downloadedConfig.toString()}`); - -const [file1Content, file2Content] = await Promise.all([ - sandbox.fs.downloadFile("/buddy/demo/data/file1.txt"), - sandbox.fs.downloadFile("/buddy/demo/data/file2.txt"), -]); -log("Downloaded multiple files:"); -log(` file1.txt: "${file1Content.toString()}"`); -log(` file2.txt: "${file2Content.toString()}"`); - -log("\n=== Example 5: Deleting Files ==="); - -await sandbox.fs.deleteFile("/buddy/demo/data/file1.txt"); -log("Deleted file1.txt"); - -await sandbox.fs.deleteFile("/buddy/demo/data/file2.txt"); -log("Deleted file2.txt"); - -await sandbox.fs.deleteFile("/buddy/demo/data"); -log("Deleted /buddy/demo/data directory"); - -await sandbox.fs.deleteFile("/buddy/demo/config.json"); -log("Deleted config.json"); - -await sandbox.fs.deleteFile("/buddy/demo"); -log("Deleted /buddy/demo directory"); - -const finalFiles = await sandbox.fs.listFiles("/buddy"); -log( - `Contents of /buddy after cleanup: ${ - finalFiles.map((f) => f.name).join(", ") || "(empty)" - }`, -); - -log("\nStopping sandbox..."); -await sandbox.stop(); - -log("FileSystem example completed!"); + const finalFiles = await sandbox.fs.listFiles("/buddy"); + log( + `Contents of /buddy after cleanup: ${ + finalFiles.map((f) => f.name).join(", ") || "(empty)" + }`, + ); +} finally { + log("\nStopping sandbox..."); + await sandbox.stop().catch(() => undefined); + log("FileSystem example completed!"); +} diff --git a/examples/src/lifecycle.ts b/examples/src/lifecycle.ts index 8bc5a5f..ec74d24 100644 --- a/examples/src/lifecycle.ts +++ b/examples/src/lifecycle.ts @@ -5,52 +5,55 @@ log("Sandbox Lifecycle Example\n"); const identifier = "lifecycle-demo-sandbox"; -let sandbox: Sandbox; - try { - sandbox = await Sandbox.getByIdentifier(identifier); + const existing = await Sandbox.getByIdentifier(identifier); log(`Found existing sandbox with identifier: ${identifier}, deleting...`); - await sandbox.destroy(); + await existing.destroy(); } catch { // Sandbox doesn't exist, nothing to delete } -log(`Creating sandbox with identifier: ${identifier}`); -sandbox = await Sandbox.create({ - identifier, - name: "Lifecycle Demo Sandbox", - os: "ubuntu:24.04", -}); -log(`Created sandbox: ${sandbox.data.identifier} (${sandbox.data.html_url})`); -log(`Status: ${sandbox.data.status}, Setup: ${sandbox.data.setup_status}\n`); - -log("Running command: uptime"); -await sandbox.runCommand({ - command: "uptime", -}); - -log("\nStopping sandbox..."); -await sandbox.stop(); -log(`Sandbox stopped. Status: ${sandbox.data.status}\n`); - -log("Starting sandbox..."); -await sandbox.start(); -log(`Sandbox started. Status: ${sandbox.data.status}\n`); - -log("Running command: df -h"); -await sandbox.runCommand({ - command: "df -h", -}); - -log("\nRestarting sandbox..."); -await sandbox.restart(); -log(`Sandbox restarted. Status: ${sandbox.data.status}\n`); - -log("Running final command: hostname"); -await sandbox.runCommand({ - command: "hostname", -}); - -log("\nCleaning up..."); -await sandbox.destroy(); -log("Sandbox deleted successfully"); +let sandbox: Sandbox | undefined; +try { + log(`Creating sandbox with identifier: ${identifier}`); + sandbox = await Sandbox.create({ + identifier, + name: "Lifecycle Demo Sandbox", + os: "ubuntu:24.04", + }); + log(`Created sandbox: ${sandbox.data.identifier} (${sandbox.data.html_url})`); + log(`Status: ${sandbox.data.status}, Setup: ${sandbox.data.setup_status}\n`); + + log("Running command: uptime"); + await sandbox.runCommand({ + command: "uptime", + }); + + log("\nStopping sandbox..."); + await sandbox.stop(); + log(`Sandbox stopped. Status: ${sandbox.data.status}\n`); + + log("Starting sandbox..."); + await sandbox.start(); + log(`Sandbox started. Status: ${sandbox.data.status}\n`); + + log("Running command: df -h"); + await sandbox.runCommand({ + command: "df -h", + }); + + log("\nRestarting sandbox..."); + await sandbox.restart(); + log(`Sandbox restarted. Status: ${sandbox.data.status}\n`); + + log("Running final command: hostname"); + await sandbox.runCommand({ + command: "hostname", + }); +} finally { + if (sandbox) { + log("\nCleaning up..."); + await sandbox.destroy().catch(() => undefined); + log("Sandbox deleted successfully"); + } +} diff --git a/examples/src/ping.ts b/examples/src/ping.ts index 7e73df8..1fc4446 100644 --- a/examples/src/ping.ts +++ b/examples/src/ping.ts @@ -24,75 +24,76 @@ try { log(`Created sandbox: ${sandbox.data.identifier} (${sandbox.data.html_url})`); } -await sandbox.start(); - -log("\n=== Mode 1: Real-time streaming ==="); -log("Output appears as the command runs:\n"); - -await sandbox.runCommand({ - command: "ping -c 4 buddy.works", - runtime: "BASH", -}); +try { + await sandbox.start(); -log("\n=== Mode 2: Parallel execution ==="); -log("Run multiple commands simultaneously, results print as they complete."); + log("\n=== Mode 1: Real-time streaming ==="); + log("Output appears as the command runs:\n"); -const [cmd1, cmd2] = await Promise.all([ - sandbox.runCommand({ - command: - "echo 'Starting slow task...' && sleep 3 && echo 'Slow task done!'", + await sandbox.runCommand({ + command: "ping -c 4 buddy.works", runtime: "BASH", - detached: true, - stdout: null, - stderr: null, - }), - sandbox.runCommand({ - command: - "echo 'Starting fast task...' && sleep 1 && echo 'Fast task done!'", + }); + + log("\n=== Mode 2: Parallel execution ==="); + log("Run multiple commands simultaneously, results print as they complete."); + + const [cmd1, cmd2] = await Promise.all([ + sandbox.runCommand({ + command: + "echo 'Starting slow task...' && sleep 3 && echo 'Slow task done!'", + runtime: "BASH", + detached: true, + stdout: null, + stderr: null, + }), + sandbox.runCommand({ + command: + "echo 'Starting fast task...' && sleep 1 && echo 'Fast task done!'", + runtime: "BASH", + detached: true, + stdout: null, + stderr: null, + }), + ]); + + log("Both commands started in background! Waiting for results...\n"); + + await Promise.all([ + cmd1.wait().then(async (finished) => { + log(`[Slow] Exit code: ${finished.data.exit_code}`); + log(`[Slow] Output:\n${await finished.output()}`); + }), + cmd2.wait().then(async (finished) => { + log(`[Fast] Exit code: ${finished.data.exit_code}`); + log(`[Fast] Output:\n${await finished.output()}`); + }), + ]); + + log("\n=== Mode 3: Fire and forget ==="); + log("Commands run on the server independently - you can check back anytime."); + log("Launching a 2-second task, then doing other work for 5 seconds...\n"); + + const cmd3 = await sandbox.runCommand({ + command: "echo 'Quick task' && sleep 2 && echo 'Done!'", runtime: "BASH", detached: true, stdout: null, stderr: null, - }), -]); - -log("Both commands started in background! Waiting for results...\n"); - -await Promise.all([ - cmd1.wait().then(async (finished) => { - log(`[Slow] Exit code: ${finished.data.exit_code}`); - log(`[Slow] Output:\n${await finished.output()}`); - }), - cmd2.wait().then(async (finished) => { - log(`[Fast] Exit code: ${finished.data.exit_code}`); - log(`[Fast] Output:\n${await finished.output()}`); - }), -]); - -log("\n=== Mode 3: Fire and forget ==="); -log("Commands run on the server independently - you can check back anytime."); -log("Launching a 2-second task, then doing other work for 5 seconds...\n"); - -const cmd3 = await sandbox.runCommand({ - command: "echo 'Quick task' && sleep 2 && echo 'Done!'", - runtime: "BASH", - detached: true, - stdout: null, - stderr: null, -}); - -log("Command running on server. Doing other work locally..."); -await new Promise((resolve) => setTimeout(resolve, 5000)); - -log("Done with local work. Checking if server command finished..."); -const start = Date.now(); -const finished3 = await cmd3.wait(); -log( - `Status check took ${Date.now() - start}ms (instant - command already done!)`, -); -log(`Output:\n${await finished3.output()}`); - -log("\nStopping sandbox..."); -await sandbox.stop(); - -log("Ping example completed!"); + }); + + log("Command running on server. Doing other work locally..."); + await new Promise((resolve) => setTimeout(resolve, 5000)); + + log("Done with local work. Checking if server command finished..."); + const start = Date.now(); + const finished3 = await cmd3.wait(); + log( + `Status check took ${Date.now() - start}ms (instant - command already done!)`, + ); + log(`Output:\n${await finished3.output()}`); +} finally { + log("\nStopping sandbox..."); + await sandbox.stop().catch(() => undefined); + log("Ping example completed!"); +} diff --git a/examples/src/snapshots.ts b/examples/src/snapshots.ts new file mode 100644 index 0000000..478faac --- /dev/null +++ b/examples/src/snapshots.ts @@ -0,0 +1,77 @@ +import { Sandbox, type Snapshot } from "@buddy-works/sandbox-sdk"; +import { log } from "@/shared/logger"; + +log("Snapshots Example\n"); + +const baseIdentifier = "snapshot-demo-base"; +const restoredIdentifier = "snapshot-demo-restored"; + +// Cleanup any leftovers from previous runs +for (const id of [baseIdentifier, restoredIdentifier]) { + try { + const existing = await Sandbox.getByIdentifier(id); + log(`Found existing sandbox '${id}', deleting...`); + await existing.destroy(); + } catch { + // not present + } +} + +let base: Sandbox | undefined; +let snapshot: Snapshot | undefined; +let restored: Sandbox | undefined; +try { + log("Creating base sandbox..."); + base = await Sandbox.create({ + identifier: baseIdentifier, + name: "Snapshot Demo Base", + os: "ubuntu:24.04", + }); + log( + `Created base sandbox: ${base.data.identifier} (${base.data.html_url})\n`, + ); + + log("Uploading a marker file to the base sandbox..."); + const markerFilename = "snapshot-marker.txt"; + const markerContent = `created at ${new Date().toISOString()}`; + await base.fs.uploadFile(Buffer.from(markerContent), markerFilename); + log(`Wrote '${markerFilename}' with content: ${markerContent}\n`); + + log("Creating a snapshot (returns immediately with status CREATING)..."); + snapshot = await base.createSnapshot({ name: "demo-snapshot" }); + log(`Snapshot id=${snapshot.id} initial status=${snapshot.data.status}\n`); + + log("Waiting until snapshot is CREATED..."); + const start = Date.now(); + await snapshot.waitUntilReady(); + log( + `Snapshot ready in ${Date.now() - start}ms (status=${snapshot.data.status})\n`, + ); + + log("Listing snapshots for the base sandbox:"); + const snapshots = await base.listSnapshots(); + for (const s of snapshots) { + log(` ${s.id} ${s.data.name} ${s.data.status}`); + } + + log("\nCreating a new sandbox from the snapshot..."); + restored = await Sandbox.createFromSnapshot(snapshot.id, { + identifier: restoredIdentifier, + name: "Snapshot Demo Restored", + }); + log( + `Restored sandbox: ${restored.data.identifier} (${restored.data.html_url})`, + ); + log(`Status: ${restored.data.status}\n`); + + log("Verifying the marker file is present in the restored sandbox..."); + const downloaded = await restored.fs.downloadFile(markerFilename); + const matches = downloaded.toString() === markerContent; + log(`Marker content matches: ${matches}\n`); +} finally { + log("Cleaning up..."); + await restored?.destroy().catch(() => undefined); + await snapshot?.delete().catch(() => undefined); + await base?.destroy().catch(() => undefined); + log("All resources deleted successfully"); +} diff --git a/examples/src/streaming.ts b/examples/src/streaming.ts index 482b263..2857b97 100644 --- a/examples/src/streaming.ts +++ b/examples/src/streaming.ts @@ -26,32 +26,33 @@ try { await sandbox.waitUntilRunning(); } -log("\n=== Example 1: Auto-streaming ==="); -log("Output streams to console as the command runs:\n"); - -await sandbox.runCommand({ - command: 'for i in 1 2 3; do echo "Line $i"; sleep 1; done', -}); - -log("\n=== Example 2: Custom log formatting ==="); -log("Manually iterate over logs to add custom prefixes:\n"); - -const command = await sandbox.runCommand({ - command: - 'echo "stdout message" && echo "stderr message" >&2 && echo "another stdout"', - detached: true, - stdout: null, - stderr: null, -}); - -for await (const entry of command.logs({ follow: true })) { - const prefix = entry.type === "STDOUT" ? "[OUT]" : "[ERR]"; - process.stdout.write(`${prefix} ${entry.data}\n`); -} +try { + log("\n=== Example 1: Auto-streaming ==="); + log("Output streams to console as the command runs:\n"); -await command.wait(); + await sandbox.runCommand({ + command: 'for i in 1 2 3; do echo "Line $i"; sleep 1; done', + }); -log("\nStopping sandbox..."); -await sandbox.stop(); + log("\n=== Example 2: Custom log formatting ==="); + log("Manually iterate over logs to add custom prefixes:\n"); -log("Streaming example completed!"); + const command = await sandbox.runCommand({ + command: + 'echo "stdout message" && echo "stderr message" >&2 && echo "another stdout"', + detached: true, + stdout: null, + stderr: null, + }); + + for await (const entry of command.logs({ follow: true })) { + const prefix = entry.type === "STDOUT" ? "[OUT]" : "[ERR]"; + process.stdout.write(`${prefix} ${entry.data}\n`); + } + + await command.wait(); +} finally { + log("\nStopping sandbox..."); + await sandbox.stop().catch(() => undefined); + log("Streaming example completed!"); +} diff --git a/examples/src/update.ts b/examples/src/update.ts new file mode 100644 index 0000000..f541f5f --- /dev/null +++ b/examples/src/update.ts @@ -0,0 +1,54 @@ +import { Sandbox } from "@buddy-works/sandbox-sdk"; +import { log } from "@/shared/logger"; + +log("Sandbox Update Example\n"); + +const identifier = "update-demo-sandbox"; + +try { + const existing = await Sandbox.getByIdentifier(identifier); + log(`Found existing sandbox with identifier: ${identifier}, deleting...`); + await existing.destroy(); +} catch { + // Sandbox doesn't exist, nothing to delete +} + +let sandbox: Sandbox | undefined; +try { + log("Creating sandbox with initial config..."); + sandbox = await Sandbox.create({ + identifier, + name: "Update Demo Sandbox", + os: "ubuntu:24.04", + timeout: 300, + tags: ["initial"], + apps: ["echo initial-app"], + }); + log(`Created sandbox: ${sandbox.data.identifier} (${sandbox.data.html_url})`); + log(`Initial timeout: ${sandbox.data.timeout}`); + log(`Initial tags: ${(sandbox.data.tags ?? []).join(", ")}`); + log( + `Initial apps: ${(sandbox.data.apps ?? []).map((a) => a.command).join(", ")}\n`, + ); + + log("Updating timeout (300 -> 1200), tags and apps in place..."); + await sandbox.update({ + timeout: 1200, + tags: ["updated", "demo"], + apps: [{ command: "echo updated-app" }], + }); + log(`Updated timeout: ${sandbox.data.timeout}`); + log(`Updated tags: ${(sandbox.data.tags ?? []).join(", ")}`); + log( + `Updated apps: ${(sandbox.data.apps ?? []).map((a) => a.command).join(", ")}`, + ); + log( + `Sandbox status: ${sandbox.data.status} (setup: ${sandbox.data.setup_status})\n`, + ); +} finally { + if (sandbox) { + log("Cleaning up..."); + await sandbox.destroy().catch(() => undefined); + log("Sandbox deleted successfully"); + } +} diff --git a/package.json b/package.json index cc80945..375b67c 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "packageManager": "pnpm@10.28.0", "devDependencies": { "@biomejs/biome": "2.3.11", + "@buddy-works/unit-tests": "0.21.2", "@dotenvx/dotenvx": "catalog:", "@hey-api/openapi-ts": "0.97.2", "@tsconfig/node20": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 57706dc..0c00837 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,6 +39,9 @@ importers: '@biomejs/biome': specifier: 2.3.11 version: 2.3.11 + '@buddy-works/unit-tests': + specifier: 0.21.2 + version: 0.21.2(vitest@4.0.17(@types/node@20.19.29)(jiti@2.6.1)(msw@2.12.7(@types/node@20.19.29)(typescript@5.9.3))(tsx@4.21.0)) '@dotenvx/dotenvx': specifier: 'catalog:' version: 1.51.4 @@ -149,24 +152,28 @@ packages: engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [musl] '@biomejs/cli-linux-arm64@2.3.11': resolution: {integrity: sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [glibc] '@biomejs/cli-linux-x64-musl@2.3.11': resolution: {integrity: sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [musl] '@biomejs/cli-linux-x64@2.3.11': resolution: {integrity: sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [glibc] '@biomejs/cli-win32-arm64@2.3.11': resolution: {integrity: sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw==} @@ -180,10 +187,37 @@ packages: cpu: [x64] os: [win32] + '@buddy-works/ci-info@1.1.2': + resolution: {integrity: sha512-DJN+/2CuyWBktfqXldlac8FDXzAdPmmwuQu6mYwWfSbh5gpSvG6Pq9SC/UTTET9pWKhj/9PqPGBNlVTG48FLTw==} + engines: {node: '>=22'} + '@buddy-works/sandbox-sdk@file:': resolution: {directory: '', type: directory} engines: {node: '>=20'} + '@buddy-works/unit-tests@0.21.2': + resolution: {integrity: sha512-RBdugo6waFwOXxElr+Tag18vXHINf9QSzRA/Wjch9RP5VqW6KwMPnO8KUckmZrun2hGPIWrPzwbm2LekNljPpA==} + peerDependencies: + cypress: '>=10.0.0' + jasmine: '>=5.0.0' + jest: '>=24.0.0' + mocha: '>=6.0.0' + playwright: '>=1.0.0' + vitest: '>=3.0.0' + peerDependenciesMeta: + cypress: + optional: true + jasmine: + optional: true + jest: + optional: true + mocha: + optional: true + playwright: + optional: true + vitest: + optional: true + '@dotenvx/dotenvx@1.51.4': resolution: {integrity: sha512-AoziS8lRQ3ew/lY5J4JSlzYSN9Fo0oiyMBY37L3Bwq4mOQJT5GSrdZYLFPt6pH1LApDI3ZJceNyx+rHRACZSeQ==} hasBin: true @@ -521,41 +555,49 @@ packages: resolution: {integrity: sha512-0eVYZxSceNqGADzhlV4ZRqkHF0fjWxRXQOB7Qwl5y1gN/XYUDvMfip+ngtzj4dM7zQT4U97hUhJ7PUKSy/JIGQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-arm64-musl@11.16.3': resolution: {integrity: sha512-B1BvLeZbgDdVN0FvU40l5Q7lej8310WlabCBaouk8jY7H7xbI8phtomTtk3Efmevgfy5hImaQJu6++OmcFb2NQ==} cpu: [arm64] os: [linux] + libc: [musl] '@oxc-resolver/binding-linux-ppc64-gnu@11.16.3': resolution: {integrity: sha512-q7khglic3Jqak7uDgA3MFnjDeI7krQT595GDZpvFq785fmFYSx8rlTkoHzmhQtUisYtl4XG7WUscwsoidFUI4w==} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-riscv64-gnu@11.16.3': resolution: {integrity: sha512-aFRNmQNPzDgQEbw2s3c8yJYRimacSDI+u9df8rn5nSKzTVitHmbEpZqfxpwNLCKIuLSNmozHR1z1OT+oZVeYqg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-riscv64-musl@11.16.3': resolution: {integrity: sha512-vZI85SvSMADcEL9G1TIrV0Rlkc1fY5Mup0DdlVC5EHPysZB4hXXHpr+h09pjlK5y+5om5foIzDRxE1baUCaWOA==} cpu: [riscv64] os: [linux] + libc: [musl] '@oxc-resolver/binding-linux-s390x-gnu@11.16.3': resolution: {integrity: sha512-xiLBnaUlddFEzRHiHiSGEMbkg8EwZY6VD8F+3GfnFsiK3xg/4boaUV2bwXd+nUzl3UDQOMW1QcZJ4jJSb0qiJA==} cpu: [s390x] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-x64-gnu@11.16.3': resolution: {integrity: sha512-6y0b05wIazJJgwu7yU/AYGFswzQQudYJBOb/otDhiDacp1+6ye8egoxx63iVo9lSpDbipL++54AJQFlcOHCB+g==} cpu: [x64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-x64-musl@11.16.3': resolution: {integrity: sha512-RmMgwuMa42c9logS7Pjprf5KCp8J1a1bFiuBFtG9/+yMu0BhY2t+0VR/um7pwtkNFvIQqAVh6gDOg/PnoKRcdQ==} cpu: [x64] os: [linux] + libc: [musl] '@oxc-resolver/binding-openharmony-arm64@11.16.3': resolution: {integrity: sha512-/7AYRkjjW7xu1nrHgWUFy99Duj4/ydOBVaHtODie9/M6fFngo+8uQDFFnzmr4q//sd/cchIerISp/8CQ5TsqIA==} @@ -620,24 +662,28 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.0-beta.60': resolution: {integrity: sha512-8xlqGLDtTP8sBfYwneTDu8+PRm5reNEHAuI/+6WPy9y350ls0KTFd3EJCOWEXWGW0F35ko9Fn9azmurBTjqOrQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@rolldown/binding-linux-x64-gnu@1.0.0-beta.60': resolution: {integrity: sha512-iR4nhVouVZK1CiGGGyz+prF5Lw9Lmz30Rl36Hajex+dFVFiegka604zBwzTp5Tl0BZnr50ztnVJ30tGrBhDr8Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.0-beta.60': resolution: {integrity: sha512-HbfNcqNeqxFjSMf1Kpe8itr2e2lr0Bm6HltD2qXtfU91bSSikVs9EWsa1ThshQ1v2ZvxXckGjlVLtah6IoslPg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.0-beta.60': resolution: {integrity: sha512-BiiamFcgTJ+ZFOUIMO9AHXUo9WXvHVwGfSrJ+Sv0AsTd2w3VN7dJGiH3WRcxKFetljJHWvGbM4fdpY5lf6RIvw==} @@ -699,66 +745,79 @@ packages: resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.55.1': resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.55.1': resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.55.1': resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.55.1': resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.55.1': resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.55.1': resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.55.1': resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.55.1': resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.55.1': resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.55.1': resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.55.1': resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.55.1': resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.55.1': resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==} @@ -1714,11 +1773,23 @@ snapshots: '@biomejs/cli-win32-x64@2.3.11': optional: true + '@buddy-works/ci-info@1.1.2': + dependencies: + cross-spawn: 7.0.6 + '@buddy-works/sandbox-sdk@file:': dependencies: p-retry: 7.1.1 zod: 4.3.5 + '@buddy-works/unit-tests@0.21.2(vitest@4.0.17(@types/node@20.19.29)(jiti@2.6.1)(msw@2.12.7(@types/node@20.19.29)(typescript@5.9.3))(tsx@4.21.0))': + dependencies: + '@buddy-works/ci-info': 1.1.2 + pathe: 2.0.3 + tslib: 2.8.1 + optionalDependencies: + vitest: 4.0.17(@types/node@20.19.29)(jiti@2.6.1)(msw@2.12.7(@types/node@20.19.29)(typescript@5.9.3))(tsx@4.21.0) + '@dotenvx/dotenvx@1.51.4': dependencies: commander: 11.1.0 diff --git a/src/core/buddy-api-client.ts b/src/core/buddy-api-client.ts index c8e980d..c41569a 100644 --- a/src/core/buddy-api-client.ts +++ b/src/core/buddy-api-client.ts @@ -1,44 +1,64 @@ import { prettifyError, type z } from "zod"; import { addSandboxResponseTransformer, + addSandboxSnapshotResponseTransformer, createSandboxDirectoryResponseTransformer, executeSandboxCommandResponseTransformer, + getProjectSnapshotsResponseTransformer, getSandboxCommandResponseTransformer, + getSandboxCommandsResponseTransformer, getSandboxContentResponseTransformer, getSandboxResponseTransformer, + getSandboxSnapshotResponseTransformer, + getSandboxSnapshotsResponseTransformer, restartSandboxResponseTransformer, startSandboxAppResponseTransformer, startSandboxResponseTransformer, stopSandboxAppResponseTransformer, stopSandboxResponseTransformer, terminateSandboxCommandResponseTransformer, + updateSandboxResponseTransformer, uploadSandboxFileResponseTransformer, } from "@/api/openapi/transformers.gen"; import type { AddSandboxData, AddSandboxResponse, + AddSandboxSnapshotData, + AddSandboxSnapshotResponse, CreateSandboxDirectoryData, CreateSandboxDirectoryResponse, DeleteSandboxData, DeleteSandboxFileData, DeleteSandboxFileResponse, DeleteSandboxResponse, + DeleteSandboxSnapshotData, + DeleteSandboxSnapshotResponse, + DeleteSnapshotData, + DeleteSnapshotResponse, DownloadSandboxContentData, ExecuteSandboxCommandData, ExecuteSandboxCommandResponse, GetIdentifiersData, GetIdentifiersResponse, + GetProjectSnapshotsData, + GetProjectSnapshotsResponse, GetSandboxAppLogsByIdData, GetSandboxAppLogsByIdResponse, GetSandboxCommandData, GetSandboxCommandLogsData, GetSandboxCommandResponse, + GetSandboxCommandsData, + GetSandboxCommandsResponse, GetSandboxContentData, GetSandboxContentResponse, GetSandboxData, GetSandboxesData, GetSandboxesResponse, GetSandboxResponse, + GetSandboxSnapshotData, + GetSandboxSnapshotResponse, + GetSandboxSnapshotsData, + GetSandboxSnapshotsResponse, RestartSandboxData, RestartSandboxResponse, SandboxCommandLog, @@ -52,6 +72,8 @@ import type { StopSandboxResponse, TerminateSandboxCommandData, TerminateSandboxCommandResponse, + UpdateSandboxData, + UpdateSandboxResponse, UploadSandboxFileData, UploadSandboxFileResponse, } from "@/api/openapi/types.gen"; @@ -60,12 +82,19 @@ import { zAddSandboxPath, zAddSandboxQuery, zAddSandboxResponse, + zAddSandboxSnapshotBody, + zAddSandboxSnapshotPath, + zAddSandboxSnapshotResponse, zCreateSandboxDirectoryPath, zCreateSandboxDirectoryResponse, zDeleteSandboxFilePath, zDeleteSandboxFileResponse, zDeleteSandboxPath, zDeleteSandboxResponse, + zDeleteSandboxSnapshotPath, + zDeleteSandboxSnapshotResponse, + zDeleteSnapshotPath, + zDeleteSnapshotResponse, zDownloadSandboxContentPath, zExecuteSandboxCommandBody, zExecuteSandboxCommandPath, @@ -73,6 +102,9 @@ import { zGetIdentifiersPath, zGetIdentifiersQuery, zGetIdentifiersResponse, + zGetProjectSnapshotsPath, + zGetProjectSnapshotsQuery, + zGetProjectSnapshotsResponse, zGetSandboxAppLogsByIdPath, zGetSandboxAppLogsByIdQuery, zGetSandboxAppLogsByIdResponse, @@ -80,6 +112,8 @@ import { zGetSandboxCommandLogsQuery, zGetSandboxCommandPath, zGetSandboxCommandResponse, + zGetSandboxCommandsPath, + zGetSandboxCommandsResponse, zGetSandboxContentPath, zGetSandboxContentResponse, zGetSandboxesPath, @@ -87,6 +121,10 @@ import { zGetSandboxesResponse, zGetSandboxPath, zGetSandboxResponse, + zGetSandboxSnapshotPath, + zGetSandboxSnapshotResponse, + zGetSandboxSnapshotsPath, + zGetSandboxSnapshotsResponse, zRestartSandboxPath, zRestartSandboxResponse, zSandboxCommandLog, @@ -100,6 +138,9 @@ import { zStopSandboxResponse, zTerminateSandboxCommandPath, zTerminateSandboxCommandResponse, + zUpdateSandboxBody, + zUpdateSandboxPath, + zUpdateSandboxResponse, zUploadSandboxFilePath, zUploadSandboxFileResponse, } from "@/api/openapi/zod.gen"; @@ -178,7 +219,7 @@ export class BuddyApiClient extends HttpClient { responseSchema, skipRetry, }: { - method: "GET" | "POST" | "DELETE"; + method: "GET" | "POST" | "DELETE" | "PATCH"; url: DataUrl; data: ClientData; bodySchema?: z.ZodType; @@ -249,6 +290,14 @@ export class BuddyApiClient extends HttpClient { request = this.delete(parameterizedUrl, requestConfig); break; } + case "PATCH": { + request = this.patch( + parameterizedUrl, + validatedBody ?? {}, + requestConfig, + ); + break; + } } const response = await request; @@ -270,6 +319,134 @@ export class BuddyApiClient extends HttpClient { }); } + /** Update an existing sandbox's configuration (timeout, apps, endpoints, etc.) */ + async updateSandbox( + data: ClientData, + ) { + return this.#requestWithValidation({ + method: "PATCH", + data, + url: "/workspaces/{workspace_domain}/sandboxes/{id}", + bodySchema: zUpdateSandboxBody, + pathSchema: zUpdateSandboxPath, + responseSchema: zUpdateSandboxResponse.transform( + updateSandboxResponseTransformer, + ), + }); + } + + /** List snapshots for a sandbox */ + async getSandboxSnapshots( + data: ClientData, + ) { + return this.#requestWithValidation({ + method: "GET", + data, + url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/snapshots", + pathSchema: zGetSandboxSnapshotsPath, + responseSchema: zGetSandboxSnapshotsResponse.transform( + getSandboxSnapshotsResponseTransformer, + ), + }); + } + + /** List all snapshots in the project (across all sandboxes, including orphans) */ + async getProjectSnapshots( + data: ClientData, + ) { + return this.#requestWithValidation({ + method: "GET", + data, + url: "/workspaces/{workspace_domain}/sandboxes/snapshots", + pathSchema: zGetProjectSnapshotsPath, + querySchema: zGetProjectSnapshotsQuery, + responseSchema: zGetProjectSnapshotsResponse.transform( + getProjectSnapshotsResponseTransformer, + ), + }); + } + + /** Create a snapshot of a sandbox */ + async addSandboxSnapshot( + data: ClientData, + ) { + return this.#requestWithValidation({ + method: "POST", + data, + url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/snapshots", + bodySchema: zAddSandboxSnapshotBody, + pathSchema: zAddSandboxSnapshotPath, + responseSchema: zAddSandboxSnapshotResponse.transform( + addSandboxSnapshotResponseTransformer, + ), + }); + } + + /** Get a single sandbox snapshot by ID */ + async getSandboxSnapshot( + data: ClientData, + ) { + return this.#requestWithValidation({ + method: "GET", + data, + url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/snapshots/{id}", + pathSchema: zGetSandboxSnapshotPath, + responseSchema: zGetSandboxSnapshotResponse.transform( + getSandboxSnapshotResponseTransformer, + ), + }); + } + + /** Delete a sandbox snapshot by ID */ + async deleteSandboxSnapshot( + data: ClientData, + ) { + try { + return await this.#requestWithValidation< + Data, + DeleteSandboxSnapshotResponse + >({ + method: "DELETE", + data, + url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/snapshots/{id}", + pathSchema: zDeleteSandboxSnapshotPath, + responseSchema: zDeleteSandboxSnapshotResponse, + skipRetry: true, + }); + } catch (error) { + // Ignore 404 errors - snapshot already deleted + if (error instanceof HttpError && error.status === 404) { + return; + } + throw error; + } + } + + /** + * Delete a snapshot by ID at the project level (works for snapshots whose + * parent sandbox has been deleted). + */ + async deleteSnapshot( + data: ClientData, + ) { + try { + return await this.#requestWithValidation({ + method: "DELETE", + data, + url: "/workspaces/{workspace_domain}/sandboxes/snapshots/{id}", + pathSchema: zDeleteSnapshotPath, + responseSchema: zDeleteSnapshotResponse, + skipRetry: true, + }); + } catch (error) { + // Ignore 404 errors - snapshot already deleted + if (error instanceof HttpError && error.status === 404) { + return; + } + throw error; + } + } + /** Get a specific sandbox by its ID */ async getSandboxById( data: ClientData, @@ -315,6 +492,21 @@ export class BuddyApiClient extends HttpClient { }); } + /** List all command executions in a sandbox (history) */ + async getSandboxCommands( + data: ClientData, + ) { + return this.#requestWithValidation({ + method: "GET", + data, + url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/commands", + pathSchema: zGetSandboxCommandsPath, + responseSchema: zGetSandboxCommandsResponse.transform( + getSandboxCommandsResponseTransformer, + ), + }); + } + /** Get a specific command execution details */ async getCommandDetails( data: ClientData, diff --git a/src/core/http-client.ts b/src/core/http-client.ts index e1634e5..bf46fca 100644 --- a/src/core/http-client.ts +++ b/src/core/http-client.ts @@ -299,6 +299,15 @@ export class HttpClient { return this.#request("DELETE", url, undefined, config); } + /** Perform a PATCH request with optional body data */ + async patch( + url: string, + data?: unknown, + config?: RequestConfig, + ): Promise> { + return this.#request("PATCH", url, data ?? {}, config); + } + /** Set the Bearer token for authenticated requests */ setAuthToken(token: string): void { this.#authToken = token; diff --git a/src/entity/sandbox.ts b/src/entity/sandbox.ts index 6170633..6f87097 100644 --- a/src/entity/sandbox.ts +++ b/src/entity/sandbox.ts @@ -1,15 +1,23 @@ import type { Writable } from "node:stream"; import type { + AddSnapshotRequest, + CloneSandboxRequest, + CreateFromSnapshotRequestWritable, CreateNewSandboxRequestWritable, ExecuteSandboxCommandRequest, GetSandboxAppLogsByIdResponse, GetSandboxResponse, SandboxAppView, SandboxIdView, + ShortSnapshotView, + SnapshotView, + UpdateSandboxRequestWritable, } from "@/api/openapi/types.gen"; import type { BuddyApiClient } from "@/core/buddy-api-client"; +import { HttpError } from "@/core/http-client"; import { Command } from "@/entity/command"; import { FileSystem } from "@/entity/filesystem"; +import { Snapshot } from "@/entity/snapshot"; import { withErrorHandler } from "@/errors"; import { type ConnectionConfig, createClient } from "@/utils/client"; import logger from "@/utils/logger"; @@ -46,6 +54,24 @@ export interface ListSandboxesConfig { connection?: ConnectionConfig; } +/** + * Configuration for creating a sandbox from an existing snapshot + */ +export interface CreateFromSnapshotConfig + extends Partial> { + /** Optional connection configuration to override defaults */ + connection?: ConnectionConfig; +} + +/** + * Configuration for cloning an existing sandbox + */ +export interface CloneSandboxConfig + extends Partial> { + /** Optional connection configuration to override defaults */ + connection?: ConnectionConfig; +} + /** * Options for running a command in the sandbox */ @@ -91,6 +117,34 @@ export class Sandbox { return this.#fs; } + /** + * Create a Sandbox instance from an `addSandbox` response and wait for it + * to reach a RUNNING state. Shared between `create` and `createFromSnapshot`. + */ + static async #finalizeNewSandbox( + sandboxResponse: NonNullable, + client: BuddyApiClient, + ): Promise { + const sandbox = new Sandbox( + sandboxResponse, + client, + PRIVATE_CONSTRUCTOR_KEY, + ); + + logger.debug(`Waiting for sandbox ${sandbox.data.id} to be ready...`); + await sandbox.waitUntilReady(); + logger.debug( + `Sandbox ${sandbox.data.id} is ready (Setup status: ${sandbox.data.setup_status})`, + ); + + await sandbox.waitUntilRunning(); + logger.debug( + `Sandbox ${sandbox.data.id} is now running. Status: ${sandbox.data.status}`, + ); + + return sandbox; + } + /** * Create a new sandbox * @param config - Sandbox configuration including identifier, name, os, and optional connection settings @@ -103,7 +157,7 @@ export class Sandbox { const requestBody: CreateNewSandboxRequestWritable = { name: `Sandbox ${new Date().toISOString()}`, - identifier: config?.identifier || `sandbox_${String(Date.now())}`, + identifier: `sandbox_${String(Date.now())}`, os: "ubuntu:24.04", ...sandboxConfig, }; @@ -112,27 +166,70 @@ export class Sandbox { body: requestBody, }); - const sandbox = new Sandbox( - sandboxResponse, - client, - PRIVATE_CONSTRUCTOR_KEY, - ); + return Sandbox.#finalizeNewSandbox(sandboxResponse, client); + }); + } - logger.debug(`Waiting for sandbox ${sandbox.data.id} to be ready...`); + /** + * Create a new sandbox from an existing snapshot + * + * @param snapshotId - ID of the snapshot to create from + * @param config - Optional sandbox configuration overrides (name, identifier, …) + * @returns A ready-to-use Sandbox instance restored from the snapshot + */ + static async createFromSnapshot( + snapshotId: string, + config?: CreateFromSnapshotConfig, + ) { + return withErrorHandler( + "Failed to create sandbox from snapshot", + async () => { + const { connection, ...snapshotConfig } = config ?? {}; + const client = createClient(connection); + + const requestBody: CreateFromSnapshotRequestWritable = { + snapshot_id: snapshotId, + name: `Sandbox ${new Date().toISOString()}`, + identifier: `sandbox_${String(Date.now())}`, + ...snapshotConfig, + }; + + const sandboxResponse = await client.addSandbox({ + body: requestBody, + }); - await sandbox.waitUntilReady(); + return Sandbox.#finalizeNewSandbox(sandboxResponse, client); + }, + ); + } - logger.debug( - `Sandbox ${sandbox.data.id} is ready (Setup status: ${sandbox.data.setup_status})`, - ); + /** + * Clone an existing sandbox by ID into a new one. + * + * Differs from `createFromSnapshot` in that it copies the current state of + * a live sandbox directly, without going through a snapshot. + * + * @param sourceSandboxId - ID of the sandbox to clone + * @param config - Optional sandbox configuration overrides (name, identifier) + * @returns A ready-to-use Sandbox instance cloned from the source + */ + static async clone(sourceSandboxId: string, config?: CloneSandboxConfig) { + return withErrorHandler("Failed to clone sandbox", async () => { + const { connection, ...cloneConfig } = config ?? {}; + const client = createClient(connection); - await sandbox.waitUntilRunning(); + const requestBody: CloneSandboxRequest = { + source_sandbox_id: sourceSandboxId, + name: `Sandbox ${new Date().toISOString()}`, + identifier: `sandbox_${String(Date.now())}`, + ...cloneConfig, + }; - logger.debug( - `Sandbox ${sandbox.data.id} is now running. Status: ${sandbox.data.status}`, - ); + const sandboxResponse = await client.addSandbox({ + body: requestBody, + }); - return sandbox; + return Sandbox.#finalizeNewSandbox(sandboxResponse, client); }); } @@ -157,17 +254,14 @@ export class Sandbox { query: { project: client.project_name, sandbox: identifier }, }); sandboxId = identifiers.sandbox_id; - } catch {} - - // : Remove this fallback when the identifiers endpoint is rolled out to prod (should be out by March 2026) - if (!sandboxId) { - try { - const sandboxList = await client.getSandboxes({}); - sandboxId = sandboxList?.sandboxes?.find( - (s) => s.identifier === identifier, - )?.id; - } catch {} - } // + } catch (error) { + // 404 means the identifier doesn't exist - fall through to the + // "not found" error below. Anything else (auth, network, 5xx) + // surfaces to the caller. + if (!(error instanceof HttpError) || error.status !== 404) { + throw error; + } + } if (!sandboxId) { throw new Error(`Sandbox with identifier '${identifier}' not found`); @@ -231,6 +325,32 @@ export class Sandbox { }); } + /** + * Get a single snapshot by its sandbox + snapshot ID without first fetching + * the parent sandbox. Useful when you already have both IDs (e.g. from a + * persisted reference). + * + * @param sandboxId - ID of the sandbox the snapshot belongs to + * @param snapshotId - ID of the snapshot to fetch + * @param config - Optional configuration including connection settings + */ + static async getSnapshotById( + sandboxId: string, + snapshotId: NonNullable, + config?: GetSandboxConfig, + ): Promise { + return withErrorHandler("Failed to get snapshot", async () => { + const { connection } = config ?? {}; + const client = createClient(connection); + + const data = await client.getSandboxSnapshot({ + path: { sandbox_id: sandboxId, id: snapshotId }, + }); + + return Snapshot._build(data, client, sandboxId); + }); + } + /** * Execute a command in the sandbox * @returns Command instance (call wait() for blocking execution) @@ -285,6 +405,175 @@ export class Sandbox { }); } + /** + * List all command executions in this sandbox (history). + * + * Returns one `Command` instance per past execution. Useful for inspecting + * previous runs, fetching their logs, or killing still-running detached + * commands. + */ + async listCommands(): Promise { + const sandboxId = this.initializedId; + return withErrorHandler("Failed to list commands", async () => { + const response = await this.#client.getSandboxCommands({ + path: { sandbox_id: sandboxId }, + }); + return (response?.commands ?? []).map( + (commandResponse) => + new Command({ + commandResponse, + client: this.#client, + sandboxId, + }), + ); + }); + } + + /** + * Update the sandbox configuration in place + * + * Accepts a partial update of any mutable sandbox field (`timeout`, `apps`, + * `endpoints`, `variables`, `tags`, `resources`, …). Updates the internal + * state with the API's response. + * + * @remarks Changing `first_boot_commands` leaves the sandbox in + * `setup_status: STALE` — the new commands only apply on first boot, so + * the sandbox must be recreated to take effect. + */ + async update(config: Partial): Promise { + const sandboxId = this.initializedId; + return withErrorHandler("Failed to update sandbox", async () => { + this.#sandboxData = await this.#client.updateSandbox({ + body: config, + path: { id: sandboxId }, + }); + }); + } + + /** + * Create a snapshot of the current sandbox + * + * Returns immediately with a snapshot whose `status` is `"CREATING"`. Call + * `snapshot.waitUntilReady()` (or `sandbox.waitForSnapshotReady(snapshot.id)`) + * before restoring from it. + * + * @param config - Optional snapshot configuration (name, description, …) + */ + async createSnapshot(config?: AddSnapshotRequest): Promise { + const sandboxId = this.initializedId; + return withErrorHandler("Failed to create snapshot", async () => { + const data = await this.#client.addSandboxSnapshot({ + body: config ?? {}, + path: { sandbox_id: sandboxId }, + }); + return Snapshot._build(data, this.#client, sandboxId); + }); + } + + /** + * List all snapshots in the project across every sandbox, including + * snapshots whose parent sandbox has been deleted. + * + * Use `Sandbox.createFromSnapshot(snapshot.id, …)` to provision a new + * sandbox from any item in the list. + * + * @param config - Optional configuration including connection settings + */ + static async listSnapshots( + config?: ListSandboxesConfig, + ): Promise { + return withErrorHandler("Failed to list project snapshots", async () => { + const { connection } = config ?? {}; + const client = createClient(connection); + + const response = await client.getProjectSnapshots({}); + return response?.snapshots ?? []; + }); + } + + /** + * List snapshots of this sandbox + */ + async listSnapshots(): Promise { + const sandboxId = this.initializedId; + return withErrorHandler("Failed to list snapshots", async () => { + const response = await this.#client.getSandboxSnapshots({ + path: { sandbox_id: sandboxId }, + }); + return (response?.snapshots ?? []).map((data) => + Snapshot._build(data, this.#client, sandboxId), + ); + }); + } + + /** + * Get a single snapshot of this sandbox by ID + */ + async getSnapshot( + snapshotId: NonNullable, + ): Promise { + const sandboxId = this.initializedId; + return withErrorHandler("Failed to get snapshot", async () => { + const data = await this.#client.getSandboxSnapshot({ + path: { sandbox_id: sandboxId, id: snapshotId }, + }); + return Snapshot._build(data, this.#client, sandboxId); + }); + } + + /** + * Delete a snapshot by ID without needing the parent sandbox. Useful for + * cleaning up snapshots whose parent sandbox has already been deleted. + * + * @param snapshotId - ID of the snapshot to delete + * @param config - Optional configuration including connection settings + */ + static async deleteSnapshot( + snapshotId: NonNullable, + config?: GetSandboxConfig, + ): Promise { + return withErrorHandler("Failed to delete snapshot", async () => { + const { connection } = config ?? {}; + const client = createClient(connection); + + await client.deleteSnapshot({ path: { id: snapshotId } }); + }); + } + + /** + * Delete a snapshot of this sandbox by ID + */ + async deleteSnapshot( + snapshotId: NonNullable, + ): Promise { + const sandboxId = this.initializedId; + return withErrorHandler("Failed to delete snapshot", async () => { + await this.#client.deleteSandboxSnapshot({ + path: { sandbox_id: sandboxId, id: snapshotId }, + }); + }); + } + + /** + * Wait until a snapshot reaches CREATED state. + * + * `createSnapshot()` returns immediately with `status: "CREATING"`. The + * snapshot cannot be used as input to `Sandbox.createFromSnapshot()` until + * it transitions to `"CREATED"`. Use this method to block until it does. + * + * @param snapshotId - ID of the snapshot to wait for + * @param pollIntervalMs - How often to check the status (default: 2000ms (2s)) + * @param maxWaitMs - Maximum time to wait before timing out (default: 180000ms (180s)) + */ + async waitForSnapshotReady( + snapshotId: NonNullable, + pollIntervalMs = 2000, + maxWaitMs = 180_000, + ): Promise { + const snapshot = await this.getSnapshot(snapshotId); + await snapshot.waitUntilReady(pollIntervalMs, maxWaitMs); + } + /** * Delete the sandbox permanently */ diff --git a/src/entity/snapshot.ts b/src/entity/snapshot.ts new file mode 100644 index 0000000..d805bda --- /dev/null +++ b/src/entity/snapshot.ts @@ -0,0 +1,122 @@ +import type { SnapshotView } from "@/api/openapi/types.gen"; +import type { BuddyApiClient } from "@/core/buddy-api-client"; +import { withErrorHandler } from "@/errors"; + +const PRIVATE_CONSTRUCTOR_KEY = Symbol("SnapshotConstructor"); +const INITIALIZE_INSTRUCTIONS = + "Snapshots are obtained via sandbox.createSnapshot(), sandbox.listSnapshots(), sandbox.getSnapshot(id), or Sandbox.getSnapshotById(sandboxId, snapshotId)."; + +export class Snapshot { + #data: SnapshotView; + readonly #client: BuddyApiClient; + readonly #sandboxId: string; + + /** The raw snapshot response data from the API */ + get data(): SnapshotView { + return this.#data; + } + + /** The snapshot ID, throws if not present */ + get id(): NonNullable { + const id = this.#data.id; + if (!id) { + throw new Error(`Snapshot ID is missing. ${INITIALIZE_INSTRUCTIONS}`); + } + return id; + } + + /** ID of the sandbox this snapshot belongs to */ + get sandboxId(): string { + return this.#sandboxId; + } + + /** + * Refresh the snapshot data from the API + * Updates the internal state with the latest snapshot information + */ + async refresh(): Promise { + return withErrorHandler("Failed to refresh snapshot", async () => { + this.#data = await this.#client.getSandboxSnapshot({ + path: { sandbox_id: this.#sandboxId, id: this.id }, + }); + }); + } + + /** + * Wait until the snapshot reaches CREATED state. + * + * `sandbox.createSnapshot()` returns immediately with `status: "CREATING"`. + * The snapshot cannot be restored until it transitions to `"CREATED"`. + * + * @param pollIntervalMs - How often to check the status (default: 2000ms (2s)) + * @param maxWaitMs - Maximum time to wait before timing out (default: 180000ms (180s)) + */ + async waitUntilReady( + pollIntervalMs = 2000, + maxWaitMs = 180_000, + ): Promise { + return withErrorHandler("Snapshot not ready", async () => { + const startTime = Date.now(); + + while (true) { + await this.refresh(); + + if (this.#data.status === "CREATED") { + return; + } + + if (this.#data.status === "FAILED") { + throw new Error( + `Snapshot ${this.id} failed. Status: ${this.#data.status}`, + ); + } + + if (Date.now() - startTime > maxWaitMs) { + throw new Error( + `Timeout waiting for snapshot ${this.id} to be CREATED. Current: ${this.#data.status}`, + ); + } + + await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); + } + }); + } + + /** + * Delete this snapshot permanently + */ + async delete(): Promise { + return withErrorHandler("Failed to delete snapshot", async () => { + await this.#client.deleteSandboxSnapshot({ + path: { sandbox_id: this.#sandboxId, id: this.id }, + }); + }); + } + + private constructor( + data: SnapshotView, + client: BuddyApiClient, + sandboxId: string, + constructorKey: symbol, + ) { + if (constructorKey !== PRIVATE_CONSTRUCTOR_KEY) { + throw new Error( + `Cannot construct Snapshot directly. ${INITIALIZE_INSTRUCTIONS}`, + ); + } + this.#data = data; + this.#client = client; + this.#sandboxId = sandboxId; + } + + /** + * @internal Factory used by the Sandbox class to construct Snapshot instances. + */ + static _build( + data: SnapshotView, + client: BuddyApiClient, + sandboxId: string, + ): Snapshot { + return new Snapshot(data, client, sandboxId, PRIVATE_CONSTRUCTOR_KEY); + } +} diff --git a/src/index.ts b/src/index.ts index 80d91c7..c215f61 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,15 +1,25 @@ -export type { SandboxAppView, SandboxIdView } from "@/api/openapi/types.gen"; +export type { + AddSnapshotRequest, + CloneSandboxRequest, + SandboxAppView, + SandboxIdView, + ShortSnapshotView, + SnapshotView, +} from "@/api/openapi/types.gen"; export { BuddyApiClient } from "@/core/buddy-api-client"; export { Command } from "@/entity/command"; export type { FileInfo, GetFileSystemConfig } from "@/entity/filesystem"; export { FileSystem } from "@/entity/filesystem"; export type { + CloneSandboxConfig, ConnectionConfig, + CreateFromSnapshotConfig, CreateSandboxConfig, GetSandboxConfig, ListSandboxesConfig, } from "@/entity/sandbox"; export { Sandbox } from "@/entity/sandbox"; +export { Snapshot } from "@/entity/snapshot"; export { BuddySDKError, ERROR_CODES, type ErrorCode } from "@/errors"; export type { Region } from "@/utils/regions"; export { API_URLS, REGIONS } from "@/utils/regions"; diff --git a/tests/buddy-api-client.test.ts b/tests/buddy-api-client.test.ts index a594ced..e7ffb6b 100644 --- a/tests/buddy-api-client.test.ts +++ b/tests/buddy-api-client.test.ts @@ -190,6 +190,36 @@ describe("BuddyApiClient", () => { expect(response?.timeout).toBe(600); }); + it("should forward source_sandbox_id when cloning a sandbox", async () => { + let receivedBody: Record | undefined; + server.use( + http.post( + `${TEST_API_URL}/workspaces/${TEST_WORKSPACE}/sandboxes`, + async ({ request }) => { + receivedBody = (await request.json()) as Record; + return HttpResponse.json({ + id: "clone-id", + name: receivedBody["name"], + identifier: receivedBody["identifier"], + status: "STARTING", + }); + }, + ), + ); + + const client = createClient(); + const response = await client.addSandbox({ + body: { + source_sandbox_id: "source-sandbox", + name: "Clone Sandbox", + identifier: "clone-sandbox", + }, + }); + + expect(receivedBody?.["source_sandbox_id"]).toBe("source-sandbox"); + expect(response?.id).toBe("clone-id"); + }); + it("should forward fetch items in the request body", async () => { let receivedBody: Record | undefined; const fetchItems = [ @@ -386,6 +416,101 @@ describe("BuddyApiClient", () => { }); }); + describe("sandbox apps", () => { + it("should start a sandbox app and return the updated sandbox", async () => { + server.use( + http.post( + `${TEST_API_URL}/workspaces/${TEST_WORKSPACE}/sandboxes/sandbox-id/apps/app-1/start`, + () => + HttpResponse.json({ + id: "sandbox-id", + status: "RUNNING", + apps: [ + { + id: "app-1", + command: "node server.js", + app_status: "RUNNING", + }, + { + id: "app-2", + command: "python worker.py", + app_status: "RUNNING", + }, + ], + }), + ), + ); + + const client = createClient(); + const response = await client.startSandboxApp({ + path: { sandbox_id: "sandbox-id", app_id: "app-1" }, + }); + + expect(response?.apps?.find((a) => a.id === "app-1")?.app_status).toBe( + "RUNNING", + ); + }); + + it("should stop a sandbox app without affecting others", async () => { + server.use( + http.post( + `${TEST_API_URL}/workspaces/${TEST_WORKSPACE}/sandboxes/sandbox-id/apps/app-1/stop`, + () => + HttpResponse.json({ + id: "sandbox-id", + status: "RUNNING", + apps: [ + { id: "app-1", command: "node server.js", app_status: "ENDED" }, + { + id: "app-2", + command: "python worker.py", + app_status: "RUNNING", + }, + ], + }), + ), + ); + + const client = createClient(); + const response = await client.stopSandboxApp({ + path: { sandbox_id: "sandbox-id", app_id: "app-1" }, + }); + + expect(response?.apps?.find((a) => a.id === "app-1")?.app_status).toBe( + "ENDED", + ); + expect(response?.apps?.find((a) => a.id === "app-2")?.app_status).toBe( + "RUNNING", + ); + }); + + it("should fetch app logs with pagination cursor", async () => { + let receivedCursor: string | null = null; + server.use( + http.get( + `${TEST_API_URL}/workspaces/${TEST_WORKSPACE}/sandboxes/sandbox-id/apps/app-1/logs`, + ({ request }) => { + receivedCursor = new URL(request.url).searchParams.get("cursor"); + return HttpResponse.json({ + logs: ["line 1", "line 2", "line 3"], + cursor: "next-page-cursor", + }); + }, + ), + ); + + const client = createClient(); + const response = await client.getSandboxAppLogs({ + path: { sandbox_id: "sandbox-id", app_id: "app-1" }, + query: { cursor: "prev-cursor" }, + }); + + expect(receivedCursor).toBe("prev-cursor"); + expect(response?.logs).toEqual(["line 1", "line 2", "line 3"]); + expect(response?.cursor).toBe("next-page-cursor"); + }); + }); + describe("file operations", () => { it("should get sandbox content", async () => { server.use( @@ -499,5 +624,240 @@ describe("BuddyApiClient", () => { }), ).resolves.not.toThrow(); }); + + it("should list commands in a sandbox", async () => { + server.use( + http.get( + `${TEST_API_URL}/workspaces/${TEST_WORKSPACE}/sandboxes/sandbox-id/commands`, + () => + HttpResponse.json({ + commands: [ + { + id: "cmd-a", + command: "echo first", + status: "SUCCESSFUL", + exit_code: 0, + }, + { + id: "cmd-b", + command: "sleep 60", + status: "INPROGRESS", + }, + ], + }), + ), + ); + + const client = createClient(); + const response = await client.getSandboxCommands({ + path: { sandbox_id: "sandbox-id" }, + }); + + expect(response?.commands).toHaveLength(2); + expect(response?.commands?.[0]?.id).toBe("cmd-a"); + expect(response?.commands?.[1]?.status).toBe("INPROGRESS"); + }); + }); + + describe("updateSandbox", () => { + it("should PATCH sandbox with new config and return updated sandbox", async () => { + let receivedBody: Record | undefined; + let receivedMethod: string | undefined; + server.use( + http.patch( + `${TEST_API_URL}/workspaces/${TEST_WORKSPACE}/sandboxes/sandbox-id`, + async ({ request }) => { + receivedMethod = request.method; + receivedBody = (await request.json()) as Record; + return HttpResponse.json({ + id: "sandbox-id", + timeout: receivedBody["timeout"], + tags: receivedBody["tags"], + status: "RUNNING", + }); + }, + ), + ); + + const client = createClient(); + const response = await client.updateSandbox({ + body: { timeout: 1200, tags: ["updated"] }, + path: { id: "sandbox-id" }, + }); + + expect(receivedMethod).toBe("PATCH"); + expect(receivedBody).toEqual({ timeout: 1200, tags: ["updated"] }); + expect(response?.timeout).toBe(1200); + expect(response?.tags).toEqual(["updated"]); + }); + }); + + describe("sandbox snapshots", () => { + it("should list all snapshots in the project (across sandboxes)", async () => { + let receivedProject: string | null = null; + server.use( + http.get( + `${TEST_API_URL}/workspaces/${TEST_WORKSPACE}/sandboxes/snapshots`, + ({ request }) => { + receivedProject = new URL(request.url).searchParams.get( + "project_name", + ); + return HttpResponse.json({ + snapshots: [ + { id: "snap-a", name: "From sandbox A", status: "CREATED" }, + { id: "snap-b", name: "Orphan", status: "CREATED" }, + ], + }); + }, + ), + ); + + const client = createClient(); + const response = await client.getProjectSnapshots({}); + + expect(receivedProject).toBe(TEST_PROJECT); + expect(response?.snapshots).toHaveLength(2); + expect(response?.snapshots?.map((s) => s.id)).toEqual([ + "snap-a", + "snap-b", + ]); + }); + + it("should list snapshots for a sandbox", async () => { + server.use( + http.get( + `${TEST_API_URL}/workspaces/${TEST_WORKSPACE}/sandboxes/sandbox-id/snapshots`, + () => + HttpResponse.json({ + snapshots: [ + { id: "snap-1", name: "First" }, + { id: "snap-2", name: "Second" }, + ], + }), + ), + ); + + const client = createClient(); + const response = await client.getSandboxSnapshots({ + path: { sandbox_id: "sandbox-id" }, + }); + + expect(response?.snapshots).toHaveLength(2); + expect(response?.snapshots?.[0]?.id).toBe("snap-1"); + }); + + it("should create a snapshot with optional name", async () => { + let receivedBody: Record | undefined; + server.use( + http.post( + `${TEST_API_URL}/workspaces/${TEST_WORKSPACE}/sandboxes/sandbox-id/snapshots`, + async ({ request }) => { + receivedBody = (await request.json()) as Record; + return HttpResponse.json( + { id: "snap-new", name: receivedBody["name"] }, + { status: 201 }, + ); + }, + ), + ); + + const client = createClient(); + const response = await client.addSandboxSnapshot({ + body: { name: "before-deploy" }, + path: { sandbox_id: "sandbox-id" }, + }); + + expect(receivedBody).toEqual({ name: "before-deploy" }); + expect(response?.id).toBe("snap-new"); + }); + + it("should fetch a specific snapshot by ID", async () => { + server.use( + http.get( + `${TEST_API_URL}/workspaces/${TEST_WORKSPACE}/sandboxes/sandbox-id/snapshots/snap-1`, + () => HttpResponse.json({ id: "snap-1", name: "First" }), + ), + ); + + const client = createClient(); + const response = await client.getSandboxSnapshot({ + path: { sandbox_id: "sandbox-id", id: "snap-1" }, + }); + + expect(response?.id).toBe("snap-1"); + }); + + it("should delete a snapshot", async () => { + server.use( + http.delete( + `${TEST_API_URL}/workspaces/${TEST_WORKSPACE}/sandboxes/sandbox-id/snapshots/snap-1`, + () => new HttpResponse(null, { status: 204 }), + ), + ); + + const client = createClient(); + await expect( + client.deleteSandboxSnapshot({ + path: { sandbox_id: "sandbox-id", id: "snap-1" }, + }), + ).resolves.not.toThrow(); + }); + + it("should not throw on 404 when deleting a snapshot (already gone)", async () => { + server.use( + http.delete( + `${TEST_API_URL}/workspaces/${TEST_WORKSPACE}/sandboxes/sandbox-id/snapshots/snap-missing`, + () => + HttpResponse.json( + { errors: [{ message: "not found" }] }, + { status: 404 }, + ), + ), + ); + + const client = createClient(); + await expect( + client.deleteSandboxSnapshot({ + path: { sandbox_id: "sandbox-id", id: "snap-missing" }, + }), + ).resolves.toBeUndefined(); + }); + + it("should delete a snapshot at the project level (no sandbox_id)", async () => { + let requested = false; + server.use( + http.delete( + `${TEST_API_URL}/workspaces/${TEST_WORKSPACE}/sandboxes/snapshots/orphan-snap`, + () => { + requested = true; + return new HttpResponse(null, { status: 204 }); + }, + ), + ); + + const client = createClient(); + await expect( + client.deleteSnapshot({ path: { id: "orphan-snap" } }), + ).resolves.not.toThrow(); + expect(requested).toBe(true); + }); + + it("should not throw on 404 when deleting a project-level snapshot", async () => { + server.use( + http.delete( + `${TEST_API_URL}/workspaces/${TEST_WORKSPACE}/sandboxes/snapshots/missing-orphan`, + () => + HttpResponse.json( + { errors: [{ message: "not found" }] }, + { status: 404 }, + ), + ), + ); + + const client = createClient(); + await expect( + client.deleteSnapshot({ path: { id: "missing-orphan" } }), + ).resolves.toBeUndefined(); + }); }); }); diff --git a/tests/errors.test.ts b/tests/errors.test.ts index 00bb6f3..fb56024 100644 --- a/tests/errors.test.ts +++ b/tests/errors.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { beforeAll, describe, expect, it } from "vitest"; import { FileSystem } from "@/entity/filesystem"; import { Sandbox } from "@/entity/sandbox"; import { BuddySDKError } from "@/errors"; @@ -50,4 +50,47 @@ describe("Error handling", () => { expect(() => new Sandbox({}, {}, Symbol())).toThrow(); }); }); + + describe("Sandbox.createFromSnapshot", () => { + it("should throw when snapshot ID does not exist", async () => { + await expect( + Sandbox.createFromSnapshot("non-existent-snapshot-id-12345", { + name: `test-restore-fail-${Date.now()}`, + identifier: `test_restore_fail_${Date.now()}`, + }), + ).rejects.toThrow(); + }); + }); + + describe("sandbox.update and sandbox.getSnapshot on a destroyed sandbox", () => { + let sandbox: Sandbox; + + beforeAll(async () => { + sandbox = await Sandbox.create({ + name: `test-error-${Date.now()}`, + identifier: `test_error_${Date.now()}`, + }); + await sandbox.destroy(); + }, 120_000); + + it("update() should throw on a deleted sandbox", async () => { + await expect(sandbox.update({ timeout: 600 })).rejects.toThrow(); + }); + + it("getSnapshot() with non-existent snapshot ID should throw", async () => { + // Re-create a sandbox for this case; destroyed one would fail at path + // resolution before reaching the snapshot lookup. + const s = await Sandbox.create({ + name: `test-snapshot-404-${Date.now()}`, + identifier: `test_snapshot_404_${Date.now()}`, + }); + try { + await expect( + s.getSnapshot("non-existent-snapshot-id-12345"), + ).rejects.toThrow(); + } finally { + await s.destroy().catch(() => undefined); + } + }, 120_000); + }); }); diff --git a/tests/filesystem.test.ts b/tests/filesystem.test.ts index 3de0118..2f06c99 100644 --- a/tests/filesystem.test.ts +++ b/tests/filesystem.test.ts @@ -204,6 +204,32 @@ describe("FileSystem", () => { await fs.promises.unlink(localPath); } }); + + it("should download a directory as a non-empty buffer (tar.gz from API)", async () => { + const dirName = `download_dir_${Date.now()}`; + await sandbox.fs.createFolder(dirName); + await sandbox.fs.uploadFile( + Buffer.from("inside-dir-1"), + `${dirName}/file1.txt`, + ); + await sandbox.fs.uploadFile( + Buffer.from("inside-dir-2"), + `${dirName}/file2.txt`, + ); + + try { + const content = await sandbox.fs.downloadFile(dirName); + expect(Buffer.isBuffer(content)).toBe(true); + expect(content.length).toBeGreaterThan(0); + // API serves directories as gzip - magic bytes 0x1f 0x8b. Soft check. + if (content.length >= 2) { + expect(content[0]).toBe(0x1f); + expect(content[1]).toBe(0x8b); + } + } finally { + await sandbox.fs.deleteFile(dirName).catch(() => undefined); + } + }, 60_000); }); describe("deleteFile()", () => { diff --git a/tests/global-teardown.ts b/tests/global-teardown.ts index cf1a778..52624a4 100644 --- a/tests/global-teardown.ts +++ b/tests/global-teardown.ts @@ -7,9 +7,12 @@ async function cleanupTestSandboxes() { const testSandboxes = sandboxes.filter( (s) => s.name?.startsWith("Sandbox 202") || - s.name?.startsWith("test-sandbox-") || + s.name?.startsWith("test-") || s.name?.startsWith("command-test-") || - s.name?.startsWith("filesystem-test-"), + s.name?.startsWith("filesystem-test-") || + s.name?.startsWith("fbc-probe-") || + s.name?.startsWith("resources-probe-") || + s.name?.startsWith("update-probe-"), ); if (testSandboxes.length === 0) { diff --git a/tests/sandbox.test.ts b/tests/sandbox.test.ts index a35017b..218cd55 100644 --- a/tests/sandbox.test.ts +++ b/tests/sandbox.test.ts @@ -260,6 +260,21 @@ describe("Sandbox", () => { expect(command.data.id).toBeDefined(); expect(command.data.status).toBeDefined(); }); + + it("should list commands history including previously executed ones", async () => { + const marker = `echo list-commands-marker-${Date.now()}`; + const command = await sandbox.runCommand({ + command: marker, + stdout: null, + stderr: null, + }); + await command.wait(); + + const history = await sandbox.listCommands(); + expect(Array.isArray(history)).toBe(true); + expect(history.length).toBeGreaterThan(0); + expect(history.some((c) => c.data.command === marker)).toBe(true); + }); }); describe("filesystem", () => { @@ -427,5 +442,199 @@ describe("Sandbox", () => { await sandbox.refresh(); expect(sandbox.data.status).toBe("RUNNING"); }); + + it("start() on a running sandbox should be a no-op", async () => { + await sandbox.refresh(); + expect(sandbox.data.status).toBe("RUNNING"); + + await expect(sandbox.start()).resolves.toBeUndefined(); + expect(sandbox.data.status).toBe("RUNNING"); + }); + + it("stop() on an already stopped sandbox should be a no-op", async () => { + await sandbox.stop(); + await sandbox.waitUntilStopped(); + expect(sandbox.data.status).toBe("STOPPED"); + + await expect(sandbox.stop()).resolves.toBeUndefined(); + expect(sandbox.data.status).toBe("STOPPED"); + + // leave the sandbox running for any later tests / cleanup + await sandbox.start(); + await sandbox.waitUntilRunning(); + }); }); + + describe("update", () => { + beforeAll(async () => { + await sandbox.refresh(); + if (sandbox.data.status !== "RUNNING") { + await sandbox.start(); + await sandbox.waitUntilRunning(); + } + }); + + it("should update timeout and tags in place", async () => { + await sandbox.update({ timeout: 1200, tags: ["updated"] }); + + expect(sandbox.data.timeout).toBe(1200); + expect(sandbox.data.tags).toEqual(expect.arrayContaining(["updated"])); + }); + }); + + describe("snapshots", () => { + const createdSnapshotIds: string[] = []; + + beforeAll(async () => { + await sandbox.refresh(); + if (sandbox.data.status !== "RUNNING") { + await sandbox.start(); + await sandbox.waitUntilRunning(); + } + }); + + afterAll(async () => { + await Promise.all( + createdSnapshotIds.map((id) => + sandbox.deleteSnapshot(id).catch(() => undefined), + ), + ); + }, 30_000); + + it("should create, list, get and delete a snapshot", async () => { + const snapshot = await sandbox.createSnapshot({ + name: `test-snap-${Date.now()}`, + }); + const snapshotId = snapshot.id; + if (!snapshotId) throw new Error("snapshot.id was not returned"); + createdSnapshotIds.push(snapshotId); + + const list = await sandbox.listSnapshots(); + expect(list.some((s) => s.id === snapshotId)).toBe(true); + + const fetched = await sandbox.getSnapshot(snapshotId); + expect(fetched.id).toBe(snapshotId); + + await sandbox.deleteSnapshot(snapshotId); + createdSnapshotIds.splice(createdSnapshotIds.indexOf(snapshotId), 1); + + const afterDelete = await sandbox.listSnapshots(); + expect(afterDelete.some((s) => s.id === snapshotId)).toBe(false); + }, 120_000); + }); +}); + +describe("Sandbox.createFromSnapshot", () => { + let baseSandbox: Sandbox; + let restoredSandbox: Sandbox | undefined; + let snapshotId: string | undefined; + let extraSnapshotId: string | undefined; + const markerFilename = `snapshot_marker_${Date.now()}.txt`; + const markerContent = `restored at ${Date.now()}`; + + beforeAll(async () => { + baseSandbox = await Sandbox.create({ + name: `test-snapshot-base-${Date.now()}`, + identifier: `test_snapshot_base_${Date.now()}`, + }); + await baseSandbox.fs.uploadFile(Buffer.from(markerContent), markerFilename); + const snapshot = await baseSandbox.createSnapshot({ + name: `test-base-snap-${Date.now()}`, + }); + snapshotId = snapshot.id; + if (snapshotId) { + await baseSandbox.waitForSnapshotReady(snapshotId); + } + }, 360_000); + + afterAll(async () => { + await restoredSandbox?.destroy().catch(() => undefined); + if (extraSnapshotId) { + await Sandbox.deleteSnapshot(extraSnapshotId).catch(() => undefined); + } + if (snapshotId) { + await baseSandbox?.deleteSnapshot(snapshotId).catch(() => undefined); + } + await baseSandbox?.destroy().catch(() => undefined); + }, 60_000); + + it("should create a sandbox from a snapshot with the given name and identifier", async () => { + if (!snapshotId) throw new Error("base snapshot was not created"); + const restoredName = `test-snapshot-restored-${Date.now()}`; + const restoredIdentifier = `test_snapshot_restored_${Date.now()}`; + restoredSandbox = await Sandbox.createFromSnapshot(snapshotId, { + name: restoredName, + identifier: restoredIdentifier, + }); + + expect(restoredSandbox.data.id).toBeDefined(); + expect(restoredSandbox.data.status).toBe("RUNNING"); + expect(restoredSandbox.data.name).toBe(restoredName); + expect(restoredSandbox.data.identifier).toBe(restoredIdentifier); + }, 120_000); + + it("restored sandbox should contain the file from the snapshot", async () => { + if (!restoredSandbox) { + throw new Error("restoredSandbox was not initialised by previous test"); + } + + const downloaded = await restoredSandbox.fs.downloadFile(markerFilename); + expect(downloaded.toString()).toBe(markerContent); + }, 60_000); + + it("Sandbox.listSnapshots should include the snapshot we created", async () => { + if (!snapshotId) throw new Error("base snapshot was not created"); + + const all = await Sandbox.listSnapshots(); + expect(Array.isArray(all)).toBe(true); + expect(all.some((s) => s.id === snapshotId)).toBe(true); + }, 60_000); + + it("Sandbox.deleteSnapshot should remove a snapshot at the project level", async () => { + if (!baseSandbox) throw new Error("baseSandbox missing"); + const extra = await baseSandbox.createSnapshot({ + name: `test-delete-static-${Date.now()}`, + }); + extraSnapshotId = extra.id; + await extra.waitUntilReady(); + + await Sandbox.deleteSnapshot(extra.id); + extraSnapshotId = undefined; + + const all = await Sandbox.listSnapshots(); + expect(all.some((s) => s.id === extra.id)).toBe(false); + }, 180_000); +}); + +describe("Sandbox.clone", () => { + let source: Sandbox; + let clone: Sandbox | undefined; + + beforeAll(async () => { + source = await Sandbox.create({ + name: `test-clone-source-${Date.now()}`, + identifier: `test_clone_source_${Date.now()}`, + }); + }, 120_000); + + afterAll(async () => { + await clone?.destroy().catch(() => undefined); + await source?.destroy().catch(() => undefined); + }, 60_000); + + it("should clone an existing sandbox under a new name and identifier", async () => { + const cloneName = `test-clone-target-${Date.now()}`; + const cloneIdentifier = `test_clone_target_${Date.now()}`; + + clone = await Sandbox.clone(source.initializedId, { + name: cloneName, + identifier: cloneIdentifier, + }); + + expect(clone.data.id).toBeDefined(); + expect(clone.data.id).not.toBe(source.data.id); + expect(clone.data.status).toBe("RUNNING"); + expect(clone.data.name).toBe(cloneName); + expect(clone.data.identifier).toBe(cloneIdentifier); + }, 120_000); }); diff --git a/tests/update-behavior.test.ts b/tests/update-behavior.test.ts new file mode 100644 index 0000000..28bda3b --- /dev/null +++ b/tests/update-behavior.test.ts @@ -0,0 +1,204 @@ +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { Sandbox } from "@/entity/sandbox"; + +/** + * Contract tests for `sandbox.update()` - assert how the backend reacts to + * PATCH on each field of UpdateSandboxRequestWritable. Documents and pins: + * - "soft" fields apply in place: sandbox stays RUNNING + setup SUCCESS + * - `first_boot_commands` transitions setup to STALE (recreate required) + * - `resources` change applies in place (no restart/recreate) + * + * If a backend change breaks these contracts, these tests fail loudly. + * + * Run with `pnpm test tests/update-behavior.test.ts` once `.env` has live + * BUDDY_TOKEN / BUDDY_WORKSPACE / BUDDY_PROJECT. + */ + +const logFieldEffect = ( + label: string, + before: { setup: string | undefined; status: string | undefined }, + after: { setup: string | undefined; status: string | undefined }, +) => { + console.log( + `[update:${label}] setup_status ${before.setup} -> ${after.setup} | status ${before.status} -> ${after.status}`, + ); +}; + +describe("Sandbox.update - soft fields (shared sandbox)", () => { + let sandbox: Sandbox; + + beforeAll(async () => { + sandbox = await Sandbox.create({ + name: `update-probe-${Date.now()}`, + identifier: `update_probe_${Date.now()}`, + timeout: 600, + tags: ["initial"], + apps: ["echo initial-app"], + first_boot_commands: "echo initial-first-boot", + }); + }, 120_000); + + afterAll(async () => { + await sandbox?.destroy().catch(() => undefined); + }, 60_000); + + const assertSoftUpdate = (label: string) => { + const before = { + setup: sandbox.data.setup_status, + status: sandbox.data.status, + }; + return { + before, + finish: () => { + const after = { + setup: sandbox.data.setup_status, + status: sandbox.data.status, + }; + logFieldEffect(label, before, after); + expect(after.setup).toBe("SUCCESS"); + expect(after.status).toBe("RUNNING"); + }, + }; + }; + + it("updates timeout in place without leaving setup STALE", async () => { + const probe = assertSoftUpdate("timeout"); + await sandbox.update({ timeout: 1800 }); + expect(sandbox.data.timeout).toBe(1800); + probe.finish(); + }); + + it("updates tags in place without leaving setup STALE", async () => { + const probe = assertSoftUpdate("tags"); + await sandbox.update({ tags: ["probed", "updated"] }); + expect(sandbox.data.tags).toEqual( + expect.arrayContaining(["probed", "updated"]), + ); + probe.finish(); + }); + + it("updates name in place without leaving setup STALE", async () => { + const newName = `renamed-${Date.now()}`; + const probe = assertSoftUpdate("name"); + await sandbox.update({ name: newName }); + expect(sandbox.data.name).toBe(newName); + probe.finish(); + }); + + it("updates variables in place without leaving setup STALE", async () => { + const probe = assertSoftUpdate("variables"); + await sandbox.update({ + variables: [{ key: "PROBE_VAR", value: "hello", type: "VAR" }], + }); + expect( + sandbox.data.variables?.some( + (v) => v.key === "PROBE_VAR" && v.value === "hello", + ), + ).toBe(true); + probe.finish(); + }); + + it("updates apps in place without leaving setup STALE", async () => { + const probe = assertSoftUpdate("apps"); + await sandbox.update({ apps: [{ command: "echo updated-app" }] }); + expect( + sandbox.data.apps?.some((a) => a.command === "echo updated-app"), + ).toBe(true); + probe.finish(); + }); + + it("updates app_dir in place without leaving setup STALE", async () => { + const probe = assertSoftUpdate("app_dir"); + await sandbox.update({ app_dir: "/workspace/probed" }); + expect(sandbox.data.app_dir).toBe("/workspace/probed"); + probe.finish(); + }); + + it("updates endpoints in place without leaving setup STALE", async () => { + const probe = assertSoftUpdate("endpoints"); + await sandbox.update({ + endpoints: [ + { + name: "probed-endpoint", + endpoint: "127.0.0.1:3000", + type: "HTTP", + region: "US", + }, + ], + }); + expect( + sandbox.data.endpoints?.some((e) => e.name === "probed-endpoint"), + ).toBe(true); + probe.finish(); + }); +}); + +describe("Sandbox.update - first_boot_commands (isolated sandbox)", () => { + let sandbox: Sandbox; + + beforeAll(async () => { + sandbox = await Sandbox.create({ + name: `fbc-probe-${Date.now()}`, + identifier: `fbc_probe_${Date.now()}`, + first_boot_commands: "echo initial-first-boot", + }); + }, 120_000); + + afterAll(async () => { + await sandbox?.destroy().catch(() => undefined); + }, 60_000); + + it("changing first_boot_commands transitions setup_status to STALE", async () => { + const before = { + setup: sandbox.data.setup_status, + status: sandbox.data.status, + }; + await sandbox.update({ + first_boot_commands: "echo CHANGED-first-boot", + }); + await sandbox.refresh(); + const after = { + setup: sandbox.data.setup_status, + status: sandbox.data.status, + }; + logFieldEffect("first_boot_commands", before, after); + + expect(sandbox.data.first_boot_commands).toBe("echo CHANGED-first-boot"); + expect(sandbox.data.setup_status).toBe("STALE"); + expect(sandbox.data.status).toBe("RUNNING"); + }); +}); + +describe("Sandbox.update - resources (isolated sandbox)", () => { + let sandbox: Sandbox; + + beforeAll(async () => { + sandbox = await Sandbox.create({ + name: `resources-probe-${Date.now()}`, + identifier: `resources_probe_${Date.now()}`, + resources: "1x2", + }); + }, 120_000); + + afterAll(async () => { + await sandbox?.destroy().catch(() => undefined); + }, 60_000); + + it("changing resources applies in place without leaving setup STALE", async () => { + const before = { + setup: sandbox.data.setup_status, + status: sandbox.data.status, + }; + await sandbox.update({ resources: "2x4" }); + await sandbox.refresh(); + const after = { + setup: sandbox.data.setup_status, + status: sandbox.data.status, + }; + logFieldEffect("resources", before, after); + + expect(sandbox.data.resources).toBe("2x4"); + expect(sandbox.data.setup_status).toBe("SUCCESS"); + expect(sandbox.data.status).toBe("RUNNING"); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts index d006d28..058d6a1 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -10,6 +10,7 @@ export default defineConfig({ fileParallelism: true, maxWorkers: 8, globalSetup: ["./tests/global-teardown.ts"], + reporters: ["default", "@buddy-works/unit-tests/vitest"], }, resolve: { alias: { From dfaf01ad611a345e16dc3c3554b989e3ed0661d4 Mon Sep 17 00:00:00 2001 From: Buddy CI Date: Thu, 21 May 2026 10:44:17 +0000 Subject: [PATCH 17/23] chore(release): bump version to 0.1.6-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 375b67c..f2a8a65 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@buddy-works/sandbox-sdk", - "version": "0.1.6-rc.0", + "version": "0.1.6-rc.1", "type": "module", "description": "TypeScript SDK for managing sandboxes through the Buddy API", "main": "./dist/index.mjs", From d998a1b8341cba0511b86cb3e7601d3aea77a972 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Ga=C5=84czarczyk?= Date: Tue, 2 Jun 2026 09:12:36 +0200 Subject: [PATCH 18/23] 0.1.7 - Spec fix (#13) * fix: updated spec * fail boot_logs * refactor: avoid redundant boot_logs slice in setup failure error --- openapi-ts.config.ts | 22 +- src/api/openapi/types.gen.ts | 396 ++++++++++++++++++---------------- src/api/openapi/zod.gen.ts | 201 +++++++++-------- src/entity/sandbox.ts | 9 +- tests/update-behavior.test.ts | 69 +----- 5 files changed, 345 insertions(+), 352 deletions(-) diff --git a/openapi-ts.config.ts b/openapi-ts.config.ts index 970bacc..92adb7a 100644 --- a/openapi-ts.config.ts +++ b/openapi-ts.config.ts @@ -6,24 +6,8 @@ if (!schemaUrl) { throw new Error("SCHEMA_URL environment variable is required"); } -// Spec has dangling `$ref: '#/components/schemas/String'` on the Crawl/VisualTest -// getToken endpoints; inject a placeholder so hey-api can resolve them. -const fetchPatchedSpec = async () => { - const response = await fetch(schemaUrl); - if (!response.ok) { - throw new Error(`Failed to fetch ${schemaUrl}: ${response.status}`); - } - const spec = (await response.json()) as { - components?: { schemas?: Record }; - }; - spec.components ??= {}; - spec.components.schemas ??= {}; - spec.components.schemas["String"] ??= { type: "string" }; - return spec; -}; - -export default defineConfig(async () => ({ - input: await fetchPatchedSpec(), +export default defineConfig({ + input: schemaUrl, output: { path: "src/api/openapi", postProcess: [ @@ -58,4 +42,4 @@ export default defineConfig(async () => ({ exportFromIndex: false, }, ], -})); +}); diff --git a/src/api/openapi/types.gen.ts b/src/api/openapi/types.gen.ts index 84ce5d5..ed96486 100644 --- a/src/api/openapi/types.gen.ts +++ b/src/api/openapi/types.gen.ts @@ -581,6 +581,191 @@ export type PipelinePropertyView = { value?: string; }; +/** + * MSSQL authentication credentials + */ +export type MssqlAuthView = { + /** + * Authentication method. Default: `PASSWORD` + */ + method?: "PASSWORD"; + /** + * The MSSQL username + */ + username: string; + /** + * The MSSQL password + */ + password: string; +}; + +/** + * MongoDB authentication credentials + */ +export type MongoAuthView = { + /** + * Authentication method. Default: `PASSWORD` + */ + method?: "PASSWORD"; + /** + * The MongoDB username + */ + username: string; + /** + * The MongoDB password + */ + password: string; +}; + +/** + * PostgreSQL authentication credentials + */ +export type PostgresqlAuthView = { + /** + * Authentication method. Default: `PASSWORD` + */ + method?: "PASSWORD"; + /** + * The PostgreSQL username + */ + username: string; + /** + * The PostgreSQL password + */ + password: string; +}; + +/** + * MySQL authentication credentials + */ +export type MysqlAuthView = { + /** + * Authentication method. Default: `PASSWORD` + */ + method?: "PASSWORD"; + /** + * The MySQL username + */ + username: string; + /** + * The MySQL password + */ + password: string; +}; + +/** + * Define proxy servers' authentication method using the following parameters + */ +export type SshAuthView = { + /** + * Authentication method + */ + method: + | "PASSWORD" + | "SSH_KEY" + | "ASSETS_KEY" + | "PROXY_CREDENTIALS" + | "PROXY_KEY"; + /** + * The username required to connect to the server + */ + username?: string; + /** + * The password required to connect to the server. Required for `PASSWORD` method + */ + password?: string; + /** + * Name of the variable containing the private key. Required for `ASSETS_KEY` method + */ + asset?: string; + /** + * Passphrase for the SSH key + */ + passphrase?: string; + /** + * The private SSH key. Required when method is `SSH_KEY` + */ + key?: string; + /** + * Path to the key on proxy server. Required for method `PROXY_KEY` + */ + key_path?: string; +}; + +/** + * Kubernetes cluster authentication method + */ +export type K8sAuthView = { + /** + * Authentication method + */ + method: "PASS" | "CERT" | "TOKEN"; + /** + * Username to the Kubernetes cluster. Required if the `auth.method` is `BASIC` + */ + username?: string; + /** + * Password to the Kubernetes cluster. Required if the `auth.method` is `BASIC` + */ + password?: string; + /** + * Kuberenetes certificate authority. Required if the `auth.method` is `CERT` + */ + certificate_authority?: string; + /** + * Kuberenetes client certificate. Required if the `auth.method` is `CERT` + */ + client_certificate?: string; + /** + * Kuberenetes client key. Required if the `auth.method` is `CERT` + */ + client_key?: string; + /** + * Token for the Kubernetes cluster. Required if the `auth.method` is `TOKEN` + */ + token?: string; +}; + +/** + * Authentication details + */ +export type GitAuthView = { + /** + * The authentication for Git + */ + method: "HTTP" | "SSH_KEY" | "ASSETS_KEY" | "CURRENT"; + /** + * Username required to connect to the Git repository. Required when method is `HTTP` + */ + username?: string; + /** + * Password required to connect to the Git repository. Required when method is `HTTP` + */ + password?: string; + /** + * Name of the variable containing the private key. Required when method is `ASSETS_KEY` + */ + asset?: string; + /** + * The private SSH key. Required when method is `SSH_KEY` + */ + key?: string; +}; + +/** + * Authentication details + */ +export type FtpAuthView = { + /** + * The username required to connect to the server + */ + username: string; + /** + * The password required to connect to the server + */ + password: string; +}; + /** * Defines how the target can be used (as deployment target, proxy, or both) */ @@ -1056,191 +1241,6 @@ export type ShortProjectView = { create_date?: Date; }; -/** - * MSSQL authentication credentials - */ -export type MssqlAuthView = { - /** - * Authentication method. Default: `PASSWORD` - */ - method?: "PASSWORD"; - /** - * The MSSQL username - */ - username: string; - /** - * The MSSQL password - */ - password: string; -}; - -/** - * MongoDB authentication credentials - */ -export type MongoAuthView = { - /** - * Authentication method. Default: `PASSWORD` - */ - method?: "PASSWORD"; - /** - * The MongoDB username - */ - username: string; - /** - * The MongoDB password - */ - password: string; -}; - -/** - * PostgreSQL authentication credentials - */ -export type PostgresqlAuthView = { - /** - * Authentication method. Default: `PASSWORD` - */ - method?: "PASSWORD"; - /** - * The PostgreSQL username - */ - username: string; - /** - * The PostgreSQL password - */ - password: string; -}; - -/** - * MySQL authentication credentials - */ -export type MysqlAuthView = { - /** - * Authentication method. Default: `PASSWORD` - */ - method?: "PASSWORD"; - /** - * The MySQL username - */ - username: string; - /** - * The MySQL password - */ - password: string; -}; - -/** - * Define proxy servers' authentication method using the following parameters - */ -export type SshAuthView = { - /** - * Authentication method - */ - method: - | "PASSWORD" - | "SSH_KEY" - | "ASSETS_KEY" - | "PROXY_CREDENTIALS" - | "PROXY_KEY"; - /** - * The username required to connect to the server - */ - username?: string; - /** - * The password required to connect to the server. Required for `PASSWORD` method - */ - password?: string; - /** - * Name of the variable containing the private key. Required for `ASSETS_KEY` method - */ - asset?: string; - /** - * Passphrase for the SSH key - */ - passphrase?: string; - /** - * The private SSH key. Required when method is `SSH_KEY` - */ - key?: string; - /** - * Path to the key on proxy server. Required for method `PROXY_KEY` - */ - key_path?: string; -}; - -/** - * Kubernetes cluster authentication method - */ -export type K8sAuthView = { - /** - * Authentication method - */ - method: "PASS" | "CERT" | "TOKEN"; - /** - * Username to the Kubernetes cluster. Required if the `auth.method` is `BASIC` - */ - username?: string; - /** - * Password to the Kubernetes cluster. Required if the `auth.method` is `BASIC` - */ - password?: string; - /** - * Kuberenetes certificate authority. Required if the `auth.method` is `CERT` - */ - certificate_authority?: string; - /** - * Kuberenetes client certificate. Required if the `auth.method` is `CERT` - */ - client_certificate?: string; - /** - * Kuberenetes client key. Required if the `auth.method` is `CERT` - */ - client_key?: string; - /** - * Token for the Kubernetes cluster. Required if the `auth.method` is `TOKEN` - */ - token?: string; -}; - -/** - * Authentication details - */ -export type GitAuthView = { - /** - * The authentication for Git - */ - method: "HTTP" | "SSH_KEY" | "ASSETS_KEY" | "CURRENT"; - /** - * Username required to connect to the Git repository. Required when method is `HTTP` - */ - username?: string; - /** - * Password required to connect to the Git repository. Required when method is `HTTP` - */ - password?: string; - /** - * Name of the variable containing the private key. Required when method is `ASSETS_KEY` - */ - asset?: string; - /** - * The private SSH key. Required when method is `SSH_KEY` - */ - key?: string; -}; - -/** - * Authentication details - */ -export type FtpAuthView = { - /** - * The username required to connect to the server - */ - username: string; - /** - * The password required to connect to the server - */ - password: string; -}; - /** * The integration to use for authentication */ @@ -2249,6 +2249,14 @@ export type TunnelView = { * The url of the tunnel */ endpoint_url?: string; + /** + * Whether the tunnel is connected to the edge (tunnel is established) + */ + active?: boolean; + /** + * Latency to the target in milliseconds. When >= 0 the target is reachable + */ + target_latency?: number; }; export type SandboxFetchView = { @@ -2674,6 +2682,9 @@ export type CreateFromSnapshotRequest = { variables?: Array; }; +/** + * The list of variables you can use the action + */ export type EnvironmentVariableView = { /** * The ID of the variable @@ -4071,6 +4082,14 @@ export type TunnelViewWritable = { * The url of the tunnel */ endpoint_url?: string; + /** + * Whether the tunnel is connected to the edge (tunnel is established) + */ + active?: boolean; + /** + * Latency to the target in milliseconds. When >= 0 the target is reachable + */ + target_latency?: number; }; export type SandboxesViewWritable = { @@ -5328,7 +5347,12 @@ export type UploadSandboxFileData = { */ path: string; }; - query?: never; + query?: { + /** + * Owner of the uploaded file inside the sandbox. Defaults to `buddy`. Any user that exists in the sandbox is accepted. + */ + userName?: string; + }; url: "/workspaces/{workspace_domain}/sandboxes/{sandbox_id}/content/upload/{path}"; }; diff --git a/src/api/openapi/zod.gen.ts b/src/api/openapi/zod.gen.ts index a4cdc95..c095140 100644 --- a/src/api/openapi/zod.gen.ts +++ b/src/api/openapi/zod.gen.ts @@ -336,6 +336,93 @@ export const zPipelinePropertyView = z.object({ value: z.string().optional(), }); +/** + * MSSQL authentication credentials + */ +export const zMssqlAuthView = z.object({ + method: z.enum(["PASSWORD"]).optional(), + username: z.string(), + password: z.string(), +}); + +/** + * MongoDB authentication credentials + */ +export const zMongoAuthView = z.object({ + method: z.enum(["PASSWORD"]).optional(), + username: z.string(), + password: z.string(), +}); + +/** + * PostgreSQL authentication credentials + */ +export const zPostgresqlAuthView = z.object({ + method: z.enum(["PASSWORD"]).optional(), + username: z.string(), + password: z.string(), +}); + +/** + * MySQL authentication credentials + */ +export const zMysqlAuthView = z.object({ + method: z.enum(["PASSWORD"]).optional(), + username: z.string(), + password: z.string(), +}); + +/** + * Define proxy servers' authentication method using the following parameters + */ +export const zSshAuthView = z.object({ + method: z.enum([ + "PASSWORD", + "SSH_KEY", + "ASSETS_KEY", + "PROXY_CREDENTIALS", + "PROXY_KEY", + ]), + username: z.string().optional(), + password: z.string().optional(), + asset: z.string().optional(), + passphrase: z.string().optional(), + key: z.string().optional(), + key_path: z.string().optional(), +}); + +/** + * Kubernetes cluster authentication method + */ +export const zK8sAuthView = z.object({ + method: z.enum(["PASS", "CERT", "TOKEN"]), + username: z.string().optional(), + password: z.string().optional(), + certificate_authority: z.string().optional(), + client_certificate: z.string().optional(), + client_key: z.string().optional(), + token: z.string().optional(), +}); + +/** + * Authentication details + */ +export const zGitAuthView = z.object({ + method: z.enum(["HTTP", "SSH_KEY", "ASSETS_KEY", "CURRENT"]), + username: z.string().optional(), + password: z.string().optional(), + asset: z.string().optional(), + key: z.string().optional(), +}); + +/** + * Authentication details + */ +export const zFtpAuthView = z.object({ + username: z.string(), + password: z.string(), +}); + /** * Defines how the target can be used (as deployment target, proxy, or both) */ @@ -457,93 +544,6 @@ export const zShortProjectView = z.object({ create_date: z.iso.datetime().optional(), }); -/** - * MSSQL authentication credentials - */ -export const zMssqlAuthView = z.object({ - method: z.enum(["PASSWORD"]).optional(), - username: z.string(), - password: z.string(), -}); - -/** - * MongoDB authentication credentials - */ -export const zMongoAuthView = z.object({ - method: z.enum(["PASSWORD"]).optional(), - username: z.string(), - password: z.string(), -}); - -/** - * PostgreSQL authentication credentials - */ -export const zPostgresqlAuthView = z.object({ - method: z.enum(["PASSWORD"]).optional(), - username: z.string(), - password: z.string(), -}); - -/** - * MySQL authentication credentials - */ -export const zMysqlAuthView = z.object({ - method: z.enum(["PASSWORD"]).optional(), - username: z.string(), - password: z.string(), -}); - -/** - * Define proxy servers' authentication method using the following parameters - */ -export const zSshAuthView = z.object({ - method: z.enum([ - "PASSWORD", - "SSH_KEY", - "ASSETS_KEY", - "PROXY_CREDENTIALS", - "PROXY_KEY", - ]), - username: z.string().optional(), - password: z.string().optional(), - asset: z.string().optional(), - passphrase: z.string().optional(), - key: z.string().optional(), - key_path: z.string().optional(), -}); - -/** - * Kubernetes cluster authentication method - */ -export const zK8sAuthView = z.object({ - method: z.enum(["PASS", "CERT", "TOKEN"]), - username: z.string().optional(), - password: z.string().optional(), - certificate_authority: z.string().optional(), - client_certificate: z.string().optional(), - client_key: z.string().optional(), - token: z.string().optional(), -}); - -/** - * Authentication details - */ -export const zGitAuthView = z.object({ - method: z.enum(["HTTP", "SSH_KEY", "ASSETS_KEY", "CURRENT"]), - username: z.string().optional(), - password: z.string().optional(), - asset: z.string().optional(), - key: z.string().optional(), -}); - -/** - * Authentication details - */ -export const zFtpAuthView = z.object({ - username: z.string(), - password: z.string(), -}); - export const zIdsView = z.object({ url: z.string().readonly().optional(), html_url: z.string().readonly().optional(), @@ -952,6 +952,16 @@ export const zTunnelView = z.object({ http: zHttpSettingsView.optional(), tls: zTlsSettingsView.optional(), endpoint_url: z.string().optional(), + active: z.boolean().optional(), + target_latency: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), }); export const zSandboxFetchView = z.object({ @@ -1180,6 +1190,9 @@ export const zCreateNewSandboxRequest = z.object({ permissions: zPermissionsView.optional(), }); +/** + * The list of variables you can use the action + */ export const zEnvironmentVariableView = z.object({ id: z .int() @@ -1721,6 +1734,16 @@ export const zTunnelViewWritable = z.object({ http: zHttpSettingsViewWritable.optional(), tls: zTlsSettingsViewWritable.optional(), endpoint_url: z.string().optional(), + active: z.boolean().optional(), + target_latency: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), }); export const zUpdateSandboxRequestWritable = z.object({ @@ -2824,6 +2847,10 @@ export const zUploadSandboxFilePath = z.object({ path: z.string().regex(/.*/), }); +export const zUploadSandboxFileQuery = z.object({ + userName: z.string().optional().default("buddy"), +}); + /** * File uploaded successfully */ diff --git a/src/entity/sandbox.ts b/src/entity/sandbox.ts index 6f87097..220e638 100644 --- a/src/entity/sandbox.ts +++ b/src/entity/sandbox.ts @@ -612,8 +612,15 @@ export class Sandbox { } if (this.data.setup_status === "FAILED") { + const logs = this.data.boot_logs ?? []; + const recent = logs.slice(-20); + const tail = recent.join("\n"); throw new Error( - `Sandbox ${sandboxId} setup failed. Status: ${this.data.setup_status}`, + `Sandbox ${sandboxId} setup failed.${ + tail + ? `\nBoot logs (last ${recent.length} of ${logs.length} lines):\n${tail}` + : " No boot logs were returned." + }`, ); } diff --git a/tests/update-behavior.test.ts b/tests/update-behavior.test.ts index 28bda3b..4bae8eb 100644 --- a/tests/update-behavior.test.ts +++ b/tests/update-behavior.test.ts @@ -14,16 +14,6 @@ import { Sandbox } from "@/entity/sandbox"; * BUDDY_TOKEN / BUDDY_WORKSPACE / BUDDY_PROJECT. */ -const logFieldEffect = ( - label: string, - before: { setup: string | undefined; status: string | undefined }, - after: { setup: string | undefined; status: string | undefined }, -) => { - console.log( - `[update:${label}] setup_status ${before.setup} -> ${after.setup} | status ${before.status} -> ${after.status}`, - ); -}; - describe("Sandbox.update - soft fields (shared sandbox)", () => { let sandbox: Sandbox; @@ -42,51 +32,33 @@ describe("Sandbox.update - soft fields (shared sandbox)", () => { await sandbox?.destroy().catch(() => undefined); }, 60_000); - const assertSoftUpdate = (label: string) => { - const before = { - setup: sandbox.data.setup_status, - status: sandbox.data.status, - }; - return { - before, - finish: () => { - const after = { - setup: sandbox.data.setup_status, - status: sandbox.data.status, - }; - logFieldEffect(label, before, after); - expect(after.setup).toBe("SUCCESS"); - expect(after.status).toBe("RUNNING"); - }, - }; + const expectSoftUpdate = () => { + expect(sandbox.data.setup_status).toBe("SUCCESS"); + expect(sandbox.data.status).toBe("RUNNING"); }; it("updates timeout in place without leaving setup STALE", async () => { - const probe = assertSoftUpdate("timeout"); await sandbox.update({ timeout: 1800 }); expect(sandbox.data.timeout).toBe(1800); - probe.finish(); + expectSoftUpdate(); }); it("updates tags in place without leaving setup STALE", async () => { - const probe = assertSoftUpdate("tags"); await sandbox.update({ tags: ["probed", "updated"] }); expect(sandbox.data.tags).toEqual( expect.arrayContaining(["probed", "updated"]), ); - probe.finish(); + expectSoftUpdate(); }); it("updates name in place without leaving setup STALE", async () => { const newName = `renamed-${Date.now()}`; - const probe = assertSoftUpdate("name"); await sandbox.update({ name: newName }); expect(sandbox.data.name).toBe(newName); - probe.finish(); + expectSoftUpdate(); }); it("updates variables in place without leaving setup STALE", async () => { - const probe = assertSoftUpdate("variables"); await sandbox.update({ variables: [{ key: "PROBE_VAR", value: "hello", type: "VAR" }], }); @@ -95,27 +67,24 @@ describe("Sandbox.update - soft fields (shared sandbox)", () => { (v) => v.key === "PROBE_VAR" && v.value === "hello", ), ).toBe(true); - probe.finish(); + expectSoftUpdate(); }); it("updates apps in place without leaving setup STALE", async () => { - const probe = assertSoftUpdate("apps"); await sandbox.update({ apps: [{ command: "echo updated-app" }] }); expect( sandbox.data.apps?.some((a) => a.command === "echo updated-app"), ).toBe(true); - probe.finish(); + expectSoftUpdate(); }); it("updates app_dir in place without leaving setup STALE", async () => { - const probe = assertSoftUpdate("app_dir"); await sandbox.update({ app_dir: "/workspace/probed" }); expect(sandbox.data.app_dir).toBe("/workspace/probed"); - probe.finish(); + expectSoftUpdate(); }); it("updates endpoints in place without leaving setup STALE", async () => { - const probe = assertSoftUpdate("endpoints"); await sandbox.update({ endpoints: [ { @@ -129,7 +98,7 @@ describe("Sandbox.update - soft fields (shared sandbox)", () => { expect( sandbox.data.endpoints?.some((e) => e.name === "probed-endpoint"), ).toBe(true); - probe.finish(); + expectSoftUpdate(); }); }); @@ -149,19 +118,10 @@ describe("Sandbox.update - first_boot_commands (isolated sandbox)", () => { }, 60_000); it("changing first_boot_commands transitions setup_status to STALE", async () => { - const before = { - setup: sandbox.data.setup_status, - status: sandbox.data.status, - }; await sandbox.update({ first_boot_commands: "echo CHANGED-first-boot", }); await sandbox.refresh(); - const after = { - setup: sandbox.data.setup_status, - status: sandbox.data.status, - }; - logFieldEffect("first_boot_commands", before, after); expect(sandbox.data.first_boot_commands).toBe("echo CHANGED-first-boot"); expect(sandbox.data.setup_status).toBe("STALE"); @@ -185,17 +145,8 @@ describe("Sandbox.update - resources (isolated sandbox)", () => { }, 60_000); it("changing resources applies in place without leaving setup STALE", async () => { - const before = { - setup: sandbox.data.setup_status, - status: sandbox.data.status, - }; await sandbox.update({ resources: "2x4" }); await sandbox.refresh(); - const after = { - setup: sandbox.data.setup_status, - status: sandbox.data.status, - }; - logFieldEffect("resources", before, after); expect(sandbox.data.resources).toBe("2x4"); expect(sandbox.data.setup_status).toBe("SUCCESS"); From 7886e9bd0a34ed6a0fb543623e33cbe513090cc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Ga=C5=84czarczyk?= Date: Wed, 5 Aug 2026 08:56:09 +0200 Subject: [PATCH 19/23] fix: Asia region (#16) --- .env.example | 2 +- README.md | 4 ++-- examples/.env.example | 2 +- src/utils/client.ts | 2 +- src/utils/regions.ts | 13 +++++++------ 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.env.example b/.env.example index 59a3e93..26faf0b 100644 --- a/.env.example +++ b/.env.example @@ -3,7 +3,7 @@ BUDDY_WORKSPACE=your-workspace BUDDY_PROJECT=your-project BUDDY_TOKEN=your-token -# Optional: API region (US, EU, or AP) - defaults to US +# Optional: API region (US, EU, or AS) - defaults to US # BUDDY_REGION=US # Optional: Custom API URL (for testing against different environments) diff --git a/README.md b/README.md index 70284e6..e5f5ff0 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Set required environment variables: export BUDDY_TOKEN="your-api-token" export BUDDY_WORKSPACE="your-workspace" export BUDDY_PROJECT="your-project" -export BUDDY_REGION="US" # Optional: US (default), EU, or AP +export BUDDY_REGION="US" # Optional: US (default), EU, or AS ``` ## Apps @@ -162,7 +162,7 @@ const sandbox = await Sandbox.create({ name: "My Sandbox", os: "ubuntu:24.04", connection: { - region: "EU" // US, EU, or AP + region: "EU" // US, EU, or AS } }); ``` diff --git a/examples/.env.example b/examples/.env.example index ba58e5b..ab9b4cd 100644 --- a/examples/.env.example +++ b/examples/.env.example @@ -4,4 +4,4 @@ BUDDY_WORKSPACE="" BUDDY_PROJECT="" # Optional - defaults to US -BUDDY_REGION="US" # Valid values: US, EU, AP +BUDDY_REGION="US" # Valid values: US, EU, AS diff --git a/src/utils/client.ts b/src/utils/client.ts index b8f7973..9b8e886 100644 --- a/src/utils/client.ts +++ b/src/utils/client.ts @@ -15,7 +15,7 @@ export interface ConnectionConfig { project?: string; /** API authentication token (falls back to BUDDY_TOKEN env var) */ token?: string; - /** API region: US, EU, or AP (falls back to BUDDY_REGION env var) */ + /** API region: US, EU, or AS (falls back to BUDDY_REGION env var) */ region?: Region; /** Custom API URL for testing (falls back to BUDDY_API_URL env var) */ apiUrl?: string; diff --git a/src/utils/regions.ts b/src/utils/regions.ts index 47d3be7..b659783 100644 --- a/src/utils/regions.ts +++ b/src/utils/regions.ts @@ -1,7 +1,7 @@ export const REGIONS = { US: "US", EU: "EU", - AP: "AP", + AS: "AS", } as const; export type Region = (typeof REGIONS)[keyof typeof REGIONS]; @@ -9,7 +9,7 @@ export type Region = (typeof REGIONS)[keyof typeof REGIONS]; export const API_URLS: Record = { US: "https://api.buddy.works", EU: "https://api.eu.buddy.works", - AP: "https://api.asia.buddy.works", + AS: "https://api.asia.buddy.works", }; export function getApiUrlFromRegion(region: Region): string { @@ -20,10 +20,11 @@ export function parseRegion(input: string | undefined): Region { if (!input) return REGIONS.US; const normalized = input.toUpperCase().trim(); + const region = REGIONS[normalized as Region]; - if (normalized === "US" || normalized === "EU" || normalized === "AP") { - return normalized as Region; - } + if (region) return region; - throw new Error(`Invalid region: "${input}". Valid regions are: US, EU, AP`); + throw new Error( + `Invalid region: "${input}". Valid regions are: ${Object.keys(REGIONS).join(", ")}`, + ); } From 18566526f61fa111e589757ed5a756192f21a4b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Ga=C5=84czarczyk?= Date: Wed, 5 Aug 2026 09:57:32 +0200 Subject: [PATCH 20/23] feat: sync openapi (#17) --- src/api/openapi/types.gen.ts | 301 ++++++++++++++++++++++++++++++++--- src/api/openapi/zod.gen.ts | 124 ++++++++++++++- 2 files changed, 401 insertions(+), 24 deletions(-) diff --git a/src/api/openapi/types.gen.ts b/src/api/openapi/types.gen.ts index ed96486..a018e12 100644 --- a/src/api/openapi/types.gen.ts +++ b/src/api/openapi/types.gen.ts @@ -13,6 +13,14 @@ export type UpdateWorkspaceMemberRequest = { * ID of permission set to automatically assign to new projects */ auto_assign_permission_set_id?: number; + /** + * Note for this resource + */ + note?: string; + /** + * YAML note for AI agents operating on this resource + */ + agent_note?: string; }; export type UpdateSsoRequest = { @@ -133,6 +141,10 @@ export type UpdateIntegrationRequest = { * The tenant ID for Azure Cloud integrations */ tenant_id?: string; + /** + * The Azure subscription ID (optional, required when the tenant has multiple subscriptions) + */ + subscription_id?: string; /** * The client ID for OAuth-based integrations */ @@ -173,6 +185,10 @@ export type UpdateIntegrationRequest = { * The cloud region (e.g., us-east-1, eu-west-1) */ region?: string; + /** + * The Sentry organization slug; when set, a webhook trigger is registered for this organization (type SENTRY) + */ + organization?: string; /** * AWS IAM role assumptions for cross-account access */ @@ -228,6 +244,14 @@ export type UpdateIntegrationRequest = { * Set to `true` to disable the integration. Disabled integrations cannot be used in actions */ disabled?: boolean; + /** + * Note for this resource + */ + note?: string; + /** + * YAML note for AI agents operating on this resource + */ + agent_note?: string; }; export type GroupPermissionView = { @@ -431,6 +455,14 @@ export type WorkspaceMemberView = { * ID of permission set to automatically assign to new projects */ auto_assign_permission_set_id?: number; + /** + * Note for this resource + */ + note?: string; + /** + * YAML note for AI agents operating on this resource + */ + agent_note?: string; }; export type WorkspaceView = { @@ -945,13 +977,14 @@ export type PipelineEventView = { | "ENVIRONMENT_DELETE" | "SANDBOX_CREATED" | "SANDBOX_DELETED" - | "SANDBOX_TIMED_OUT"; + | "SANDBOX_TIMED_OUT" + | "SENTRY"; /** * The list of refs (branches/tags) that trigger the pipeline for push/ref events */ refs?: Array; /** - * The list of pull request events that trigger the pipeline. Examples: `OPENED`, `EDITED`, `CLOSED`, `LABELED`, `UNLABELED`, `REVIEW_REQUESTED`, `REVIEW_REQUESTED_REMOVED`, `SYNCHRONIZED` + * The list of pull request events that trigger the pipeline. Examples: `opened`, `reopened`, `synchronize` */ events?: Array; /** @@ -998,6 +1031,38 @@ export type PipelineEventView = { * The list of sandbox targets that trigger the pipeline for sandbox events (type `SANDBOX_CREATED`, `SANDBOX_DELETED`, `SANDBOX_TIMED_OUT`) */ targets?: Array; + /** + * The identifier of the Sentry integration whose webhooks trigger the pipeline (type `SENTRY`) + */ + integration?: string; + /** + * The list of Sentry project slugs that trigger the pipeline; empty means all projects (type `SENTRY`) + */ + projects?: Array; + /** + * The list of Sentry issue levels that trigger the pipeline e.g., `error`, `fatal`; empty means all levels (type `SENTRY`) + */ + levels?: Array; + /** + * The list of Sentry issue statuses that trigger the pipeline e.g., `unresolved`; empty means all statuses (type `SENTRY`) + */ + statuses?: Array; + /** + * The list of Sentry issue substatuses that trigger the pipeline e.g., `new`, `regressed`; empty means all substatuses (type `SENTRY`) + */ + substatuses?: Array; + /** + * The minimum number of events in the Sentry issue required to trigger the pipeline (type `SENTRY`) + */ + count?: number; + /** + * The minimum number of users affected by the Sentry issue required to trigger the pipeline (type `SENTRY`) + */ + user_count?: number; + /** + * The number of minutes to skip subsequent runs for the same Sentry issue after a triggered run (type `SENTRY`) + */ + snooze_minutes?: number; }; /** @@ -1205,6 +1270,14 @@ export type ShortPipelineView = { * The folder name where the pipeline is organized */ folder?: string; + /** + * Note for this resource + */ + note?: string; + /** + * YAML note for AI agents operating on this resource + */ + agent_note?: string; }; /** @@ -1321,7 +1394,8 @@ export type IntegrationView = { | "CONTENTFUL" | "JIRA" | "NPM_REGISTRY" - | "ANTHROPIC"; + | "ANTHROPIC" + | "GOOGLE_GEMINI"; /** * The authentication method used by the integration */ @@ -1360,6 +1434,10 @@ export type IntegrationView = { * The webhook URL for receiving notifications */ webhook_address?: string; + /** + * The Sentry organization slug; when set, a webhook trigger is registered for this organization (type SENTRY) + */ + organization?: string; /** * The JWT audience for token validation */ @@ -1381,6 +1459,14 @@ export type IntegrationView = { * Set to `true` to disable the integration. Disabled integrations cannot be used in actions */ disabled?: boolean; + /** + * Note for this resource + */ + note?: string; + /** + * YAML note for AI agents operating on this resource + */ + agent_note?: string; }; export type IdsView = { @@ -1440,6 +1526,14 @@ export type IdsView = { * The ID of the route */ route_id?: string; + /** + * The ID of the tunnel agent + */ + agent_id?: string; + /** + * The ID of the tunnel + */ + tunnel_id?: string; }; export type AddWorkspaceMemberRequest = { @@ -1455,6 +1549,14 @@ export type AddWorkspaceMemberRequest = { * ID of permission set to automatically assign to new projects */ auto_assign_permission_set_id?: number; + /** + * Note for this resource + */ + note?: string; + /** + * YAML note for AI agents operating on this resource + */ + agent_note?: string; /** * The email address of the user */ @@ -1530,6 +1632,10 @@ export type AddIntegrationRequest = { * The tenant ID for Azure Cloud integrations */ tenant_id?: string; + /** + * The Azure subscription ID (optional, required when the tenant has multiple subscriptions) + */ + subscription_id?: string; /** * The client ID for OAuth-based integrations */ @@ -1570,6 +1676,10 @@ export type AddIntegrationRequest = { * The cloud region (e.g., us-east-1, eu-west-1) */ region?: string; + /** + * The Sentry organization slug; when set, a webhook trigger is registered for this organization (type SENTRY) + */ + organization?: string; /** * AWS IAM role assumptions for cross-account access */ @@ -1625,6 +1735,14 @@ export type AddIntegrationRequest = { * Set to `true` to disable the integration. Disabled integrations cannot be used in actions */ disabled?: boolean; + /** + * Note for this resource + */ + note?: string; + /** + * YAML note for AI agents operating on this resource + */ + agent_note?: string; /** * The type of integration */ @@ -1681,7 +1799,8 @@ export type AddIntegrationRequest = { | "CONTENTFUL" | "JIRA" | "NPM_REGISTRY" - | "ANTHROPIC"; + | "ANTHROPIC" + | "GOOGLE_GEMINI"; /** * The scope of the integration */ @@ -1812,7 +1931,8 @@ export type IntegrationIdView = { | "CONTENTFUL" | "JIRA" | "NPM_REGISTRY" - | "ANTHROPIC"; + | "ANTHROPIC" + | "GOOGLE_GEMINI"; /** * The authentication method used by the integration */ @@ -2039,6 +2159,14 @@ export type UpdateSandboxRequest = { */ variables?: Array; permissions?: PermissionsView; + /** + * Note for this resource + */ + note?: string; + /** + * YAML note for AI agents operating on this resource + */ + agent_note?: string; }; /** @@ -2073,10 +2201,6 @@ export type AddVariableInObjectRequest = { * If set to `true` the variable value will be encrypted and hidden */ encrypted?: boolean; - /** - * The optional description of the variable - */ - description?: string; /** * Initial path for the variable */ @@ -2113,6 +2237,14 @@ export type AddVariableInObjectRequest = { * Set to `true` to disable the variable. Disabled variables are not injected anywhere */ disabled?: boolean; + /** + * Note for this resource + */ + note?: string; + /** + * YAML note for AI agents operating on this resource + */ + agent_note?: string; /** * The type of the added variable */ @@ -2620,6 +2752,14 @@ export type CreateNewSandboxRequest = { */ timeout?: number; permissions?: PermissionsView; + /** + * Note for this resource + */ + note?: string; + /** + * YAML note for AI agents operating on this resource + */ + agent_note?: string; }; export type CreateFromSnapshotRequest = { @@ -2721,10 +2861,6 @@ export type EnvironmentVariableView = { * Available only if `type=VAR`. If set to `true` the variable value can be set by Buddy actions only for execution time */ run_only_settable?: boolean; - /** - * The optional description of the variable - */ - description?: string; /** * Initial path for the variable */ @@ -2777,6 +2913,14 @@ export type EnvironmentVariableView = { * Set to `true` to disable the variable. Disabled variables are not injected anywhere */ disabled?: boolean; + /** + * Note for this resource + */ + note?: string; + /** + * YAML note for AI agents operating on this resource + */ + agent_note?: string; }; export type CloneSandboxRequest = { @@ -2892,6 +3036,14 @@ export type SandboxResponse = { ssh_port?: number; project?: ProjectView; permissions?: PermissionsView; + /** + * Note for this resource + */ + note?: string; + /** + * YAML note for AI agents operating on this resource + */ + agent_note?: string; /** * The environment variables of the sandbox */ @@ -2966,6 +3118,14 @@ export type WorkspaceMemberViewWritable = { * ID of permission set to automatically assign to new projects */ auto_assign_permission_set_id?: number; + /** + * Note for this resource + */ + note?: string; + /** + * YAML note for AI agents operating on this resource + */ + agent_note?: string; }; export type WorkspaceViewWritable = { @@ -3104,13 +3264,14 @@ export type PipelineEventViewWritable = { | "ENVIRONMENT_DELETE" | "SANDBOX_CREATED" | "SANDBOX_DELETED" - | "SANDBOX_TIMED_OUT"; + | "SANDBOX_TIMED_OUT" + | "SENTRY"; /** * The list of refs (branches/tags) that trigger the pipeline for push/ref events */ refs?: Array; /** - * The list of pull request events that trigger the pipeline. Examples: `OPENED`, `EDITED`, `CLOSED`, `LABELED`, `UNLABELED`, `REVIEW_REQUESTED`, `REVIEW_REQUESTED_REMOVED`, `SYNCHRONIZED` + * The list of pull request events that trigger the pipeline. Examples: `opened`, `reopened`, `synchronize` */ events?: Array; /** @@ -3157,6 +3318,38 @@ export type PipelineEventViewWritable = { * The list of sandbox targets that trigger the pipeline for sandbox events (type `SANDBOX_CREATED`, `SANDBOX_DELETED`, `SANDBOX_TIMED_OUT`) */ targets?: Array; + /** + * The identifier of the Sentry integration whose webhooks trigger the pipeline (type `SENTRY`) + */ + integration?: string; + /** + * The list of Sentry project slugs that trigger the pipeline; empty means all projects (type `SENTRY`) + */ + projects?: Array; + /** + * The list of Sentry issue levels that trigger the pipeline e.g., `error`, `fatal`; empty means all levels (type `SENTRY`) + */ + levels?: Array; + /** + * The list of Sentry issue statuses that trigger the pipeline e.g., `unresolved`; empty means all statuses (type `SENTRY`) + */ + statuses?: Array; + /** + * The list of Sentry issue substatuses that trigger the pipeline e.g., `new`, `regressed`; empty means all substatuses (type `SENTRY`) + */ + substatuses?: Array; + /** + * The minimum number of events in the Sentry issue required to trigger the pipeline (type `SENTRY`) + */ + count?: number; + /** + * The minimum number of users affected by the Sentry issue required to trigger the pipeline (type `SENTRY`) + */ + user_count?: number; + /** + * The number of minutes to skip subsequent runs for the same Sentry issue after a triggered run (type `SENTRY`) + */ + snooze_minutes?: number; }; /** @@ -3356,6 +3549,14 @@ export type ShortPipelineViewWritable = { * The folder name where the pipeline is organized */ folder?: string; + /** + * Note for this resource + */ + note?: string; + /** + * YAML note for AI agents operating on this resource + */ + agent_note?: string; }; /** @@ -3456,7 +3657,8 @@ export type IntegrationViewWritable = { | "CONTENTFUL" | "JIRA" | "NPM_REGISTRY" - | "ANTHROPIC"; + | "ANTHROPIC" + | "GOOGLE_GEMINI"; /** * The authentication method used by the integration */ @@ -3495,6 +3697,10 @@ export type IntegrationViewWritable = { * The webhook URL for receiving notifications */ webhook_address?: string; + /** + * The Sentry organization slug; when set, a webhook trigger is registered for this organization (type SENTRY) + */ + organization?: string; /** * The JWT audience for token validation */ @@ -3516,6 +3722,14 @@ export type IntegrationViewWritable = { * Set to `true` to disable the integration. Disabled integrations cannot be used in actions */ disabled?: boolean; + /** + * Note for this resource + */ + note?: string; + /** + * YAML note for AI agents operating on this resource + */ + agent_note?: string; }; export type IdsViewWritable = { @@ -3567,6 +3781,14 @@ export type IdsViewWritable = { * The ID of the route */ route_id?: string; + /** + * The ID of the tunnel agent + */ + agent_id?: string; + /** + * The ID of the tunnel + */ + tunnel_id?: string; }; /** @@ -3673,7 +3895,8 @@ export type IntegrationIdViewWritable = { | "CONTENTFUL" | "JIRA" | "NPM_REGISTRY" - | "ANTHROPIC"; + | "ANTHROPIC" + | "GOOGLE_GEMINI"; /** * The authentication method used by the integration */ @@ -3884,6 +4107,14 @@ export type UpdateSandboxRequestWritable = { */ variables?: Array; permissions?: PermissionsView; + /** + * Note for this resource + */ + note?: string; + /** + * YAML note for AI agents operating on this resource + */ + agent_note?: string; }; /** @@ -3910,10 +4141,6 @@ export type AddVariableInObjectRequestWritable = { * If set to `true` the variable value will be encrypted and hidden */ encrypted?: boolean; - /** - * The optional description of the variable - */ - description?: string; /** * Initial path for the variable */ @@ -3950,6 +4177,14 @@ export type AddVariableInObjectRequestWritable = { * Set to `true` to disable the variable. Disabled variables are not injected anywhere */ disabled?: boolean; + /** + * Note for this resource + */ + note?: string; + /** + * YAML note for AI agents operating on this resource + */ + agent_note?: string; /** * The type of the added variable */ @@ -4302,6 +4537,14 @@ export type CreateNewSandboxRequestWritable = { */ timeout?: number; permissions?: PermissionsView; + /** + * Note for this resource + */ + note?: string; + /** + * YAML note for AI agents operating on this resource + */ + agent_note?: string; }; export type CreateFromSnapshotRequestWritable = { @@ -4454,6 +4697,14 @@ export type SandboxResponseWritable = { ssh_port?: number; project?: ProjectViewWritable; permissions?: PermissionsView; + /** + * Note for this resource + */ + note?: string; + /** + * YAML note for AI agents operating on this resource + */ + agent_note?: string; /** * The environment variables of the sandbox */ @@ -4596,6 +4847,14 @@ export type GetIdentifiersData = { * The path of the route. Resolved together with route_subdomain and route_domain against the parent distribution. */ route_path?: string; + /** + * The human-readable ID of the tunnel agent + */ + agent?: string; + /** + * The name of the tunnel. Requires agent to be resolved. + */ + tunnel?: string; }; url: "/workspaces/{workspace_domain}/identifiers"; }; diff --git a/src/api/openapi/zod.gen.ts b/src/api/openapi/zod.gen.ts index c095140..b2a6778 100644 --- a/src/api/openapi/zod.gen.ts +++ b/src/api/openapi/zod.gen.ts @@ -14,6 +14,8 @@ export const zUpdateWorkspaceMemberRequest = z.object({ error: "Invalid value: Expected int32 to be <= 2147483647", }) .optional(), + note: z.string().optional(), + agent_note: z.string().optional(), }); export const zUpdateSsoRequest = z.object({ @@ -169,6 +171,7 @@ export const zUpdateIntegrationRequest = z.object({ password: z.string().optional(), app_id: z.string().optional(), tenant_id: z.string().optional(), + subscription_id: z.string().optional(), client_id: z.string().optional(), client_token: z.string().optional(), server_id: z.string().optional(), @@ -179,6 +182,7 @@ export const zUpdateIntegrationRequest = z.object({ webhook_address: z.string().optional(), slack_user_id: z.string().optional(), region: z.string().optional(), + organization: z.string().optional(), role_assumptions: z.array(zRoleAssumptionView).optional(), all_pipelines_allowed: z.boolean().optional(), allowed_pipelines: z.array(zPipelineIdView).optional(), @@ -204,6 +208,8 @@ export const zUpdateIntegrationRequest = z.object({ google_project: z.string().optional(), atop_url: z.string().optional(), disabled: z.boolean().optional(), + note: z.string().optional(), + agent_note: z.string().optional(), }); export const zShortWorkspaceView = z.object({ @@ -255,6 +261,8 @@ export const zWorkspaceMemberView = z.object({ error: "Invalid value: Expected int32 to be <= 2147483647", }) .optional(), + note: z.string().optional(), + agent_note: z.string().optional(), }); export const zWorkspaceMembersView = z.object({ @@ -567,6 +575,8 @@ export const zIdsView = z.object({ crawl_suite_id: z.string().optional(), distribution_id: z.string().optional(), route_id: z.string().optional(), + agent_id: z.string().optional(), + tunnel_id: z.string().optional(), }); export const zAddWorkspaceMemberRequest = z.object({ @@ -581,6 +591,8 @@ export const zAddWorkspaceMemberRequest = z.object({ error: "Invalid value: Expected int32 to be <= 2147483647", }) .optional(), + note: z.string().optional(), + agent_note: z.string().optional(), email: z.string(), }); @@ -602,6 +614,7 @@ export const zAddIntegrationRequest = z.object({ password: z.string().optional(), app_id: z.string().optional(), tenant_id: z.string().optional(), + subscription_id: z.string().optional(), client_id: z.string().optional(), client_token: z.string().optional(), server_id: z.string().optional(), @@ -612,6 +625,7 @@ export const zAddIntegrationRequest = z.object({ webhook_address: z.string().optional(), slack_user_id: z.string().optional(), region: z.string().optional(), + organization: z.string().optional(), role_assumptions: z.array(zRoleAssumptionView).optional(), all_pipelines_allowed: z.boolean().optional(), allowed_pipelines: z.array(zPipelineIdView).optional(), @@ -637,6 +651,8 @@ export const zAddIntegrationRequest = z.object({ google_project: z.string().optional(), atop_url: z.string().optional(), disabled: z.boolean().optional(), + note: z.string().optional(), + agent_note: z.string().optional(), type: z.enum([ "GIT_HUB", "BITBUCKET", @@ -691,6 +707,7 @@ export const zAddIntegrationRequest = z.object({ "JIRA", "NPM_REGISTRY", "ANTHROPIC", + "GOOGLE_GEMINI", ]), scope: z.enum(["WORKSPACE", "PROJECT", "ENVIRONMENT"]), project_name: z.string().optional(), @@ -775,6 +792,7 @@ export const zIntegrationIdView = z.object({ "JIRA", "NPM_REGISTRY", "ANTHROPIC", + "GOOGLE_GEMINI", ]) .optional(), auth_type: z @@ -874,7 +892,6 @@ export const zAddVariableInObjectRequest = z.object({ settable: z.boolean().optional(), run_only_settable: z.boolean().optional(), encrypted: z.boolean().optional(), - description: z.string().optional(), init_path: z.string().optional(), defaults: z.string().optional(), file_path: z.string().optional(), @@ -884,6 +901,8 @@ export const zAddVariableInObjectRequest = z.object({ passphrase: z.string().optional(), key_identifier: z.string().optional(), disabled: z.boolean().optional(), + note: z.string().optional(), + agent_note: z.string().optional(), type: z.enum([ "VAR", "FILE", @@ -1016,6 +1035,8 @@ export const zUpdateSandboxRequest = z.object({ endpoints: z.array(zTunnelView).optional(), variables: z.array(zAddVariableInObjectRequest).optional(), permissions: zPermissionsView.optional(), + note: z.string().optional(), + agent_note: z.string().optional(), }); export const zSandboxesView = z.object({ @@ -1188,6 +1209,8 @@ export const zCreateNewSandboxRequest = z.object({ }) .optional(), permissions: zPermissionsView.optional(), + note: z.string().optional(), + agent_note: z.string().optional(), }); /** @@ -1219,7 +1242,6 @@ export const zEnvironmentVariableView = z.object({ encrypted: z.boolean().optional(), settable: z.boolean().optional(), run_only_settable: z.boolean().optional(), - description: z.string().optional(), init_path: z.string().optional(), defaults: z.string().optional(), file_path: z.string().optional(), @@ -1233,6 +1255,8 @@ export const zEnvironmentVariableView = z.object({ passphrase: z.string().optional(), key_identifier: z.string().optional(), disabled: z.boolean().optional(), + note: z.string().optional(), + agent_note: z.string().optional(), }); export const zCreateFromSnapshotRequest = z.object({ @@ -1327,6 +1351,8 @@ export const zSandboxResponse = z.object({ .optional(), project: zProjectView.optional(), permissions: zPermissionsView.optional(), + note: z.string().optional(), + agent_note: z.string().optional(), variables: z.array(zEnvironmentVariableView).optional(), }); @@ -1379,6 +1405,8 @@ export const zWorkspaceMemberViewWritable = z.object({ error: "Invalid value: Expected int32 to be <= 2147483647", }) .optional(), + note: z.string().optional(), + agent_note: z.string().optional(), }); export const zWorkspaceMembersViewWritable = z.object({ @@ -1487,6 +1515,8 @@ export const zIdsViewWritable = z.object({ crawl_suite_id: z.string().optional(), distribution_id: z.string().optional(), route_id: z.string().optional(), + agent_id: z.string().optional(), + tunnel_id: z.string().optional(), }); /** @@ -1564,6 +1594,7 @@ export const zIntegrationIdViewWritable = z.object({ "JIRA", "NPM_REGISTRY", "ANTHROPIC", + "GOOGLE_GEMINI", ]) .optional(), auth_type: z @@ -1657,7 +1688,6 @@ export const zAddVariableInObjectRequestWritable = z.object({ settable: z.boolean().optional(), run_only_settable: z.boolean().optional(), encrypted: z.boolean().optional(), - description: z.string().optional(), init_path: z.string().optional(), defaults: z.string().optional(), file_path: z.string().optional(), @@ -1667,6 +1697,8 @@ export const zAddVariableInObjectRequestWritable = z.object({ passphrase: z.string().optional(), key_identifier: z.string().optional(), disabled: z.boolean().optional(), + note: z.string().optional(), + agent_note: z.string().optional(), type: z.enum([ "VAR", "FILE", @@ -1783,6 +1815,8 @@ export const zUpdateSandboxRequestWritable = z.object({ endpoints: z.array(zTunnelViewWritable).optional(), variables: z.array(zAddVariableInObjectRequestWritable).optional(), permissions: zPermissionsView.optional(), + note: z.string().optional(), + agent_note: z.string().optional(), }); export const zSandboxesViewWritable = z.object({ @@ -1921,6 +1955,8 @@ export const zCreateNewSandboxRequestWritable = z.object({ }) .optional(), permissions: zPermissionsView.optional(), + note: z.string().optional(), + agent_note: z.string().optional(), }); export const zCreateFromSnapshotRequestWritable = z.object({ @@ -2007,6 +2043,8 @@ export const zSandboxResponseWritable = z.object({ .optional(), project: zProjectViewWritable.optional(), permissions: zPermissionsView.optional(), + note: z.string().optional(), + agent_note: z.string().optional(), variables: z.array(zEnvironmentVariableView).optional(), }); @@ -2041,6 +2079,7 @@ export const zPipelineEventView = z.object({ "SANDBOX_CREATED", "SANDBOX_DELETED", "SANDBOX_TIMED_OUT", + "SENTRY", ]) .optional(), refs: z.array(z.string()).optional(), @@ -2064,6 +2103,38 @@ export const zPipelineEventView = z.object({ prefix: z.string().optional(), whitelist: z.array(z.string()).optional(), targets: z.array(z.unknown()).optional(), + integration: z.string().optional(), + projects: z.array(z.string()).optional(), + levels: z.array(z.string()).optional(), + statuses: z.array(z.string()).optional(), + substatuses: z.array(z.string()).optional(), + count: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + user_count: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + snooze_minutes: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), }); /** @@ -2168,6 +2239,8 @@ export const zShortPipelineView = z.object({ cpu: z.enum(["X64", "ARM", "X86"]).optional(), description_required: z.boolean().optional(), folder: z.string().optional(), + note: z.string().optional(), + agent_note: z.string().optional(), }); /** @@ -2234,6 +2307,7 @@ export const zIntegrationView = z.object({ "JIRA", "NPM_REGISTRY", "ANTHROPIC", + "GOOGLE_GEMINI", ]) .optional(), auth_type: z @@ -2256,12 +2330,15 @@ export const zIntegrationView = z.object({ google_project: z.string().optional(), host_url: z.string().optional(), webhook_address: z.string().optional(), + organization: z.string().optional(), audience: z.string().optional(), atop_url: z.string().optional(), permissions: zIntegrationPermissionsView.optional(), all_pipelines_allowed: z.boolean().optional(), allowed_pipelines: z.array(zShortPipelineView).optional(), disabled: z.boolean().optional(), + note: z.string().optional(), + agent_note: z.string().optional(), }); export const zIntegrationsViewWritable = z.object({ @@ -2289,6 +2366,7 @@ export const zPipelineEventViewWritable = z.object({ "SANDBOX_CREATED", "SANDBOX_DELETED", "SANDBOX_TIMED_OUT", + "SENTRY", ]) .optional(), refs: z.array(z.string()).optional(), @@ -2312,6 +2390,38 @@ export const zPipelineEventViewWritable = z.object({ prefix: z.string().optional(), whitelist: z.array(z.string()).optional(), targets: z.array(z.unknown()).optional(), + integration: z.string().optional(), + projects: z.array(z.string()).optional(), + levels: z.array(z.string()).optional(), + statuses: z.array(z.string()).optional(), + substatuses: z.array(z.string()).optional(), + count: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + user_count: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), + snooze_minutes: z + .int() + .min(-2147483648, { + error: "Invalid value: Expected int32 to be >= -2147483648", + }) + .max(2147483647, { + error: "Invalid value: Expected int32 to be <= 2147483647", + }) + .optional(), }); /** @@ -2414,6 +2524,8 @@ export const zShortPipelineViewWritable = z.object({ cpu: z.enum(["X64", "ARM", "X86"]).optional(), description_required: z.boolean().optional(), folder: z.string().optional(), + note: z.string().optional(), + agent_note: z.string().optional(), }); /** @@ -2478,6 +2590,7 @@ export const zIntegrationViewWritable = z.object({ "JIRA", "NPM_REGISTRY", "ANTHROPIC", + "GOOGLE_GEMINI", ]) .optional(), auth_type: z @@ -2500,12 +2613,15 @@ export const zIntegrationViewWritable = z.object({ google_project: z.string().optional(), host_url: z.string().optional(), webhook_address: z.string().optional(), + organization: z.string().optional(), audience: z.string().optional(), atop_url: z.string().optional(), permissions: zIntegrationPermissionsView.optional(), all_pipelines_allowed: z.boolean().optional(), allowed_pipelines: z.array(zShortPipelineViewWritable).optional(), disabled: z.boolean().optional(), + note: z.string().optional(), + agent_note: z.string().optional(), }); export const zGetWorkspacesResponse = zWorkspacesView; @@ -2542,6 +2658,8 @@ export const zGetIdentifiersQuery = z.object({ route_subdomain: z.string().optional(), route_domain: z.string().optional(), route_path: z.string().optional(), + agent: z.string().optional(), + tunnel: z.string().optional(), }); export const zGetIdentifiersResponse = zIdsView; From 903525d99873d857cccbb4152d61fe42503da940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Ga=C5=84czarczyk?= Date: Fri, 7 Aug 2026 07:02:01 +0200 Subject: [PATCH 21/23] feat: back off while polling instead of a fixed 1s interval (#15) * feat: back off while polling instead of a fixed 1s interval * chore: drop unused exports from poll helper --- README.md | 26 ++++ src/entity/command.ts | 18 ++- src/entity/sandbox.ts | 245 ++++++++++++++++++++++++-------------- src/entity/snapshot.ts | 48 ++++---- src/utils/poll.ts | 76 ++++++++++++ tests/create-wait.test.ts | 211 ++++++++++++++++++++++++++++++++ tests/poll.test.ts | 176 +++++++++++++++++++++++++++ 7 files changed, 684 insertions(+), 116 deletions(-) create mode 100644 src/utils/poll.ts create mode 100644 tests/create-wait.test.ts create mode 100644 tests/poll.test.ts diff --git a/README.md b/README.md index e5f5ff0..f037cf7 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,32 @@ export BUDDY_PROJECT="your-project" export BUDDY_REGION="US" # Optional: US (default), EU, or AS ``` +## Waiting for readiness + +`Sandbox.create()` blocks until the sandbox has finished setup and reached +`RUNNING`, so the instance it returns is ready to use. While waiting it polls +the API on a backoff starting at 100ms and growing to 500ms. + +Pass `wait: false` to skip the wait and drive it yourself. The instance you get +back carries everything the create call returned - `id`, `identifier`, `url`, +`resources` - but connection details are not settled yet. + +```typescript +const sandbox = await Sandbox.create({ identifier: "my-sandbox", wait: false }); + +// ... do other work while the sandbox boots ... + +await sandbox.waitUntilReady(); // setup_status: SUCCESS +await sandbox.waitUntilRunning(); // status: RUNNING +``` + +The waiters accept an explicit interval, which pins polling to that fixed value +instead of backing off: + +```typescript +await sandbox.waitUntilRunning(500); // check every 500ms +``` + ## Apps Sandboxes can run multiple apps simultaneously. Each app is a long-running process defined by a command string. diff --git a/src/entity/command.ts b/src/entity/command.ts index 7aea107..5abbe17 100644 --- a/src/entity/command.ts +++ b/src/entity/command.ts @@ -1,5 +1,6 @@ import type { ExecuteSandboxCommandResponse } from "@/api/openapi/types.gen"; import type { BuddyApiClient } from "@/core/buddy-api-client"; +import { pollUntil, resolvePollInterval } from "@/utils/poll"; /** Represents a running or completed command execution in a sandbox */ export class Command { @@ -101,9 +102,11 @@ export class Command { * @returns Final command response */ protected async pollForCommandCompletion( - pollIntervalMs = 1000, + pollIntervalMs?: number, ): Promise { - while (true) { + let finalResponse: ExecuteSandboxCommandResponse | undefined; + + await pollUntil(async () => { const commandResponse = await this.client.getCommandDetails({ path: { sandbox_id: this.sandboxId, @@ -115,10 +118,17 @@ export class Command { commandResponse.status === "SUCCESSFUL" || commandResponse.status === "FAILED" ) { - return commandResponse; + finalResponse = commandResponse; + return true; } - await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); + return false; + }, resolvePollInterval(pollIntervalMs)); + + if (!finalResponse) { + throw new Error(`Command ${this.commandId} finished without a response.`); } + + return finalResponse; } } diff --git a/src/entity/sandbox.ts b/src/entity/sandbox.ts index 220e638..3f1cdd7 100644 --- a/src/entity/sandbox.ts +++ b/src/entity/sandbox.ts @@ -21,9 +21,12 @@ import { Snapshot } from "@/entity/snapshot"; import { withErrorHandler } from "@/errors"; import { type ConnectionConfig, createClient } from "@/utils/client"; import logger from "@/utils/logger"; +import { pollUntil, resolvePollInterval } from "@/utils/poll"; export type { ConnectionConfig }; +const CREATE_MAX_INTERVAL_MS = 500; + // Symbol for private constructor protection const PRIVATE_CONSTRUCTOR_KEY = Symbol("SandboxConstructor"); const INITIALIZE_INSTRUCTIONS = @@ -36,6 +39,8 @@ export interface CreateSandboxConfig extends Partial { /** Optional connection configuration to override defaults */ connection?: ConnectionConfig; + /** Block until the sandbox is running (default: true) */ + wait?: boolean; } /** @@ -61,6 +66,8 @@ export interface CreateFromSnapshotConfig extends Partial> { /** Optional connection configuration to override defaults */ connection?: ConnectionConfig; + /** Block until the sandbox is running (default: true) */ + wait?: boolean; } /** @@ -70,6 +77,8 @@ export interface CloneSandboxConfig extends Partial> { /** Optional connection configuration to override defaults */ connection?: ConnectionConfig; + /** Block until the sandbox is running (default: true) */ + wait?: boolean; } /** @@ -124,6 +133,7 @@ export class Sandbox { static async #finalizeNewSandbox( sandboxResponse: NonNullable, client: BuddyApiClient, + wait = true, ): Promise { const sandbox = new Sandbox( sandboxResponse, @@ -131,28 +141,74 @@ export class Sandbox { PRIVATE_CONSTRUCTOR_KEY, ); - logger.debug(`Waiting for sandbox ${sandbox.data.id} to be ready...`); - await sandbox.waitUntilReady(); - logger.debug( - `Sandbox ${sandbox.data.id} is ready (Setup status: ${sandbox.data.setup_status})`, - ); + if (!wait) { + return sandbox; + } - await sandbox.waitUntilRunning(); + logger.debug(`Waiting for sandbox ${sandbox.data.id} to be ready...`); + await sandbox.#waitUntilReadyAndRunning(); logger.debug( - `Sandbox ${sandbox.data.id} is now running. Status: ${sandbox.data.status}`, + `Sandbox ${sandbox.data.id} is now running (Setup status: ${sandbox.data.setup_status}, Status: ${sandbox.data.status})`, ); return sandbox; } + /** + * Wait for setup to finish and the sandbox to reach RUNNING in one polling + * loop, since a single refresh already carries both fields. + * + * Setup is unbounded because `first_boot_commands` may run long; the + * RUNNING deadline only starts once setup has succeeded. + */ + async #waitUntilReadyAndRunning(maxRunningWaitMs = 60_000): Promise { + const sandboxId = this.initializedId; + return withErrorHandler("Sandbox not running", async () => { + let readyAt: number | undefined; + + await pollUntil( + async () => { + await this.refresh(); + + this.#assertSetupNotFailed(sandboxId); + + if (this.data.status === "FAILED") { + throw new Error( + `Sandbox ${sandboxId} failed. Status: ${this.data.status}`, + ); + } + + if (this.data.setup_status !== "SUCCESS") { + return false; + } + + if (this.data.status === "RUNNING") { + return true; + } + + readyAt ??= Date.now(); + + if (Date.now() - readyAt > maxRunningWaitMs) { + throw new Error( + `Timeout waiting for sandbox ${sandboxId} to be RUNNING. Current: ${this.data.status}`, + ); + } + + return false; + }, + { initialIntervalMs: 100, maxIntervalMs: CREATE_MAX_INTERVAL_MS }, + ); + }); + } + /** * Create a new sandbox * @param config - Sandbox configuration including identifier, name, os, and optional connection settings - * @returns A ready-to-use Sandbox instance + * @returns A ready-to-use Sandbox instance, or an unready one when `wait` is false */ static async create(config?: CreateSandboxConfig) { return withErrorHandler("Failed to create sandbox", async () => { - const { connection, ...sandboxConfig } = config ?? {}; + const { connection, wait, ...sandboxConfig } = config ?? {}; const client = createClient(connection); const requestBody: CreateNewSandboxRequestWritable = { @@ -166,7 +222,7 @@ export class Sandbox { body: requestBody, }); - return Sandbox.#finalizeNewSandbox(sandboxResponse, client); + return Sandbox.#finalizeNewSandbox(sandboxResponse, client, wait); }); } @@ -184,7 +240,7 @@ export class Sandbox { return withErrorHandler( "Failed to create sandbox from snapshot", async () => { - const { connection, ...snapshotConfig } = config ?? {}; + const { connection, wait, ...snapshotConfig } = config ?? {}; const client = createClient(connection); const requestBody: CreateFromSnapshotRequestWritable = { @@ -198,7 +254,7 @@ export class Sandbox { body: requestBody, }); - return Sandbox.#finalizeNewSandbox(sandboxResponse, client); + return Sandbox.#finalizeNewSandbox(sandboxResponse, client, wait); }, ); } @@ -215,7 +271,7 @@ export class Sandbox { */ static async clone(sourceSandboxId: string, config?: CloneSandboxConfig) { return withErrorHandler("Failed to clone sandbox", async () => { - const { connection, ...cloneConfig } = config ?? {}; + const { connection, wait, ...cloneConfig } = config ?? {}; const client = createClient(connection); const requestBody: CloneSandboxRequest = { @@ -229,7 +285,7 @@ export class Sandbox { body: requestBody, }); - return Sandbox.#finalizeNewSandbox(sandboxResponse, client); + return Sandbox.#finalizeNewSandbox(sandboxResponse, client, wait); }); } @@ -562,12 +618,12 @@ export class Sandbox { * it transitions to `"CREATED"`. Use this method to block until it does. * * @param snapshotId - ID of the snapshot to wait for - * @param pollIntervalMs - How often to check the status (default: 2000ms (2s)) + * @param pollIntervalMs - Fixed interval between checks (default: back off from 100ms to 2000ms) * @param maxWaitMs - Maximum time to wait before timing out (default: 180000ms (180s)) */ async waitForSnapshotReady( snapshotId: NonNullable, - pollIntervalMs = 2000, + pollIntervalMs?: number, maxWaitMs = 180_000, ): Promise { const snapshot = await this.getSnapshot(snapshotId); @@ -597,115 +653,124 @@ export class Sandbox { }); } + /** Throw if `setup_status` has reached a terminal failure state */ + #assertSetupNotFailed(sandboxId: string): void { + if (this.data.setup_status === "FAILED") { + const logs = this.data.boot_logs ?? []; + const recent = logs.slice(-20); + const tail = recent.join("\n"); + throw new Error( + `Sandbox ${sandboxId} setup failed.${ + tail + ? `\nBoot logs (last ${recent.length} of ${logs.length} lines):\n${tail}` + : " No boot logs were returned." + }`, + ); + } + + if (this.data.setup_status === "STALE") { + throw new Error( + `Sandbox ${sandboxId} setup is stale. The first_boot_commands were changed but not applied. Recreate the sandbox to apply them.`, + ); + } + } + /** * Wait until the sandbox setup is complete - * @param pollIntervalMs - How often to check the status (default: 1000ms) + * @param pollIntervalMs - Fixed interval between checks (default: back off from 100ms to 1000ms) */ - async waitUntilReady(pollIntervalMs = 1000): Promise { + async waitUntilReady(pollIntervalMs?: number): Promise { const sandboxId = this.initializedId; return withErrorHandler("Sandbox not ready", async () => { - while (true) { + await pollUntil(async () => { await this.refresh(); if (this.data.setup_status === "SUCCESS") { - return; - } - - if (this.data.setup_status === "FAILED") { - const logs = this.data.boot_logs ?? []; - const recent = logs.slice(-20); - const tail = recent.join("\n"); - throw new Error( - `Sandbox ${sandboxId} setup failed.${ - tail - ? `\nBoot logs (last ${recent.length} of ${logs.length} lines):\n${tail}` - : " No boot logs were returned." - }`, - ); + return true; } - if (this.data.setup_status === "STALE") { - throw new Error( - `Sandbox ${sandboxId} setup is stale. The first_boot_commands were changed but not applied. Recreate the sandbox to apply them.`, - ); - } + this.#assertSetupNotFailed(sandboxId); - await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); - } + return false; + }, resolvePollInterval(pollIntervalMs)); }); } /** * Wait until the sandbox is in RUNNING state - * @param pollIntervalMs - How often to check the status (default: 1000ms) + * @param pollIntervalMs - Fixed interval between checks (default: back off from 100ms to 1000ms) * @param maxWaitMs - Maximum time to wait before timing out (default: 60000ms) */ async waitUntilRunning( - pollIntervalMs = 1000, + pollIntervalMs?: number, maxWaitMs = 60_000, ): Promise { const sandboxId = this.initializedId; return withErrorHandler("Sandbox not running", async () => { - const startTime = Date.now(); - - while (true) { - await this.refresh(); - - if (this.data.status === "RUNNING") { - return; - } - - if (this.data.status === "FAILED") { - throw new Error( - `Sandbox ${sandboxId} failed. Status: ${this.data.status}`, - ); - } - - if (Date.now() - startTime > maxWaitMs) { - throw new Error( - `Timeout waiting for sandbox ${sandboxId} to be RUNNING. Current: ${this.data.status}`, - ); - } - - await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); - } + await pollUntil( + async () => { + await this.refresh(); + + if (this.data.status === "RUNNING") { + return true; + } + + if (this.data.status === "FAILED") { + throw new Error( + `Sandbox ${sandboxId} failed. Status: ${this.data.status}`, + ); + } + + return false; + }, + { + ...resolvePollInterval(pollIntervalMs), + maxWaitMs, + onTimeout: () => + new Error( + `Timeout waiting for sandbox ${sandboxId} to be RUNNING. Current: ${this.data.status}`, + ), + }, + ); }); } /** * Wait until the sandbox is in STOPPED state - * @param pollIntervalMs - How often to check the status (default: 1000ms) + * @param pollIntervalMs - Fixed interval between checks (default: back off from 100ms to 1000ms) * @param maxWaitMs - Maximum time to wait before timing out (default: 60000ms) */ async waitUntilStopped( - pollIntervalMs = 1000, + pollIntervalMs?: number, maxWaitMs = 60_000, ): Promise { const sandboxId = this.initializedId; return withErrorHandler("Sandbox not stopped", async () => { - const startTime = Date.now(); - - while (true) { - await this.refresh(); - - if (this.data.status === "STOPPED") { - return; - } - - if (this.data.status === "FAILED") { - throw new Error( - `Sandbox ${sandboxId} failed. Status: ${this.data.status}`, - ); - } - - if (Date.now() - startTime > maxWaitMs) { - throw new Error( - `Timeout waiting for sandbox ${sandboxId} to be STOPPED. Current: ${this.data.status}`, - ); - } - - await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); - } + await pollUntil( + async () => { + await this.refresh(); + + if (this.data.status === "STOPPED") { + return true; + } + + if (this.data.status === "FAILED") { + throw new Error( + `Sandbox ${sandboxId} failed. Status: ${this.data.status}`, + ); + } + + return false; + }, + { + ...resolvePollInterval(pollIntervalMs), + maxWaitMs, + onTimeout: () => + new Error( + `Timeout waiting for sandbox ${sandboxId} to be STOPPED. Current: ${this.data.status}`, + ), + }, + ); }); } diff --git a/src/entity/snapshot.ts b/src/entity/snapshot.ts index d805bda..764d28b 100644 --- a/src/entity/snapshot.ts +++ b/src/entity/snapshot.ts @@ -1,7 +1,9 @@ import type { SnapshotView } from "@/api/openapi/types.gen"; import type { BuddyApiClient } from "@/core/buddy-api-client"; import { withErrorHandler } from "@/errors"; +import { pollUntil, resolvePollInterval } from "@/utils/poll"; +const SNAPSHOT_MAX_INTERVAL_MS = 2000; const PRIVATE_CONSTRUCTOR_KEY = Symbol("SnapshotConstructor"); const INITIALIZE_INSTRUCTIONS = "Snapshots are obtained via sandbox.createSnapshot(), sandbox.listSnapshots(), sandbox.getSnapshot(id), or Sandbox.getSnapshotById(sandboxId, snapshotId)."; @@ -48,37 +50,39 @@ export class Snapshot { * `sandbox.createSnapshot()` returns immediately with `status: "CREATING"`. * The snapshot cannot be restored until it transitions to `"CREATED"`. * - * @param pollIntervalMs - How often to check the status (default: 2000ms (2s)) + * @param pollIntervalMs - Fixed interval between checks (default: back off from 100ms to 2000ms) * @param maxWaitMs - Maximum time to wait before timing out (default: 180000ms (180s)) */ async waitUntilReady( - pollIntervalMs = 2000, + pollIntervalMs?: number, maxWaitMs = 180_000, ): Promise { return withErrorHandler("Snapshot not ready", async () => { - const startTime = Date.now(); + await pollUntil( + async () => { + await this.refresh(); - while (true) { - await this.refresh(); + if (this.#data.status === "CREATED") { + return true; + } - if (this.#data.status === "CREATED") { - return; - } + if (this.#data.status === "FAILED") { + throw new Error( + `Snapshot ${this.id} failed. Status: ${this.#data.status}`, + ); + } - if (this.#data.status === "FAILED") { - throw new Error( - `Snapshot ${this.id} failed. Status: ${this.#data.status}`, - ); - } - - if (Date.now() - startTime > maxWaitMs) { - throw new Error( - `Timeout waiting for snapshot ${this.id} to be CREATED. Current: ${this.#data.status}`, - ); - } - - await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); - } + return false; + }, + { + ...resolvePollInterval(pollIntervalMs, SNAPSHOT_MAX_INTERVAL_MS), + maxWaitMs, + onTimeout: () => + new Error( + `Timeout waiting for snapshot ${this.id} to be CREATED. Current: ${this.#data.status}`, + ), + }, + ); }); } diff --git a/src/utils/poll.ts b/src/utils/poll.ts new file mode 100644 index 0000000..82726ad --- /dev/null +++ b/src/utils/poll.ts @@ -0,0 +1,76 @@ +const DEFAULT_INITIAL_INTERVAL_MS = 100; +const DEFAULT_MAX_INTERVAL_MS = 1000; +const DEFAULT_BACKOFF_FACTOR = 1.5; + +/** Configuration for a backing-off polling loop */ +export interface PollOptions { + /** Delay before the first re-check (default: 100ms) */ + initialIntervalMs?: number; + /** Upper bound the delay grows to (default: 1000ms) */ + maxIntervalMs?: number; + /** Multiplier applied to the delay after each poll (default: 1.5) */ + factor?: number; + /** Maximum total time to wait; omit for an unbounded wait */ + maxWaitMs?: number; + /** Builds the error thrown once `maxWaitMs` is exceeded */ + onTimeout?: () => Error; +} + +/** + * Turn a caller-supplied `pollIntervalMs` into poll options: omitted opts into + * backoff, an explicit number keeps a fixed interval of exactly that length. + */ +export function resolvePollInterval( + pollIntervalMs: number | undefined, + maxIntervalMs: number = DEFAULT_MAX_INTERVAL_MS, +): Pick { + if (pollIntervalMs === undefined) { + return { initialIntervalMs: DEFAULT_INITIAL_INTERVAL_MS, maxIntervalMs }; + } + + return { + initialIntervalMs: pollIntervalMs, + maxIntervalMs: pollIntervalMs, + }; +} + +/** Suspend execution for the given number of milliseconds */ +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * Poll `check` until it returns true, growing the delay by `factor` after each + * attempt up to `maxIntervalMs`. + * + * `check` runs before the first sleep and throws on terminal failure states, + * so callers keep their own error messages. + */ +export async function pollUntil( + check: () => Promise, + options: PollOptions = {}, +): Promise { + const { + initialIntervalMs = DEFAULT_INITIAL_INTERVAL_MS, + maxIntervalMs = DEFAULT_MAX_INTERVAL_MS, + factor = DEFAULT_BACKOFF_FACTOR, + maxWaitMs, + onTimeout, + } = options; + + const startTime = Date.now(); + let delay = Math.min(initialIntervalMs, maxIntervalMs); + + while (true) { + if (await check()) { + return; + } + + if (maxWaitMs !== undefined && Date.now() - startTime > maxWaitMs) { + throw onTimeout?.() ?? new Error(`Timeout after ${String(maxWaitMs)}ms`); + } + + await sleep(delay); + delay = Math.min(delay * factor, maxIntervalMs); + } +} diff --git a/tests/create-wait.test.ts b/tests/create-wait.test.ts new file mode 100644 index 0000000..cfd6ada --- /dev/null +++ b/tests/create-wait.test.ts @@ -0,0 +1,211 @@ +import { HttpResponse, http } from "msw"; +import { setupServer } from "msw/node"; +import { + afterAll, + afterEach, + beforeAll, + describe, + expect, + it, + vi, +} from "vitest"; +import { Sandbox } from "@/entity/sandbox"; + +const TEST_API_URL = "https://api.test.buddy.works"; +const TEST_WORKSPACE = "test-workspace"; +const SANDBOX_ID = "sandbox-123"; + +const connection = { + workspace: TEST_WORKSPACE, + project: "test-project", + token: "test-token", + apiUrl: TEST_API_URL, +}; + +const SANDBOX_URL = `${TEST_API_URL}/workspaces/${TEST_WORKSPACE}/sandboxes`; + +const server = setupServer(); + +beforeAll(() => server.listen({ onUnhandledRequest: "error" })); +afterEach(() => { + server.resetHandlers(); + vi.restoreAllMocks(); +}); +afterAll(() => server.close()); + +/** HttpClient's per-request abort timer, which also goes through setTimeout */ +const HTTP_REQUEST_TIMEOUT_MS = 30_000; + +/** + * Record the sleeps the polling loop schedules, ignoring the request-abort + * timers the HTTP client sets around every call. + */ +function recordPollDelays(): number[] { + const delays: number[] = []; + const scheduleTimeout = globalThis.setTimeout; + + vi.spyOn(globalThis, "setTimeout").mockImplementation((( + callback: () => void, + ms?: number, + ) => { + if (ms !== HTTP_REQUEST_TIMEOUT_MS) { + delays.push(ms ?? 0); + } + return scheduleTimeout(callback, ms); + }) as typeof setTimeout); + + return delays; +} + +/** Keep the sandbox un-ready for `count` polls, then report it ready */ +function notReadyFor(count: number) { + return [ + ...Array.from({ length: count }, () => ({ + setup_status: "INPROGRESS", + status: "STARTING", + })), + { setup_status: "SUCCESS", status: "RUNNING" }, + ]; +} + +/** + * Serve a POST /sandboxes that always reports STARTING, then walk + * GET /sandboxes/:id through the supplied states one call at a time, + * repeating the last state once exhausted. + */ +function mockSandboxLifecycle( + states: { setup_status: string; status: string }[], +) { + const requestedBodies: Record[] = []; + let getCount = 0; + + server.use( + http.post(SANDBOX_URL, async ({ request }) => { + requestedBodies.push((await request.json()) as Record); + return HttpResponse.json({ + id: SANDBOX_ID, + name: "Test Sandbox", + os: "ubuntu:24.04", + setup_status: "INPROGRESS", + status: "STARTING", + }); + }), + http.get(`${SANDBOX_URL}/${SANDBOX_ID}`, () => { + const state = states[Math.min(getCount, states.length - 1)]; + getCount += 1; + return HttpResponse.json({ + id: SANDBOX_ID, + name: "Test Sandbox", + os: "ubuntu:24.04", + ...state, + }); + }), + ); + + return { + requestedBodies, + getCount: () => getCount, + }; +} + +describe("Sandbox.create readiness", () => { + it("issues a single GET per poll while waiting for both fields", async () => { + const lifecycle = mockSandboxLifecycle([ + { setup_status: "INPROGRESS", status: "STARTING" }, + { setup_status: "SUCCESS", status: "STARTING" }, + { setup_status: "SUCCESS", status: "RUNNING" }, + ]); + + const sandbox = await Sandbox.create({ connection }); + + expect(sandbox.data.status).toBe("RUNNING"); + expect(sandbox.data.setup_status).toBe("SUCCESS"); + expect(lifecycle.getCount()).toBe(3); + }); + + it("returns as soon as the first poll sees a ready sandbox", async () => { + const lifecycle = mockSandboxLifecycle([ + { setup_status: "SUCCESS", status: "RUNNING" }, + ]); + + const started = Date.now(); + await Sandbox.create({ connection }); + const elapsed = Date.now() - started; + + expect(lifecycle.getCount()).toBe(1); + expect(elapsed).toBeLessThan(100); + }); + + it("backs off from 100ms while waiting", async () => { + const delays = recordPollDelays(); + mockSandboxLifecycle(notReadyFor(3)); + + await Sandbox.create({ connection }); + + expect(delays).toEqual([100, 150, 225]); + }); + + it("caps the create backoff at 500ms", async () => { + const delays = recordPollDelays(); + mockSandboxLifecycle(notReadyFor(8)); + + await Sandbox.create({ connection }); + + expect(delays.map(Math.round)).toEqual([ + 100, 150, 225, 338, 500, 500, 500, 500, + ]); + expect(Math.max(...delays)).toBe(500); + }); + + it("skips polling entirely when wait is false", async () => { + const lifecycle = mockSandboxLifecycle([ + { setup_status: "INPROGRESS", status: "STARTING" }, + ]); + + const sandbox = await Sandbox.create({ connection, wait: false }); + + expect(sandbox.data.id).toBe(SANDBOX_ID); + expect(lifecycle.getCount()).toBe(0); + }); + + it("does not forward wait to the API request body", async () => { + const lifecycle = mockSandboxLifecycle([ + { setup_status: "SUCCESS", status: "RUNNING" }, + ]); + + await Sandbox.create({ connection, wait: false, name: "Named Sandbox" }); + + expect(lifecycle.requestedBodies).toHaveLength(1); + expect(lifecycle.requestedBodies[0]).not.toHaveProperty("wait"); + expect(lifecycle.requestedBodies[0]).toMatchObject({ + name: "Named Sandbox", + }); + }); + + it("surfaces boot logs when setup fails", async () => { + mockSandboxLifecycle([ + { setup_status: "INPROGRESS", status: "STARTING" }, + { setup_status: "FAILED", status: "STARTING" }, + ]); + + await expect(Sandbox.create({ connection })).rejects.toThrow( + `Sandbox ${SANDBOX_ID} setup failed.`, + ); + }); + + it("reports a stale setup", async () => { + mockSandboxLifecycle([{ setup_status: "STALE", status: "STARTING" }]); + + await expect(Sandbox.create({ connection })).rejects.toThrow( + "setup is stale", + ); + }); + + it("fails fast when the sandbox reports FAILED", async () => { + mockSandboxLifecycle([{ setup_status: "INPROGRESS", status: "FAILED" }]); + + await expect(Sandbox.create({ connection })).rejects.toThrow( + `Sandbox ${SANDBOX_ID} failed.`, + ); + }); +}); diff --git a/tests/poll.test.ts b/tests/poll.test.ts new file mode 100644 index 0000000..a0b83d2 --- /dev/null +++ b/tests/poll.test.ts @@ -0,0 +1,176 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { pollUntil, resolvePollInterval } from "@/utils/poll"; + +/** Record the delay handed to every setTimeout call while keeping timers working */ +function recordDelays(): number[] { + const delays: number[] = []; + const scheduleTimeout = globalThis.setTimeout; + + vi.spyOn(globalThis, "setTimeout").mockImplementation((( + callback: () => void, + ms?: number, + ) => { + delays.push(ms ?? 0); + return scheduleTimeout(callback, ms); + }) as typeof setTimeout); + + return delays; +} + +describe("resolvePollInterval", () => { + it("backs off from 100ms to 1000ms when no interval is given", () => { + expect(resolvePollInterval(undefined)).toEqual({ + initialIntervalMs: 100, + maxIntervalMs: 1000, + }); + }); + + it("honours a custom ceiling for the backoff case", () => { + expect(resolvePollInterval(undefined, 2000)).toEqual({ + initialIntervalMs: 100, + maxIntervalMs: 2000, + }); + }); + + it("pins to a fixed interval when one is given explicitly", () => { + expect(resolvePollInterval(500)).toEqual({ + initialIntervalMs: 500, + maxIntervalMs: 500, + }); + }); + + it("lets an explicit interval exceed the default ceiling", () => { + expect(resolvePollInterval(5000)).toEqual({ + initialIntervalMs: 5000, + maxIntervalMs: 5000, + }); + }); +}); + +describe("pollUntil", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + vi.useRealTimers(); + }); + + it("does not sleep when the first check already succeeds", async () => { + const delays = recordDelays(); + + await pollUntil(async () => true); + + expect(delays).toEqual([]); + }); + + it("grows the delay by 1.5x up to the 1000ms ceiling", async () => { + const delays = recordDelays(); + let calls = 0; + + const settled = pollUntil(async () => { + calls += 1; + return calls > 9; + }); + + await vi.runAllTimersAsync(); + await settled; + + expect(delays.map(Math.round)).toEqual([ + 100, 150, 225, 338, 506, 759, 1000, 1000, 1000, + ]); + }); + + it("keeps a constant delay when an explicit interval is used", async () => { + const delays = recordDelays(); + let calls = 0; + + const settled = pollUntil(async () => { + calls += 1; + return calls > 4; + }, resolvePollInterval(500)); + + await vi.runAllTimersAsync(); + await settled; + + expect(delays).toEqual([500, 500, 500, 500]); + }); + + it("clamps the first delay to maxIntervalMs", async () => { + const delays = recordDelays(); + let calls = 0; + + const settled = pollUntil( + async () => { + calls += 1; + return calls > 2; + }, + { initialIntervalMs: 5000, maxIntervalMs: 200 }, + ); + + await vi.runAllTimersAsync(); + await settled; + + expect(delays).toEqual([200, 200]); + }); + + it("throws the onTimeout error once maxWaitMs is exceeded", async () => { + const settled = pollUntil(async () => false, { + initialIntervalMs: 100, + maxIntervalMs: 100, + maxWaitMs: 250, + onTimeout: () => new Error("gave up waiting"), + }); + + const assertion = expect(settled).rejects.toThrow("gave up waiting"); + await vi.runAllTimersAsync(); + await assertion; + }); + + it("falls back to a generic timeout error without onTimeout", async () => { + const settled = pollUntil(async () => false, { + initialIntervalMs: 100, + maxIntervalMs: 100, + maxWaitMs: 150, + }); + + const assertion = expect(settled).rejects.toThrow("Timeout after 150ms"); + await vi.runAllTimersAsync(); + await assertion; + }); + + it("never times out when maxWaitMs is omitted", async () => { + let calls = 0; + + const settled = pollUntil(async () => { + calls += 1; + return calls > 50; + }); + + await vi.runAllTimersAsync(); + + await expect(settled).resolves.toBeUndefined(); + expect(calls).toBe(51); + }); + + it("propagates a throw from check without sleeping again", async () => { + const delays = recordDelays(); + let calls = 0; + + const settled = pollUntil(async () => { + calls += 1; + if (calls === 3) { + throw new Error("terminal state"); + } + return false; + }); + + const assertion = expect(settled).rejects.toThrow("terminal state"); + await vi.runAllTimersAsync(); + await assertion; + + expect(calls).toBe(3); + expect(delays).toEqual([100, 150]); + }); +}); From 8fe7c7c23d690ecb894c2a0c3e036397a3f5e6a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20Ga=C5=84czarczyk?= Date: Fri, 7 Aug 2026 07:02:14 +0200 Subject: [PATCH 22/23] feat: dual ESM/CJS build with accurate engines range (#18) * feat: dual ESM/CJS build with accurate engines range CJS entry requires p-retry (pure ESM), which needs require(esm) support, so narrow engines to ^20.19.0 || >=22.12.0. * checks: legacyCjs --- package.json | 17 ++++++++++++----- tsdown.config.ts | 3 ++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 411ad6b..07c20c9 100644 --- a/package.json +++ b/package.json @@ -3,12 +3,19 @@ "version": "0.1.7", "type": "module", "description": "TypeScript SDK for managing sandboxes through the Buddy API", - "main": "./dist/index.mjs", - "types": "./dist/index.d.mts", + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.cts", "exports": { ".": { - "types": "./dist/index.d.mts", - "default": "./dist/index.mjs" + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } } }, "files": [ @@ -34,7 +41,7 @@ "typescript" ], "engines": { - "node": ">=20" + "node": "^20.19.0 || >=22.12.0" }, "sideEffects": false, "author": "Cyprian Zdebski ", diff --git a/tsdown.config.ts b/tsdown.config.ts index 6d7c958..07ddd9f 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -3,7 +3,8 @@ import { defineConfig } from "tsdown"; export default defineConfig({ entry: ["./src/index.ts"], platform: "node", - format: "esm", + format: ["esm", "cjs"], dts: true, fixedExtension: true, + checks: { legacyCjs: false }, }); From fec4d8c3ee0b86b0b4083faa4d787d49f1d0e9e9 Mon Sep 17 00:00:00 2001 From: Buddy CI Date: Fri, 7 Aug 2026 05:09:21 +0000 Subject: [PATCH 23/23] chore(release): bump version to 0.1.8-rc.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 07c20c9..9e0de54 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@buddy-works/sandbox-sdk", - "version": "0.1.7", + "version": "0.1.8-rc.0", "type": "module", "description": "TypeScript SDK for managing sandboxes through the Buddy API", "main": "./dist/index.cjs",