flatlock 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/bin/flatcover.js +322 -58
  2. package/bin/flatlock-cmp.js +2 -2
  3. package/dist/compare.d.ts +85 -0
  4. package/dist/compare.d.ts.map +1 -0
  5. package/dist/detect.d.ts +33 -0
  6. package/dist/detect.d.ts.map +1 -0
  7. package/dist/index.d.ts +72 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/parsers/index.d.ts +5 -0
  10. package/dist/parsers/index.d.ts.map +1 -0
  11. package/dist/parsers/npm.d.ts +154 -0
  12. package/dist/parsers/npm.d.ts.map +1 -0
  13. package/dist/parsers/pnpm/detect.d.ts +136 -0
  14. package/dist/parsers/pnpm/detect.d.ts.map +1 -0
  15. package/dist/parsers/pnpm/index.d.ts +154 -0
  16. package/dist/parsers/pnpm/index.d.ts.map +1 -0
  17. package/dist/parsers/pnpm/internal.d.ts +5 -0
  18. package/dist/parsers/pnpm/internal.d.ts.map +1 -0
  19. package/dist/parsers/pnpm/shrinkwrap.d.ts +129 -0
  20. package/dist/parsers/pnpm/shrinkwrap.d.ts.map +1 -0
  21. package/dist/parsers/pnpm/v5.d.ts +139 -0
  22. package/dist/parsers/pnpm/v5.d.ts.map +1 -0
  23. package/dist/parsers/pnpm/v6plus.d.ts +212 -0
  24. package/dist/parsers/pnpm/v6plus.d.ts.map +1 -0
  25. package/dist/parsers/pnpm.d.ts +2 -0
  26. package/dist/parsers/pnpm.d.ts.map +1 -0
  27. package/dist/parsers/types.d.ts +23 -0
  28. package/dist/parsers/types.d.ts.map +1 -0
  29. package/dist/parsers/yarn-berry.d.ts +197 -0
  30. package/dist/parsers/yarn-berry.d.ts.map +1 -0
  31. package/dist/parsers/yarn-classic.d.ts +110 -0
  32. package/dist/parsers/yarn-classic.d.ts.map +1 -0
  33. package/dist/result.d.ts +12 -0
  34. package/dist/result.d.ts.map +1 -0
  35. package/dist/set.d.ts +238 -0
  36. package/dist/set.d.ts.map +1 -0
  37. package/package.json +9 -4
  38. package/src/compare.js +5 -7
package/bin/flatcover.js CHANGED
@@ -14,6 +14,9 @@
14
14
 
15
15
  import { parseArgs } from 'node:util';
16
16
  import { readFileSync } from 'node:fs';
17
+ import { readFile, writeFile, rename, mkdir } from 'node:fs/promises';
18
+ import { createReadStream } from 'node:fs';
19
+ import { createInterface } from 'node:readline';
17
20
  import { dirname, join } from 'node:path';
18
21
  import { Pool, RetryAgent } from 'undici';
19
22
  import { FlatlockSet } from '../src/set.js';
