@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/api/index.ts
CHANGED
|
@@ -1,17 +1,36 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* DocCov API - Plain Vercel Node.js handlers
|
|
3
|
+
*
|
|
4
|
+
* No framework, just VercelRequest/VercelResponse for maximum reliability.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
8
|
+
import type {
|
|
9
|
+
BuildPlan,
|
|
10
|
+
BuildPlanEnvironment,
|
|
11
|
+
BuildPlanExecutionResult,
|
|
12
|
+
BuildPlanStep,
|
|
13
|
+
BuildPlanStepResult,
|
|
14
|
+
BuildPlanTarget,
|
|
15
|
+
GitHubProjectContext,
|
|
16
|
+
} from '@doccov/sdk';
|
|
17
|
+
import { fetchGitHubContext, parseScanGitHubUrl } from '@doccov/sdk';
|
|
18
|
+
import type { OpenPkg } from '@openpkg-ts/spec';
|
|
19
|
+
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
|
20
|
+
import { Sandbox } from '@vercel/sandbox';
|
|
21
|
+
import { generateObject } from 'ai';
|
|
22
|
+
import ms from 'ms';
|
|
23
|
+
import { z } from 'zod';
|
|
4
24
|
|
|
5
25
|
export const config = {
|
|
6
|
-
runtime: '
|
|
26
|
+
runtime: 'nodejs',
|
|
27
|
+
maxDuration: 300,
|
|
7
28
|
};
|
|
8
29
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
//
|
|
12
|
-
app.use('*', cors());
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// Types & Helpers
|
|
32
|
+
// =============================================================================
|
|
13
33
|
|
|
14
|
-
// Types
|
|
15
34
|
type BadgeColor =
|
|
16
35
|
| 'brightgreen'
|
|
17
36
|
| 'green'
|
|
@@ -22,13 +41,20 @@ type BadgeColor =
|
|
|
22
41
|
| 'lightgrey';
|
|
23
42
|
|
|
24
43
|
interface OpenPkgSpec {
|
|
25
|
-
docs?: {
|
|
26
|
-
coverageScore?: number;
|
|
27
|
-
};
|
|
44
|
+
docs?: { coverageScore?: number };
|
|
28
45
|
[key: string]: unknown;
|
|
29
46
|
}
|
|
30
47
|
|
|
31
|
-
|
|
48
|
+
function cors(res: VercelResponse): void {
|
|
49
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
50
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
51
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function json(res: VercelResponse, data: unknown, status = 200): void {
|
|
55
|
+
res.status(status).json(data);
|
|
56
|
+
}
|
|
57
|
+
|
|
32
58
|
function getColorForScore(score: number): BadgeColor {
|
|
33
59
|
if (score >= 90) return 'brightgreen';
|
|
34
60
|
if (score >= 80) return 'green';
|
|
@@ -102,117 +128,902 @@ async function fetchSpecFromGitHub(
|
|
|
102
128
|
return null;
|
|
103
129
|
}
|
|
104
130
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
131
|
+
function getSandboxRuntime(_runtime: string): 'node22' {
|
|
132
|
+
return 'node22'; // Always use node22 for Vercel Sandbox
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Common CLI tools that are typically devDependencies (not globally available).
|
|
137
|
+
* These need to be run via package manager exec (npx, pnpm exec, etc.)
|
|
138
|
+
*/
|
|
139
|
+
const LOCAL_BINARIES = new Set([
|
|
140
|
+
'turbo',
|
|
141
|
+
'tsc',
|
|
142
|
+
'tsup',
|
|
143
|
+
'esbuild',
|
|
144
|
+
'vite',
|
|
145
|
+
'rollup',
|
|
146
|
+
'webpack',
|
|
147
|
+
'parcel',
|
|
148
|
+
'swc',
|
|
149
|
+
'bunchee',
|
|
150
|
+
'unbuild',
|
|
151
|
+
'microbundle',
|
|
152
|
+
'preconstruct',
|
|
153
|
+
'changesets',
|
|
154
|
+
'eslint',
|
|
155
|
+
'prettier',
|
|
156
|
+
'vitest',
|
|
157
|
+
'jest',
|
|
158
|
+
'mocha',
|
|
159
|
+
'ava',
|
|
160
|
+
'c8',
|
|
161
|
+
'nyc',
|
|
162
|
+
'size-limit',
|
|
163
|
+
'publint',
|
|
164
|
+
'attw',
|
|
165
|
+
'are-the-types-wrong',
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Wrap local binaries with package manager exec command.
|
|
170
|
+
* Transforms ["turbo", "build"] -> ["pnpm", "exec", "turbo", "build"] (or npx for npm)
|
|
171
|
+
*/
|
|
172
|
+
function wrapLocalBinary(
|
|
173
|
+
cmd: string,
|
|
174
|
+
args: string[],
|
|
175
|
+
packageManager: string,
|
|
176
|
+
): { cmd: string; args: string[] } {
|
|
177
|
+
// If command is a local binary, wrap with package manager exec
|
|
178
|
+
if (LOCAL_BINARIES.has(cmd)) {
|
|
179
|
+
switch (packageManager) {
|
|
180
|
+
case 'pnpm':
|
|
181
|
+
return { cmd: 'pnpm', args: ['exec', cmd, ...args] };
|
|
182
|
+
case 'yarn':
|
|
183
|
+
return { cmd: 'yarn', args: [cmd, ...args] }; // yarn runs local bins directly
|
|
184
|
+
case 'bun':
|
|
185
|
+
return { cmd: 'bunx', args: [cmd, ...args] };
|
|
186
|
+
case 'npm':
|
|
187
|
+
default:
|
|
188
|
+
return { cmd: 'npx', args: [cmd, ...args] };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return { cmd, args };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Normalize cwd for sandbox commands.
|
|
196
|
+
* Vercel Sandbox default working directory is /vercel/sandbox (where git repos are cloned).
|
|
197
|
+
* - If cwd is undefined, empty, or '.' - return undefined (use sandbox default)
|
|
198
|
+
* - If cwd is an absolute path - return as-is
|
|
199
|
+
* - If cwd is a relative subdirectory - convert to absolute path under /vercel/sandbox
|
|
200
|
+
*/
|
|
201
|
+
function normalizeCwd(cwd: string | undefined): string | undefined {
|
|
202
|
+
if (!cwd || cwd === '.' || cwd === './') return undefined;
|
|
203
|
+
// If already absolute, return as-is
|
|
204
|
+
if (cwd.startsWith('/')) return cwd;
|
|
205
|
+
// Convert relative path to absolute path within sandbox
|
|
206
|
+
return `/vercel/sandbox/${cwd}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
interface SpecSummary {
|
|
210
|
+
name: string;
|
|
211
|
+
version: string;
|
|
212
|
+
coverage: number;
|
|
213
|
+
exports: number;
|
|
214
|
+
types: number;
|
|
215
|
+
documented: number;
|
|
216
|
+
undocumented: number;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function createSpecSummary(spec: OpenPkg): SpecSummary {
|
|
220
|
+
const exports = spec.exports?.length ?? 0;
|
|
221
|
+
const types = spec.types?.length ?? 0;
|
|
222
|
+
|
|
223
|
+
// Count documented vs undocumented exports
|
|
224
|
+
const documented =
|
|
225
|
+
spec.exports?.filter((e) => e.description && e.description.trim().length > 0).length ?? 0;
|
|
226
|
+
const undocumented = exports - documented;
|
|
227
|
+
|
|
228
|
+
// Calculate coverage (documented / total * 100)
|
|
229
|
+
const coverage = exports > 0 ? Math.round((documented / exports) * 100) : 0;
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
name: spec.meta?.name ?? 'unknown',
|
|
233
|
+
version: spec.meta?.version ?? '0.0.0',
|
|
234
|
+
coverage,
|
|
235
|
+
exports,
|
|
236
|
+
types,
|
|
237
|
+
documented,
|
|
238
|
+
undocumented,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// =============================================================================
|
|
243
|
+
// Plan Agent (AI-powered build plan generation)
|
|
244
|
+
// =============================================================================
|
|
245
|
+
|
|
246
|
+
const BuildPlanStepSchema = z.object({
|
|
247
|
+
id: z.string().describe('Unique identifier (e.g., "install", "build-types")'),
|
|
248
|
+
name: z.string().describe('Human-readable step name'),
|
|
249
|
+
command: z.string().describe('Command to execute'),
|
|
250
|
+
args: z.array(z.string()).describe('Command arguments'),
|
|
251
|
+
cwd: z.string().optional().describe('Working directory relative to repo root'),
|
|
252
|
+
timeout: z.number().optional().describe('Timeout in milliseconds'),
|
|
253
|
+
optional: z.boolean().optional().describe('If true, failure does not stop execution'),
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const BuildPlanEnvironmentSchema = z.object({
|
|
257
|
+
runtime: z.literal('node22').describe('Runtime (always node22)'),
|
|
258
|
+
packageManager: z.enum(['npm', 'yarn', 'pnpm', 'bun']).describe('Package manager'),
|
|
259
|
+
requiredTools: z.array(z.string()).optional().describe('Additional required tools'),
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const BuildPlanReasoningSchema = z.object({
|
|
263
|
+
summary: z.string().describe('Brief summary of the approach (1-2 sentences)'),
|
|
264
|
+
rationale: z.string().describe('Why this approach was chosen'),
|
|
265
|
+
concerns: z.array(z.string()).describe('Potential issues or concerns'),
|
|
119
266
|
});
|
|
120
267
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
268
|
+
const AIBuildPlanSchema = z.object({
|
|
269
|
+
environment: BuildPlanEnvironmentSchema,
|
|
270
|
+
steps: z.array(BuildPlanStepSchema).describe('Steps to execute in order'),
|
|
271
|
+
entryPoints: z.array(z.string()).describe('Entry point files to analyze'),
|
|
272
|
+
reasoning: BuildPlanReasoningSchema,
|
|
273
|
+
confidence: z.enum(['high', 'medium', 'low']).describe('Confidence in this plan'),
|
|
124
274
|
});
|
|
125
275
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
276
|
+
type AIBuildPlanOutput = z.infer<typeof AIBuildPlanSchema>;
|
|
277
|
+
|
|
278
|
+
interface GenerateBuildPlanOptions {
|
|
279
|
+
targetPackage?: string;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function formatContext(context: GitHubProjectContext): string {
|
|
283
|
+
const sections: string[] = [];
|
|
284
|
+
|
|
285
|
+
sections.push(`=== Repository ===
|
|
286
|
+
Owner: ${context.metadata.owner}
|
|
287
|
+
Repo: ${context.metadata.repo}
|
|
288
|
+
Language: ${context.metadata.language ?? 'unknown'}
|
|
289
|
+
Topics: ${context.metadata.topics.join(', ') || 'none'}
|
|
290
|
+
Description: ${context.metadata.description ?? 'none'}`);
|
|
291
|
+
|
|
292
|
+
sections.push(`=== Environment ===
|
|
293
|
+
Package Manager: ${context.packageManager}
|
|
294
|
+
Is Monorepo: ${context.workspace.isMonorepo}
|
|
295
|
+
Workspace Tool: ${context.workspace.tool ?? 'none'}
|
|
296
|
+
Workspace Packages: ${context.workspace.packages?.join(', ') ?? 'none'}`);
|
|
297
|
+
|
|
298
|
+
sections.push(`=== Build Hints ===
|
|
299
|
+
Has TypeScript: ${context.buildHints.hasTypeScript}
|
|
300
|
+
Has WASM: ${context.buildHints.hasWasm}
|
|
301
|
+
Has Native Modules: ${context.buildHints.hasNativeModules}
|
|
302
|
+
Has Build Script: ${context.buildHints.hasBuildScript}
|
|
303
|
+
Build Script: ${context.buildHints.buildScript ?? 'none'}
|
|
304
|
+
Frameworks: ${context.buildHints.frameworks.join(', ') || 'none'}`);
|
|
305
|
+
|
|
306
|
+
if (context.files.packageJson) {
|
|
307
|
+
const truncated =
|
|
308
|
+
context.files.packageJson.length > 3000
|
|
309
|
+
? `${context.files.packageJson.slice(0, 3000)}\n... (truncated)`
|
|
310
|
+
: context.files.packageJson;
|
|
311
|
+
sections.push(`=== package.json ===\n${truncated}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (context.files.tsconfigJson) {
|
|
315
|
+
sections.push(`=== tsconfig.json ===\n${context.files.tsconfigJson}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (context.files.lockfile) {
|
|
319
|
+
const preview = context.files.lockfile.content.slice(0, 500);
|
|
320
|
+
sections.push(`=== ${context.files.lockfile.name} (preview) ===\n${preview}\n...`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return sections.join('\n\n');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const PLAN_SYSTEM_PROMPT = `You are a build system expert. Your task is to analyze a GitHub repository and generate a build plan to analyze its TypeScript/JavaScript API.
|
|
327
|
+
|
|
328
|
+
The goal is to:
|
|
329
|
+
1. Install dependencies
|
|
330
|
+
2. Build the project (if needed) to generate TypeScript declarations
|
|
331
|
+
3. Identify entry points for API documentation analysis
|
|
332
|
+
|
|
333
|
+
Package Manager Selection:
|
|
334
|
+
- If a lockfile is detected (package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lockb), use that package manager
|
|
335
|
+
- If Package Manager is "unknown" (no lockfile), default to npm with "npm install" (NOT "npm ci")
|
|
336
|
+
- IMPORTANT: "npm ci" and "--frozen-lockfile" flags ONLY work when a lockfile exists
|
|
337
|
+
- When no lockfile: use "npm install", "yarn install", "pnpm install", or "bun install" (without frozen flags)
|
|
338
|
+
|
|
339
|
+
Install Commands by Package Manager:
|
|
340
|
+
- npm with lockfile: ["npm", "ci"]
|
|
341
|
+
- npm without lockfile: ["npm", "install"]
|
|
342
|
+
- yarn with lockfile: ["yarn", "install", "--frozen-lockfile"]
|
|
343
|
+
- yarn without lockfile: ["yarn", "install"]
|
|
344
|
+
- pnpm with lockfile: ["pnpm", "install", "--frozen-lockfile"]
|
|
345
|
+
- pnpm without lockfile: ["pnpm", "install"]
|
|
346
|
+
- bun with lockfile: ["bun", "install", "--frozen-lockfile"]
|
|
347
|
+
- bun without lockfile: ["bun", "install"]
|
|
348
|
+
|
|
349
|
+
General Guidelines:
|
|
350
|
+
- For TypeScript projects, look for "types" or "exports" fields in package.json
|
|
351
|
+
- For monorepos, focus on the target package if specified
|
|
352
|
+
- Common entry points: src/index.ts, dist/index.d.ts, lib/index.ts, distribution/index.d.ts
|
|
353
|
+
- WASM projects may need build steps before .d.ts files exist
|
|
354
|
+
- Be conservative with timeouts (default 60000ms, increase for builds)
|
|
355
|
+
- Installation is usually required first
|
|
356
|
+
- Build step is needed if package.json has a "build" script and the types are in dist/distribution folder
|
|
357
|
+
|
|
358
|
+
Step ID conventions:
|
|
359
|
+
- "install" - install dependencies
|
|
360
|
+
- "build" - main build step
|
|
361
|
+
- "build-types" - generate type declarations
|
|
362
|
+
- "analyze" - run doccov spec (added automatically)`;
|
|
363
|
+
|
|
364
|
+
function generatePlanPrompt(
|
|
365
|
+
context: GitHubProjectContext,
|
|
366
|
+
options: GenerateBuildPlanOptions,
|
|
367
|
+
): string {
|
|
368
|
+
let prompt = `Analyze this repository and generate a build plan:\n\n${formatContext(context)}`;
|
|
369
|
+
|
|
370
|
+
if (options.targetPackage) {
|
|
371
|
+
prompt += `\n\nTarget Package: ${options.targetPackage}
|
|
372
|
+
Focus the plan on building and analyzing only this package within the monorepo.`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
prompt += `\n\nGenerate a build plan with:
|
|
376
|
+
- environment: Runtime and package manager configuration
|
|
377
|
+
- steps: Ordered build steps (install, build, etc.)
|
|
378
|
+
- entryPoints: TypeScript entry files to analyze (relative paths)
|
|
379
|
+
- reasoning: Explain your approach
|
|
380
|
+
- confidence: How confident you are in this plan`;
|
|
381
|
+
|
|
382
|
+
return prompt;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function transformToBuildPlan(
|
|
386
|
+
output: AIBuildPlanOutput,
|
|
387
|
+
context: GitHubProjectContext,
|
|
388
|
+
options: GenerateBuildPlanOptions,
|
|
389
|
+
): BuildPlan {
|
|
390
|
+
const target: BuildPlanTarget = {
|
|
391
|
+
type: 'github',
|
|
392
|
+
repoUrl: `https://github.com/${context.metadata.owner}/${context.metadata.repo}`,
|
|
393
|
+
ref: context.ref,
|
|
394
|
+
rootPath: options.targetPackage,
|
|
395
|
+
entryPoints: output.entryPoints,
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const environment: BuildPlanEnvironment = {
|
|
399
|
+
runtime: output.environment.runtime,
|
|
400
|
+
packageManager: output.environment.packageManager,
|
|
401
|
+
requiredTools: output.environment.requiredTools,
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const steps: BuildPlanStep[] = output.steps.map((step) => ({
|
|
405
|
+
id: step.id,
|
|
406
|
+
name: step.name,
|
|
407
|
+
command: step.command,
|
|
408
|
+
args: step.args,
|
|
409
|
+
cwd: step.cwd,
|
|
410
|
+
timeout: step.timeout,
|
|
411
|
+
optional: step.optional,
|
|
412
|
+
}));
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
version: '1.0.0',
|
|
416
|
+
generatedAt: new Date().toISOString(),
|
|
417
|
+
target,
|
|
418
|
+
environment,
|
|
419
|
+
steps,
|
|
420
|
+
reasoning: {
|
|
421
|
+
summary: output.reasoning.summary,
|
|
422
|
+
rationale: output.reasoning.rationale,
|
|
423
|
+
concerns: output.reasoning.concerns,
|
|
424
|
+
},
|
|
425
|
+
confidence: output.confidence,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function generateBuildPlan(
|
|
430
|
+
context: GitHubProjectContext,
|
|
431
|
+
options: GenerateBuildPlanOptions = {},
|
|
432
|
+
): Promise<BuildPlan> {
|
|
433
|
+
const anthropic = createAnthropic({
|
|
434
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
435
|
+
});
|
|
436
|
+
const model = anthropic('claude-sonnet-4-20250514');
|
|
437
|
+
const prompt = generatePlanPrompt(context, options);
|
|
438
|
+
|
|
439
|
+
const { object } = await generateObject({
|
|
440
|
+
model,
|
|
441
|
+
schema: AIBuildPlanSchema,
|
|
442
|
+
system: PLAN_SYSTEM_PROMPT,
|
|
443
|
+
prompt,
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
return transformToBuildPlan(object, context, options);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// =============================================================================
|
|
450
|
+
// Route Handlers
|
|
451
|
+
// =============================================================================
|
|
452
|
+
|
|
453
|
+
async function handleRoot(_req: VercelRequest, res: VercelResponse): Promise<void> {
|
|
454
|
+
json(res, {
|
|
455
|
+
name: 'DocCov API',
|
|
456
|
+
version: '0.4.0',
|
|
457
|
+
status: 'ok',
|
|
458
|
+
timestamp: new Date().toISOString(),
|
|
459
|
+
endpoints: [
|
|
460
|
+
{ method: 'GET', path: '/', description: 'API info and health status' },
|
|
461
|
+
{
|
|
462
|
+
method: 'GET',
|
|
463
|
+
path: '/badge/:owner/:repo',
|
|
464
|
+
description: 'Get coverage badge for a GitHub repo',
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
method: 'GET',
|
|
468
|
+
path: '/spec/:owner/:repo/:ref?',
|
|
469
|
+
description: 'Get OpenPkg spec for a GitHub repo',
|
|
470
|
+
},
|
|
471
|
+
{ method: 'POST', path: '/plan', description: 'Generate AI build plan for a GitHub repo' },
|
|
472
|
+
{ method: 'POST', path: '/execute', description: 'Execute a build plan and return results' },
|
|
473
|
+
{
|
|
474
|
+
method: 'POST',
|
|
475
|
+
path: '/execute-stream',
|
|
476
|
+
description: 'Execute a build plan with SSE streaming',
|
|
477
|
+
},
|
|
478
|
+
],
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async function handleBadge(
|
|
483
|
+
req: VercelRequest,
|
|
484
|
+
res: VercelResponse,
|
|
485
|
+
owner: string,
|
|
486
|
+
repo: string,
|
|
487
|
+
): Promise<void> {
|
|
488
|
+
const branch = (req.query.branch as string) ?? 'main';
|
|
130
489
|
|
|
131
490
|
try {
|
|
132
491
|
const spec = await fetchSpecFromGitHub(owner, repo, branch);
|
|
133
492
|
|
|
134
493
|
if (!spec) {
|
|
135
494
|
const svg = generateBadgeSvg('docs', 'not found', 'lightgrey');
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
495
|
+
res.setHeader('Content-Type', 'image/svg+xml');
|
|
496
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
497
|
+
res.status(404).send(svg);
|
|
498
|
+
return;
|
|
140
499
|
}
|
|
141
500
|
|
|
142
501
|
const coverageScore = spec.docs?.coverageScore ?? 0;
|
|
143
502
|
const svg = generateBadgeSvg('docs', `${coverageScore}%`, getColorForScore(coverageScore));
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
'Cache-Control': 'public, max-age=300',
|
|
148
|
-
});
|
|
503
|
+
res.setHeader('Content-Type', 'image/svg+xml');
|
|
504
|
+
res.setHeader('Cache-Control', 'public, max-age=300');
|
|
505
|
+
res.status(200).send(svg);
|
|
149
506
|
} catch {
|
|
150
507
|
const svg = generateBadgeSvg('docs', 'error', 'red');
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
});
|
|
508
|
+
res.setHeader('Content-Type', 'image/svg+xml');
|
|
509
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
510
|
+
res.status(500).send(svg);
|
|
155
511
|
}
|
|
156
|
-
}
|
|
512
|
+
}
|
|
157
513
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
514
|
+
async function handleSpec(
|
|
515
|
+
_req: VercelRequest,
|
|
516
|
+
res: VercelResponse,
|
|
517
|
+
owner: string,
|
|
518
|
+
repo: string,
|
|
519
|
+
ref?: string,
|
|
520
|
+
): Promise<void> {
|
|
521
|
+
const actualRef = ref ?? 'main';
|
|
522
|
+
const spec = await fetchSpecFromGitHub(owner, repo, actualRef);
|
|
165
523
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
524
|
+
if (!spec) {
|
|
525
|
+
json(res, { error: 'Spec not found' }, 404);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
169
528
|
|
|
529
|
+
res.setHeader('Cache-Control', 'public, max-age=300');
|
|
530
|
+
json(res, spec);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async function handleSpecPr(
|
|
534
|
+
_req: VercelRequest,
|
|
535
|
+
res: VercelResponse,
|
|
536
|
+
owner: string,
|
|
537
|
+
repo: string,
|
|
538
|
+
pr: string,
|
|
539
|
+
): Promise<void> {
|
|
170
540
|
try {
|
|
171
|
-
// Get PR head SHA from GitHub API
|
|
172
541
|
const prResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls/${pr}`, {
|
|
173
542
|
headers: { 'User-Agent': 'DocCov' },
|
|
174
543
|
});
|
|
175
544
|
|
|
176
545
|
if (!prResponse.ok) {
|
|
177
|
-
|
|
546
|
+
json(res, { error: 'PR not found' }, 404);
|
|
547
|
+
return;
|
|
178
548
|
}
|
|
179
549
|
|
|
180
550
|
const prData = (await prResponse.json()) as { head: { sha: string } };
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
// Fetch spec from PR head
|
|
184
|
-
const specUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${headSha}/openpkg.json`;
|
|
551
|
+
const specUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${prData.head.sha}/openpkg.json`;
|
|
185
552
|
const specResponse = await fetch(specUrl);
|
|
186
553
|
|
|
187
554
|
if (!specResponse.ok) {
|
|
188
|
-
|
|
555
|
+
json(res, { error: 'Spec not found in PR' }, 404);
|
|
556
|
+
return;
|
|
189
557
|
}
|
|
190
558
|
|
|
191
559
|
const spec = await specResponse.json();
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
});
|
|
560
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
561
|
+
json(res, spec);
|
|
195
562
|
} catch {
|
|
196
|
-
|
|
563
|
+
json(res, { error: 'Failed to fetch PR spec' }, 500);
|
|
197
564
|
}
|
|
198
|
-
}
|
|
565
|
+
}
|
|
199
566
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const { owner, repo } = c.req.param();
|
|
203
|
-
const ref = c.req.param('ref') ?? 'main';
|
|
567
|
+
async function handlePlan(req: VercelRequest, res: VercelResponse): Promise<void> {
|
|
568
|
+
const body = req.body as { url?: string; ref?: string; package?: string };
|
|
204
569
|
|
|
205
|
-
|
|
570
|
+
if (!body.url) {
|
|
571
|
+
json(res, { error: 'url is required' }, 400);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
206
574
|
|
|
207
|
-
|
|
208
|
-
|
|
575
|
+
let repoUrl: string;
|
|
576
|
+
try {
|
|
577
|
+
const parsed = parseScanGitHubUrl(body.url);
|
|
578
|
+
if (!parsed) {
|
|
579
|
+
json(res, { error: 'Invalid GitHub URL' }, 400);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
repoUrl = `https://github.com/${parsed.owner}/${parsed.repo}`;
|
|
583
|
+
} catch {
|
|
584
|
+
json(res, { error: 'Invalid GitHub URL' }, 400);
|
|
585
|
+
return;
|
|
209
586
|
}
|
|
210
587
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
588
|
+
try {
|
|
589
|
+
const context = await fetchGitHubContext(repoUrl, body.ref);
|
|
590
|
+
|
|
591
|
+
if (context.metadata.isPrivate) {
|
|
592
|
+
json(
|
|
593
|
+
res,
|
|
594
|
+
{
|
|
595
|
+
error: 'Private repositories are not supported',
|
|
596
|
+
hint: 'Use a public repository or run doccov locally',
|
|
597
|
+
},
|
|
598
|
+
403,
|
|
599
|
+
);
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const plan = await generateBuildPlan(context, { targetPackage: body.package });
|
|
604
|
+
|
|
605
|
+
json(res, {
|
|
606
|
+
plan,
|
|
607
|
+
context: {
|
|
608
|
+
owner: context.metadata.owner,
|
|
609
|
+
repo: context.metadata.repo,
|
|
610
|
+
ref: context.ref,
|
|
611
|
+
packageManager: context.packageManager,
|
|
612
|
+
isMonorepo: context.workspace.isMonorepo,
|
|
613
|
+
},
|
|
614
|
+
});
|
|
615
|
+
} catch (error) {
|
|
616
|
+
console.error('Plan generation error:', error);
|
|
617
|
+
|
|
618
|
+
if (error instanceof Error) {
|
|
619
|
+
if (error.message.includes('404') || error.message.includes('not found')) {
|
|
620
|
+
json(res, { error: 'Repository not found' }, 404);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (error.message.includes('rate limit')) {
|
|
624
|
+
json(res, { error: 'GitHub API rate limit exceeded' }, 429);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
json(
|
|
630
|
+
res,
|
|
631
|
+
{
|
|
632
|
+
error: 'Failed to generate build plan',
|
|
633
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
634
|
+
},
|
|
635
|
+
500,
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async function handleExecute(req: VercelRequest, res: VercelResponse): Promise<void> {
|
|
641
|
+
const includeSpec = req.query.includeSpec === 'true';
|
|
642
|
+
const body = req.body as { plan?: BuildPlan };
|
|
643
|
+
|
|
644
|
+
if (!body.plan) {
|
|
645
|
+
json(res, { error: 'plan is required' }, 400);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const { plan } = body;
|
|
650
|
+
|
|
651
|
+
if (!plan.target?.repoUrl || !plan.steps) {
|
|
652
|
+
json(res, { error: 'Invalid plan structure' }, 400);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const startTime = Date.now();
|
|
657
|
+
const stepResults: BuildPlanStepResult[] = [];
|
|
658
|
+
let sandbox: Awaited<ReturnType<typeof Sandbox.create>> | null = null;
|
|
659
|
+
|
|
660
|
+
try {
|
|
661
|
+
sandbox = await Sandbox.create({
|
|
662
|
+
source: { url: plan.target.repoUrl, type: 'git' as const },
|
|
663
|
+
resources: { vcpus: 4 },
|
|
664
|
+
timeout: ms('5m'),
|
|
665
|
+
runtime: getSandboxRuntime(plan.environment.runtime),
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
for (const step of plan.steps) {
|
|
669
|
+
const stepStart = Date.now();
|
|
670
|
+
|
|
671
|
+
try {
|
|
672
|
+
const normalizedCwd = normalizeCwd(step.cwd);
|
|
673
|
+
const { cmd, args } = wrapLocalBinary(step.command, step.args, plan.environment.packageManager);
|
|
674
|
+
const result = await sandbox.runCommand({
|
|
675
|
+
cmd,
|
|
676
|
+
args,
|
|
677
|
+
...(normalizedCwd ? { cwd: normalizedCwd } : {}),
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
const stdout = result.stdout ? await result.stdout() : '';
|
|
681
|
+
const stderr = result.stderr ? await result.stderr() : '';
|
|
682
|
+
|
|
683
|
+
stepResults.push({
|
|
684
|
+
stepId: step.id,
|
|
685
|
+
success: result.exitCode === 0,
|
|
686
|
+
duration: Date.now() - stepStart,
|
|
687
|
+
output: stdout.slice(0, 5000),
|
|
688
|
+
error: result.exitCode !== 0 ? stderr.slice(0, 2000) : undefined,
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
if (result.exitCode !== 0 && !step.optional) {
|
|
692
|
+
throw new Error(`Step '${step.name}' failed: ${stderr}`);
|
|
693
|
+
}
|
|
694
|
+
} catch (stepError) {
|
|
695
|
+
stepResults.push({
|
|
696
|
+
stepId: step.id,
|
|
697
|
+
success: false,
|
|
698
|
+
duration: Date.now() - stepStart,
|
|
699
|
+
error: stepError instanceof Error ? stepError.message : 'Unknown error',
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
if (!step.optional) {
|
|
703
|
+
throw stepError;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Run doccov spec
|
|
709
|
+
const specStart = Date.now();
|
|
710
|
+
let entryPoint = plan.target.entryPoints[0] ?? 'src/index.ts';
|
|
711
|
+
const analyzeCwd = normalizeCwd(plan.target.rootPath);
|
|
712
|
+
|
|
713
|
+
// If running in a subdirectory (rootPath), strip the rootPath prefix from entryPoint
|
|
714
|
+
if (plan.target.rootPath && entryPoint.startsWith(plan.target.rootPath + '/')) {
|
|
715
|
+
entryPoint = entryPoint.slice(plan.target.rootPath.length + 1);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
await sandbox.runCommand({ cmd: 'npm', args: ['install', '-g', '@doccov/cli@latest'] });
|
|
719
|
+
|
|
720
|
+
const specResult = await sandbox.runCommand({
|
|
721
|
+
cmd: 'doccov',
|
|
722
|
+
args: ['spec', entryPoint, '--output', 'openpkg.json'],
|
|
723
|
+
...(analyzeCwd ? { cwd: analyzeCwd } : {}),
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
const specStdout = specResult.stdout ? await specResult.stdout() : '';
|
|
727
|
+
const specStderr = specResult.stderr ? await specResult.stderr() : '';
|
|
728
|
+
|
|
729
|
+
stepResults.push({
|
|
730
|
+
stepId: 'analyze',
|
|
731
|
+
success: specResult.exitCode === 0,
|
|
732
|
+
duration: Date.now() - specStart,
|
|
733
|
+
output: specStdout.slice(0, 2000),
|
|
734
|
+
error: specResult.exitCode !== 0 ? specStderr.slice(0, 2000) : undefined,
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
if (specResult.exitCode !== 0) {
|
|
738
|
+
throw new Error(`Spec generation failed: ${specStderr}`);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Read openpkg.json from the correct location (rootPath subdirectory if set)
|
|
742
|
+
const specFilePath = plan.target.rootPath
|
|
743
|
+
? `${plan.target.rootPath}/openpkg.json`
|
|
744
|
+
: 'openpkg.json';
|
|
745
|
+
const specStream = await sandbox.readFile({ path: specFilePath });
|
|
746
|
+
const chunks: Buffer[] = [];
|
|
747
|
+
for await (const chunk of specStream as AsyncIterable<Buffer>) {
|
|
748
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
749
|
+
}
|
|
750
|
+
const specContent = Buffer.concat(chunks).toString('utf-8');
|
|
751
|
+
const spec = JSON.parse(specContent) as OpenPkg;
|
|
752
|
+
const summary = createSpecSummary(spec);
|
|
753
|
+
|
|
754
|
+
const result = {
|
|
755
|
+
success: true,
|
|
756
|
+
summary,
|
|
757
|
+
stepResults,
|
|
758
|
+
totalDuration: Date.now() - startTime,
|
|
759
|
+
...(includeSpec ? { spec } : {}),
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
json(res, result);
|
|
763
|
+
} catch (error) {
|
|
764
|
+
console.error('Execution error:', error);
|
|
765
|
+
|
|
766
|
+
const result: BuildPlanExecutionResult = {
|
|
767
|
+
success: false,
|
|
768
|
+
stepResults,
|
|
769
|
+
totalDuration: Date.now() - startTime,
|
|
770
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
json(res, result);
|
|
774
|
+
} finally {
|
|
775
|
+
if (sandbox) {
|
|
776
|
+
try {
|
|
777
|
+
await sandbox.stop();
|
|
778
|
+
} catch {
|
|
779
|
+
// Ignore cleanup errors
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
async function handleExecuteStream(req: VercelRequest, res: VercelResponse): Promise<void> {
|
|
786
|
+
const includeSpec = req.query.includeSpec === 'true';
|
|
787
|
+
|
|
788
|
+
// SSE headers
|
|
789
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
790
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
791
|
+
res.setHeader('Connection', 'keep-alive');
|
|
792
|
+
|
|
793
|
+
const send = (event: string, data: unknown) => {
|
|
794
|
+
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
const body = req.body as { plan?: BuildPlan };
|
|
798
|
+
|
|
799
|
+
if (!body.plan) {
|
|
800
|
+
send('error', { error: 'plan is required' });
|
|
801
|
+
res.end();
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const { plan } = body;
|
|
806
|
+
|
|
807
|
+
if (!plan.target?.repoUrl || !plan.steps) {
|
|
808
|
+
send('error', { error: 'Invalid plan structure' });
|
|
809
|
+
res.end();
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const startTime = Date.now();
|
|
814
|
+
const stepResults: BuildPlanStepResult[] = [];
|
|
815
|
+
let sandbox: Awaited<ReturnType<typeof Sandbox.create>> | null = null;
|
|
816
|
+
|
|
817
|
+
try {
|
|
818
|
+
send('progress', { stage: 'init', message: 'Creating sandbox...', progress: 5 });
|
|
819
|
+
|
|
820
|
+
sandbox = await Sandbox.create({
|
|
821
|
+
source: { url: plan.target.repoUrl, type: 'git' as const },
|
|
822
|
+
resources: { vcpus: 4 },
|
|
823
|
+
timeout: ms('5m'),
|
|
824
|
+
runtime: getSandboxRuntime(plan.environment.runtime),
|
|
825
|
+
});
|
|
215
826
|
|
|
216
|
-
|
|
827
|
+
send('progress', { stage: 'cloned', message: 'Repository cloned', progress: 15 });
|
|
217
828
|
|
|
218
|
-
|
|
829
|
+
const totalSteps = plan.steps.length + 1; // +1 for analyze step
|
|
830
|
+
let completedSteps = 0;
|
|
831
|
+
|
|
832
|
+
for (const step of plan.steps) {
|
|
833
|
+
const stepStart = Date.now();
|
|
834
|
+
const progressBase = 15 + (completedSteps / totalSteps) * 70;
|
|
835
|
+
|
|
836
|
+
send('step:start', { stepId: step.id, name: step.name, progress: Math.round(progressBase) });
|
|
837
|
+
|
|
838
|
+
try {
|
|
839
|
+
const normalizedCwd = normalizeCwd(step.cwd);
|
|
840
|
+
const { cmd, args } = wrapLocalBinary(step.command, step.args, plan.environment.packageManager);
|
|
841
|
+
const result = await sandbox.runCommand({
|
|
842
|
+
cmd,
|
|
843
|
+
args,
|
|
844
|
+
...(normalizedCwd ? { cwd: normalizedCwd } : {}),
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
const stdout = result.stdout ? await result.stdout() : '';
|
|
848
|
+
const stderr = result.stderr ? await result.stderr() : '';
|
|
849
|
+
|
|
850
|
+
const stepResult: BuildPlanStepResult = {
|
|
851
|
+
stepId: step.id,
|
|
852
|
+
success: result.exitCode === 0,
|
|
853
|
+
duration: Date.now() - stepStart,
|
|
854
|
+
output: stdout.slice(0, 5000),
|
|
855
|
+
error: result.exitCode !== 0 ? stderr.slice(0, 2000) : undefined,
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
stepResults.push(stepResult);
|
|
859
|
+
completedSteps++;
|
|
860
|
+
|
|
861
|
+
send('step:complete', {
|
|
862
|
+
...stepResult,
|
|
863
|
+
progress: Math.round(15 + (completedSteps / totalSteps) * 70),
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
if (result.exitCode !== 0 && !step.optional) {
|
|
867
|
+
throw new Error(`Step '${step.name}' failed: ${stderr}`);
|
|
868
|
+
}
|
|
869
|
+
} catch (stepError) {
|
|
870
|
+
const stepResult: BuildPlanStepResult = {
|
|
871
|
+
stepId: step.id,
|
|
872
|
+
success: false,
|
|
873
|
+
duration: Date.now() - stepStart,
|
|
874
|
+
error: stepError instanceof Error ? stepError.message : 'Unknown error',
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
stepResults.push(stepResult);
|
|
878
|
+
send('step:error', stepResult);
|
|
879
|
+
|
|
880
|
+
if (!step.optional) {
|
|
881
|
+
throw stepError;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// Run doccov spec
|
|
887
|
+
send('step:start', { stepId: 'analyze', name: 'Analyzing API', progress: 85 });
|
|
888
|
+
const specStart = Date.now();
|
|
889
|
+
let entryPoint = plan.target.entryPoints[0] ?? 'src/index.ts';
|
|
890
|
+
const analyzeCwd = normalizeCwd(plan.target.rootPath);
|
|
891
|
+
|
|
892
|
+
// If running in a subdirectory (rootPath), strip the rootPath prefix from entryPoint
|
|
893
|
+
// e.g., rootPath="packages/v0-sdk", entryPoint="packages/v0-sdk/src/index.ts" -> "src/index.ts"
|
|
894
|
+
if (plan.target.rootPath && entryPoint.startsWith(plan.target.rootPath + '/')) {
|
|
895
|
+
entryPoint = entryPoint.slice(plan.target.rootPath.length + 1);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
await sandbox.runCommand({ cmd: 'npm', args: ['install', '-g', '@doccov/cli@latest'] });
|
|
899
|
+
|
|
900
|
+
const specResult = await sandbox.runCommand({
|
|
901
|
+
cmd: 'doccov',
|
|
902
|
+
args: ['spec', entryPoint, '--output', 'openpkg.json'],
|
|
903
|
+
...(analyzeCwd ? { cwd: analyzeCwd } : {}),
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
const specStdout = specResult.stdout ? await specResult.stdout() : '';
|
|
907
|
+
const specStderr = specResult.stderr ? await specResult.stderr() : '';
|
|
908
|
+
|
|
909
|
+
stepResults.push({
|
|
910
|
+
stepId: 'analyze',
|
|
911
|
+
success: specResult.exitCode === 0,
|
|
912
|
+
duration: Date.now() - specStart,
|
|
913
|
+
output: specStdout.slice(0, 2000),
|
|
914
|
+
error: specResult.exitCode !== 0 ? specStderr.slice(0, 2000) : undefined,
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
if (specResult.exitCode !== 0) {
|
|
918
|
+
throw new Error(`Spec generation failed: ${specStderr}`);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
send('step:complete', { stepId: 'analyze', success: true, progress: 95 });
|
|
922
|
+
|
|
923
|
+
// Read openpkg.json from the correct location (rootPath subdirectory if set)
|
|
924
|
+
const specFilePath = plan.target.rootPath
|
|
925
|
+
? `${plan.target.rootPath}/openpkg.json`
|
|
926
|
+
: 'openpkg.json';
|
|
927
|
+
const specStream = await sandbox.readFile({ path: specFilePath });
|
|
928
|
+
const chunks: Buffer[] = [];
|
|
929
|
+
for await (const chunk of specStream as AsyncIterable<Buffer>) {
|
|
930
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
931
|
+
}
|
|
932
|
+
const specContent = Buffer.concat(chunks).toString('utf-8');
|
|
933
|
+
const spec = JSON.parse(specContent) as OpenPkg;
|
|
934
|
+
const summary = createSpecSummary(spec);
|
|
935
|
+
|
|
936
|
+
const result = {
|
|
937
|
+
success: true,
|
|
938
|
+
summary,
|
|
939
|
+
stepResults,
|
|
940
|
+
totalDuration: Date.now() - startTime,
|
|
941
|
+
...(includeSpec ? { spec } : {}),
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
send('complete', result);
|
|
945
|
+
} catch (error) {
|
|
946
|
+
console.error('Execution error:', error);
|
|
947
|
+
|
|
948
|
+
const result: BuildPlanExecutionResult = {
|
|
949
|
+
success: false,
|
|
950
|
+
stepResults,
|
|
951
|
+
totalDuration: Date.now() - startTime,
|
|
952
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
send('error', result);
|
|
956
|
+
} finally {
|
|
957
|
+
if (sandbox) {
|
|
958
|
+
try {
|
|
959
|
+
await sandbox.stop();
|
|
960
|
+
} catch {
|
|
961
|
+
// Ignore cleanup errors
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
res.end();
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// =============================================================================
|
|
969
|
+
// Main Router
|
|
970
|
+
// =============================================================================
|
|
971
|
+
|
|
972
|
+
export default async function handler(req: VercelRequest, res: VercelResponse): Promise<void> {
|
|
973
|
+
// CORS
|
|
974
|
+
cors(res);
|
|
975
|
+
|
|
976
|
+
if (req.method === 'OPTIONS') {
|
|
977
|
+
res.status(200).end();
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
|
|
982
|
+
const path = url.pathname;
|
|
983
|
+
|
|
984
|
+
try {
|
|
985
|
+
// GET /
|
|
986
|
+
if (path === '/' && req.method === 'GET') {
|
|
987
|
+
return handleRoot(req, res);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// GET /badge/:owner/:repo
|
|
991
|
+
const badgeMatch = path.match(/^\/badge\/([^/]+)\/([^/]+)$/);
|
|
992
|
+
if (badgeMatch && req.method === 'GET') {
|
|
993
|
+
return handleBadge(req, res, badgeMatch[1], badgeMatch[2]);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// GET /spec/:owner/:repo/pr/:pr
|
|
997
|
+
const specPrMatch = path.match(/^\/spec\/([^/]+)\/([^/]+)\/pr\/(\d+)$/);
|
|
998
|
+
if (specPrMatch && req.method === 'GET') {
|
|
999
|
+
return handleSpecPr(req, res, specPrMatch[1], specPrMatch[2], specPrMatch[3]);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// GET /spec/:owner/:repo/:ref?
|
|
1003
|
+
const specMatch = path.match(/^\/spec\/([^/]+)\/([^/]+)(?:\/([^/]+))?$/);
|
|
1004
|
+
if (specMatch && req.method === 'GET') {
|
|
1005
|
+
return handleSpec(req, res, specMatch[1], specMatch[2], specMatch[3]);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// POST /plan
|
|
1009
|
+
if (path === '/plan' && req.method === 'POST') {
|
|
1010
|
+
return handlePlan(req, res);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// POST /execute
|
|
1014
|
+
if (path === '/execute' && req.method === 'POST') {
|
|
1015
|
+
return handleExecute(req, res);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// POST /execute-stream (SSE)
|
|
1019
|
+
if (path === '/execute-stream' && req.method === 'POST') {
|
|
1020
|
+
return handleExecuteStream(req, res);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// 404
|
|
1024
|
+
json(res, { error: 'Not found' }, 404);
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
console.error('Handler error:', error);
|
|
1027
|
+
json(res, { error: 'Internal server error' }, 500);
|
|
1028
|
+
}
|
|
1029
|
+
}
|