@agent-foundry/studio 1.0.0

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.
Files changed (88) hide show
  1. package/README.md +96 -0
  2. package/dist/db/client.d.ts +59 -0
  3. package/dist/db/client.d.ts.map +1 -0
  4. package/dist/db/client.js +51 -0
  5. package/dist/db/client.js.map +1 -0
  6. package/dist/db/deployments.d.ts +65 -0
  7. package/dist/db/deployments.d.ts.map +1 -0
  8. package/dist/db/deployments.js +249 -0
  9. package/dist/db/deployments.js.map +1 -0
  10. package/dist/db/index.d.ts +7 -0
  11. package/dist/db/index.d.ts.map +1 -0
  12. package/dist/db/index.js +7 -0
  13. package/dist/db/index.js.map +1 -0
  14. package/dist/db/projects.d.ts +48 -0
  15. package/dist/db/projects.d.ts.map +1 -0
  16. package/dist/db/projects.js +192 -0
  17. package/dist/db/projects.js.map +1 -0
  18. package/dist/index.d.ts +11 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +15 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/oss/client.d.ts +65 -0
  23. package/dist/oss/client.d.ts.map +1 -0
  24. package/dist/oss/client.js +146 -0
  25. package/dist/oss/client.js.map +1 -0
  26. package/dist/oss/index.d.ts +7 -0
  27. package/dist/oss/index.d.ts.map +1 -0
  28. package/dist/oss/index.js +7 -0
  29. package/dist/oss/index.js.map +1 -0
  30. package/dist/oss/types.d.ts +96 -0
  31. package/dist/oss/types.d.ts.map +1 -0
  32. package/dist/oss/types.js +5 -0
  33. package/dist/oss/types.js.map +1 -0
  34. package/dist/oss/uploader.d.ts +72 -0
  35. package/dist/oss/uploader.d.ts.map +1 -0
  36. package/dist/oss/uploader.js +185 -0
  37. package/dist/oss/uploader.js.map +1 -0
  38. package/dist/types/deployment.d.ts +112 -0
  39. package/dist/types/deployment.d.ts.map +1 -0
  40. package/dist/types/deployment.js +7 -0
  41. package/dist/types/deployment.js.map +1 -0
  42. package/dist/types/index.d.ts +8 -0
  43. package/dist/types/index.d.ts.map +1 -0
  44. package/dist/types/index.js +8 -0
  45. package/dist/types/index.js.map +1 -0
  46. package/dist/types/project.d.ts +90 -0
  47. package/dist/types/project.d.ts.map +1 -0
  48. package/dist/types/project.js +8 -0
  49. package/dist/types/project.js.map +1 -0
  50. package/dist/types/user.d.ts +71 -0
  51. package/dist/types/user.d.ts.map +1 -0
  52. package/dist/types/user.js +8 -0
  53. package/dist/types/user.js.map +1 -0
  54. package/dist/types/workspace.d.ts +88 -0
  55. package/dist/types/workspace.d.ts.map +1 -0
  56. package/dist/types/workspace.js +27 -0
  57. package/dist/types/workspace.js.map +1 -0
  58. package/dist/utils/build.d.ts +78 -0
  59. package/dist/utils/build.d.ts.map +1 -0
  60. package/dist/utils/build.js +148 -0
  61. package/dist/utils/build.js.map +1 -0
  62. package/dist/utils/index.d.ts +6 -0
  63. package/dist/utils/index.d.ts.map +1 -0
  64. package/dist/utils/index.js +6 -0
  65. package/dist/utils/index.js.map +1 -0
  66. package/dist/utils/manifest.d.ts +106 -0
  67. package/dist/utils/manifest.d.ts.map +1 -0
  68. package/dist/utils/manifest.js +109 -0
  69. package/dist/utils/manifest.js.map +1 -0
  70. package/package.json +62 -0
  71. package/src/db/client.ts +92 -0
  72. package/src/db/deployments.ts +316 -0
  73. package/src/db/index.ts +7 -0
  74. package/src/db/projects.ts +246 -0
  75. package/src/db/schema.sql +156 -0
  76. package/src/index.ts +18 -0
  77. package/src/oss/client.ts +183 -0
  78. package/src/oss/index.ts +7 -0
  79. package/src/oss/types.ts +126 -0
  80. package/src/oss/uploader.ts +254 -0
  81. package/src/types/deployment.ts +147 -0
  82. package/src/types/index.ts +8 -0
  83. package/src/types/project.ts +114 -0
  84. package/src/types/user.ts +91 -0
  85. package/src/types/workspace.ts +124 -0
  86. package/src/utils/build.ts +199 -0
  87. package/src/utils/index.ts +6 -0
  88. package/src/utils/manifest.ts +224 -0
