@patchstack/connect 0.2.3 → 0.2.5

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 CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Connect a JavaScript / Node.js application to [Patchstack](https://patchstack.com) for continuous vulnerability monitoring. Scans your `package-lock.json` and reports installed packages so Patchstack can match them against its vulnerability database and notify you when something needs patching.
4
4
 
5
+ For how this repo fits with the wider Patchstack ecosystem (`saas`, `hub`, `patchstack-website`, `patchstack-connect`), see [`patchstack/saas` → `docs/ecosystem.md`](https://github.com/patchstack/saas/blob/main/docs/ecosystem.md).
6
+
5
7
  ## Install prompt (for AI coding tools)
6
8
 
7
9
  Copy and paste this prompt into any AI coding assistant (Cursor, v0, Bolt, Lovable, Claude Code, etc.):
@@ -21,6 +23,7 @@ That's it. The first `scan`:
21
23
  2. POSTs the package list to Patchstack with **no** UUID.
22
24
  3. Patchstack provisions a fresh site and returns its UUID.
23
25
  4. The connector writes the UUID to `.patchstackrc.json` so the next `scan` targets the same site.
26
+ 5. The connector prints a claim URL — open it in a browser to attach the new site to your Patchstack account. You can re-display it any time with `npx @patchstack/connect status`.
24
27
 
25
28
  Then wire it into builds:
26
29
 
@@ -71,7 +74,7 @@ Precedence (highest wins):
71
74
  Environment variables:
72
75
 
73
76
  - `PATCHSTACK_SITE_UUID` — the site UUID from your Patchstack dashboard
74
- - `PATCHSTACK_ENDPOINT` — override the API endpoint (default `https://app.patchstack.com/monitor/pulse/manifest`)
77
+ - `PATCHSTACK_ENDPOINT` — override the API endpoint (default `https://api.patchstack.com/monitor/pulse/manifest`)
75
78
  - `PATCHSTACK_TIMEOUT_MS` — request timeout in milliseconds (default `30000`)
76
79
 
77
80
  `.patchstackrc.json` example:
@@ -113,10 +116,10 @@ That's the entire payload. No source code, no environment variables, no file pat
113
116
  ## Supported lockfiles
114
117
 
115
118
  - ✅ `package-lock.json` (npm v6 / v2 / v3) — parsed directly
119
+ - ✅ `pnpm-lock.yaml` (pnpm v5 / v6 / v7 / v8 / v9) — parsed directly
120
+ - ✅ `yarn.lock` (yarn classic v1 and yarn berry v2+) — parsed directly
116
121
  - ✅ `bun.lockb` (binary) — package list resolved by walking `node_modules/`
117
122
  - ✅ `bun.lock` (text) — same fallback; direct parsing coming
118
- - ❌ `yarn.lock` — coming soon
119
- - ❌ `pnpm-lock.yaml` — coming soon
120
123
 
121
124
  If both a Bun lockfile and `node_modules/` are present, the connector walks `node_modules/` to enumerate the installed packages. Run `bun install` (or `npm install`) before scanning so the directory is populated.
122
125
 
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 path3 from "path";
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 = path3.join(cwd, "package-lock.json");
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 = path3.join(cwd, "bun.lock");
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 = path3.join(cwd, "bun.lockb");
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 yarnLock = path3.join(cwd, "yarn.lock");
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
- throw new PatchstackError(
251
- "pnpm-lock.yaml detected but not yet supported. Open an issue at github.com/patchstack/connect to request support.",
252
- "LOCKFILE_UNSUPPORTED"
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
  }
@@ -382,12 +833,16 @@ function compareSegments(a, b) {
382
833
  }
383
834
 
384
835
  // src/client.ts
385
- var DEFAULT_ENDPOINT = "https://app.patchstack.com/monitor/pulse/manifest";
836
+ var DEFAULT_ENDPOINT = "https://api.patchstack.com/monitor/pulse/manifest";
386
837
  var DEFAULT_TIMEOUT_MS = 3e4;
387
838
  function buildEndpointUrl(base, siteUuid) {
388
839
  const trimmed = base.replace(/\/$/, "");
389
840
  return siteUuid !== void 0 && siteUuid !== null && siteUuid.length > 0 ? `${trimmed}/${encodeURIComponent(siteUuid)}` : trimmed;
390
841
  }
842
+ function buildClaimUrl(endpoint, siteUuid) {
843
+ const origin = new URL(endpoint).origin;
844
+ return `${origin}/monitor/claim?site=${encodeURIComponent(siteUuid)}`;
845
+ }
391
846
  async function postManifest(config, payload) {
392
847
  const url = buildEndpointUrl(config.endpoint, config.siteUuid);
393
848
  const timeoutMs = config.timeoutMs;
@@ -455,8 +910,8 @@ function isTimeoutError(cause) {
455
910
  }
456
911
 
457
912
  // src/config.ts
458
- import { readFile as readFile3, writeFile } from "fs/promises";
459
- import path4 from "path";
913
+ import { readFile as readFile5, writeFile } from "fs/promises";
914
+ import path5 from "path";
460
915
  var CONFIG_FILENAME = ".patchstackrc.json";
461
916
  async function resolveConfig(options) {
462
917
  const fromFile = await readConfigFile(options.cwd);
@@ -483,7 +938,7 @@ async function resolveConfig(options) {
483
938
  };
484
939
  }
485
940
  async function writeConfigFile(cwd, config) {
486
- const target = path4.join(cwd, CONFIG_FILENAME);
941
+ const target = path5.join(cwd, CONFIG_FILENAME);
487
942
  const content = JSON.stringify(config, null, 2) + "\n";
488
943
  await writeFile(target, content, "utf8");
489
944
  return target;
@@ -493,10 +948,10 @@ async function persistSiteUuid(cwd, siteUuid) {
493
948
  return writeConfigFile(cwd, { ...existing, siteUuid });
494
949
  }
495
950
  async function readConfigFile(cwd) {
496
- const target = path4.join(cwd, CONFIG_FILENAME);
951
+ const target = path5.join(cwd, CONFIG_FILENAME);
497
952
  let raw;
498
953
  try {
499
- raw = await readFile3(target, "utf8");
954
+ raw = await readFile5(target, "utf8");
500
955
  } catch (err) {
501
956
  if (err.code === "ENOENT") {
502
957
  return {};
@@ -559,7 +1014,7 @@ Options (for scan and status):
559
1014
 
560
1015
  Environment:
561
1016
  PATCHSTACK_SITE_UUID Site UUID
562
- PATCHSTACK_ENDPOINT API endpoint (default: https://app.patchstack.com/monitor/pulse/manifest)
1017
+ PATCHSTACK_ENDPOINT API endpoint (default: https://api.patchstack.com/monitor/pulse/manifest)
563
1018
  PATCHSTACK_TIMEOUT_MS Request timeout in ms (default: 30000)
564
1019
 
565
1020
  Precedence: CLI flag > environment variable > .patchstackrc.json.
@@ -671,6 +1126,11 @@ async function runScan(args) {
671
1126
  } else {
672
1127
  console.log(`Server response: ${response.message ?? JSON.stringify(response)}`);
673
1128
  }
1129
+ if (provisioning && response.uuid !== void 0 && response.uuid.length > 0) {
1130
+ console.log("");
1131
+ console.log("Claim this site to view vulnerability reports in your Patchstack dashboard:");
1132
+ console.log(` ${buildClaimUrl(config.endpoint, response.uuid)}`);
1133
+ }
674
1134
  return 0;
675
1135
  }
676
1136
  async function runStatus(args) {
@@ -682,6 +1142,9 @@ async function runStatus(args) {
682
1142
  console.log(`Site UUID: ${config.siteUuid ?? "(none yet \u2014 the next `scan` will provision one)"}`);
683
1143
  console.log(`Endpoint: ${config.endpoint}`);
684
1144
  console.log(`Timeout: ${config.timeoutMs}ms`);
1145
+ if (config.siteUuid !== null) {
1146
+ console.log(`Claim URL: ${buildClaimUrl(config.endpoint, config.siteUuid)}`);
1147
+ }
685
1148
  return 0;
686
1149
  }
687
1150
  async function main() {