@patchstack/connect 0.5.12 → 0.5.15

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
@@ -221,7 +221,7 @@ function unreadableWarning(filename, report) {
221
221
  if (report.unreadable === 0) return null;
222
222
  const shown = report.samples.join(", ");
223
223
  const one = report.unreadable === 1;
224
- return `${filename}: ${report.unreadable} entr${one ? "y was" : "ies were"} not in a form this scanner reads (${shown}${report.unreadable > report.samples.length ? ", \u2026" : ""}), so ${one ? "it is" : "they are"} absent from this manifest. Anything installed only through ${one ? "that entry" : "those entries"} is not being checked.`;
224
+ return `${filename}: ${report.unreadable} entr${one ? "y" : "ies"} could not be read (${shown}${report.unreadable > report.samples.length ? ", \u2026" : ""}), so ${one ? "that package is" : "those packages are"} not being checked.`;
225
225
  }
226
226
 
227
227
  // src/parsers/bun.ts
@@ -1123,7 +1123,7 @@ async function scanLockfile(cwd) {
1123
1123
  warnings.push(staleWarning("node_modules/", missing));
1124
1124
  }
1125
1125
  warnings.push(
1126
- walkWasCandidate ? "Reporting node_modules/, which is what the build loads." : "Scanned node_modules/ instead. Delete the stale lockfile to silence this warning."
1126
+ walkWasCandidate ? "Checked the installed packages in node_modules/, which is what the build loads." : "Checked the installed packages in node_modules/ instead. Delete the out-of-date package list file to stop this warning."
1127
1127
  );
1128
1128
  return manifestWith(walked, warnings);
1129
1129
  }
@@ -1135,7 +1135,7 @@ async function scanLockfile(cwd) {
1135
1135
  );
1136
1136
  }
1137
1137
  warnings.push(
1138
- `No fully-consistent source found; reporting ${firstParsed.filename}. The manifest may understate the real dependency set.`
1138
+ `No package list matches package.json; checked ${firstParsed.filename}, so some packages may be missing. Reinstall your packages to fix this.`
1139
1139
  );
1140
1140
  return manifestWith(firstParsed.packages, warnings);
1141
1141
  }
@@ -1149,12 +1149,19 @@ function otherCompleteSources(all, current, declared) {
1149
1149
  }
1150
1150
  function conflictWarning(filename, conflicts) {
1151
1151
  const shown = conflicts.slice(0, 3).map(({ other, conflict }) => `${conflict.name} ${conflict.version} vs ${conflict.otherVersion} in ${other}`).join("; ");
1152
- return `${filename} and another lockfile disagree about installed versions (${shown}${conflicts.length > 3 ? `, +${conflicts.length - 3} more` : ""}). Scanned node_modules/ instead. Remove whichever lockfile your package manager does not maintain.`;
1153
- }
1152
+ return `${filename} and another package list file give different versions (${shown}${conflicts.length > 3 ? `, +${conflicts.length - 3} more` : ""}). Checked node_modules/ instead. Delete the file your package manager does not use.`;
1153
+ }
1154
+ var INSTALL_BY_SOURCE = {
1155
+ "package-lock.json": "npm install",
1156
+ "bun.lock": "bun install",
1157
+ "bun.lockb": "bun install",
1158
+ "pnpm-lock.yaml": "pnpm install",
1159
+ "yarn.lock": "yarn install"
1160
+ };
1154
1161
  function staleWarning(source, missing) {
1155
1162
  const sample = missing.slice(0, 3).join(", ");
1156
1163
  const suffix = missing.length > 3 ? `, +${missing.length - 3} more` : "";
1157
- return `${source} looks stale: package.json declares ${missing.length} dependenc${missing.length === 1 ? "y" : "ies"} it does not contain (${sample}${suffix}).`;
1164
+ return `${source} is out of date: it is missing ${missing.length} package${missing.length === 1 ? "" : "s"} listed in package.json (${sample}${suffix}). Run ${INSTALL_BY_SOURCE[source] ?? "your install command"} to update it.`;
1158
1165
  }
