@doccov/api 0.3.5 → 0.3.7
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/.vercelignore +0 -3
- package/CHANGELOG.md +27 -0
- package/api/index.ts +894 -83
- package/package.json +5 -8
- package/src/routes/plan.ts +14 -8
- package/vercel.json +2 -6
- package/api/[...path].ts +0 -35
- package/bunup.config.ts +0 -16
- package/functions/execute-stream.ts +0 -273
- package/functions/execute.ts +0 -204
- package/functions/plan.ts +0 -104
- package/lib/plan-agent.ts +0 -252
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doccov/api",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "DocCov API - Badge endpoint and coverage services",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"doccov",
|
|
@@ -19,20 +19,18 @@
|
|
|
19
19
|
"author": "Ryan Waits",
|
|
20
20
|
"type": "module",
|
|
21
21
|
"scripts": {
|
|
22
|
-
"build": "bunup",
|
|
23
22
|
"dev": "bun run --hot src/index.ts",
|
|
24
23
|
"start": "bun run src/index.ts",
|
|
25
|
-
"lint": "biome check src/
|
|
26
|
-
"lint:fix": "biome check --write src/
|
|
27
|
-
"format": "biome format --write src/
|
|
24
|
+
"lint": "biome check src/ api/",
|
|
25
|
+
"lint:fix": "biome check --write src/ api/",
|
|
26
|
+
"format": "biome format --write src/ api/"
|
|
28
27
|
},
|
|
29
28
|
"dependencies": {
|
|
30
29
|
"@ai-sdk/anthropic": "^2.0.55",
|
|
31
|
-
"@doccov/sdk": "^0.
|
|
30
|
+
"@doccov/sdk": "^0.15.0",
|
|
32
31
|
"@openpkg-ts/spec": "^0.9.0",
|
|
33
32
|
"@vercel/sandbox": "^1.0.3",
|
|
34
33
|
"ai": "^5.0.111",
|
|
35
|
-
"hono": "^4.0.0",
|
|
36
34
|
"ms": "^2.1.3",
|
|
37
35
|
"zod": "^3.25.0"
|
|
38
36
|
},
|
|
@@ -41,7 +39,6 @@
|
|
|
41
39
|
"@types/ms": "^0.7.34",
|
|
42
40
|
"@types/node": "^20.0.0",
|
|
43
41
|
"@vercel/node": "^3.0.0",
|
|
44
|
-
"bunup": "latest",
|
|
45
42
|
"typescript": "^5.0.0"
|
|
46
43
|
}
|
|
47
44
|
}
|
package/src/routes/plan.ts
CHANGED
|
@@ -34,10 +34,13 @@ planRoute.post('/', async (c) => {
|
|
|
34
34
|
|
|
35
35
|
// Check for private repos
|
|
36
36
|
if (context.metadata.isPrivate) {
|
|
37
|
-
return c.json(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
return c.json(
|
|
38
|
+
{
|
|
39
|
+
error: 'Private repositories are not supported',
|
|
40
|
+
hint: 'Use a public repository or run doccov locally',
|
|
41
|
+
},
|
|
42
|
+
403,
|
|
43
|
+
);
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
// Generate build plan using AI
|
|
@@ -67,9 +70,12 @@ planRoute.post('/', async (c) => {
|
|
|
67
70
|
}
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
return c.json(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
return c.json(
|
|
74
|
+
{
|
|
75
|
+
error: 'Failed to generate build plan',
|
|
76
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
77
|
+
},
|
|
78
|
+
500,
|
|
79
|
+
);
|
|
74
80
|
}
|
|
75
81
|
});
|
package/vercel.json
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
+
"framework": null,
|
|
2
3
|
"installCommand": "bun install",
|
|
3
|
-
"
|
|
4
|
+
"outputDirectory": ".",
|
|
4
5
|
"rewrites": [
|
|
5
|
-
{ "source": "/badge/:path*", "destination": "/api" },
|
|
6
|
-
{ "source": "/spec/:path*", "destination": "/api" },
|
|
7
|
-
{ "source": "/plan", "destination": "/api/plan" },
|
|
8
|
-
{ "source": "/execute", "destination": "/api/execute" },
|
|
9
|
-
{ "source": "/execute-stream", "destination": "/api/execute-stream" },
|
|
10
6
|
{ "source": "/(.*)", "destination": "/api" }
|
|
11
7
|
]
|
|
12
8
|
}
|
package/api/[...path].ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Catch-all router for Vercel serverless functions.
|
|
3
|
-
* Routes requests to the appropriate bundled function handler.
|
|
4
|
-
*/
|
|
5
|
-
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
|
6
|
-
|
|
7
|
-
// Import bundled handlers (built by bunup)
|
|
8
|
-
import executeHandler from '../dist/functions/execute';
|
|
9
|
-
import executeStreamHandler from '../dist/functions/execute-stream';
|
|
10
|
-
import planHandler from '../dist/functions/plan';
|
|
11
|
-
|
|
12
|
-
export const config = {
|
|
13
|
-
runtime: 'nodejs',
|
|
14
|
-
maxDuration: 300,
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
type Handler = (req: VercelRequest, res: VercelResponse) => Promise<void | VercelResponse>;
|
|
18
|
-
|
|
19
|
-
const handlers: Record<string, Handler> = {
|
|
20
|
-
execute: executeHandler,
|
|
21
|
-
'execute-stream': executeStreamHandler,
|
|
22
|
-
plan: planHandler,
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
26
|
-
// Extract path from catch-all
|
|
27
|
-
const pathSegments = req.query.path;
|
|
28
|
-
const route = Array.isArray(pathSegments) ? pathSegments[0] : pathSegments;
|
|
29
|
-
|
|
30
|
-
if (!route || !handlers[route]) {
|
|
31
|
-
return res.status(404).json({ error: 'Not found', availableRoutes: Object.keys(handlers) });
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return handlers[route](req, res);
|
|
35
|
-
}
|
package/bunup.config.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from 'bunup';
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
entry: ['functions/execute.ts', 'functions/execute-stream.ts', 'functions/plan.ts'],
|
|
5
|
-
dts: false,
|
|
6
|
-
clean: true,
|
|
7
|
-
splitting: false,
|
|
8
|
-
format: ['esm'],
|
|
9
|
-
outDir: 'dist/functions',
|
|
10
|
-
external: [
|
|
11
|
-
'@vercel/node',
|
|
12
|
-
'@vercel/sandbox',
|
|
13
|
-
'@doccov/sdk',
|
|
14
|
-
'@openpkg-ts/spec',
|
|
15
|
-
],
|
|
16
|
-
});
|
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GET /execute-stream - Execute a build plan with SSE streaming progress
|
|
3
|
-
*
|
|
4
|
-
* Query params:
|
|
5
|
-
* - plan: Base64-encoded BuildPlan JSON (required)
|
|
6
|
-
*
|
|
7
|
-
* SSE Events:
|
|
8
|
-
* - step-start: { stepId, name }
|
|
9
|
-
* - step-progress: { stepId, output }
|
|
10
|
-
* - step-complete: { stepId, success, duration, error? }
|
|
11
|
-
* - complete: { success, spec?, totalDuration, error? }
|
|
12
|
-
* - error: { message }
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import type { BuildPlan, BuildPlanExecutionResult, BuildPlanStepResult } from '@doccov/sdk';
|
|
16
|
-
import type { OpenPkg } from '@openpkg-ts/spec';
|
|
17
|
-
import { Sandbox } from '@vercel/sandbox';
|
|
18
|
-
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
|
19
|
-
import ms from 'ms';
|
|
20
|
-
|
|
21
|
-
export const config = {
|
|
22
|
-
runtime: 'nodejs',
|
|
23
|
-
maxDuration: 300, // 5 minutes for full execution
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* SSE event types for execute streaming
|
|
28
|
-
*/
|
|
29
|
-
type ExecuteEvent =
|
|
30
|
-
| { type: 'step-start'; stepId: string; name: string }
|
|
31
|
-
| { type: 'step-progress'; stepId: string; output: string }
|
|
32
|
-
| { type: 'step-complete'; stepId: string; success: boolean; duration: number; error?: string }
|
|
33
|
-
| { type: 'complete'; success: boolean; spec?: OpenPkg; totalDuration: number; error?: string }
|
|
34
|
-
| { type: 'error'; message: string };
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Map runtime to Vercel sandbox runtime
|
|
38
|
-
*/
|
|
39
|
-
function getSandboxRuntime(runtime: string): 'node22' | 'node24' {
|
|
40
|
-
if (runtime === 'node24') return 'node24';
|
|
41
|
-
return 'node22'; // Default to node22 for everything else
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
45
|
-
// CORS
|
|
46
|
-
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
47
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
|
48
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
49
|
-
|
|
50
|
-
if (req.method === 'OPTIONS') {
|
|
51
|
-
return res.status(200).end();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (req.method !== 'GET') {
|
|
55
|
-
return res.status(405).json({ error: 'Method not allowed' });
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Get plan from query string (base64 encoded)
|
|
59
|
-
const planBase64 = req.query.plan as string;
|
|
60
|
-
|
|
61
|
-
if (!planBase64) {
|
|
62
|
-
return res.status(400).json({ error: 'plan query param is required (base64 encoded JSON)' });
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
let plan: BuildPlan;
|
|
66
|
-
try {
|
|
67
|
-
const planJson = Buffer.from(planBase64, 'base64').toString('utf-8');
|
|
68
|
-
plan = JSON.parse(planJson) as BuildPlan;
|
|
69
|
-
} catch {
|
|
70
|
-
return res.status(400).json({ error: 'Invalid plan: must be valid base64-encoded JSON' });
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Validate plan structure
|
|
74
|
-
if (!plan.target?.repoUrl || !plan.steps) {
|
|
75
|
-
return res.status(400).json({ error: 'Invalid plan structure' });
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Set SSE headers
|
|
79
|
-
res.setHeader('Content-Type', 'text/event-stream');
|
|
80
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
81
|
-
res.setHeader('Connection', 'keep-alive');
|
|
82
|
-
|
|
83
|
-
// Send initial comment
|
|
84
|
-
res.write(':ok\n\n');
|
|
85
|
-
|
|
86
|
-
// Helper to send SSE event
|
|
87
|
-
const sendEvent = (event: ExecuteEvent) => {
|
|
88
|
-
const data = JSON.stringify(event);
|
|
89
|
-
res.write(`data: ${data}\n\n`);
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
// Run execution with streaming progress
|
|
93
|
-
await runExecuteWithProgress(plan, sendEvent);
|
|
94
|
-
|
|
95
|
-
res.end();
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async function runExecuteWithProgress(
|
|
99
|
-
plan: BuildPlan,
|
|
100
|
-
sendEvent: (event: ExecuteEvent) => void,
|
|
101
|
-
): Promise<void> {
|
|
102
|
-
const startTime = Date.now();
|
|
103
|
-
const stepResults: BuildPlanStepResult[] = [];
|
|
104
|
-
let sandbox: Awaited<ReturnType<typeof Sandbox.create>> | null = null;
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
// Create sandbox with git source
|
|
108
|
-
sandbox = await Sandbox.create({
|
|
109
|
-
source: {
|
|
110
|
-
url: plan.target.repoUrl,
|
|
111
|
-
type: 'git',
|
|
112
|
-
},
|
|
113
|
-
resources: { vcpus: 4 },
|
|
114
|
-
timeout: ms('5m'),
|
|
115
|
-
runtime: getSandboxRuntime(plan.environment.runtime),
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
// Execute each step
|
|
119
|
-
for (const step of plan.steps) {
|
|
120
|
-
const stepStart = Date.now();
|
|
121
|
-
|
|
122
|
-
sendEvent({
|
|
123
|
-
type: 'step-start',
|
|
124
|
-
stepId: step.id,
|
|
125
|
-
name: step.name,
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
const result = await sandbox.runCommand({
|
|
130
|
-
cmd: step.command,
|
|
131
|
-
args: step.args,
|
|
132
|
-
cwd: step.cwd,
|
|
133
|
-
timeout: step.timeout ?? 60000,
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// stdout/stderr are async functions in Vercel Sandbox
|
|
137
|
-
const stdout = result.stdout ? await result.stdout() : '';
|
|
138
|
-
const stderr = result.stderr ? await result.stderr() : '';
|
|
139
|
-
|
|
140
|
-
const success = result.exitCode === 0;
|
|
141
|
-
const duration = Date.now() - stepStart;
|
|
142
|
-
|
|
143
|
-
stepResults.push({
|
|
144
|
-
stepId: step.id,
|
|
145
|
-
success,
|
|
146
|
-
duration,
|
|
147
|
-
output: stdout.slice(0, 5000),
|
|
148
|
-
error: !success ? stderr.slice(0, 2000) : undefined,
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
sendEvent({
|
|
152
|
-
type: 'step-complete',
|
|
153
|
-
stepId: step.id,
|
|
154
|
-
success,
|
|
155
|
-
duration,
|
|
156
|
-
error: !success ? stderr.slice(0, 500) : undefined,
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
// Stop on non-optional failures
|
|
160
|
-
if (!success && !step.optional) {
|
|
161
|
-
throw new Error(`Step '${step.name}' failed: ${stderr.slice(0, 500)}`);
|
|
162
|
-
}
|
|
163
|
-
} catch (stepError) {
|
|
164
|
-
const duration = Date.now() - stepStart;
|
|
165
|
-
const errorMsg = stepError instanceof Error ? stepError.message : 'Unknown error';
|
|
166
|
-
|
|
167
|
-
stepResults.push({
|
|
168
|
-
stepId: step.id,
|
|
169
|
-
success: false,
|
|
170
|
-
duration,
|
|
171
|
-
error: errorMsg,
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
sendEvent({
|
|
175
|
-
type: 'step-complete',
|
|
176
|
-
stepId: step.id,
|
|
177
|
-
success: false,
|
|
178
|
-
duration,
|
|
179
|
-
error: errorMsg.slice(0, 500),
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
if (!step.optional) {
|
|
183
|
-
throw stepError;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Run doccov spec to generate the OpenPkg spec
|
|
189
|
-
const analyzeStep = {
|
|
190
|
-
id: 'analyze',
|
|
191
|
-
name: 'Generate OpenPkg spec',
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
sendEvent({
|
|
195
|
-
type: 'step-start',
|
|
196
|
-
stepId: analyzeStep.id,
|
|
197
|
-
name: analyzeStep.name,
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
const specStart = Date.now();
|
|
201
|
-
const entryPoint = plan.target.entryPoints[0] ?? 'src/index.ts';
|
|
202
|
-
|
|
203
|
-
// Install doccov CLI first
|
|
204
|
-
await sandbox.runCommand({
|
|
205
|
-
cmd: 'npm',
|
|
206
|
-
args: ['install', '-g', '@doccov/cli@latest'],
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// Run spec generation
|
|
210
|
-
const specResult = await sandbox.runCommand({
|
|
211
|
-
cmd: 'doccov',
|
|
212
|
-
args: ['spec', entryPoint, '--output', 'openpkg.json'],
|
|
213
|
-
cwd: plan.target.rootPath,
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
const specStdout = specResult.stdout ? await specResult.stdout() : '';
|
|
217
|
-
const specStderr = specResult.stderr ? await specResult.stderr() : '';
|
|
218
|
-
const specDuration = Date.now() - specStart;
|
|
219
|
-
|
|
220
|
-
stepResults.push({
|
|
221
|
-
stepId: 'analyze',
|
|
222
|
-
success: specResult.exitCode === 0,
|
|
223
|
-
duration: specDuration,
|
|
224
|
-
output: specStdout.slice(0, 2000),
|
|
225
|
-
error: specResult.exitCode !== 0 ? specStderr.slice(0, 2000) : undefined,
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
sendEvent({
|
|
229
|
-
type: 'step-complete',
|
|
230
|
-
stepId: 'analyze',
|
|
231
|
-
success: specResult.exitCode === 0,
|
|
232
|
-
duration: specDuration,
|
|
233
|
-
error: specResult.exitCode !== 0 ? specStderr.slice(0, 500) : undefined,
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
if (specResult.exitCode !== 0) {
|
|
237
|
-
throw new Error(`Spec generation failed: ${specStderr.slice(0, 500)}`);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Read the generated spec - readFile returns a readable stream
|
|
241
|
-
const specStream = await sandbox.readFile({ path: 'openpkg.json' });
|
|
242
|
-
const chunks: Buffer[] = [];
|
|
243
|
-
for await (const chunk of specStream as AsyncIterable<Buffer>) {
|
|
244
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
245
|
-
}
|
|
246
|
-
const specContent = Buffer.concat(chunks).toString('utf-8');
|
|
247
|
-
const spec = JSON.parse(specContent) as OpenPkg;
|
|
248
|
-
|
|
249
|
-
sendEvent({
|
|
250
|
-
type: 'complete',
|
|
251
|
-
success: true,
|
|
252
|
-
spec,
|
|
253
|
-
totalDuration: Date.now() - startTime,
|
|
254
|
-
});
|
|
255
|
-
} catch (error) {
|
|
256
|
-
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
257
|
-
|
|
258
|
-
sendEvent({
|
|
259
|
-
type: 'complete',
|
|
260
|
-
success: false,
|
|
261
|
-
totalDuration: Date.now() - startTime,
|
|
262
|
-
error: errorMsg,
|
|
263
|
-
});
|
|
264
|
-
} finally {
|
|
265
|
-
if (sandbox) {
|
|
266
|
-
try {
|
|
267
|
-
await sandbox.stop();
|
|
268
|
-
} catch {
|
|
269
|
-
// Ignore cleanup errors
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
package/functions/execute.ts
DELETED
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* POST /execute - Execute a build plan in a Vercel Sandbox
|
|
3
|
-
*
|
|
4
|
-
* Request body:
|
|
5
|
-
* - plan: BuildPlan object (required)
|
|
6
|
-
*
|
|
7
|
-
* Response:
|
|
8
|
-
* - BuildPlanExecutionResult with spec and step results
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { BuildPlan, BuildPlanExecutionResult, BuildPlanStepResult } from '@doccov/sdk';
|
|
12
|
-
import type { OpenPkg } from '@openpkg-ts/spec';
|
|
13
|
-
import { Sandbox } from '@vercel/sandbox';
|
|
14
|
-
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
|
15
|
-
import ms from 'ms';
|
|
16
|
-
|
|
17
|
-
export const config = {
|
|
18
|
-
runtime: 'nodejs',
|
|
19
|
-
maxDuration: 300, // 5 minutes for full execution
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
interface ExecuteRequestBody {
|
|
23
|
-
plan: BuildPlan;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Map package manager to install command
|
|
28
|
-
*/
|
|
29
|
-
function getInstallCommand(pm: string): { cmd: string; args: string[] } {
|
|
30
|
-
switch (pm) {
|
|
31
|
-
case 'bun':
|
|
32
|
-
return { cmd: 'bun', args: ['install', '--ignore-scripts'] };
|
|
33
|
-
case 'pnpm':
|
|
34
|
-
return { cmd: 'pnpm', args: ['install', '--ignore-scripts'] };
|
|
35
|
-
case 'yarn':
|
|
36
|
-
return { cmd: 'yarn', args: ['install', '--ignore-scripts'] };
|
|
37
|
-
default:
|
|
38
|
-
return { cmd: 'npm', args: ['install', '--ignore-scripts', '--legacy-peer-deps'] };
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Map runtime to Vercel sandbox runtime
|
|
44
|
-
*/
|
|
45
|
-
function getSandboxRuntime(runtime: string): 'node22' | 'node24' {
|
|
46
|
-
if (runtime === 'node24') return 'node24';
|
|
47
|
-
return 'node22'; // Default to node22 for everything else
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
51
|
-
// CORS
|
|
52
|
-
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
53
|
-
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
|
54
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
55
|
-
|
|
56
|
-
if (req.method === 'OPTIONS') {
|
|
57
|
-
return res.status(200).end();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (req.method !== 'POST') {
|
|
61
|
-
return res.status(405).json({ error: 'Method not allowed' });
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const body = req.body as ExecuteRequestBody;
|
|
65
|
-
|
|
66
|
-
if (!body.plan) {
|
|
67
|
-
return res.status(400).json({ error: 'plan is required' });
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const { plan } = body;
|
|
71
|
-
|
|
72
|
-
// Validate plan structure
|
|
73
|
-
if (!plan.target?.repoUrl || !plan.steps) {
|
|
74
|
-
return res.status(400).json({ error: 'Invalid plan structure' });
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const startTime = Date.now();
|
|
78
|
-
const stepResults: BuildPlanStepResult[] = [];
|
|
79
|
-
let sandbox: Awaited<ReturnType<typeof Sandbox.create>> | null = null;
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
// Create sandbox with git source
|
|
83
|
-
sandbox = await Sandbox.create({
|
|
84
|
-
source: {
|
|
85
|
-
url: plan.target.repoUrl,
|
|
86
|
-
type: 'git',
|
|
87
|
-
},
|
|
88
|
-
resources: { vcpus: 4 },
|
|
89
|
-
timeout: ms('5m'),
|
|
90
|
-
runtime: getSandboxRuntime(plan.environment.runtime),
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// Execute each step
|
|
94
|
-
for (const step of plan.steps) {
|
|
95
|
-
const stepStart = Date.now();
|
|
96
|
-
|
|
97
|
-
try {
|
|
98
|
-
const result = await sandbox.runCommand({
|
|
99
|
-
cmd: step.command,
|
|
100
|
-
args: step.args,
|
|
101
|
-
cwd: step.cwd,
|
|
102
|
-
timeout: step.timeout ?? 60000,
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// stdout/stderr are async functions in Vercel Sandbox
|
|
106
|
-
const stdout = result.stdout ? await result.stdout() : '';
|
|
107
|
-
const stderr = result.stderr ? await result.stderr() : '';
|
|
108
|
-
|
|
109
|
-
stepResults.push({
|
|
110
|
-
stepId: step.id,
|
|
111
|
-
success: result.exitCode === 0,
|
|
112
|
-
duration: Date.now() - stepStart,
|
|
113
|
-
output: stdout.slice(0, 5000), // Truncate output
|
|
114
|
-
error: result.exitCode !== 0 ? stderr.slice(0, 2000) : undefined,
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
// Stop on non-optional failures
|
|
118
|
-
if (result.exitCode !== 0 && !step.optional) {
|
|
119
|
-
throw new Error(`Step '${step.name}' failed: ${stderr}`);
|
|
120
|
-
}
|
|
121
|
-
} catch (stepError) {
|
|
122
|
-
stepResults.push({
|
|
123
|
-
stepId: step.id,
|
|
124
|
-
success: false,
|
|
125
|
-
duration: Date.now() - stepStart,
|
|
126
|
-
error: stepError instanceof Error ? stepError.message : 'Unknown error',
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
if (!step.optional) {
|
|
130
|
-
throw stepError;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Run doccov spec to generate the OpenPkg spec
|
|
136
|
-
const specStart = Date.now();
|
|
137
|
-
const entryPoint = plan.target.entryPoints[0] ?? 'src/index.ts';
|
|
138
|
-
|
|
139
|
-
// Install doccov CLI first
|
|
140
|
-
await sandbox.runCommand({
|
|
141
|
-
cmd: 'npm',
|
|
142
|
-
args: ['install', '-g', '@doccov/cli@latest'],
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// Run spec generation
|
|
146
|
-
const specResult = await sandbox.runCommand({
|
|
147
|
-
cmd: 'doccov',
|
|
148
|
-
args: ['spec', entryPoint, '--output', 'openpkg.json'],
|
|
149
|
-
cwd: plan.target.rootPath,
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
const specStdout = specResult.stdout ? await specResult.stdout() : '';
|
|
153
|
-
const specStderr = specResult.stderr ? await specResult.stderr() : '';
|
|
154
|
-
|
|
155
|
-
stepResults.push({
|
|
156
|
-
stepId: 'analyze',
|
|
157
|
-
success: specResult.exitCode === 0,
|
|
158
|
-
duration: Date.now() - specStart,
|
|
159
|
-
output: specStdout.slice(0, 2000),
|
|
160
|
-
error: specResult.exitCode !== 0 ? specStderr.slice(0, 2000) : undefined,
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
if (specResult.exitCode !== 0) {
|
|
164
|
-
throw new Error(`Spec generation failed: ${specStderr}`);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Read the generated spec - readFile returns a readable stream
|
|
168
|
-
const specStream = await sandbox.readFile({ path: 'openpkg.json' });
|
|
169
|
-
const chunks: Buffer[] = [];
|
|
170
|
-
for await (const chunk of specStream as AsyncIterable<Buffer>) {
|
|
171
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
172
|
-
}
|
|
173
|
-
const specContent = Buffer.concat(chunks).toString('utf-8');
|
|
174
|
-
const spec = JSON.parse(specContent) as OpenPkg;
|
|
175
|
-
|
|
176
|
-
const result: BuildPlanExecutionResult = {
|
|
177
|
-
success: true,
|
|
178
|
-
spec,
|
|
179
|
-
stepResults,
|
|
180
|
-
totalDuration: Date.now() - startTime,
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
return res.status(200).json(result);
|
|
184
|
-
} catch (error) {
|
|
185
|
-
console.error('Execution error:', error);
|
|
186
|
-
|
|
187
|
-
const result: BuildPlanExecutionResult = {
|
|
188
|
-
success: false,
|
|
189
|
-
stepResults,
|
|
190
|
-
totalDuration: Date.now() - startTime,
|
|
191
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
return res.status(200).json(result); // Return 200 with success: false
|
|
195
|
-
} finally {
|
|
196
|
-
if (sandbox) {
|
|
197
|
-
try {
|
|
198
|
-
await sandbox.stop();
|
|
199
|
-
} catch {
|
|
200
|
-
// Ignore cleanup errors
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|