@agent-foundry/studio 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,412 @@
1
+ /**
2
+ * BFF API Client
3
+ *
4
+ * Client for interacting with the BFF Studio API endpoints.
5
+ * All write operations (create, update, delete) should go through this client.
6
+ *
7
+ * The BFF uses SQLAlchemy with service_role credentials, which bypasses RLS.
8
+ * This is the secure and recommended way to perform write operations.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { createBFFClient } from '@agent-foundry/studio/bff';
13
+ *
14
+ * const client = createBFFClient({
15
+ * baseUrl: 'http://localhost:11001',
16
+ * authToken: 'your-supabase-jwt',
17
+ * });
18
+ *
19
+ * // Create a project
20
+ * const project = await client.projects.create({
21
+ * name: 'My App',
22
+ * slug: 'my-app',
23
+ * rootPath: '/Users/me/projects/my-app',
24
+ * });
25
+ * ```
26
+ */
27
+
28
+ import type { StudioProject, CreateProjectInput, UpdateProjectInput } from '../types/project';
29
+ import type { Deployment, DeploymentMetadata } from '../types/deployment';
30
+ import type {
31
+ BFFClientConfig,
32
+ CreateProjectRequest,
33
+ UpdateProjectRequest,
34
+ ForkProjectRequest,
35
+ ProjectResponse,
36
+ ProjectListResponse,
37
+ StartDeploymentRequest,
38
+ StartDeploymentResponse,
39
+ UpdateDeploymentStatusRequest,
40
+ CompleteDeploymentRequest,
41
+ CompleteDeploymentResponse,
42
+ FailDeploymentRequest,
43
+ DeploymentListResponse,
44
+ PublishRequest,
45
+ PublishResponse,
46
+ APIError,
47
+ } from './types';
48
+
49
+ /**
50
+ * Error thrown by BFF API client
51
+ */
52
+ export class BFFAPIError extends Error {
53
+ constructor(
54
+ message: string,
55
+ public readonly status: number,
56
+ public readonly detail?: string,
57
+ public readonly code?: string
58
+ ) {
59
+ super(message);
60
+ this.name = 'BFFAPIError';
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Projects API client
66
+ */
67
+ export class ProjectsAPI {
68
+ constructor(
69
+ private readonly baseUrl: string,
70
+ private readonly authToken: string,
71
+ private readonly timeout: number
72
+ ) {}
73
+
74
+ private get headers(): HeadersInit {
75
+ return {
76
+ 'Content-Type': 'application/json',
77
+ 'Authorization': `Bearer ${this.authToken}`,
78
+ };
79
+ }
80
+
81
+ private async request<T>(
82
+ method: string,
83
+ path: string,
84
+ body?: unknown
85
+ ): Promise<T> {
86
+ const controller = new AbortController();
87
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
88
+
89
+ try {
90
+ const response = await fetch(`${this.baseUrl}${path}`, {
91
+ method,
92
+ headers: this.headers,
93
+ body: body ? JSON.stringify(body) : undefined,
94
+ signal: controller.signal,
95
+ });
96
+
97
+ clearTimeout(timeoutId);
98
+
99
+ if (!response.ok) {
100
+ let detail: string | undefined;
101
+ let code: string | undefined;
102
+ try {
103
+ const error: APIError = await response.json();
104
+ detail = error.detail;
105
+ code = error.code;
106
+ } catch {
107
+ detail = await response.text();
108
+ }
109
+ throw new BFFAPIError(
110
+ `BFF API error: ${response.status}`,
111
+ response.status,
112
+ detail,
113
+ code
114
+ );
115
+ }
116
+
117
+ // Handle 204 No Content
118
+ if (response.status === 204) {
119
+ return undefined as T;
120
+ }
121
+
122
+ return response.json();
123
+ } finally {
124
+ clearTimeout(timeoutId);
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Create a new project
130
+ */
131
+ async create(input: CreateProjectInput): Promise<StudioProject> {
132
+ const request: CreateProjectRequest = {
133
+ name: input.name,
134
+ slug: input.slug,
135
+ description: input.description,
136
+ rootPath: input.rootPath,
137
+ framework: input.framework,
138
+ config: input.config,
139
+ parentProjectId: input.parentProjectId,
140
+ };
141
+
142
+ return this.request<ProjectResponse>('POST', '/studio/projects', request);
143
+ }
144
+
145
+ /**
146
+ * Get a project by ID
147
+ */
148
+ async get(id: string): Promise<ProjectResponse> {
149
+ return this.request<ProjectResponse>('GET', `/studio/projects/${id}`);
150
+ }
151
+
152
+ /**
153
+ * List all projects with optional pagination
154
+ */
155
+ async list(options?: {
156
+ page?: number;
157
+ pageSize?: number;
158
+ framework?: string;
159
+ search?: string;
160
+ }): Promise<ProjectListResponse> {
161
+ const params = new URLSearchParams();
162
+ if (options?.page) params.set('page', String(options.page));
163
+ if (options?.pageSize) params.set('pageSize', String(options.pageSize));
164
+ if (options?.framework) params.set('framework', options.framework);
165
+ if (options?.search) params.set('search', options.search);
166
+
167
+ const query = params.toString();
168
+ const path = query ? `/studio/projects?${query}` : '/studio/projects';
169
+ return this.request<ProjectListResponse>('GET', path);
170
+ }
171
+
172
+ /**
173
+ * Update a project
174
+ */
175
+ async update(id: string, input: UpdateProjectInput): Promise<StudioProject> {
176
+ const request: UpdateProjectRequest = {
177
+ name: input.name,
178
+ description: input.description,
179
+ config: input.config,
180
+ };
181
+
182
+ return this.request<ProjectResponse>('PUT', `/studio/projects/${id}`, request);
183
+ }
184
+
185
+ /**
186
+ * Delete a project
187
+ */
188
+ async delete(id: string): Promise<void> {
189
+ return this.request<void>('DELETE', `/studio/projects/${id}`);
190
+ }
191
+
192
+ /**
193
+ * Fork a project
194
+ */
195
+ async fork(
196
+ id: string,
197
+ options: { newSlug: string; newRootPath: string; newName?: string }
198
+ ): Promise<StudioProject> {
199
+ const request: ForkProjectRequest = {
200
+ newSlug: options.newSlug,
201
+ newRootPath: options.newRootPath,
202
+ newName: options.newName,
203
+ };
204
+
205
+ return this.request<ProjectResponse>('POST', `/studio/projects/${id}/fork`, request);
206
+ }
207
+
208
+ /**
209
+ * Publish a project to Feed
210
+ */
211
+ async publish(
212
+ id: string,
213
+ options?: PublishRequest
214
+ ): Promise<PublishResponse> {
215
+ return this.request<PublishResponse>(
216
+ 'POST',
217
+ `/studio/projects/${id}/publish`,
218
+ options ?? {}
219
+ );
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Deployments API client
225
+ */
226
+ export class DeploymentsAPI {
227
+ constructor(
228
+ private readonly baseUrl: string,
229
+ private readonly authToken: string,
230
+ private readonly timeout: number
231
+ ) {}
232
+
233
+ private get headers(): HeadersInit {
234
+ return {
235
+ 'Content-Type': 'application/json',
236
+ 'Authorization': `Bearer ${this.authToken}`,
237
+ };
238
+ }
239
+
240
+ private async request<T>(
241
+ method: string,
242
+ path: string,
243
+ body?: unknown
244
+ ): Promise<T> {
245
+ const controller = new AbortController();
246
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
247
+
248
+ try {
249
+ const response = await fetch(`${this.baseUrl}${path}`, {
250
+ method,
251
+ headers: this.headers,
252
+ body: body ? JSON.stringify(body) : undefined,
253
+ signal: controller.signal,
254
+ });
255
+
256
+ clearTimeout(timeoutId);
257
+
258
+ if (!response.ok) {
259
+ let detail: string | undefined;
260
+ let code: string | undefined;
261
+ try {
262
+ const error: APIError = await response.json();
263
+ detail = error.detail;
264
+ code = error.code;
265
+ } catch {
266
+ detail = await response.text();
267
+ }
268
+ throw new BFFAPIError(
269
+ `BFF API error: ${response.status}`,
270
+ response.status,
271
+ detail,
272
+ code
273
+ );
274
+ }
275
+
276
+ return response.json();
277
+ } finally {
278
+ clearTimeout(timeoutId);
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Start a new deployment (creates deployment record and returns STS credentials)
284
+ */
285
+ async start(input: {
286
+ projectId: string;
287
+ version?: string;
288
+ metadata?: Partial<DeploymentMetadata>;
289
+ }): Promise<StartDeploymentResponse> {
290
+ const request: StartDeploymentRequest = {
291
+ projectId: input.projectId,
292
+ version: input.version,
293
+ metadata: input.metadata,
294
+ };
295
+
296
+ return this.request<StartDeploymentResponse>(
297
+ 'POST',
298
+ '/studio/deployments/start',
299
+ request
300
+ );
301
+ }
302
+
303
+ /**
304
+ * Get a deployment by ID
305
+ */
306
+ async get(id: string): Promise<Deployment> {
307
+ return this.request<Deployment>('GET', `/studio/deployments/${id}`);
308
+ }
309
+
310
+ /**
311
+ * List deployments for a project
312
+ */
313
+ async list(projectId: string, limit?: number): Promise<DeploymentListResponse> {
314
+ const params = new URLSearchParams();
315
+ if (limit) params.set('limit', String(limit));
316
+
317
+ const query = params.toString();
318
+ const path = query
319
+ ? `/studio/projects/${projectId}/deployments?${query}`
320
+ : `/studio/projects/${projectId}/deployments`;
321
+
322
+ return this.request<DeploymentListResponse>('GET', path);
323
+ }
324
+
325
+ /**
326
+ * Update deployment status
327
+ */
328
+ async updateStatus(
329
+ id: string,
330
+ input: UpdateDeploymentStatusRequest
331
+ ): Promise<{ status: string; deploymentId: string }> {
332
+ return this.request<{ status: string; deploymentId: string }>(
333
+ 'PUT',
334
+ `/studio/deployments/${id}/status`,
335
+ input
336
+ );
337
+ }
338
+
339
+ /**
340
+ * Mark deployment as complete (after successful OSS upload)
341
+ */
342
+ async complete(
343
+ id: string,
344
+ input: CompleteDeploymentRequest
345
+ ): Promise<CompleteDeploymentResponse> {
346
+ return this.request<CompleteDeploymentResponse>(
347
+ 'POST',
348
+ `/studio/deployments/${id}/complete`,
349
+ input
350
+ );
351
+ }
352
+
353
+ /**
354
+ * Mark deployment as failed
355
+ */
356
+ async fail(
357
+ id: string,
358
+ input: FailDeploymentRequest
359
+ ): Promise<{ status: string; deploymentId: string }> {
360
+ return this.request<{ status: string; deploymentId: string }>(
361
+ 'POST',
362
+ `/studio/deployments/${id}/fail`,
363
+ input
364
+ );
365
+ }
366
+ }
367
+
368
+ /**
369
+ * BFF Client - main entry point for BFF API operations
370
+ */
371
+ export interface BFFClient {
372
+ /** Projects API */
373
+ readonly projects: ProjectsAPI;
374
+
375
+ /** Deployments API */
376
+ readonly deployments: DeploymentsAPI;
377
+ }
378
+
379
+ /**
380
+ * Create a new BFF client
381
+ *
382
+ * @param config - Client configuration
383
+ * @returns BFFClient instance
384
+ *
385
+ * @example
386
+ * ```typescript
387
+ * const client = createBFFClient({
388
+ * baseUrl: 'http://localhost:11001',
389
+ * authToken: 'your-supabase-jwt',
390
+ * });
391
+ *
392
+ * // Create a project
393
+ * const project = await client.projects.create({
394
+ * name: 'My App',
395
+ * slug: 'my-app',
396
+ * rootPath: '/Users/me/projects/my-app',
397
+ * });
398
+ *
399
+ * // Start a deployment
400
+ * const deployment = await client.deployments.start({
401
+ * projectId: project.id,
402
+ * });
403
+ * ```
404
+ */
405
+ export function createBFFClient(config: BFFClientConfig): BFFClient {
406
+ const timeout = config.timeout ?? 30000;
407
+
408
+ return {
409
+ projects: new ProjectsAPI(config.baseUrl, config.authToken, timeout),
410
+ deployments: new DeploymentsAPI(config.baseUrl, config.authToken, timeout),
411
+ };
412
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * BFF module - API client for BFF Studio endpoints
3
+ *
4
+ * Use this for all write operations (create, update, delete).
5
+ * The BFF uses SQLAlchemy with service_role credentials, which bypasses RLS.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { createBFFClient } from '@agent-foundry/studio/bff';
10
+ *
11
+ * const client = createBFFClient({
12
+ * baseUrl: 'http://localhost:11001',
13
+ * authToken: 'your-supabase-jwt',
14
+ * });
15
+ *
16
+ * // Create a project (write operation via BFF)
17
+ * const project = await client.projects.create({
18
+ * name: 'My App',
19
+ * slug: 'my-app',
20
+ * rootPath: '/path/to/project',
21
+ * });
22
+ * ```
23
+ */
24
+
25
+ export * from './types';
26
+ export {
27
+ createBFFClient,
28
+ BFFAPIError,
29
+ ProjectsAPI,
30
+ DeploymentsAPI,
31
+ } from './client';
32
+ export type { BFFClient } from './client';
@@ -0,0 +1,212 @@
1
+ /**
2
+ * BFF API Types
3
+ *
4
+ * Response types from the BFF Studio API endpoints.
5
+ * These match the Pydantic models in services/bff/app/models/studio.py
6
+ */
7
+
8
+ import type {
9
+ StudioProject,
10
+ ProjectConfig,
11
+ ProjectFramework,
12
+ } from '../types/project';
13
+ import type {
14
+ DeploymentStatus,
15
+ DeploymentMetadata,
16
+ AppManifest,
17
+ } from '../types/deployment';
18
+
19
+ // =============================================================================
20
+ // Project API Types
21
+ // =============================================================================
22
+
23
+ /**
24
+ * Request body for creating a project via BFF
25
+ */
26
+ export interface CreateProjectRequest {
27
+ name: string;
28
+ slug: string;
29
+ description?: string;
30
+ rootPath: string;
31
+ framework?: ProjectFramework;
32
+ config?: ProjectConfig;
33
+ parentProjectId?: string;
34
+ }
35
+
36
+ /**
37
+ * Request body for updating a project via BFF
38
+ */
39
+ export interface UpdateProjectRequest {
40
+ name?: string;
41
+ description?: string;
42
+ config?: Partial<ProjectConfig>;
43
+ }
44
+
45
+ /**
46
+ * Request body for forking a project via BFF
47
+ */
48
+ export interface ForkProjectRequest {
49
+ newSlug: string;
50
+ newRootPath: string;
51
+ newName?: string;
52
+ }
53
+
54
+ /**
55
+ * Project response from BFF API
56
+ */
57
+ export interface ProjectResponse extends StudioProject {
58
+ latestDeployment?: DeploymentSummary;
59
+ }
60
+
61
+ /**
62
+ * Paginated project list response
63
+ */
64
+ export interface ProjectListResponse {
65
+ projects: ProjectResponse[];
66
+ total: number;
67
+ page: number;
68
+ pageSize: number;
69
+ }
70
+
71
+ // =============================================================================
72
+ // Deployment API Types
73
+ // =============================================================================
74
+
75
+ /**
76
+ * Deployment summary (for lists)
77
+ */
78
+ export interface DeploymentSummary {
79
+ id: string;
80
+ version: string;
81
+ status: DeploymentStatus;
82
+ ossUrl?: string;
83
+ bundleSizeBytes?: number;
84
+ createdAt: string;
85
+ publishedAt?: string;
86
+ }
87
+
88
+ /**
89
+ * Request body for starting a deployment
90
+ */
91
+ export interface StartDeploymentRequest {
92
+ projectId: string;
93
+ version?: string;
94
+ metadata?: Partial<DeploymentMetadata>;
95
+ }
96
+
97
+ /**
98
+ * STS credentials for OSS upload
99
+ */
100
+ export interface STSCredentials {
101
+ accessKeyId: string;
102
+ accessKeySecret: string;
103
+ securityToken: string;
104
+ expiration: string;
105
+ }
106
+
107
+ /**
108
+ * Response from starting a deployment
109
+ */
110
+ export interface StartDeploymentResponse {
111
+ deploymentId: string;
112
+ credentials: STSCredentials;
113
+ bucket: string;
114
+ region: string;
115
+ keyPrefix: string;
116
+ }
117
+
118
+ /**
119
+ * Request body for updating deployment status
120
+ */
121
+ export interface UpdateDeploymentStatusRequest {
122
+ status: DeploymentStatus;
123
+ buildLog?: string;
124
+ errorMessage?: string;
125
+ metadata?: Partial<DeploymentMetadata>;
126
+ }
127
+
128
+ /**
129
+ * Request body for completing a deployment
130
+ */
131
+ export interface CompleteDeploymentRequest {
132
+ bucket: string;
133
+ keyPrefix: string;
134
+ ossUrl: string;
135
+ totalBytes: number;
136
+ fileCount: number;
137
+ }
138
+
139
+ /**
140
+ * Response from completing a deployment
141
+ */
142
+ export interface CompleteDeploymentResponse {
143
+ deploymentId: string;
144
+ ossUrl: string;
145
+ version: string;
146
+ }
147
+
148
+ /**
149
+ * Request body for failing a deployment
150
+ */
151
+ export interface FailDeploymentRequest {
152
+ errorMessage: string;
153
+ buildLog?: string;
154
+ }
155
+
156
+ /**
157
+ * Deployment list response
158
+ */
159
+ export interface DeploymentListResponse {
160
+ deployments: DeploymentSummary[];
161
+ total: number;
162
+ }
163
+
164
+ // =============================================================================
165
+ // Publish API Types
166
+ // =============================================================================
167
+
168
+ /**
169
+ * Request body for publishing to Feed
170
+ */
171
+ export interface PublishRequest {
172
+ deploymentId?: string;
173
+ manifest?: AppManifest;
174
+ status?: 'canary' | 'stable';
175
+ }
176
+
177
+ /**
178
+ * Response from publishing to Feed
179
+ */
180
+ export interface PublishResponse {
181
+ appId: string;
182
+ artifactId: string;
183
+ deploymentId: string;
184
+ shareUrl: string;
185
+ entryUrl: string;
186
+ }
187
+
188
+ // =============================================================================
189
+ // Error Types
190
+ // =============================================================================
191
+
192
+ /**
193
+ * API error response
194
+ */
195
+ export interface APIError {
196
+ detail: string;
197
+ code?: string;
198
+ }
199
+
200
+ /**
201
+ * BFF client configuration
202
+ */
203
+ export interface BFFClientConfig {
204
+ /** BFF base URL (e.g., "http://localhost:11001") */
205
+ baseUrl: string;
206
+
207
+ /** Supabase JWT token for authentication */
208
+ authToken: string;
209
+
210
+ /** Request timeout in milliseconds (default: 30000) */
211
+ timeout?: number;
212
+ }