@doccov/api 0.2.1 → 0.2.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @doccov/api
2
2
 
3
+ ## 0.2.2
4
+
5
+ ### Patch Changes
6
+
7
+ - feat(cli): add markdown docs impact detection to diff command
8
+ refactor(cli): consolidate fix functionality into check command
9
+ refactor(sdk): reuse detection extraction
10
+ fix(api): bug in api scan
11
+ fix(api): monorepo detection in scan
12
+ fix(api): improve scan-stream reliability and ref support
13
+
3
14
  ## 0.2.1
4
15
 
5
16
  ### Patch Changes
@@ -1,6 +1,14 @@
1
1
  import { Writable } from 'node:stream';
2
2
  import type { VercelRequest, VercelResponse } from '@vercel/node';
3
3
  import { Sandbox } from '@vercel/sandbox';
4
+ import {
5
+ SandboxFileSystem,
6
+ detectBuildInfo,
7
+ detectMonorepo,
8
+ detectPackageManager,
9
+ getInstallCommand,
10
+ getPrimaryBuildScript,
11
+ } from '@doccov/sdk';
4
12
 
5
13
  export const config = {
6
14
  runtime: 'nodejs',
@@ -13,6 +21,7 @@ interface JobEvent {
13
21
  message?: string;
14
22
  progress?: number;
15
23
  result?: ScanResult;
24
+ availablePackages?: string[];
16
25
  }
17
26
 
18
27
  interface ScanResult {
@@ -120,6 +129,42 @@ async function runScanWithProgress(
120
129
  });
121
130
 
122
131
  try {
132
+ // Create filesystem abstraction for SDK detection functions
133
+ const fs = new SandboxFileSystem(sandbox);
134
+
135
+ // Checkout specific ref if not main/master
136
+ if (options.ref && options.ref !== 'main' && options.ref !== 'master') {
137
+ sendEvent({
138
+ type: 'progress',
139
+ stage: 'cloning',
140
+ message: `Checking out ${options.ref}...`,
141
+ progress: 7,
142
+ });
143
+
144
+ const checkoutCapture = createCaptureStream();
145
+ const checkoutResult = await sandbox.runCommand({
146
+ cmd: 'git',
147
+ args: ['checkout', options.ref],
148
+ stdout: checkoutCapture.stream,
149
+ stderr: checkoutCapture.stream,
150
+ });
151
+
152
+ if (checkoutResult.exitCode !== 0) {
153
+ // Try fetching the ref first (might be a tag not fetched by shallow clone)
154
+ await sandbox.runCommand({
155
+ cmd: 'git',
156
+ args: ['fetch', '--depth', '1', 'origin', `refs/tags/${options.ref}:refs/tags/${options.ref}`],
157
+ });
158
+ const retryResult = await sandbox.runCommand({
159
+ cmd: 'git',
160
+ args: ['checkout', options.ref],
161
+ });
162
+ if (retryResult.exitCode !== 0) {
163
+ throw new Error(`Failed to checkout ${options.ref}: ${checkoutCapture.getOutput()}`);
164
+ }
165
+ }
166
+ }
167
+
123
168
  sendEvent({
124
169
  type: 'progress',
125
170
  stage: 'detecting',
@@ -127,57 +172,57 @@ async function runScanWithProgress(
127
172
  progress: 10,
128
173
  });
129
174
 
130
- // Detect package manager
131
- const lsCapture = createCaptureStream();
132
- await sandbox.runCommand({
133
- cmd: 'ls',
134
- args: ['-1'],
135
- stdout: lsCapture.stream,
136
- });
137
- const files = lsCapture.getOutput();
138
-
139
- let installCmd: string;
140
- let installArgs: string[];
141
- let pm: 'pnpm' | 'bun' | 'npm' = 'npm';
142
- let pmMessage = 'Detected npm project';
143
-
144
- if (files.includes('pnpm-lock.yaml')) {
145
- pm = 'pnpm';
146
- installCmd = 'pnpm';
147
- installArgs = ['install', '--frozen-lockfile'];
148
- pmMessage = 'Detected pnpm monorepo';
149
- } else if (files.includes('bun.lock') || files.includes('bun.lockb')) {
150
- pm = 'bun';
151
- installCmd = 'bun';
152
- installArgs = ['install', '--frozen-lockfile'];
153
- pmMessage = 'Detected bun project';
154
- } else {
155
- installCmd = 'npm';
156
- installArgs = ['install', '--ignore-scripts', '--legacy-peer-deps'];
157
- }
158
-
175
+ // Detect package manager using SDK
176
+ const pmInfo = await detectPackageManager(fs);
177
+ const pmMessage = pmInfo.lockfile ? `Detected ${pmInfo.name} project` : 'No lockfile detected';
159
178
  sendEvent({ type: 'progress', stage: 'detecting', message: pmMessage, progress: 15 });
160
179
 
161
- // Install package manager if needed
162
- if (pm === 'pnpm') {
180
+ // Early monorepo detection - fail fast if monorepo without package param
181
+ if (!options.package) {
182
+ const mono = await detectMonorepo(fs);
183
+
184
+ if (mono.isMonorepo) {
185
+ sendEvent({
186
+ type: 'progress',
187
+ stage: 'detecting',
188
+ message: 'Monorepo detected, listing packages...',
189
+ progress: 17,
190
+ });
191
+
192
+ const availablePackages = mono.packages
193
+ .filter((p) => !p.private)
194
+ .map((p) => p.name);
195
+
196
+ await sandbox.stop();
197
+ sendEvent({
198
+ type: 'error',
199
+ message: `Monorepo detected. Please specify a package to analyze using the 'package' query parameter.`,
200
+ availablePackages,
201
+ });
202
+ return;
203
+ }
204
+ }
205
+
206
+ // Install package manager if needed (npm and pnpm are pre-installed in node22)
207
+ if (pmInfo.name === 'bun') {
163
208
  sendEvent({
164
209
  type: 'progress',
165
210
  stage: 'installing',
166
- message: 'Installing pnpm...',
211
+ message: 'Installing bun...',
167
212
  progress: 18,
168
213
  });
169
- await sandbox.runCommand({ cmd: 'npm', args: ['install', '-g', 'pnpm'] });
170
- } else if (pm === 'bun') {
214
+ await sandbox.runCommand({ cmd: 'npm', args: ['install', '-g', 'bun'] });
215
+ } else if (pmInfo.name === 'yarn') {
171
216
  sendEvent({
172
217
  type: 'progress',
173
218
  stage: 'installing',
174
- message: 'Installing bun...',
219
+ message: 'Installing yarn...',
175
220
  progress: 18,
176
221
  });
177
- await sandbox.runCommand({ cmd: 'npm', args: ['install', '-g', 'bun'] });
222
+ await sandbox.runCommand({ cmd: 'npm', args: ['install', '-g', 'yarn'] });
178
223
  }
179
224
 
180
- // Install dependencies
225
+ // Install dependencies with fallback chain
181
226
  sendEvent({
182
227
  type: 'progress',
183
228
  stage: 'installing',
@@ -185,60 +230,99 @@ async function runScanWithProgress(
185
230
  progress: 20,
186
231
  });
187
232
 
233
+ let installed = false;
234
+ let activePm = pmInfo.name;
188
235
  const installCapture = createCaptureStream();
189
- const install = await sandbox.runCommand({
190
- cmd: installCmd,
191
- args: installArgs,
236
+
237
+ // Try primary package manager using SDK's getInstallCommand
238
+ const primaryCmd = getInstallCommand(pmInfo);
239
+ const primaryResult = await sandbox.runCommand({
240
+ cmd: primaryCmd[0],
241
+ args: primaryCmd.slice(1),
192
242
  stdout: installCapture.stream,
193
243
  stderr: installCapture.stream,
194
244
  });
195
245
 
196
- if (install.exitCode !== 0) {
197
- throw new Error(`${installCmd} install failed: ${installCapture.getOutput().slice(-300)}`);
198
- }
199
-
200
- sendEvent({
201
- type: 'progress',
202
- stage: 'installing',
203
- message: 'Dependencies installed',
204
- progress: 40,
205
- });
206
-
207
- // Check for build script
208
- const pkgCapture = createCaptureStream();
209
- await sandbox.runCommand({
210
- cmd: 'cat',
211
- args: ['package.json'],
212
- stdout: pkgCapture.stream,
213
- });
214
-
215
- try {
216
- const pkgJson = JSON.parse(pkgCapture.getOutput()) as { scripts?: Record<string, string> };
217
- const scripts = pkgJson.scripts ?? {};
218
- const buildScript = scripts.build ? 'build' : scripts.compile ? 'compile' : null;
246
+ if (primaryResult.exitCode === 0) {
247
+ installed = true;
248
+ } else {
249
+ const errorOutput = installCapture.getOutput();
219
250
 
220
- if (buildScript) {
251
+ // Check if it's a workspace:* protocol error - try bun fallback
252
+ if (errorOutput.includes('workspace:') || errorOutput.includes('EUNSUPPORTEDPROTOCOL')) {
221
253
  sendEvent({
222
254
  type: 'progress',
223
- stage: 'building',
224
- message: 'Running build...',
225
- progress: 45,
255
+ stage: 'installing',
256
+ message: 'Trying bun fallback for workspace protocol...',
257
+ progress: 25,
226
258
  });
227
259
 
228
- const buildCapture = createCaptureStream();
229
- const buildResult = await sandbox.runCommand({
230
- cmd: pm === 'npm' ? 'npm' : pm,
231
- args: pm === 'npm' ? ['run', buildScript] : [buildScript],
232
- stdout: buildCapture.stream,
233
- stderr: buildCapture.stream,
260
+ // Install bun if not already the primary
261
+ if (pmInfo.name !== 'bun') {
262
+ await sandbox.runCommand({ cmd: 'npm', args: ['install', '-g', 'bun'] });
263
+ }
264
+
265
+ const bunCapture = createCaptureStream();
266
+ const bunResult = await sandbox.runCommand({
267
+ cmd: 'bun',
268
+ args: ['install'],
269
+ stdout: bunCapture.stream,
270
+ stderr: bunCapture.stream,
234
271
  });
235
272
 
236
- const buildMessage =
237
- buildResult.exitCode === 0 ? 'Build complete' : 'Build failed (continuing)';
238
- sendEvent({ type: 'progress', stage: 'building', message: buildMessage, progress: 55 });
273
+ if (bunResult.exitCode === 0) {
274
+ installed = true;
275
+ activePm = 'bun'; // Update pm for build step
276
+ }
239
277
  }
240
- } catch {
241
- // Ignore package.json errors
278
+ }
279
+
280
+ if (installed) {
281
+ sendEvent({
282
+ type: 'progress',
283
+ stage: 'installing',
284
+ message: 'Dependencies installed',
285
+ progress: 40,
286
+ });
287
+ } else {
288
+ // Graceful degradation - continue with limited analysis
289
+ sendEvent({
290
+ type: 'progress',
291
+ stage: 'installing',
292
+ message: 'Install failed (continuing with limited analysis)',
293
+ progress: 40,
294
+ });
295
+ }
296
+
297
+ // Check for build script using SDK
298
+ const buildInfo = await detectBuildInfo(fs);
299
+ const buildScript = getPrimaryBuildScript(buildInfo);
300
+
301
+ if (buildScript) {
302
+ sendEvent({
303
+ type: 'progress',
304
+ stage: 'building',
305
+ message: 'Running build...',
306
+ progress: 45,
307
+ });
308
+
309
+ const buildCapture = createCaptureStream();
310
+ // Use activePm (may have changed to bun as fallback)
311
+ const buildCmd =
312
+ activePm === 'npm' || activePm === 'yarn'
313
+ ? [activePm, 'run', buildScript]
314
+ : [activePm, buildScript];
315
+
316
+ const buildResult = await sandbox.runCommand({
317
+ cmd: buildCmd[0],
318
+ args: buildCmd.slice(1),
319
+ stdout: buildCapture.stream,
320
+ stderr: buildCapture.stream,
321
+ });
322
+
323
+ const buildMessage =
324
+ buildResult.exitCode === 0 ? 'Build complete' : 'Build failed (continuing)';
325
+ sendEvent({ type: 'progress', stage: 'building', message: buildMessage, progress: 55 });
242
326
  }
243
327
 
244
328
  // Install doccov CLI
@@ -290,30 +374,54 @@ async function runScanWithProgress(
290
374
  progress: 85,
291
375
  });
292
376
 
293
- // Extract summary
377
+ // Check if spec file was created
378
+ const checkFileCapture = createCaptureStream();
379
+ await sandbox.runCommand({
380
+ cmd: 'cat',
381
+ args: [specFile],
382
+ stdout: checkFileCapture.stream,
383
+ stderr: checkFileCapture.stream,
384
+ });
385
+ const specContent = checkFileCapture.getOutput();
386
+
387
+ if (!specContent.trim() || specContent.includes('No such file')) {
388
+ throw new Error(`Spec file not found or empty. Generate output: ${genOutput.slice(-500)}`);
389
+ }
390
+
391
+ // Extract summary with error handling
294
392
  const extractScript = `
295
393
  const fs = require('fs');
296
- const spec = JSON.parse(fs.readFileSync('${specFile}', 'utf-8'));
297
- const undocumented = [];
298
- const drift = [];
299
- for (const exp of spec.exports || []) {
300
- const docs = exp.docs;
301
- if (!docs) continue;
302
- if ((docs.missing?.length || 0) > 0 || (docs.coverageScore || 0) < 100) {
303
- undocumented.push(exp.name);
394
+ try {
395
+ if (!fs.existsSync('${specFile}')) {
396
+ console.error('Spec file not found: ${specFile}');
397
+ process.exit(1);
304
398
  }
305
- for (const d of docs.drift || []) {
306
- drift.push({ export: exp.name, type: d.type, issue: d.issue });
399
+ const content = fs.readFileSync('${specFile}', 'utf-8');
400
+ const spec = JSON.parse(content);
401
+ const undocumented = [];
402
+ const drift = [];
403
+ for (const exp of spec.exports || []) {
404
+ const docs = exp.docs;
405
+ if (!docs) continue;
406
+ if ((docs.missing?.length || 0) > 0 || (docs.coverageScore || 0) < 100) {
407
+ undocumented.push(exp.name);
408
+ }
409
+ for (const d of docs.drift || []) {
410
+ drift.push({ export: exp.name, type: d.type, issue: d.issue });
411
+ }
307
412
  }
413
+ console.log(JSON.stringify({
414
+ coverage: spec.docs?.coverageScore || 0,
415
+ exportCount: spec.exports?.length || 0,
416
+ typeCount: spec.types?.length || 0,
417
+ undocumented: undocumented.slice(0, 50),
418
+ drift: drift.slice(0, 20),
419
+ driftCount: drift.length,
420
+ }));
421
+ } catch (e) {
422
+ console.error('Extract error:', e.message);
423
+ process.exit(1);
308
424
  }
309
- console.log(JSON.stringify({
310
- coverage: spec.docs?.coverageScore || 0,
311
- exportCount: spec.exports?.length || 0,
312
- typeCount: spec.types?.length || 0,
313
- undocumented: undocumented.slice(0, 50),
314
- drift: drift.slice(0, 20),
315
- driftCount: drift.length,
316
- }));
317
425
  `.replace(/\n/g, ' ');
318
426
 
319
427
  const nodeCapture = createCaptureStream();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doccov/api",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "DocCov API - Badge endpoint and coverage services",
5
5
  "keywords": [
6
6
  "doccov",