@@ -21,6 +24,7 @@ import { FlatlockSet } from '../src/set.js';
21
24
  const { values, positionals } = parseArgs({
22
25
  options: {
23
26
  workspace: { type: 'string', short: 'w' },
27
+ list: { type: 'string', short: 'l' },
24
28
  dev: { type: 'boolean', default: false },
25
29
  peer: { type: 'boolean', default: true },
26
30
  specs: { type: 'boolean', short: 's', default: false },
@@ -34,25 +38,39 @@ const { values, positionals } = parseArgs({
34
38
  concurrency: { type: 'string', default: '20' },
35
39
  progress: { type: 'boolean', default: false },
36
40
  summary: { type: 'boolean', default: false },
41
+ before: { type: 'string', short: 'b' },
42
+ cache: { type: 'string', short: 'c' },
37
43
  help: { type: 'boolean', short: 'h' }
38
44
  },
39
45
  allowPositionals: true
40
46
  });
41
47
 
42
- if (values.help || positionals.length === 0) {
48
+ // Check if stdin input is requested via '-' positional argument (Unix convention)
49
+ const useStdin = positionals[0] === '-';
50
+
51
+ // Determine if we have a valid input source
52
+ const hasInputSource = positionals.length > 0 || values.list;
53
+
54
+ if (values.help || !hasInputSource) {
43
55
  console.log(`flatcover - Check lockfile package coverage against a registry
44
56
 
45
57
  Usage:
46
58
  flatcover <lockfile> --cover
47
- flatcover <lockfile> --cover --registry <url>
59
+ flatcover --list packages.json --cover
60
+ cat packages.ndjson | flatcover - --cover
48
61
  flatcover <lockfile> --cover --registry <url> --auth user:pass
49
62
 
63
+ Input sources (mutually exclusive):
64
+ <lockfile> Parse lockfile (package-lock.json, pnpm-lock.yaml, yarn.lock)
65
+ -l, --list <file> Read JSON array of {name, version} objects from file
66
+ - Read NDJSON {name, version} objects from stdin (one per line)
67
+
50
68
  Options:
51
- -w, --workspace <path> Workspace path within monorepo
69
+ -w, --workspace <path> Workspace path within monorepo (lockfile mode only)
52
70
  -s, --specs Include version (name@version or {name,version})
53
71
  --json Output as JSON array
54
72
  --ndjson Output as newline-delimited JSON (streaming)
55
- --full Include all metadata (integrity, resolved)
73
+ --full Include all metadata (integrity, resolved, time)
56
74
  --dev Include dev dependencies (default: false)
57
75
  --peer Include peer dependencies (default: true)
58
76
  -h, --help Show this help
@@ -65,17 +83,38 @@ Coverage options:
65
83
  --concurrency <n> Concurrent requests (default: 20)
66
84
  --progress Show progress on stderr
67
85
  --summary Show coverage summary on stderr
86
+ --before <date> Only count versions published before this ISO date
87
+ -c, --cache <dir> Cache packuments to disk for faster subsequent runs
68
88
 
69
89
  Output formats (with --cover):
70
- (default) CSV: package,version,present
71
- --json [{"name":"...","version":"...","present":true}, ...]
72
- --ndjson {"name":"...","version":"...","present":true} per line
90
+ (default) CSV format (sorted by name, version)
91
+ --json JSON array (sorted by name, version)
92
+ --ndjson Newline-delimited JSON (streaming, unsorted)
93
+
94
+ Output fields:
95
+ (default) name, version, present
96
+ --full Adds: spec, integrity, resolved, time (works with all formats)
73
97
 
74
98
  Examples:
99
+ # From lockfile
75
100
  flatcover package-lock.json --cover
101
+ flatcover package-lock.json --cover --full --json
102
+
103
+ # From JSON list file
104
+ flatcover --list packages.json --cover --summary
105
+ echo '[{"name":"lodash","version":"4.17.21"}]' > pkgs.json && flatcover -l pkgs.json --cover
106
+
107
+ # Time-travel reanalysis: capture full output with timestamps
108
+ flatcover package-lock.json --cover --full --json > coverage.json
109
+ # Later, filter locally by publication date without re-fetching registry
110
+
111
+ # From stdin (NDJSON) - use '-' to read from stdin
112
+ echo '{"name":"lodash","version":"4.17.21"}' | flatcover - --cover
113
+ cat packages.ndjson | flatcover - --cover --json
114
+
115
+ # With custom registry
76
116
  flatcover package-lock.json --cover --registry https://npm.pkg.github.com --token ghp_xxx
77
- flatcover pnpm-lock.yaml --cover --auth admin:secret --ndjson
78
- flatcover pnpm-lock.yaml -w packages/core --cover --summary`);
117
+ flatcover pnpm-lock.yaml --cover --auth admin:secret --ndjson`);
79
118
  process.exit(values.help ? 0 : 1);
80
119
  }
81
120
 
@@ -89,6 +128,19 @@ if (values.auth && values.token) {
89
128
  process.exit(1);
90
129
  }
91
130
 
131
+ // Validate mutually exclusive input sources
132
+ // Note: useStdin means positionals[0] === '-', so it's already counted in positionals.length
133
+ if (positionals.length > 0 && values.list) {
134
+ console.error('Error: Cannot use both lockfile/stdin and --list');
135
+ process.exit(1);
136
+ }
137
+
138
+ // --workspace only works with lockfile input (not stdin or --list)
139
+ if (values.workspace && (useStdin || values.list || !positionals.length)) {
140
+ console.error('Error: --workspace can only be used with lockfile input');
141
+ process.exit(1);
142
+ }
143
+
92
144
  // --full implies --specs
93
145
  if (values.full) {
94
146
  values.specs = true;
@@ -102,6 +154,70 @@ if (values.cover) {
102
154
  const lockfilePath = positionals[0];
103
155
  const concurrency = Math.max(1, Math.min(50, Number.parseInt(values.concurrency, 10) || 20));
104
156
 
157
+ /**
158
+ * Read packages from a JSON list file
159
+ * @param {string} filePath - Path to JSON file containing [{name, version}, ...]
160
+ * @returns {Array<{ name: string, version: string }>}
161
+ */
162
+ function readJsonList(filePath) {
163
+ const content = readFileSync(filePath, 'utf8');
164
+ const data = JSON.parse(content);
165
+
166
+ if (!Array.isArray(data)) {
167
+ throw new Error('--list file must contain a JSON array');
168
+ }
169
+
170
+ const packages = [];
171
+ for (const item of data) {
172
+ if (!item.name || !item.version) {
173
+ throw new Error('Each item in --list must have "name" and "version" fields');
174
+ }
175
+ packages.push({
176
+ name: item.name,
177
+ version: item.version,
178
+ integrity: item.integrity,
179
+ resolved: item.resolved
180
+ });
181
+ }
182
+
183
+ return packages;
184
+ }
185
+
186
+ /**
187
+ * Read packages from stdin as NDJSON
188
+ * @returns {Promise<Array<{ name: string, version: string }>>}
189
+ */
190
+ async function readStdinNdjson() {
191
+ const packages = [];
192
+
193
+ const rl = createInterface({
194
+ input: process.stdin,
195
+ crlfDelay: Infinity
196
+ });
197
+
198
+ for await (const line of rl) {
199
+ const trimmed = line.trim();
200
+ if (!trimmed) continue;
201
+
202
+ try {
203
+ const item = JSON.parse(trimmed);
204
+ if (!item.name || !item.version) {
205
+ throw new Error('Each line must have "name" and "version" fields');
206
+ }
207
+ packages.push({
208
+ name: item.name,
209
+ version: item.version,
210
+ integrity: item.integrity,
211
+ resolved: item.resolved
212
+ });
213
+ } catch (err) {
214
+ throw new Error(`Invalid JSON on stdin: ${err.message}`);
215
+ }
216
+ }
217
+
218
+ return packages;
219
+ }
220
+
105
221
  /**
106
222
  * Encode package name for URL (handle scoped packages)
107
223
  * @param {string} name - Package name like @babel/core
@@ -112,6 +228,68 @@ function encodePackageName(name) {
112
228
  return name.replace('/', '%2f');
113
229
  }
114
230
 
231
+ /**
232
+ * Read cached packument metadata (etag, lastModified)
233
+ * @param {string} cacheDir - Cache directory path
234
+ * @param {string} encodedName - URL-encoded package name
235
+ * @returns {Promise<{ etag?: string, lastModified?: string } | null>}
236
+ */
237
+ async function readCacheMeta(cacheDir, encodedName) {
238
+ try {
239
+ const metaPath = join(cacheDir, `${encodedName}.meta.json`);
240
+ const content = await readFile(metaPath, 'utf8');
241
+ return JSON.parse(content);
242
+ } catch {
243
+ return null;
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Read cached packument from disk
249
+ * @param {string} cacheDir - Cache directory path
250
+ * @param {string} encodedName - URL-encoded package name
251
+ * @returns {Promise<object | null>}
252
+ */
253
+ async function readCachedPackument(cacheDir, encodedName) {
254
+ try {
255
+ const cachePath = join(cacheDir, `${encodedName}.json`);
256
+ const content = await readFile(cachePath, 'utf8');
257
+ return JSON.parse(content);
258
+ } catch {
259
+ return null;
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Write packument and metadata to cache atomically
265
+ * @param {string} cacheDir - Cache directory path
266
+ * @param {string} encodedName - URL-encoded package name
267
+ * @param {string} body - Raw packument JSON string
268
+ * @param {{ etag?: string, lastModified?: string }} meta - Cache metadata
269
+ */
270
+ async function writeCache(cacheDir, encodedName, body, meta) {
271
+ await mkdir(cacheDir, { recursive: true });
272
+
273
+ const cachePath = join(cacheDir, `${encodedName}.json`);
274
+ const metaPath = join(cacheDir, `${encodedName}.meta.json`);
275
+ const pid = process.pid;
276
+
277
+ // Write packument atomically
278
+ const tmpCachePath = `${cachePath}.${pid}.tmp`;
279
+ await writeFile(tmpCachePath, body);
280
+ await rename(tmpCachePath, cachePath);
281
+
282
+ // Write metadata atomically
283
+ const metaObj = {
284
+ etag: meta.etag,
285
+ lastModified: meta.lastModified,
286
+ fetchedAt: new Date().toISOString()
287
+ };
288
+ const tmpMetaPath = `${metaPath}.${pid}.tmp`;
289
+ await writeFile(tmpMetaPath, JSON.stringify(metaObj));
290
+ await rename(tmpMetaPath, metaPath);
291
+ }
292
+
115
293
  /**
116
294
  * Create undici client with retry support
117
295
  * @param {string} registryUrl
@@ -161,42 +339,55 @@ function createClient(registryUrl, { auth, token }) {
161
339
 
162
340
  /**
163
341
  * Check coverage for all dependencies
164
- * @param {Array<{ name: string, version: string }>} deps
165
- * @param {{ registry: string, auth?: string, token?: string, progress: boolean }} options
166
- * @returns {AsyncGenerator<{ name: string, version: string, present: boolean, error?: string }>}
342
+ * @param {Array<{ name: string, version: string, integrity?: string, resolved?: string }>} deps
343
+ * @param {{ registry: string, auth?: string, token?: string, progress: boolean, before?: string, cache?: string }} options
344
+ * @returns {AsyncGenerator<{ name: string, version: string, present: boolean, integrity?: string, resolved?: string, error?: string }>}
167
345
  */
168
- async function* checkCoverage(deps, { registry, auth, token, progress }) {
346
+ async function* checkCoverage(deps, { registry, auth, token, progress, before, cache }) {
169
347
  const { client, headers, baseUrl } = createClient(registry, { auth, token });
170
348
 
171
349
  // Group by package name to avoid duplicate requests
172
- /** @type {Map<string, Set<string>>} */
350
+ // Store full dep info (including integrity/resolved) keyed by version
351
+ /** @type {Map<string, Map<string, { name: string, version: string, integrity?: string, resolved?: string }>>} */
173
352
  const byPackage = new Map();
174
353
  for (const dep of deps) {
175
354
  if (!byPackage.has(dep.name)) {
176
- byPackage.set(dep.name, new Set());
355
+ byPackage.set(dep.name, new Map());
177
356
  }
178
- byPackage.get(dep.name).add(dep.version);
357
+ byPackage.get(dep.name).set(dep.version, dep);
179
358
  }
180
359
 
181
360
  const packages = [...byPackage.entries()];
182
361
  let completed = 0;
183
- const total = packages.length;
362
+ const total = deps.length;
184
363
 
185
364
  // Process in batches for bounded concurrency
186
365
  for (let i = 0; i < packages.length; i += concurrency) {
187
366
  const batch = packages.slice(i, i + concurrency);
188
367
 
189
368
  const results = await Promise.all(
190
- batch.map(async ([name, versions]) => {
369
+ batch.map(async ([name, versionMap]) => {
191
370
  const encodedName = encodePackageName(name);
192
371
  const basePath = baseUrl.pathname.replace(/\/$/, '');
193
372
  const path = `${basePath}/${encodedName}`;
194
373
 
195
374
  try {
375
+ // Build request headers, adding conditional request headers if cached
376
+ const reqHeaders = { ...headers };
377
+ let cacheMeta = null;
378
+ if (cache) {
379
+ cacheMeta = await readCacheMeta(cache, encodedName);
380
+ if (cacheMeta?.etag) {
381
+ reqHeaders['If-None-Match'] = cacheMeta.etag;
382
+ } else if (cacheMeta?.lastModified) {
383
+ reqHeaders['If-Modified-Since'] = cacheMeta.lastModified;
384
+ }
385
+ }
386
+
196
387
  const response = await client.request({
197
388
  method: 'GET',
198
389
  path,
199
- headers
390
+ headers: reqHeaders
200
391
  });
201
392
 
202
393
  const chunks = [];
@@ -210,27 +401,59 @@ async function* checkCoverage(deps, { registry, auth, token, progress }) {
210
401
  }
211
402
 
212
403
  let packumentVersions = null;
213
- if (response.statusCode === 200) {
404
+ let packumentTime = null;
405
+
406
+ if (response.statusCode === 304 && cache) {
407
+ // Cache hit - read from disk
408
+ const cachedPackument = await readCachedPackument(cache, encodedName);
409
+ if (cachedPackument) {
410
+ packumentVersions = cachedPackument.versions || {};
411
+ packumentTime = cachedPackument.time || {};
412
+ }
413
+ } else if (response.statusCode === 200) {
214
414
  const body = Buffer.concat(chunks).toString('utf8');
215
415
  const packument = JSON.parse(body);
216
416
  packumentVersions = packument.versions || {};
417
+ packumentTime = packument.time || {};
418
+
419
+ // Write to cache if enabled
420
+ if (cache) {
421
+ await writeCache(cache, encodedName, body, {
422
+ etag: response.headers.etag,
423
+ lastModified: response.headers['last-modified']
424
+ });
425
+ }
217
426
  }
218
427
 
219
- // Check each version
428
+ // Check each version, preserving integrity/resolved from original dep
220
429
  const versionResults = [];
221
- for (const version of versions) {
222
- const present = packumentVersions ? !!packumentVersions[version] : false;
223
- versionResults.push({ name, version, present });
430
+ for (const [version, dep] of versionMap) {
431
+ let present = packumentVersions ? !!packumentVersions[version] : false;
432
+
433
+ // Time travel: if --before set, only count if published before that date
434
+ if (present && before && packumentTime[version] >= before) {
435
+ present = false;
436
+ }
437
+ const result = { name, version, present };
438
+ if (dep.integrity) result.integrity = dep.integrity;
439
+ if (dep.resolved) result.resolved = dep.resolved;
440
+ if (packumentTime && packumentTime[version]) result.time = packumentTime[version];
441
+ versionResults.push(result);
224
442
  }
225
443
  return versionResults;
226
444
  } catch (err) {
227
445
  // Return error for all versions of this package
228
- return [...versions].map(version => ({
229
- name,
230
- version,
231
- present: false,
232
- error: err.message
233
- }));
446
+ return [...versionMap.values()].map(dep => {
447
+ const result = {
448
+ name: dep.name,
449
+ version: dep.version,
450
+ present: false,
451
+ error: err.message
452
+ };
453
+ if (dep.integrity) result.integrity = dep.integrity;
454
+ if (dep.resolved) result.resolved = dep.resolved;
455
+ return result;
456
+ });
234
457
  }
235
458
  })
236
459
  );
@@ -239,10 +462,10 @@ async function* checkCoverage(deps, { registry, auth, token, progress }) {
239
462
  for (const packageResults of results) {
240
463
  for (const result of packageResults) {
241
464
  yield result;
242
- }
243
- completed++;
244
- if (progress) {
245
- process.stderr.write(`\r Checking: ${completed}/${total} packages`);
465
+ completed++;
466
+ if (progress) {
467
+ process.stderr.write(`\r Checking: ${completed}/${total} package specs`);
468
+ }
246
469
  }
247
470
  }
248
471
  }
@@ -260,7 +483,7 @@ async function* checkCoverage(deps, { registry, auth, token, progress }) {
260
483
  */
261
484
  function formatDep(dep, { specs, full }) {
262
485
  if (full) {
263
- const obj = { name: dep.name, version: dep.version };
486
+ const obj = { name: dep.name, version: dep.version, spec: `${dep.name}@${dep.version}` };
264
487
  if (dep.integrity) obj.integrity = dep.integrity;
265
488
  if (dep.resolved) obj.resolved = dep.resolved;
266
489
  return obj;
@@ -300,10 +523,10 @@ function outputDeps(deps, { specs, json, ndjson, full }) {
300
523
 
301
524
  /**
302
525
  * Output coverage results
303
- * @param {AsyncGenerator<{ name: string, version: string, present: boolean, error?: string }>} results
304
- * @param {{ json: boolean, ndjson: boolean, summary: boolean }} options
526
+ * @param {AsyncGenerator<{ name: string, version: string, present: boolean, integrity?: string, resolved?: string, error?: string }>} results
527
+ * @param {{ json: boolean, ndjson: boolean, summary: boolean, full: boolean }} options
305
528
  */
306
- async function outputCoverage(results, { json, ndjson, summary }) {
529
+ async function outputCoverage(results, { json, ndjson, summary, full }) {
307
530
  const all = [];
308
531
  let presentCount = 0;
309
532
  let missingCount = 0;
@@ -317,7 +540,12 @@ async function outputCoverage(results, { json, ndjson, summary }) {
317
540
 
318
541
  if (ndjson) {
319
542
  // Stream immediately
320
- console.log(JSON.stringify({ name: result.name, version: result.version, present: result.present }));
543
+ const obj = { name: result.name, version: result.version, present: result.present };
544
+ if (full) obj.spec = `${result.name}@${result.version}`;
545
+ if (full && result.integrity) obj.integrity = result.integrity;
546
+ if (full && result.resolved) obj.resolved = result.resolved;
547
+ if (full && result.time) obj.time = result.time;
548
+ console.log(JSON.stringify(obj));
321
549
  } else {
322
550
  all.push(result);
323
551
  }
@@ -328,13 +556,27 @@ async function outputCoverage(results, { json, ndjson, summary }) {
328
556
  all.sort((a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version));
329
557
 
330
558
  if (json) {
331
- const data = all.map(r => ({ name: r.name, version: r.version, present: r.present }));
559
+ const data = all.map(r => {
560
+ const obj = { name: r.name, version: r.version, present: r.present };
561
+ if (full) obj.spec = `${r.name}@${r.version}`;
562
+ if (full && r.integrity) obj.integrity = r.integrity;
563
+ if (full && r.resolved) obj.resolved = r.resolved;
564
+ if (full && r.time) obj.time = r.time;
565
+ return obj;
566
+ });
332
567
  console.log(JSON.stringify(data, null, 2));
333
568
  } else {
334
569
  // CSV output
335
- console.log('package,version,present');
336
- for (const r of all) {
337
- console.log(`${r.name},${r.version},${r.present}`);
570
+ if (full) {
571
+ console.log('package,version,spec,present,integrity,resolved,time');
572
+ for (const r of all) {
573
+ console.log(`${r.name},${r.version},${r.name}@${r.version},${r.present},${r.integrity || ''},${r.resolved || ''},${r.time || ''}`);
574
+ }
575
+ } else {
576
+ console.log('package,version,present');
577
+ for (const r of all) {
578
+ console.log(`${r.name},${r.version},${r.present}`);
579
+ }
338
580
  }
339
581
  }
340
582
  }
@@ -350,22 +592,41 @@ async function outputCoverage(results, { json, ndjson, summary }) {
350
592
  }
351
593
 
352
594
  try {
353
- const lockfile = await FlatlockSet.fromPath(lockfilePath);
354
595
  let deps;
355
596
 
356
- if (values.workspace) {
357
- const repoDir = dirname(lockfilePath);
358
- const workspacePkgPath = join(repoDir, values.workspace, 'package.json');
359
- const workspacePkg = JSON.parse(readFileSync(workspacePkgPath, 'utf8'));
360
-
361
- deps = await lockfile.dependenciesOf(workspacePkg, {
362
- workspacePath: values.workspace,
363
- repoDir,
364
- dev: values.dev,
365
- peer: values.peer
366
- });
597
+ // Determine input source and load dependencies
598
+ if (useStdin) {
599
+ // Read from stdin (NDJSON)
600
+ deps = await readStdinNdjson();
601
+ if (deps.length === 0) {
602
+ console.error('Error: No packages read from stdin');
603
+ process.exit(1);
604
+ }
605
+ } else if (values.list) {
606
+ // Read from JSON list file
607
+ deps = readJsonList(values.list);
608
+ if (deps.length === 0) {
609
+ console.error('Error: No packages found in --list file');
610
+ process.exit(1);
611
+ }
367
612
  } else {
368
- deps = lockfile;
613
+ // Read from lockfile (existing behavior)
614
+ const lockfile = await FlatlockSet.fromPath(lockfilePath);
615
+
616
+ if (values.workspace) {
617
+ const repoDir = dirname(lockfilePath);
618
+ const workspacePkgPath = join(repoDir, values.workspace, 'package.json');
619
+ const workspacePkg = JSON.parse(readFileSync(workspacePkgPath, 'utf8'));
620
+
621
+ deps = await lockfile.dependenciesOf(workspacePkg, {
622
+ workspacePath: values.workspace,
623
+ repoDir,
624
+ dev: values.dev,
625
+ peer: values.peer
626
+ });
627
+ } else {
628
+ deps = lockfile;
629
+ }
369
630
  }
370
631
 
371
632
  if (values.cover) {
@@ -375,13 +636,16 @@ try {
375
636
  registry: values.registry,
376
637
  auth: values.auth,
377
638
  token: values.token,
378
- progress: values.progress
639
+ progress: values.progress,
640
+ before: values.before,
641
+ cache: values.cache
379
642
  });
380
643
 
381
644
  await outputCoverage(results, {
382
645
  json: values.json,
383
646
  ndjson: values.ndjson,
384
- summary: values.summary
647
+ summary: values.summary,
648
+ full: values.full
385
649
  });
386
650
  } else {
387
651
  // Standard flatlock mode
@@ -187,7 +187,7 @@ Examples:
187
187
  const wsNote = result.workspaceCount > 0 ? ` (${result.workspaceCount} workspaces excluded)` : '';
188
188
  console.log(`✓ ${result.path}${wsNote}`);
189
189
  console.log(` count: flatlock=${result.flatlockCount} ${result.source}=${result.comparisonCount}`);
190
- console.log(` sets: equinumerous`);
190
+ console.log(` sets: equinumerous\n`);
191
191
  }
192
192
  } else {
193
193
  // Determine if this is a "superset" (flatlock found more, expected for pnpm)
@@ -203,7 +203,7 @@ Examples:
203
203
  console.log(`⊃ ${result.path}${wsNote}`);
204
204
  console.log(` count: flatlock=${result.flatlockCount} ${result.source}=${result.comparisonCount}`);
205
205
  console.log(` sets: SUPERSET (+${result.onlyInFlatlock.length} reachable deps)`);
206
- console.log(` note: flatlock's reachability analysis found transitive deps pnpm omits`);
206
+ console.log(` note: flatlock's reachability analysis found transitive deps pnpm omits\n`);
207
207
  }
208
208
  } else {
209
209
  mismatchCount++;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Compare flatlock output against established parser for a lockfile
3
+ * @param {string} filepath - Path to lockfile
4
+ * @param {CompareOptions} [options] - Options
5
+ * @returns {Promise<ComparisonResult>}
6
+ */
7
+ export function compare(filepath: string, options?: CompareOptions): Promise<ComparisonResult>;
8
+ /**
9
+ * Compare multiple lockfiles
10
+ * @param {string[]} filepaths - Paths to lockfiles
11
+ * @param {CompareOptions} [options] - Options
12
+ * @returns {AsyncGenerator<ComparisonResult & { filepath: string }>}
13
+ */
14
+ export function compareAll(filepaths: string[], options?: CompareOptions): AsyncGenerator<ComparisonResult & {
15
+ filepath: string;
16
+ }>;
17
+ /**
18
+ * Check which optional comparison parsers are available
19
+ * @returns {Promise<{ arborist: boolean, cyclonedx: boolean, pnpmLockfileFs: boolean, yarnCore: boolean }>}
20
+ */
21
+ export function getAvailableParsers(): Promise<{
22
+ arborist: boolean;
23
+ cyclonedx: boolean;
24
+ pnpmLockfileFs: boolean;
25
+ yarnCore: boolean;
26
+ }>;
27
+ export type CompareOptions = {
28
+ /**
29
+ * - Temp directory for Arborist/CycloneDX (npm only)
30
+ */
31
+ tmpDir?: string;
32
+ /**
33
+ * - Workspace paths for CycloneDX (-w flag)
34
+ */
35
+ workspace?: string[];
36
+ };
37
+ export type ComparisonResult = {
38
+ /**
39
+ * - Lockfile type
40
+ */
41
+ type: string;
42
+ /**
43
+ * - Comparison source used (e.g., '@npmcli/arborist', '@cyclonedx/cyclonedx-npm')
44
+ */
45
+ source?: string;
46
+ /**
47
+ * - Whether flatlock and comparison have same cardinality
48
+ */
49
+ equinumerous: boolean | null;
50
+ /**
51
+ * - Number of packages found by flatlock
52
+ */
53
+ flatlockCount: number;
54
+ /**
55
+ * - Number of packages found by comparison parser
56
+ */
57
+ comparisonCount?: number;
58
+ /**
59
+ * - Number of workspace packages skipped
60
+ */
61
+ workspaceCount?: number;
62
+ /**
63
+ * - Packages only found by flatlock
64
+ */
65
+ onlyInFlatlock?: string[];
66
+ /**
67
+ * - Packages only found by comparison parser
68
+ */
69
+ onlyInComparison?: string[];
70
+ };
71
+ export type PackagesResult = {
72
+ /**
73
+ * - Set of package@version strings
74
+ */
75
+ packages: Set<string>;
76
+ /**
77
+ * - Number of workspace packages skipped
78
+ */
79
+ workspaceCount: number;
80
+ /**
81
+ * - Comparison source used
82
+ */
83
+ source: string;
84
+ };
85
+ //# sourceMappingURL=compare.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compare.d.ts","sourceRoot":"","sources":["../src/compare.js"],"names":[],"mappings":"AAyhBA;;;;;GAKG;AACH,kCAJW,MAAM,YACN,cAAc,GACZ,OAAO,CAAC,gBAAgB,CAAC,CA4CrC;AAED;;;;;GAKG;AACH,sCAJW,MAAM,EAAE,YACR,cAAc,GACZ,cAAc,CAAC,gBAAgB,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAMnE;AAED;;;GAGG;AACH,uCAFa,OAAO,CAAC;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,cAAc,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC,CAgB1G;;;;;aAjhBa,MAAM;;;;gBACN,MAAM,EAAE;;;;;;UAKR,MAAM;;;;aACN,MAAM;;;;kBACN,OAAO,GAAG,IAAI;;;;mBACd,MAAM;;;;sBACN,MAAM;;;;qBACN,MAAM;;;;qBACN,MAAM,EAAE;;;;uBACR,MAAM,EAAE;;;;;;cAKR,GAAG,CAAC,MAAM,CAAC;;;;oBACX,MAAM;;;;YACN,MAAM"}