@vibescope/mcp-server 0.0.1 → 0.1.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.
- package/README.md +113 -98
- package/dist/api-client.d.ts +1114 -0
- package/dist/api-client.js +698 -0
- package/dist/cli.d.ts +1 -6
- package/dist/cli.js +39 -240
- package/dist/config/tool-categories.d.ts +31 -0
- package/dist/config/tool-categories.js +253 -0
- package/dist/handlers/blockers.js +57 -58
- package/dist/handlers/bodies-of-work.d.ts +2 -0
- package/dist/handlers/bodies-of-work.js +106 -476
- package/dist/handlers/cost.d.ts +1 -0
- package/dist/handlers/cost.js +35 -113
- package/dist/handlers/decisions.d.ts +2 -0
- package/dist/handlers/decisions.js +28 -27
- package/dist/handlers/deployment.js +112 -828
- package/dist/handlers/discovery.js +31 -0
- package/dist/handlers/fallback.d.ts +2 -0
- package/dist/handlers/fallback.js +39 -134
- package/dist/handlers/findings.js +43 -67
- package/dist/handlers/git-issues.d.ts +9 -13
- package/dist/handlers/git-issues.js +80 -225
- package/dist/handlers/ideas.d.ts +3 -0
- package/dist/handlers/ideas.js +53 -134
- package/dist/handlers/index.d.ts +2 -0
- package/dist/handlers/index.js +6 -0
- package/dist/handlers/milestones.d.ts +2 -0
- package/dist/handlers/milestones.js +51 -98
- package/dist/handlers/organizations.js +79 -275
- package/dist/handlers/progress.d.ts +2 -0
- package/dist/handlers/progress.js +25 -123
- package/dist/handlers/project.js +42 -221
- package/dist/handlers/requests.d.ts +2 -0
- package/dist/handlers/requests.js +23 -83
- package/dist/handlers/session.js +99 -585
- package/dist/handlers/sprints.d.ts +32 -0
- package/dist/handlers/sprints.js +274 -0
- package/dist/handlers/tasks.d.ts +7 -10
- package/dist/handlers/tasks.js +230 -900
- package/dist/handlers/tool-docs.d.ts +8 -0
- package/dist/handlers/tool-docs.js +657 -0
- package/dist/handlers/types.d.ts +11 -3
- package/dist/handlers/validation.d.ts +1 -1
- package/dist/handlers/validation.js +26 -153
- package/dist/index.js +473 -160
- package/dist/knowledge.js +106 -9
- package/dist/tools.js +4 -0
- package/dist/validators.d.ts +21 -0
- package/dist/validators.js +91 -0
- package/package.json +2 -3
- package/src/api-client.ts +1752 -0
- package/src/cli.test.ts +128 -302
- package/src/cli.ts +41 -285
- package/src/handlers/__test-setup__.ts +210 -0
- package/src/handlers/__test-utils__.ts +4 -134
- package/src/handlers/blockers.test.ts +114 -124
- package/src/handlers/blockers.ts +68 -70
- package/src/handlers/bodies-of-work.test.ts +236 -831
- package/src/handlers/bodies-of-work.ts +194 -525
- package/src/handlers/cost.test.ts +149 -113
- package/src/handlers/cost.ts +44 -132
- package/src/handlers/decisions.test.ts +111 -209
- package/src/handlers/decisions.ts +35 -27
- package/src/handlers/deployment.test.ts +193 -239
- package/src/handlers/deployment.ts +140 -895
- package/src/handlers/discovery.test.ts +20 -67
- package/src/handlers/discovery.ts +32 -0
- package/src/handlers/fallback.test.ts +128 -361
- package/src/handlers/fallback.ts +62 -148
- package/src/handlers/findings.test.ts +127 -345
- package/src/handlers/findings.ts +49 -66
- package/src/handlers/git-issues.test.ts +623 -0
- package/src/handlers/git-issues.ts +174 -0
- package/src/handlers/ideas.test.ts +229 -343
- package/src/handlers/ideas.ts +69 -143
- package/src/handlers/index.ts +6 -0
- package/src/handlers/milestones.test.ts +167 -281
- package/src/handlers/milestones.ts +54 -93
- package/src/handlers/organizations.test.ts +275 -467
- package/src/handlers/organizations.ts +84 -294
- package/src/handlers/progress.test.ts +112 -218
- package/src/handlers/progress.ts +29 -142
- package/src/handlers/project.test.ts +203 -226
- package/src/handlers/project.ts +48 -238
- package/src/handlers/requests.test.ts +74 -342
- package/src/handlers/requests.ts +25 -83
- package/src/handlers/session.test.ts +241 -206
- package/src/handlers/session.ts +110 -657
- package/src/handlers/sprints.test.ts +711 -0
- package/src/handlers/sprints.ts +497 -0
- package/src/handlers/tasks.test.ts +608 -353
- package/src/handlers/tasks.ts +248 -1025
- package/src/handlers/types.ts +12 -4
- package/src/handlers/validation.test.ts +189 -572
- package/src/handlers/validation.ts +29 -166
- package/src/index.ts +473 -184
- package/src/knowledge.ts +107 -9
- package/src/tools.ts +2506 -0
- package/src/validators.test.ts +223 -223
- package/src/validators.ts +127 -0
- package/tsconfig.json +1 -1
- package/vitest.config.ts +14 -13
- package/dist/cli.test.d.ts +0 -1
- package/dist/cli.test.js +0 -367
- package/dist/handlers/__test-utils__.d.ts +0 -72
- package/dist/handlers/__test-utils__.js +0 -176
- package/dist/handlers/checkouts.d.ts +0 -37
- package/dist/handlers/checkouts.js +0 -377
- package/dist/handlers/knowledge-query.d.ts +0 -22
- package/dist/handlers/knowledge-query.js +0 -253
- package/dist/handlers/knowledge.d.ts +0 -12
- package/dist/handlers/knowledge.js +0 -108
- package/dist/handlers/roles.d.ts +0 -30
- package/dist/handlers/roles.js +0 -281
- package/dist/handlers/tasks.test.d.ts +0 -1
- package/dist/handlers/tasks.test.js +0 -431
- package/dist/utils.test.d.ts +0 -1
- package/dist/utils.test.js +0 -532
- package/dist/validators.test.d.ts +0 -1
- package/dist/validators.test.js +0 -176
- package/src/tmpclaude-0078-cwd +0 -1
- package/src/tmpclaude-0ee1-cwd +0 -1
- package/src/tmpclaude-2dd5-cwd +0 -1
- package/src/tmpclaude-344c-cwd +0 -1
- package/src/tmpclaude-3860-cwd +0 -1
- package/src/tmpclaude-4b63-cwd +0 -1
- package/src/tmpclaude-5c73-cwd +0 -1
- package/src/tmpclaude-5ee3-cwd +0 -1
- package/src/tmpclaude-6795-cwd +0 -1
- package/src/tmpclaude-709e-cwd +0 -1
- package/src/tmpclaude-9839-cwd +0 -1
- package/src/tmpclaude-d829-cwd +0 -1
- package/src/tmpclaude-e072-cwd +0 -1
- package/src/tmpclaude-f6ee-cwd +0 -1
- package/tmpclaude-0439-cwd +0 -1
- package/tmpclaude-132f-cwd +0 -1
- package/tmpclaude-15bb-cwd +0 -1
- package/tmpclaude-165a-cwd +0 -1
- package/tmpclaude-1ba9-cwd +0 -1
- package/tmpclaude-21a3-cwd +0 -1
- package/tmpclaude-2a38-cwd +0 -1
- package/tmpclaude-2adf-cwd +0 -1
- package/tmpclaude-2f56-cwd +0 -1
- package/tmpclaude-3626-cwd +0 -1
- package/tmpclaude-3727-cwd +0 -1
- package/tmpclaude-40bc-cwd +0 -1
- package/tmpclaude-436f-cwd +0 -1
- package/tmpclaude-4783-cwd +0 -1
- package/tmpclaude-4b6d-cwd +0 -1
- package/tmpclaude-4ba4-cwd +0 -1
- package/tmpclaude-51e6-cwd +0 -1
- package/tmpclaude-5ecf-cwd +0 -1
- package/tmpclaude-6f97-cwd +0 -1
- package/tmpclaude-7fb2-cwd +0 -1
- package/tmpclaude-825c-cwd +0 -1
- package/tmpclaude-8baf-cwd +0 -1
- package/tmpclaude-8d9f-cwd +0 -1
- package/tmpclaude-975c-cwd +0 -1
- package/tmpclaude-9983-cwd +0 -1
- package/tmpclaude-a045-cwd +0 -1
- package/tmpclaude-ac4a-cwd +0 -1
- package/tmpclaude-b593-cwd +0 -1
- package/tmpclaude-b891-cwd +0 -1
- package/tmpclaude-c032-cwd +0 -1
- package/tmpclaude-cf43-cwd +0 -1
- package/tmpclaude-d040-cwd +0 -1
- package/tmpclaude-dcdd-cwd +0 -1
- package/tmpclaude-dcee-cwd +0 -1
- package/tmpclaude-e16b-cwd +0 -1
- package/tmpclaude-ecd2-cwd +0 -1
- package/tmpclaude-f48d-cwd +0 -1
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
validateUUID,
|
|
22
22
|
validateEnvironment,
|
|
23
23
|
} from '../validators.js';
|
|
24
|
+
import { getApiClient } from '../api-client.js';
|
|
24
25
|
|
|
25
26
|
export const requestDeployment: Handler = async (args, ctx) => {
|
|
26
27
|
const { project_id, environment = 'production', version_bump = 'patch', notes, git_ref } = args as {
|
|
@@ -31,8 +32,7 @@ export const requestDeployment: Handler = async (args, ctx) => {
|
|
|
31
32
|
git_ref?: string;
|
|
32
33
|
};
|
|
33
34
|
|
|
34
|
-
const {
|
|
35
|
-
const currentSessionId = session.currentSessionId;
|
|
35
|
+
const { session } = ctx;
|
|
36
36
|
|
|
37
37
|
validateRequired(project_id, 'project_id');
|
|
38
38
|
validateUUID(project_id, 'project_id');
|
|
@@ -46,220 +46,39 @@ export const requestDeployment: Handler = async (args, ctx) => {
|
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (existingDeployment) {
|
|
58
|
-
return {
|
|
59
|
-
result: {
|
|
60
|
-
success: false,
|
|
61
|
-
error: 'A deployment is already in progress',
|
|
62
|
-
existing_deployment_id: existingDeployment.id,
|
|
63
|
-
existing_status: existingDeployment.status,
|
|
64
|
-
hint: 'Wait for the current deployment to complete or cancel it first',
|
|
65
|
-
},
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Check for unvalidated completed tasks
|
|
70
|
-
const { data: unvalidatedTasks } = await supabase
|
|
71
|
-
.from('tasks')
|
|
72
|
-
.select('id, title, completed_at, completed_by_session_id')
|
|
73
|
-
.eq('project_id', project_id)
|
|
74
|
-
.eq('status', 'completed')
|
|
75
|
-
.is('validated_at', null)
|
|
76
|
-
.order('completed_at', { ascending: true });
|
|
77
|
-
|
|
78
|
-
if (unvalidatedTasks && unvalidatedTasks.length > 0) {
|
|
79
|
-
return {
|
|
80
|
-
result: {
|
|
81
|
-
success: false,
|
|
82
|
-
error: 'Cannot deploy: There are unvalidated completed tasks',
|
|
83
|
-
unvalidated_tasks: unvalidatedTasks.map(t => ({
|
|
84
|
-
id: t.id,
|
|
85
|
-
title: t.title,
|
|
86
|
-
completed_at: t.completed_at,
|
|
87
|
-
})),
|
|
88
|
-
unvalidated_count: unvalidatedTasks.length,
|
|
89
|
-
hint: 'All completed tasks must be validated before deployment. Use validate_task to review each task.',
|
|
90
|
-
action: `Call validate_task(task_id: "${unvalidatedTasks[0].id}", approved: true/false, validation_notes: "...")`,
|
|
91
|
-
},
|
|
92
|
-
};
|
|
93
|
-
}
|
|
49
|
+
const apiClient = getApiClient();
|
|
50
|
+
const response = await apiClient.requestDeployment(project_id, {
|
|
51
|
+
environment,
|
|
52
|
+
version_bump,
|
|
53
|
+
notes,
|
|
54
|
+
git_ref
|
|
55
|
+
});
|
|
94
56
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
.from('projects')
|
|
98
|
-
.select('current_version')
|
|
99
|
-
.eq('id', project_id)
|
|
100
|
-
.single();
|
|
101
|
-
|
|
102
|
-
const currentVersion = project?.current_version || '0.0.0';
|
|
103
|
-
|
|
104
|
-
// Create new deployment
|
|
105
|
-
const { data: deployment, error } = await supabase
|
|
106
|
-
.from('deployments')
|
|
107
|
-
.insert({
|
|
108
|
-
project_id,
|
|
109
|
-
environment,
|
|
110
|
-
version_bump,
|
|
111
|
-
notes,
|
|
112
|
-
git_ref,
|
|
113
|
-
requested_by: 'agent',
|
|
114
|
-
requesting_agent_session_id: currentSessionId,
|
|
115
|
-
})
|
|
116
|
-
.select()
|
|
117
|
-
.single();
|
|
118
|
-
|
|
119
|
-
if (error) throw error;
|
|
120
|
-
|
|
121
|
-
// Auto-convert pending deployment requirements to tasks
|
|
122
|
-
const { data: pendingRequirements } = await supabase
|
|
123
|
-
.from('deployment_requirements')
|
|
124
|
-
.select('id, type, title, description, stage, blocking')
|
|
125
|
-
.eq('project_id', project_id)
|
|
126
|
-
.eq('status', 'pending')
|
|
127
|
-
.is('converted_task_id', null);
|
|
128
|
-
|
|
129
|
-
const convertedTasks: Array<{ task_id: string; requirement_id: string; title: string }> = [];
|
|
130
|
-
|
|
131
|
-
if (pendingRequirements && pendingRequirements.length > 0) {
|
|
132
|
-
for (const req of pendingRequirements) {
|
|
133
|
-
const isDeployStage = req.stage === 'deployment';
|
|
134
|
-
const isBlocking = req.blocking ?? isDeployStage;
|
|
135
|
-
const titlePrefix = isBlocking
|
|
136
|
-
? 'DEPLOY:'
|
|
137
|
-
: isDeployStage
|
|
138
|
-
? 'DEPLOY:'
|
|
139
|
-
: req.stage === 'verification'
|
|
140
|
-
? 'VERIFY:'
|
|
141
|
-
: 'PREP:';
|
|
142
|
-
|
|
143
|
-
// Create linked task
|
|
144
|
-
const { data: newTask } = await supabase
|
|
145
|
-
.from('tasks')
|
|
146
|
-
.insert({
|
|
147
|
-
project_id,
|
|
148
|
-
title: `${titlePrefix} ${req.title}`,
|
|
149
|
-
description: `[${req.type}] ${req.description || req.title}`,
|
|
150
|
-
priority: 1,
|
|
151
|
-
status: 'pending',
|
|
152
|
-
blocking: isBlocking,
|
|
153
|
-
created_by: 'agent',
|
|
154
|
-
created_by_session_id: currentSessionId,
|
|
155
|
-
})
|
|
156
|
-
.select('id')
|
|
157
|
-
.single();
|
|
158
|
-
|
|
159
|
-
if (newTask) {
|
|
160
|
-
// Link task to requirement WITHOUT changing status
|
|
161
|
-
// This keeps the requirement visible in the deployment steps list (permanent)
|
|
162
|
-
await supabase
|
|
163
|
-
.from('deployment_requirements')
|
|
164
|
-
.update({
|
|
165
|
-
converted_task_id: newTask.id,
|
|
166
|
-
})
|
|
167
|
-
.eq('id', req.id);
|
|
168
|
-
|
|
169
|
-
convertedTasks.push({
|
|
170
|
-
task_id: newTask.id,
|
|
171
|
-
requirement_id: req.id,
|
|
172
|
-
title: `${titlePrefix} ${req.title}`,
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
}
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
throw new Error(response.error || 'Failed to request deployment');
|
|
176
59
|
}
|
|
177
60
|
|
|
178
|
-
|
|
179
|
-
const convertedMsg = convertedTasks.length > 0
|
|
180
|
-
? ` (${convertedTasks.length} requirements converted to tasks)`
|
|
181
|
-
: '';
|
|
182
|
-
await supabase.from('progress_logs').insert({
|
|
183
|
-
project_id,
|
|
184
|
-
summary: `Deployment requested for ${environment} (${version_bump} bump from ${currentVersion})${convertedMsg}`,
|
|
185
|
-
details: notes || undefined,
|
|
186
|
-
created_by: 'agent',
|
|
187
|
-
created_by_session_id: currentSessionId,
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
return {
|
|
191
|
-
result: {
|
|
192
|
-
success: true,
|
|
193
|
-
deployment_id: deployment.id,
|
|
194
|
-
status: deployment.status,
|
|
195
|
-
environment: deployment.environment,
|
|
196
|
-
version_bump,
|
|
197
|
-
current_version: currentVersion,
|
|
198
|
-
converted_requirements: convertedTasks.length,
|
|
199
|
-
converted_tasks: convertedTasks.length > 0 ? convertedTasks : undefined,
|
|
200
|
-
message: convertedTasks.length > 0
|
|
201
|
-
? `Deployment created. ${convertedTasks.length} requirements converted to tasks. Run build/tests then call claim_deployment_validation.`
|
|
202
|
-
: 'Deployment created. Run build/tests then call claim_deployment_validation.',
|
|
203
|
-
},
|
|
204
|
-
};
|
|
61
|
+
return { result: response.data };
|
|
205
62
|
};
|
|
206
63
|
|
|
207
64
|
export const claimDeploymentValidation: Handler = async (args, ctx) => {
|
|
208
65
|
const { project_id } = args as { project_id: string };
|
|
209
|
-
const {
|
|
210
|
-
const currentSessionId = session.currentSessionId;
|
|
66
|
+
const { session } = ctx;
|
|
211
67
|
|
|
212
68
|
validateRequired(project_id, 'project_id');
|
|
213
69
|
validateUUID(project_id, 'project_id');
|
|
214
70
|
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
.
|
|
219
|
-
|
|
220
|
-
.eq('status', 'pending')
|
|
221
|
-
.single();
|
|
222
|
-
|
|
223
|
-
if (fetchError || !deployment) {
|
|
224
|
-
return {
|
|
225
|
-
result: {
|
|
226
|
-
success: false,
|
|
227
|
-
error: 'No pending deployment found',
|
|
228
|
-
hint: 'Use request_deployment to create a deployment first, or check_deployment_status to see current state',
|
|
229
|
-
},
|
|
230
|
-
};
|
|
231
|
-
}
|
|
71
|
+
const apiClient = getApiClient();
|
|
72
|
+
const response = await apiClient.claimDeploymentValidation(
|
|
73
|
+
project_id,
|
|
74
|
+
session.currentSessionId || undefined
|
|
75
|
+
);
|
|
232
76
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
.from('deployments')
|
|
236
|
-
.update({
|
|
237
|
-
status: 'validating',
|
|
238
|
-
validation_agent_session_id: currentSessionId,
|
|
239
|
-
validation_started_at: new Date().toISOString(),
|
|
240
|
-
})
|
|
241
|
-
.eq('id', deployment.id)
|
|
242
|
-
.eq('status', 'pending')
|
|
243
|
-
.select()
|
|
244
|
-
.single();
|
|
245
|
-
|
|
246
|
-
if (updateError || !updated) {
|
|
247
|
-
return {
|
|
248
|
-
result: {
|
|
249
|
-
success: false,
|
|
250
|
-
error: 'Failed to claim validation - deployment may have been claimed by another agent',
|
|
251
|
-
},
|
|
252
|
-
};
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
throw new Error(response.error || 'Failed to claim deployment validation');
|
|
253
79
|
}
|
|
254
80
|
|
|
255
|
-
return {
|
|
256
|
-
result: {
|
|
257
|
-
success: true,
|
|
258
|
-
deployment_id: deployment.id,
|
|
259
|
-
status: 'validating',
|
|
260
|
-
message: 'Validation claimed. Run build and tests, then call report_validation with results.',
|
|
261
|
-
},
|
|
262
|
-
};
|
|
81
|
+
return { result: response.data };
|
|
263
82
|
};
|
|
264
83
|
|
|
265
84
|
export const reportValidation: Handler = async (args, ctx) => {
|
|
@@ -270,8 +89,7 @@ export const reportValidation: Handler = async (args, ctx) => {
|
|
|
270
89
|
error_message?: string;
|
|
271
90
|
};
|
|
272
91
|
|
|
273
|
-
const {
|
|
274
|
-
const currentSessionId = session.currentSessionId;
|
|
92
|
+
const { session } = ctx;
|
|
275
93
|
|
|
276
94
|
validateRequired(project_id, 'project_id');
|
|
277
95
|
validateUUID(project_id, 'project_id');
|
|
@@ -282,232 +100,54 @@ export const reportValidation: Handler = async (args, ctx) => {
|
|
|
282
100
|
});
|
|
283
101
|
}
|
|
284
102
|
|
|
285
|
-
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
.eq('status', 'validating')
|
|
291
|
-
.single();
|
|
292
|
-
|
|
293
|
-
if (fetchError || !deployment) {
|
|
294
|
-
return {
|
|
295
|
-
result: {
|
|
296
|
-
success: false,
|
|
297
|
-
error: 'No deployment being validated. Use claim_deployment_validation first.',
|
|
298
|
-
},
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const validationPassed = build_passed && (tests_passed !== false);
|
|
303
|
-
const newStatus = validationPassed ? 'ready' : 'failed';
|
|
304
|
-
|
|
305
|
-
const { error: updateError } = await supabase
|
|
306
|
-
.from('deployments')
|
|
307
|
-
.update({
|
|
308
|
-
status: newStatus,
|
|
309
|
-
build_passed,
|
|
310
|
-
tests_passed: tests_passed ?? null,
|
|
311
|
-
validation_completed_at: new Date().toISOString(),
|
|
312
|
-
validation_error: error_message || null,
|
|
313
|
-
})
|
|
314
|
-
.eq('id', deployment.id);
|
|
315
|
-
|
|
316
|
-
if (updateError) throw updateError;
|
|
317
|
-
|
|
318
|
-
// Log result
|
|
319
|
-
await supabase.from('progress_logs').insert({
|
|
320
|
-
project_id,
|
|
321
|
-
summary: validationPassed
|
|
322
|
-
? `Deployment validation passed - ready to deploy`
|
|
323
|
-
: `Deployment validation failed: ${error_message || 'build/tests failed'}`,
|
|
324
|
-
details: `Build: ${build_passed ? 'passed' : 'failed'}, Tests: ${tests_passed === undefined ? 'skipped' : tests_passed ? 'passed' : 'failed'}`,
|
|
325
|
-
created_by: 'agent',
|
|
326
|
-
created_by_session_id: currentSessionId,
|
|
103
|
+
const apiClient = getApiClient();
|
|
104
|
+
const response = await apiClient.reportValidation(project_id, {
|
|
105
|
+
build_passed,
|
|
106
|
+
tests_passed: tests_passed ?? true,
|
|
107
|
+
error_message
|
|
327
108
|
});
|
|
328
109
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if (!validationPassed) {
|
|
332
|
-
const failureType = !build_passed ? 'build' : 'test';
|
|
333
|
-
const { data: newTask } = await supabase
|
|
334
|
-
.from('tasks')
|
|
335
|
-
.insert({
|
|
336
|
-
project_id,
|
|
337
|
-
title: `Fix ${failureType} failure`,
|
|
338
|
-
description: error_message || `${failureType} failed during deployment validation`,
|
|
339
|
-
priority: 1,
|
|
340
|
-
status: 'pending',
|
|
341
|
-
created_by: 'agent',
|
|
342
|
-
created_by_session_id: currentSessionId,
|
|
343
|
-
estimated_minutes: 30,
|
|
344
|
-
})
|
|
345
|
-
.select('id')
|
|
346
|
-
.single();
|
|
347
|
-
|
|
348
|
-
createdTaskId = newTask?.id || null;
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
throw new Error(response.error || 'Failed to report validation');
|
|
349
112
|
}
|
|
350
113
|
|
|
351
|
-
return {
|
|
352
|
-
result: {
|
|
353
|
-
success: true,
|
|
354
|
-
status: newStatus,
|
|
355
|
-
passed: validationPassed,
|
|
356
|
-
...(createdTaskId && { fix_task_id: createdTaskId }),
|
|
357
|
-
},
|
|
358
|
-
};
|
|
114
|
+
return { result: response.data };
|
|
359
115
|
};
|
|
360
116
|
|
|
361
117
|
export const checkDeploymentStatus: Handler = async (args, ctx) => {
|
|
362
118
|
const { project_id } = args as { project_id: string };
|
|
363
|
-
const { supabase } = ctx;
|
|
364
119
|
|
|
365
120
|
validateRequired(project_id, 'project_id');
|
|
366
121
|
validateUUID(project_id, 'project_id');
|
|
367
122
|
|
|
368
|
-
|
|
369
|
-
const
|
|
370
|
-
.from('deployments')
|
|
371
|
-
.select('*')
|
|
372
|
-
.eq('project_id', project_id)
|
|
373
|
-
.order('created_at', { ascending: false })
|
|
374
|
-
.limit(1)
|
|
375
|
-
.single();
|
|
376
|
-
|
|
377
|
-
if (error || !deployment) {
|
|
378
|
-
return {
|
|
379
|
-
result: {
|
|
380
|
-
has_deployment: false,
|
|
381
|
-
message: 'No deployments found for this project',
|
|
382
|
-
},
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Auto-timeout stale deployments
|
|
387
|
-
const DEPLOYMENT_TIMEOUT_MS: Record<string, number> = {
|
|
388
|
-
pending: 30 * 60 * 1000,
|
|
389
|
-
validating: 15 * 60 * 1000,
|
|
390
|
-
ready: 30 * 60 * 1000,
|
|
391
|
-
deploying: 10 * 60 * 1000,
|
|
392
|
-
};
|
|
123
|
+
const apiClient = getApiClient();
|
|
124
|
+
const response = await apiClient.checkDeploymentStatus(project_id);
|
|
393
125
|
|
|
394
|
-
if (!
|
|
395
|
-
|
|
396
|
-
if (timeout) {
|
|
397
|
-
const startTime =
|
|
398
|
-
deployment.status === 'deploying'
|
|
399
|
-
? deployment.deployment_started_at
|
|
400
|
-
: deployment.status === 'validating'
|
|
401
|
-
? deployment.validation_started_at
|
|
402
|
-
: deployment.created_at;
|
|
403
|
-
|
|
404
|
-
if (startTime && Date.now() - new Date(startTime).getTime() > timeout) {
|
|
405
|
-
const timeoutError = `Timed out: deployment was stuck in '${deployment.status}' state for too long`;
|
|
406
|
-
await supabase
|
|
407
|
-
.from('deployments')
|
|
408
|
-
.update({
|
|
409
|
-
status: 'failed',
|
|
410
|
-
deployment_error: timeoutError,
|
|
411
|
-
deployment_completed_at: new Date().toISOString(),
|
|
412
|
-
})
|
|
413
|
-
.eq('id', deployment.id);
|
|
414
|
-
|
|
415
|
-
deployment.status = 'failed';
|
|
416
|
-
deployment.deployment_error = timeoutError;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
throw new Error(response.error || 'Failed to check deployment status');
|
|
419
128
|
}
|
|
420
129
|
|
|
421
|
-
return {
|
|
422
|
-
result: {
|
|
423
|
-
has_deployment: true,
|
|
424
|
-
deployment: {
|
|
425
|
-
id: deployment.id,
|
|
426
|
-
status: deployment.status,
|
|
427
|
-
environment: deployment.environment,
|
|
428
|
-
requested_by: deployment.requested_by,
|
|
429
|
-
build_passed: deployment.build_passed,
|
|
430
|
-
tests_passed: deployment.tests_passed,
|
|
431
|
-
validation_error: deployment.validation_error,
|
|
432
|
-
deployment_error: deployment.deployment_error,
|
|
433
|
-
deployment_summary: deployment.deployment_summary,
|
|
434
|
-
notes: deployment.notes,
|
|
435
|
-
git_ref: deployment.git_ref,
|
|
436
|
-
created_at: deployment.created_at,
|
|
437
|
-
validation_started_at: deployment.validation_started_at,
|
|
438
|
-
validation_completed_at: deployment.validation_completed_at,
|
|
439
|
-
deployment_started_at: deployment.deployment_started_at,
|
|
440
|
-
deployment_completed_at: deployment.deployment_completed_at,
|
|
441
|
-
},
|
|
442
|
-
},
|
|
443
|
-
};
|
|
130
|
+
return { result: response.data };
|
|
444
131
|
};
|
|
445
132
|
|
|
446
133
|
export const startDeployment: Handler = async (args, ctx) => {
|
|
447
134
|
const { project_id } = args as { project_id: string };
|
|
448
|
-
const {
|
|
449
|
-
const currentSessionId = session.currentSessionId;
|
|
135
|
+
const { session } = ctx;
|
|
450
136
|
|
|
451
137
|
validateRequired(project_id, 'project_id');
|
|
452
138
|
validateUUID(project_id, 'project_id');
|
|
453
139
|
|
|
454
|
-
|
|
455
|
-
const
|
|
456
|
-
supabase
|
|
457
|
-
.from('deployments')
|
|
458
|
-
.select('id, environment')
|
|
459
|
-
.eq('project_id', project_id)
|
|
460
|
-
.eq('status', 'ready')
|
|
461
|
-
.single(),
|
|
462
|
-
supabase
|
|
463
|
-
.from('projects')
|
|
464
|
-
.select('deployment_instructions, git_main_branch')
|
|
465
|
-
.eq('id', project_id)
|
|
466
|
-
.single(),
|
|
467
|
-
]);
|
|
468
|
-
|
|
469
|
-
if (deploymentResult.error || !deploymentResult.data) {
|
|
470
|
-
return {
|
|
471
|
-
result: {
|
|
472
|
-
success: false,
|
|
473
|
-
error: 'No deployment ready. Must pass validation first.',
|
|
474
|
-
},
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
const deployment = deploymentResult.data;
|
|
479
|
-
const project = projectResult.data;
|
|
480
|
-
|
|
481
|
-
const { error: updateError } = await supabase
|
|
482
|
-
.from('deployments')
|
|
483
|
-
.update({
|
|
484
|
-
status: 'deploying',
|
|
485
|
-
deployment_started_at: new Date().toISOString(),
|
|
486
|
-
})
|
|
487
|
-
.eq('id', deployment.id);
|
|
488
|
-
|
|
489
|
-
if (updateError) throw updateError;
|
|
490
|
-
|
|
491
|
-
await supabase.from('progress_logs').insert({
|
|
140
|
+
const apiClient = getApiClient();
|
|
141
|
+
const response = await apiClient.startDeployment(
|
|
492
142
|
project_id,
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
created_by_session_id: currentSessionId,
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
const result: Record<string, unknown> = {
|
|
499
|
-
success: true,
|
|
500
|
-
status: 'deploying',
|
|
501
|
-
env: deployment.environment,
|
|
502
|
-
};
|
|
143
|
+
session.currentSessionId || undefined
|
|
144
|
+
);
|
|
503
145
|
|
|
504
|
-
if (
|
|
505
|
-
|
|
506
|
-
} else {
|
|
507
|
-
result.instructions = `No deployment instructions configured. Common steps:\n1. Push to ${project?.git_main_branch || 'main'} branch\n2. Or run your deploy command (e.g., fly deploy, vercel deploy)\n3. Call complete_deployment when done`;
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
throw new Error(response.error || 'Failed to start deployment');
|
|
508
148
|
}
|
|
509
149
|
|
|
510
|
-
return { result };
|
|
150
|
+
return { result: response.data };
|
|
511
151
|
};
|
|
512
152
|
|
|
513
153
|
export const completeDeployment: Handler = async (args, ctx) => {
|
|
@@ -517,8 +157,7 @@ export const completeDeployment: Handler = async (args, ctx) => {
|
|
|
517
157
|
summary?: string;
|
|
518
158
|
};
|
|
519
159
|
|
|
520
|
-
const {
|
|
521
|
-
const currentSessionId = session.currentSessionId;
|
|
160
|
+
const { session } = ctx;
|
|
522
161
|
|
|
523
162
|
validateRequired(project_id, 'project_id');
|
|
524
163
|
validateUUID(project_id, 'project_id');
|
|
@@ -529,122 +168,33 @@ export const completeDeployment: Handler = async (args, ctx) => {
|
|
|
529
168
|
});
|
|
530
169
|
}
|
|
531
170
|
|
|
532
|
-
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
.eq('status', 'deploying')
|
|
538
|
-
.single();
|
|
539
|
-
|
|
540
|
-
if (fetchError || !deployment) {
|
|
541
|
-
return {
|
|
542
|
-
result: {
|
|
543
|
-
success: false,
|
|
544
|
-
error: 'No deployment in progress. Use start_deployment first.',
|
|
545
|
-
},
|
|
546
|
-
};
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
const newStatus = success ? 'deployed' : 'failed';
|
|
550
|
-
let newVersion: string | null = null;
|
|
551
|
-
|
|
552
|
-
// If successful, calculate and store new version
|
|
553
|
-
if (success) {
|
|
554
|
-
const { data: project } = await supabase
|
|
555
|
-
.from('projects')
|
|
556
|
-
.select('current_version')
|
|
557
|
-
.eq('id', project_id)
|
|
558
|
-
.single();
|
|
559
|
-
|
|
560
|
-
const currentVersion = project?.current_version || '0.0.0';
|
|
561
|
-
const versionBump = deployment.version_bump || 'patch';
|
|
562
|
-
const parts = currentVersion.split('.').map((p: string) => parseInt(p, 10) || 0);
|
|
563
|
-
let [major, minor, patch] = [parts[0] || 0, parts[1] || 0, parts[2] || 0];
|
|
564
|
-
|
|
565
|
-
switch (versionBump) {
|
|
566
|
-
case 'major': major += 1; minor = 0; patch = 0; break;
|
|
567
|
-
case 'minor': minor += 1; patch = 0; break;
|
|
568
|
-
default: patch += 1;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
newVersion = `${major}.${minor}.${patch}`;
|
|
171
|
+
const apiClient = getApiClient();
|
|
172
|
+
const response = await apiClient.completeDeployment(project_id, {
|
|
173
|
+
success,
|
|
174
|
+
summary
|
|
175
|
+
});
|
|
572
176
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
.update({ current_version: newVersion })
|
|
576
|
-
.eq('id', project_id);
|
|
177
|
+
if (!response.ok) {
|
|
178
|
+
throw new Error(response.error || 'Failed to complete deployment');
|
|
577
179
|
}
|
|
578
180
|
|
|
579
|
-
|
|
580
|
-
.from('deployments')
|
|
581
|
-
.update({
|
|
582
|
-
status: newStatus,
|
|
583
|
-
version: newVersion,
|
|
584
|
-
deployment_completed_at: new Date().toISOString(),
|
|
585
|
-
deployment_summary: summary || null,
|
|
586
|
-
})
|
|
587
|
-
.eq('id', deployment.id);
|
|
588
|
-
|
|
589
|
-
if (updateError) throw updateError;
|
|
590
|
-
|
|
591
|
-
await supabase.from('progress_logs').insert({
|
|
592
|
-
project_id,
|
|
593
|
-
summary: success
|
|
594
|
-
? `Deployed to ${deployment.environment}${newVersion ? ` v${newVersion}` : ''}`
|
|
595
|
-
: `Deployment failed`,
|
|
596
|
-
details: summary || undefined,
|
|
597
|
-
created_by: 'agent',
|
|
598
|
-
created_by_session_id: currentSessionId,
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
return {
|
|
602
|
-
result: {
|
|
603
|
-
success: true,
|
|
604
|
-
status: newStatus,
|
|
605
|
-
...(newVersion && { version: newVersion }),
|
|
606
|
-
},
|
|
607
|
-
};
|
|
181
|
+
return { result: response.data };
|
|
608
182
|
};
|
|
609
183
|
|
|
610
184
|
export const cancelDeployment: Handler = async (args, ctx) => {
|
|
611
185
|
const { project_id, reason } = args as { project_id: string; reason?: string };
|
|
612
|
-
const { supabase, session } = ctx;
|
|
613
|
-
const currentSessionId = session.currentSessionId;
|
|
614
186
|
|
|
615
187
|
validateRequired(project_id, 'project_id');
|
|
616
188
|
validateUUID(project_id, 'project_id');
|
|
617
189
|
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
.select('id')
|
|
621
|
-
.eq('project_id', project_id)
|
|
622
|
-
.not('status', 'in', '("deployed","failed")')
|
|
623
|
-
.single();
|
|
190
|
+
const apiClient = getApiClient();
|
|
191
|
+
const response = await apiClient.cancelDeployment(project_id, reason);
|
|
624
192
|
|
|
625
|
-
if (
|
|
626
|
-
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
throw new Error(response.error || 'Failed to cancel deployment');
|
|
627
195
|
}
|
|
628
196
|
|
|
629
|
-
|
|
630
|
-
.from('deployments')
|
|
631
|
-
.update({
|
|
632
|
-
status: 'failed',
|
|
633
|
-
deployment_error: `Cancelled: ${reason || 'unspecified'}`,
|
|
634
|
-
deployment_completed_at: new Date().toISOString(),
|
|
635
|
-
})
|
|
636
|
-
.eq('id', deployment.id);
|
|
637
|
-
|
|
638
|
-
if (updateError) throw updateError;
|
|
639
|
-
|
|
640
|
-
await supabase.from('progress_logs').insert({
|
|
641
|
-
project_id,
|
|
642
|
-
summary: `Deployment cancelled${reason ? `: ${reason}` : ''}`,
|
|
643
|
-
created_by: 'agent',
|
|
644
|
-
created_by_session_id: currentSessionId,
|
|
645
|
-
});
|
|
646
|
-
|
|
647
|
-
return { result: { success: true } };
|
|
197
|
+
return { result: response.data };
|
|
648
198
|
};
|
|
649
199
|
|
|
650
200
|
export const addDeploymentRequirement: Handler = async (args, ctx) => {
|
|
@@ -658,9 +208,6 @@ export const addDeploymentRequirement: Handler = async (args, ctx) => {
|
|
|
658
208
|
blocking?: boolean;
|
|
659
209
|
};
|
|
660
210
|
|
|
661
|
-
const { supabase, session } = ctx;
|
|
662
|
-
const currentSessionId = session.currentSessionId;
|
|
663
|
-
|
|
664
211
|
validateRequired(project_id, 'project_id');
|
|
665
212
|
validateUUID(project_id, 'project_id');
|
|
666
213
|
validateRequired(type, 'type');
|
|
@@ -676,95 +223,37 @@ export const addDeploymentRequirement: Handler = async (args, ctx) => {
|
|
|
676
223
|
throw new ValidationError(`stage must be one of: ${validStages.join(', ')}`);
|
|
677
224
|
}
|
|
678
225
|
|
|
679
|
-
const
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
stage,
|
|
688
|
-
blocking,
|
|
689
|
-
created_by_session_id: currentSessionId,
|
|
690
|
-
})
|
|
691
|
-
.select('id, type, title, stage, blocking')
|
|
692
|
-
.single();
|
|
693
|
-
|
|
694
|
-
if (error) throw new Error(`Failed to add requirement: ${error.message}`);
|
|
695
|
-
|
|
696
|
-
const blockingText = blocking ? ' (BLOCKING)' : '';
|
|
697
|
-
await supabase.from('progress_logs').insert({
|
|
698
|
-
project_id,
|
|
699
|
-
summary: `Added ${stage} deployment requirement${blockingText}: ${title}`,
|
|
700
|
-
details: `Type: ${type}, Stage: ${stage}${blocking ? ', Blocking: true' : ''}${file_path ? `, File: ${file_path}` : ''}`,
|
|
701
|
-
created_by: 'agent',
|
|
702
|
-
created_by_session_id: currentSessionId,
|
|
226
|
+
const apiClient = getApiClient();
|
|
227
|
+
const response = await apiClient.addDeploymentRequirement(project_id, {
|
|
228
|
+
type: type as 'migration' | 'env_var' | 'config' | 'manual' | 'breaking_change' | 'agent_task',
|
|
229
|
+
title,
|
|
230
|
+
description,
|
|
231
|
+
file_path,
|
|
232
|
+
stage: stage as 'preparation' | 'deployment' | 'verification',
|
|
233
|
+
blocking
|
|
703
234
|
});
|
|
704
235
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
? 'Will run after deployment for verification.'
|
|
711
|
-
: 'Will run during preparation phase.';
|
|
712
|
-
|
|
713
|
-
return {
|
|
714
|
-
result: {
|
|
715
|
-
success: true,
|
|
716
|
-
requirement_id: requirement.id,
|
|
717
|
-
stage: requirement.stage,
|
|
718
|
-
message: `Added ${type} requirement. ${stageMessage}`,
|
|
719
|
-
},
|
|
720
|
-
};
|
|
236
|
+
if (!response.ok) {
|
|
237
|
+
throw new Error(response.error || 'Failed to add deployment requirement');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return { result: response.data };
|
|
721
241
|
};
|
|
722
242
|
|
|
723
243
|
export const completeDeploymentRequirement: Handler = async (args, ctx) => {
|
|
724
244
|
const { requirement_id } = args as { requirement_id: string };
|
|
725
|
-
const { supabase, session } = ctx;
|
|
726
|
-
const currentSessionId = session.currentSessionId;
|
|
727
245
|
|
|
728
246
|
validateRequired(requirement_id, 'requirement_id');
|
|
729
247
|
validateUUID(requirement_id, 'requirement_id');
|
|
730
248
|
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
.select('id, title, status')
|
|
734
|
-
.eq('id', requirement_id)
|
|
735
|
-
.single();
|
|
249
|
+
const apiClient = getApiClient();
|
|
250
|
+
const response = await apiClient.completeDeploymentRequirement(requirement_id);
|
|
736
251
|
|
|
737
|
-
if (
|
|
738
|
-
throw new Error('
|
|
252
|
+
if (!response.ok) {
|
|
253
|
+
throw new Error(response.error || 'Failed to complete deployment requirement');
|
|
739
254
|
}
|
|
740
255
|
|
|
741
|
-
|
|
742
|
-
return {
|
|
743
|
-
result: {
|
|
744
|
-
success: false,
|
|
745
|
-
error: `Requirement is already ${requirement.status}`,
|
|
746
|
-
},
|
|
747
|
-
};
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
const { error: updateError } = await supabase
|
|
751
|
-
.from('deployment_requirements')
|
|
752
|
-
.update({
|
|
753
|
-
status: 'completed',
|
|
754
|
-
completed_at: new Date().toISOString(),
|
|
755
|
-
completed_by: currentSessionId || 'agent',
|
|
756
|
-
})
|
|
757
|
-
.eq('id', requirement_id);
|
|
758
|
-
|
|
759
|
-
if (updateError) throw updateError;
|
|
760
|
-
|
|
761
|
-
return {
|
|
762
|
-
result: {
|
|
763
|
-
success: true,
|
|
764
|
-
requirement_id,
|
|
765
|
-
title: requirement.title,
|
|
766
|
-
},
|
|
767
|
-
};
|
|
256
|
+
return { result: response.data };
|
|
768
257
|
};
|
|
769
258
|
|
|
770
259
|
export const getDeploymentRequirements: Handler = async (args, ctx) => {
|
|
@@ -774,41 +263,20 @@ export const getDeploymentRequirements: Handler = async (args, ctx) => {
|
|
|
774
263
|
stage?: string;
|
|
775
264
|
};
|
|
776
265
|
|
|
777
|
-
const { supabase } = ctx;
|
|
778
|
-
|
|
779
266
|
validateRequired(project_id, 'project_id');
|
|
780
267
|
validateUUID(project_id, 'project_id');
|
|
781
268
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
.order('created_at', { ascending: false });
|
|
788
|
-
|
|
789
|
-
if (status !== 'all') {
|
|
790
|
-
query = query.eq('status', status);
|
|
791
|
-
}
|
|
269
|
+
const apiClient = getApiClient();
|
|
270
|
+
const response = await apiClient.getDeploymentRequirements(project_id, {
|
|
271
|
+
status: status as 'pending' | 'completed' | 'converted_to_task' | 'all',
|
|
272
|
+
stage: stage as 'preparation' | 'deployment' | 'verification' | 'all' | undefined
|
|
273
|
+
});
|
|
792
274
|
|
|
793
|
-
if (
|
|
794
|
-
|
|
275
|
+
if (!response.ok) {
|
|
276
|
+
throw new Error(response.error || 'Failed to get deployment requirements');
|
|
795
277
|
}
|
|
796
278
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
if (error) throw new Error(`Failed to fetch requirements: ${error.message}`);
|
|
800
|
-
|
|
801
|
-
const preparationPending = requirements?.filter(r => r.status === 'pending' && r.stage === 'preparation').length || 0;
|
|
802
|
-
const deploymentPending = requirements?.filter(r => r.status === 'pending' && r.stage === 'deployment').length || 0;
|
|
803
|
-
|
|
804
|
-
return {
|
|
805
|
-
result: {
|
|
806
|
-
requirements: requirements || [],
|
|
807
|
-
preparation_pending: preparationPending,
|
|
808
|
-
deployment_pending: deploymentPending,
|
|
809
|
-
deployment_blocked: preparationPending > 0 || deploymentPending > 0,
|
|
810
|
-
},
|
|
811
|
-
};
|
|
279
|
+
return { result: response.data };
|
|
812
280
|
};
|
|
813
281
|
|
|
814
282
|
// ============================================================================
|
|
@@ -836,9 +304,6 @@ export const scheduleDeployment: Handler = async (args, ctx) => {
|
|
|
836
304
|
git_ref?: string;
|
|
837
305
|
};
|
|
838
306
|
|
|
839
|
-
const { supabase, session } = ctx;
|
|
840
|
-
const currentSessionId = session.currentSessionId;
|
|
841
|
-
|
|
842
307
|
validateRequired(project_id, 'project_id');
|
|
843
308
|
validateUUID(project_id, 'project_id');
|
|
844
309
|
validateRequired(scheduled_at, 'scheduled_at');
|
|
@@ -873,47 +338,22 @@ export const scheduleDeployment: Handler = async (args, ctx) => {
|
|
|
873
338
|
});
|
|
874
339
|
}
|
|
875
340
|
|
|
876
|
-
|
|
877
|
-
const
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
auto_trigger,
|
|
886
|
-
notes: notes || null,
|
|
887
|
-
git_ref: git_ref || null,
|
|
888
|
-
created_by: 'agent',
|
|
889
|
-
created_by_session_id: currentSessionId,
|
|
890
|
-
})
|
|
891
|
-
.select('id, scheduled_at, schedule_type')
|
|
892
|
-
.single();
|
|
893
|
-
|
|
894
|
-
if (error) throw new Error(`Failed to create schedule: ${error.message}`);
|
|
895
|
-
|
|
896
|
-
// Log progress
|
|
897
|
-
await supabase.from('progress_logs').insert({
|
|
898
|
-
project_id,
|
|
899
|
-
summary: `Scheduled ${schedule_type} deployment to ${environment} for ${scheduledDate.toISOString()}`,
|
|
900
|
-
details: `Auto-trigger: ${auto_trigger}, Version bump: ${version_bump}`,
|
|
901
|
-
created_by: 'agent',
|
|
902
|
-
created_by_session_id: currentSessionId,
|
|
341
|
+
const apiClient = getApiClient();
|
|
342
|
+
const response = await apiClient.scheduleDeployment(project_id, {
|
|
343
|
+
environment: environment as 'development' | 'staging' | 'production',
|
|
344
|
+
version_bump: version_bump as 'patch' | 'minor' | 'major',
|
|
345
|
+
schedule_type: schedule_type as 'once' | 'daily' | 'weekly' | 'monthly',
|
|
346
|
+
scheduled_at: scheduledDate.toISOString(),
|
|
347
|
+
auto_trigger,
|
|
348
|
+
notes,
|
|
349
|
+
git_ref
|
|
903
350
|
});
|
|
904
351
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
schedule_type: schedule.schedule_type,
|
|
911
|
-
auto_trigger,
|
|
912
|
-
message: auto_trigger
|
|
913
|
-
? 'Deployment scheduled. Will trigger automatically when time arrives.'
|
|
914
|
-
: 'Deployment scheduled. Manual trigger required from dashboard.',
|
|
915
|
-
},
|
|
916
|
-
};
|
|
352
|
+
if (!response.ok) {
|
|
353
|
+
throw new Error(response.error || 'Failed to schedule deployment');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return { result: response.data };
|
|
917
357
|
};
|
|
918
358
|
|
|
919
359
|
export const getScheduledDeployments: Handler = async (args, ctx) => {
|
|
@@ -922,43 +362,17 @@ export const getScheduledDeployments: Handler = async (args, ctx) => {
|
|
|
922
362
|
include_disabled?: boolean;
|
|
923
363
|
};
|
|
924
364
|
|
|
925
|
-
const { supabase } = ctx;
|
|
926
|
-
|
|
927
365
|
validateRequired(project_id, 'project_id');
|
|
928
366
|
validateUUID(project_id, 'project_id');
|
|
929
367
|
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
.select('*')
|
|
933
|
-
.eq('project_id', project_id)
|
|
934
|
-
.order('scheduled_at', { ascending: true });
|
|
368
|
+
const apiClient = getApiClient();
|
|
369
|
+
const response = await apiClient.getScheduledDeployments(project_id, include_disabled);
|
|
935
370
|
|
|
936
|
-
if (!
|
|
937
|
-
|
|
371
|
+
if (!response.ok) {
|
|
372
|
+
throw new Error(response.error || 'Failed to get scheduled deployments');
|
|
938
373
|
}
|
|
939
374
|
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
if (error) throw new Error(`Failed to fetch schedules: ${error.message}`);
|
|
943
|
-
|
|
944
|
-
const now = new Date();
|
|
945
|
-
const schedulesWithStatus = (schedules || []).map(s => ({
|
|
946
|
-
...s,
|
|
947
|
-
is_due: s.enabled && new Date(s.scheduled_at) <= now,
|
|
948
|
-
}));
|
|
949
|
-
|
|
950
|
-
const dueCount = schedulesWithStatus.filter(s => s.is_due && s.auto_trigger).length;
|
|
951
|
-
|
|
952
|
-
return {
|
|
953
|
-
result: {
|
|
954
|
-
schedules: schedulesWithStatus,
|
|
955
|
-
count: schedulesWithStatus.length,
|
|
956
|
-
due_count: dueCount,
|
|
957
|
-
...(dueCount > 0 && {
|
|
958
|
-
hint: 'There are due schedules. Call trigger_scheduled_deployment to execute.',
|
|
959
|
-
}),
|
|
960
|
-
},
|
|
961
|
-
};
|
|
375
|
+
return { result: response.data };
|
|
962
376
|
};
|
|
963
377
|
|
|
964
378
|
export const updateScheduledDeployment: Handler = async (args, ctx) => {
|
|
@@ -984,8 +398,6 @@ export const updateScheduledDeployment: Handler = async (args, ctx) => {
|
|
|
984
398
|
git_ref?: string;
|
|
985
399
|
};
|
|
986
400
|
|
|
987
|
-
const { supabase } = ctx;
|
|
988
|
-
|
|
989
401
|
validateRequired(schedule_id, 'schedule_id');
|
|
990
402
|
validateUUID(schedule_id, 'schedule_id');
|
|
991
403
|
|
|
@@ -1027,242 +439,75 @@ export const updateScheduledDeployment: Handler = async (args, ctx) => {
|
|
|
1027
439
|
return { result: { success: false, error: 'No updates provided' } };
|
|
1028
440
|
}
|
|
1029
441
|
|
|
1030
|
-
const
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
442
|
+
const apiClient = getApiClient();
|
|
443
|
+
const response = await apiClient.updateScheduledDeployment(schedule_id, updates as {
|
|
444
|
+
environment?: 'development' | 'staging' | 'production';
|
|
445
|
+
version_bump?: 'patch' | 'minor' | 'major';
|
|
446
|
+
schedule_type?: 'once' | 'daily' | 'weekly' | 'monthly';
|
|
447
|
+
scheduled_at?: string;
|
|
448
|
+
auto_trigger?: boolean;
|
|
449
|
+
enabled?: boolean;
|
|
450
|
+
notes?: string;
|
|
451
|
+
git_ref?: string;
|
|
452
|
+
});
|
|
1034
453
|
|
|
1035
|
-
if (
|
|
454
|
+
if (!response.ok) {
|
|
455
|
+
throw new Error(response.error || 'Failed to update scheduled deployment');
|
|
456
|
+
}
|
|
1036
457
|
|
|
1037
|
-
return { result:
|
|
458
|
+
return { result: response.data };
|
|
1038
459
|
};
|
|
1039
460
|
|
|
1040
461
|
export const deleteScheduledDeployment: Handler = async (args, ctx) => {
|
|
1041
462
|
const { schedule_id } = args as { schedule_id: string };
|
|
1042
|
-
const { supabase } = ctx;
|
|
1043
463
|
|
|
1044
464
|
validateRequired(schedule_id, 'schedule_id');
|
|
1045
465
|
validateUUID(schedule_id, 'schedule_id');
|
|
1046
466
|
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1049
|
-
.delete()
|
|
1050
|
-
.eq('id', schedule_id);
|
|
467
|
+
const apiClient = getApiClient();
|
|
468
|
+
const response = await apiClient.deleteScheduledDeployment(schedule_id);
|
|
1051
469
|
|
|
1052
|
-
if (
|
|
470
|
+
if (!response.ok) {
|
|
471
|
+
throw new Error(response.error || 'Failed to delete scheduled deployment');
|
|
472
|
+
}
|
|
1053
473
|
|
|
1054
|
-
return { result:
|
|
474
|
+
return { result: response.data };
|
|
1055
475
|
};
|
|
1056
476
|
|
|
1057
477
|
export const triggerScheduledDeployment: Handler = async (args, ctx) => {
|
|
1058
478
|
const { schedule_id } = args as { schedule_id: string };
|
|
1059
|
-
const {
|
|
1060
|
-
const currentSessionId = session.currentSessionId;
|
|
479
|
+
const { session } = ctx;
|
|
1061
480
|
|
|
1062
481
|
validateRequired(schedule_id, 'schedule_id');
|
|
1063
482
|
validateUUID(schedule_id, 'schedule_id');
|
|
1064
483
|
|
|
1065
|
-
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1068
|
-
.
|
|
1069
|
-
|
|
1070
|
-
.single();
|
|
1071
|
-
|
|
1072
|
-
if (fetchError || !schedule) {
|
|
1073
|
-
return { result: { success: false, error: 'Schedule not found' } };
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
if (!schedule.enabled) {
|
|
1077
|
-
return { result: { success: false, error: 'Schedule is disabled' } };
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
// Check for existing active deployment
|
|
1081
|
-
const { data: existingDeployment } = await supabase
|
|
1082
|
-
.from('deployments')
|
|
1083
|
-
.select('id, status')
|
|
1084
|
-
.eq('project_id', schedule.project_id)
|
|
1085
|
-
.not('status', 'in', '("deployed","failed")')
|
|
1086
|
-
.single();
|
|
1087
|
-
|
|
1088
|
-
if (existingDeployment) {
|
|
1089
|
-
return {
|
|
1090
|
-
result: {
|
|
1091
|
-
success: false,
|
|
1092
|
-
error: 'A deployment is already in progress',
|
|
1093
|
-
existing_deployment_id: existingDeployment.id,
|
|
1094
|
-
hint: 'Wait for current deployment to complete or cancel it first',
|
|
1095
|
-
},
|
|
1096
|
-
};
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
// Create the deployment (similar to request_deployment)
|
|
1100
|
-
const { data: deployment, error: deployError } = await supabase
|
|
1101
|
-
.from('deployments')
|
|
1102
|
-
.insert({
|
|
1103
|
-
project_id: schedule.project_id,
|
|
1104
|
-
environment: schedule.environment,
|
|
1105
|
-
version_bump: schedule.version_bump,
|
|
1106
|
-
notes: schedule.notes,
|
|
1107
|
-
git_ref: schedule.git_ref,
|
|
1108
|
-
requested_by: 'agent',
|
|
1109
|
-
requesting_agent_session_id: currentSessionId,
|
|
1110
|
-
})
|
|
1111
|
-
.select('id, status')
|
|
1112
|
-
.single();
|
|
1113
|
-
|
|
1114
|
-
if (deployError) throw new Error(`Failed to create deployment: ${deployError.message}`);
|
|
1115
|
-
|
|
1116
|
-
// Auto-convert pending deployment requirements to tasks
|
|
1117
|
-
const { data: pendingRequirements } = await supabase
|
|
1118
|
-
.from('deployment_requirements')
|
|
1119
|
-
.select('id, type, title, description, stage, blocking')
|
|
1120
|
-
.eq('project_id', schedule.project_id)
|
|
1121
|
-
.eq('status', 'pending')
|
|
1122
|
-
.is('converted_task_id', null);
|
|
1123
|
-
|
|
1124
|
-
const convertedTasks: Array<{ task_id: string; requirement_id: string; title: string }> = [];
|
|
1125
|
-
|
|
1126
|
-
if (pendingRequirements && pendingRequirements.length > 0) {
|
|
1127
|
-
for (const req of pendingRequirements) {
|
|
1128
|
-
const isDeployStage = req.stage === 'deployment';
|
|
1129
|
-
const isBlocking = req.blocking ?? isDeployStage;
|
|
1130
|
-
const titlePrefix = isBlocking
|
|
1131
|
-
? 'DEPLOY:'
|
|
1132
|
-
: isDeployStage
|
|
1133
|
-
? 'DEPLOY:'
|
|
1134
|
-
: req.stage === 'verification'
|
|
1135
|
-
? 'VERIFY:'
|
|
1136
|
-
: 'PREP:';
|
|
1137
|
-
|
|
1138
|
-
// Create linked task
|
|
1139
|
-
const { data: newTask } = await supabase
|
|
1140
|
-
.from('tasks')
|
|
1141
|
-
.insert({
|
|
1142
|
-
project_id: schedule.project_id,
|
|
1143
|
-
title: `${titlePrefix} ${req.title}`,
|
|
1144
|
-
description: `[${req.type}] ${req.description || req.title}`,
|
|
1145
|
-
priority: 1,
|
|
1146
|
-
status: 'pending',
|
|
1147
|
-
blocking: isBlocking,
|
|
1148
|
-
created_by: 'agent',
|
|
1149
|
-
created_by_session_id: currentSessionId,
|
|
1150
|
-
})
|
|
1151
|
-
.select('id')
|
|
1152
|
-
.single();
|
|
1153
|
-
|
|
1154
|
-
if (newTask) {
|
|
1155
|
-
// Link task to requirement WITHOUT changing status
|
|
1156
|
-
// This keeps the requirement visible in the deployment steps list (permanent)
|
|
1157
|
-
await supabase
|
|
1158
|
-
.from('deployment_requirements')
|
|
1159
|
-
.update({
|
|
1160
|
-
converted_task_id: newTask.id,
|
|
1161
|
-
})
|
|
1162
|
-
.eq('id', req.id);
|
|
1163
|
-
|
|
1164
|
-
convertedTasks.push({
|
|
1165
|
-
task_id: newTask.id,
|
|
1166
|
-
requirement_id: req.id,
|
|
1167
|
-
title: `${titlePrefix} ${req.title}`,
|
|
1168
|
-
});
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
// Update the schedule
|
|
1174
|
-
const scheduleUpdates: Record<string, unknown> = {
|
|
1175
|
-
last_triggered_at: new Date().toISOString(),
|
|
1176
|
-
last_deployment_id: deployment.id,
|
|
1177
|
-
trigger_count: schedule.trigger_count + 1,
|
|
1178
|
-
};
|
|
1179
|
-
|
|
1180
|
-
// For recurring schedules, calculate next run time
|
|
1181
|
-
if (schedule.schedule_type !== 'once') {
|
|
1182
|
-
const currentScheduledAt = new Date(schedule.scheduled_at);
|
|
1183
|
-
let nextScheduledAt: Date;
|
|
1184
|
-
|
|
1185
|
-
switch (schedule.schedule_type) {
|
|
1186
|
-
case 'daily':
|
|
1187
|
-
nextScheduledAt = new Date(currentScheduledAt.getTime() + 24 * 60 * 60 * 1000);
|
|
1188
|
-
break;
|
|
1189
|
-
case 'weekly':
|
|
1190
|
-
nextScheduledAt = new Date(currentScheduledAt.getTime() + 7 * 24 * 60 * 60 * 1000);
|
|
1191
|
-
break;
|
|
1192
|
-
case 'monthly':
|
|
1193
|
-
nextScheduledAt = new Date(currentScheduledAt.getTime() + 30 * 24 * 60 * 60 * 1000);
|
|
1194
|
-
break;
|
|
1195
|
-
default:
|
|
1196
|
-
nextScheduledAt = currentScheduledAt;
|
|
1197
|
-
}
|
|
484
|
+
const apiClient = getApiClient();
|
|
485
|
+
const response = await apiClient.triggerScheduledDeployment(
|
|
486
|
+
schedule_id,
|
|
487
|
+
session.currentSessionId || undefined
|
|
488
|
+
);
|
|
1198
489
|
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
// One-time schedule, disable it
|
|
1202
|
-
scheduleUpdates.enabled = false;
|
|
490
|
+
if (!response.ok) {
|
|
491
|
+
throw new Error(response.error || 'Failed to trigger scheduled deployment');
|
|
1203
492
|
}
|
|
1204
493
|
|
|
1205
|
-
|
|
1206
|
-
.from('scheduled_deployments')
|
|
1207
|
-
.update(scheduleUpdates)
|
|
1208
|
-
.eq('id', schedule_id);
|
|
1209
|
-
|
|
1210
|
-
// Log progress
|
|
1211
|
-
const convertedMsg = convertedTasks.length > 0
|
|
1212
|
-
? `, ${convertedTasks.length} requirements converted to tasks`
|
|
1213
|
-
: '';
|
|
1214
|
-
await supabase.from('progress_logs').insert({
|
|
1215
|
-
project_id: schedule.project_id,
|
|
1216
|
-
summary: `Triggered scheduled deployment to ${schedule.environment}${convertedMsg}`,
|
|
1217
|
-
details: `Schedule: ${schedule.schedule_type}, Trigger #${schedule.trigger_count + 1}`,
|
|
1218
|
-
created_by: 'agent',
|
|
1219
|
-
created_by_session_id: currentSessionId,
|
|
1220
|
-
});
|
|
1221
|
-
|
|
1222
|
-
return {
|
|
1223
|
-
result: {
|
|
1224
|
-
success: true,
|
|
1225
|
-
deployment_id: deployment.id,
|
|
1226
|
-
schedule_id,
|
|
1227
|
-
schedule_type: schedule.schedule_type,
|
|
1228
|
-
next_scheduled_at: schedule.schedule_type !== 'once' ? scheduleUpdates.scheduled_at : null,
|
|
1229
|
-
converted_requirements: convertedTasks.length,
|
|
1230
|
-
converted_tasks: convertedTasks.length > 0 ? convertedTasks : undefined,
|
|
1231
|
-
message: convertedTasks.length > 0
|
|
1232
|
-
? `Deployment created from schedule. ${convertedTasks.length} requirements converted to tasks. Run validation then deploy.`
|
|
1233
|
-
: 'Deployment created from schedule. Run validation then deploy.',
|
|
1234
|
-
},
|
|
1235
|
-
};
|
|
494
|
+
return { result: response.data };
|
|
1236
495
|
};
|
|
1237
496
|
|
|
1238
497
|
export const checkDueDeployments: Handler = async (args, ctx) => {
|
|
1239
498
|
const { project_id } = args as { project_id: string };
|
|
1240
|
-
const { supabase } = ctx;
|
|
1241
499
|
|
|
1242
500
|
validateRequired(project_id, 'project_id');
|
|
1243
501
|
validateUUID(project_id, 'project_id');
|
|
1244
502
|
|
|
1245
|
-
|
|
1246
|
-
const
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
.
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
.order('scheduled_at', { ascending: true });
|
|
1254
|
-
|
|
1255
|
-
if (error) throw new Error(`Failed to check schedules: ${error.message}`);
|
|
1256
|
-
|
|
1257
|
-
return {
|
|
1258
|
-
result: {
|
|
1259
|
-
due_schedules: dueSchedules || [],
|
|
1260
|
-
count: dueSchedules?.length || 0,
|
|
1261
|
-
...(dueSchedules && dueSchedules.length > 0 && {
|
|
1262
|
-
hint: `Call trigger_scheduled_deployment(schedule_id: "${dueSchedules[0].id}") to trigger the first due deployment`,
|
|
1263
|
-
}),
|
|
1264
|
-
},
|
|
1265
|
-
};
|
|
503
|
+
const apiClient = getApiClient();
|
|
504
|
+
const response = await apiClient.checkDueDeployments(project_id);
|
|
505
|
+
|
|
506
|
+
if (!response.ok) {
|
|
507
|
+
throw new Error(response.error || 'Failed to check due deployments');
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return { result: response.data };
|
|
1266
511
|
};
|
|
1267
512
|
|
|
1268
513
|
/**
|