@patchstack/connect 0.3.18 → 0.3.20

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.
@@ -0,0 +1,1090 @@
1
+ // src/config.ts
2
+ import { readFile, writeFile } from "fs/promises";
3
+ import path from "path";
4
+
5
+ // src/types.ts
6
+ var PatchstackError = class extends Error {
7
+ constructor(message, code, cause) {
8
+ super(message);
9
+ this.code = code;
10
+ this.cause = cause;
11
+ this.name = "PatchstackError";
12
+ }
13
+ code;
14
+ cause;
15
+ };
16
+
17
+ // src/client.ts
18
+ var DEFAULT_ENDPOINT = "https://api.patchstack.com/monitor/pulse/manifest";
19
+ var DEFAULT_TIMEOUT_MS = 3e4;
20
+ function buildEndpointUrl(base, siteUuid) {
21
+ const trimmed = base.replace(/\/$/, "");
22
+ return siteUuid !== void 0 && siteUuid !== null && siteUuid.length > 0 ? `${trimmed}/${encodeURIComponent(siteUuid)}` : trimmed;
23
+ }
24
+ async function postManifest(config, payload) {
25
+ const url = buildEndpointUrl(config.endpoint, config.siteUuid);
26
+ const timeoutMs = config.timeoutMs;
27
+ let response;
28
+ try {
29
+ response = await fetch(url, {
30
+ method: "POST",
31
+ headers: {
32
+ "Content-Type": "application/json",
33
+ Accept: "application/json",
34
+ "User-Agent": "@patchstack/connect"
35
+ },
36
+ body: JSON.stringify({ ...payload, environment: config.environment }),
37
+ signal: AbortSignal.timeout(timeoutMs)
38
+ });
39
+ } catch (cause) {
40
+ if (isTimeoutError(cause)) {
41
+ throw new PatchstackError(
42
+ `Patchstack request to ${url} timed out after ${timeoutMs}ms. Override with PATCHSTACK_TIMEOUT_MS.`,
43
+ "NETWORK_TIMEOUT",
44
+ cause
45
+ );
46
+ }
47
+ throw new PatchstackError(
48
+ `Could not reach Patchstack at ${url}. Check your network connection.`,
49
+ "NETWORK_ERROR",
50
+ cause
51
+ );
52
+ }
53
+ const text = await response.text();
54
+ let body = null;
55
+ try {
56
+ body = text.length > 0 ? JSON.parse(text) : null;
57
+ } catch {
58
+ body = null;
59
+ }
60
+ if (response.status === 404) {
61
+ throw new PatchstackError(
62
+ body?.error ?? "Site not found. Check that your site UUID is correct and that the app is registered as a Pulse app in your Patchstack dashboard.",
63
+ "SITE_NOT_FOUND"
64
+ );
65
+ }
66
+ if (response.status === 422) {
67
+ throw new PatchstackError(
68
+ body?.message ?? "Patchstack rejected the manifest payload (validation failed).",
69
+ "VALIDATION_ERROR"
70
+ );
71
+ }
72
+ if (response.status < 200 || response.status >= 300) {
73
+ throw new PatchstackError(
74
+ `Patchstack returned ${response.status}: ${text.slice(0, 200)}`,
75
+ "SERVER_ERROR"
76
+ );
77
+ }
78
+ if (body === null) {
79
+ throw new PatchstackError("Patchstack returned an empty response.", "SERVER_ERROR");
80
+ }
81
+ return body;
82
+ }
83
+ function isTimeoutError(cause) {
84
+ if (cause instanceof Error) {
85
+ return cause.name === "TimeoutError" || cause.name === "AbortError";
86
+ }
87
+ return false;
88
+ }
89
+
90
+ // src/config.ts
91
+ var CONFIG_FILENAME = ".patchstackrc.json";
92
+ var DEFAULT_ENVIRONMENT = "production";
93
+ async function resolveConfig(options) {
94
+ const fromFile = await readConfigFile(options.cwd);
95
+ const fromEnv = readEnv();
96
+ const siteUuid = options.cliSiteUuid ?? fromEnv.siteUuid ?? fromFile.siteUuid ?? null;
97
+ const endpoint = options.cliEndpoint ?? fromEnv.endpoint ?? fromFile.endpoint ?? DEFAULT_ENDPOINT;
98
+ const timeoutMs = fromEnv.timeoutMs ?? fromFile.timeoutMs ?? DEFAULT_TIMEOUT_MS;
99
+ const environmentRaw = fromEnv.environment ?? fromFile.environment;
100
+ if (environmentRaw !== void 0 && !isEnvironment(environmentRaw)) {
101
+ throw new PatchstackError(
102
+ `Environment must be "production" or "sandbox"; got "${environmentRaw}".`,
103
+ "CONFIG_INVALID"
104
+ );
105
+ }
106
+ const environment = environmentRaw ?? DEFAULT_ENVIRONMENT;
107
+ if (siteUuid !== null && siteUuid.length > 0 && !isUuid(siteUuid)) {
108
+ throw new PatchstackError(
109
+ `Site UUID "${siteUuid}" does not look like a valid UUID.`,
110
+ "CONFIG_INVALID"
111
+ );
112
+ }
113
+ if (options.requireSiteUuid && (siteUuid === null || siteUuid.length === 0)) {
114
+ throw new PatchstackError(
115
+ "No site UUID configured. Run `patchstack-connect scan` to provision one, or set PATCHSTACK_SITE_UUID.",
116
+ "CONFIG_MISSING"
117
+ );
118
+ }
119
+ return {
120
+ siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
121
+ endpoint,
122
+ timeoutMs,
123
+ environment,
124
+ widget: fromFile.widget !== false
125
+ };
126
+ }
127
+ async function readConfigFile(cwd) {
128
+ const target = path.join(cwd, CONFIG_FILENAME);
129
+ let raw;
130
+ try {
131
+ raw = await readFile(target, "utf8");
132
+ } catch (err) {
133
+ if (err.code === "ENOENT") {
134
+ return {};
135
+ }
136
+ throw new PatchstackError(
137
+ `Could not read ${target}: ${err.message}`,
138
+ "CONFIG_INVALID",
139
+ err
140
+ );
141
+ }
142
+ try {
143
+ return JSON.parse(raw);
144
+ } catch (err) {
145
+ throw new PatchstackError(
146
+ `Config file ${target} contains invalid JSON.`,
147
+ "CONFIG_INVALID",
148
+ err
149
+ );
150
+ }
151
+ }
152
+ function readEnv() {
153
+ const timeoutRaw = process.env.PATCHSTACK_TIMEOUT_MS;
154
+ let timeoutMs;
155
+ if (timeoutRaw !== void 0 && timeoutRaw.length > 0) {
156
+ const parsed = Number(timeoutRaw);
157
+ if (!Number.isFinite(parsed) || parsed <= 0) {
158
+ throw new PatchstackError(
159
+ `PATCHSTACK_TIMEOUT_MS must be a positive number; got "${timeoutRaw}".`,
160
+ "CONFIG_INVALID"
161
+ );
162
+ }
163
+ timeoutMs = parsed;
164
+ }
165
+ const environmentRaw = process.env.PATCHSTACK_ENVIRONMENT;
166
+ return {
167
+ siteUuid: process.env.PATCHSTACK_SITE_UUID ?? void 0,
168
+ endpoint: process.env.PATCHSTACK_ENDPOINT ?? void 0,
169
+ timeoutMs,
170
+ environment: environmentRaw !== void 0 && environmentRaw.length > 0 ? environmentRaw : void 0
171
+ };
172
+ }
173
+ function isUuid(value) {
174
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
175
+ }
176
+ function isEnvironment(value) {
177
+ return value === "production" || value === "sandbox";
178
+ }
179
+
180
+ // src/parsers/index.ts
181
+ import { access } from "fs/promises";
182
+ import path6 from "path";
183
+
184
+ // src/parsers/consistency.ts
185
+ import { readFile as readFile2 } from "fs/promises";
186
+ import path2 from "path";
187
+ async function readDeclaredDependencyNames(cwd) {
188
+ let raw;
189
+ try {
190
+ raw = await readFile2(path2.join(cwd, "package.json"), "utf8");
191
+ } catch {
192
+ return [];
193
+ }
194
+ let parsed;
195
+ try {
196
+ parsed = JSON.parse(raw);
197
+ } catch {
198
+ return [];
199
+ }
200
+ if (typeof parsed !== "object" || parsed === null) {
201
+ return [];
202
+ }
203
+ const names = /* @__PURE__ */ new Set();
204
+ for (const group of ["dependencies", "devDependencies"]) {
205
+ const deps = parsed[group];
206
+ if (typeof deps !== "object" || deps === null) {
207
+ continue;
208
+ }
209
+ for (const [name, spec] of Object.entries(deps)) {
210
+ if (typeof spec === "string" && /^(file|link|workspace|portal):/.test(spec)) {
211
+ continue;
212
+ }
213
+ names.add(name);
214
+ }
215
+ }
216
+ return [...names];
217
+ }
218
+ function missingDependencies(declared, packages) {
219
+ const present = new Set(packages.map((entry) => entry.name));
220
+ return declared.filter((name) => !present.has(name));
221
+ }
222
+
223
+ // src/parsers/npm.ts
224
+ import { readFile as readFile3 } from "fs/promises";
225
+ import path3 from "path";
226
+ async function parseNpmLockfile(lockfilePath) {
227
+ let raw;
228
+ try {
229
+ raw = await readFile3(lockfilePath, "utf8");
230
+ } catch (cause) {
231
+ throw new PatchstackError(`Could not read lockfile at ${lockfilePath}`, "LOCKFILE_NOT_FOUND", cause);
232
+ }
233
+ let parsed;
234
+ try {
235
+ parsed = JSON.parse(raw);
236
+ } catch (cause) {
237
+ throw new PatchstackError(`Lockfile at ${lockfilePath} is not valid JSON`, "LOCKFILE_PARSE_ERROR", cause);
238
+ }
239
+ if (parsed.packages) {
240
+ return extractFromV2(parsed.packages);
241
+ }
242
+ if (parsed.dependencies) {
243
+ return extractFromV1(parsed.dependencies);
244
+ }
245
+ throw new PatchstackError(
246
+ `Lockfile at ${lockfilePath} has no "packages" or "dependencies" key`,
247
+ "LOCKFILE_PARSE_ERROR"
248
+ );
249
+ }
250
+ function extractFromV2(packages) {
251
+ const entries = [];
252
+ for (const [pkgPath, pkg] of Object.entries(packages)) {
253
+ if (pkgPath === "") {
254
+ continue;
255
+ }
256
+ if (pkg.link === true) {
257
+ continue;
258
+ }
259
+ if (typeof pkg.version !== "string" || pkg.version.length === 0) {
260
+ continue;
261
+ }
262
+ const name = pkg.name ?? extractNameFromPath(pkgPath);
263
+ if (name === null) {
264
+ continue;
265
+ }
266
+ entries.push({
267
+ name,
268
+ version: pkg.version,
269
+ path: pkgPath,
270
+ direct: isDirectV2(pkgPath)
271
+ });
272
+ }
273
+ return entries;
274
+ }
275
+ function extractFromV1(deps, acc = [], depth = 0) {
276
+ for (const [name, dep] of Object.entries(deps)) {
277
+ if (typeof dep.version === "string" && dep.version.length > 0) {
278
+ acc.push({ name, version: dep.version, direct: depth === 0 });
279
+ }
280
+ if (dep.dependencies) {
281
+ extractFromV1(dep.dependencies, acc, depth + 1);
282
+ }
283
+ }
284
+ return acc;
285
+ }
286
+ function extractNameFromPath(pkgPath) {
287
+ const segments = pkgPath.split("node_modules" + path3.sep === pkgPath ? path3.sep : "/");
288
+ const parts = pkgPath.split("/");
289
+ const nmIndex = parts.lastIndexOf("node_modules");
290
+ if (nmIndex === -1 || nmIndex >= parts.length - 1) {
291
+ return segments[segments.length - 1] ?? null;
292
+ }
293
+ const tail = parts.slice(nmIndex + 1);
294
+ if (tail.length === 0) {
295
+ return null;
296
+ }
297
+ const first = tail[0];
298
+ if (first !== void 0 && first.startsWith("@") && tail.length >= 2) {
299
+ return `${first}/${tail[1]}`;
300
+ }
301
+ return first ?? null;
302
+ }
303
+ function isDirectV2(pkgPath) {
304
+ const parts = pkgPath.split("/");
305
+ const nmCount = parts.filter((p) => p === "node_modules").length;
306
+ return nmCount === 1;
307
+ }
308
+
309
+ // src/parsers/node_modules.ts
310
+ import { lstat, readFile as readFile4, readdir, stat } from "fs/promises";
311
+ import path4 from "path";
312
+ async function walkNodeModules(cwd) {
313
+ const root = path4.join(cwd, "node_modules");
314
+ try {
315
+ const info = await stat(root);
316
+ if (!info.isDirectory()) {
317
+ throw new PatchstackError(
318
+ `${root} exists but is not a directory.`,
319
+ "LOCKFILE_NOT_FOUND"
320
+ );
321
+ }
322
+ } catch (cause) {
323
+ if (cause instanceof PatchstackError) {
324
+ throw cause;
325
+ }
326
+ throw new PatchstackError(
327
+ `node_modules/ not found at ${cwd}. Install dependencies first (e.g. \`bun install\` or \`npm install\`).`,
328
+ "LOCKFILE_NOT_FOUND",
329
+ cause
330
+ );
331
+ }
332
+ const entries = [];
333
+ await walk(root, entries, 0);
334
+ return entries;
335
+ }
336
+ async function walk(dir, acc, depth) {
337
+ let names;
338
+ try {
339
+ names = await readdir(dir);
340
+ } catch {
341
+ return;
342
+ }
343
+ for (const name of names) {
344
+ if (name.startsWith(".")) {
345
+ continue;
346
+ }
347
+ const fullPath = path4.join(dir, name);
348
+ if (!await isPlainDirectory(fullPath)) {
349
+ continue;
350
+ }
351
+ if (name.startsWith("@")) {
352
+ let subNames;
353
+ try {
354
+ subNames = await readdir(fullPath);
355
+ } catch {
356
+ continue;
357
+ }
358
+ for (const sub of subNames) {
359
+ if (sub.startsWith(".")) {
360
+ continue;
361
+ }
362
+ const scopedDir = path4.join(fullPath, sub);
363
+ if (!await isPlainDirectory(scopedDir)) {
364
+ continue;
365
+ }
366
+ await readPackage(scopedDir, depth, acc);
367
+ await walkNested(scopedDir, acc, depth);
368
+ }
369
+ continue;
370
+ }
371
+ await readPackage(fullPath, depth, acc);
372
+ await walkNested(fullPath, acc, depth);
373
+ }
374
+ }
375
+ async function readPackage(pkgDir, depth, acc) {
376
+ let raw;
377
+ try {
378
+ raw = await readFile4(path4.join(pkgDir, "package.json"), "utf8");
379
+ } catch {
380
+ return;
381
+ }
382
+ let parsed;
383
+ try {
384
+ parsed = JSON.parse(raw);
385
+ } catch {
386
+ return;
387
+ }
388
+ if (typeof parsed.name !== "string" || parsed.name.length === 0) {
389
+ return;
390
+ }
391
+ if (typeof parsed.version !== "string" || parsed.version.length === 0) {
392
+ return;
393
+ }
394
+ acc.push({
395
+ name: parsed.name,
396
+ version: parsed.version,
397
+ direct: depth === 0
398
+ });
399
+ }
400
+ async function walkNested(pkgDir, acc, depth) {
401
+ const nested = path4.join(pkgDir, "node_modules");
402
+ if (!await isPlainDirectory(nested)) {
403
+ return;
404
+ }
405
+ await walk(nested, acc, depth + 1);
406
+ }
407
+ async function isPlainDirectory(dir) {
408
+ try {
409
+ const info = await lstat(dir);
410
+ return info.isDirectory() && !info.isSymbolicLink();
411
+ } catch {
412
+ return false;
413
+ }
414
+ }
415
+
416
+ // src/parsers/pnpm.ts
417
+ import { readFile as readFile5 } from "fs/promises";
418
+ async function parsePnpmLockfile(lockfilePath) {
419
+ let raw;
420
+ try {
421
+ raw = await readFile5(lockfilePath, "utf8");
422
+ } catch (cause) {
423
+ throw new PatchstackError(
424
+ `Could not read lockfile at ${lockfilePath}`,
425
+ "LOCKFILE_NOT_FOUND",
426
+ cause
427
+ );
428
+ }
429
+ const lines = raw.split(/\r?\n/);
430
+ const directNames = collectDirectDepNames(lines);
431
+ const packageKeys = collectPackagesBlockKeys(lines);
432
+ if (packageKeys.length === 0) {
433
+ throw new PatchstackError(
434
+ `Lockfile at ${lockfilePath} has no "packages" entries`,
435
+ "LOCKFILE_PARSE_ERROR"
436
+ );
437
+ }
438
+ const entries = [];
439
+ for (const key of packageKeys) {
440
+ const parsed = parsePackageKey(key);
441
+ if (parsed === null) {
442
+ continue;
443
+ }
444
+ entries.push({
445
+ name: parsed.name,
446
+ version: parsed.version,
447
+ direct: directNames.has(parsed.name)
448
+ });
449
+ }
450
+ return entries;
451
+ }
452
+ function parsePackageKey(rawKey) {
453
+ let k = rawKey.trim();
454
+ if (k.length === 0) {
455
+ return null;
456
+ }
457
+ if (k.startsWith("'") && k.endsWith("'") || k.startsWith('"') && k.endsWith('"')) {
458
+ k = k.slice(1, -1);
459
+ }
460
+ if (k.startsWith("/")) {
461
+ k = k.slice(1);
462
+ }
463
+ const parenIdx = k.indexOf("(");
464
+ if (parenIdx >= 0) {
465
+ k = k.slice(0, parenIdx);
466
+ }
467
+ let scopePrefix = "";
468
+ let body = k;
469
+ if (k.startsWith("@")) {
470
+ const firstSlash = k.indexOf("/");
471
+ if (firstSlash <= 0) {
472
+ return null;
473
+ }
474
+ scopePrefix = k.slice(0, firstSlash + 1);
475
+ body = k.slice(firstSlash + 1);
476
+ }
477
+ const slashIdx = body.indexOf("/");
478
+ const atIdx = body.indexOf("@");
479
+ let sepIdx;
480
+ if (slashIdx < 0 && atIdx < 0) {
481
+ return null;
482
+ } else if (slashIdx < 0) {
483
+ sepIdx = atIdx;
484
+ } else if (atIdx < 0) {
485
+ sepIdx = slashIdx;
486
+ } else {
487
+ sepIdx = Math.min(slashIdx, atIdx);
488
+ }
489
+ const name = scopePrefix + body.slice(0, sepIdx);
490
+ let version = body.slice(sepIdx + 1);
491
+ const underscoreIdx = version.indexOf("_");
492
+ if (underscoreIdx >= 0) {
493
+ version = version.slice(0, underscoreIdx);
494
+ }
495
+ if (name.length === 0 || version.length === 0) {
496
+ return null;
497
+ }
498
+ return { name, version };
499
+ }
500
+ function indentOf(line) {
501
+ let i = 0;
502
+ while (i < line.length && line[i] === " ") {
503
+ i++;
504
+ }
505
+ return i;
506
+ }
507
+ function isBlankOrComment(line) {
508
+ const trimmed = line.trim();
509
+ return trimmed.length === 0 || trimmed.startsWith("#");
510
+ }
511
+ function collectPackagesBlockKeys(lines) {
512
+ const keys = [];
513
+ let inBlock = false;
514
+ let childIndent = null;
515
+ for (const line of lines) {
516
+ if (isBlankOrComment(line)) {
517
+ continue;
518
+ }
519
+ const indent = indentOf(line);
520
+ if (!inBlock) {
521
+ if (indent === 0 && line.trim() === "packages:") {
522
+ inBlock = true;
523
+ }
524
+ continue;
525
+ }
526
+ if (indent === 0) {
527
+ break;
528
+ }
529
+ if (childIndent === null) {
530
+ childIndent = indent;
531
+ }
532
+ if (indent !== childIndent) {
533
+ continue;
534
+ }
535
+ const content = line.slice(indent);
536
+ if (!content.endsWith(":")) {
537
+ continue;
538
+ }
539
+ keys.push(content.slice(0, -1));
540
+ }
541
+ return keys;
542
+ }
543
+ function collectDirectDepNames(lines) {
544
+ const names = /* @__PURE__ */ new Set();
545
+ collectFromImporters(lines, names);
546
+ for (const section of ["dependencies", "devDependencies", "optionalDependencies"]) {
547
+ collectFromTopLevelSection(lines, section, names);
548
+ }
549
+ return names;
550
+ }
551
+ var DEP_SECTIONS = /* @__PURE__ */ new Set([
552
+ "dependencies",
553
+ "devDependencies",
554
+ "optionalDependencies"
555
+ ]);
556
+ function collectFromImporters(lines, out) {
557
+ let inImporters = false;
558
+ let importerIndent = null;
559
+ let inDepSection = false;
560
+ let depSectionIndent = null;
561
+ let leafIndent = null;
562
+ for (const line of lines) {
563
+ if (isBlankOrComment(line)) {
564
+ continue;
565
+ }
566
+ const indent = indentOf(line);
567
+ const trimmed = line.trim();
568
+ if (!inImporters) {
569
+ if (indent === 0 && trimmed === "importers:") {
570
+ inImporters = true;
571
+ }
572
+ continue;
573
+ }
574
+ if (indent === 0) {
575
+ break;
576
+ }
577
+ if (importerIndent === null) {
578
+ importerIndent = indent;
579
+ }
580
+ if (indent === importerIndent) {
581
+ inDepSection = false;
582
+ depSectionIndent = null;
583
+ leafIndent = null;
584
+ continue;
585
+ }
586
+ if (!inDepSection) {
587
+ const key = stripTrailingColon(trimmed);
588
+ if (key !== null && DEP_SECTIONS.has(key)) {
589
+ inDepSection = true;
590
+ depSectionIndent = indent;
591
+ }
592
+ continue;
593
+ }
594
+ if (depSectionIndent !== null && indent <= depSectionIndent) {
595
+ inDepSection = false;
596
+ depSectionIndent = null;
597
+ leafIndent = null;
598
+ const key = stripTrailingColon(trimmed);
599
+ if (key !== null && DEP_SECTIONS.has(key)) {
600
+ inDepSection = true;
601
+ depSectionIndent = indent;
602
+ }
603
+ continue;
604
+ }
605
+ if (leafIndent === null) {
606
+ leafIndent = indent;
607
+ }
608
+ if (indent !== leafIndent) {
609
+ continue;
610
+ }
611
+ const name = extractLeafName(trimmed);
612
+ if (name !== null) {
613
+ out.add(name);
614
+ }
615
+ }
616
+ }
617
+ function collectFromTopLevelSection(lines, section, out) {
618
+ let inSection = false;
619
+ let leafIndent = null;
620
+ for (const line of lines) {
621
+ if (isBlankOrComment(line)) {
622
+ continue;
623
+ }
624
+ const indent = indentOf(line);
625
+ const trimmed = line.trim();
626
+ if (!inSection) {
627
+ if (indent === 0 && trimmed === `${section}:`) {
628
+ inSection = true;
629
+ }
630
+ continue;
631
+ }
632
+ if (indent === 0) {
633
+ break;
634
+ }
635
+ if (leafIndent === null) {
636
+ leafIndent = indent;
637
+ }
638
+ if (indent !== leafIndent) {
639
+ continue;
640
+ }
641
+ const name = extractLeafName(trimmed);
642
+ if (name !== null) {
643
+ out.add(name);
644
+ }
645
+ }
646
+ }
647
+ function stripTrailingColon(s) {
648
+ if (!s.endsWith(":")) {
649
+ return null;
650
+ }
651
+ return s.slice(0, -1).trim();
652
+ }
653
+ function extractLeafName(trimmed) {
654
+ const colonIdx = trimmed.indexOf(":");
655
+ if (colonIdx < 0) {
656
+ return null;
657
+ }
658
+ let name = trimmed.slice(0, colonIdx).trim();
659
+ if (name.length === 0) {
660
+ return null;
661
+ }
662
+ if (name.startsWith("'") && name.endsWith("'") || name.startsWith('"') && name.endsWith('"')) {
663
+ name = name.slice(1, -1);
664
+ }
665
+ return name.length > 0 ? name : null;
666
+ }
667
+
668
+ // src/parsers/yarn.ts
669
+ import { readFile as readFile6 } from "fs/promises";
670
+ import path5 from "path";
671
+ async function parseYarnLockfile(lockfilePath) {
672
+ let raw;
673
+ try {
674
+ raw = await readFile6(lockfilePath, "utf8");
675
+ } catch (cause) {
676
+ throw new PatchstackError(
677
+ `Could not read lockfile at ${lockfilePath}`,
678
+ "LOCKFILE_NOT_FOUND",
679
+ cause
680
+ );
681
+ }
682
+ const blocks = parseBlocks(raw);
683
+ if (blocks.length === 0) {
684
+ throw new PatchstackError(
685
+ `Lockfile at ${lockfilePath} contains no package entries`,
686
+ "LOCKFILE_PARSE_ERROR"
687
+ );
688
+ }
689
+ const directNames = await readDirectDepNames(path5.dirname(lockfilePath));
690
+ const entries = [];
691
+ const seen = /* @__PURE__ */ new Set();
692
+ for (const block of blocks) {
693
+ if (block.version.length === 0 || block.names.size === 0) {
694
+ continue;
695
+ }
696
+ for (const name of block.names) {
697
+ const dedupKey = `${name}@${block.version}`;
698
+ if (seen.has(dedupKey)) {
699
+ continue;
700
+ }
701
+ seen.add(dedupKey);
702
+ entries.push({
703
+ name,
704
+ version: block.version,
705
+ direct: directNames.has(name)
706
+ });
707
+ }
708
+ }
709
+ return entries;
710
+ }
711
+ function parseBlocks(raw) {
712
+ const lines = raw.split(/\r?\n/);
713
+ const blocks = [];
714
+ let current = null;
715
+ const finalize = () => {
716
+ if (current !== null && current.version.length > 0 && current.names.size > 0) {
717
+ blocks.push(current);
718
+ }
719
+ current = null;
720
+ };
721
+ for (const line of lines) {
722
+ const trimmed = line.trim();
723
+ if (trimmed.length === 0 || trimmed.startsWith("#")) {
724
+ continue;
725
+ }
726
+ const indent = countLeadingSpaces(line);
727
+ if (indent === 0) {
728
+ finalize();
729
+ if (!trimmed.endsWith(":")) {
730
+ continue;
731
+ }
732
+ const keyLine = trimmed.slice(0, -1);
733
+ const names = /* @__PURE__ */ new Set();
734
+ for (const spec of splitDescriptors(keyLine)) {
735
+ const name = extractName(spec);
736
+ if (name !== null) {
737
+ names.add(name);
738
+ }
739
+ }
740
+ current = { names, version: "" };
741
+ continue;
742
+ }
743
+ if (current === null) {
744
+ continue;
745
+ }
746
+ const version = parseVersionField(trimmed);
747
+ if (version !== null) {
748
+ current.version = version;
749
+ }
750
+ }
751
+ finalize();
752
+ return blocks;
753
+ }
754
+ function countLeadingSpaces(line) {
755
+ let i = 0;
756
+ while (i < line.length && line[i] === " ") {
757
+ i++;
758
+ }
759
+ return i;
760
+ }
761
+ function splitDescriptors(keyLine) {
762
+ const parts = [];
763
+ let current = "";
764
+ let quote = null;
765
+ for (let i = 0; i < keyLine.length; i++) {
766
+ const c = keyLine[i];
767
+ if (quote !== null) {
768
+ current += c;
769
+ if (c === quote) {
770
+ quote = null;
771
+ }
772
+ continue;
773
+ }
774
+ if (c === '"' || c === "'") {
775
+ quote = c;
776
+ current += c;
777
+ continue;
778
+ }
779
+ if (c === ",") {
780
+ const piece = current.trim();
781
+ if (piece.length > 0) {
782
+ parts.push(piece);
783
+ }
784
+ current = "";
785
+ continue;
786
+ }
787
+ current += c;
788
+ }
789
+ const tail = current.trim();
790
+ if (tail.length > 0) {
791
+ parts.push(tail);
792
+ }
793
+ return parts;
794
+ }
795
+ function extractName(rawSpec) {
796
+ let s = rawSpec.trim();
797
+ if (s.length === 0) {
798
+ return null;
799
+ }
800
+ if (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'")) {
801
+ s = s.slice(1, -1);
802
+ }
803
+ const atIdx = s.lastIndexOf("@");
804
+ if (atIdx <= 0) {
805
+ return null;
806
+ }
807
+ const name = s.slice(0, atIdx);
808
+ return name.length > 0 ? name : null;
809
+ }
810
+ function parseVersionField(content) {
811
+ if (!content.startsWith("version")) {
812
+ return null;
813
+ }
814
+ const after = content.slice("version".length);
815
+ const firstChar = after.charAt(0);
816
+ if (firstChar !== " " && firstChar !== " " && firstChar !== ":") {
817
+ return null;
818
+ }
819
+ let rest = firstChar === ":" ? after.slice(1) : after;
820
+ rest = rest.trim();
821
+ if (rest.length === 0) {
822
+ return null;
823
+ }
824
+ if (rest.startsWith('"') && rest.endsWith('"') || rest.startsWith("'") && rest.endsWith("'")) {
825
+ rest = rest.slice(1, -1);
826
+ }
827
+ return rest.length > 0 ? rest : null;
828
+ }
829
+ async function readDirectDepNames(cwd) {
830
+ const names = /* @__PURE__ */ new Set();
831
+ let raw;
832
+ try {
833
+ raw = await readFile6(path5.join(cwd, "package.json"), "utf8");
834
+ } catch {
835
+ return names;
836
+ }
837
+ let parsed;
838
+ try {
839
+ parsed = JSON.parse(raw);
840
+ } catch {
841
+ return names;
842
+ }
843
+ if (typeof parsed !== "object" || parsed === null) {
844
+ return names;
845
+ }
846
+ const obj = parsed;
847
+ for (const field of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) {
848
+ const section = obj[field];
849
+ if (typeof section !== "object" || section === null) {
850
+ continue;
851
+ }
852
+ for (const name of Object.keys(section)) {
853
+ names.add(name);
854
+ }
855
+ }
856
+ return names;
857
+ }
858
+
859
+ // src/parsers/index.ts
860
+ var LOCKFILE_CANDIDATES = [
861
+ { filename: "package-lock.json", strategy: "npm-lockfile" },
862
+ { filename: "bun.lock", strategy: "node-modules-walk" },
863
+ { filename: "bun.lockb", strategy: "node-modules-walk" },
864
+ { filename: "pnpm-lock.yaml", strategy: "pnpm-lockfile" },
865
+ { filename: "yarn.lock", strategy: "yarn-lockfile" }
866
+ ];
867
+ async function detectLockfile(cwd) {
868
+ const probed = await Promise.all(
869
+ LOCKFILE_CANDIDATES.map(async (candidate) => {
870
+ const filePath = path6.join(cwd, candidate.filename);
871
+ return { ...candidate, filePath, present: await exists(filePath) };
872
+ })
873
+ );
874
+ const match = probed.find((candidate) => candidate.present);
875
+ if (match) {
876
+ return {
877
+ ecosystem: "npm",
878
+ filePath: match.filePath,
879
+ filename: match.filename,
880
+ strategy: match.strategy
881
+ };
882
+ }
883
+ const expected = LOCKFILE_CANDIDATES.map((candidate) => candidate.filename).join(", ");
884
+ throw new PatchstackError(
885
+ `No lockfile found in ${cwd}. Expected one of: ${expected}.`,
886
+ "LOCKFILE_NOT_FOUND"
887
+ );
888
+ }
889
+ async function scanLockfile(cwd) {
890
+ const candidates = await presentLockfiles(cwd);
891
+ if (candidates.length === 0) {
892
+ await detectLockfile(cwd);
893
+ }
894
+ const declared = await readDeclaredDependencyNames(cwd);
895
+ const warnings = [];
896
+ let firstParsed = null;
897
+ let walkTried = false;
898
+ for (const candidate of candidates) {
899
+ let packages;
900
+ try {
901
+ packages = await runStrategy(candidate, cwd);
902
+ } catch {
903
+ continue;
904
+ }
905
+ walkTried ||= candidate.strategy === "node-modules-walk";
906
+ firstParsed ??= { packages, filename: candidate.filename };
907
+ const missing = missingDependencies(declared, packages);
908
+ if (missing.length === 0) {
909
+ return manifestWith(packages, warnings);
910
+ }
911
+ warnings.push(staleWarning(candidate.filename, missing));
912
+ }
913
+ if (!walkTried) {
914
+ try {
915
+ const packages = await walkNodeModules(cwd);
916
+ const missing = missingDependencies(declared, packages);
917
+ if (missing.length > 0) {
918
+ warnings.push(staleWarning("node_modules/", missing));
919
+ }
920
+ warnings.push("Scanned node_modules/ instead. Delete the stale lockfile to silence this warning.");
921
+ return manifestWith(packages, warnings);
922
+ } catch {
923
+ }
924
+ }
925
+ if (firstParsed === null) {
926
+ const expected = LOCKFILE_CANDIDATES.map((candidate) => candidate.filename).join(", ");
927
+ throw new PatchstackError(
928
+ `No readable lockfile found in ${cwd}. Expected one of: ${expected}.`,
929
+ "LOCKFILE_NOT_FOUND"
930
+ );
931
+ }
932
+ warnings.push(
933
+ `No fully-consistent source found; reporting ${firstParsed.filename}. The manifest may understate the real dependency set.`
934
+ );
935
+ return manifestWith(firstParsed.packages, warnings);
936
+ }
937
+ function manifestWith(packages, warnings) {
938
+ return warnings.length > 0 ? { ecosystem: "npm", packages, warnings } : { ecosystem: "npm", packages };
939
+ }
940
+ function staleWarning(source, missing) {
941
+ const sample = missing.slice(0, 3).join(", ");
942
+ const suffix = missing.length > 3 ? `, +${missing.length - 3} more` : "";
943
+ return `${source} looks stale: package.json declares ${missing.length} dependenc${missing.length === 1 ? "y" : "ies"} it does not contain (${sample}${suffix}).`;
944
+ }
945
+ async function presentLockfiles(cwd) {
946
+ const probed = await Promise.all(
947
+ LOCKFILE_CANDIDATES.map(async (candidate) => {
948
+ const filePath = path6.join(cwd, candidate.filename);
949
+ return { ...candidate, filePath, present: await exists(filePath) };
950
+ })
951
+ );
952
+ return probed.filter((candidate) => candidate.present).map(({ filename, strategy, filePath }) => ({ ecosystem: "npm", filename, strategy, filePath }));
953
+ }
954
+ async function runStrategy(detected, cwd) {
955
+ switch (detected.strategy) {
956
+ case "npm-lockfile":
957
+ return parseNpmLockfile(detected.filePath);
958
+ case "pnpm-lockfile":
959
+ return parsePnpmLockfile(detected.filePath);
960
+ case "yarn-lockfile":
961
+ return parseYarnLockfile(detected.filePath);
962
+ case "node-modules-walk":
963
+ return walkNodeModules(cwd);
964
+ }
965
+ }
966
+ async function exists(filePath) {
967
+ try {
968
+ await access(filePath);
969
+ return true;
970
+ } catch {
971
+ return false;
972
+ }
973
+ }
974
+
975
+ // src/normalize.ts
976
+ function buildWirePayload(manifest) {
977
+ const seen = /* @__PURE__ */ new Map();
978
+ const wirePackages = [];
979
+ for (const entry of manifest.packages) {
980
+ const versions = seen.get(entry.name);
981
+ if (versions) {
982
+ if (versions.has(entry.version)) {
983
+ continue;
984
+ }
985
+ versions.add(entry.version);
986
+ } else {
987
+ seen.set(entry.name, /* @__PURE__ */ new Set([entry.version]));
988
+ }
989
+ wirePackages.push({ name: entry.name, version: entry.version });
990
+ }
991
+ wirePackages.sort((a, b) => {
992
+ if (a.name === b.name) {
993
+ return compareVersions(a.version, b.version);
994
+ }
995
+ return a.name < b.name ? -1 : 1;
996
+ });
997
+ const duplicateNames = [];
998
+ for (const [name, versions] of seen) {
999
+ if (versions.size > 1) {
1000
+ duplicateNames.push(name);
1001
+ }
1002
+ }
1003
+ return {
1004
+ payload: { ecosystem: manifest.ecosystem, packages: wirePackages },
1005
+ stats: {
1006
+ uniqueNames: seen.size,
1007
+ duplicateNames,
1008
+ totalEntries: manifest.packages.length
1009
+ }
1010
+ };
1011
+ }
1012
+ function compareVersions(a, b) {
1013
+ if (a === b) {
1014
+ return 0;
1015
+ }
1016
+ const [aBase, aPre] = splitPrerelease(a);
1017
+ const [bBase, bPre] = splitPrerelease(b);
1018
+ const baseCmp = compareSegments(aBase.split("."), bBase.split("."));
1019
+ if (baseCmp !== 0) {
1020
+ return baseCmp;
1021
+ }
1022
+ if (aPre === null && bPre === null) {
1023
+ return 0;
1024
+ }
1025
+ if (aPre === null) {
1026
+ return 1;
1027
+ }
1028
+ if (bPre === null) {
1029
+ return -1;
1030
+ }
1031
+ return compareSegments(aPre.split("."), bPre.split("."));
1032
+ }
1033
+ function splitPrerelease(version) {
1034
+ const cleaned = version.replace(/^[v=]+/, "").split("+")[0];
1035
+ const dashIndex = cleaned.indexOf("-");
1036
+ if (dashIndex === -1) {
1037
+ return [cleaned, null];
1038
+ }
1039
+ return [cleaned.slice(0, dashIndex), cleaned.slice(dashIndex + 1)];
1040
+ }
1041
+ function compareSegments(a, b) {
1042
+ const max = Math.max(a.length, b.length);
1043
+ for (let i = 0; i < max; i++) {
1044
+ const aPart = a[i];
1045
+ const bPart = b[i];
1046
+ if (aPart === void 0) {
1047
+ return -1;
1048
+ }
1049
+ if (bPart === void 0) {
1050
+ return 1;
1051
+ }
1052
+ const aNum = /^\d+$/.test(aPart);
1053
+ const bNum = /^\d+$/.test(bPart);
1054
+ if (aNum && bNum) {
1055
+ const diff = Number(aPart) - Number(bPart);
1056
+ if (diff !== 0) {
1057
+ return diff < 0 ? -1 : 1;
1058
+ }
1059
+ continue;
1060
+ }
1061
+ if (aNum) {
1062
+ return -1;
1063
+ }
1064
+ if (bNum) {
1065
+ return 1;
1066
+ }
1067
+ if (aPart < bPart) {
1068
+ return -1;
1069
+ }
1070
+ if (aPart > bPart) {
1071
+ return 1;
1072
+ }
1073
+ }
1074
+ return 0;
1075
+ }
1076
+
1077
+ // src/protect/refresh-manifest.ts
1078
+ async function reportManifest(cwd) {
1079
+ const config = await resolveConfig({ cwd });
1080
+ if (config.siteUuid === null || config.siteUuid.length === 0) {
1081
+ return;
1082
+ }
1083
+ const manifest = await scanLockfile(cwd);
1084
+ const { payload } = buildWirePayload(manifest);
1085
+ await postManifest(config, payload);
1086
+ }
1087
+ export {
1088
+ reportManifest
1089
+ };
1090
+ //# sourceMappingURL=refresh-manifest-XBOGN446.js.map