1159
1166
  async function presentLockfiles(cwd) {
1160
1167
  const probed = await Promise.all(
@@ -1622,21 +1629,28 @@ var CLAIM_TOKEN_HEADER = "X-Patchstack-Claim-Token";
1622
1629
  function claimTokenHeader(config) {
1623
1630
  return config.endpointTrusted !== false && isHeaderValue(config.claimToken) ? { [CLAIM_TOKEN_HEADER]: config.claimToken } : {};
1624
1631
  }
1625
- function claimOutcomeLines(claim3, config) {
1626
- if (typeof config.claimToken !== "string" || config.claimToken === "") return [];
1632
+ function claimOutcome(claim3, config) {
1633
+ if (typeof config.claimToken !== "string" || config.claimToken === "") return null;
1627
1634
  if (claim3?.state === "claimed" || claim3?.state === "owned-by-you") {
1628
- const dashboardUrl = safeRemoteUrl(claim3.dashboard_url);
1629
- const dashboard = dashboardUrl === null ? [] : [`Dashboard: ${dashboardUrl}`];
1630
- return [
1631
- claim3.state === "claimed" ? "Connected to your Patchstack account." : "This site is already connected to your Patchstack account.",
1632
- ...dashboard
1633
- ];
1635
+ return {
1636
+ connected: true,
1637
+ summary: claim3.state === "claimed" ? "Connected to your Patchstack account" : "This project is already connected to your Patchstack account",
1638
+ dashboardUrl: safeRemoteUrl(claim3.dashboard_url),
1639
+ hint: []
1640
+ };
1634
1641
  }
1635
- const why = claim3 === void 0 ? "Patchstack did not act on the claim token" : claim3.state === "owned-by-other" ? "this site belongs to a different Patchstack account" : claim3.reason === "expired" ? "the claim token has expired" : "Patchstack did not recognise the claim token";
1636
- return [
1637
- `Not connected to your account: ${why}.`,
1638
- "Open the dashboard link below to connect it, or copy a fresh prompt from the dashboard."
1639
- ];
1642
+ const why = claimFailureReason(claim3);
1643
+ return {
1644
+ connected: false,
1645
+ summary: `Not connected to your account: ${why}`,
1646
+ dashboardUrl: null,
1647
+ hint: ["Open the dashboard link below to connect it, or copy a fresh prompt from the dashboard."]
1648
+ };
1649
+ }
1650
+ function claimFailureReason(claim3) {
1651
+ if (claim3 === void 0) return "Patchstack did not act on the claim token";
1652
+ if (claim3.state === "owned-by-other") return "this project belongs to a different Patchstack account";
1653
+ return claim3.reason === "expired" ? "the claim token has expired" : "Patchstack did not recognise the claim token";
1640
1654
  }
1641
1655
  function buildEndpointUrl(base, siteUuid) {
1642
1656
  const trimmed = base.replace(/\/$/, "");
@@ -1645,13 +1659,13 @@ function buildEndpointUrl(base, siteUuid) {
1645
1659
  function authFailureMessage(status, config) {
1646
1660
  const hasCredential = typeof config.pulseAuth === "string" && config.pulseAuth.length > 0;
1647
1661
  if (status === 401 && !hasCredential) {
1648
- return "Patchstack requires an API credential for this site and none is configured. Run `npx patchstack-connect login`, or set PATCHSTACK_API_KEY.";
1662
+ return "Patchstack needs this project's API key, and none is set here. Run npx @patchstack/connect login, or set PATCHSTACK_API_KEY.";
1649
1663
  }
1650
1664
  if (status === 401) {
1651
- return "Patchstack rejected this API credential. It may have expired, been revoked, or the site may no longer exist. Run `npx patchstack-connect login` to issue a new one.";
1665
+ return "Patchstack did not accept this project's API key: it may have expired or been revoked, or the project may no longer exist. Run npx @patchstack/connect login to get a new one.";
1652
1666
  }
1653
1667
  if (status === 403) {
1654
- return "This API credential is not permitted to act on this site. Check that siteUuid in .patchstackrc.json matches the credential (a credential is issued for one site).";
1668
+ return "This API key belongs to a different project. Check that siteUuid in .patchstackrc.json is the project the key was made for.";
1655
1669
  }
1656
1670
  return null;
1657
1671
  }
@@ -2064,7 +2078,7 @@ function renderDemoGuide(state) {
2064
2078
  `Patchstack demo guide \u2014 ${scenario.name}`,
2065
2079
  "",
2066
2080
  "Goal: prove that a live Patchstack virtual patch blocks the vulnerable payload while normal traffic still succeeds.",
2067
- "Deployment required: no. Keep the app running locally; the connector and generated guard contact Patchstack\u2019s production API.",
2081
+ "Deployment required: no. Keep the app running locally; Connect and the generated guard contact Patchstack\u2019s production API.",
2068
2082
  "",
2069
2083
  `1. ${siteReady ? "\u2713" : "\u25CB"} Connect this project from Bolt\u2019s Host account dropdown`,
2070
2084
  siteReady ? ` Ready \u2014 .patchstackrc.json contains site ${state.siteUuid}.` : " Click \u201CConnect Patchstack\u201D in Bolt. This creates .patchstackrc.json; no separate CLI login is needed.",
@@ -3825,7 +3839,7 @@ function classifyArchitecture(cwd) {
3825
3839
  return {
3826
3840
  requestPath: "none",
3827
3841
  evidence: statics.map((label) => `static build: ${label}`),
3828
- note: `This project builds a static site (${statics.join(", ")}) and nothing in it receives a request, so there is no request path for a runtime guard to attach to. Dependency monitoring and the disclosure widget still apply; runtime protection does not.`
3842
+ note: `This project builds a static site (${statics.join(", ")}) and nothing in it receives a request, so there is no request path for a runtime guard to attach to. Dependency monitoring and the Patchstack Connector still apply; runtime protection does not.`
3829
3843
  };
3830
3844
  }
3831
3845
  return {
@@ -3840,7 +3854,21 @@ import { readFileSync as readFileSync7, existsSync as existsSync3 } from "fs";
3840
3854
  import { join as join5, dirname as dirname2 } from "path";
3841
3855
  import { fileURLToPath } from "url";
3842
3856
  var read = (p) => readFileSync7(p, "utf8");
3843
- var log = (msg) => console.log(`patchstack protect: ${msg}`);
3857
+ var logSink = null;
3858
+ var log = (msg) => {
3859
+ if (logSink !== null) logSink(msg);
3860
+ else console.log(`patchstack protect: ${msg}`);
3861
+ };
3862
+ function collectProtectLog(fn) {
3863
+ const lines = [];
3864
+ const previous = logSink;
3865
+ logSink = (msg) => lines.push(...msg.split("\n"));
3866
+ try {
3867
+ return { result: fn(), lines };
3868
+ } finally {
3869
+ logSink = previous;
3870
+ }
3871
+ }
3844
3872
  function hasDependency(cwd, name) {
3845
3873
  try {
3846
3874
  const pkg = JSON.parse(read(join5(cwd, "package.json")));
@@ -5502,6 +5530,147 @@ function ensureSourceWidget(cwd, siteUuid, jsxShell = null) {
5502
5530
  return { shell, action };
5503
5531
  }
5504
5532
 
5533
+ // src/progress.ts
5534
+ var PROGRESS_STEPS = [
5535
+ { step: "installed", label: "Install the Patchstack connector" },
5536
+ { step: "connected", label: "Connect project to Patchstack account" },
5537
+ { step: "synced", label: "Sync and monitor your project" },
5538
+ { step: "deployed", label: "Deploy project to protect live app" }
5539
+ ];
5540
+ function stepLabel(step, environment) {
5541
+ if (step === "synced" && environment) return `Sync and monitor in ${environment} environment`;
5542
+ return PROGRESS_STEPS.find((entry) => entry.step === step).label;
5543
+ }
5544
+ function deployNote(context) {
5545
+ if (context.environment !== "production") return null;
5546
+ return context.environmentSource === "builder" ? "Reported as a publish. Patchstack ticks this once it sees the live site." : "Built for production. Patchstack ticks this once it sees the live site.";
5547
+ }
5548
+ var ANSI = {
5549
+ reset: "\x1B[0m",
5550
+ bold: "\x1B[1m",
5551
+ dim: "\x1B[2m",
5552
+ green: "\x1B[32m",
5553
+ yellow: "\x1B[33m",
5554
+ cyan: "\x1B[36m"
5555
+ };
5556
+ function nextProgressStep(progress) {
5557
+ return PROGRESS_STEPS.find(({ step }) => !progress[step])?.step ?? null;
5558
+ }
5559
+ function nextStepTitle(step, context) {
5560
+ switch (step) {
5561
+ case "installed":
5562
+ return "install the Patchstack connector";
5563
+ case "connected":
5564
+ return "connect this project to your Patchstack account";
5565
+ case "synced":
5566
+ return "sync this project with Patchstack";
5567
+ case "deployed":
5568
+ return context.environment === "production" ? "open your live app so Patchstack can see it" : "deploy your project to protect the live app";
5569
+ }
5570
+ }
5571
+ function nextStepLines(step, context) {
5572
+ switch (step) {
5573
+ case "installed":
5574
+ return [`Run: ${context.installCommand}`, "Then run: npx @patchstack/connect setup"];
5575
+ case "connected":
5576
+ if (context.siteUuid === null) {
5577
+ return [
5578
+ "Run: npx @patchstack/connect setup",
5579
+ "It adds this project to Patchstack and prints the link to connect it.",
5580
+ 'Cannot run commands here? See "When your tool will not run this CLI" in AGENT-INSTALL.md.'
5581
+ ];
5582
+ }
5583
+ return [
5584
+ context.claimUrl !== null ? `Open ${context.claimUrl}` : "Run: npx @patchstack/connect claim",
5585
+ // A production run is the deploy itself, so there is nothing further to point at.
5586
+ ...context.environment === "production" ? [] : ["Already connected? Then commit, set PATCHSTACK_API_KEY on your host, and deploy."]
5587
+ ];
5588
+ case "synced":
5589
+ return ["Run: npx @patchstack/connect scan"];
5590
+ case "deployed":
5591
+ if (context.environment === "production") {
5592
+ return ["Open the live site once so Patchstack can see it.", "Not published yet? Publish or deploy it now."];
5593
+ }
5594
+ return [
5595
+ "Commit your changes. Never commit .patchstackrc.local.json.",
5596
+ "Set PATCHSTACK_API_KEY (from .patchstackrc.local.json) on your hosting platform.",
5597
+ "Deploy or publish. The live site keeps its old version until you do."
5598
+ ];
5599
+ }
5600
+ }
5601
+ function renderProgress(progress, context, options) {
5602
+ const paint = (code, text) => options.useColor ? `${code}${text}${ANSI.reset}` : text;
5603
+ const lines = [];
5604
+ for (const { step } of PROGRESS_STEPS) {
5605
+ const label = stepLabel(step, context.environment);
5606
+ lines.push(progress[step] ? ` ${paint(ANSI.green, "\u2714")} ${label}` : ` ${paint(ANSI.yellow, "\u2718")} ${label}`);
5607
+ const note = step === "deployed" && !progress.deployed ? deployNote(context) : null;
5608
+ if (note !== null) lines.push(` ${paint(ANSI.dim, note)}`);
5609
+ }
5610
+ const next = nextProgressStep(progress);
5611
+ lines.push("");
5612
+ if (next === null) {
5613
+ lines.push(paint(ANSI.bold, "All done."));
5614
+ return lines;
5615
+ }
5616
+ lines.push(`${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, `Next: ${nextStepTitle(next, context)}`)}`);
5617
+ for (const line of nextStepLines(next, context)) {
5618
+ lines.push(` ${line}`);
5619
+ }
5620
+ return lines;
5621
+ }
5622
+
5623
+ // src/report.ts
5624
+ function emptyReport() {
5625
+ return { done: [], missing: [] };
5626
+ }
5627
+ var ANSI2 = {
5628
+ reset: "\x1B[0m",
5629
+ bold: "\x1B[1m",
5630
+ dim: "\x1B[2m",
5631
+ green: "\x1B[32m",
5632
+ yellow: "\x1B[33m"
5633
+ };
5634
+ function renderStatus(title, report, progress, context, options) {
5635
+ const paint = (code, text) => options.useColor ? `${code}${text}${ANSI2.reset}` : text;
5636
+ const lines = [paint(ANSI2.bold, title), ""];
5637
+ if (report.done.length > 0) {
5638
+ lines.push(paint(ANSI2.bold, "Done"));
5639
+ for (const item of report.done) lines.push(` ${paint(ANSI2.green, "\u2714")} ${item}`);
5640
+ lines.push("");
5641
+ }
5642
+ if (report.missing.length > 0) {
5643
+ lines.push(paint(ANSI2.bold, "Missing"));
5644
+ for (const item of report.missing) {
5645
+ lines.push(` ${paint(ANSI2.yellow, "\u2718")} ${item.text}`);
5646
+ for (const hint of item.hint ?? []) lines.push(` ${hint}`);
5647
+ if (options.verbose === true) {
5648
+ for (const detail2 of item.detail ?? []) lines.push(` ${paint(ANSI2.dim, detail2)}`);
5649
+ }
5650
+ }
5651
+ lines.push("");
5652
+ }
5653
+ if (options.withoutProgress === true) {
5654
+ if (lines[lines.length - 1] === "") lines.pop();
5655
+ return lines;
5656
+ }
5657
+ lines.push(...renderProgress(progress, context, { useColor: options.useColor }));
5658
+ return lines;
5659
+ }
5660
+ function renderHookSummary(report, progress, context) {
5661
+ const lowerFirst = (text) => text.charAt(0).toLowerCase() + text.slice(1);
5662
+ const lines = [];
5663
+ if (report.done.length > 0) {
5664
+ lines.push(`Patchstack: ${report.done.map(lowerFirst).join("; ")}.`);
5665
+ }
5666
+ const next = nextProgressStep(progress);
5667
+ if (next !== null) {
5668
+ const first = next === "deployed" && context.environment === "production" ? void 0 : nextStepLines(next, context)[0];
5669
+ lines.push(`Patchstack next step: ${nextStepTitle(next, context)}.${first !== void 0 ? ` ${first}` : ""}`);
5670
+ }
5671
+ return lines;
5672
+ }
5673
+
5505
5674
  // src/guide.ts
5506
5675
  var PROD_MARKER_NEEDLE = "__PATCHSTACK_PROD__";
5507
5676
  var WIDGET_NEEDLE2 = "patchstack-widget";
@@ -5698,8 +5867,12 @@ async function collectGuideState(cwd) {
5698
5867
  let claimUrl = null;
5699
5868
  let endpointOverride = null;
5700
5869
  let widgetOptOut = false;
5870
+ let environment = null;
5871
+ let environmentSource = null;
5701
5872
  try {
5702
5873
  const config = await resolveConfig({ cwd });
5874
+ environment = config.environment;
5875
+ environmentSource = config.environmentSource ?? null;
5703
5876
  siteUuid = config.siteUuid;
5704
5877
  if (siteUuid !== null && config.endpointTrusted !== false) {
5705
5878
  claimUrl = buildClaimUrl(config.endpoint, siteUuid);
@@ -5726,6 +5899,8 @@ async function collectGuideState(cwd) {
5726
5899
  siteUuid,
5727
5900
  claimUrl,
5728
5901
  endpointOverride,
5902
+ environment,
5903
+ environmentSource,
5729
5904
  hasBuildScript: Boolean(pkg?.scripts?.build?.trim()),
5730
5905
  installScanWired: (pkg?.scripts?.postinstall ?? "").includes("patchstack-connect scan"),
5731
5906
  // The scan has to run first: a later prebuild command may upload and stamp the map that the bundle
@@ -5744,7 +5919,7 @@ async function collectGuideState(cwd) {
5744
5919
  protectionApplicable: protection.applicable
5745
5920
  };
5746
5921
  }
5747
- var ANSI = {
5922
+ var ANSI3 = {
5748
5923
  reset: "\x1B[0m",
5749
5924
  bold: "\x1B[1m",
5750
5925
  dim: "\x1B[2m",
@@ -5758,9 +5933,6 @@ function needsSourceProductionMarker(state) {
5758
5933
  }
5759
5934
  return !state.widgetFileHint.toLowerCase().endsWith(".html");
5760
5935
  }
5761
- function widgetTagInPlace(state) {
5762
- return state.siteUuid !== null && !state.widgetOptOut && state.widgetInstalled && state.widgetTokenMatches !== false;
5763
- }
5764
5936
  function countRemainingSteps(state) {
5765
5937
  return [
5766
5938
  state.installed?.section === "dependencies",
@@ -5772,217 +5944,143 @@ function countRemainingSteps(state) {
5772
5944
  !state.protectionApplicable || state.protectionWired
5773
5945
  ].filter((step) => !step).length;
5774
5946
  }
5775
- function renderGuideChecklist(state, useColor) {
5776
- const paint = (code, text) => useColor ? `${code}${text}${ANSI.reset}` : text;
5777
- const done = (text) => ` ${paint(ANSI.green, "\u2714")} ${text}`;
5778
- const todo = (text) => ` ${paint(ANSI.yellow, "\u2716")} ${paint(ANSI.bold, text)}`;
5779
- const detail = (text) => ` ${paint(ANSI.dim, text)}`;
5947
+ function guideProgress(state, known = {}) {
5948
+ return {
5949
+ installed: state.installed !== null,
5950
+ // Claim state lives on the server and nothing on disk records it, so only a caller that has just
5951
+ // heard from the server (a scan's claim outcome) can mark it done.
5952
+ connected: false,
5953
+ // `.patchstackrc.json` only gains a site UUID from a manifest the server stored.
5954
+ synced: state.siteUuid !== null,
5955
+ // Only the dashboard can see the live site, so nothing the CLI runs marks this done.
5956
+ deployed: false,
5957
+ ...known
5958
+ };
5959
+ }
5960
+ function guideNextStepContext(state) {
5961
+ return {
5962
+ installCommand: installCommand(state.packageManager),
5963
+ siteUuid: state.siteUuid,
5964
+ claimUrl: state.claimUrl,
5965
+ environment: state.environment,
5966
+ environmentSource: state.environmentSource
5967
+ };
5968
+ }
5969
+ function notConnectedItem(why = []) {
5970
+ return {
5971
+ text: "Not connected to your Patchstack account",
5972
+ hint: [...why, "Until it is, anyone who opens your app can connect it to their own account."]
5973
+ };
5974
+ }
5975
+ function buildScriptLines(state) {
5780
5976
  const lines = [];
5781
- const headerParts = [state.framework, state.packageManager].filter(
5782
- (part) => part !== null
5783
- );
5784
- const name = state.projectName ?? path12.basename(process.cwd());
5785
- lines.push(paint(ANSI.bold, `Patchstack setup status \u2014 ${name} (${headerParts.join(" \xB7 ")})`));
5786
- if (state.endpointOverride !== null) {
5787
- lines.push(
5788
- detail(
5789
- `endpoint override in effect: ${state.endpointOverride} (set via .patchstackrc.json, PATCHSTACK_ENDPOINT, or --endpoint)`
5790
- )
5791
- );
5792
- }
5793
- lines.push("");
5794
- if (!state.hasPackageJson) {
5795
- lines.push(todo("No package.json found in this directory."));
5796
- lines.push(detail("For a JS/Node app, run the guide from its package directory; check that package.json is readable and valid."));
5797
- lines.push(detail("For a standalone HTML/CSS/browser-JavaScript site, use the disclosure widget directly."));
5798
- lines.push(detail("Do not create a Node project, build hooks, or a server just to install the widget."));
5799
- lines.push(detail("Use the correct site UUID or widget snippet from the Patchstack dashboard; never invent one."));
5800
- lines.push(detail('See "Plain HTML sites" in AGENT-INSTALL.md. Widget-only setup provides no dependency scan or runtime protection.'));
5801
- return lines.join("\n");
5802
- }
5803
- if (countRemainingSteps(state) > 0) {
5804
- lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Tell the user what you are doing as you go.")}`);
5805
- lines.push(" Installs here take minutes and they are watching output they cannot read.");
5806
- lines.push(" Say it in plain words: their site is built out of hundreds of ready-made");
5807
- lines.push(" pieces of code, Patchstack keeps track of which ones it uses and warns them");
5808
- lines.push(" when one turns out to be unsafe, and nothing their visitors see changes.");
5809
- lines.push(" Avoid lockfile / dependency / manifest / npm / UUID when speaking to them.");
5810
- lines.push("");
5811
- }
5812
- if (state.siteUuid === null) {
5813
- lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Nothing is set up yet \u2014 two commands cover the steps below:")}`);
5814
- if (state.installed?.section !== "dependencies") {
5815
- lines.push(` ${installCommand(state.packageManager)}`);
5816
- }
5817
- lines.push(" npx @patchstack/connect setup");
5818
- lines.push(" setup provisions the site, adds the widget, installs and verifies runtime protection,");
5819
- lines.push(" and wires the install and build scans, then prints this checklist again. Anything");
5820
- lines.push(" still listed there is a step setup could not apply safely, and is yours to finish.");
5821
- lines.push("");
5822
- }
5823
- if (state.installed?.section === "dependencies") {
5824
- lines.push(done(`@patchstack/connect installed (${state.installed.version}, ${state.installed.section})`));
5825
- } else if (state.installed !== null) {
5826
- lines.push(todo(`Move @patchstack/connect to runtime dependencies (currently ${state.installed.section})`));
5827
- lines.push(detail(`Run \u2192 ${installCommand(state.packageManager)}`));
5828
- lines.push(detail("The generated guard imports @patchstack/connect/protect at runtime."));
5829
- } else {
5830
- lines.push(todo("Install @patchstack/connect as a runtime dependency"));
5831
- lines.push(detail(`Run \u2192 ${installCommand(state.packageManager)}`));
5977
+ if (!state.installScanWired) lines.push('"postinstall": "patchstack-connect scan"');
5978
+ if (state.hasBuildScript && !(state.prebuildWired && state.postbuildWired)) {
5832
5979
  if (state.packageManager === "bun") {
5833
- lines.push(detail(`(if bun isn't available here, ${INSTALL_COMMANDS.npm} works too)`));
5834
- }
5835
- }
5836
- if (state.siteUuid !== null) {
5837
- lines.push(done(`Site provisioned (${state.siteUuid})`));
5838
- } else {
5839
- lines.push(todo("Provision the site \u2014 run the first scan"));
5840
- lines.push(detail("Run \u2192 npx @patchstack/connect scan"));
5841
- lines.push(detail("Reads the lockfile, registers the project, writes .patchstackrc.json,"));
5842
- lines.push(detail("and prints a dashboard link. The CLI prints the link but never opens it."));
5843
- lines.push(detail("If your tool refuses to run this command, hand it to the person instead of working"));
5844
- lines.push(detail('around it \u2014 see "When your tool will not run this CLI" in the reference guide.'));
5845
- }
5846
- if (state.installScanWired) {
5847
- lines.push(done("Dependency-install scan wired (postinstall)"));
5848
- } else {
5849
- lines.push(todo("Scan again whenever dependencies are installed"));
5850
- lines.push(detail('Edit package.json \u2192 "postinstall": "patchstack-connect scan"'));
5851
- }
5852
- if (!state.hasBuildScript) {
5853
- lines.push(done("No build script to integrate (postinstall covers dependency changes)"));
5854
- } else if (state.prebuildWired && state.postbuildWired) {
5855
- lines.push(done("Build hooks wired (scan before builds, mark-build after)"));
5856
- } else if (state.packageManager === "bun") {
5857
- lines.push(todo('Wire the build hooks yourself \u2014 edit package.json (bun skips pre/post hooks, so chain inside "build")'));
5858
- lines.push(detail('Edit package.json \u2192 "build": "patchstack-connect scan && <existing build command> && patchstack-connect mark-build"'));
5859
- } else {
5860
- lines.push(todo('Wire the build hooks yourself \u2014 edit package.json "scripts" (chain with && if a hook already exists)'));
5861
- if (!state.prebuildWired) {
5862
- lines.push(detail('Edit package.json \u2192 "prebuild": "patchstack-connect scan"'));
5863
- }
5864
- if (!state.postbuildWired) {
5865
- lines.push(detail('Edit package.json \u2192 "postbuild": "patchstack-connect mark-build"'));
5866
- }
5867
- }
5868
- const widgetOk = state.widgetInstalled && state.widgetTokenMatches !== false;
5869
- if (state.widgetOptOut && !widgetOk) {
5870
- lines.push(done('Disclosure widget disabled by config ("widget": false in .patchstackrc.json)'));
5871
- } else if (widgetOk) {
5872
- lines.push(done("Disclosure widget installed"));
5873
- } else if (state.widgetInstalled) {
5874
- lines.push(todo("Fix the disclosure widget yourself \u2014 its site UUID doesn't match this project's"));
5875
- lines.push(detail(`Edit the widget tag \u2192 set data-site-uuid (or userToken) to '${state.siteUuid}' (a wrong UUID makes the widget silently no-op)`));
5876
- } else if (state.siteUuid === null) {
5877
- lines.push(todo('Add the "Report a vulnerability" widget \u2014 the first scan does this for you'));
5878
- lines.push(detail("Run \u2192 npx @patchstack/connect scan (provisions the site and adds the widget tag"));
5879
- lines.push(detail(" to the root HTML shell: index.html / public/index.html / src/app.html)"));
5880
- } else {
5881
- lines.push(todo('Add the "Report a vulnerability" widget yourself \u2014 this root is code, not a plain HTML shell'));
5882
- lines.push(detail("Note \u2192 a normal `scan` adds this tag to a plain HTML shell automatically; add it by hand here:"));
5883
- const placement = state.widgetFileHint !== null ? `Edit ${state.widgetFileHint} \u2192 put it just before </body>:` : "Edit your root layout \u2192 put it just before </body> (the framework's HTML/layout mechanism, never a JS entry point):";
5884
- lines.push(detail(placement));
5885
- lines.push(detail(` ${buildWidgetTag(state.siteUuid)}`));
5886
- lines.push(detail("The site UUID is public by design \u2014 it ships in client-side HTML."));
5887
- }
5888
- if (needsSourceProductionMarker(state)) {
5889
- if (state.productionMarkerWired) {
5890
- lines.push(done("Production marker wired (widget switches to report mode on the published site)"));
5980
+ lines.push('"build": "patchstack-connect scan && <existing build command> && patchstack-connect mark-build"');
5891
5981
  } else {
5892
- lines.push(todo("Add the production marker \u2014 this root is server-rendered, so mark-build cannot stamp it"));
5893
- lines.push(
5894
- detail(
5895
- "Without it the widget treats the published site as build mode and shows the claim flow to visitors."
5896
- )
5897
- );
5898
- const gate = productionGate(state.framework);
5899
- if (hasJsxShell(state.framework)) {
5900
- lines.push(detail(`Run \u2192 npx @patchstack/connect scan (adds it to ${state.widgetFileHint} automatically)`));
5901
- lines.push(detail("Or add it by hand, in <head> above the widget tag:"));
5902
- for (const snippetLine of buildSourceMarkerSnippet(state.framework).split("\n")) {
5903
- lines.push(detail(` ${snippetLine}`));
5904
- }
5905
- } else {
5906
- lines.push(
5907
- detail(
5908
- `Edit ${state.widgetFileHint} \u2192 using the framework's head mechanism, emit an inline`
5909
- )
5910
- );
5911
- lines.push(detail(` <script>window.__PATCHSTACK_PROD__=true;</script> only when ${gate}.`));
5912
- }
5913
- lines.push(detail(`The ${gate} guard is required \u2014 an ungated marker also hides the claim flow in preview.`));
5982
+ if (!state.prebuildWired) lines.push('"prebuild": "patchstack-connect scan"');
5983
+ if (!state.postbuildWired) lines.push('"postbuild": "patchstack-connect mark-build"');
5914
5984
  }
5915
5985
  }
5916
- if (!state.protectionApplicable) {
5917
- lines.push(done("Runtime protection: not applicable \u2014 this project has no request path"));
5918
- for (const check of state.protectionChecks.filter((item) => item.hint !== void 0 && item.group !== "reporting")) {
5919
- lines.push(detail(check.hint ?? ""));
5920
- }
5921
- } else if (state.protectionWired) {
5922
- lines.push(done(`Runtime protection wired (${state.protectionStack})`));
5923
- } else {
5924
- lines.push(todo(`Finish runtime protection (${state.protectionStack})`));
5925
- for (const check of state.protectionChecks.filter((item) => !item.ok)) {
5926
- lines.push(detail(`${check.label}${check.hint ? ` \u2014 ${check.hint}` : ""}`));
5927
- }
5928
- lines.push(detail("Verify \u2192 npx @patchstack/connect protect --check"));
5929
- lines.push(detail("Prove a request reaches the guard (starts your app) \u2192 npx @patchstack/connect protect --check --runtime"));
5986
+ return lines;
5987
+ }
5988
+ function guideMissing(state, known = {}) {
5989
+ const missing = [];
5990
+ if (state.installed?.section === "devDependencies") {
5991
+ missing.push({
5992
+ text: "Patchstack is installed as a development tool only, so your live app cannot load it",
5993
+ hint: [`Run: ${installCommand(state.packageManager)}`]
5994
+ });
5930
5995
  }
5931
- lines.push("");
5932
- if (state.claimUrl !== null) {
5933
- lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Connect this site to your Patchstack account:")}`);
5934
- let route = 1;
5935
- if (widgetTagInPlace(state)) {
5936
- lines.push(` ${route++}. In the preview \u2014 while the site is unclaimed the widget shows a`);
5937
- lines.push(' "Connect this website" panel. Signing in there attaches the site.');
5938
- }
5939
- lines.push(` ${route++}. In a browser \u2014 open the dashboard link (the CLI never opens it):`);
5940
- lines.push(` ${paint(ANSI.cyan, state.claimUrl)}`);
5941
- lines.push(` ${route}. From this terminal \u2192 npx @patchstack/connect claim`);
5942
- lines.push(" (prints a link to sign in with, then attaches the site to that account)");
5943
- lines.push(detail("Reports have no owner to reach until one of these completes. The site UUID ships"));
5944
- lines.push(detail("in the page and claiming is first-come, so an unclaimed site stays claimable by"));
5945
- lines.push(detail("anyone who loads it."));
5946
- if (state.endpointOverride !== null) {
5947
- lines.push(detail("(this URL inherits the endpoint override above)"));
5948
- }
5949
- } else {
5950
- lines.push(detail("The dashboard link appears after the first scan (re-print any time with `status`)."));
5996
+ if (state.siteUuid === null) return missing;
5997
+ if (!state.widgetOptOut && state.widgetInstalled && state.widgetTokenMatches === false) {
5998
+ missing.push({
5999
+ text: "The Patchstack widget on your page belongs to a different project",
6000
+ hint: [`Set data-site-uuid to '${state.siteUuid}' on the widget tag.`]
6001
+ });
6002
+ } else if (!state.widgetOptOut && !state.widgetInstalled) {
6003
+ missing.push({
6004
+ text: "The Patchstack widget is not on your page yet",
6005
+ hint: [
6006
+ state.widgetFileHint !== null ? `Add this to ${state.widgetFileHint}, just before </body>:` : "Add this to your main page layout, just before </body>:",
6007
+ ` ${buildWidgetTag(state.siteUuid)}`
6008
+ ]
6009
+ });
5951
6010
  }
5952
- if (widgetTagInPlace(state)) {
5953
- lines.push("");
5954
- lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Refresh the preview to see the widget:")}`);
5955
- lines.push(" The widget loads with the page, so a preview that was already open still shows");
5956
- lines.push(" the HTML from before this change. Builders that hot reload refresh it themselves;");
5957
- lines.push(" if nothing appears, refresh the preview once.");
5958
- lines.push(' Unclaimed, the widget shows the "Connect this website" panel; the public');
5959
- lines.push(' "Report a vulnerability" button takes its place once the site is claimed.');
5960
- }
5961
- if (state.siteUuid !== null) {
5962
- lines.push("");
5963
- lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Deploy to put this on your live site:")}`);
5964
- lines.push(" These are source changes. Your deployed site keeps serving its previous build,");
5965
- lines.push(" so visitors only get the widget after you deploy (or hit Publish) again.");
6011
+ if (state.protectionApplicable && !state.protectionWired) {
6012
+ const failing = state.protectionChecks.filter((item) => !item.ok && item.group !== "reporting");
6013
+ const generic = state.protectionStack === "generic";
6014
+ missing.push({
6015
+ key: "runtime-protection",
6016
+ text: generic ? "Runtime protection: not added to your server yet" : `Runtime protection: not finished for ${state.protectionStack}`,
6017
+ hint: generic ? ["Add Patchstack where requests enter your app. Run npx @patchstack/connect protect for the steps."] : ["Run: npx @patchstack/connect protect, then npx @patchstack/connect protect --check"],
6018
+ detail: failing.map((check) => `${check.label}${check.hint ? ` \u2014 ${check.hint}` : ""}`)
6019
+ });
5966
6020
  }
5967
- const remaining = countRemainingSteps(state);
5968
- lines.push("");
5969
- if (remaining === 0) {
5970
- lines.push(
5971
- done(
5972
- paint(
5973
- ANSI.bold,
5974
- "Ready to deploy. Everything above is in the working tree only: commit .patchstackrc.json, package.json, the runtime guard changes, and the file carrying the widget snippet; add PATCHSTACK_API_KEY to the hosting platform; then deploy. Never commit .patchstackrc.local.json \u2014 it holds the API key, and setup has already added it to .gitignore."
5975
- )
5976
- )
5977
- );
5978
- if (state.claimUrl !== null) {
5979
- lines.push(detail("Until the site is attached to an account (dashboard link above, or `claim`), nobody can see its reports."));
5980
- }
5981
- } else {
5982
- lines.push(
5983
- ` ${paint(ANSI.yellow, String(remaining))} step(s) remaining \u2014 details in the reference guide below.`
6021
+ const scripts = buildScriptLines(state);
6022
+ if (scripts.length > 0) {
6023
+ missing.push({
6024
+ text: "Patchstack does not check your packages on install and build yet",
6025
+ hint: ["Run: npx @patchstack/connect setup (it adds the build steps to package.json)"],
6026
+ detail: [
6027
+ state.packageManager === "bun" ? "Add to package.json scripts:" : "Add to package.json scripts (chain with && if one exists):",
6028
+ ...scripts.map((line) => ` ${line}`)
6029
+ ]
6030
+ });
6031
+ }
6032
+ if (needsSourceProductionMarker(state) && !state.productionMarkerWired) {
6033
+ const gate = productionGate(state.framework);
6034
+ missing.push(
6035
+ hasJsxShell(state.framework) ? {
6036
+ text: "Your live app does not tell Patchstack it is live yet",
6037
+ hint: [`Run: npx @patchstack/connect scan (it edits ${state.widgetFileHint})`],
6038
+ detail: [`Or add inside <head>:`, ...buildSourceMarkerSnippet(state.framework).split("\n").map((line) => ` ${line}`)]
6039
+ } : {
6040
+ text: "Your live app does not tell Patchstack it is live yet",
6041
+ hint: [
6042
+ `Add this inside <head> in ${state.widgetFileHint}, only when ${gate}:`,
6043
+ " <script>window.__PATCHSTACK_PROD__=true;</script>"
6044
+ ]
6045
+ }
5984
6046
  );
5985
6047
  }
6048
+ if (known.connected !== true) missing.push(notConnectedItem());
6049
+ return missing;
6050
+ }
6051
+ function renderGuideChecklist(state, useColor2, known = {}, options = {}) {
6052
+ const paint = (code, text) => useColor2 ? `${code}${text}${ANSI3.reset}` : text;
6053
+ const name = state.projectName ?? path12.basename(process.cwd());
6054
+ const title = `Patchstack status for ${name}`;
6055
+ if (!state.hasPackageJson) {
6056
+ return [
6057
+ paint(ANSI3.bold, title),
6058
+ "",
6059
+ paint(ANSI3.bold, "Missing"),
6060
+ ` ${paint(ANSI3.yellow, "\u2718")} No package.json here.`,
6061
+ " For a JS/Node app, run this from its package directory.",
6062
+ ' For a plain HTML site, follow "Plain HTML sites" in AGENT-INSTALL.md. It gets the widget only: no dependency scan or runtime protection.',
6063
+ " Do not create a Node project just to add the widget.",
6064
+ " Use the site UUID or widget snippet from the Patchstack dashboard. Never invent one."
6065
+ ].join("\n");
6066
+ }
6067
+ const lines = renderStatus(
6068
+ title,
6069
+ { done: [], missing: guideMissing(state, known) },
6070
+ guideProgress(state, known),
6071
+ guideNextStepContext(state),
6072
+ { useColor: useColor2, verbose: options.verbose }
6073
+ );
6074
+ if (options.verbose === true) {
6075
+ const verbose2 = [
6076
+ `Project: ${[state.framework, state.packageManager].filter((part) => part !== null).join(" \xB7 ")}`,
6077
+ ...state.siteUuid !== null ? [`Site UUID: ${state.siteUuid}`] : [],
6078
+ ...state.environment !== null ? [`Environment: ${state.environment}${state.environmentSource !== null ? ` (${state.environmentSource})` : ""}`] : [],
6079
+ ...state.endpointOverride !== null ? [`Endpoint override: ${state.endpointOverride}`] : [],
6080
+ ...state.widgetOptOut ? ['Widget is off ("widget": false in .patchstackrc.json).'] : []
6081
+ ];
6082
+ lines.splice(1, 0, ...verbose2.map((line) => paint(ANSI3.dim, line)));
6083
+ }
5986
6084
  return lines.join("\n");
5987
6085
  }
5988
6086
 
@@ -7494,14 +7592,16 @@ function requestMemberAccesses(params, body, ts, opts = {}) {
7494
7592
  const inner = unwrap(e);
7495
7593
  return Boolean(inner && ts.isCallExpression(inner) && ts.isPropertyAccessExpression(inner.expression) && ["json", "formData"].includes(inner.expression.name.text) && ts.isIdentifier(inner.expression.expression) && inner.expression.expression.text === reqName);
7496
7594
  };
7595
+ const isCallee = (n) => Boolean(n.parent && (ts.isCallExpression(n.parent) || ts.isNewExpression(n.parent)) && n.parent.expression === n);
7596
+ const ACCESSOR_SOURCES = /* @__PURE__ */ new Set(["header", "cookie", "form-body"]);
7497
7597
  const visit = (n) => {
7498
- if (ts.isPropertyAccessExpression(n) && isReqSourceExpr(n.expression)) {
7598
+ if (ts.isPropertyAccessExpression(n) && isReqSourceExpr(n.expression) && !isCallee(n)) {
7499
7599
  record(n.name.text, sourceOfExpr(n.expression));
7500
7600
  }
7501
7601
  if (ts.isElementAccessExpression(n) && isReqSourceExpr(n.expression) && n.argumentExpression && ts.isStringLiteral(n.argumentExpression)) {
7502
7602
  record(n.argumentExpression.text, sourceOfExpr(n.expression));
7503
7603
  }
7504
- if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression) && n.expression.name.text === "get" && isReqSourceExpr(n.expression.expression) && n.arguments.length === 1 && n.arguments[0] && ts.isStringLiteral(n.arguments[0])) {
7604
+ if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression) && n.expression.name.text === "get" && isReqSourceExpr(n.expression.expression) && n.arguments.length === 1 && n.arguments[0] && ts.isStringLiteral(n.arguments[0]) && ACCESSOR_SOURCES.has(sourceOfExpr(n.expression.expression))) {
7505
7605
  record(n.arguments[0].text, sourceOfExpr(n.expression.expression));
7506
7606
  }
7507
7607
  if (ts.isVariableDeclaration(n) && n.initializer) {
@@ -7553,6 +7653,7 @@ function requestMemberAccesses(params, body, ts, opts = {}) {
7553
7653
 
7554
7654
  // src/map/flows.ts
7555
7655
  var MAX_DEPENDENCY_INPUT_FLOWS_PER_ENDPOINT = 100;
7656
+ var ACCESSOR_NAMESPACES = /* @__PURE__ */ new Set(["headers", "cookies"]);
7556
7657
  function spaceOfKey(key) {
7557
7658
  if (key === "query") return "get";
7558
7659
  if (key === "params") return "route-param";
@@ -7578,18 +7679,18 @@ function linkFlows(bodyNode, params, inputs, sinks, ts, invocations) {
7578
7679
  }
7579
7680
  const CONTAINER_KEYS = /* @__PURE__ */ new Set([...REQ_SOURCES, "data", "input", "payload"]);
7580
7681
  const rootPath = /* @__PURE__ */ new Map();
7581
- const addRoot = (name, path17, space) => {
7582
- if (!rootPath.has(name)) rootPath.set(name, { path: path17, space });
7682
+ const addRoot = (name, path17, space, accessor = false, request = false) => {
7683
+ if (!rootPath.has(name)) rootPath.set(name, { path: path17, space, ...accessor ? { accessor } : {}, ...request ? { request } : {} });
7583
7684
  };
7584
- for (const p of params ?? []) {
7685
+ for (const [index, p] of (params ?? []).entries()) {
7585
7686
  if (!p?.name) continue;
7586
- if (ts.isIdentifier(p.name)) addRoot(p.name.text, "");
7687
+ if (ts.isIdentifier(p.name)) addRoot(p.name.text, "", void 0, false, index === 0);
7587
7688
  else if (ts.isObjectBindingPattern(p.name)) {
7588
7689
  for (const el of p.name.elements) {
7589
7690
  if (!ts.isBindingElement(el) || !ts.isIdentifier(el.name)) continue;
7590
7691
  const key = bindingKey(el, ts);
7591
7692
  const container = key !== void 0 && CONTAINER_KEYS.has(key);
7592
- addRoot(el.name.text, container ? "" : key ?? el.name.text, container ? spaceOfKey(key) : void 0);
7693
+ addRoot(el.name.text, container ? "" : key ?? el.name.text, container ? spaceOfKey(key) : void 0, container && ACCESSOR_NAMESPACES.has(key));
7593
7694
  }
7594
7695
  }
7595
7696
  }
@@ -7601,7 +7702,8 @@ function linkFlows(bodyNode, params, inputs, sinks, ts, invocations) {
7601
7702
  const m = cur.expression.name.text;
7602
7703
  if (["json", "formData", "text"].includes(m)) {
7603
7704
  const root = rootIdentifier(cur.expression.expression, ts);
7604
- return root && rootPath.has(root) ? { path: "", space: "post" } : void 0;
7705
+ if (!root || !rootPath.has(root)) return void 0;
7706
+ return m === "formData" ? { path: "", space: "post", accessor: true } : { path: "", space: "post" };
7605
7707
  }
7606
7708
  if (["parse", "safeParse", "validate", "cast"].includes(m)) {
7607
7709
  for (const a of cur.arguments) {
@@ -7611,21 +7713,21 @@ function linkFlows(bodyNode, params, inputs, sinks, ts, invocations) {
7611
7713
  return void 0;
7612
7714
  }
7613
7715
  }
7614
- const p = pathFromTainted(cur, ts, rootPath);
7615
- return p;
7716
+ return pathFromTainted(cur, ts, rootPath) ?? accessorRead(cur, ts, rootPath);
7616
7717
  };
7617
7718
  const aliasVisit = (n) => {
7618
7719
  if (ts.isVariableDeclaration(n) && n.initializer) {
7619
7720
  const base = requestReadPath(n.initializer);
7620
7721
  if (base !== void 0) {
7621
- if (ts.isIdentifier(n.name)) addRoot(n.name.text, base.path, base.space);
7722
+ if (ts.isIdentifier(n.name)) addRoot(n.name.text, base.path, base.space, base.accessor === true);
7622
7723
  else if (ts.isObjectBindingPattern(n.name)) {
7623
7724
  for (const el of n.name.elements) {
7624
7725
  if (!ts.isBindingElement(el) || !ts.isIdentifier(el.name)) continue;
7625
7726
  const key = bindingKey(el, ts);
7626
- const space = base.space ?? (base.path === "" ? spaceOfKey(key) : void 0);
7627
- const container = base.path === "" && key !== void 0 && CONTAINER_KEYS.has(key);
7628
- addRoot(el.name.text, container ? "" : join22(base.path, key ?? el.name.text), space);
7727
+ const namespace = base.request === true && key !== void 0 && REQ_SOURCES.includes(key);
7728
+ const container = base.path === "" && key !== void 0 && CONTAINER_KEYS.has(key) && (namespace || !REQ_SOURCES.includes(key));
7729
+ const space = base.space ?? (namespace ? spaceOfKey(key) : void 0);
7730
+ addRoot(el.name.text, container ? "" : join22(base.path, key ?? el.name.text), space, namespace && ACCESSOR_NAMESPACES.has(key));
7629
7731
  }
7630
7732
  }
7631
7733
  }
@@ -7657,7 +7759,7 @@ function linkFlows(bodyNode, params, inputs, sinks, ts, invocations) {
7657
7759
  for (const a of args) for (const l of sinkArgumentLimitations(a, ts, rootPath)) sinkLimits.push(l);
7658
7760
  for (let i = 0; i < args.length; i++) {
7659
7761
  const role = argumentRoleOf(sink.kind, method, i, args.length);
7660
- const whole = pathFromTainted(args[i], ts, rootPath);
7762
+ const whole = pathFromTainted(args[i], ts, rootPath) ?? accessorRead(args[i], ts, rootPath);
7661
7763
  for (const read2 of taintedReadPaths(args[i], ts, rootPath)) {
7662
7764
  const key = `${read2.space ?? "*"}:${read2.path}`;
7663
7765
  const exact = whole !== void 0 && whole.path === read2.path && whole.space === read2.space;
@@ -7819,6 +7921,18 @@ function taintedReadPaths(node, ts, rootPath, includeDeferredBodies = true) {
7819
7921
  const visit = (n) => {
7820
7922
  if (!n) return;
7821
7923
  if (!includeDeferredBodies && (ts.isArrowFunction(n) || ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n))) return;
7924
+ if (ts.isCallExpression(n)) {
7925
+ const accessed = accessorRead(n, ts, rootPath);
7926
+ if (accessed !== void 0) {
7927
+ add(accessed);
7928
+ return;
7929
+ }
7930
+ if (ts.isPropertyAccessExpression(n.expression)) {
7931
+ visit(n.expression.expression);
7932
+ for (const arg of n.arguments ?? []) visit(arg);
7933
+ return;
7934
+ }
7935
+ }
7822
7936
  if (ts.isPropertyAccessExpression(n) || ts.isElementAccessExpression(n)) {
7823
7937
  const read2 = pathFromTainted(n, ts, rootPath);
7824
7938
  if (read2 !== void 0) {
@@ -7838,11 +7952,11 @@ function taintedReadPaths(node, ts, rootPath, includeDeferredBodies = true) {
7838
7952
  function sinkArgumentLimitations(node, ts, rootPath) {
7839
7953
  const out = [];
7840
7954
  const seen = /* @__PURE__ */ new Set();
7841
- const add = (kind, detail, n) => {
7842
- const key = `${kind}:${detail}`;
7955
+ const add = (kind, detail2, n) => {
7956
+ const key = `${kind}:${detail2}`;
7843
7957
  if (seen.has(key)) return;
7844
7958
  seen.add(key);
7845
- out.push({ kind, detail, line: lineOf(n) });
7959
+ out.push({ kind, detail: detail2, line: lineOf(n) });
7846
7960
  };
7847
7961
  const text = (n) => {
7848
7962
  try {
@@ -7869,7 +7983,25 @@ function sinkArgumentLimitations(node, ts, rootPath) {
7869
7983
  visit(node);
7870
7984
  return out;
7871
7985
  }
7872
- function pathFromTainted(node, ts, rootPath) {
7986
+ function accessorRead(node, ts, rootPath) {
7987
+ let cur = node;
7988
+ while (cur && (ts.isAwaitExpression(cur) || ts.isParenthesizedExpression(cur) || ts.isNonNullExpression(cur))) cur = cur.expression;
7989
+ if (!cur || !ts.isCallExpression(cur) || !ts.isPropertyAccessExpression(cur.expression)) return void 0;
7990
+ const [name] = cur.arguments;
7991
+ if (cur.expression.name.text !== "get" || cur.arguments.length !== 1 || !name || !ts.isStringLiteralLike(name)) return void 0;
7992
+ if (!isAccessor(cur.expression.expression, ts, rootPath)) return void 0;
7993
+ return pathFromTainted(cur.expression.expression, ts, rootPath, [name.text]);
7994
+ }
7995
+ function isAccessor(node, ts, rootPath) {
7996
+ let cur = node;
7997
+ while (cur && (ts.isParenthesizedExpression(cur) || ts.isNonNullExpression(cur))) cur = cur.expression;
7998
+ if (ts.isIdentifier(cur)) return rootPath.get(cur.text)?.accessor === true;
7999
+ if (ts.isPropertyAccessExpression(cur) && ACCESSOR_NAMESPACES.has(cur.name.text) && ts.isIdentifier(cur.expression)) {
8000
+ return rootPath.get(cur.expression.text)?.request === true;
8001
+ }
8002
+ return false;
8003
+ }
8004
+ function pathFromTainted(node, ts, rootPath, trailing = []) {
7873
8005
  const segs = [];
7874
8006
  let cur = node;
7875
8007
  for (; ; ) {
@@ -7893,12 +8025,14 @@ function pathFromTainted(node, ts, rootPath) {
7893
8025
  if (!cur || !ts.isIdentifier(cur)) return void 0;
7894
8026
  const base = rootPath.get(cur.text);
7895
8027
  if (base === void 0) return void 0;
8028
+ segs.push(...trailing);
7896
8029
  let space = base.space;
7897
- if (base.path === "" && segs.length > 1 && REQ_SOURCES.includes(segs[0])) {
8030
+ if (base.request === true && base.path === "" && segs.length > 1 && REQ_SOURCES.includes(segs[0])) {
7898
8031
  space = spaceOfKey(segs[0]) ?? space;
7899
8032
  segs.shift();
7900
8033
  }
7901
- return { path: normalizePath([base.path, ...segs].filter(Boolean).join(".")), space };
8034
+ const request = base.request === true && segs.length === 0 ? { request: true } : {};
8035
+ return { path: normalizePath([base.path, ...segs].filter(Boolean).join(".")), space, ...request };
7902
8036
  }
7903
8037
 
7904
8038
  // src/map/invocations.ts
@@ -8798,7 +8932,7 @@ function isPreBundleBuildHook(env = process.env) {
8798
8932
  return /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(env.npm_lifecycle_script ?? "");
8799
8933
  }
8800
8934
  function undeliveredReportLines(err, config, cwd) {
8801
- const lines = [`patchstack: manifest not reported \u2014 ${err.message}`];
8935
+ const lines = [`patchstack: could not send the package list to Patchstack \u2014 ${err.message}`];
8802
8936
  if (err.code === "UNAUTHORIZED") {
8803
8937
  const hasCredential = typeof config.pulseAuth === "string" && config.pulseAuth.length > 0;
8804
8938
  if (hasCredential) {
@@ -8813,7 +8947,7 @@ function undeliveredReportLines(err, config, cwd) {
8813
8947
  }
8814
8948
  }
8815
8949
  lines.push(
8816
- "patchstack: continuing the build. Patchstack keeps the last manifest it accepted for this site until a scan that can report."
8950
+ "patchstack: continuing the build. Patchstack keeps the last package list it received until a check gets through."
8817
8951
  );
8818
8952
  return lines;
8819
8953
  }
@@ -8964,9 +9098,9 @@ import path15 from "path";
8964
9098
  var SCAN_COMMAND = "patchstack-connect scan";
8965
9099
  var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
8966
9100
  function setupProtection(cwd) {
8967
- const install = runProtect(cwd);
9101
+ const { result: install, lines: log2 } = collectProtectLog(() => runProtect(cwd));
8968
9102
  const verification = runVerify(cwd);
8969
- return { install, verification };
9103
+ return { install, verification, log: log2 };
8970
9104
  }
8971
9105
  function appendHook(existing, command) {
8972
9106
  if (existing === void 0 || existing.trim().length === 0) {
@@ -9059,7 +9193,7 @@ Usage:
9059
9193
  If no UUID is configured, the server
9060
9194
  provisions one and we persist it. After a
9061
9195
  successful post it also adds/updates the
9062
- disclosure-widget <script> tag in the root
9196
+ Patchstack Connector <script> tag in the root
9063
9197
  HTML shell (index.html, public/index.html,
9064
9198
  or src/app.html) \u2014 opt out with
9065
9199
  "widget": false in .patchstackrc.json.
@@ -9168,6 +9302,8 @@ Usage:
9168
9302
  Global:
9169
9303
  --version Print the installed version of this package and exit
9170
9304
  --help Print this help and exit
9305
+ --verbose Also print the technical detail: site ID, endpoint, checksum,
9306
+ environment and why, files written, installer steps
9171
9307
 
9172
9308
  Options (for scan, setup, status, and uninstall):
9173
9309
  --site-uuid <uuid> Override the configured site UUID
@@ -9223,6 +9359,13 @@ Examples:
9223
9359
  npx @patchstack/connect demo-guide node-serialize
9224
9360
  `;
9225
9361
  var VALUE_FLAGS = /* @__PURE__ */ new Set(["site-uuid", "endpoint", "dir", "url", "out", "claim-token"]);
9362
+ var verbose = false;
9363
+ function detail(line) {
9364
+ if (verbose) console.log(line);
9365
+ }
9366
+ function useColor() {
9367
+ return process.stdout.isTTY === true && process.env.NO_COLOR === void 0;
9368
+ }
9226
9369
  function parseArgs(argv) {
9227
9370
  const args = argv.slice(2);
9228
9371
  const positional = [];
@@ -9281,7 +9424,7 @@ async function runInit(args) {
9281
9424
  const target = await writeConfigFile(process.cwd(), { siteUuid: uuid });
9282
9425
  console.log(`Wrote ${target}`);
9283
9426
  console.log("");
9284
- console.log("Next: run `npx @patchstack/connect scan` to send your first manifest.");
9427
+ console.log("Next: run `npx @patchstack/connect scan`.");
9285
9428
  return 0;
9286
9429
  }
9287
9430
  async function runClaim(args) {
@@ -9303,7 +9446,7 @@ async function runClaim(args) {
9303
9446
  console.log(` ${result2.dashboardUrl ?? buildClaimUrl(config.endpoint, siteUuid)}`);
9304
9447
  if (result2.credentialSaved === true) {
9305
9448
  const ignore = await secretFileIgnored(process.cwd());
9306
- console.log(` A credential for this site was issued and saved to ${SECRET_CONFIG_FILENAME}.`);
9449
+ console.log(` Saved a new credential to ${SECRET_CONFIG_FILENAME}.`);
9307
9450
  console.log(gitignoreOutcomeLine(ignore));
9308
9451
  }
9309
9452
  console.log("");
@@ -9314,8 +9457,7 @@ async function runClaim(args) {
9314
9457
  Your code: ${userCode}`);
9315
9458
  console.log(` Claim at: ${verificationUri}
9316
9459
  `);
9317
- console.log(" Open that link and sign in to Patchstack \u2014 or create an account \u2014 to attach");
9318
- console.log(" this site to it. Whoever approves becomes the site's owner.\n");
9460
+ console.log(" Open the link and sign in (or sign up) to connect this site. Whoever approves owns it.\n");
9319
9461
  };
9320
9462
  if (args.flags.has("wait")) {
9321
9463
  const pending = readPendingClaim(siteUuid);
@@ -9359,8 +9501,7 @@ async function runClaim(args) {
9359
9501
  return 1;
9360
9502
  }
9361
9503
  prompt(started.pending.userCode, started.pending.verificationUri);
9362
- console.log(" Give that link to the user. When they confirm they have claimed the site, run");
9363
- console.log(" this same command again (or `claim --wait` to block until they do).\n");
9504
+ console.log(" Give the link to the user. Once they approve, run this command again (or `claim --wait`).\n");
9364
9505
  return 0;
9365
9506
  }
9366
9507
  const result = await claim2(config, (userCode, verificationUri) => {
@@ -9390,8 +9531,7 @@ async function runLogin(args) {
9390
9531
  console.log(`
9391
9532
  \u2713 Credential restored and saved to ${SECRET_CONFIG_FILENAME}.`);
9392
9533
  console.log(gitignoreOutcomeLine(ignore));
9393
- console.log(" The previous credential no longer works. Update it anywhere else it was set:");
9394
- console.log(" CI secrets, hosting env vars, preview environments, other checkouts.\n");
9534
+ console.log(" The old credential no longer works. Update it in CI, hosting and other checkouts.\n");
9395
9535
  return 0;
9396
9536
  };
9397
9537
  if (args.flags.has("wait")) {
@@ -9412,9 +9552,8 @@ async function runLogin(args) {
9412
9552
  Your code: ${userCode}`);
9413
9553
  console.log(` Approve at: ${verificationUri}
9414
9554
  `);
9415
- console.log(" Open that link and approve it as the site's owner. Approving issues a new");
9416
- console.log(" credential and stops the current one working \u2014 CI, deploys and any other");
9417
- console.log(" machine using it will need the new value.\n");
9555
+ console.log(" Open the link and approve as the site's owner. This replaces the current credential,");
9556
+ console.log(" so CI, deploys and other machines will need the new one.\n");
9418
9557
  };
9419
9558
  if (process.stdout.isTTY !== true) {
9420
9559
  const existing = config.siteUuid === null ? null : readPendingLogin(config.siteUuid);
@@ -9439,8 +9578,7 @@ async function runLogin(args) {
9439
9578
  return 1;
9440
9579
  }
9441
9580
  prompt(started.pending.userCode, started.pending.verificationUri);
9442
- console.log(" Give that link to the user. When they confirm they have approved it, run");
9443
- console.log(" this same command again (or `login --wait` to block until they do).\n");
9581
+ console.log(" Give the link to the user. Once they approve, run this command again (or `login --wait`).\n");
9444
9582
  return 0;
9445
9583
  }
9446
9584
  const result = await login(config, (userCode, verificationUri) => {
@@ -9458,8 +9596,8 @@ var MAX_RETRY_TIMEOUT_MS = 18e4;
9458
9596
  async function postManifestAccepted(config, payload) {
9459
9597
  const { response, environmentUsed } = await postManifestWithEnvironmentFallback(config, payload);
9460
9598
  if (environmentUsed !== config.environment) {
9461
- console.warn(
9462
- `patchstack: this Patchstack API does not know the ${config.environment} label yet; the report was accepted as ${environmentUsed}, which also keeps it apart from production.`
9599
+ detail(
9600
+ `This Patchstack API does not know the ${config.environment} label yet; the report was accepted as ${environmentUsed}, which also keeps it apart from production.`
9463
9601
  );
9464
9602
  }
9465
9603
  return response;
@@ -9475,23 +9613,27 @@ async function postManifestWithPatience(config, payload) {
9475
9613
  if (!(err instanceof PatchstackError) || err.code !== "NETWORK_TIMEOUT") throw err;
9476
9614
  const timeoutMs = Math.min(config.timeoutMs * RETRY_TIMEOUT_FACTOR, MAX_RETRY_TIMEOUT_MS);
9477
9615
  if (timeoutMs <= config.timeoutMs) throw err;
9478
- console.warn(
9479
- `patchstack: the report timed out after ${config.timeoutMs}ms; trying once more with ${timeoutMs}ms.`
9480
- );
9616
+ console.warn("Patchstack is slow to answer. Trying once more\u2026");
9617
+ detail(`The report timed out after ${config.timeoutMs}ms; trying once more with ${timeoutMs}ms.`);
9481
9618
  const response = await postManifestAccepted({ ...config, timeoutMs }, payload);
9482
9619
  try {
9483
9620
  const target = await persistTimeout(process.cwd(), timeoutMs);
9484
- console.log(`Saved a ${timeoutMs}ms request timeout to ${target}, so builds inherit it.`);
9621
+ detail(`Saved a ${timeoutMs}ms request timeout to ${target}, so builds inherit it.`);
9485
9622
  } catch {
9486
- console.warn(
9487
- `patchstack: could not save the timeout; set PATCHSTACK_TIMEOUT_MS=${timeoutMs} where builds run.`
9488
- );
9623
+ console.warn(`Could not save the longer wait. Set PATCHSTACK_TIMEOUT_MS=${timeoutMs} where your builds run.`);
9489
9624
  }
9490
9625
  return response;
9491
9626
  }
9492
9627
  }
9628
+ function plural(count, one, many) {
9629
+ return `${count} ${count === 1 ? one : many}`;
9630
+ }
9493
9631
  async function runScan(args, options = {}) {
9494
9632
  const dryRun = args.flags.get("dry-run") === true;
9633
+ const say = (line) => {
9634
+ if (verbose || dryRun) console.log(line);
9635
+ };
9636
+ const report = options.report ?? emptyReport();
9495
9637
  const config = await resolveCliConfig(args, {
9496
9638
  cliClaimToken: getStringFlag(args.flags, "claim-token"),
9497
9639
  // The one command that reports them, so the one command that resolves them.
@@ -9499,48 +9641,41 @@ async function runScan(args, options = {}) {
9499
9641
  });
9500
9642
  const manifest = await scanLockfile(process.cwd());
9501
9643
  for (const warning of manifest.warnings ?? []) {
9502
- console.warn(`patchstack: ${warning}`);
9644
+ if (dryRun) console.warn(`patchstack: ${warning}`);
9645
+ else report.missing.push({ text: warning });
9503
9646
  }
9504
9647
  const installPaths = args.flags.get("install-paths") === true;
9505
9648
  const { payload, stats } = buildWirePayload(manifest, { installPaths });
9506
- console.log(
9507
- `Found ${payload.packages.length} unique package versions across ${stats.uniqueNames} package names (${manifest.ecosystem} ecosystem).`
9508
- );
9649
+ say(`Found ${payload.packages.length} package versions across ${stats.uniqueNames} packages (${manifest.ecosystem}).`);
9509
9650
  if (installPaths) {
9510
9651
  const located = payload.packages.filter((pkg) => pkg.paths !== void 0).length;
9511
9652
  console.log(
9512
- payload.installPathsComplete ? `Including each package's install location in the dependency tree (--install-paths), for all ${located}.` : `Including install locations (--install-paths), for ${located} of ${payload.packages.length} \u2014 this lockfile format does not record them for the rest, which will be reported as "not recorded" rather than "not installed there".`
9653
+ payload.installPathsComplete ? `Including install locations (--install-paths) for all ${located}.` : `Including install locations (--install-paths) for ${located} of ${payload.packages.length}. This lockfile does not record the rest; they are sent as "not recorded".`
9513
9654
  );
9514
9655
  }
9515
9656
  if (config.environment === "local") {
9516
- console.log(
9517
- "Reporting from this machine as the local environment: the dashboard will show the app as configured, not deployed. A build on a platform this recognises reports production or sandbox on its own; on a platform it does not know, set PATCHSTACK_ENVIRONMENT=production for the build that goes live."
9518
- );
9657
+ say("Environment: local (this machine). Set PATCHSTACK_ENVIRONMENT=production on a live build your host does not identify.");
9519
9658
  } else {
9520
9659
  const because = (config.environmentEvidence ?? []).length > 0 ? ` (${config.environmentEvidence.join("; ")})` : "";
9521
- console.log(`Reporting under the ${config.environment} environment${because}. Override with PATCHSTACK_ENVIRONMENT.`);
9660
+ say(`Environment: ${config.environment}${because}.`);
9522
9661
  }
9523
9662
  if (config.endpoint !== DEFAULT_ENDPOINT) {
9524
- console.log(
9525
- `Using endpoint override: ${config.endpoint} (set via --endpoint, PATCHSTACK_ENDPOINT, or .patchstackrc.json).`
9526
- );
9663
+ say(`Endpoint override: ${config.endpoint}`);
9527
9664
  }
9528
9665
  if (stats.duplicateNames.length > 0) {
9529
9666
  const sample = stats.duplicateNames.slice(0, 10).join(", ");
9530
9667
  const more = stats.duplicateNames.length > 10 ? `, +${stats.duplicateNames.length - 10} more` : "";
9531
- console.log(
9532
- `${stats.duplicateNames.length} package(s) appear at multiple versions: ${sample}${more}`
9533
- );
9668
+ say(`${stats.duplicateNames.length} package(s) at more than one version: ${sample}${more}`);
9534
9669
  }
9535
9670
  const body = buildManifestBody(config, payload);
9536
9671
  if (typeof body.url === "string") {
9537
- console.log(`Reporting this app's address as ${body.url}.`);
9672
+ say(`Reporting app address: ${body.url}`);
9538
9673
  }
9539
9674
  if (typeof body.name === "string") {
9540
- console.log(`Reporting this app's name as "${body.name}".`);
9675
+ say(`Reporting app name: "${body.name}"`);
9541
9676
  }
9542
9677
  if (typeof config.claimToken === "string" && config.claimToken !== "") {
9543
- console.log("A claim token is set: the site will be connected to the Patchstack account that issued it.");
9678
+ say("Claim token set. The site will join the Patchstack account that issued it.");
9544
9679
  }
9545
9680
  if (dryRun) {
9546
9681
  console.log("");
@@ -9559,15 +9694,15 @@ async function runScan(args, options = {}) {
9559
9694
  }
9560
9695
  if (isPreBundleBuildHook()) {
9561
9696
  const cleared = applyBuildStamp(process.cwd(), null);
9562
- if (cleared.kind === "cleared") console.log(`Removed the previous map binding from ${cleared.file}.`);
9697
+ if (cleared.kind === "cleared") say(`Removed the previous map binding from ${cleared.file}.`);
9563
9698
  }
9564
9699
  const shellFramework = detectStack(payload.packages).framework;
9565
9700
  if (config.widget) {
9566
- reportSourceMarker(shellFramework, computeManifestChecksum(payload.packages));
9701
+ reportSourceMarker(shellFramework, computeManifestChecksum(payload.packages), report);
9567
9702
  }
9568
9703
  const provisioning = config.siteUuid === null;
9569
9704
  if (provisioning) {
9570
- console.log("No site UUID configured \u2014 provisioning a new Patchstack site from this manifest\u2026");
9705
+ say("No site yet. Creating one on Patchstack\u2026");
9571
9706
  }
9572
9707
  let response;
9573
9708
  try {
@@ -9577,113 +9712,147 @@ async function runScan(args, options = {}) {
9577
9712
  for (const line of undeliveredReportLines(err, config, process.cwd())) console.error(line);
9578
9713
  return 0;
9579
9714
  }
9715
+ const checked = `Checked ${plural(stats.uniqueNames, "package", "packages")}`;
9716
+ if (response.stored) {
9717
+ report.done.unshift(checked);
9718
+ say(`Stored manifest #${response.manifest_id} (checksum ${response.checksum}).`);
9719
+ } else if (response.reason === "duplicate") {
9720
+ report.done.unshift(checked, "No changes since the last check");
9721
+ } else {
9722
+ report.missing.push({
9723
+ text: "Patchstack did not save this check",
9724
+ hint: [response.message ?? "Run this again in a few minutes."],
9725
+ detail: [`Server response: ${JSON.stringify(response)}`]
9726
+ });
9727
+ }
9580
9728
  if (provisioning && response.uuid !== void 0 && response.uuid.length > 0) {
9581
9729
  const target = await persistSiteUuid(process.cwd(), response.uuid);
9582
- console.log(`Provisioned site ${response.uuid}. Saved UUID to ${target}.`);
9730
+ report.done.push("Added this project to Patchstack");
9731
+ say(`Created site ${response.uuid}. Saved to ${target}.`);
9583
9732
  }
9584
9733
  if (typeof response.api_key === "string" && response.api_key.length > 0) {
9585
9734
  const hadCredentialInConfig = await credentialInCommittedConfig(process.cwd());
9586
9735
  const saved = await persistApiKey(process.cwd(), response.api_key);
9587
- console.log(`Saved API key to ${saved.path}. Do not commit it.`);
9588
- console.log(
9589
- saved.ignored ? " Added to .gitignore." : ` NOT ignored by git \u2014 ${saved.reason ?? "unknown reason"}. Add \`${SECRET_CONFIG_FILENAME}\` to .gitignore yourself before committing.`
9590
- );
9736
+ if (saved.ignored) {
9737
+ report.done.push(`Saved the project's API key in ${SECRET_CONFIG_FILENAME}. Never commit this file`);
9738
+ say(`Saved API key to ${saved.path} (git-ignored).`);
9739
+ } else {
9740
+ report.missing.push({
9741
+ text: `${SECRET_CONFIG_FILENAME} holds your API key, and git does not ignore it`,
9742
+ hint: [`Add ${SECRET_CONFIG_FILENAME} to .gitignore before you commit. Never commit this file.`],
9743
+ detail: [`Saved to ${saved.path}. Not ignored: ${saved.reason ?? "unknown reason"}.`]
9744
+ });
9745
+ }
9591
9746
  if (hadCredentialInConfig) {
9592
- console.log(
9593
- "A credential was also present in .patchstackrc.json and has been removed from it. If that file was ever committed, rotate the credential from the dashboard."
9594
- );
9747
+ report.missing.push({
9748
+ text: "Your API key was in .patchstackrc.json",
9749
+ hint: [
9750
+ `It is now in ${SECRET_CONFIG_FILENAME}. If .patchstackrc.json was ever committed, replace the key in the Patchstack dashboard.`
9751
+ ]
9752
+ });
9595
9753
  }
9596
9754
  }
9597
- if (response.stored) {
9598
- console.log(`Stored manifest #${response.manifest_id} (checksum ${response.checksum}).`);
9599
- } else if (response.reason === "duplicate") {
9600
- console.log("Manifest unchanged since last scan \u2014 nothing to store.");
9601
- } else {
9602
- console.log(`Server response: ${response.message ?? JSON.stringify(response)}`);
9603
- }
9604
- const claimLines = claimOutcomeLines(response.claim, config);
9605
- if (claimLines.length > 0) {
9606
- console.log("");
9607
- for (const line of claimLines) console.log(line);
9755
+ const claim3 = claimOutcome(response.claim, config);
9756
+ if (claim3 !== null && claim3.connected) {
9757
+ report.done.push(claim3.dashboardUrl !== null ? `${claim3.summary}: ${claim3.dashboardUrl}` : claim3.summary);
9758
+ } else if (claim3 !== null) {
9759
+ report.missing.push(notConnectedItem([`${claim3.summary}.`, ...claim3.hint]));
9608
9760
  }
9609
9761
  const connected = response.claim?.state === "claimed" || response.claim?.state === "owned-by-you";
9610
9762
  const effectiveUuid = config.siteUuid ?? response.uuid ?? null;
9611
9763
  if (config.widget && effectiveUuid !== null && effectiveUuid.length > 0) {
9612
- reportSourceWidget(effectiveUuid, shellFramework);
9613
- }
9614
- const linkUuid = response.uuid ?? config.siteUuid;
9615
- if (!connected && (provisioning || claimLines.length > 0) && linkUuid !== null && linkUuid !== void 0 && linkUuid.length > 0) {
9616
- console.log("");
9617
- console.log("Connect this site to your Patchstack account \u2014 any one of these:");
9618
- if (config.widget) {
9619
- console.log(' 1. In the preview \u2014 the widget shows a "Connect this website" panel while the');
9620
- console.log(" site is unclaimed. Signing in there attaches it.");
9621
- console.log(" 2. Open this dashboard link in a browser:");
9622
- console.log(` ${buildClaimUrl(config.endpoint, linkUuid)}`);
9623
- console.log(" 3. From this terminal: npx @patchstack/connect claim");
9624
- } else {
9625
- console.log(" 1. Open this dashboard link in a browser:");
9626
- console.log(` ${buildClaimUrl(config.endpoint, linkUuid)}`);
9627
- console.log(" 2. From this terminal: npx @patchstack/connect claim");
9628
- }
9629
- if (config.endpoint !== DEFAULT_ENDPOINT) {
9630
- console.log(" (this URL inherits the endpoint override above)");
9631
- }
9632
- }
9633
- if (options.showRemainingSetup !== false) {
9764
+ reportSourceWidget(effectiveUuid, shellFramework, report);
9765
+ }
9766
+ const synced = effectiveUuid !== null && effectiveUuid.length > 0 && (response.stored || response.reason === "duplicate");
9767
+ const outcome = {
9768
+ connected,
9769
+ synced,
9770
+ // Only the dashboard can see the live site; a production scan is reported, not confirmed.
9771
+ deployed: false
9772
+ };
9773
+ options.onReported?.(outcome);
9774
+ if (options.report === void 0) {
9634
9775
  try {
9635
- const state = await collectGuideState(process.cwd());
9636
- const remaining = countRemainingSteps(state);
9637
- if (remaining > 0) {
9638
- const hooksMissing = !(state.prebuildWired && state.postbuildWired);
9639
- console.log("");
9640
- console.log(
9641
- `Setup not complete \u2014 ${remaining} step(s) remaining${hooksMissing ? ", including the package.json build hooks (which scan can't wire)" : ""}.`
9642
- );
9643
- console.log("Run `npx @patchstack/connect guide` for the exact steps to finish for this project.");
9644
- }
9776
+ await printScanReport(config, effectiveUuid, outcome, report, options.showRemainingSetup !== false);
9645
9777
  } catch {
9646
9778
  }
9647
9779
  }
9648
9780
  return 0;
9649
9781
  }
9650
- function reportSourceWidget(siteUuid, framework) {
9782
+ function mergeMissing(...lists) {
9783
+ const merged = [];
9784
+ for (const item of lists.flat()) {
9785
+ const key = item.key ?? item.text;
9786
+ if (!merged.some((existing) => (existing.key ?? existing.text) === key)) merged.push(item);
9787
+ }
9788
+ return merged;
9789
+ }
9790
+ function scanNextStepContext(config, state, siteUuid) {
9791
+ const uuid = siteUuid !== null && siteUuid.length > 0 ? siteUuid : state.siteUuid;
9792
+ return {
9793
+ ...guideNextStepContext(state),
9794
+ environment: config.environment,
9795
+ environmentSource: config.environmentSource,
9796
+ siteUuid: uuid,
9797
+ claimUrl: uuid !== null && config.endpointTrusted !== false ? buildClaimUrl(config.endpoint, uuid) : null
9798
+ };
9799
+ }
9800
+ async function printScanReport(config, siteUuid, outcome, report, withProgress) {
9801
+ const state = await collectGuideState(process.cwd());
9802
+ const context = scanNextStepContext(config, state, siteUuid);
9803
+ const progress = guideProgress(state, outcome);
9804
+ if ((isInstallOrBuildHook() || runningInCi()) && !verbose) {
9805
+ for (const item of report.missing) console.warn(`Patchstack: ${[item.text + ".", ...item.hint ?? []].join(" ")}`);
9806
+ for (const line of renderHookSummary(report, progress, context)) console.log(line);
9807
+ return;
9808
+ }
9809
+ if (verbose) console.log("");
9810
+ const missing = mergeMissing(report.missing, withProgress ? guideMissing(state, outcome) : []);
9811
+ for (const line of renderStatus("Patchstack scan", { done: report.done, missing }, progress, context, {
9812
+ useColor: useColor(),
9813
+ verbose,
9814
+ withoutProgress: !withProgress
9815
+ })) {
9816
+ console.log(line);
9817
+ }
9818
+ }
9819
+ function reportSourceWidget(siteUuid, framework, report) {
9820
+ const handAdd = (why) => {
9821
+ report.missing.push({
9822
+ text: "The Patchstack widget is not on your page yet",
9823
+ hint: [why, ` ${buildWidgetTag(siteUuid)}`]
9824
+ });
9825
+ };
9651
9826
  try {
9652
9827
  const hint = hasJsxShell(framework) ? resolveWidgetFileHint(process.cwd(), framework) : null;
9653
9828
  const jsxShell = hint !== null && !hint.toLowerCase().endsWith(".html") ? hint : null;
9654
9829
  const result = ensureSourceWidget(process.cwd(), siteUuid, jsxShell);
9655
9830
  switch (result.action) {
9656
9831
  case "added":
9657
- console.log(`Widget: added the disclosure widget tag to ${result.shell}. Reload your preview to see it.`);
9658
- console.log(' Unclaimed, it shows a "Connect this website" panel; the "Report a vulnerability" button replaces it once the site is claimed.');
9832
+ report.done.push(`Added the Patchstack widget to ${result.shell}`);
9659
9833
  break;
9660
9834
  case "updated":
9661
- console.log(`Widget: updated the managed tag in ${result.shell} to site ${siteUuid}.`);
9835
+ report.done.push(`Updated the Patchstack widget in ${result.shell}`);
9662
9836
  break;
9663
9837
  case "unchanged":
9664
- console.log(`Widget: already installed in ${result.shell}.`);
9838
+ report.done.push(`The Patchstack widget is already in ${result.shell}`);
9665
9839
  break;
9666
9840
  case "manual":
9667
- console.log(`Widget: found an existing (manual) install in ${result.shell} \u2014 left untouched.`);
9841
+ report.done.push(`Left your own Patchstack widget in ${result.shell} as it is`);
9668
9842
  break;
9669
9843
  case "no-body":
9670
- console.log(`Widget: ${result.shell} has no </body> tag to anchor on. Add this tag to your root layout manually:`);
9671
- console.log(` ${buildWidgetTag(siteUuid)}`);
9844
+ handAdd(`${result.shell} has no </body>. Add this to your main page layout, just before </body>:`);
9672
9845
  break;
9673
9846
  case "no-shell":
9674
- console.log("Widget: no root shell found to edit (index.html / public/index.html / src/app.html, or a JSX root).");
9675
- console.log("Add this tag to your root layout before </body> (run `guide` for framework-specific placement):");
9676
- console.log(` ${buildWidgetTag(siteUuid)}`);
9847
+ handAdd("Add this to your main page layout, just before </body>:");
9677
9848
  break;
9678
9849
  }
9679
- if (result.action === "added" || result.action === "updated") {
9680
- console.log(' (opt out any time with "widget": false in .patchstackrc.json)');
9681
- }
9682
9850
  } catch (err) {
9683
- console.warn(`Widget: skipped (${err.message}).`);
9851
+ handAdd("Add this to your main page layout, just before </body>:");
9852
+ detail(`Widget: skipped (${err.message}).`);
9684
9853
  }
9685
9854
  }
9686
- function reportSourceMarker(framework, checksum = null) {
9855
+ function reportSourceMarker(framework, checksum, report) {
9687
9856
  try {
9688
9857
  const shell = resolveWidgetFileHint(process.cwd(), framework);
9689
9858
  if (shell === null || shell.toLowerCase().endsWith(".html")) {
@@ -9692,30 +9861,25 @@ function reportSourceMarker(framework, checksum = null) {
9692
9861
  const result = ensureSourceMarker(process.cwd(), shell, framework, checksum);
9693
9862
  switch (result.action) {
9694
9863
  case "added":
9695
- console.log(`Production marker: added to ${shell} (guarded by ${productionGate(framework)}).`);
9696
- console.log(" This root is server-rendered, so the marker ships in source rather than built HTML.");
9864
+ report.done.push(`Set up ${shell} to tell Patchstack when it runs as your live app`);
9865
+ detail(`Production marker: added to ${shell} (only set when ${productionGate(framework)}).`);
9697
9866
  break;
9698
9867
  case "manual":
9699
- console.log(`Production marker: already set in ${shell} \u2014 left untouched.`);
9868
+ detail(`Production marker: already in ${shell}.`);
9700
9869
  break;
9701
9870
  case "no-anchor":
9702
9871
  case "unsupported":
9703
- console.log(
9704
- `Production marker: ${shell} needs it by hand \u2014 without it the widget reads the published site as build mode.`
9705
- );
9706
- if (hasJsxShell(framework)) {
9707
- for (const line of buildSourceMarkerSnippet(framework).split("\n")) {
9708
- console.log(` ${line}`);
9709
- }
9710
- } else {
9711
- console.log(
9712
- ` Emit <script>window.__PATCHSTACK_PROD__=true;</script> only when ${productionGate(framework)}.`
9713
- );
9714
- }
9872
+ report.missing.push({
9873
+ text: "Your live app does not tell Patchstack it is live yet",
9874
+ hint: hasJsxShell(framework) ? [`Add this inside <head> in ${shell}:`, ...buildSourceMarkerSnippet(framework).split("\n").map((line) => ` ${line}`)] : [
9875
+ `Add this inside <head> in ${shell}, only when ${productionGate(framework)}:`,
9876
+ " <script>window.__PATCHSTACK_PROD__=true;</script>"
9877
+ ]
9878
+ });
9715
9879
  break;
9716
9880
  }
9717
9881
  } catch (err) {
9718
- console.warn(`Production marker: skipped (${err.message}).`);
9882
+ detail(`Production marker: skipped (${err.message}).`);
9719
9883
  }
9720
9884
  }
9721
9885
  async function runProtectCommand(args) {
@@ -9855,8 +10019,7 @@ async function runGuide(args) {
9855
10019
  let allDone = false;
9856
10020
  try {
9857
10021
  const state = await collectGuideState(process.cwd());
9858
- const useColor = process.stdout.isTTY === true && process.env.NO_COLOR === void 0;
9859
- console.log(renderGuideChecklist(state, useColor));
10022
+ console.log(renderGuideChecklist(state, useColor(), {}, { verbose }));
9860
10023
  allDone = countRemainingSteps(state) === 0;
9861
10024
  } catch {
9862
10025
  }
@@ -9874,96 +10037,85 @@ async function runGuide(args) {
9874
10037
  console.log(readFileSync19(guidePath, "utf8"));
9875
10038
  return 0;
9876
10039
  }
10040
+ function reportProtection(protection, report) {
10041
+ for (const line of protection.log) detail(`patchstack protect: ${line}`);
10042
+ if (protection.install.status === "not-applicable") {
10043
+ report.done.push("Runtime protection is not needed: this project has no server");
10044
+ if (protection.install.leftovers.length > 0) {
10045
+ report.missing.push({
10046
+ text: "Files from an earlier Patchstack protection setup are not used here",
10047
+ hint: [`You can delete: ${protection.install.leftovers.join(", ")}`]
10048
+ });
10049
+ }
10050
+ return;
10051
+ }
10052
+ if (protection.verification.wired) {
10053
+ const changed = protection.install.status === "wired" && protection.install.changed.length > 0;
10054
+ report.done.push(
10055
+ changed ? `Added runtime protection (${protection.verification.stack})` : `Runtime protection is already set up (${protection.verification.stack})`
10056
+ );
10057
+ return;
10058
+ }
10059
+ if (protection.install.status === "scaffolded" && protection.install.plan.includes("Could not locate a server entry")) {
10060
+ report.missing.push({
10061
+ key: "runtime-protection",
10062
+ text: "Runtime protection: no server file found",
10063
+ hint: ["Add Patchstack where requests enter your app. Run npx @patchstack/connect protect for the steps."]
10064
+ });
10065
+ }
10066
+ }
10067
+ function buildStepsLine(wired) {
10068
+ if (wired.strategy === "postinstall-only") {
10069
+ return wired.changed ? "Added a package check after each install to package.json" : "The package check after each install is already in package.json";
10070
+ }
10071
+ return wired.changed ? "Added the build steps to package.json" : "The build steps are already in package.json";
10072
+ }
9877
10073
  async function runSetup(args) {
9878
10074
  if (args.flags.get("dry-run") === true) {
9879
- console.error("Error: setup does not support --dry-run. Use `scan --dry-run` to preview the manifest.");
10075
+ console.error("Setup cannot run as a preview. To see what would be sent, run: npx @patchstack/connect scan --dry-run");
9880
10076
  return 1;
9881
10077
  }
9882
10078
  const before = await collectGuideState(process.cwd());
9883
10079
  if (!before.hasPackageJson) {
9884
- console.error("Error: no package.json found. Run setup from the project root.");
10080
+ console.error("No package.json here. Run setup from the folder that has your package.json.");
9885
10081
  console.error("For a standalone HTML site, use the widget-only instructions in AGENT-INSTALL.md; do not create a Node project just to run setup.");
9886
10082
  return 1;
9887
10083
  }
9888
10084
  if (before.installed === null) {
9889
10085
  console.error(
9890
- `Error: @patchstack/connect is not declared in package.json.
10086
+ `The Patchstack connector is not installed in this project yet.
9891
10087
  Run: ${installCommand(before.packageManager)}`
9892
10088
  );
9893
10089
  return 1;
9894
10090
  }
9895
- console.log("Patchstack setup \u2014 applying bounded project changes");
9896
- console.log(" 1. Scan dependencies, provision/reuse the site, and manage the source widget");
9897
- const scanCode = await runScan(args, { showRemainingSetup: false });
10091
+ const report = emptyReport();
10092
+ let reported = {};
10093
+ const scanCode = await runScan(args, {
10094
+ report,
10095
+ onReported: (outcome) => {
10096
+ reported = outcome;
10097
+ }
10098
+ });
9898
10099
  if (scanCode !== 0) {
9899
10100
  return scanCode;
9900
10101
  }
9901
- console.log("");
9902
- console.log(" 2. Install and verify runtime protection");
9903
- const protection = setupProtection(process.cwd());
9904
- if (protection.install.status === "not-applicable") {
9905
- console.log("Runtime protection: not applicable to this project \u2014 nothing installed.");
9906
- console.log(` ${protection.install.reason}`);
9907
- if (protection.install.leftovers.length > 0) {
9908
- console.log(" An earlier run scaffolded a guard here before that was established. These files do");
9909
- console.log(" nothing on this project and can be deleted:");
9910
- for (const file of protection.install.leftovers) console.log(` ${file}`);
9911
- }
9912
- } else if (protection.verification.wired) {
9913
- console.log(`Runtime protection: wired (${protection.verification.stack}).`);
9914
- } else {
9915
- console.log(`Runtime protection: manual wiring remains (${protection.verification.stack}):`);
9916
- for (const check of protection.verification.checks) {
9917
- console.log(` ${check.ok ? "\u2713" : "\u2717"} ${check.label}${!check.ok && check.hint ? ` \u2014 ${check.hint}` : ""}`);
9918
- }
9919
- console.log("Run `npx @patchstack/connect protect --check` after completing the failed checks.");
9920
- }
9921
- console.log("");
9922
- console.log(" 3. Wire dependency-install and production-build scans into package.json");
10102
+ reportProtection(setupProtection(process.cwd()), report);
9923
10103
  const wired = wireBuildScripts(process.cwd(), before.packageManager);
9924
- console.log(`Build integration: ${wired.detail}`);
9925
- console.log("");
9926
- console.log(" 4. Verify setup status");
10104
+ report.done.push(buildStepsLine(wired));
10105
+ detail(`Build hooks: ${wired.detail}`);
10106
+ const config = await resolveCliConfig(args);
9927
10107
  const after = await collectGuideState(process.cwd());
9928
- const useColor = process.stdout.isTTY === true && process.env.NO_COLOR === void 0;
9929
- console.log(renderGuideChecklist(after, useColor));
9930
- const remaining = countRemainingSteps(after);
9931
- if (remaining > 0) {
9932
- console.log("");
9933
- console.log(`Setup applied its bounded changes; ${remaining} manual step(s) remain above.`);
10108
+ const missing = mergeMissing(report.missing, guideMissing(after, reported));
10109
+ const context = scanNextStepContext(config, after, after.siteUuid);
10110
+ if (verbose) console.log("");
10111
+ for (const line of renderStatus("Patchstack setup", { done: report.done, missing }, guideProgress(after, reported), context, {
10112
+ useColor: useColor(),
10113
+ verbose
10114
+ })) {
10115
+ console.log(line);
9934
10116
  }
9935
- const outcome = setupOutcome(after, protection, wired);
9936
- console.log("");
9937
- console.log("Outcome \u2014 relay this to the user as it is:");
9938
- for (const [label, value] of outcome) console.log(` ${label}: ${value}`);
9939
10117
  return 0;
9940
10118
  }
9941
- function setupOutcome(state, protection, wired) {
9942
- const remaining = countRemainingSteps(state);
9943
- const lines = [];
9944
- lines.push(["Status", remaining === 0 ? "Ready to deploy (configured locally; nothing is live yet)" : `Configured locally; ${remaining} step(s) still to finish (see the checklist above)`]);
9945
- lines.push(["Monitoring", "starts with the first build that runs on your hosting platform"]);
9946
- if (protection.install.status === "not-applicable") {
9947
- lines.push(["Runtime protection", "not applicable \u2014 this project has no request path (static build)"]);
9948
- } else if (protection.verification.wired) {
9949
- lines.push(["Runtime protection", `wired (${protection.verification.stack}) \u2014 local wiring verified; verify against the live site after deploy with \`protect --check --runtime\``]);
9950
- } else {
9951
- lines.push(["Runtime protection", `not wired yet (${protection.verification.stack}) \u2014 finish the checks above, then \`npx @patchstack/connect protect --check\``]);
9952
- }
9953
- lines.push(["Next steps", [
9954
- "add PATCHSTACK_API_KEY (from .patchstackrc.local.json) to your hosting platform's environment variables",
9955
- "commit .patchstackrc.json, package.json and the widget change (never .patchstackrc.local.json)",
9956
- 'deploy, then check the site in the Patchstack dashboard \u2014 it reads "Deployed" once the live site is seen'
9957
- ].join("; ")]);
9958
- const warnings = [];
9959
- if (!wired.changed && wired.strategy === "postinstall-only") warnings.push("no build script, so only dependency installs are scanned");
9960
- if (protection.install.status === "not-applicable" && protection.install.leftovers.length > 0) {
9961
- warnings.push(`earlier guard scaffold does nothing here and can be deleted: ${protection.install.leftovers.join(", ")}`);
9962
- }
9963
- if (state.claimUrl !== null) warnings.push('the site is not attached to an account until someone signs in through the widget\'s "Connect this website" panel, the dashboard link is opened, or `npx @patchstack/connect claim` completes');
9964
- if (warnings.length > 0) lines.push(["Warnings", warnings.join("; ")]);
9965
- return lines;
9966
- }
9967
10119
  async function runStatus(args) {
9968
10120
  const config = await resolveCliConfig(args);
9969
10121
  console.log(`Site UUID: ${config.siteUuid ?? "(none yet \u2014 the next `scan` will provision one)"}`);
@@ -9974,24 +10126,14 @@ async function runStatus(args) {
9974
10126
  console.log(`Environment: ${config.environment}`);
9975
10127
  if (config.siteUuid !== null) {
9976
10128
  console.log(`Dashboard URL: ${buildClaimUrl(config.endpoint, config.siteUuid)}`);
9977
- console.log(` Not attached to an account yet? Sign in through the widget's "Connect this`);
9978
- console.log(' website" panel on the preview, open the link above, or run');
9979
- console.log(" `npx @patchstack/connect claim`.");
10129
+ console.log(" Not connected yet? Open the link above or run `npx @patchstack/connect claim`.");
9980
10130
  switch (await fetchSiteStatus(config)) {
9981
10131
  case "active":
9982
10132
  console.log("Site status: active on Patchstack");
9983
10133
  break;
9984
10134
  case "removed":
9985
10135
  console.log("Site status: removed from Patchstack");
9986
- console.log(
9987
- " The site record no longer exists (deleted from the dashboard or via the"
9988
- );
9989
- console.log(
9990
- " widget uninstall flow). The local integration files are still in this"
9991
- );
9992
- console.log(
9993
- ' project \u2014 see "Uninstalling" in AGENT-INSTALL.md to remove them.'
9994
- );
10136
+ console.log(' The local files are still here. See "Uninstalling" in AGENT-INSTALL.md to remove them.');
9995
10137
  break;
9996
10138
  case "unknown":
9997
10139
  console.log("Site status: could not be verified (Patchstack unreachable)");
@@ -10026,8 +10168,7 @@ async function runUninstall(args) {
10026
10168
  break;
10027
10169
  }
10028
10170
  console.log("");
10029
- console.log("This command only signals Patchstack. The local integration files must still be");
10030
- console.log('removed \u2014 follow the "Uninstalling" steps in AGENT-INSTALL.md.');
10171
+ console.log('Local files are not touched. Remove them with the "Uninstalling" steps in AGENT-INSTALL.md.');
10031
10172
  return 0;
10032
10173
  }
10033
10174
  function describeStack(stack) {
@@ -10060,7 +10201,7 @@ async function reportBuildStamp(config, payload, marker) {
10060
10201
  await postManifestWithEnvironmentFallback(bounded, payload, marker);
10061
10202
  } catch (err) {
10062
10203
  console.warn(
10063
- `mark-build: this build was not reported to Patchstack (${err.message}). The pages were still marked as described above.`
10204
+ `mark-build: could not tell Patchstack about this build (${err.message}). The pages were still marked.`
10064
10205
  );
10065
10206
  }
10066
10207
  }
@@ -10122,22 +10263,14 @@ async function runMarkBuild(args) {
10122
10263
  const files = findHtmlFiles(dir);
10123
10264
  if (files.length === 0) {
10124
10265
  console.warn(`mark-build: found ${dir} but no HTML files in it.`);
10125
- console.warn(
10126
- "mark-build: this build looks server-rendered, so the production flag has nothing to stamp."
10127
- );
10128
- console.warn(
10129
- "mark-build: add the marker to your root shell instead \u2014 run `patchstack-connect guide` for the snippet."
10130
- );
10266
+ console.warn("mark-build: this looks like a server-rendered app. Run `npx @patchstack/connect guide` to see what your main layout needs.");
10131
10267
  await reportBuildStamp(reported, wirePayload, "no-pages");
10132
10268
  return 0;
10133
10269
  }
10134
10270
  const staleBy = staleBuildAge(files, startedAt);
10135
10271
  if (staleBy !== null) {
10136
10272
  console.warn(
10137
- `mark-build: nothing in ${dir} has been written in the last ${Math.round(staleBy / 6e4)} minute(s), so this looks like output from an earlier build.`
10138
- );
10139
- console.warn(
10140
- "mark-build: run it as a postbuild hook (or straight after the build) so what is stamped is what was just produced."
10273
+ `mark-build: nothing in ${dir} changed in the last ${Math.round(staleBy / 6e4)} minute(s). Run it right after the build (as postbuild).`
10141
10274
  );
10142
10275
  }
10143
10276
  const snippet = published ? buildInjectionSnippet(checksum, stack) : "";
@@ -10160,7 +10293,8 @@ async function runMarkBuild(args) {
10160
10293
  }
10161
10294
  const stackSummary = stack !== null ? describeStack(stack) : null;
10162
10295
  if (published) {
10163
- console.log(
10296
+ console.log(`Patchstack: marked ${plural(files.length, "page", "pages")} as your live app.`);
10297
+ detail(
10164
10298
  `mark-build: marked ${marked} HTML file(s) in ${dir}${checksum !== null ? ` (build ${checksum})` : ""}${widgetTouched > 0 ? `, widget tag ensured in ${widgetTouched}` : ""}${stackSummary !== null ? ` [${stackSummary}]` : ""}.`
10165
10299
  );
10166
10300
  await reportBuildStamp(reported, wirePayload, "stamped");
@@ -10168,13 +10302,10 @@ async function runMarkBuild(args) {
10168
10302
  }
10169
10303
  const because = environmentEvidence.length > 0 ? ` (${environmentEvidence.join("; ")})` : "";
10170
10304
  console.log(
10171
- `mark-build: ${environment} build${because} \u2014 production marker withheld from ${files.length} HTML file(s) in ${dir}${widgetTouched > 0 ? `, widget tag ensured in ${widgetTouched}` : ""}${stackSummary !== null ? ` [${stackSummary}]` : ""}.`
10172
- );
10173
- console.log(
10174
- "mark-build: the marker tells Patchstack a page is the live site, so only the build that deploys carries it. Your hosting platform's build stamps it automatically."
10305
+ `Patchstack: ${environment} build, so ${plural(files.length, "page is", "pages are")} not marked as your live app. Publishing this folder by hand? Run again with --production.`
10175
10306
  );
10176
- console.log(
10177
- "mark-build: publishing this directory by hand? Re-run with --production (or set PATCHSTACK_ENVIRONMENT=production) so the deployed pages carry it."
10307
+ detail(
10308
+ `mark-build: ${environment} build${because}, so no production marker in ${files.length} HTML file(s) in ${dir}${widgetTouched > 0 ? `, widget tag ensured in ${widgetTouched}` : ""}${stackSummary !== null ? ` [${stackSummary}]` : ""}.`
10178
10309
  );
10179
10310
  await reportBuildStamp(reported, wirePayload, "withheld");
10180
10311
  return 0;
@@ -10188,8 +10319,28 @@ function packageVersion() {
10188
10319
  return "unknown";
10189
10320
  }
10190
10321
  }
10322
+ function plainErrorMessage(err) {
10323
+ switch (err.code) {
10324
+ case "NETWORK_ERROR":
10325
+ return "Could not reach Patchstack. Check your internet connection and try again.";
10326
+ case "NETWORK_TIMEOUT":
10327
+ return "Patchstack took too long to answer. Try again in a minute.";
10328
+ case "SERVER_ERROR":
10329
+ return "Patchstack could not handle the request right now. Try again in a few minutes.";
10330
+ case "LOCKFILE_NOT_FOUND":
10331
+ return "Could not find the list of installed packages. Install your packages first (for example npm install), then try again.";
10332
+ case "LOCKFILE_PARSE_ERROR":
10333
+ case "LOCKFILE_UNSUPPORTED":
10334
+ return "Could not read the list of installed packages. Reinstall your packages (for example npm install), then try again.";
10335
+ case "CONFIG_MISSING":
10336
+ return "This project is not set up with Patchstack yet. Run npx @patchstack/connect setup first.";
10337
+ default:
10338
+ return null;
10339
+ }
10340
+ }
10191
10341
  async function main() {
10192
10342
  const args = parseArgs(process.argv);
10343
+ verbose = args.flags.get("verbose") === true;
10193
10344
  if (args.flags.has("version") || args.command === "version") {
10194
10345
  console.log(packageVersion());
10195
10346
  return 0;
@@ -10234,7 +10385,9 @@ async function main() {
10234
10385
  }
10235
10386
  main().then((code) => process.exit(code)).catch((err) => {
10236
10387
  if (err instanceof PatchstackError) {
10237
- console.error(`Error (${err.code}): ${err.message}`);
10388
+ const plain = plainErrorMessage(err);
10389
+ console.error(`${plain ?? err.message} (${err.code})`);
10390
+ if (plain !== null && verbose) console.error(`Detail: ${err.message}`);
10238
10391
  process.exit(1);
10239
10392
  }
10240
10393
  console.error("Unexpected error:", err);