@@ -0,0 +1,316 @@
1
+ /**
2
+ * Deployments Repository
3
+ *
4
+ * CRUD operations for studio_deployments table.
5
+ */
6
+
7
+ import { SupabaseClient } from '@supabase/supabase-js';
8
+ import type {
9
+ Deployment,
10
+ CreateDeploymentInput,
11
+ UpdateDeploymentInput,
12
+ DeploymentListFilters,
13
+ DeploymentStatus,
14
+ DeploymentMetadata,
15
+ } from '../types/deployment';
16
+
17
+ /**
18
+ * Database row type (snake_case)
19
+ */
20
+ interface DeploymentRow {
21
+ id: string;
22
+ project_id: string;
23
+ user_id: string;
24
+ version: string;
25
+ status: DeploymentStatus;
26
+ oss_bucket: string | null;
27
+ oss_key: string | null;
28
+ oss_url: string | null;
29
+ bundle_size_bytes: number | null;
30
+ build_log: string | null;
31
+ error_message: string | null;
32
+ metadata: DeploymentMetadata;
33
+ created_at: string;
34
+ published_at: string | null;
35
+ }
36
+
37
+ /**
38
+ * Convert database row to Deployment
39
+ */
40
+ function rowToDeployment(row: DeploymentRow): Deployment {
41
+ return {
42
+ id: row.id,
43
+ projectId: row.project_id,
44
+ userId: row.user_id,
45
+ version: row.version,
46
+ status: row.status,
47
+ ossBucket: row.oss_bucket ?? undefined,
48
+ ossKey: row.oss_key ?? undefined,
49
+ ossUrl: row.oss_url ?? undefined,
50
+ bundleSizeBytes: row.bundle_size_bytes ?? undefined,
51
+ buildLog: row.build_log ?? undefined,
52
+ errorMessage: row.error_message ?? undefined,
53
+ metadata: row.metadata,
54
+ createdAt: row.created_at,
55
+ publishedAt: row.published_at ?? undefined,
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Generate a version string based on timestamp
61
+ */
62
+ function generateVersion(): string {
63
+ const now = new Date();
64
+ const year = now.getFullYear();
65
+ const month = String(now.getMonth() + 1).padStart(2, '0');
66
+ const day = String(now.getDate()).padStart(2, '0');
67
+ const hours = String(now.getHours()).padStart(2, '0');
68
+ const minutes = String(now.getMinutes()).padStart(2, '0');
69
+ return `${year}.${month}.${day}-${hours}${minutes}`;
70
+ }
71
+
72
+ /**
73
+ * Deployments Repository - manages studio_deployments table
74
+ */
75
+ export class DeploymentsRepository {
76
+ private readonly TABLE = 'studio_deployments';
77
+
78
+ constructor(private readonly supabase: SupabaseClient) {}
79
+
80
+ /**
81
+ * Create a new deployment
82
+ */
83
+ async create(input: CreateDeploymentInput): Promise<Deployment> {
84
+ const { data: { user } } = await this.supabase.auth.getUser();
85
+ if (!user) {
86
+ throw new Error('Not authenticated');
87
+ }
88
+
89
+ const version = input.version ?? generateVersion();
90
+
91
+ const { data, error } = await this.supabase
92
+ .from(this.TABLE)
93
+ .insert({
94
+ project_id: input.projectId,
95
+ user_id: user.id,
96
+ version,
97
+ status: 'pending',
98
+ metadata: input.metadata ?? {},
99
+ })
100
+ .select()
101
+ .single();
102
+
103
+ if (error) {
104
+ throw new Error(`Failed to create deployment: ${error.message}`);
105
+ }
106
+
107
+ return rowToDeployment(data as DeploymentRow);
108
+ }
109
+
110
+ /**
111
+ * Get a deployment by ID
112
+ */
113
+ async getById(id: string): Promise<Deployment | null> {
114
+ const { data, error } = await this.supabase
115
+ .from(this.TABLE)
116
+ .select()
117
+ .eq('id', id)
118
+ .single();
119
+
120
+ if (error) {
121
+ if (error.code === 'PGRST116') {
122
+ return null; // Not found
123
+ }
124
+ throw new Error(`Failed to get deployment: ${error.message}`);
125
+ }
126
+
127
+ return rowToDeployment(data as DeploymentRow);
128
+ }
129
+
130
+ /**
131
+ * List deployments with optional filters
132
+ */
133
+ async list(filters: DeploymentListFilters = {}): Promise<Deployment[]> {
134
+ let query = this.supabase.from(this.TABLE).select();
135
+
136
+ // Apply filters
137
+ if (filters.projectId) {
138
+ query = query.eq('project_id', filters.projectId);
139
+ }
140
+
141
+ if (filters.status) {
142
+ query = query.eq('status', filters.status);
143
+ }
144
+
145
+ // Ordering
146
+ const orderBy = filters.orderBy ?? 'createdAt';
147
+ const orderColumn = orderBy === 'createdAt' ? 'created_at' :
148
+ orderBy === 'publishedAt' ? 'published_at' : 'version';
149
+ const ascending = filters.orderDir === 'asc';
150
+ query = query.order(orderColumn, { ascending, nullsFirst: false });
151
+
152
+ // Pagination
153
+ if (filters.limit) {
154
+ query = query.limit(filters.limit);
155
+ }
156
+ if (filters.offset) {
157
+ query = query.range(filters.offset, filters.offset + (filters.limit ?? 50) - 1);
158
+ }
159
+
160
+ const { data, error } = await query;
161
+
162
+ if (error) {
163
+ throw new Error(`Failed to list deployments: ${error.message}`);
164
+ }
165
+
166
+ return (data as DeploymentRow[]).map(rowToDeployment);
167
+ }
168
+
169
+ /**
170
+ * Update a deployment
171
+ */
172
+ async update(id: string, input: UpdateDeploymentInput): Promise<Deployment> {
173
+ const updateData: Record<string, unknown> = {};
174
+
175
+ if (input.status !== undefined) {
176
+ updateData.status = input.status;
177
+ }
178
+ if (input.ossBucket !== undefined) {
179
+ updateData.oss_bucket = input.ossBucket;
180
+ }
181
+ if (input.ossKey !== undefined) {
182
+ updateData.oss_key = input.ossKey;
183
+ }
184
+ if (input.ossUrl !== undefined) {
185
+ updateData.oss_url = input.ossUrl;
186
+ }
187
+ if (input.bundleSizeBytes !== undefined) {
188
+ updateData.bundle_size_bytes = input.bundleSizeBytes;
189
+ }
190
+ if (input.buildLog !== undefined) {
191
+ updateData.build_log = input.buildLog;
192
+ }
193
+ if (input.errorMessage !== undefined) {
194
+ updateData.error_message = input.errorMessage;
195
+ }
196
+ if (input.metadata !== undefined) {
197
+ // Merge with existing metadata
198
+ const existing = await this.getById(id);
199
+ if (!existing) {
200
+ throw new Error('Deployment not found');
201
+ }
202
+ updateData.metadata = { ...existing.metadata, ...input.metadata };
203
+ }
204
+ if (input.publishedAt !== undefined) {
205
+ updateData.published_at = input.publishedAt;
206
+ }
207
+
208
+ const { data, error } = await this.supabase
209
+ .from(this.TABLE)
210
+ .update(updateData)
211
+ .eq('id', id)
212
+ .select()
213
+ .single();
214
+
215
+ if (error) {
216
+ throw new Error(`Failed to update deployment: ${error.message}`);
217
+ }
218
+
219
+ return rowToDeployment(data as DeploymentRow);
220
+ }
221
+
222
+ /**
223
+ * Mark deployment as building
224
+ */
225
+ async markBuilding(id: string): Promise<Deployment> {
226
+ return this.update(id, { status: 'building' });
227
+ }
228
+
229
+ /**
230
+ * Mark deployment as uploading
231
+ */
232
+ async markUploading(id: string): Promise<Deployment> {
233
+ return this.update(id, { status: 'uploading' });
234
+ }
235
+
236
+ /**
237
+ * Mark deployment as published
238
+ */
239
+ async markPublished(
240
+ id: string,
241
+ ossInfo: { bucket: string; key: string; url: string; sizeBytes: number }
242
+ ): Promise<Deployment> {
243
+ return this.update(id, {
244
+ status: 'published',
245
+ ossBucket: ossInfo.bucket,
246
+ ossKey: ossInfo.key,
247
+ ossUrl: ossInfo.url,
248
+ bundleSizeBytes: ossInfo.sizeBytes,
249
+ publishedAt: new Date().toISOString(),
250
+ });
251
+ }
252
+
253
+ /**
254
+ * Mark deployment as failed
255
+ */
256
+ async markFailed(id: string, errorMessage: string, buildLog?: string): Promise<Deployment> {
257
+ return this.update(id, {
258
+ status: 'failed',
259
+ errorMessage,
260
+ buildLog,
261
+ });
262
+ }
263
+
264
+ /**
265
+ * Get the latest successful deployment for a project
266
+ */
267
+ async getLatestPublished(projectId: string): Promise<Deployment | null> {
268
+ const { data, error } = await this.supabase
269
+ .from(this.TABLE)
270
+ .select()
271
+ .eq('project_id', projectId)
272
+ .eq('status', 'published')
273
+ .order('published_at', { ascending: false })
274
+ .limit(1)
275
+ .single();
276
+
277
+ if (error) {
278
+ if (error.code === 'PGRST116') {
279
+ return null; // Not found
280
+ }
281
+ throw new Error(`Failed to get latest deployment: ${error.message}`);
282
+ }
283
+
284
+ return rowToDeployment(data as DeploymentRow);
285
+ }
286
+
287
+ /**
288
+ * Delete a deployment
289
+ */
290
+ async delete(id: string): Promise<void> {
291
+ const { error } = await this.supabase
292
+ .from(this.TABLE)
293
+ .delete()
294
+ .eq('id', id);
295
+
296
+ if (error) {
297
+ throw new Error(`Failed to delete deployment: ${error.message}`);
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Append to build log
303
+ */
304
+ async appendBuildLog(id: string, logLine: string): Promise<void> {
305
+ const existing = await this.getById(id);
306
+ if (!existing) {
307
+ throw new Error('Deployment not found');
308
+ }
309
+
310
+ const newLog = existing.buildLog
311
+ ? `${existing.buildLog}\n${logLine}`
312
+ : logLine;
313
+
314
+ await this.update(id, { buildLog: newLog });
315
+ }
316
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Database module - Supabase client for Studio
3
+ */
4
+
5
+ export { createStudioClient, type StudioClientConfig } from './client';
6
+ export { ProjectsRepository } from './projects';
7
+ export { DeploymentsRepository } from './deployments';
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Projects Repository
3
+ *
4
+ * CRUD operations for studio_projects table.
5
+ */
6
+
7
+ import { SupabaseClient } from '@supabase/supabase-js';
8
+ import type {
9
+ StudioProject,
10
+ CreateProjectInput,
11
+ UpdateProjectInput,
12
+ ProjectListFilters,
13
+ ProjectConfig,
14
+ } from '../types/project';
15
+
16
+ /**
17
+ * Database row type (snake_case)
18
+ */
19
+ interface ProjectRow {
20
+ id: string;
21
+ user_id: string;
22
+ name: string;
23
+ slug: string;
24
+ description: string | null;
25
+ root_path: string;
26
+ framework: string;
27
+ config: ProjectConfig;
28
+ parent_project_id: string | null;
29
+ created_at: string;
30
+ updated_at: string;
31
+ }
32
+
33
+ /**
34
+ * Convert database row to StudioProject
35
+ */
36
+ function rowToProject(row: ProjectRow): StudioProject {
37
+ return {
38
+ id: row.id,
39
+ userId: row.user_id,
40
+ name: row.name,
41
+ slug: row.slug,
42
+ description: row.description ?? undefined,
43
+ rootPath: row.root_path,
44
+ framework: row.framework as StudioProject['framework'],
45
+ config: row.config,
46
+ parentProjectId: row.parent_project_id ?? undefined,
47
+ createdAt: row.created_at,
48
+ updatedAt: row.updated_at,
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Projects Repository - manages studio_projects table
54
+ */
55
+ export class ProjectsRepository {
56
+ private readonly TABLE = 'studio_projects';
57
+
58
+ constructor(private readonly supabase: SupabaseClient) {}
59
+
60
+ /**
61
+ * Create a new project
62
+ */
63
+ async create(input: CreateProjectInput): Promise<StudioProject> {
64
+ const { data: { user } } = await this.supabase.auth.getUser();
65
+ if (!user) {
66
+ throw new Error('Not authenticated');
67
+ }
68
+
69
+ const { data, error } = await this.supabase
70
+ .from(this.TABLE)
71
+ .insert({
72
+ user_id: user.id,
73
+ name: input.name,
74
+ slug: input.slug,
75
+ description: input.description,
76
+ root_path: input.rootPath,
77
+ framework: input.framework ?? 'vite-react',
78
+ config: input.config ?? {},
79
+ parent_project_id: input.parentProjectId,
80
+ })
81
+ .select()
82
+ .single();
83
+
84
+ if (error) {
85
+ throw new Error(`Failed to create project: ${error.message}`);
86
+ }
87
+
88
+ return rowToProject(data as ProjectRow);
89
+ }
90
+
91
+ /**
92
+ * Get a project by ID
93
+ */
94
+ async getById(id: string): Promise<StudioProject | null> {
95
+ const { data, error } = await this.supabase
96
+ .from(this.TABLE)
97
+ .select()
98
+ .eq('id', id)
99
+ .single();
100
+
101
+ if (error) {
102
+ if (error.code === 'PGRST116') {
103
+ return null; // Not found
104
+ }
105
+ throw new Error(`Failed to get project: ${error.message}`);
106
+ }
107
+
108
+ return rowToProject(data as ProjectRow);
109
+ }
110
+
111
+ /**
112
+ * Get a project by slug (for current user)
113
+ */
114
+ async getBySlug(slug: string): Promise<StudioProject | null> {
115
+ const { data, error } = await this.supabase
116
+ .from(this.TABLE)
117
+ .select()
118
+ .eq('slug', slug)
119
+ .single();
120
+
121
+ if (error) {
122
+ if (error.code === 'PGRST116') {
123
+ return null; // Not found
124
+ }
125
+ throw new Error(`Failed to get project: ${error.message}`);
126
+ }
127
+
128
+ return rowToProject(data as ProjectRow);
129
+ }
130
+
131
+ /**
132
+ * List projects with optional filters
133
+ */
134
+ async list(filters: ProjectListFilters = {}): Promise<StudioProject[]> {
135
+ let query = this.supabase.from(this.TABLE).select();
136
+
137
+ // Apply filters
138
+ if (filters.framework) {
139
+ query = query.eq('framework', filters.framework);
140
+ }
141
+
142
+ if (filters.search) {
143
+ query = query.ilike('name', `%${filters.search}%`);
144
+ }
145
+
146
+ // Ordering
147
+ const orderBy = filters.orderBy ?? 'createdAt';
148
+ const orderColumn = orderBy === 'createdAt' ? 'created_at' :
149
+ orderBy === 'updatedAt' ? 'updated_at' : 'name';
150
+ const ascending = filters.orderDir === 'asc';
151
+ query = query.order(orderColumn, { ascending });
152
+
153
+ // Pagination
154
+ if (filters.limit) {
155
+ query = query.limit(filters.limit);
156
+ }
157
+ if (filters.offset) {
158
+ query = query.range(filters.offset, filters.offset + (filters.limit ?? 50) - 1);
159
+ }
160
+
161
+ const { data, error } = await query;
162
+
163
+ if (error) {
164
+ throw new Error(`Failed to list projects: ${error.message}`);
165
+ }
166
+
167
+ return (data as ProjectRow[]).map(rowToProject);
168
+ }
169
+
170
+ /**
171
+ * Update a project
172
+ */
173
+ async update(id: string, input: UpdateProjectInput): Promise<StudioProject> {
174
+ const updateData: Record<string, unknown> = {};
175
+
176
+ if (input.name !== undefined) {
177
+ updateData.name = input.name;
178
+ }
179
+ if (input.description !== undefined) {
180
+ updateData.description = input.description;
181
+ }
182
+ if (input.config !== undefined) {
183
+ // Merge with existing config
184
+ const existing = await this.getById(id);
185
+ if (!existing) {
186
+ throw new Error('Project not found');
187
+ }
188
+ updateData.config = { ...existing.config, ...input.config };
189
+ }
190
+
191
+ const { data, error } = await this.supabase
192
+ .from(this.TABLE)
193
+ .update(updateData)
194
+ .eq('id', id)
195
+ .select()
196
+ .single();
197
+
198
+ if (error) {
199
+ throw new Error(`Failed to update project: ${error.message}`);
200
+ }
201
+
202
+ return rowToProject(data as ProjectRow);
203
+ }
204
+
205
+ /**
206
+ * Delete a project
207
+ */
208
+ async delete(id: string): Promise<void> {
209
+ const { error } = await this.supabase
210
+ .from(this.TABLE)
211
+ .delete()
212
+ .eq('id', id);
213
+
214
+ if (error) {
215
+ throw new Error(`Failed to delete project: ${error.message}`);
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Fork a project (create a copy)
221
+ */
222
+ async fork(id: string, newSlug: string, newRootPath: string): Promise<StudioProject> {
223
+ const original = await this.getById(id);
224
+ if (!original) {
225
+ throw new Error('Original project not found');
226
+ }
227
+
228
+ return this.create({
229
+ name: `${original.name} (Fork)`,
230
+ slug: newSlug,
231
+ description: original.description,
232
+ rootPath: newRootPath,
233
+ framework: original.framework,
234
+ config: original.config,
235
+ parentProjectId: original.id,
236
+ });
237
+ }
238
+
239
+ /**
240
+ * Check if a slug is available for current user
241
+ */
242
+ async isSlugAvailable(slug: string): Promise<boolean> {
243
+ const existing = await this.getBySlug(slug);
244
+ return existing === null;
245
+ }
246
+ }