@patchstack/connect 0.2.4 → 0.2.6
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/README.md +6 -3
- package/dist/cli.js +597 -24
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +482 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -3
- package/dist/index.d.ts +11 -3
- package/dist/index.js +478 -22
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/parsers/index.ts
|
|
4
4
|
import { access } from "fs/promises";
|
|
5
|
-
import
|
|
5
|
+
import path4 from "path";
|
|
6
6
|
|
|
7
7
|
// src/types.ts
|
|
8
8
|
var PatchstackError = class extends Error {
|
|
@@ -209,9 +209,452 @@ async function isPlainDirectory(dir) {
|
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
// src/parsers/pnpm.ts
|
|
213
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
214
|
+
async function parsePnpmLockfile(lockfilePath) {
|
|
215
|
+
let raw;
|
|
216
|
+
try {
|
|
217
|
+
raw = await readFile3(lockfilePath, "utf8");
|
|
218
|
+
} catch (cause) {
|
|
219
|
+
throw new PatchstackError(
|
|
220
|
+
`Could not read lockfile at ${lockfilePath}`,
|
|
221
|
+
"LOCKFILE_NOT_FOUND",
|
|
222
|
+
cause
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
const lines = raw.split(/\r?\n/);
|
|
226
|
+
const directNames = collectDirectDepNames(lines);
|
|
227
|
+
const packageKeys = collectPackagesBlockKeys(lines);
|
|
228
|
+
if (packageKeys.length === 0) {
|
|
229
|
+
throw new PatchstackError(
|
|
230
|
+
`Lockfile at ${lockfilePath} has no "packages" entries`,
|
|
231
|
+
"LOCKFILE_PARSE_ERROR"
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const entries = [];
|
|
235
|
+
for (const key of packageKeys) {
|
|
236
|
+
const parsed = parsePackageKey(key);
|
|
237
|
+
if (parsed === null) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
entries.push({
|
|
241
|
+
name: parsed.name,
|
|
242
|
+
version: parsed.version,
|
|
243
|
+
direct: directNames.has(parsed.name)
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
return entries;
|
|
247
|
+
}
|
|
248
|
+
function parsePackageKey(rawKey) {
|
|
249
|
+
let k = rawKey.trim();
|
|
250
|
+
if (k.length === 0) {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
if (k.startsWith("'") && k.endsWith("'") || k.startsWith('"') && k.endsWith('"')) {
|
|
254
|
+
k = k.slice(1, -1);
|
|
255
|
+
}
|
|
256
|
+
if (k.startsWith("/")) {
|
|
257
|
+
k = k.slice(1);
|
|
258
|
+
}
|
|
259
|
+
const parenIdx = k.indexOf("(");
|
|
260
|
+
if (parenIdx >= 0) {
|
|
261
|
+
k = k.slice(0, parenIdx);
|
|
262
|
+
}
|
|
263
|
+
let scopePrefix = "";
|
|
264
|
+
let body = k;
|
|
265
|
+
if (k.startsWith("@")) {
|
|
266
|
+
const firstSlash = k.indexOf("/");
|
|
267
|
+
if (firstSlash <= 0) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
scopePrefix = k.slice(0, firstSlash + 1);
|
|
271
|
+
body = k.slice(firstSlash + 1);
|
|
272
|
+
}
|
|
273
|
+
const slashIdx = body.indexOf("/");
|
|
274
|
+
const atIdx = body.indexOf("@");
|
|
275
|
+
let sepIdx;
|
|
276
|
+
if (slashIdx < 0 && atIdx < 0) {
|
|
277
|
+
return null;
|
|
278
|
+
} else if (slashIdx < 0) {
|
|
279
|
+
sepIdx = atIdx;
|
|
280
|
+
} else if (atIdx < 0) {
|
|
281
|
+
sepIdx = slashIdx;
|
|
282
|
+
} else {
|
|
283
|
+
sepIdx = Math.min(slashIdx, atIdx);
|
|
284
|
+
}
|
|
285
|
+
const name = scopePrefix + body.slice(0, sepIdx);
|
|
286
|
+
let version = body.slice(sepIdx + 1);
|
|
287
|
+
const underscoreIdx = version.indexOf("_");
|
|
288
|
+
if (underscoreIdx >= 0) {
|
|
289
|
+
version = version.slice(0, underscoreIdx);
|
|
290
|
+
}
|
|
291
|
+
if (name.length === 0 || version.length === 0) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
return { name, version };
|
|
295
|
+
}
|
|
296
|
+
function indentOf(line) {
|
|
297
|
+
let i = 0;
|
|
298
|
+
while (i < line.length && line[i] === " ") {
|
|
299
|
+
i++;
|
|
300
|
+
}
|
|
301
|
+
return i;
|
|
302
|
+
}
|
|
303
|
+
function isBlankOrComment(line) {
|
|
304
|
+
const trimmed = line.trim();
|
|
305
|
+
return trimmed.length === 0 || trimmed.startsWith("#");
|
|
306
|
+
}
|
|
307
|
+
function collectPackagesBlockKeys(lines) {
|
|
308
|
+
const keys = [];
|
|
309
|
+
let inBlock = false;
|
|
310
|
+
let childIndent = null;
|
|
311
|
+
for (const line of lines) {
|
|
312
|
+
if (isBlankOrComment(line)) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const indent = indentOf(line);
|
|
316
|
+
if (!inBlock) {
|
|
317
|
+
if (indent === 0 && line.trim() === "packages:") {
|
|
318
|
+
inBlock = true;
|
|
319
|
+
}
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (indent === 0) {
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
if (childIndent === null) {
|
|
326
|
+
childIndent = indent;
|
|
327
|
+
}
|
|
328
|
+
if (indent !== childIndent) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const content = line.slice(indent);
|
|
332
|
+
if (!content.endsWith(":")) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
keys.push(content.slice(0, -1));
|
|
336
|
+
}
|
|
337
|
+
return keys;
|
|
338
|
+
}
|
|
339
|
+
function collectDirectDepNames(lines) {
|
|
340
|
+
const names = /* @__PURE__ */ new Set();
|
|
341
|
+
collectFromImporters(lines, names);
|
|
342
|
+
for (const section of ["dependencies", "devDependencies", "optionalDependencies"]) {
|
|
343
|
+
collectFromTopLevelSection(lines, section, names);
|
|
344
|
+
}
|
|
345
|
+
return names;
|
|
346
|
+
}
|
|
347
|
+
var DEP_SECTIONS = /* @__PURE__ */ new Set([
|
|
348
|
+
"dependencies",
|
|
349
|
+
"devDependencies",
|
|
350
|
+
"optionalDependencies"
|
|
351
|
+
]);
|
|
352
|
+
function collectFromImporters(lines, out) {
|
|
353
|
+
let inImporters = false;
|
|
354
|
+
let importerIndent = null;
|
|
355
|
+
let inDepSection = false;
|
|
356
|
+
let depSectionIndent = null;
|
|
357
|
+
let leafIndent = null;
|
|
358
|
+
for (const line of lines) {
|
|
359
|
+
if (isBlankOrComment(line)) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const indent = indentOf(line);
|
|
363
|
+
const trimmed = line.trim();
|
|
364
|
+
if (!inImporters) {
|
|
365
|
+
if (indent === 0 && trimmed === "importers:") {
|
|
366
|
+
inImporters = true;
|
|
367
|
+
}
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (indent === 0) {
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
if (importerIndent === null) {
|
|
374
|
+
importerIndent = indent;
|
|
375
|
+
}
|
|
376
|
+
if (indent === importerIndent) {
|
|
377
|
+
inDepSection = false;
|
|
378
|
+
depSectionIndent = null;
|
|
379
|
+
leafIndent = null;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
if (!inDepSection) {
|
|
383
|
+
const key = stripTrailingColon(trimmed);
|
|
384
|
+
if (key !== null && DEP_SECTIONS.has(key)) {
|
|
385
|
+
inDepSection = true;
|
|
386
|
+
depSectionIndent = indent;
|
|
387
|
+
}
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (depSectionIndent !== null && indent <= depSectionIndent) {
|
|
391
|
+
inDepSection = false;
|
|
392
|
+
depSectionIndent = null;
|
|
393
|
+
leafIndent = null;
|
|
394
|
+
const key = stripTrailingColon(trimmed);
|
|
395
|
+
if (key !== null && DEP_SECTIONS.has(key)) {
|
|
396
|
+
inDepSection = true;
|
|
397
|
+
depSectionIndent = indent;
|
|
398
|
+
}
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (leafIndent === null) {
|
|
402
|
+
leafIndent = indent;
|
|
403
|
+
}
|
|
404
|
+
if (indent !== leafIndent) {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
const name = extractLeafName(trimmed);
|
|
408
|
+
if (name !== null) {
|
|
409
|
+
out.add(name);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function collectFromTopLevelSection(lines, section, out) {
|
|
414
|
+
let inSection = false;
|
|
415
|
+
let leafIndent = null;
|
|
416
|
+
for (const line of lines) {
|
|
417
|
+
if (isBlankOrComment(line)) {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
const indent = indentOf(line);
|
|
421
|
+
const trimmed = line.trim();
|
|
422
|
+
if (!inSection) {
|
|
423
|
+
if (indent === 0 && trimmed === `${section}:`) {
|
|
424
|
+
inSection = true;
|
|
425
|
+
}
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (indent === 0) {
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
if (leafIndent === null) {
|
|
432
|
+
leafIndent = indent;
|
|
433
|
+
}
|
|
434
|
+
if (indent !== leafIndent) {
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
const name = extractLeafName(trimmed);
|
|
438
|
+
if (name !== null) {
|
|
439
|
+
out.add(name);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function stripTrailingColon(s) {
|
|
444
|
+
if (!s.endsWith(":")) {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
return s.slice(0, -1).trim();
|
|
448
|
+
}
|
|
449
|
+
function extractLeafName(trimmed) {
|
|
450
|
+
const colonIdx = trimmed.indexOf(":");
|
|
451
|
+
if (colonIdx < 0) {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
let name = trimmed.slice(0, colonIdx).trim();
|
|
455
|
+
if (name.length === 0) {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
if (name.startsWith("'") && name.endsWith("'") || name.startsWith('"') && name.endsWith('"')) {
|
|
459
|
+
name = name.slice(1, -1);
|
|
460
|
+
}
|
|
461
|
+
return name.length > 0 ? name : null;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// src/parsers/yarn.ts
|
|
465
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
466
|
+
import path3 from "path";
|
|
467
|
+
async function parseYarnLockfile(lockfilePath) {
|
|
468
|
+
let raw;
|
|
469
|
+
try {
|
|
470
|
+
raw = await readFile4(lockfilePath, "utf8");
|
|
471
|
+
} catch (cause) {
|
|
472
|
+
throw new PatchstackError(
|
|
473
|
+
`Could not read lockfile at ${lockfilePath}`,
|
|
474
|
+
"LOCKFILE_NOT_FOUND",
|
|
475
|
+
cause
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
const blocks = parseBlocks(raw);
|
|
479
|
+
if (blocks.length === 0) {
|
|
480
|
+
throw new PatchstackError(
|
|
481
|
+
`Lockfile at ${lockfilePath} contains no package entries`,
|
|
482
|
+
"LOCKFILE_PARSE_ERROR"
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
const directNames = await readDirectDepNames(path3.dirname(lockfilePath));
|
|
486
|
+
const entries = [];
|
|
487
|
+
const seen = /* @__PURE__ */ new Set();
|
|
488
|
+
for (const block of blocks) {
|
|
489
|
+
if (block.version.length === 0 || block.names.size === 0) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
for (const name of block.names) {
|
|
493
|
+
const dedupKey = `${name}@${block.version}`;
|
|
494
|
+
if (seen.has(dedupKey)) {
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
seen.add(dedupKey);
|
|
498
|
+
entries.push({
|
|
499
|
+
name,
|
|
500
|
+
version: block.version,
|
|
501
|
+
direct: directNames.has(name)
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return entries;
|
|
506
|
+
}
|
|
507
|
+
function parseBlocks(raw) {
|
|
508
|
+
const lines = raw.split(/\r?\n/);
|
|
509
|
+
const blocks = [];
|
|
510
|
+
let current = null;
|
|
511
|
+
const finalize = () => {
|
|
512
|
+
if (current !== null && current.version.length > 0 && current.names.size > 0) {
|
|
513
|
+
blocks.push(current);
|
|
514
|
+
}
|
|
515
|
+
current = null;
|
|
516
|
+
};
|
|
517
|
+
for (const line of lines) {
|
|
518
|
+
const trimmed = line.trim();
|
|
519
|
+
if (trimmed.length === 0 || trimmed.startsWith("#")) {
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
const indent = countLeadingSpaces(line);
|
|
523
|
+
if (indent === 0) {
|
|
524
|
+
finalize();
|
|
525
|
+
if (!trimmed.endsWith(":")) {
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
const keyLine = trimmed.slice(0, -1);
|
|
529
|
+
const names = /* @__PURE__ */ new Set();
|
|
530
|
+
for (const spec of splitDescriptors(keyLine)) {
|
|
531
|
+
const name = extractName(spec);
|
|
532
|
+
if (name !== null) {
|
|
533
|
+
names.add(name);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
current = { names, version: "" };
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
if (current === null) {
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
const version = parseVersionField(trimmed);
|
|
543
|
+
if (version !== null) {
|
|
544
|
+
current.version = version;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
finalize();
|
|
548
|
+
return blocks;
|
|
549
|
+
}
|
|
550
|
+
function countLeadingSpaces(line) {
|
|
551
|
+
let i = 0;
|
|
552
|
+
while (i < line.length && line[i] === " ") {
|
|
553
|
+
i++;
|
|
554
|
+
}
|
|
555
|
+
return i;
|
|
556
|
+
}
|
|
557
|
+
function splitDescriptors(keyLine) {
|
|
558
|
+
const parts = [];
|
|
559
|
+
let current = "";
|
|
560
|
+
let quote = null;
|
|
561
|
+
for (let i = 0; i < keyLine.length; i++) {
|
|
562
|
+
const c = keyLine[i];
|
|
563
|
+
if (quote !== null) {
|
|
564
|
+
current += c;
|
|
565
|
+
if (c === quote) {
|
|
566
|
+
quote = null;
|
|
567
|
+
}
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
if (c === '"' || c === "'") {
|
|
571
|
+
quote = c;
|
|
572
|
+
current += c;
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
if (c === ",") {
|
|
576
|
+
const piece = current.trim();
|
|
577
|
+
if (piece.length > 0) {
|
|
578
|
+
parts.push(piece);
|
|
579
|
+
}
|
|
580
|
+
current = "";
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
current += c;
|
|
584
|
+
}
|
|
585
|
+
const tail = current.trim();
|
|
586
|
+
if (tail.length > 0) {
|
|
587
|
+
parts.push(tail);
|
|
588
|
+
}
|
|
589
|
+
return parts;
|
|
590
|
+
}
|
|
591
|
+
function extractName(rawSpec) {
|
|
592
|
+
let s = rawSpec.trim();
|
|
593
|
+
if (s.length === 0) {
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
if (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'")) {
|
|
597
|
+
s = s.slice(1, -1);
|
|
598
|
+
}
|
|
599
|
+
const atIdx = s.lastIndexOf("@");
|
|
600
|
+
if (atIdx <= 0) {
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
const name = s.slice(0, atIdx);
|
|
604
|
+
return name.length > 0 ? name : null;
|
|
605
|
+
}
|
|
606
|
+
function parseVersionField(content) {
|
|
607
|
+
if (!content.startsWith("version")) {
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
const after = content.slice("version".length);
|
|
611
|
+
const firstChar = after.charAt(0);
|
|
612
|
+
if (firstChar !== " " && firstChar !== " " && firstChar !== ":") {
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
let rest = firstChar === ":" ? after.slice(1) : after;
|
|
616
|
+
rest = rest.trim();
|
|
617
|
+
if (rest.length === 0) {
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
if (rest.startsWith('"') && rest.endsWith('"') || rest.startsWith("'") && rest.endsWith("'")) {
|
|
621
|
+
rest = rest.slice(1, -1);
|
|
622
|
+
}
|
|
623
|
+
return rest.length > 0 ? rest : null;
|
|
624
|
+
}
|
|
625
|
+
async function readDirectDepNames(cwd) {
|
|
626
|
+
const names = /* @__PURE__ */ new Set();
|
|
627
|
+
let raw;
|
|
628
|
+
try {
|
|
629
|
+
raw = await readFile4(path3.join(cwd, "package.json"), "utf8");
|
|
630
|
+
} catch {
|
|
631
|
+
return names;
|
|
632
|
+
}
|
|
633
|
+
let parsed;
|
|
634
|
+
try {
|
|
635
|
+
parsed = JSON.parse(raw);
|
|
636
|
+
} catch {
|
|
637
|
+
return names;
|
|
638
|
+
}
|
|
639
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
640
|
+
return names;
|
|
641
|
+
}
|
|
642
|
+
const obj = parsed;
|
|
643
|
+
for (const field of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) {
|
|
644
|
+
const section = obj[field];
|
|
645
|
+
if (typeof section !== "object" || section === null) {
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
for (const name of Object.keys(section)) {
|
|
649
|
+
names.add(name);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return names;
|
|
653
|
+
}
|
|
654
|
+
|
|
212
655
|
// src/parsers/index.ts
|
|
213
656
|
async function detectLockfile(cwd) {
|
|
214
|
-
const npmLock =
|
|
657
|
+
const npmLock = path4.join(cwd, "package-lock.json");
|
|
215
658
|
if (await exists(npmLock)) {
|
|
216
659
|
return {
|
|
217
660
|
ecosystem: "npm",
|
|
@@ -220,7 +663,7 @@ async function detectLockfile(cwd) {
|
|
|
220
663
|
strategy: "npm-lockfile"
|
|
221
664
|
};
|
|
222
665
|
}
|
|
223
|
-
const bunLock =
|
|
666
|
+
const bunLock = path4.join(cwd, "bun.lock");
|
|
224
667
|
if (await exists(bunLock)) {
|
|
225
668
|
return {
|
|
226
669
|
ecosystem: "npm",
|
|
@@ -229,7 +672,7 @@ async function detectLockfile(cwd) {
|
|
|
229
672
|
strategy: "node-modules-walk"
|
|
230
673
|
};
|
|
231
674
|
}
|
|
232
|
-
const bunLockB =
|
|
675
|
+
const bunLockB = path4.join(cwd, "bun.lockb");
|
|
233
676
|
if (await exists(bunLockB)) {
|
|
234
677
|
return {
|
|
235
678
|
ecosystem: "npm",
|
|
@@ -238,19 +681,23 @@ async function detectLockfile(cwd) {
|
|
|
238
681
|
strategy: "node-modules-walk"
|
|
239
682
|
};
|
|
240
683
|
}
|
|
241
|
-
const
|
|
242
|
-
if (await exists(yarnLock)) {
|
|
243
|
-
throw new PatchstackError(
|
|
244
|
-
"yarn.lock detected but not yet supported. Run `npm install` to generate a package-lock.json, or open an issue at github.com/patchstack/connect.",
|
|
245
|
-
"LOCKFILE_UNSUPPORTED"
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
const pnpmLock = path3.join(cwd, "pnpm-lock.yaml");
|
|
684
|
+
const pnpmLock = path4.join(cwd, "pnpm-lock.yaml");
|
|
249
685
|
if (await exists(pnpmLock)) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
686
|
+
return {
|
|
687
|
+
ecosystem: "npm",
|
|
688
|
+
filePath: pnpmLock,
|
|
689
|
+
filename: "pnpm-lock.yaml",
|
|
690
|
+
strategy: "pnpm-lockfile"
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
const yarnLock = path4.join(cwd, "yarn.lock");
|
|
694
|
+
if (await exists(yarnLock)) {
|
|
695
|
+
return {
|
|
696
|
+
ecosystem: "npm",
|
|
697
|
+
filePath: yarnLock,
|
|
698
|
+
filename: "yarn.lock",
|
|
699
|
+
strategy: "yarn-lockfile"
|
|
700
|
+
};
|
|
254
701
|
}
|
|
255
702
|
throw new PatchstackError(
|
|
256
703
|
`No lockfile found in ${cwd}. Expected one of: package-lock.json, bun.lock, bun.lockb, yarn.lock, pnpm-lock.yaml.`,
|
|
@@ -266,6 +713,10 @@ async function runStrategy(detected, cwd) {
|
|
|
266
713
|
switch (detected.strategy) {
|
|
267
714
|
case "npm-lockfile":
|
|
268
715
|
return parseNpmLockfile(detected.filePath);
|
|
716
|
+
case "pnpm-lockfile":
|
|
717
|
+
return parsePnpmLockfile(detected.filePath);
|
|
718
|
+
case "yarn-lockfile":
|
|
719
|
+
return parseYarnLockfile(detected.filePath);
|
|
269
720
|
case "node-modules-walk":
|
|
270
721
|
return walkNodeModules(cwd);
|
|
271
722
|
}
|
|
@@ -381,13 +832,99 @@ function compareSegments(a, b) {
|
|
|
381
832
|
return 0;
|
|
382
833
|
}
|
|
383
834
|
|
|
835
|
+
// src/buildFlag.ts
|
|
836
|
+
import { promises as fs } from "fs";
|
|
837
|
+
import path5 from "path";
|
|
838
|
+
var FLAG_MARKER = "__PATCHSTACK_PROD__";
|
|
839
|
+
var SNIPPET = "<script>window.__PATCHSTACK_PROD__=true;/*patchstack:production*/</script>";
|
|
840
|
+
var DEFAULT_DIRS = ["dist", "build", "out", ".output/public"];
|
|
841
|
+
async function pathExists(target) {
|
|
842
|
+
try {
|
|
843
|
+
await fs.access(target);
|
|
844
|
+
return true;
|
|
845
|
+
} catch {
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
async function resolveBuildDir(cwd, override) {
|
|
850
|
+
if (override !== void 0 && override.length > 0) {
|
|
851
|
+
const dir = path5.resolve(cwd, override);
|
|
852
|
+
return await pathExists(dir) ? dir : null;
|
|
853
|
+
}
|
|
854
|
+
for (const candidate of DEFAULT_DIRS) {
|
|
855
|
+
const dir = path5.resolve(cwd, candidate);
|
|
856
|
+
if (await pathExists(dir)) {
|
|
857
|
+
return dir;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
async function findHtmlFiles(dir, depth = 3) {
|
|
863
|
+
const found = [];
|
|
864
|
+
let entries;
|
|
865
|
+
try {
|
|
866
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
867
|
+
} catch {
|
|
868
|
+
return found;
|
|
869
|
+
}
|
|
870
|
+
for (const entry of entries) {
|
|
871
|
+
const full = path5.join(dir, entry.name);
|
|
872
|
+
if (entry.isDirectory()) {
|
|
873
|
+
if (depth > 0) {
|
|
874
|
+
found.push(...await findHtmlFiles(full, depth - 1));
|
|
875
|
+
}
|
|
876
|
+
} else if (entry.isFile() && entry.name.toLowerCase().endsWith(".html")) {
|
|
877
|
+
found.push(full);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return found;
|
|
881
|
+
}
|
|
882
|
+
function injectFlag(html) {
|
|
883
|
+
if (html.includes(FLAG_MARKER)) {
|
|
884
|
+
return null;
|
|
885
|
+
}
|
|
886
|
+
const headOpen = /<head[^>]*>/i;
|
|
887
|
+
if (headOpen.test(html)) {
|
|
888
|
+
return html.replace(headOpen, (match) => `${match}${SNIPPET}`);
|
|
889
|
+
}
|
|
890
|
+
const bodyOpen = /<body[^>]*>/i;
|
|
891
|
+
if (bodyOpen.test(html)) {
|
|
892
|
+
return html.replace(bodyOpen, (match) => `${match}${SNIPPET}`);
|
|
893
|
+
}
|
|
894
|
+
return `${SNIPPET}${html}`;
|
|
895
|
+
}
|
|
896
|
+
async function markProductionBuild(cwd, override) {
|
|
897
|
+
const dir = await resolveBuildDir(cwd, override);
|
|
898
|
+
if (dir === null) {
|
|
899
|
+
return { dir: null, patched: [], skipped: [] };
|
|
900
|
+
}
|
|
901
|
+
const files = await findHtmlFiles(dir);
|
|
902
|
+
const patched = [];
|
|
903
|
+
const skipped = [];
|
|
904
|
+
for (const file of files) {
|
|
905
|
+
const html = await fs.readFile(file, "utf8");
|
|
906
|
+
const next = injectFlag(html);
|
|
907
|
+
if (next === null) {
|
|
908
|
+
skipped.push(file);
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
await fs.writeFile(file, next, "utf8");
|
|
912
|
+
patched.push(file);
|
|
913
|
+
}
|
|
914
|
+
return { dir, patched, skipped };
|
|
915
|
+
}
|
|
916
|
+
|
|
384
917
|
// src/client.ts
|
|
385
|
-
var DEFAULT_ENDPOINT = "
|
|
918
|
+
var DEFAULT_ENDPOINT = "https://api.patchstack.com/monitor/pulse/manifest";
|
|
386
919
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
387
920
|
function buildEndpointUrl(base, siteUuid) {
|
|
388
921
|
const trimmed = base.replace(/\/$/, "");
|
|
389
922
|
return siteUuid !== void 0 && siteUuid !== null && siteUuid.length > 0 ? `${trimmed}/${encodeURIComponent(siteUuid)}` : trimmed;
|
|
390
923
|
}
|
|
924
|
+
function buildClaimUrl(endpoint, siteUuid) {
|
|
925
|
+
const origin = new URL(endpoint).origin;
|
|
926
|
+
return `${origin}/monitor/claim?site=${encodeURIComponent(siteUuid)}`;
|
|
927
|
+
}
|
|
391
928
|
async function postManifest(config, payload) {
|
|
392
929
|
const url = buildEndpointUrl(config.endpoint, config.siteUuid);
|
|
393
930
|
const timeoutMs = config.timeoutMs;
|
|
@@ -455,8 +992,8 @@ function isTimeoutError(cause) {
|
|
|
455
992
|
}
|
|
456
993
|
|
|
457
994
|
// src/config.ts
|
|
458
|
-
import { readFile as
|
|
459
|
-
import
|
|
995
|
+
import { readFile as readFile5, writeFile } from "fs/promises";
|
|
996
|
+
import path6 from "path";
|
|
460
997
|
var CONFIG_FILENAME = ".patchstackrc.json";
|
|
461
998
|
async function resolveConfig(options) {
|
|
462
999
|
const fromFile = await readConfigFile(options.cwd);
|
|
@@ -483,7 +1020,7 @@ async function resolveConfig(options) {
|
|
|
483
1020
|
};
|
|
484
1021
|
}
|
|
485
1022
|
async function writeConfigFile(cwd, config) {
|
|
486
|
-
const target =
|
|
1023
|
+
const target = path6.join(cwd, CONFIG_FILENAME);
|
|
487
1024
|
const content = JSON.stringify(config, null, 2) + "\n";
|
|
488
1025
|
await writeFile(target, content, "utf8");
|
|
489
1026
|
return target;
|
|
@@ -493,10 +1030,10 @@ async function persistSiteUuid(cwd, siteUuid) {
|
|
|
493
1030
|
return writeConfigFile(cwd, { ...existing, siteUuid });
|
|
494
1031
|
}
|
|
495
1032
|
async function readConfigFile(cwd) {
|
|
496
|
-
const target =
|
|
1033
|
+
const target = path6.join(cwd, CONFIG_FILENAME);
|
|
497
1034
|
let raw;
|
|
498
1035
|
try {
|
|
499
|
-
raw = await
|
|
1036
|
+
raw = await readFile5(target, "utf8");
|
|
500
1037
|
} catch (err) {
|
|
501
1038
|
if (err.code === "ENOENT") {
|
|
502
1039
|
return {};
|
|
@@ -550,6 +1087,10 @@ Usage:
|
|
|
550
1087
|
patchstack-connect init <site-uuid> Optional: pre-seed .patchstackrc.json
|
|
551
1088
|
with an existing site UUID
|
|
552
1089
|
patchstack-connect status [options] Show current configuration
|
|
1090
|
+
patchstack-connect mark-build [--dir <path>] Inject the production flag into
|
|
1091
|
+
the built HTML (run as a postbuild
|
|
1092
|
+
step). Tells the widget it's live so
|
|
1093
|
+
it hides the claim flow.
|
|
553
1094
|
patchstack-connect help Print this message
|
|
554
1095
|
|
|
555
1096
|
Options (for scan and status):
|
|
@@ -557,9 +1098,13 @@ Options (for scan and status):
|
|
|
557
1098
|
--endpoint <url> Override the API endpoint
|
|
558
1099
|
--dry-run (scan only) Show the payload without posting
|
|
559
1100
|
|
|
1101
|
+
Options (for mark-build):
|
|
1102
|
+
--dir <path> Build output dir (default: auto-detect dist/, build/,
|
|
1103
|
+
out/, .output/public)
|
|
1104
|
+
|
|
560
1105
|
Environment:
|
|
561
1106
|
PATCHSTACK_SITE_UUID Site UUID
|
|
562
|
-
PATCHSTACK_ENDPOINT API endpoint (default:
|
|
1107
|
+
PATCHSTACK_ENDPOINT API endpoint (default: https://api.patchstack.com/monitor/pulse/manifest)
|
|
563
1108
|
PATCHSTACK_TIMEOUT_MS Request timeout in ms (default: 30000)
|
|
564
1109
|
|
|
565
1110
|
Precedence: CLI flag > environment variable > .patchstackrc.json.
|
|
@@ -570,7 +1115,7 @@ Examples:
|
|
|
570
1115
|
npx @patchstack/connect init 550e8400-e29b-41d4-a716-446655440000
|
|
571
1116
|
npx @patchstack/connect scan --site-uuid 550e8400-...-446655440000
|
|
572
1117
|
`;
|
|
573
|
-
var VALUE_FLAGS = /* @__PURE__ */ new Set(["site-uuid", "endpoint"]);
|
|
1118
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set(["site-uuid", "endpoint", "dir"]);
|
|
574
1119
|
function parseArgs(argv) {
|
|
575
1120
|
const args = argv.slice(2);
|
|
576
1121
|
const positional = [];
|
|
@@ -671,6 +1216,11 @@ async function runScan(args) {
|
|
|
671
1216
|
} else {
|
|
672
1217
|
console.log(`Server response: ${response.message ?? JSON.stringify(response)}`);
|
|
673
1218
|
}
|
|
1219
|
+
if (provisioning && response.uuid !== void 0 && response.uuid.length > 0) {
|
|
1220
|
+
console.log("");
|
|
1221
|
+
console.log("Claim this site to view vulnerability reports in your Patchstack dashboard:");
|
|
1222
|
+
console.log(` ${buildClaimUrl(config.endpoint, response.uuid)}`);
|
|
1223
|
+
}
|
|
674
1224
|
return 0;
|
|
675
1225
|
}
|
|
676
1226
|
async function runStatus(args) {
|
|
@@ -682,6 +1232,27 @@ async function runStatus(args) {
|
|
|
682
1232
|
console.log(`Site UUID: ${config.siteUuid ?? "(none yet \u2014 the next `scan` will provision one)"}`);
|
|
683
1233
|
console.log(`Endpoint: ${config.endpoint}`);
|
|
684
1234
|
console.log(`Timeout: ${config.timeoutMs}ms`);
|
|
1235
|
+
if (config.siteUuid !== null) {
|
|
1236
|
+
console.log(`Claim URL: ${buildClaimUrl(config.endpoint, config.siteUuid)}`);
|
|
1237
|
+
}
|
|
1238
|
+
return 0;
|
|
1239
|
+
}
|
|
1240
|
+
async function runMarkBuild(args) {
|
|
1241
|
+
const result = await markProductionBuild(process.cwd(), getStringFlag(args.flags, "dir"));
|
|
1242
|
+
if (result.dir === null) {
|
|
1243
|
+
console.warn(
|
|
1244
|
+
"patchstack: no build output found (looked for dist/, build/, out/, .output/public). Pass --dir <path> if your build outputs elsewhere. Skipping production flag."
|
|
1245
|
+
);
|
|
1246
|
+
return 0;
|
|
1247
|
+
}
|
|
1248
|
+
if (result.patched.length === 0 && result.skipped.length === 0) {
|
|
1249
|
+
console.warn(`patchstack: no HTML files found under ${result.dir}; skipping production flag.`);
|
|
1250
|
+
return 0;
|
|
1251
|
+
}
|
|
1252
|
+
const already = result.skipped.length > 0 ? ` (${result.skipped.length} already marked)` : "";
|
|
1253
|
+
console.log(
|
|
1254
|
+
`patchstack: marked ${result.patched.length} HTML file(s) as a production build in ${result.dir}${already}.`
|
|
1255
|
+
);
|
|
685
1256
|
return 0;
|
|
686
1257
|
}
|
|
687
1258
|
async function main() {
|
|
@@ -697,6 +1268,8 @@ async function main() {
|
|
|
697
1268
|
return runScan(args);
|
|
698
1269
|
case "status":
|
|
699
1270
|
return runStatus(args);
|
|
1271
|
+
case "mark-build":
|
|
1272
|
+
return runMarkBuild(args);
|
|
700
1273
|
default:
|
|
701
1274
|
console.error(`Unknown command: ${args.command}
|
|
702
1275
|
`);
|