lockdelta 0.1.0 → 0.1.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/dist/cli.js CHANGED
@@ -4,158 +4,151 @@
4
4
  import { writeFileSync } from "fs";
5
5
  import { Command } from "commander";
6
6
 
7
- // src/index.ts
8
- import { readFileSync } from "fs";
9
-
10
7
  // src/core/report.ts
11
8
  import { posix as posix2 } from "path";
12
9
 
13
- // src/ecosystems/python/parsers/toml.ts
14
- import { parse } from "smol-toml";
15
- function parseTomlPackages(content) {
16
- try {
17
- const data = parse(content);
18
- const packages = {};
19
- for (const pkg of [...data.package ?? [], ...data.packages ?? []]) {
20
- if (typeof pkg.name === "string" && typeof pkg.version === "string") {
21
- packages[pkg.name] = pkg.version;
22
- }
23
- }
24
- return packages;
25
- } catch {
26
- return parseTomlPackagesRegex(content);
27
- }
28
- }
29
- function parseTomlPackagesRegex(content) {
30
- const packages = {};
31
- const blocks = content.split(/\[\[packages?\]\]/);
32
- for (const block of blocks) {
33
- const nameMatch = block.match(/\nname\s*=\s*"([^"]+)"/);
34
- const versionMatch = block.match(/\nversion\s*=\s*"([^"]+)"/);
35
- if (nameMatch && versionMatch) {
36
- packages[nameMatch[1]] = versionMatch[1];
37
- }
38
- }
39
- return packages;
40
- }
41
-
42
- // src/ecosystems/python/pyproject.ts
43
- import { parse as parse2 } from "smol-toml";
44
- function normalizePythonName(name) {
45
- return name.toLowerCase().replace(/[-_.]+/g, "_");
46
- }
47
- function extractPkgName(dep) {
48
- const match = String(dep).match(/^([\w][\w.-]*)/);
49
- return match ? normalizePythonName(match[1]) : null;
10
+ // src/ecosystems/deno/deno-json.ts
11
+ function normalizeDenoName(name) {
12
+ return name.toLowerCase();
50
13
  }
51
14
  function parseDirectDeps(content) {
52
15
  const prod = /* @__PURE__ */ new Set();
53
- const dev = /* @__PURE__ */ new Set();
54
16
  let data;
55
17
  try {
56
- data = parse2(content);
18
+ data = JSON.parse(content);
57
19
  } catch {
58
- return { prod, dev };
59
- }
60
- const project = data["project"];
61
- const pep517Deps = project?.["dependencies"];
62
- if (Array.isArray(pep517Deps)) {
63
- for (const dep of pep517Deps) {
64
- const name = extractPkgName(dep);
65
- if (name) prod.add(name);
66
- }
20
+ return { prod, dev: /* @__PURE__ */ new Set() };
67
21
  }
68
- const optDeps = project?.["optional-dependencies"];
69
- if (optDeps && typeof optDeps === "object") {
70
- for (const group of Object.values(optDeps)) {
71
- if (Array.isArray(group)) {
72
- for (const dep of group) {
73
- const name = extractPkgName(dep);
74
- if (name && !prod.has(name)) dev.add(name);
75
- }
76
- }
22
+ const imports = data.imports;
23
+ if (imports) {
24
+ for (const specifier of Object.values(imports)) {
25
+ const name = extractPackageName(specifier);
26
+ if (name) prod.add(normalizeDenoName(name));
77
27
  }
78
28
  }
79
- const tool = data["tool"];
80
- const poetry = tool?.["poetry"];
81
- if (poetry) {
82
- const poetryDeps = poetry["dependencies"];
83
- if (poetryDeps) {
84
- for (const key of Object.keys(poetryDeps)) {
85
- if (key.toLowerCase() !== "python") prod.add(normalizePythonName(key));
86
- }
87
- }
88
- const devDeps = poetry["dev-dependencies"];
89
- if (devDeps) {
90
- for (const key of Object.keys(devDeps)) {
91
- const normalized = normalizePythonName(key);
92
- if (!prod.has(normalized)) dev.add(normalized);
93
- }
94
- }
95
- const groups = poetry["group"];
96
- if (groups) {
97
- for (const group of Object.values(groups)) {
98
- const groupDeps = group["dependencies"];
99
- if (groupDeps) {
100
- for (const key of Object.keys(groupDeps)) {
101
- const normalized = normalizePythonName(key);
102
- if (!prod.has(normalized)) dev.add(normalized);
103
- }
104
- }
105
- }
106
- }
29
+ const workspace = data.workspace;
30
+ for (const specifier of workspace?.dependencies ?? []) {
31
+ const name = extractPackageName(specifier);
32
+ if (name) prod.add(normalizeDenoName(name));
107
33
  }
108
- const uv = tool?.["uv"];
109
- const uvDevDeps = uv?.["dev-dependencies"];
110
- if (Array.isArray(uvDevDeps)) {
111
- for (const dep of uvDevDeps) {
112
- const name = extractPkgName(dep);
113
- if (name && !prod.has(name)) dev.add(name);
114
- }
34
+ return { prod, dev: /* @__PURE__ */ new Set() };
35
+ }
36
+ function extractPackageName(specifier) {
37
+ const withoutProtocol = specifier.replace(/^(?:npm|jsr|node):/, "");
38
+ if (specifier.startsWith("node:")) return null;
39
+ if (withoutProtocol.startsWith("@")) {
40
+ const atIdx2 = withoutProtocol.indexOf("@", 1);
41
+ return atIdx2 > 0 ? withoutProtocol.slice(0, atIdx2) : withoutProtocol;
115
42
  }
116
- const depGroups = data["dependency-groups"];
117
- if (depGroups && typeof depGroups === "object") {
118
- for (const group of Object.values(depGroups)) {
119
- if (Array.isArray(group)) {
120
- for (const entry of group) {
121
- if (typeof entry === "string") {
122
- const name = extractPkgName(entry);
123
- if (name && !prod.has(name)) dev.add(name);
124
- }
125
- }
43
+ const atIdx = withoutProtocol.indexOf("@");
44
+ return atIdx > 0 ? withoutProtocol.slice(0, atIdx) : withoutProtocol || null;
45
+ }
46
+
47
+ // src/ecosystems/deno/parsers/deno-lock.ts
48
+ function parseDenoLock(content) {
49
+ const data = JSON.parse(content);
50
+ const result = {};
51
+ for (const [key, registry2] of [
52
+ ["npm", data.packages?.npm],
53
+ ["jsr", data.packages?.jsr]
54
+ ]) {
55
+ if (!registry2) continue;
56
+ for (const specifier of Object.keys(registry2)) {
57
+ const { name, version } = splitSpecifier(specifier);
58
+ const resultKey = key === "jsr" ? `jsr:${name}` : name;
59
+ if (name && version && !result[resultKey]) {
60
+ result[resultKey] = version;
126
61
  }
127
62
  }
128
63
  }
129
- return { prod, dev };
64
+ return result;
65
+ }
66
+ function splitSpecifier(specifier) {
67
+ if (specifier.startsWith("@")) {
68
+ const atIdx2 = specifier.indexOf("@", 1);
69
+ if (atIdx2 < 0) return { name: specifier, version: "" };
70
+ return { name: specifier.slice(0, atIdx2), version: specifier.slice(atIdx2 + 1) };
71
+ }
72
+ const atIdx = specifier.indexOf("@");
73
+ if (atIdx < 0) return { name: specifier, version: "" };
74
+ return { name: specifier.slice(0, atIdx), version: specifier.slice(atIdx + 1) };
130
75
  }
131
76
 
132
- // src/ecosystems/python/index.ts
133
- var SUPPORTED_LOCKFILES = [
134
- { filename: "uv.lock", type: "uv" },
135
- { filename: "poetry.lock", type: "poetry" },
136
- { filename: "pdm.lock", type: "pdm" },
137
- { filename: "pylock.toml", type: "pylock" }
138
- // PEP 751
139
- ];
140
- var lockfileTypeMap = new Map(SUPPORTED_LOCKFILES.map((l) => [l.filename, l.type]));
141
- var pythonEcosystem = {
142
- name: "python",
77
+ // src/ecosystems/deno/index.ts
78
+ var SUPPORTED_LOCKFILES = [{ filename: "deno.lock", type: "deno" }];
79
+ var denoEcosystem = {
80
+ name: "deno",
143
81
  supportedLockfiles: SUPPORTED_LOCKFILES,
144
- manifestName: "pyproject.toml",
82
+ manifestName: "deno.json",
145
83
  getLockfileType(filename) {
146
- return lockfileTypeMap.get(filename);
84
+ return filename === "deno.lock" ? "deno" : void 0;
147
85
  },
148
86
  parseLockfile(content, _lockfileType) {
149
- return parseTomlPackages(content);
87
+ return parseDenoLock(content);
150
88
  },
151
89
  parseDirectDeps(manifestContent) {
152
90
  return parseDirectDeps(manifestContent);
153
91
  },
154
92
  normalizeName(name) {
155
- return normalizePythonName(name);
93
+ return normalizeDenoName(name);
156
94
  }
157
95
  };
158
96
 
97
+ // src/ecosystems/javascript/package-json.ts
98
+ var PROD_SECTIONS = ["dependencies", "optionalDependencies", "peerDependencies"];
99
+ function normalizeJsName(name) {
100
+ return name.toLowerCase();
101
+ }
102
+ function parseDirectDeps2(content) {
103
+ const prod = /* @__PURE__ */ new Set();
104
+ const dev = /* @__PURE__ */ new Set();
105
+ let data;
106
+ try {
107
+ data = JSON.parse(content);
108
+ } catch {
109
+ return { prod, dev };
110
+ }
111
+ for (const section of PROD_SECTIONS) {
112
+ const deps = data[section];
113
+ if (deps && typeof deps === "object") {
114
+ for (const name of Object.keys(deps)) {
115
+ prod.add(normalizeJsName(name));
116
+ }
117
+ }
118
+ }
119
+ const devDeps = data.devDependencies;
120
+ if (devDeps && typeof devDeps === "object") {
121
+ for (const name of Object.keys(devDeps)) {
122
+ const normalized = normalizeJsName(name);
123
+ if (!prod.has(normalized)) dev.add(normalized);
124
+ }
125
+ }
126
+ return { prod, dev };
127
+ }
128
+
129
+ // src/ecosystems/javascript/parsers/bun.ts
130
+ function parseBunLock(content) {
131
+ const data = JSON.parse(content);
132
+ const result = {};
133
+ for (const [name, entry] of Object.entries(data.packages ?? {})) {
134
+ if (!Array.isArray(entry)) continue;
135
+ const nameAtVersion = entry[0];
136
+ if (typeof nameAtVersion !== "string") continue;
137
+ const version = extractVersion(nameAtVersion);
138
+ if (!version || version.startsWith("workspace:")) continue;
139
+ result[name] = version;
140
+ }
141
+ return result;
142
+ }
143
+ function extractVersion(nameAtVersion) {
144
+ if (nameAtVersion.startsWith("@")) {
145
+ const atIdx2 = nameAtVersion.indexOf("@", 1);
146
+ return atIdx2 > 0 ? nameAtVersion.slice(atIdx2 + 1) : "";
147
+ }
148
+ const atIdx = nameAtVersion.indexOf("@");
149
+ return atIdx > 0 ? nameAtVersion.slice(atIdx + 1) : "";
150
+ }
151
+
159
152
  // src/ecosystems/javascript/parsers/npm.ts
160
153
  function parseNpmLock(content) {
161
154
  const data = JSON.parse(content);
@@ -195,69 +188,10 @@ function parseV1Dependencies(deps, result = {}) {
195
188
  return result;
196
189
  }
197
190
 
198
- // src/ecosystems/javascript/parsers/yarn.ts
199
- import { parse as parseYaml } from "yaml";
200
- function parseYarnLock(content) {
201
- return isYarnBerry(content) ? parseYarnBerry(content) : parseYarnV1(content);
202
- }
203
- function isYarnBerry(content) {
204
- return content.includes("__metadata:");
205
- }
206
- function extractNameFromSpecifier(spec) {
207
- const trimmed = spec.trim().replace(/^"|"$/g, "");
208
- if (trimmed.startsWith("@")) {
209
- const idx = trimmed.indexOf("@", 1);
210
- return idx > 0 ? trimmed.slice(0, idx) : trimmed;
211
- }
212
- const atIdx = trimmed.indexOf("@");
213
- return atIdx > 0 ? trimmed.slice(0, atIdx) : trimmed;
214
- }
215
- function parseYarnV1(content) {
216
- const packages = {};
217
- const blocks = content.split(/\n\n+/);
218
- for (const block of blocks) {
219
- const trimmed = block.trim();
220
- if (!trimmed || trimmed.startsWith("#")) continue;
221
- const versionMatch = trimmed.match(/^[ \t]+version "([^"]+)"/m);
222
- if (!versionMatch) continue;
223
- const headerLine = trimmed.split("\n")[0].trim().replace(/:$/, "");
224
- const firstSpecifier = headerLine.split(",")[0].trim().replace(/^"|"$/g, "");
225
- const name = extractNameFromSpecifier(firstSpecifier);
226
- if (name && !packages[name]) {
227
- packages[name] = versionMatch[1];
228
- }
229
- }
230
- return packages;
231
- }
232
- function parseYarnBerry(content) {
233
- const data = parseYaml(content);
234
- const packages = {};
235
- for (const [key, value] of Object.entries(data)) {
236
- if (key === "__metadata") continue;
237
- if (typeof value !== "object" || !value) continue;
238
- const entry = value;
239
- if (entry.linkType === "soft") continue;
240
- if (!entry.version) continue;
241
- const cleanKey = key.replace(/^"|"$/g, "");
242
- const name = extractNameFromBerryKey(cleanKey);
243
- if (name && !packages[name]) {
244
- packages[name] = entry.version;
245
- }
246
- }
247
- return packages;
248
- }
249
- function extractNameFromBerryKey(key) {
250
- if (key.startsWith("@")) {
251
- const idx = key.indexOf("@", 1);
252
- return idx > 0 ? key.slice(0, idx) : key;
253
- }
254
- return key.split("@")[0];
255
- }
256
-
257
191
  // src/ecosystems/javascript/parsers/pnpm.ts
258
- import { parse as parseYaml2 } from "yaml";
192
+ import { parse as parseYaml } from "yaml";
259
193
  function parsePnpmLock(content) {
260
- const data = parseYaml2(content);
194
+ const data = parseYaml(content);
261
195
  if (!data?.packages) return {};
262
196
  const lockfileVersion = parseLockfileVersion(data.lockfileVersion);
263
197
  if (lockfileVersion >= 9) {
@@ -267,13 +201,14 @@ function parsePnpmLock(content) {
267
201
  }
268
202
  function parseLockfileVersion(v) {
269
203
  if (typeof v === "number") return v;
270
- if (typeof v === "string") return parseFloat(v);
204
+ if (typeof v === "string") return Number.parseFloat(v);
271
205
  return 0;
272
206
  }
273
207
  function parsePnpmV9(packages) {
274
208
  const result = {};
275
209
  for (const key of Object.keys(packages)) {
276
- let name, version;
210
+ let name;
211
+ let version;
277
212
  if (key.startsWith("@")) {
278
213
  const atIdx = key.indexOf("@", 1);
279
214
  if (atIdx < 0) continue;
@@ -296,7 +231,8 @@ function parsePnpmLegacy(packages) {
296
231
  const result = {};
297
232
  for (const key of Object.keys(packages)) {
298
233
  const cleaned = key.startsWith("/") ? key.slice(1) : key;
299
- let name, version;
234
+ let name;
235
+ let version;
300
236
  if (cleaned.startsWith("@")) {
301
237
  const secondSlash = cleaned.indexOf("/", cleaned.indexOf("/") + 1);
302
238
  const secondAt = cleaned.indexOf("@", 1);
@@ -333,59 +269,63 @@ function stripVersionSuffix(version) {
333
269
  return version.split("(")[0].split("_")[0].trim();
334
270
  }
335
271
 
336
- // src/ecosystems/javascript/parsers/bun.ts
337
- function parseBunLock(content) {
338
- const data = JSON.parse(content);
339
- const result = {};
340
- for (const [name, entry] of Object.entries(data.packages ?? {})) {
341
- if (!Array.isArray(entry)) continue;
342
- const nameAtVersion = entry[0];
343
- if (typeof nameAtVersion !== "string") continue;
344
- const version = extractVersion(nameAtVersion);
345
- if (!version || version.startsWith("workspace:")) continue;
346
- result[name] = version;
347
- }
348
- return result;
349
- }
350
- function extractVersion(nameAtVersion) {
351
- if (nameAtVersion.startsWith("@")) {
352
- const atIdx2 = nameAtVersion.indexOf("@", 1);
353
- return atIdx2 > 0 ? nameAtVersion.slice(atIdx2 + 1) : "";
354
- }
355
- const atIdx = nameAtVersion.indexOf("@");
356
- return atIdx > 0 ? nameAtVersion.slice(atIdx + 1) : "";
272
+ // src/ecosystems/javascript/parsers/yarn.ts
273
+ import { parse as parseYaml2 } from "yaml";
274
+ function parseYarnLock(content) {
275
+ return isYarnBerry(content) ? parseYarnBerry(content) : parseYarnV1(content);
357
276
  }
358
-
359
- // src/ecosystems/javascript/package-json.ts
360
- var PROD_SECTIONS = ["dependencies", "optionalDependencies", "peerDependencies"];
361
- function normalizeJsName(name) {
362
- return name.toLowerCase();
277
+ function isYarnBerry(content) {
278
+ return content.includes("__metadata:");
363
279
  }
364
- function parseDirectDeps2(content) {
365
- const prod = /* @__PURE__ */ new Set();
366
- const dev = /* @__PURE__ */ new Set();
367
- let data;
368
- try {
369
- data = JSON.parse(content);
370
- } catch {
371
- return { prod, dev };
280
+ function extractNameFromSpecifier(spec) {
281
+ const trimmed = spec.trim().replace(/^"|"$/g, "");
282
+ if (trimmed.startsWith("@")) {
283
+ const idx = trimmed.indexOf("@", 1);
284
+ return idx > 0 ? trimmed.slice(0, idx) : trimmed;
372
285
  }
373
- for (const section of PROD_SECTIONS) {
374
- const deps = data[section];
375
- if (deps && typeof deps === "object") {
376
- for (const name of Object.keys(deps)) {
377
- prod.add(normalizeJsName(name));
378
- }
286
+ const atIdx = trimmed.indexOf("@");
287
+ return atIdx > 0 ? trimmed.slice(0, atIdx) : trimmed;
288
+ }
289
+ function parseYarnV1(content) {
290
+ const packages = {};
291
+ const blocks = content.split(/\n\n+/);
292
+ for (const block of blocks) {
293
+ const trimmed = block.trim();
294
+ if (!trimmed || trimmed.startsWith("#")) continue;
295
+ const versionMatch = trimmed.match(/^[ \t]+version "([^"]+)"/m);
296
+ if (!versionMatch) continue;
297
+ const headerLine = trimmed.split("\n")[0].trim().replace(/:$/, "");
298
+ const firstSpecifier = headerLine.split(",")[0].trim().replace(/^"|"$/g, "");
299
+ const name = extractNameFromSpecifier(firstSpecifier);
300
+ if (name && !packages[name]) {
301
+ packages[name] = versionMatch[1];
379
302
  }
380
303
  }
381
- const devDeps = data["devDependencies"];
382
- if (devDeps && typeof devDeps === "object") {
383
- for (const name of Object.keys(devDeps)) {
384
- const normalized = normalizeJsName(name);
385
- if (!prod.has(normalized)) dev.add(normalized);
304
+ return packages;
305
+ }
306
+ function parseYarnBerry(content) {
307
+ const data = parseYaml2(content);
308
+ const packages = {};
309
+ for (const [key, value] of Object.entries(data)) {
310
+ if (key === "__metadata") continue;
311
+ if (typeof value !== "object" || !value) continue;
312
+ const entry = value;
313
+ if (entry.linkType === "soft") continue;
314
+ if (!entry.version) continue;
315
+ const cleanKey = key.replace(/^"|"$/g, "");
316
+ const name = extractNameFromBerryKey(cleanKey);
317
+ if (name && !packages[name]) {
318
+ packages[name] = entry.version;
386
319
  }
387
320
  }
388
- return { prod, dev };
321
+ return packages;
322
+ }
323
+ function extractNameFromBerryKey(key) {
324
+ if (key.startsWith("@")) {
325
+ const idx = key.indexOf("@", 1);
326
+ return idx > 0 ? key.slice(0, idx) : key;
327
+ }
328
+ return key.split("@")[0];
389
329
  }
390
330
 
391
331
  // src/ecosystems/javascript/index.ts
@@ -395,13 +335,13 @@ var SUPPORTED_LOCKFILES2 = [
395
335
  { filename: "pnpm-lock.yaml", type: "pnpm" },
396
336
  { filename: "bun.lock", type: "bun" }
397
337
  ];
398
- var lockfileTypeMap2 = new Map(SUPPORTED_LOCKFILES2.map((l) => [l.filename, l.type]));
338
+ var lockfileTypeMap = new Map(SUPPORTED_LOCKFILES2.map((l) => [l.filename, l.type]));
399
339
  var javascriptEcosystem = {
400
340
  name: "javascript",
401
341
  supportedLockfiles: SUPPORTED_LOCKFILES2,
402
342
  manifestName: "package.json",
403
343
  getLockfileType(filename) {
404
- return lockfileTypeMap2.get(filename);
344
+ return lockfileTypeMap.get(filename);
405
345
  },
406
346
  parseLockfile(content, lockfileType) {
407
347
  switch (lockfileType) {
@@ -425,90 +365,149 @@ var javascriptEcosystem = {
425
365
  }
426
366
  };
427
367
 
428
- // src/ecosystems/deno/parsers/deno-lock.ts
429
- function parseDenoLock(content) {
430
- const data = JSON.parse(content);
431
- const result = {};
432
- for (const [key, registry2] of [
433
- ["npm", data.packages?.npm],
434
- ["jsr", data.packages?.jsr]
435
- ]) {
436
- if (!registry2) continue;
437
- for (const specifier of Object.keys(registry2)) {
438
- const { name, version } = splitSpecifier(specifier);
439
- const resultKey = key === "jsr" ? `jsr:${name}` : name;
440
- if (name && version && !result[resultKey]) {
441
- result[resultKey] = version;
368
+ // src/ecosystems/python/parsers/toml.ts
369
+ import { parse } from "smol-toml";
370
+ function parseTomlPackages(content) {
371
+ try {
372
+ const data = parse(content);
373
+ const packages = {};
374
+ for (const pkg of [...data.package ?? [], ...data.packages ?? []]) {
375
+ if (typeof pkg.name === "string" && typeof pkg.version === "string") {
376
+ packages[pkg.name] = pkg.version;
442
377
  }
443
378
  }
379
+ return packages;
380
+ } catch {
381
+ return parseTomlPackagesRegex(content);
444
382
  }
445
- return result;
446
383
  }
447
- function splitSpecifier(specifier) {
448
- if (specifier.startsWith("@")) {
449
- const atIdx2 = specifier.indexOf("@", 1);
450
- if (atIdx2 < 0) return { name: specifier, version: "" };
451
- return { name: specifier.slice(0, atIdx2), version: specifier.slice(atIdx2 + 1) };
384
+ function parseTomlPackagesRegex(content) {
385
+ const packages = {};
386
+ const blocks = content.split(/\[\[packages?\]\]/);
387
+ for (const block of blocks) {
388
+ const nameMatch = block.match(/\nname\s*=\s*"([^"]+)"/);
389
+ const versionMatch = block.match(/\nversion\s*=\s*"([^"]+)"/);
390
+ if (nameMatch && versionMatch) {
391
+ packages[nameMatch[1]] = versionMatch[1];
392
+ }
452
393
  }
453
- const atIdx = specifier.indexOf("@");
454
- if (atIdx < 0) return { name: specifier, version: "" };
455
- return { name: specifier.slice(0, atIdx), version: specifier.slice(atIdx + 1) };
394
+ return packages;
456
395
  }
457
396
 
458
- // src/ecosystems/deno/deno-json.ts
459
- function normalizeDenoName(name) {
460
- return name.toLowerCase();
397
+ // src/ecosystems/python/pyproject.ts
398
+ import { parse as parse2 } from "smol-toml";
399
+ function normalizePythonName(name) {
400
+ return name.toLowerCase().replace(/[-_.]+/g, "_");
401
+ }
402
+ function extractPkgName(dep) {
403
+ const match = String(dep).match(/^([\w][\w.-]*)/);
404
+ return match ? normalizePythonName(match[1]) : null;
461
405
  }
462
406
  function parseDirectDeps3(content) {
463
407
  const prod = /* @__PURE__ */ new Set();
408
+ const dev = /* @__PURE__ */ new Set();
464
409
  let data;
465
410
  try {
466
- data = JSON.parse(content);
411
+ data = parse2(content);
467
412
  } catch {
468
- return { prod, dev: /* @__PURE__ */ new Set() };
413
+ return { prod, dev };
469
414
  }
470
- const imports = data["imports"];
471
- if (imports) {
472
- for (const specifier of Object.values(imports)) {
473
- const name = extractPackageName(specifier);
474
- if (name) prod.add(normalizeDenoName(name));
415
+ const project = data.project;
416
+ const pep517Deps = project?.dependencies;
417
+ if (Array.isArray(pep517Deps)) {
418
+ for (const dep of pep517Deps) {
419
+ const name = extractPkgName(dep);
420
+ if (name) prod.add(name);
475
421
  }
476
422
  }
477
- const workspace = data["workspace"];
478
- for (const specifier of workspace?.dependencies ?? []) {
479
- const name = extractPackageName(specifier);
480
- if (name) prod.add(normalizeDenoName(name));
423
+ const optDeps = project?.["optional-dependencies"];
424
+ if (optDeps && typeof optDeps === "object") {
425
+ for (const group of Object.values(optDeps)) {
426
+ if (Array.isArray(group)) {
427
+ for (const dep of group) {
428
+ const name = extractPkgName(dep);
429
+ if (name && !prod.has(name)) dev.add(name);
430
+ }
431
+ }
432
+ }
481
433
  }
482
- return { prod, dev: /* @__PURE__ */ new Set() };
483
- }
484
- function extractPackageName(specifier) {
485
- const withoutProtocol = specifier.replace(/^(?:npm|jsr|node):/, "");
486
- if (specifier.startsWith("node:")) return null;
487
- if (withoutProtocol.startsWith("@")) {
488
- const atIdx2 = withoutProtocol.indexOf("@", 1);
489
- return atIdx2 > 0 ? withoutProtocol.slice(0, atIdx2) : withoutProtocol;
434
+ const tool = data.tool;
435
+ const poetry = tool?.poetry;
436
+ if (poetry) {
437
+ const poetryDeps = poetry.dependencies;
438
+ if (poetryDeps) {
439
+ for (const key of Object.keys(poetryDeps)) {
440
+ if (key.toLowerCase() !== "python") prod.add(normalizePythonName(key));
441
+ }
442
+ }
443
+ const devDeps = poetry["dev-dependencies"];
444
+ if (devDeps) {
445
+ for (const key of Object.keys(devDeps)) {
446
+ const normalized = normalizePythonName(key);
447
+ if (!prod.has(normalized)) dev.add(normalized);
448
+ }
449
+ }
450
+ const groups = poetry.group;
451
+ if (groups) {
452
+ for (const group of Object.values(groups)) {
453
+ const groupDeps = group.dependencies;
454
+ if (groupDeps) {
455
+ for (const key of Object.keys(groupDeps)) {
456
+ const normalized = normalizePythonName(key);
457
+ if (!prod.has(normalized)) dev.add(normalized);
458
+ }
459
+ }
460
+ }
461
+ }
490
462
  }
491
- const atIdx = withoutProtocol.indexOf("@");
492
- return atIdx > 0 ? withoutProtocol.slice(0, atIdx) : withoutProtocol || null;
463
+ const uv = tool?.uv;
464
+ const uvDevDeps = uv?.["dev-dependencies"];
465
+ if (Array.isArray(uvDevDeps)) {
466
+ for (const dep of uvDevDeps) {
467
+ const name = extractPkgName(dep);
468
+ if (name && !prod.has(name)) dev.add(name);
469
+ }
470
+ }
471
+ const depGroups = data["dependency-groups"];
472
+ if (depGroups && typeof depGroups === "object") {
473
+ for (const group of Object.values(depGroups)) {
474
+ if (Array.isArray(group)) {
475
+ for (const entry of group) {
476
+ if (typeof entry === "string") {
477
+ const name = extractPkgName(entry);
478
+ if (name && !prod.has(name)) dev.add(name);
479
+ }
480
+ }
481
+ }
482
+ }
483
+ }
484
+ return { prod, dev };
493
485
  }
494
486
 
495
- // src/ecosystems/deno/index.ts
496
- var SUPPORTED_LOCKFILES3 = [{ filename: "deno.lock", type: "deno" }];
497
- var denoEcosystem = {
498
- name: "deno",
487
+ // src/ecosystems/python/index.ts
488
+ var SUPPORTED_LOCKFILES3 = [
489
+ { filename: "uv.lock", type: "uv" },
490
+ { filename: "poetry.lock", type: "poetry" },
491
+ { filename: "pdm.lock", type: "pdm" },
492
+ { filename: "pylock.toml", type: "pylock" }
493
+ // PEP 751
494
+ ];
495
+ var lockfileTypeMap2 = new Map(SUPPORTED_LOCKFILES3.map((l) => [l.filename, l.type]));
496
+ var pythonEcosystem = {
497
+ name: "python",
499
498
  supportedLockfiles: SUPPORTED_LOCKFILES3,
500
- manifestName: "deno.json",
499
+ manifestName: "pyproject.toml",
501
500
  getLockfileType(filename) {
502
- return filename === "deno.lock" ? "deno" : void 0;
501
+ return lockfileTypeMap2.get(filename);
503
502
  },
504
503
  parseLockfile(content, _lockfileType) {
505
- return parseDenoLock(content);
504
+ return parseTomlPackages(content);
506
505
  },
507
506
  parseDirectDeps(manifestContent) {
508
507
  return parseDirectDeps3(manifestContent);
509
508
  },
510
509
  normalizeName(name) {
511
- return normalizeDenoName(name);
510
+ return normalizePythonName(name);
512
511
  }
513
512
  };
514
513
 
@@ -533,6 +532,29 @@ registerEcosystem(pythonEcosystem);
533
532
  registerEcosystem(javascriptEcosystem);
534
533
  registerEcosystem(denoEcosystem);
535
534
 
535
+ // src/core/diff.ts
536
+ function diffPackages(oldPkgs, newPkgs, directDeps, normalizeName) {
537
+ const allNames = /* @__PURE__ */ new Set([...Object.keys(oldPkgs), ...Object.keys(newPkgs)]);
538
+ const changes = [];
539
+ for (const name of [...allNames].sort()) {
540
+ const inOld = name in oldPkgs;
541
+ const inNew = name in newPkgs;
542
+ if (inOld && inNew && oldPkgs[name] === newPkgs[name]) continue;
543
+ const normalized = normalizeName(name);
544
+ const isProd = directDeps.prod.has(normalized);
545
+ const isDev = directDeps.dev.has(normalized) && !isProd;
546
+ changes.push({
547
+ name,
548
+ change_type: !inOld ? "added" : !inNew ? "removed" : "updated",
549
+ old_version: inOld ? oldPkgs[name] : null,
550
+ new_version: inNew ? newPkgs[name] : null,
551
+ is_direct: isProd || isDev,
552
+ is_dev: isDev
553
+ });
554
+ }
555
+ return changes;
556
+ }
557
+
536
558
  // src/core/discovery.ts
537
559
  import { posix } from "path";
538
560
  function workspaceFromPath(filePath) {
@@ -596,6 +618,7 @@ function resolveLockfilePair(baseFiles, headFiles) {
596
618
  basePath: chosen.path,
597
619
  baseType: chosen.type,
598
620
  headPath: chosen.path,
621
+ // biome-ignore lint/style/noNonNullAssertion: path is guaranteed present (comes from common set)
599
622
  headType: headByPath.get(chosen.path).type,
600
623
  migrationNote: null,
601
624
  ecosystemName: chosen.ecosystemName
@@ -638,29 +661,6 @@ function resolveLockfilePair(baseFiles, headFiles) {
638
661
  return null;
639
662
  }
640
663
 
641
- // src/core/diff.ts
642
- function diffPackages(oldPkgs, newPkgs, directDeps, normalizeName) {
643
- const allNames = /* @__PURE__ */ new Set([...Object.keys(oldPkgs), ...Object.keys(newPkgs)]);
644
- const changes = [];
645
- for (const name of [...allNames].sort()) {
646
- const inOld = name in oldPkgs;
647
- const inNew = name in newPkgs;
648
- if (inOld && inNew && oldPkgs[name] === newPkgs[name]) continue;
649
- const normalized = normalizeName(name);
650
- const isProd = directDeps.prod.has(normalized);
651
- const isDev = directDeps.dev.has(normalized) && !isProd;
652
- changes.push({
653
- name,
654
- change_type: !inOld ? "added" : !inNew ? "removed" : "updated",
655
- old_version: inOld ? oldPkgs[name] : null,
656
- new_version: inNew ? newPkgs[name] : null,
657
- is_direct: isProd || isDev,
658
- is_dev: isDev
659
- });
660
- }
661
- return changes;
662
- }
663
-
664
664
  // src/core/report.ts
665
665
  async function buildLockfileEntry(pair, workspace, getBase, getHead) {
666
666
  const ecosystem = getEcosystemByName(pair.ecosystemName);
@@ -793,14 +793,29 @@ function gitLsTree(ref) {
793
793
  // src/sources/github.ts
794
794
  import { execFileSync as execFileSync2 } from "child_process";
795
795
  var API_BASE = "https://api.github.com";
796
- function token() {
797
- const t = process.env["GITHUB_TOKEN"];
798
- if (!t) throw new Error("GITHUB_TOKEN is required for GitHub API access");
799
- return t;
796
+ var cachedToken;
797
+ function resolveToken() {
798
+ if (cachedToken) return cachedToken;
799
+ if (process.env.GITHUB_TOKEN) {
800
+ cachedToken = process.env.GITHUB_TOKEN;
801
+ return cachedToken;
802
+ }
803
+ try {
804
+ const t = execFileSync2("gh", ["auth", "token"], {
805
+ encoding: "utf-8",
806
+ stdio: ["pipe", "pipe", "pipe"]
807
+ }).trim();
808
+ if (t) {
809
+ cachedToken = t;
810
+ return cachedToken;
811
+ }
812
+ } catch {
813
+ }
814
+ throw new Error("No GitHub token found. Set GITHUB_TOKEN or run `gh auth login`.");
800
815
  }
801
816
  function headers(accept = "application/vnd.github+json") {
802
817
  return {
803
- Authorization: `Bearer ${token()}`,
818
+ Authorization: `Bearer ${resolveToken()}`,
804
819
  Accept: accept,
805
820
  "X-GitHub-Api-Version": "2022-11-28"
806
821
  };
@@ -830,7 +845,7 @@ async function getPrShas(prNumber, repo) {
830
845
  return { baseRefOid: data.base.sha, headRefOid: data.head.sha };
831
846
  }
832
847
  function detectRepo() {
833
- const fromEnv = process.env["GITHUB_REPOSITORY"];
848
+ const fromEnv = process.env.GITHUB_REPOSITORY;
834
849
  if (fromEnv) return fromEnv;
835
850
  try {
836
851
  const remote = execFileSync2("git", ["remote", "get-url", "origin"], {
@@ -841,15 +856,27 @@ function detectRepo() {
841
856
  if (match) return match[1];
842
857
  } catch {
843
858
  }
844
- throw new Error(
845
- "Could not detect GitHub repo \u2014 set GITHUB_REPOSITORY or pass --repo"
846
- );
859
+ throw new Error("Could not detect GitHub repo \u2014 set GITHUB_REPOSITORY or pass --repo");
860
+ }
861
+
862
+ // src/sources/local.ts
863
+ import { readFileSync } from "fs";
864
+ function readLocalFile(path) {
865
+ try {
866
+ return readFileSync(path, "utf-8");
867
+ } catch {
868
+ return null;
869
+ }
847
870
  }
848
871
 
849
872
  // src/index.ts
850
873
  async function resolveApiShas(options) {
851
874
  if (options.baseSha && options.headSha) {
852
- return { baseSha: options.baseSha, headSha: options.headSha, repo: options.repo ?? detectRepo() };
875
+ return {
876
+ baseSha: options.baseSha,
877
+ headSha: options.headSha,
878
+ repo: options.repo ?? detectRepo()
879
+ };
853
880
  }
854
881
  if (options.prNumber) {
855
882
  const repo = options.repo ?? detectRepo();
@@ -863,15 +890,8 @@ async function run(options = {}) {
863
890
  if (options.oldFile && options.newFile) {
864
891
  const oldPath = options.oldFile;
865
892
  const newPath = options.newFile;
866
- const readLocal = (filePath) => {
867
- try {
868
- return readFileSync(filePath, "utf-8");
869
- } catch {
870
- return null;
871
- }
872
- };
873
- const getBase2 = (path) => Promise.resolve(path === oldPath ? readLocal(oldPath) : null);
874
- const getHead2 = (path) => Promise.resolve(path === newPath ? readLocal(newPath) : null);
893
+ const getBase2 = (path) => Promise.resolve(path === oldPath ? readLocalFile(oldPath) : null);
894
+ const getHead2 = (path) => Promise.resolve(path === newPath ? readLocalFile(newPath) : null);
875
895
  const lockfiles2 = await collectLockfileEntries({
876
896
  getBase: getBase2,
877
897
  getHead: getHead2,
@@ -924,22 +944,22 @@ async function run(options = {}) {
924
944
 
925
945
  // src/cli.ts
926
946
  var program = new Command();
927
- program.name("depdiff").description("Diff dependency lockfiles between git refs, PRs, or local files").version("0.1.0").option(
947
+ program.name("lockdelta").description("Diff dependency lockfiles between git refs, PRs, or local files").version("0.1.0").option(
928
948
  "--base <ref>",
929
949
  'Base git ref (default: HEAD~1). In CI, reads GITHUB_BASE_REF \u2014 may need "origin/" prefix.',
930
- process.env["GITHUB_BASE_REF"]
950
+ process.env.GITHUB_BASE_REF
931
951
  ).option(
932
952
  "--head <ref>",
933
953
  "Head git ref (default: HEAD). In CI, reads GITHUB_HEAD_REF.",
934
- process.env["GITHUB_HEAD_REF"]
954
+ process.env.GITHUB_HEAD_REF
935
955
  ).option(
936
956
  "--pr <number>",
937
957
  "GitHub PR number. Fetches exact SHAs via gh CLI.",
938
- process.env["GITHUB_PR_NUMBER"]
958
+ process.env.GITHUB_PR_NUMBER
939
959
  ).option(
940
960
  "--repo <owner/name>",
941
961
  "GitHub repo in OWNER/NAME format. Auto-detected if omitted.",
942
- process.env["GITHUB_REPOSITORY"]
962
+ process.env.GITHUB_REPOSITORY
943
963
  ).option("--lockfile <path>", "Specific lockfile path. Auto-discovers all lockfiles if omitted.").option("--type <type>", "Force lockfile type: uv, poetry, pdm. Only used with --lockfile.").option("--old <path>", "Old lockfile path (local file comparison mode).").option("--new <path>", "New lockfile path (local file comparison mode).").option("--output <path>", "Write JSON report to file instead of stdout.").action(async (opts) => {
944
964
  try {
945
965
  const report = await run({
@@ -954,7 +974,8 @@ program.name("depdiff").description("Diff dependency lockfiles between git refs,
954
974
  onNote: (msg) => process.stderr.write(`Note: ${msg}
955
975
  `)
956
976
  });
957
- const json = JSON.stringify(report, null, 2) + "\n";
977
+ const json = `${JSON.stringify(report, null, 2)}
978
+ `;
958
979
  if (opts.output) {
959
980
  writeFileSync(opts.output, json, "utf-8");
960
981
  process.stderr.write(`Report written to ${opts.output}