@doccov/api 0.3.3 → 0.3.5
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 +5 -0
- package/CHANGELOG.md +15 -0
- package/api/[...path].ts +35 -0
- package/bunup.config.ts +16 -0
- package/functions/execute-stream.ts +273 -0
- package/functions/execute.ts +204 -0
- package/functions/plan.ts +104 -0
- package/lib/plan-agent.ts +252 -0
- package/package.json +10 -6
- package/src/index.ts +9 -7
- package/src/routes/plan.ts +75 -0
- package/tsconfig.json +2 -2
- package/vercel.json +5 -3
- package/api/scan/detect.ts +0 -121
- package/api/scan-stream.ts +0 -460
- package/api/scan.ts +0 -63
- package/src/routes/scan.ts +0 -240
- package/src/sandbox-runner.ts +0 -82
- package/src/scan-worker.ts +0 -122
- package/src/stores/job-store.interface.ts +0 -25
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI-powered build plan generation agent.
|
|
3
|
+
* Uses Claude to analyze repository context and generate execution plans.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
7
|
+
import { generateObject } from 'ai';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import type {
|
|
10
|
+
BuildPlan,
|
|
11
|
+
BuildPlanEnvironment,
|
|
12
|
+
BuildPlanStep,
|
|
13
|
+
BuildPlanTarget,
|
|
14
|
+
GitHubProjectContext,
|
|
15
|
+
} from '@doccov/sdk';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Zod schema for build plan validation.
|
|
19
|
+
*/
|
|
20
|
+
const BuildPlanStepSchema = z.object({
|
|
21
|
+
id: z.string().describe('Unique identifier (e.g., "install", "build-types")'),
|
|
22
|
+
name: z.string().describe('Human-readable step name'),
|
|
23
|
+
command: z.string().describe('Command to execute'),
|
|
24
|
+
args: z.array(z.string()).describe('Command arguments'),
|
|
25
|
+
cwd: z.string().optional().describe('Working directory relative to repo root'),
|
|
26
|
+
timeout: z.number().optional().describe('Timeout in milliseconds'),
|
|
27
|
+
optional: z.boolean().optional().describe('If true, failure does not stop execution'),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const BuildPlanEnvironmentSchema = z.object({
|
|
31
|
+
runtime: z.enum(['node22', 'node24']).describe('Runtime to use (node22 or node24)'),
|
|
32
|
+
packageManager: z.enum(['npm', 'yarn', 'pnpm', 'bun']).describe('Package manager'),
|
|
33
|
+
requiredTools: z.array(z.string()).optional().describe('Additional required tools'),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const BuildPlanReasoningSchema = z.object({
|
|
37
|
+
summary: z.string().describe('Brief summary of the approach (1-2 sentences)'),
|
|
38
|
+
rationale: z.string().describe('Why this approach was chosen'),
|
|
39
|
+
concerns: z.array(z.string()).describe('Potential issues or concerns'),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const AIBuildPlanSchema = z.object({
|
|
43
|
+
environment: BuildPlanEnvironmentSchema,
|
|
44
|
+
steps: z.array(BuildPlanStepSchema).describe('Steps to execute in order'),
|
|
45
|
+
entryPoints: z.array(z.string()).describe('Entry point files to analyze'),
|
|
46
|
+
reasoning: BuildPlanReasoningSchema,
|
|
47
|
+
confidence: z.enum(['high', 'medium', 'low']).describe('Confidence in this plan'),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
type AIBuildPlanOutput = z.infer<typeof AIBuildPlanSchema>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Options for build plan generation.
|
|
54
|
+
*/
|
|
55
|
+
export interface GenerateBuildPlanOptions {
|
|
56
|
+
/** Target a specific package in a monorepo */
|
|
57
|
+
targetPackage?: string;
|
|
58
|
+
/** Override the default model */
|
|
59
|
+
model?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Format project context for the AI prompt.
|
|
64
|
+
*/
|
|
65
|
+
function formatContext(context: GitHubProjectContext): string {
|
|
66
|
+
const sections: string[] = [];
|
|
67
|
+
|
|
68
|
+
// Repository metadata
|
|
69
|
+
sections.push(`=== Repository ===
|
|
70
|
+
Owner: ${context.metadata.owner}
|
|
71
|
+
Repo: ${context.metadata.repo}
|
|
72
|
+
Language: ${context.metadata.language ?? 'unknown'}
|
|
73
|
+
Topics: ${context.metadata.topics.join(', ') || 'none'}
|
|
74
|
+
Description: ${context.metadata.description ?? 'none'}`);
|
|
75
|
+
|
|
76
|
+
// Environment detection
|
|
77
|
+
sections.push(`=== Environment ===
|
|
78
|
+
Package Manager: ${context.packageManager}
|
|
79
|
+
Is Monorepo: ${context.workspace.isMonorepo}
|
|
80
|
+
Workspace Tool: ${context.workspace.tool ?? 'none'}
|
|
81
|
+
Workspace Packages: ${context.workspace.packages?.join(', ') ?? 'none'}`);
|
|
82
|
+
|
|
83
|
+
// Build hints
|
|
84
|
+
sections.push(`=== Build Hints ===
|
|
85
|
+
Has TypeScript: ${context.buildHints.hasTypeScript}
|
|
86
|
+
Has WASM: ${context.buildHints.hasWasm}
|
|
87
|
+
Has Native Modules: ${context.buildHints.hasNativeModules}
|
|
88
|
+
Has Build Script: ${context.buildHints.hasBuildScript}
|
|
89
|
+
Build Script: ${context.buildHints.buildScript ?? 'none'}
|
|
90
|
+
Frameworks: ${context.buildHints.frameworks.join(', ') || 'none'}`);
|
|
91
|
+
|
|
92
|
+
// File contents
|
|
93
|
+
if (context.files.packageJson) {
|
|
94
|
+
const truncated =
|
|
95
|
+
context.files.packageJson.length > 3000
|
|
96
|
+
? `${context.files.packageJson.slice(0, 3000)}\n... (truncated)`
|
|
97
|
+
: context.files.packageJson;
|
|
98
|
+
sections.push(`=== package.json ===\n${truncated}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (context.files.tsconfigJson) {
|
|
102
|
+
sections.push(`=== tsconfig.json ===\n${context.files.tsconfigJson}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (context.files.lockfile) {
|
|
106
|
+
// Only include first 500 chars of lockfile
|
|
107
|
+
const preview = context.files.lockfile.content.slice(0, 500);
|
|
108
|
+
sections.push(`=== ${context.files.lockfile.name} (preview) ===\n${preview}\n...`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return sections.join('\n\n');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* System prompt for build plan generation.
|
|
116
|
+
*/
|
|
117
|
+
const 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.
|
|
118
|
+
|
|
119
|
+
The goal is to:
|
|
120
|
+
1. Install dependencies
|
|
121
|
+
2. Build the project (if needed) to generate TypeScript declarations
|
|
122
|
+
3. Identify entry points for API documentation analysis
|
|
123
|
+
|
|
124
|
+
Package Manager Selection:
|
|
125
|
+
- If a lockfile is detected (package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lockb), use that package manager
|
|
126
|
+
- If Package Manager is "unknown" (no lockfile), default to npm with "npm install" (NOT "npm ci")
|
|
127
|
+
- IMPORTANT: "npm ci" and "--frozen-lockfile" flags ONLY work when a lockfile exists
|
|
128
|
+
- When no lockfile: use "npm install", "yarn install", "pnpm install", or "bun install" (without frozen flags)
|
|
129
|
+
|
|
130
|
+
Install Commands by Package Manager:
|
|
131
|
+
- npm with lockfile: ["npm", "ci"]
|
|
132
|
+
- npm without lockfile: ["npm", "install"]
|
|
133
|
+
- yarn with lockfile: ["yarn", "install", "--frozen-lockfile"]
|
|
134
|
+
- yarn without lockfile: ["yarn", "install"]
|
|
135
|
+
- pnpm with lockfile: ["pnpm", "install", "--frozen-lockfile"]
|
|
136
|
+
- pnpm without lockfile: ["pnpm", "install"]
|
|
137
|
+
- bun with lockfile: ["bun", "install", "--frozen-lockfile"]
|
|
138
|
+
- bun without lockfile: ["bun", "install"]
|
|
139
|
+
|
|
140
|
+
General Guidelines:
|
|
141
|
+
- For TypeScript projects, look for "types" or "exports" fields in package.json
|
|
142
|
+
- For monorepos, focus on the target package if specified
|
|
143
|
+
- Common entry points: src/index.ts, dist/index.d.ts, lib/index.ts, distribution/index.d.ts
|
|
144
|
+
- WASM projects may need build steps before .d.ts files exist
|
|
145
|
+
- Be conservative with timeouts (default 60000ms, increase for builds)
|
|
146
|
+
- Installation is usually required first
|
|
147
|
+
- Build step is needed if package.json has a "build" script and the types are in dist/distribution folder
|
|
148
|
+
|
|
149
|
+
Step ID conventions:
|
|
150
|
+
- "install" - install dependencies
|
|
151
|
+
- "build" - main build step
|
|
152
|
+
- "build-types" - generate type declarations
|
|
153
|
+
- "analyze" - run doccov spec (added automatically)`;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Generate a build plan prompt.
|
|
157
|
+
*/
|
|
158
|
+
function generatePrompt(
|
|
159
|
+
context: GitHubProjectContext,
|
|
160
|
+
options: GenerateBuildPlanOptions,
|
|
161
|
+
): string {
|
|
162
|
+
let prompt = `Analyze this repository and generate a build plan:\n\n${formatContext(context)}`;
|
|
163
|
+
|
|
164
|
+
if (options.targetPackage) {
|
|
165
|
+
prompt += `\n\nTarget Package: ${options.targetPackage}
|
|
166
|
+
Focus the plan on building and analyzing only this package within the monorepo.`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
prompt += `\n\nGenerate a build plan with:
|
|
170
|
+
- environment: Runtime and package manager configuration
|
|
171
|
+
- steps: Ordered build steps (install, build, etc.)
|
|
172
|
+
- entryPoints: TypeScript entry files to analyze (relative paths)
|
|
173
|
+
- reasoning: Explain your approach
|
|
174
|
+
- confidence: How confident you are in this plan`;
|
|
175
|
+
|
|
176
|
+
return prompt;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get the Anthropic model for plan generation.
|
|
181
|
+
*/
|
|
182
|
+
function getModel() {
|
|
183
|
+
const anthropic = createAnthropic();
|
|
184
|
+
return anthropic('claude-sonnet-4-20250514');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Generate a build plan for a GitHub repository.
|
|
189
|
+
*/
|
|
190
|
+
export async function generateBuildPlan(
|
|
191
|
+
context: GitHubProjectContext,
|
|
192
|
+
options: GenerateBuildPlanOptions = {},
|
|
193
|
+
): Promise<BuildPlan> {
|
|
194
|
+
const model = getModel();
|
|
195
|
+
const prompt = generatePrompt(context, options);
|
|
196
|
+
|
|
197
|
+
const { object } = await generateObject({
|
|
198
|
+
model,
|
|
199
|
+
schema: AIBuildPlanSchema,
|
|
200
|
+
system: SYSTEM_PROMPT,
|
|
201
|
+
prompt,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
return transformToBuildPlan(object, context, options);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Transform AI output to full BuildPlan.
|
|
209
|
+
*/
|
|
210
|
+
function transformToBuildPlan(
|
|
211
|
+
output: AIBuildPlanOutput,
|
|
212
|
+
context: GitHubProjectContext,
|
|
213
|
+
options: GenerateBuildPlanOptions,
|
|
214
|
+
): BuildPlan {
|
|
215
|
+
const target: BuildPlanTarget = {
|
|
216
|
+
type: 'github',
|
|
217
|
+
repoUrl: `https://github.com/${context.metadata.owner}/${context.metadata.repo}`,
|
|
218
|
+
ref: context.ref,
|
|
219
|
+
rootPath: options.targetPackage,
|
|
220
|
+
entryPoints: output.entryPoints,
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const environment: BuildPlanEnvironment = {
|
|
224
|
+
runtime: output.environment.runtime,
|
|
225
|
+
packageManager: output.environment.packageManager,
|
|
226
|
+
requiredTools: output.environment.requiredTools,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const steps: BuildPlanStep[] = output.steps.map((step) => ({
|
|
230
|
+
id: step.id,
|
|
231
|
+
name: step.name,
|
|
232
|
+
command: step.command,
|
|
233
|
+
args: step.args,
|
|
234
|
+
cwd: step.cwd,
|
|
235
|
+
timeout: step.timeout,
|
|
236
|
+
optional: step.optional,
|
|
237
|
+
}));
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
version: '1.0.0',
|
|
241
|
+
generatedAt: new Date().toISOString(),
|
|
242
|
+
target,
|
|
243
|
+
environment,
|
|
244
|
+
steps,
|
|
245
|
+
reasoning: {
|
|
246
|
+
summary: output.reasoning.summary,
|
|
247
|
+
rationale: output.reasoning.rationale,
|
|
248
|
+
concerns: output.reasoning.concerns,
|
|
249
|
+
},
|
|
250
|
+
confidence: output.confidence,
|
|
251
|
+
};
|
|
252
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doccov/api",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "DocCov API - Badge endpoint and coverage services",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"doccov",
|
|
@@ -18,17 +18,20 @@
|
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"author": "Ryan Waits",
|
|
20
20
|
"type": "module",
|
|
21
|
-
"main": "./src/index.ts",
|
|
22
21
|
"scripts": {
|
|
22
|
+
"build": "bunup",
|
|
23
23
|
"dev": "bun run --hot src/index.ts",
|
|
24
24
|
"start": "bun run src/index.ts",
|
|
25
|
-
"lint": "biome check src/",
|
|
26
|
-
"lint:fix": "biome check --write src/",
|
|
27
|
-
"format": "biome format --write src/"
|
|
25
|
+
"lint": "biome check src/ functions/ lib/",
|
|
26
|
+
"lint:fix": "biome check --write src/ functions/ lib/",
|
|
27
|
+
"format": "biome format --write src/ functions/ lib/"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@
|
|
30
|
+
"@ai-sdk/anthropic": "^2.0.55",
|
|
31
|
+
"@doccov/sdk": "^0.13.0",
|
|
32
|
+
"@openpkg-ts/spec": "^0.9.0",
|
|
31
33
|
"@vercel/sandbox": "^1.0.3",
|
|
34
|
+
"ai": "^5.0.111",
|
|
32
35
|
"hono": "^4.0.0",
|
|
33
36
|
"ms": "^2.1.3",
|
|
34
37
|
"zod": "^3.25.0"
|
|
@@ -38,6 +41,7 @@
|
|
|
38
41
|
"@types/ms": "^0.7.34",
|
|
39
42
|
"@types/node": "^20.0.0",
|
|
40
43
|
"@vercel/node": "^3.0.0",
|
|
44
|
+
"bunup": "latest",
|
|
41
45
|
"typescript": "^5.0.0"
|
|
42
46
|
}
|
|
43
47
|
}
|
package/src/index.ts
CHANGED
|
@@ -2,20 +2,20 @@ import { Hono } from 'hono';
|
|
|
2
2
|
import { cors } from 'hono/cors';
|
|
3
3
|
import { rateLimit } from './middleware/rate-limit';
|
|
4
4
|
import { badgeRoute } from './routes/badge';
|
|
5
|
-
import {
|
|
5
|
+
import { planRoute } from './routes/plan';
|
|
6
6
|
|
|
7
7
|
const app = new Hono();
|
|
8
8
|
|
|
9
9
|
// Middleware
|
|
10
10
|
app.use('*', cors());
|
|
11
11
|
|
|
12
|
-
// Rate limit /
|
|
12
|
+
// Rate limit /plan endpoint: 10 requests per minute per IP
|
|
13
13
|
app.use(
|
|
14
|
-
'/
|
|
14
|
+
'/plan',
|
|
15
15
|
rateLimit({
|
|
16
16
|
windowMs: 60 * 1000,
|
|
17
17
|
max: 10,
|
|
18
|
-
message: 'Too many
|
|
18
|
+
message: 'Too many plan requests. Please try again in a minute.',
|
|
19
19
|
}),
|
|
20
20
|
);
|
|
21
21
|
|
|
@@ -23,10 +23,12 @@ app.use(
|
|
|
23
23
|
app.get('/', (c) => {
|
|
24
24
|
return c.json({
|
|
25
25
|
name: 'DocCov API',
|
|
26
|
-
version: '0.
|
|
26
|
+
version: '0.4.0',
|
|
27
27
|
endpoints: {
|
|
28
28
|
badge: '/badge/:owner/:repo',
|
|
29
|
-
|
|
29
|
+
plan: '/plan',
|
|
30
|
+
execute: '/execute',
|
|
31
|
+
'execute-stream': '/execute-stream',
|
|
30
32
|
health: '/health',
|
|
31
33
|
},
|
|
32
34
|
});
|
|
@@ -38,7 +40,7 @@ app.get('/health', (c) => {
|
|
|
38
40
|
|
|
39
41
|
// Routes
|
|
40
42
|
app.route('/badge', badgeRoute);
|
|
41
|
-
app.route('/
|
|
43
|
+
app.route('/plan', planRoute);
|
|
42
44
|
|
|
43
45
|
// Vercel serverless handler + Bun auto-serves this export
|
|
44
46
|
export default app;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan route for local development (mirrors api/plan.ts)
|
|
3
|
+
* Does NOT use Vercel Sandbox - only GitHub API + Anthropic Claude
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { fetchGitHubContext, parseScanGitHubUrl } from '@doccov/sdk';
|
|
7
|
+
import { Hono } from 'hono';
|
|
8
|
+
import { generateBuildPlan } from '../../lib/plan-agent';
|
|
9
|
+
|
|
10
|
+
export const planRoute = new Hono();
|
|
11
|
+
|
|
12
|
+
planRoute.post('/', async (c) => {
|
|
13
|
+
const body = await c.req.json<{ url: string; ref?: string; package?: string }>();
|
|
14
|
+
|
|
15
|
+
if (!body.url) {
|
|
16
|
+
return c.json({ error: 'url is required' }, 400);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Validate URL format
|
|
20
|
+
let repoUrl: string;
|
|
21
|
+
try {
|
|
22
|
+
const parsed = parseScanGitHubUrl(body.url);
|
|
23
|
+
if (!parsed) {
|
|
24
|
+
return c.json({ error: 'Invalid GitHub URL' }, 400);
|
|
25
|
+
}
|
|
26
|
+
repoUrl = `https://github.com/${parsed.owner}/${parsed.repo}`;
|
|
27
|
+
} catch {
|
|
28
|
+
return c.json({ error: 'Invalid GitHub URL' }, 400);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
// Fetch project context from GitHub
|
|
33
|
+
const context = await fetchGitHubContext(repoUrl, body.ref);
|
|
34
|
+
|
|
35
|
+
// Check for private repos
|
|
36
|
+
if (context.metadata.isPrivate) {
|
|
37
|
+
return c.json({
|
|
38
|
+
error: 'Private repositories are not supported',
|
|
39
|
+
hint: 'Use a public repository or run doccov locally',
|
|
40
|
+
}, 403);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Generate build plan using AI
|
|
44
|
+
const plan = await generateBuildPlan(context, {
|
|
45
|
+
targetPackage: body.package,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return c.json({
|
|
49
|
+
plan,
|
|
50
|
+
context: {
|
|
51
|
+
owner: context.metadata.owner,
|
|
52
|
+
repo: context.metadata.repo,
|
|
53
|
+
ref: context.ref,
|
|
54
|
+
packageManager: context.packageManager,
|
|
55
|
+
isMonorepo: context.workspace.isMonorepo,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error('Plan generation error:', error);
|
|
60
|
+
|
|
61
|
+
if (error instanceof Error) {
|
|
62
|
+
if (error.message.includes('404') || error.message.includes('not found')) {
|
|
63
|
+
return c.json({ error: 'Repository not found' }, 404);
|
|
64
|
+
}
|
|
65
|
+
if (error.message.includes('rate limit')) {
|
|
66
|
+
return c.json({ error: 'GitHub API rate limit exceeded' }, 429);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return c.json({
|
|
71
|
+
error: 'Failed to generate build plan',
|
|
72
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
73
|
+
}, 500);
|
|
74
|
+
}
|
|
75
|
+
});
|
package/tsconfig.json
CHANGED
package/vercel.json
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
+
"installCommand": "bun install",
|
|
3
|
+
"buildCommand": "bun run build",
|
|
2
4
|
"rewrites": [
|
|
3
5
|
{ "source": "/badge/:path*", "destination": "/api" },
|
|
4
6
|
{ "source": "/spec/:path*", "destination": "/api" },
|
|
5
|
-
{ "source": "/
|
|
6
|
-
{ "source": "/
|
|
7
|
-
{ "source": "/
|
|
7
|
+
{ "source": "/plan", "destination": "/api/plan" },
|
|
8
|
+
{ "source": "/execute", "destination": "/api/execute" },
|
|
9
|
+
{ "source": "/execute-stream", "destination": "/api/execute-stream" },
|
|
8
10
|
{ "source": "/(.*)", "destination": "/api" }
|
|
9
11
|
]
|
|
10
12
|
}
|
package/api/scan/detect.ts
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Detect endpoint - uses SDK detection via SandboxFileSystem.
|
|
3
|
-
* Detects monorepo structure and package manager for a GitHub repository.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
detectMonorepo as sdkDetectMonorepo,
|
|
8
|
-
detectPackageManager,
|
|
9
|
-
SandboxFileSystem,
|
|
10
|
-
} from '@doccov/sdk';
|
|
11
|
-
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
|
12
|
-
import { Sandbox } from '@vercel/sandbox';
|
|
13
|
-
|
|
14
|
-
export const config = {
|
|
15
|
-
runtime: 'nodejs',
|
|
16
|
-
maxDuration: 60, // Quick detection, 1 minute max
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
interface DetectRequestBody {
|
|
20
|
-
url: string;
|
|
21
|
-
ref?: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface PackageInfo {
|
|
25
|
-
name: string;
|
|
26
|
-
path: string;
|
|
27
|
-
description?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface DetectResponse {
|
|
31
|
-
isMonorepo: boolean;
|
|
32
|
-
packageManager: 'npm' | 'pnpm' | 'bun' | 'yarn';
|
|
33
|
-
packages?: PackageInfo[];
|
|
34
|
-
defaultPackage?: string;
|
|
35
|
-
error?: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
39
|
-
// CORS
|
|
40
|
-
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
41
|
-
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
|
42
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
43
|
-
|
|
44
|
-
if (req.method === 'OPTIONS') {
|
|
45
|
-
return res.status(200).end();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (req.method !== 'POST') {
|
|
49
|
-
return res.status(405).json({ error: 'Method not allowed' });
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const body = req.body as DetectRequestBody;
|
|
53
|
-
|
|
54
|
-
if (!body.url) {
|
|
55
|
-
return res.status(400).json({ error: 'url is required' });
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
const result = await detectRepoStructure(body.url);
|
|
60
|
-
return res.status(200).json(result);
|
|
61
|
-
} catch (error) {
|
|
62
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
63
|
-
return res.status(500).json({
|
|
64
|
-
isMonorepo: false,
|
|
65
|
-
packageManager: 'npm',
|
|
66
|
-
error: message,
|
|
67
|
-
} as DetectResponse);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Detect repository structure using SDK utilities via SandboxFileSystem.
|
|
73
|
-
*/
|
|
74
|
-
async function detectRepoStructure(url: string): Promise<DetectResponse> {
|
|
75
|
-
const sandbox = await Sandbox.create({
|
|
76
|
-
source: {
|
|
77
|
-
url,
|
|
78
|
-
type: 'git',
|
|
79
|
-
},
|
|
80
|
-
resources: { vcpus: 2 },
|
|
81
|
-
timeout: 60 * 1000, // 1 minute
|
|
82
|
-
runtime: 'node22',
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
try {
|
|
86
|
-
// Create SDK FileSystem abstraction for sandbox
|
|
87
|
-
const fs = new SandboxFileSystem(sandbox);
|
|
88
|
-
|
|
89
|
-
// Use SDK detection functions
|
|
90
|
-
const [monoInfo, pmInfo] = await Promise.all([
|
|
91
|
-
sdkDetectMonorepo(fs),
|
|
92
|
-
detectPackageManager(fs),
|
|
93
|
-
]);
|
|
94
|
-
|
|
95
|
-
if (!monoInfo.isMonorepo) {
|
|
96
|
-
return {
|
|
97
|
-
isMonorepo: false,
|
|
98
|
-
packageManager: pmInfo.name,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Map SDK package info to API response format
|
|
103
|
-
const packages: PackageInfo[] = monoInfo.packages
|
|
104
|
-
.filter((p) => !p.private)
|
|
105
|
-
.map((p) => ({
|
|
106
|
-
name: p.name,
|
|
107
|
-
path: p.path,
|
|
108
|
-
description: p.description,
|
|
109
|
-
}))
|
|
110
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
isMonorepo: true,
|
|
114
|
-
packageManager: pmInfo.name,
|
|
115
|
-
packages,
|
|
116
|
-
defaultPackage: packages[0]?.name,
|
|
117
|
-
};
|
|
118
|
-
} finally {
|
|
119
|
-
await sandbox.stop();
|
|
120
|
-
}
|
|
121
|
-
}
|