@patchstack/connect 0.5.11 → 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
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { readFileSync as readFileSync19 } from "fs";
5
- import path17 from "path";
5
+ import path16 from "path";
6
6
  import { createRequire as createRequire2 } from "module";
7
7
 
8
8
  // src/safe-file.ts
@@ -25,9 +25,9 @@ import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
25
25
  function pathError(message) {
26
26
  return Object.assign(new Error(message), { code: "EINVAL" });
27
27
  }
28
- function existingEntry(path18) {
28
+ function existingEntry(path17) {
29
29
  try {
30
- return lstatSync(path18);
30
+ return lstatSync(path17);
31
31
  } catch (error) {
32
32
  if (error.code === "ENOENT") return null;
33
33
  throw error;
@@ -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(
@@ -1488,8 +1495,8 @@ async function readBoundedJson(response, maxBytes = MAX_CONTROL_RESPONSE_BYTES)
1488
1495
  var TOKEN_SKEW_MS = 6e4;
1489
1496
  function buildTokenUrl(manifestEndpoint) {
1490
1497
  const url = new URL(manifestEndpoint);
1491
- const path18 = url.pathname.replace(/\/$/, "");
1492
- url.pathname = path18.endsWith("/manifest") ? `${path18.slice(0, -"/manifest".length)}/token` : "/monitor/pulse/token";
1498
+ const path17 = url.pathname.replace(/\/$/, "");
1499
+ url.pathname = path17.endsWith("/manifest") ? `${path17.slice(0, -"/manifest".length)}/token` : "/monitor/pulse/token";
1493
1500
  url.search = "";
1494
1501
  url.hash = "";
1495
1502
  return url.toString();
@@ -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,20 +1659,20 @@ 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
  }
1658
1672
  function buildRulesUrl(manifestEndpoint, siteUuid) {
1659
1673
  const url = new URL(manifestEndpoint);
1660
- const path18 = url.pathname.replace(/\/$/, "");
1661
- url.pathname = path18.endsWith("/manifest") ? `${path18.slice(0, -"/manifest".length)}/rules/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/rules/${encodeURIComponent(siteUuid)}`;
1674
+ const path17 = url.pathname.replace(/\/$/, "");
1675
+ url.pathname = path17.endsWith("/manifest") ? `${path17.slice(0, -"/manifest".length)}/rules/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/rules/${encodeURIComponent(siteUuid)}`;
1662
1676
  url.search = "";
1663
1677
  url.hash = "";
1664
1678
  return url.toString();
@@ -1673,16 +1687,16 @@ function buildSettingsUrl(endpoint, siteUuid) {
1673
1687
  }
1674
1688
  function buildPackageRemovedUrl(manifestEndpoint, siteUuid) {
1675
1689
  const url = new URL(manifestEndpoint);
1676
- const path18 = url.pathname.replace(/\/$/, "");
1677
- url.pathname = path18.endsWith("/manifest") ? `${path18.slice(0, -"/manifest".length)}/package-removed/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/package-removed/${encodeURIComponent(siteUuid)}`;
1690
+ const path17 = url.pathname.replace(/\/$/, "");
1691
+ url.pathname = path17.endsWith("/manifest") ? `${path17.slice(0, -"/manifest".length)}/package-removed/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/package-removed/${encodeURIComponent(siteUuid)}`;
1678
1692
  url.search = "";
1679
1693
  url.hash = "";
1680
1694
  return url.toString();
1681
1695
  }
1682
1696
  function buildInputMapUrl(manifestEndpoint, siteUuid) {
1683
1697
  const url = new URL(manifestEndpoint);
1684
- const path18 = url.pathname.replace(/\/$/, "");
1685
- url.pathname = path18.endsWith("/manifest") ? `${path18.slice(0, -"/manifest".length)}/input-map/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/input-map/${encodeURIComponent(siteUuid)}`;
1698
+ const path17 = url.pathname.replace(/\/$/, "");
1699
+ url.pathname = path17.endsWith("/manifest") ? `${path17.slice(0, -"/manifest".length)}/input-map/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/input-map/${encodeURIComponent(siteUuid)}`;
1686
1700
  url.search = "";
1687
1701
  url.hash = "";
1688
1702
  return url.toString();
@@ -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.",
@@ -2336,6 +2350,7 @@ function isUnpublishableHost(hostname) {
2336
2350
  if (!host.includes(".")) return true;
2337
2351
  return RESERVED_SUFFIXES.some((suffix) => host.endsWith(suffix));
2338
2352
  }
2353
+ var URL_MAX_LENGTH = 191;
2339
2354
  function normaliseSiteUrl(value) {
2340
2355
  const trimmed = (value ?? "").trim();
2341
2356
  if (trimmed === "") return null;
@@ -2349,7 +2364,7 @@ function normaliseSiteUrl(value) {
2349
2364
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
2350
2365
  if (parsed.hostname === "" || isUnpublishableHost(parsed.hostname)) return null;
2351
2366
  const origin = `${parsed.protocol}//${parsed.host}`;
2352
- return origin.length <= 191 ? origin : null;
2367
+ return origin.length <= URL_MAX_LENGTH ? origin : null;
2353
2368
  }
2354
2369
  var PRODUCTION_URL_SOURCES = [
2355
2370
  {
@@ -2678,10 +2693,10 @@ async function persistTimeout(cwd, timeoutMs) {
2678
2693
  return writeConfigFile(cwd, { ...existing, timeoutMs });
2679
2694
  }
2680
2695
  async function persistApiKey(cwd, apiKey) {
2681
- const { apiKey: _movedKey, pulseAuth: _movedPulse, ...publicConfig } = await readConfigFile(cwd);
2696
+ const { apiKey: movedKey, pulseAuth: movedPulse, ...publicConfig } = await readConfigFile(cwd);
2682
2697
  const existingSecrets = await readSecretFile(cwd);
2683
2698
  const target = await writeSecretFile(cwd, { ...existingSecrets, apiKey, pulseAuth: void 0 });
2684
- if (_movedKey !== void 0 || _movedPulse !== void 0) {
2699
+ if (movedKey !== void 0 || movedPulse !== void 0) {
2685
2700
  await writeConfigFile(cwd, publicConfig);
2686
2701
  }
2687
2702
  return target;
@@ -3460,10 +3475,10 @@ function detectDeploymentShapes(cwd, opts = {}) {
3460
3475
  boundary = realpathSync3(boundary);
3461
3476
  } catch {
3462
3477
  }
3463
- const inProject = (path18) => {
3478
+ const inProject = (path17) => {
3464
3479
  if (opts.followOutside) return true;
3465
3480
  try {
3466
- return isInside(realpathSync3(path18), boundary);
3481
+ return isInside(realpathSync3(path17), boundary);
3467
3482
  } catch {
3468
3483
  return false;
3469
3484
  }
@@ -3824,7 +3839,7 @@ function classifyArchitecture(cwd) {
3824
3839
  return {
3825
3840
  requestPath: "none",
3826
3841
  evidence: statics.map((label) => `static build: ${label}`),
3827
- 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.`
3828
3843
  };
3829
3844
  }
3830
3845
  return {
@@ -3839,7 +3854,21 @@ import { readFileSync as readFileSync7, existsSync as existsSync3 } from "fs";
3839
3854
  import { join as join5, dirname as dirname2 } from "path";
3840
3855
  import { fileURLToPath } from "url";
3841
3856
  var read = (p) => readFileSync7(p, "utf8");
3842
- 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
+ }
3843
3872
  function hasDependency(cwd, name) {
3844
3873
  try {
3845
3874
  const pkg = JSON.parse(read(join5(cwd, "package.json")));
@@ -4100,14 +4129,6 @@ var tanstackSupabaseAdapter = {
4100
4129
  // src/protect/install/adapters/next.ts
4101
4130
  import { existsSync as existsSync5 } from "fs";
4102
4131
  import { join as join7 } from "path";
4103
- function hasNextDep(cwd) {
4104
- try {
4105
- const pkg = JSON.parse(read(join7(cwd, "package.json")));
4106
- return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.next);
4107
- } catch {
4108
- return false;
4109
- }
4110
- }
4111
4132
  function middlewareInfo(cwd) {
4112
4133
  const candidates = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"];
4113
4134
  for (const rel of candidates) {
@@ -4117,7 +4138,7 @@ function middlewareInfo(cwd) {
4117
4138
  return { relDir: useSrc ? "src" : ".", relFile: useSrc ? "src/middleware.ts" : "middleware.ts", exists: false };
4118
4139
  }
4119
4140
  function detect2(cwd) {
4120
- return hasNextDep(cwd);
4141
+ return hasDependency(cwd, "next");
4121
4142
  }
4122
4143
  function rulesFile(relDir) {
4123
4144
  return relDir === "." ? "patchstack.rules.json" : `${relDir}/patchstack.rules.json`;
@@ -4171,9 +4192,6 @@ var nextAdapter = {
4171
4192
  verify: verify2
4172
4193
  };
4173
4194
 
4174
- // src/protect/install/adapters/sveltekit.ts
4175
- import { join as join9 } from "path";
4176
-
4177
4195
  // src/protect/install/seam.ts
4178
4196
  import { existsSync as existsSync6 } from "fs";
4179
4197
  import { join as join8, dirname as dirname3 } from "path";
@@ -4231,12 +4249,7 @@ var SPEC = {
4231
4249
  seamLabel: "SvelteKit server hook"
4232
4250
  };
4233
4251
  function detect3(cwd) {
4234
- try {
4235
- const pkg = JSON.parse(read(join9(cwd, "package.json")));
4236
- return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }["@sveltejs/kit"]);
4237
- } catch {
4238
- return false;
4239
- }
4252
+ return hasDependency(cwd, "@sveltejs/kit");
4240
4253
  }
4241
4254
  function wire3(cwd, opts) {
4242
4255
  return wireSeam(cwd, opts, SPEC);
@@ -4253,7 +4266,6 @@ var sveltekitAdapter = {
4253
4266
  };
4254
4267
 
4255
4268
  // src/protect/install/adapters/astro.ts
4256
- import { join as join10 } from "path";
4257
4269
  var SPEC2 = {
4258
4270
  templateName: "astro-middleware.ts",
4259
4271
  candidates: ["src/middleware.ts", "src/middleware.js", "src/middleware/index.ts", "src/middleware/index.js"],
@@ -4263,12 +4275,7 @@ var SPEC2 = {
4263
4275
  seamLabel: "Astro middleware"
4264
4276
  };
4265
4277
  function detect4(cwd) {
4266
- try {
4267
- const pkg = JSON.parse(read(join10(cwd, "package.json")));
4268
- return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.astro);
4269
- } catch {
4270
- return false;
4271
- }
4278
+ return hasDependency(cwd, "astro");
4272
4279
  }
4273
4280
  function wire4(cwd, opts) {
4274
4281
  return wireSeam(cwd, opts, SPEC2);
@@ -4312,7 +4319,7 @@ var nuxtAdapter = {
4312
4319
 
4313
4320
  // src/protect/install/find-app.ts
4314
4321
  import { readFileSync as readFileSync8, readdirSync as readdirSync3, lstatSync as lstatSync2 } from "fs";
4315
- import { join as join11, relative as relative3, dirname as dirname4 } from "path";
4322
+ import { join as join9, relative as relative3, dirname as dirname4 } from "path";
4316
4323
  var SKIP_DIRS2 = /* @__PURE__ */ new Set([
4317
4324
  "node_modules",
4318
4325
  "patchstack",
@@ -4344,7 +4351,7 @@ function findAppInstance(cwd, re) {
4344
4351
  }
4345
4352
  for (const name of entries) {
4346
4353
  if (SKIP_DIRS2.has(name) || name.startsWith(".")) continue;
4347
- const p = join11(dir, name);
4354
+ const p = join9(dir, name);
4348
4355
  let st;
4349
4356
  try {
4350
4357
  st = lstatSync2(p);
@@ -4403,7 +4410,7 @@ function declaredEntries(cwd) {
4403
4410
  const paths = /* @__PURE__ */ new Set();
4404
4411
  let pkg;
4405
4412
  try {
4406
- pkg = JSON.parse(readFileSync8(join11(cwd, "package.json"), "utf8"));
4413
+ pkg = JSON.parse(readFileSync8(join9(cwd, "package.json"), "utf8"));
4407
4414
  } catch {
4408
4415
  return paths;
4409
4416
  }
@@ -4430,16 +4437,16 @@ function importSpecifier(fromRel, toRel, preserveExtension = false) {
4430
4437
 
4431
4438
  // src/protect/install/register.ts
4432
4439
  import { existsSync as existsSync9 } from "fs";
4433
- import { join as join14, resolve as resolve3 } from "path";
4440
+ import { join as join12, resolve as resolve3 } from "path";
4434
4441
 
4435
4442
  // src/protect/install/generic.ts
4436
4443
  import { readFileSync as readFileSync9, existsSync as existsSync8, readdirSync as readdirSync4, lstatSync as lstatSync3 } from "fs";
4437
- import { join as join13, dirname as dirname6 } from "path";
4444
+ import { join as join11, dirname as dirname6 } from "path";
4438
4445
 
4439
4446
  // src/protect/install/source-scope.ts
4440
4447
  import { execFileSync } from "child_process";
4441
4448
  import { existsSync as existsSync7 } from "fs";
4442
- import { dirname as dirname5, join as join12, resolve as resolve2 } from "path";
4449
+ import { dirname as dirname5, join as join10, resolve as resolve2 } from "path";
4443
4450
 
4444
4451
  // src/protect/install/node-flags.ts
4445
4452
  var flagName = (token) => token.split("=")[0];
@@ -4729,8 +4736,8 @@ function moduleKindOf(filePath, packageIsModule) {
4729
4736
  }
4730
4737
  function resolvesToGuardModule(specifier, query) {
4731
4738
  const resolved = resolve2(dirname5(query.fromFile), specifier);
4732
- if (withoutModuleExtension(resolved) !== join12(query.guardDir, "guard")) return false;
4733
- const existing = GUARD_FILENAMES.filter((name) => existsSync7(join12(query.guardDir, name)));
4739
+ if (withoutModuleExtension(resolved) !== join10(query.guardDir, "guard")) return false;
4740
+ const existing = GUARD_FILENAMES.filter((name) => existsSync7(join10(query.guardDir, name)));
4734
4741
  if (existing.length === 0) return false;
4735
4742
  const extension = /(\.[A-Za-z0-9]+)$/.exec(specifier)?.[1] ?? "";
4736
4743
  if (!query.strictExtensions) return true;
@@ -4749,16 +4756,16 @@ function withoutModuleExtension(filePath) {
4749
4756
  // src/protect/install/generic.ts
4750
4757
  function packageIsEsm(cwd) {
4751
4758
  try {
4752
- return JSON.parse(read(join13(cwd, "package.json"))).type === "module";
4759
+ return JSON.parse(read(join11(cwd, "package.json"))).type === "module";
4753
4760
  } catch {
4754
4761
  return false;
4755
4762
  }
4756
4763
  }
4757
4764
  function genericDir(cwd) {
4758
- return existsSync8(join13(cwd, "src")) ? "src/patchstack" : "patchstack";
4765
+ return existsSync8(join11(cwd, "src")) ? "src/patchstack" : "patchstack";
4759
4766
  }
4760
4767
  function hasTypeScript(cwd) {
4761
- return existsSync8(join13(cwd, "tsconfig.json")) || candidateEntries(cwd).some((entry) => /\.tsx?$/.test(entry));
4768
+ return existsSync8(join11(cwd, "tsconfig.json")) || candidateEntries(cwd).some((entry) => /\.tsx?$/.test(entry));
4762
4769
  }
4763
4770
  function genericGuardTarget(cwd) {
4764
4771
  if (hasTypeScript(cwd)) {
@@ -4772,7 +4779,7 @@ function genericGuardTarget(cwd) {
4772
4779
  function genericScaffoldFiles(cwd) {
4773
4780
  const dir = genericDir(cwd);
4774
4781
  const candidates = ["guard.ts", "guard.js", "guard.cjs", "rules.json"];
4775
- return candidates.map((file) => `${dir}/${file}`).filter((relative8) => existsSync8(join13(cwd, relative8)));
4782
+ return candidates.map((file) => `${dir}/${file}`).filter((relative8) => existsSync8(join11(cwd, relative8)));
4776
4783
  }
4777
4784
  function scaffoldGeneric(cwd, opts, guardTemplate, guardFile) {
4778
4785
  const target = genericGuardTarget(cwd);
@@ -4780,15 +4787,15 @@ function scaffoldGeneric(cwd, opts, guardTemplate, guardFile) {
4780
4787
  guardFile ??= target.file;
4781
4788
  const templates = templatesDir();
4782
4789
  const dir = genericDir(cwd);
4783
- const dst = join13(cwd, dir);
4790
+ const dst = join11(cwd, dir);
4784
4791
  ensureProjectDirectorySync(cwd, dst);
4785
- copyProjectFileSync(cwd, join13(templates, guardTemplate), join13(dst, guardFile));
4792
+ copyProjectFileSync(cwd, join11(templates, guardTemplate), join11(dst, guardFile));
4786
4793
  const guardRel = `${dir}/${guardFile}`;
4787
4794
  const changed = [guardRel];
4788
4795
  if (!opts.demo) bakeSiteUuid(cwd, guardRel);
4789
- const rulesDst = join13(dst, "rules.json");
4796
+ const rulesDst = join11(dst, "rules.json");
4790
4797
  if (opts.demo || !existsSync8(rulesDst)) {
4791
- copyProjectFileSync(cwd, join13(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
4798
+ copyProjectFileSync(cwd, join11(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
4792
4799
  changed.push(`${dir}/rules.json`);
4793
4800
  }
4794
4801
  return { changed, dir };
@@ -4809,25 +4816,17 @@ function candidateEntries(cwd) {
4809
4816
  "app.ts",
4810
4817
  "app.js"
4811
4818
  ];
4812
- const hits = names.filter((n) => existsSync8(join13(cwd, n)));
4819
+ const hits = names.filter((n) => existsSync8(join11(cwd, n)));
4813
4820
  try {
4814
- const pkg = JSON.parse(read(join13(cwd, "package.json")));
4815
- if (typeof pkg.main === "string" && existsSync8(join13(cwd, pkg.main))) hits.push(pkg.main);
4821
+ const pkg = JSON.parse(read(join11(cwd, "package.json")));
4822
+ if (typeof pkg.main === "string" && existsSync8(join11(cwd, pkg.main))) hits.push(pkg.main);
4816
4823
  } catch {
4817
4824
  }
4818
4825
  return [...new Set(hits)];
4819
4826
  }
4820
- function usesExpress(cwd) {
4821
- try {
4822
- const pkg = JSON.parse(read(join13(cwd, "package.json")));
4823
- return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.express);
4824
- } catch {
4825
- return false;
4826
- }
4827
- }
4828
4827
  function wiringPlan(cwd, dir) {
4829
4828
  const entries = candidateEntries(cwd);
4830
- const express = usesExpress(cwd);
4829
+ const express = hasDependency(cwd, "express");
4831
4830
  const target = genericGuardTarget(cwd);
4832
4831
  const lines = [
4833
4832
  `no built-in adapter matched this stack \u2014 scaffolded a generic guard at ${dir}/${target.file} + ${dir}/rules.json.`,
@@ -4923,9 +4922,9 @@ function escapeForRegExp(value) {
4923
4922
  function genericVerify(cwd) {
4924
4923
  const dir = genericDir(cwd);
4925
4924
  const target = genericGuardTarget(cwd);
4926
- const present2 = GUARD_FILENAMES.find((name) => existsSync8(join13(cwd, dir, name)));
4925
+ const present2 = GUARD_FILENAMES.find((name) => existsSync8(join11(cwd, dir, name)));
4927
4926
  const scaffolded = present2 !== void 0;
4928
- const imported = scaffolded && guardIsImported(cwd, join13(cwd, dir, present2));
4927
+ const imported = scaffolded && guardIsImported(cwd, join11(cwd, dir, present2));
4929
4928
  return {
4930
4929
  wired: scaffolded && imported,
4931
4930
  checks: [
@@ -4961,7 +4960,7 @@ var NON_RUNTIME_DIRS = /* @__PURE__ */ new Set([
4961
4960
  function guardIsImported(cwd, guardPath) {
4962
4961
  const guardDir = dirname6(guardPath);
4963
4962
  const packageIsModule = packageIsEsm(cwd);
4964
- const root = existsSync8(join13(cwd, "src")) ? join13(cwd, "src") : cwd;
4963
+ const root = existsSync8(join11(cwd, "src")) ? join11(cwd, "src") : cwd;
4965
4964
  let found = false;
4966
4965
  const walk2 = (d, depth) => {
4967
4966
  if (found || depth > 8) return;
@@ -4974,7 +4973,7 @@ function guardIsImported(cwd, guardPath) {
4974
4973
  for (const name of entries) {
4975
4974
  if (found) return;
4976
4975
  if (NON_RUNTIME_DIRS.has(name) || name.startsWith(".")) continue;
4977
- const p = join13(d, name);
4976
+ const p = join11(d, name);
4978
4977
  let st;
4979
4978
  try {
4980
4979
  st = lstatSync3(p);
@@ -5033,7 +5032,7 @@ function guardTarget(cwd, entryRel, spec) {
5033
5032
  }
5034
5033
  function packageType(cwd) {
5035
5034
  try {
5036
- return JSON.parse(read(join14(cwd, "package.json"))).type;
5035
+ return JSON.parse(read(join12(cwd, "package.json"))).type;
5037
5036
  } catch {
5038
5037
  return void 0;
5039
5038
  }
@@ -5069,8 +5068,8 @@ function bindsGuard(line, maskedLine, importName, call, fileRel, guard) {
5069
5068
  }
5070
5069
  function namesGuardModule(specifier, fileRel, guard) {
5071
5070
  return resolvesToGuardModule(specifier, {
5072
- fromFile: join14(guard.cwd, fileRel),
5073
- guardDir: resolve3(join14(guard.cwd, guard.guardDir)),
5071
+ fromFile: join12(guard.cwd, fileRel),
5072
+ guardDir: resolve3(join12(guard.cwd, guard.guardDir)),
5074
5073
  kind: moduleKindOf(fileRel, packageType(guard.cwd) === "module"),
5075
5074
  strictExtensions: true
5076
5075
  });
@@ -5089,7 +5088,7 @@ function wireRegister(cwd, opts, spec) {
5089
5088
  }
5090
5089
  const target = guardTarget(cwd, entry.relPath, spec);
5091
5090
  const { changed, dir } = scaffoldGeneric(cwd, opts, target.template, target.file);
5092
- const p = join14(cwd, entry.relPath);
5091
+ const p = join12(cwd, entry.relPath);
5093
5092
  const s = read(p);
5094
5093
  const state = wiringState(s, spec, entry.appVar, entry.relPath, { cwd, guardDir: dir });
5095
5094
  if (state.importAtTopLevel && state.callInAppScope) {
@@ -5159,17 +5158,17 @@ function firstStatementLine(lines) {
5159
5158
  return i;
5160
5159
  }
5161
5160
  function serverIsGuarded(cwd, spec, server, guardDir) {
5162
- const source = read(join14(cwd, server.relPath));
5161
+ const source = read(join12(cwd, server.relPath));
5163
5162
  const state = wiringState(source, spec, server.appVar, server.relPath, { cwd, guardDir });
5164
5163
  if (!state.importAtTopLevel || !state.callInAppScope || !state.ordered) return false;
5165
5164
  return !spec.callAfter || routesBefore(source, server.appVar, state.callIndex).length === 0;
5166
5165
  }
5167
5166
  function verifyRegister(cwd, spec) {
5168
- const dir = existsSync9(join14(cwd, "src")) ? "src/patchstack" : "patchstack";
5167
+ const dir = existsSync9(join12(cwd, "src")) ? "src/patchstack" : "patchstack";
5169
5168
  const entry = findAppInstance(cwd, spec.appRe);
5170
5169
  const target = entry ? guardTarget(cwd, entry.relPath, spec) : null;
5171
- const scaffolded = target ? existsSync9(join14(cwd, dir, target.file)) : false;
5172
- const entrySource = entry ? read(join14(cwd, entry.relPath)) : "";
5170
+ const scaffolded = target ? existsSync9(join12(cwd, dir, target.file)) : false;
5171
+ const entrySource = entry ? read(join12(cwd, entry.relPath)) : "";
5173
5172
  const state = entry ? wiringState(entrySource, spec, entry.appVar, entry.relPath, { cwd, guardDir: dir }) : { importAtTopLevel: false, callInAppScope: false, ordered: false, callIndex: -1, appIndex: -1 };
5174
5173
  const wired = state.importAtTopLevel && state.callInAppScope && state.ordered;
5175
5174
  const early = spec.callAfter && entry ? routesBefore(entrySource, entry.appVar, state.callIndex) : [];
@@ -5260,7 +5259,7 @@ var expressAdapter = {
5260
5259
 
5261
5260
  // src/protect/install/reporting.ts
5262
5261
  import { existsSync as existsSync10 } from "fs";
5263
- import { join as join15 } from "path";
5262
+ import { join as join13 } from "path";
5264
5263
 
5265
5264
  // src/protect/reporting-state.js
5266
5265
  var REPORTING_STATES = Object.freeze([
@@ -5318,7 +5317,7 @@ function explainReportingState(state) {
5318
5317
 
5319
5318
  // src/protect/install/reporting.ts
5320
5319
  function siteIdentity(cwd, env) {
5321
- const rc = join15(cwd, ".patchstackrc.json");
5320
+ const rc = join13(cwd, ".patchstackrc.json");
5322
5321
  let recorded;
5323
5322
  if (existsSync10(rc)) {
5324
5323
  try {
@@ -5531,6 +5530,147 @@ function ensureSourceWidget(cwd, siteUuid, jsxShell = null) {
5531
5530
  return { shell, action };
5532
5531
  }
5533
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
+
5534
5674
  // src/guide.ts
5535
5675
  var PROD_MARKER_NEEDLE = "__PATCHSTACK_PROD__";
5536
5676
  var WIDGET_NEEDLE2 = "patchstack-widget";
@@ -5727,8 +5867,12 @@ async function collectGuideState(cwd) {
5727
5867
  let claimUrl = null;
5728
5868
  let endpointOverride = null;
5729
5869
  let widgetOptOut = false;
5870
+ let environment = null;
5871
+ let environmentSource = null;
5730
5872
  try {
5731
5873
  const config = await resolveConfig({ cwd });
5874
+ environment = config.environment;
5875
+ environmentSource = config.environmentSource ?? null;
5732
5876
  siteUuid = config.siteUuid;
5733
5877
  if (siteUuid !== null && config.endpointTrusted !== false) {
5734
5878
  claimUrl = buildClaimUrl(config.endpoint, siteUuid);
@@ -5755,6 +5899,8 @@ async function collectGuideState(cwd) {
5755
5899
  siteUuid,
5756
5900
  claimUrl,
5757
5901
  endpointOverride,
5902
+ environment,
5903
+ environmentSource,
5758
5904
  hasBuildScript: Boolean(pkg?.scripts?.build?.trim()),
5759
5905
  installScanWired: (pkg?.scripts?.postinstall ?? "").includes("patchstack-connect scan"),
5760
5906
  // The scan has to run first: a later prebuild command may upload and stamp the map that the bundle
@@ -5773,7 +5919,7 @@ async function collectGuideState(cwd) {
5773
5919
  protectionApplicable: protection.applicable
5774
5920
  };
5775
5921
  }
5776
- var ANSI = {
5922
+ var ANSI3 = {
5777
5923
  reset: "\x1B[0m",
5778
5924
  bold: "\x1B[1m",
5779
5925
  dim: "\x1B[2m",
@@ -5787,9 +5933,6 @@ function needsSourceProductionMarker(state) {
5787
5933
  }
5788
5934
  return !state.widgetFileHint.toLowerCase().endsWith(".html");
5789
5935
  }
5790
- function widgetTagInPlace(state) {
5791
- return state.siteUuid !== null && !state.widgetOptOut && state.widgetInstalled && state.widgetTokenMatches !== false;
5792
- }
5793
5936
  function countRemainingSteps(state) {
5794
5937
  return [
5795
5938
  state.installed?.section === "dependencies",
@@ -5801,214 +5944,146 @@ function countRemainingSteps(state) {
5801
5944
  !state.protectionApplicable || state.protectionWired
5802
5945
  ].filter((step) => !step).length;
5803
5946
  }
5804
- function renderGuideChecklist(state, useColor) {
5805
- const paint = (code, text) => useColor ? `${code}${text}${ANSI.reset}` : text;
5806
- const done = (text) => ` ${paint(ANSI.green, "\u2714")} ${text}`;
5807
- const todo = (text) => ` ${paint(ANSI.yellow, "\u2716")} ${paint(ANSI.bold, text)}`;
5808
- 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) {
5809
5976
  const lines = [];
5810
- const headerParts = [state.framework, state.packageManager].filter(
5811
- (part) => part !== null
5812
- );
5813
- const name = state.projectName ?? path12.basename(process.cwd());
5814
- lines.push(paint(ANSI.bold, `Patchstack setup status \u2014 ${name} (${headerParts.join(" \xB7 ")})`));
5815
- if (state.endpointOverride !== null) {
5816
- lines.push(
5817
- detail(
5818
- `endpoint override in effect: ${state.endpointOverride} (set via .patchstackrc.json, PATCHSTACK_ENDPOINT, or --endpoint)`
5819
- )
5820
- );
5821
- }
5822
- lines.push("");
5823
- if (!state.hasPackageJson) {
5824
- lines.push(todo("No package.json found in this directory."));
5825
- lines.push(detail("For a JS/Node app, run the guide from its package directory; check that package.json is readable and valid."));
5826
- lines.push(detail("For a standalone HTML/CSS/browser-JavaScript site, use the disclosure widget directly."));
5827
- lines.push(detail("Do not create a Node project, build hooks, or a server just to install the widget."));
5828
- lines.push(detail("Use the correct site UUID or widget snippet from the Patchstack dashboard; never invent one."));
5829
- lines.push(detail('See "Plain HTML sites" in AGENT-INSTALL.md. Widget-only setup provides no dependency scan or runtime protection.'));
5830
- return lines.join("\n");
5831
- }
5832
- if (countRemainingSteps(state) > 0) {
5833
- lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Tell the user what you are doing as you go.")}`);
5834
- lines.push(" Installs here take minutes and they are watching output they cannot read.");
5835
- lines.push(" Say it in plain words: their site is built out of hundreds of ready-made");
5836
- lines.push(" pieces of code, Patchstack keeps track of which ones it uses and warns them");
5837
- lines.push(" when one turns out to be unsafe, and nothing their visitors see changes.");
5838
- lines.push(" Avoid lockfile / dependency / manifest / npm / UUID when speaking to them.");
5839
- lines.push("");
5840
- }
5841
- if (state.installed?.section === "dependencies") {
5842
- lines.push(done(`@patchstack/connect installed (${state.installed.version}, ${state.installed.section})`));
5843
- } else if (state.installed !== null) {
5844
- lines.push(todo(`Move @patchstack/connect to runtime dependencies (currently ${state.installed.section})`));
5845
- lines.push(detail(`Run \u2192 ${installCommand(state.packageManager)}`));
5846
- lines.push(detail("The generated guard imports @patchstack/connect/protect at runtime."));
5847
- } else {
5848
- lines.push(todo("Install @patchstack/connect as a runtime dependency"));
5849
- 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)) {
5850
5979
  if (state.packageManager === "bun") {
5851
- lines.push(detail(`(if bun isn't available here, ${INSTALL_COMMANDS.npm} works too)`));
5852
- }
5853
- }
5854
- if (state.siteUuid !== null) {
5855
- lines.push(done(`Site provisioned (${state.siteUuid})`));
5856
- } else {
5857
- lines.push(todo("Provision the site \u2014 run the first scan"));
5858
- lines.push(detail("Run \u2192 npx @patchstack/connect scan"));
5859
- lines.push(detail("Reads the lockfile, registers the project, writes .patchstackrc.json,"));
5860
- lines.push(detail("and prints a dashboard link. The CLI prints the link but never opens it."));
5861
- lines.push(detail("If your tool refuses to run this command, hand it to the person instead of working"));
5862
- lines.push(detail('around it \u2014 see "When your tool will not run this CLI" in the reference guide.'));
5863
- }
5864
- if (state.installScanWired) {
5865
- lines.push(done("Dependency-install scan wired (postinstall)"));
5866
- } else {
5867
- lines.push(todo("Scan again whenever dependencies are installed"));
5868
- lines.push(detail('Edit package.json \u2192 "postinstall": "patchstack-connect scan"'));
5869
- }
5870
- if (!state.hasBuildScript) {
5871
- lines.push(done("No build script to integrate (postinstall covers dependency changes)"));
5872
- } else if (state.prebuildWired && state.postbuildWired) {
5873
- lines.push(done("Build hooks wired (scan before builds, mark-build after)"));
5874
- } else if (state.packageManager === "bun") {
5875
- lines.push(todo('Wire the build hooks yourself \u2014 edit package.json (bun skips pre/post hooks, so chain inside "build")'));
5876
- lines.push(detail('Edit package.json \u2192 "build": "patchstack-connect scan && <existing build command> && patchstack-connect mark-build"'));
5877
- } else {
5878
- lines.push(todo('Wire the build hooks yourself \u2014 edit package.json "scripts" (chain with && if a hook already exists)'));
5879
- if (!state.prebuildWired) {
5880
- lines.push(detail('Edit package.json \u2192 "prebuild": "patchstack-connect scan"'));
5881
- }
5882
- if (!state.postbuildWired) {
5883
- lines.push(detail('Edit package.json \u2192 "postbuild": "patchstack-connect mark-build"'));
5884
- }
5885
- }
5886
- const widgetOk = state.widgetInstalled && state.widgetTokenMatches !== false;
5887
- if (state.widgetOptOut && !widgetOk) {
5888
- lines.push(done('Disclosure widget disabled by config ("widget": false in .patchstackrc.json)'));
5889
- } else if (widgetOk) {
5890
- lines.push(done("Disclosure widget installed"));
5891
- } else if (state.widgetInstalled) {
5892
- lines.push(todo("Fix the disclosure widget yourself \u2014 its site UUID doesn't match this project's"));
5893
- 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)`));
5894
- } else if (state.siteUuid === null) {
5895
- lines.push(todo('Add the "Report a vulnerability" widget \u2014 the first scan does this for you'));
5896
- lines.push(detail("Run \u2192 npx @patchstack/connect scan (provisions the site and adds the widget tag"));
5897
- lines.push(detail(" to the root HTML shell: index.html / public/index.html / src/app.html)"));
5898
- } else {
5899
- lines.push(todo('Add the "Report a vulnerability" widget yourself \u2014 this root is code, not a plain HTML shell'));
5900
- lines.push(detail("Note \u2192 a normal `scan` adds this tag to a plain HTML shell automatically; add it by hand here:"));
5901
- 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):";
5902
- lines.push(detail(placement));
5903
- lines.push(detail(` ${buildWidgetTag(state.siteUuid)}`));
5904
- lines.push(detail("The site UUID is public by design \u2014 it ships in client-side HTML."));
5905
- }
5906
- if (needsSourceProductionMarker(state)) {
5907
- if (state.productionMarkerWired) {
5908
- 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"');
5909
5981
  } else {
5910
- lines.push(todo("Add the production marker \u2014 this root is server-rendered, so mark-build cannot stamp it"));
5911
- lines.push(
5912
- detail(
5913
- "Without it the widget treats the published site as build mode and shows the claim flow to visitors."
5914
- )
5915
- );
5916
- const gate = productionGate(state.framework);
5917
- if (hasJsxShell(state.framework)) {
5918
- lines.push(detail(`Run \u2192 npx @patchstack/connect scan (adds it to ${state.widgetFileHint} automatically)`));
5919
- lines.push(detail("Or add it by hand, in <head> above the widget tag:"));
5920
- for (const snippetLine of buildSourceMarkerSnippet(state.framework).split("\n")) {
5921
- lines.push(detail(` ${snippetLine}`));
5922
- }
5923
- } else {
5924
- lines.push(
5925
- detail(
5926
- `Edit ${state.widgetFileHint} \u2192 using the framework's head mechanism, emit an inline`
5927
- )
5928
- );
5929
- lines.push(detail(` <script>window.__PATCHSTACK_PROD__=true;</script> only when ${gate}.`));
5930
- }
5931
- 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"');
5932
5984
  }
5933
5985
  }
5934
- if (!state.protectionApplicable) {
5935
- lines.push(done("Runtime protection: not applicable \u2014 this project has no request path"));
5936
- for (const check of state.protectionChecks.filter((item) => item.hint !== void 0 && item.group !== "reporting")) {
5937
- lines.push(detail(check.hint ?? ""));
5938
- }
5939
- } else if (state.protectionWired) {
5940
- lines.push(done(`Runtime protection wired (${state.protectionStack})`));
5941
- } else {
5942
- lines.push(todo(`Finish runtime protection (${state.protectionStack})`));
5943
- for (const check of state.protectionChecks.filter((item) => !item.ok)) {
5944
- lines.push(detail(`${check.label}${check.hint ? ` \u2014 ${check.hint}` : ""}`));
5945
- }
5946
- lines.push(detail("Verify \u2192 npx @patchstack/connect protect --check"));
5947
- 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
+ });
5948
5995
  }
5949
- lines.push("");
5950
- if (state.claimUrl !== null) {
5951
- lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Connect this site to your Patchstack account:")}`);
5952
- let route = 1;
5953
- if (widgetTagInPlace(state)) {
5954
- lines.push(` ${route++}. In the preview \u2014 while the site is unclaimed the widget shows a`);
5955
- lines.push(' "Connect this website" panel. Signing in there attaches the site.');
5956
- }
5957
- lines.push(` ${route++}. In a browser \u2014 open the dashboard link (the CLI never opens it):`);
5958
- lines.push(` ${paint(ANSI.cyan, state.claimUrl)}`);
5959
- lines.push(` ${route}. From this terminal \u2192 npx @patchstack/connect claim`);
5960
- lines.push(" (prints a link to sign in with, then attaches the site to that account)");
5961
- lines.push(detail("Reports have no owner to reach until one of these completes. The site UUID ships"));
5962
- lines.push(detail("in the page and claiming is first-come, so an unclaimed site stays claimable by"));
5963
- lines.push(detail("anyone who loads it."));
5964
- if (state.endpointOverride !== null) {
5965
- lines.push(detail("(this URL inherits the endpoint override above)"));
5966
- }
5967
- } else {
5968
- 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
+ });
5969
6010
  }
5970
- if (widgetTagInPlace(state)) {
5971
- lines.push("");
5972
- lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Refresh the preview to see the widget:")}`);
5973
- lines.push(" The widget loads with the page, so a preview that was already open still shows");
5974
- lines.push(" the HTML from before this change. Builders that hot reload refresh it themselves;");
5975
- lines.push(" if nothing appears, refresh the preview once.");
5976
- lines.push(' Unclaimed, the widget shows the "Connect this website" panel; the public');
5977
- lines.push(' "Report a vulnerability" button takes its place once the site is claimed.');
5978
- }
5979
- if (state.siteUuid !== null) {
5980
- lines.push("");
5981
- lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Deploy to put this on your live site:")}`);
5982
- lines.push(" These are source changes. Your deployed site keeps serving its previous build,");
5983
- 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
+ });
5984
6020
  }
5985
- const remaining = countRemainingSteps(state);
5986
- lines.push("");
5987
- if (remaining === 0) {
5988
- lines.push(
5989
- done(
5990
- paint(
5991
- ANSI.bold,
5992
- "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."
5993
- )
5994
- )
5995
- );
5996
- if (state.claimUrl !== null) {
5997
- lines.push(detail("Until the site is attached to an account (dashboard link above, or `claim`), nobody can see its reports."));
5998
- }
5999
- } else {
6000
- lines.push(
6001
- ` ${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
+ }
6002
6046
  );
6003
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
+ }
6004
6084
  return lines.join("\n");
6005
6085
  }
6006
6086
 
6007
- // src/login.ts
6008
- import { mkdtempSync } from "fs";
6009
- import { tmpdir as tmpdir2 } from "os";
6010
- import path14 from "path";
6011
-
6012
6087
  // src/device-flow.ts
6013
6088
  import {
6014
6089
  closeSync as closeSync2,
@@ -6309,7 +6384,7 @@ import { relative as relative4 } from "path";
6309
6384
 
6310
6385
  // src/protect/install/runtime/entry.ts
6311
6386
  import { existsSync as existsSync13, readFileSync as readFileSync13, realpathSync as realpathSync4, statSync as statSync6 } from "fs";
6312
- import { join as join16, resolve as resolve4, sep as sep2 } from "path";
6387
+ import { join as join14, resolve as resolve4, sep as sep2 } from "path";
6313
6388
  var CONVENTIONAL = [
6314
6389
  "server.js",
6315
6390
  "server.mjs",
@@ -6335,15 +6410,15 @@ var CONVENTIONAL = [
6335
6410
  var LOADABLE = /\.(?:js|mjs|cjs)$/;
6336
6411
  function readPackage2(cwd) {
6337
6412
  try {
6338
- const parsed = JSON.parse(readFileSync13(join16(cwd, "package.json"), "utf8"));
6413
+ const parsed = JSON.parse(readFileSync13(join14(cwd, "package.json"), "utf8"));
6339
6414
  return parsed !== null && typeof parsed === "object" ? parsed : null;
6340
6415
  } catch {
6341
6416
  return null;
6342
6417
  }
6343
6418
  }
6344
- function isFile(path18) {
6419
+ function isFile(path17) {
6345
6420
  try {
6346
- return statSync6(path18).isFile();
6421
+ return statSync6(path17).isFile();
6347
6422
  } catch {
6348
6423
  return false;
6349
6424
  }
@@ -6352,14 +6427,14 @@ function loadable(cwd, candidate) {
6352
6427
  const written = resolve4(cwd, candidate);
6353
6428
  if (!LOADABLE.test(written) || !isFile(written)) return null;
6354
6429
  let root;
6355
- let path18;
6430
+ let path17;
6356
6431
  try {
6357
6432
  root = realpathSync4(cwd);
6358
- path18 = realpathSync4(written);
6433
+ path17 = realpathSync4(written);
6359
6434
  } catch {
6360
6435
  return null;
6361
6436
  }
6362
- if (path18 !== root && !path18.startsWith(root + sep2)) return null;
6437
+ if (path17 !== root && !path17.startsWith(root + sep2)) return null;
6363
6438
  return written;
6364
6439
  }
6365
6440
  function passable(token) {
@@ -6405,7 +6480,7 @@ function resolveEntry(cwd) {
6405
6480
  }
6406
6481
  return {
6407
6482
  kind: "unavailable",
6408
- reason: existsSync13(join16(cwd, "package.json")) ? 'no directly loadable entry was found \u2014 declare one as a "start" script of the form `node <file>`, or run the check against a built output' : "there is no package.json here"
6483
+ reason: existsSync13(join14(cwd, "package.json")) ? 'no directly loadable entry was found \u2014 declare one as a "start" script of the form `node <file>`, or run the check against a built output' : "there is no package.json here"
6409
6484
  };
6410
6485
  }
6411
6486
 
@@ -6439,8 +6514,8 @@ function preloadPath() {
6439
6514
  // beside this file, in the repository
6440
6515
  ];
6441
6516
  for (const candidate of candidates) {
6442
- const path18 = fileURLToPath2(candidate);
6443
- if (existsSync14(path18)) return path18;
6517
+ const path17 = fileURLToPath2(candidate);
6518
+ if (existsSync14(path17)) return path17;
6444
6519
  }
6445
6520
  return null;
6446
6521
  }
@@ -6787,15 +6862,15 @@ function formatRuntimeCheck(report) {
6787
6862
  }
6788
6863
 
6789
6864
  // src/map-command.ts
6790
- import path15 from "path";
6865
+ import path14 from "path";
6791
6866
 
6792
6867
  // src/map/ts-loader.ts
6793
6868
  import { createRequire } from "module";
6794
6869
  import { pathToFileURL } from "url";
6795
- import { join as join17 } from "path";
6870
+ import { join as join15 } from "path";
6796
6871
  async function loadTypeScript(cwd) {
6797
6872
  try {
6798
- const req = createRequire(pathToFileURL(join17(cwd, "package.json")));
6873
+ const req = createRequire(pathToFileURL(join15(cwd, "package.json")));
6799
6874
  const resolved = req.resolve("typescript");
6800
6875
  const mod = await import(pathToFileURL(resolved).href);
6801
6876
  return mod.default ?? mod;
@@ -7225,7 +7300,7 @@ function dedupeSinks(sinks, owner) {
7225
7300
 
7226
7301
  // src/map/module-graph.ts
7227
7302
  import { readFileSync as readFileSync14, realpathSync as realpathSync5, statSync as statSync7 } from "fs";
7228
- import { dirname as dirname8, join as join18, relative as relative5, resolve as resolvePath } from "path";
7303
+ import { dirname as dirname8, join as join16, relative as relative5, resolve as resolvePath } from "path";
7229
7304
  var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
7230
7305
  function createModuleGraph(ts, opts) {
7231
7306
  const cache = /* @__PURE__ */ new Map();
@@ -7303,7 +7378,7 @@ function resolveRelativeModule(fromFile, spec) {
7303
7378
  }
7304
7379
  candidates.push(base);
7305
7380
  for (const e of RESOLVE_EXTS) candidates.push(base + e);
7306
- for (const e of RESOLVE_EXTS) candidates.push(join18(base, "index" + e));
7381
+ for (const e of RESOLVE_EXTS) candidates.push(join16(base, "index" + e));
7307
7382
  for (const c of candidates) {
7308
7383
  try {
7309
7384
  if (statSync7(c).isFile()) return c;
@@ -7332,8 +7407,8 @@ function collectCallees(sf, ts) {
7332
7407
  }
7333
7408
 
7334
7409
  // src/map/coordinates.ts
7335
- function runtimeCoordinate(source, path18) {
7336
- if (/\[\d*\]/.test(path18)) {
7410
+ function runtimeCoordinate(source, path17) {
7411
+ if (/\[\d*\]/.test(path17)) {
7337
7412
  return { runtimeParameter: null, runtimeParameterReason: "array traversal: needs an array_key_value rule, not a dotted parameter" };
7338
7413
  }
7339
7414
  switch (source) {
@@ -7342,15 +7417,15 @@ function runtimeCoordinate(source, path18) {
7342
7417
  case "multipart":
7343
7418
  case "body":
7344
7419
  case "server-fn-data":
7345
- return { runtimeParameter: `post.${path18}` };
7420
+ return { runtimeParameter: `post.${path17}` };
7346
7421
  case "query":
7347
- return { runtimeParameter: `get.${path18}` };
7422
+ return { runtimeParameter: `get.${path17}` };
7348
7423
  case "cookie":
7349
- return { runtimeParameter: `cookie.${path18}` };
7424
+ return { runtimeParameter: `cookie.${path17}` };
7350
7425
  case "file":
7351
- return { runtimeParameter: `files.${path18}` };
7426
+ return { runtimeParameter: `files.${path17}` };
7352
7427
  case "header":
7353
- return { runtimeParameter: `server.HTTP_${path18.toUpperCase().replace(/-/g, "_")}` };
7428
+ return { runtimeParameter: `server.HTTP_${path17.toUpperCase().replace(/-/g, "_")}` };
7354
7429
  case "route-param":
7355
7430
  return { runtimeParameter: null, runtimeParameterReason: "route parameters are not exposed by the runtime resolver" };
7356
7431
  default:
@@ -7382,8 +7457,8 @@ function addressSpaceOf(source) {
7382
7457
  function isProvenFlow(confidence) {
7383
7458
  return confidence === "exact-local" || confidence === "transformed-local";
7384
7459
  }
7385
- function inputIdOf(source, path18) {
7386
- return `${addressSpaceOf(source)}:${path18}`;
7460
+ function inputIdOf(source, path17) {
7461
+ return `${addressSpaceOf(source)}:${path17}`;
7387
7462
  }
7388
7463
  function withCoordinates(fields, source) {
7389
7464
  return fields.map((f) => {
@@ -7517,14 +7592,16 @@ function requestMemberAccesses(params, body, ts, opts = {}) {
7517
7592
  const inner = unwrap(e);
7518
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);
7519
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"]);
7520
7597
  const visit = (n) => {
7521
- if (ts.isPropertyAccessExpression(n) && isReqSourceExpr(n.expression)) {
7598
+ if (ts.isPropertyAccessExpression(n) && isReqSourceExpr(n.expression) && !isCallee(n)) {
7522
7599
  record(n.name.text, sourceOfExpr(n.expression));
7523
7600
  }
7524
7601
  if (ts.isElementAccessExpression(n) && isReqSourceExpr(n.expression) && n.argumentExpression && ts.isStringLiteral(n.argumentExpression)) {
7525
7602
  record(n.argumentExpression.text, sourceOfExpr(n.expression));
7526
7603
  }
7527
- 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))) {
7528
7605
  record(n.arguments[0].text, sourceOfExpr(n.expression.expression));
7529
7606
  }
7530
7607
  if (ts.isVariableDeclaration(n) && n.initializer) {
@@ -7576,6 +7653,7 @@ function requestMemberAccesses(params, body, ts, opts = {}) {
7576
7653
 
7577
7654
  // src/map/flows.ts
7578
7655
  var MAX_DEPENDENCY_INPUT_FLOWS_PER_ENDPOINT = 100;
7656
+ var ACCESSOR_NAMESPACES = /* @__PURE__ */ new Set(["headers", "cookies"]);
7579
7657
  function spaceOfKey(key) {
7580
7658
  if (key === "query") return "get";
7581
7659
  if (key === "params") return "route-param";
@@ -7601,18 +7679,18 @@ function linkFlows(bodyNode, params, inputs, sinks, ts, invocations) {
7601
7679
  }
7602
7680
  const CONTAINER_KEYS = /* @__PURE__ */ new Set([...REQ_SOURCES, "data", "input", "payload"]);
7603
7681
  const rootPath = /* @__PURE__ */ new Map();
7604
- const addRoot = (name, path18, space) => {
7605
- if (!rootPath.has(name)) rootPath.set(name, { path: path18, 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 } : {} });
7606
7684
  };
7607
- for (const p of params ?? []) {
7685
+ for (const [index, p] of (params ?? []).entries()) {
7608
7686
  if (!p?.name) continue;
7609
- if (ts.isIdentifier(p.name)) addRoot(p.name.text, "");
7687
+ if (ts.isIdentifier(p.name)) addRoot(p.name.text, "", void 0, false, index === 0);
7610
7688
  else if (ts.isObjectBindingPattern(p.name)) {
7611
7689
  for (const el of p.name.elements) {
7612
7690
  if (!ts.isBindingElement(el) || !ts.isIdentifier(el.name)) continue;
7613
7691
  const key = bindingKey(el, ts);
7614
7692
  const container = key !== void 0 && CONTAINER_KEYS.has(key);
7615
- 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));
7616
7694
  }
7617
7695
  }
7618
7696
  }
@@ -7624,7 +7702,8 @@ function linkFlows(bodyNode, params, inputs, sinks, ts, invocations) {
7624
7702
  const m = cur.expression.name.text;
7625
7703
  if (["json", "formData", "text"].includes(m)) {
7626
7704
  const root = rootIdentifier(cur.expression.expression, ts);
7627
- 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" };
7628
7707
  }
7629
7708
  if (["parse", "safeParse", "validate", "cast"].includes(m)) {
7630
7709
  for (const a of cur.arguments) {
@@ -7634,21 +7713,21 @@ function linkFlows(bodyNode, params, inputs, sinks, ts, invocations) {
7634
7713
  return void 0;
7635
7714
  }
7636
7715
  }
7637
- const p = pathFromTainted(cur, ts, rootPath);
7638
- return p;
7716
+ return pathFromTainted(cur, ts, rootPath) ?? accessorRead(cur, ts, rootPath);
7639
7717
  };
7640
7718
  const aliasVisit = (n) => {
7641
7719
  if (ts.isVariableDeclaration(n) && n.initializer) {
7642
7720
  const base = requestReadPath(n.initializer);
7643
7721
  if (base !== void 0) {
7644
- 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);
7645
7723
  else if (ts.isObjectBindingPattern(n.name)) {
7646
7724
  for (const el of n.name.elements) {
7647
7725
  if (!ts.isBindingElement(el) || !ts.isIdentifier(el.name)) continue;
7648
7726
  const key = bindingKey(el, ts);
7649
- const space = base.space ?? (base.path === "" ? spaceOfKey(key) : void 0);
7650
- const container = base.path === "" && key !== void 0 && CONTAINER_KEYS.has(key);
7651
- 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));
7652
7731
  }
7653
7732
  }
7654
7733
  }
@@ -7680,7 +7759,7 @@ function linkFlows(bodyNode, params, inputs, sinks, ts, invocations) {
7680
7759
  for (const a of args) for (const l of sinkArgumentLimitations(a, ts, rootPath)) sinkLimits.push(l);
7681
7760
  for (let i = 0; i < args.length; i++) {
7682
7761
  const role = argumentRoleOf(sink.kind, method, i, args.length);
7683
- const whole = pathFromTainted(args[i], ts, rootPath);
7762
+ const whole = pathFromTainted(args[i], ts, rootPath) ?? accessorRead(args[i], ts, rootPath);
7684
7763
  for (const read2 of taintedReadPaths(args[i], ts, rootPath)) {
7685
7764
  const key = `${read2.space ?? "*"}:${read2.path}`;
7686
7765
  const exact = whole !== void 0 && whole.path === read2.path && whole.space === read2.space;
@@ -7797,8 +7876,8 @@ function dedupeLimitations(list) {
7797
7876
  function join22(base, seg) {
7798
7877
  return base ? `${base}.${seg}` : seg;
7799
7878
  }
7800
- function normalizePath(path18) {
7801
- return path18.replace(/\[\d*\]/g, "").split(".").filter(Boolean).join(".");
7879
+ function normalizePath(path17) {
7880
+ return path17.replace(/\[\d*\]/g, "").split(".").filter(Boolean).join(".");
7802
7881
  }
7803
7882
  function fluentChainCalls(call, ts) {
7804
7883
  let root = call;
@@ -7842,6 +7921,18 @@ function taintedReadPaths(node, ts, rootPath, includeDeferredBodies = true) {
7842
7921
  const visit = (n) => {
7843
7922
  if (!n) return;
7844
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
+ }
7845
7936
  if (ts.isPropertyAccessExpression(n) || ts.isElementAccessExpression(n)) {
7846
7937
  const read2 = pathFromTainted(n, ts, rootPath);
7847
7938
  if (read2 !== void 0) {
@@ -7861,11 +7952,11 @@ function taintedReadPaths(node, ts, rootPath, includeDeferredBodies = true) {
7861
7952
  function sinkArgumentLimitations(node, ts, rootPath) {
7862
7953
  const out = [];
7863
7954
  const seen = /* @__PURE__ */ new Set();
7864
- const add = (kind, detail, n) => {
7865
- const key = `${kind}:${detail}`;
7955
+ const add = (kind, detail2, n) => {
7956
+ const key = `${kind}:${detail2}`;
7866
7957
  if (seen.has(key)) return;
7867
7958
  seen.add(key);
7868
- out.push({ kind, detail, line: lineOf(n) });
7959
+ out.push({ kind, detail: detail2, line: lineOf(n) });
7869
7960
  };
7870
7961
  const text = (n) => {
7871
7962
  try {
@@ -7892,7 +7983,25 @@ function sinkArgumentLimitations(node, ts, rootPath) {
7892
7983
  visit(node);
7893
7984
  return out;
7894
7985
  }
7895
- 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 = []) {
7896
8005
  const segs = [];
7897
8006
  let cur = node;
7898
8007
  for (; ; ) {
@@ -7916,12 +8025,14 @@ function pathFromTainted(node, ts, rootPath) {
7916
8025
  if (!cur || !ts.isIdentifier(cur)) return void 0;
7917
8026
  const base = rootPath.get(cur.text);
7918
8027
  if (base === void 0) return void 0;
8028
+ segs.push(...trailing);
7919
8029
  let space = base.space;
7920
- 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])) {
7921
8031
  space = spaceOfKey(segs[0]) ?? space;
7922
8032
  segs.shift();
7923
8033
  }
7924
- 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 };
7925
8036
  }
7926
8037
 
7927
8038
  // src/map/invocations.ts
@@ -8164,7 +8275,7 @@ function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, c
8164
8275
 
8165
8276
  // src/map/imports.ts
8166
8277
  import { readFileSync as readFileSync15 } from "fs";
8167
- import { join as join19 } from "path";
8278
+ import { join as join17 } from "path";
8168
8279
  var MAX_SITES2 = 5;
8169
8280
  var NPM_NAME = /^(?:@[^/@\s~][^/@\s]*\/)?[^/@\s.~][^/@\s]*$/;
8170
8281
  function isNpmPackageName(pkg) {
@@ -8175,7 +8286,7 @@ function readPathAliases(cwd) {
8175
8286
  for (const name of ["tsconfig.json", "jsconfig.json"]) {
8176
8287
  let paths;
8177
8288
  try {
8178
- const raw = readFileSync15(join19(cwd, name), "utf8");
8289
+ const raw = readFileSync15(join17(cwd, name), "utf8");
8179
8290
  const parsed = JSON.parse(stripJsonComments(raw));
8180
8291
  paths = parsed.compilerOptions?.paths;
8181
8292
  } catch {
@@ -8662,7 +8773,7 @@ function getStringFlag(flags, name) {
8662
8773
 
8663
8774
  // src/build-stamp.ts
8664
8775
  import { lstatSync as lstatSync4, readFileSync as readFileSync17, readdirSync as readdirSync6, realpathSync as realpathSync7 } from "fs";
8665
- import { join as join20, relative as relative7, sep as sep3 } from "path";
8776
+ import { join as join18, relative as relative7, sep as sep3 } from "path";
8666
8777
  var RULES_FILE_NAMES = /* @__PURE__ */ new Set(["rules.json", "patchstack.rules.json"]);
8667
8778
  var SKIP_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "coverage", "tmp", "vendor"]);
8668
8779
  var MAX_DEPTH2 = 6;
@@ -8680,11 +8791,11 @@ function guardImportsRulesFile(dir, rulesName) {
8680
8791
  }
8681
8792
  for (const name of siblings) {
8682
8793
  if (!GUARD_SOURCE.test(name)) continue;
8683
- const path18 = join20(dir, name);
8794
+ const path17 = join18(dir, name);
8684
8795
  try {
8685
- const entry = lstatSync4(path18);
8796
+ const entry = lstatSync4(path17);
8686
8797
  if (!entry.isFile() || entry.isSymbolicLink() || entry.size > MAX_GUARD_SOURCE_BYTES) continue;
8687
- const source = stripComments(readFileSync17(path18, "utf8"));
8798
+ const source = stripComments(readFileSync17(path17, "utf8"));
8688
8799
  if (source.includes("@patchstack/connect/protect") && importsRules.test(source)) return true;
8689
8800
  } catch {
8690
8801
  }
@@ -8705,9 +8816,9 @@ function findRulesFile(cwd) {
8705
8816
  } catch {
8706
8817
  return { kind: "none" };
8707
8818
  }
8708
- const contained = (path18) => {
8819
+ const contained = (path17) => {
8709
8820
  try {
8710
- const real = realpathSync7(path18);
8821
+ const real = realpathSync7(path17);
8711
8822
  return real === root || real.startsWith(root + sep3);
8712
8823
  } catch {
8713
8824
  return false;
@@ -8723,25 +8834,25 @@ function findRulesFile(cwd) {
8723
8834
  }
8724
8835
  for (const name of entries) {
8725
8836
  if (name.startsWith(".")) continue;
8726
- const path18 = join20(dir, name);
8837
+ const path17 = join18(dir, name);
8727
8838
  let entry;
8728
8839
  try {
8729
- entry = lstatSync4(path18);
8840
+ entry = lstatSync4(path17);
8730
8841
  } catch {
8731
8842
  continue;
8732
8843
  }
8733
8844
  if (entry.isSymbolicLink()) continue;
8734
8845
  if (entry.isDirectory()) {
8735
- if (!SKIP_DIRECTORIES.has(name)) walk2(path18, depth + 1);
8846
+ if (!SKIP_DIRECTORIES.has(name)) walk2(path17, depth + 1);
8736
8847
  continue;
8737
8848
  }
8738
8849
  if (!entry.isFile() || !RULES_FILE_NAMES.has(name)) continue;
8739
- if (!contained(path18)) continue;
8850
+ if (!contained(path17)) continue;
8740
8851
  if (!guardImportsRulesFile(dir, name)) continue;
8741
8852
  try {
8742
- if (isRuleBundle(JSON.parse(readFileSync17(path18, "utf8")))) found.push(path18);
8853
+ if (isRuleBundle(JSON.parse(readFileSync17(path17, "utf8")))) found.push(path17);
8743
8854
  } catch {
8744
- unreadable.push(path18);
8855
+ unreadable.push(path17);
8745
8856
  }
8746
8857
  }
8747
8858
  };
@@ -8821,7 +8932,7 @@ function isPreBundleBuildHook(env = process.env) {
8821
8932
  return /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(env.npm_lifecycle_script ?? "");
8822
8933
  }
8823
8934
  function undeliveredReportLines(err, config, cwd) {
8824
- const lines = [`patchstack: manifest not reported \u2014 ${err.message}`];
8935
+ const lines = [`patchstack: could not send the package list to Patchstack \u2014 ${err.message}`];
8825
8936
  if (err.code === "UNAUTHORIZED") {
8826
8937
  const hasCredential = typeof config.pulseAuth === "string" && config.pulseAuth.length > 0;
8827
8938
  if (hasCredential) {
@@ -8836,7 +8947,7 @@ function undeliveredReportLines(err, config, cwd) {
8836
8947
  }
8837
8948
  }
8838
8949
  lines.push(
8839
- "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."
8840
8951
  );
8841
8952
  return lines;
8842
8953
  }
@@ -8933,7 +9044,7 @@ async function runMap(flags) {
8933
9044
  const json = JSON.stringify(map, null, 2);
8934
9045
  const out = getStringFlag(flags, "out");
8935
9046
  if (out) {
8936
- atomicWriteFileSync(path15.resolve(out), json, { encoding: "utf8" });
9047
+ atomicWriteFileSync(path14.resolve(out), json, { encoding: "utf8" });
8937
9048
  console.error(`patchstack: wrote ${out}`);
8938
9049
  } else if (flags.get("upload") !== true) {
8939
9050
  console.log(json);
@@ -8983,13 +9094,13 @@ async function runMap(flags) {
8983
9094
 
8984
9095
  // src/setup.ts
8985
9096
  import { readFileSync as readFileSync18 } from "fs";
8986
- import path16 from "path";
9097
+ import path15 from "path";
8987
9098
  var SCAN_COMMAND = "patchstack-connect scan";
8988
9099
  var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
8989
9100
  function setupProtection(cwd) {
8990
- const install = runProtect(cwd);
9101
+ const { result: install, lines: log2 } = collectProtectLog(() => runProtect(cwd));
8991
9102
  const verification = runVerify(cwd);
8992
- return { install, verification };
9103
+ return { install, verification, log: log2 };
8993
9104
  }
8994
9105
  function appendHook(existing, command) {
8995
9106
  if (existing === void 0 || existing.trim().length === 0) {
@@ -9008,7 +9119,7 @@ function prependHook(existing, command) {
9008
9119
  return [command, ...remaining].join(" && ");
9009
9120
  }
9010
9121
  function wireBuildScripts(cwd, packageManager) {
9011
- const target = path16.join(cwd, "package.json");
9122
+ const target = path15.join(cwd, "package.json");
9012
9123
  const raw = readFileSync18(target, "utf8");
9013
9124
  const pkg = JSON.parse(raw);
9014
9125
  const scripts = pkg.scripts ?? {};
@@ -9082,7 +9193,7 @@ Usage:
9082
9193
  If no UUID is configured, the server
9083
9194
  provisions one and we persist it. After a
9084
9195
  successful post it also adds/updates the
9085
- disclosure-widget <script> tag in the root
9196
+ Patchstack Connector <script> tag in the root
9086
9197
  HTML shell (index.html, public/index.html,
9087
9198
  or src/app.html) \u2014 opt out with
9088
9199
  "widget": false in .patchstackrc.json.
@@ -9191,6 +9302,8 @@ Usage:
9191
9302
  Global:
9192
9303
  --version Print the installed version of this package and exit
9193
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
9194
9307
 
9195
9308
  Options (for scan, setup, status, and uninstall):
9196
9309
  --site-uuid <uuid> Override the configured site UUID
@@ -9246,6 +9359,13 @@ Examples:
9246
9359
  npx @patchstack/connect demo-guide node-serialize
9247
9360
  `;
9248
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
+ }
9249
9369
  function parseArgs(argv) {
9250
9370
  const args = argv.slice(2);
9251
9371
  const positional = [];
@@ -9276,6 +9396,20 @@ function parseArgs(argv) {
9276
9396
  flags
9277
9397
  };
9278
9398
  }
9399
+ function resolveCliConfig(args, extra = {}) {
9400
+ return resolveConfig({
9401
+ cwd: process.cwd(),
9402
+ cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
9403
+ cliEndpoint: getStringFlag(args.flags, "endpoint"),
9404
+ ...extra
9405
+ });
9406
+ }
9407
+ function runningInCi() {
9408
+ return process.env.CI !== void 0 && process.env.CI !== "" && process.env.CI !== "false";
9409
+ }
9410
+ function gitignoreOutcomeLine(ignore) {
9411
+ return ignore.ignored ? " Added to .gitignore." : ` NOT ignored by git \u2014 ${ignore.reason ?? "unknown reason"}. Add it to .gitignore yourself before committing.`;
9412
+ }
9279
9413
  async function runInit(args) {
9280
9414
  const uuid = args.positional[0];
9281
9415
  if (!uuid) {
@@ -9283,26 +9417,22 @@ async function runInit(args) {
9283
9417
  console.error("Usage: patchstack-connect init <site-uuid>");
9284
9418
  return 1;
9285
9419
  }
9286
- if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(uuid)) {
9420
+ if (!isCanonicalUuid(uuid)) {
9287
9421
  console.error(`Error: "${uuid}" does not look like a valid UUID.`);
9288
9422
  return 1;
9289
9423
  }
9290
9424
  const target = await writeConfigFile(process.cwd(), { siteUuid: uuid });
9291
9425
  console.log(`Wrote ${target}`);
9292
9426
  console.log("");
9293
- console.log("Next: run `npx @patchstack/connect scan` to send your first manifest.");
9427
+ console.log("Next: run `npx @patchstack/connect scan`.");
9294
9428
  return 0;
9295
9429
  }
9296
9430
  async function runClaim(args) {
9297
- if (process.env.CI !== void 0 && process.env.CI !== "" && process.env.CI !== "false") {
9431
+ if (runningInCi()) {
9298
9432
  console.error("`claim` is interactive and cannot run in CI. Claim the site from a developer machine.");
9299
9433
  return 1;
9300
9434
  }
9301
- const config = await resolveConfig({
9302
- cwd: process.cwd(),
9303
- cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
9304
- cliEndpoint: getStringFlag(args.flags, "endpoint")
9305
- });
9435
+ const config = await resolveCliConfig(args);
9306
9436
  const siteUuid = config.siteUuid;
9307
9437
  if (siteUuid === null) {
9308
9438
  console.error("\n No site to claim yet \u2014 run `npx @patchstack/connect scan` first.\n");
@@ -9316,10 +9446,8 @@ async function runClaim(args) {
9316
9446
  console.log(` ${result2.dashboardUrl ?? buildClaimUrl(config.endpoint, siteUuid)}`);
9317
9447
  if (result2.credentialSaved === true) {
9318
9448
  const ignore = await secretFileIgnored(process.cwd());
9319
- console.log(` A credential for this site was issued and saved to ${SECRET_CONFIG_FILENAME}.`);
9320
- console.log(
9321
- ignore.ignored ? " Added to .gitignore." : ` NOT ignored by git \u2014 ${ignore.reason ?? "unknown reason"}. Add it to .gitignore yourself before committing.`
9322
- );
9449
+ console.log(` Saved a new credential to ${SECRET_CONFIG_FILENAME}.`);
9450
+ console.log(gitignoreOutcomeLine(ignore));
9323
9451
  }
9324
9452
  console.log("");
9325
9453
  return 0;
@@ -9329,8 +9457,7 @@ async function runClaim(args) {
9329
9457
  Your code: ${userCode}`);
9330
9458
  console.log(` Claim at: ${verificationUri}
9331
9459
  `);
9332
- console.log(" Open that link and sign in to Patchstack \u2014 or create an account \u2014 to attach");
9333
- 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");
9334
9461
  };
9335
9462
  if (args.flags.has("wait")) {
9336
9463
  const pending = readPendingClaim(siteUuid);
@@ -9374,8 +9501,7 @@ async function runClaim(args) {
9374
9501
  return 1;
9375
9502
  }
9376
9503
  prompt(started.pending.userCode, started.pending.verificationUri);
9377
- console.log(" Give that link to the user. When they confirm they have claimed the site, run");
9378
- 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");
9379
9505
  return 0;
9380
9506
  }
9381
9507
  const result = await claim2(config, (userCode, verificationUri) => {
@@ -9395,24 +9521,17 @@ async function runClaim(args) {
9395
9521
  return 1;
9396
9522
  }
9397
9523
  async function runLogin(args) {
9398
- if (process.env.CI !== void 0 && process.env.CI !== "" && process.env.CI !== "false") {
9524
+ if (runningInCi()) {
9399
9525
  console.error("`login` is interactive and cannot run in CI. Set PATCHSTACK_PULSE_AUTH instead.");
9400
9526
  return 1;
9401
9527
  }
9402
- const config = await resolveConfig({
9403
- cwd: process.cwd(),
9404
- cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
9405
- cliEndpoint: getStringFlag(args.flags, "endpoint")
9406
- });
9528
+ const config = await resolveCliConfig(args);
9407
9529
  const approved = async () => {
9408
9530
  const ignore = await secretFileIgnored(process.cwd());
9409
9531
  console.log(`
9410
9532
  \u2713 Credential restored and saved to ${SECRET_CONFIG_FILENAME}.`);
9411
- console.log(
9412
- ignore.ignored ? " Added to .gitignore." : ` NOT ignored by git \u2014 ${ignore.reason ?? "unknown reason"}. Add it to .gitignore yourself before committing.`
9413
- );
9414
- console.log(" The previous credential no longer works. Update it anywhere else it was set:");
9415
- console.log(" CI secrets, hosting env vars, preview environments, other checkouts.\n");
9533
+ console.log(gitignoreOutcomeLine(ignore));
9534
+ console.log(" The old credential no longer works. Update it in CI, hosting and other checkouts.\n");
9416
9535
  return 0;
9417
9536
  };
9418
9537
  if (args.flags.has("wait")) {
@@ -9433,9 +9552,8 @@ async function runLogin(args) {
9433
9552
  Your code: ${userCode}`);
9434
9553
  console.log(` Approve at: ${verificationUri}
9435
9554
  `);
9436
- console.log(" Open that link and approve it as the site's owner. Approving issues a new");
9437
- console.log(" credential and stops the current one working \u2014 CI, deploys and any other");
9438
- 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");
9439
9557
  };
9440
9558
  if (process.stdout.isTTY !== true) {
9441
9559
  const existing = config.siteUuid === null ? null : readPendingLogin(config.siteUuid);
@@ -9460,8 +9578,7 @@ async function runLogin(args) {
9460
9578
  return 1;
9461
9579
  }
9462
9580
  prompt(started.pending.userCode, started.pending.verificationUri);
9463
- console.log(" Give that link to the user. When they confirm they have approved it, run");
9464
- 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");
9465
9582
  return 0;
9466
9583
  }
9467
9584
  const result = await login(config, (userCode, verificationUri) => {
@@ -9479,8 +9596,8 @@ var MAX_RETRY_TIMEOUT_MS = 18e4;
9479
9596
  async function postManifestAccepted(config, payload) {
9480
9597
  const { response, environmentUsed } = await postManifestWithEnvironmentFallback(config, payload);
9481
9598
  if (environmentUsed !== config.environment) {
9482
- console.warn(
9483
- `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.`
9484
9601
  );
9485
9602
  }
9486
9603
  return response;
@@ -9496,75 +9613,69 @@ async function postManifestWithPatience(config, payload) {
9496
9613
  if (!(err instanceof PatchstackError) || err.code !== "NETWORK_TIMEOUT") throw err;
9497
9614
  const timeoutMs = Math.min(config.timeoutMs * RETRY_TIMEOUT_FACTOR, MAX_RETRY_TIMEOUT_MS);
9498
9615
  if (timeoutMs <= config.timeoutMs) throw err;
9499
- console.warn(
9500
- `patchstack: the report timed out after ${config.timeoutMs}ms; trying once more with ${timeoutMs}ms.`
9501
- );
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.`);
9502
9618
  const response = await postManifestAccepted({ ...config, timeoutMs }, payload);
9503
9619
  try {
9504
9620
  const target = await persistTimeout(process.cwd(), timeoutMs);
9505
- 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.`);
9506
9622
  } catch {
9507
- console.warn(
9508
- `patchstack: could not save the timeout; set PATCHSTACK_TIMEOUT_MS=${timeoutMs} where builds run.`
9509
- );
9623
+ console.warn(`Could not save the longer wait. Set PATCHSTACK_TIMEOUT_MS=${timeoutMs} where your builds run.`);
9510
9624
  }
9511
9625
  return response;
9512
9626
  }
9513
9627
  }
9628
+ function plural(count, one, many) {
9629
+ return `${count} ${count === 1 ? one : many}`;
9630
+ }
9514
9631
  async function runScan(args, options = {}) {
9515
9632
  const dryRun = args.flags.get("dry-run") === true;
9516
- const config = await resolveConfig({
9517
- cwd: process.cwd(),
9518
- cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
9519
- cliEndpoint: getStringFlag(args.flags, "endpoint"),
9633
+ const say = (line) => {
9634
+ if (verbose || dryRun) console.log(line);
9635
+ };
9636
+ const report = options.report ?? emptyReport();
9637
+ const config = await resolveCliConfig(args, {
9520
9638
  cliClaimToken: getStringFlag(args.flags, "claim-token"),
9521
9639
  // The one command that reports them, so the one command that resolves them.
9522
9640
  detectSiteIdentity: true
9523
9641
  });
9524
9642
  const manifest = await scanLockfile(process.cwd());
9525
9643
  for (const warning of manifest.warnings ?? []) {
9526
- console.warn(`patchstack: ${warning}`);
9644
+ if (dryRun) console.warn(`patchstack: ${warning}`);
9645
+ else report.missing.push({ text: warning });
9527
9646
  }
9528
9647
  const installPaths = args.flags.get("install-paths") === true;
9529
9648
  const { payload, stats } = buildWirePayload(manifest, { installPaths });
9530
- console.log(
9531
- `Found ${payload.packages.length} unique package versions across ${stats.uniqueNames} package names (${manifest.ecosystem} ecosystem).`
9532
- );
9649
+ say(`Found ${payload.packages.length} package versions across ${stats.uniqueNames} packages (${manifest.ecosystem}).`);
9533
9650
  if (installPaths) {
9534
9651
  const located = payload.packages.filter((pkg) => pkg.paths !== void 0).length;
9535
9652
  console.log(
9536
- 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".`
9537
9654
  );
9538
9655
  }
9539
9656
  if (config.environment === "local") {
9540
- console.log(
9541
- "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."
9542
- );
9657
+ say("Environment: local (this machine). Set PATCHSTACK_ENVIRONMENT=production on a live build your host does not identify.");
9543
9658
  } else {
9544
9659
  const because = (config.environmentEvidence ?? []).length > 0 ? ` (${config.environmentEvidence.join("; ")})` : "";
9545
- console.log(`Reporting under the ${config.environment} environment${because}. Override with PATCHSTACK_ENVIRONMENT.`);
9660
+ say(`Environment: ${config.environment}${because}.`);
9546
9661
  }
9547
9662
  if (config.endpoint !== DEFAULT_ENDPOINT) {
9548
- console.log(
9549
- `Using endpoint override: ${config.endpoint} (set via --endpoint, PATCHSTACK_ENDPOINT, or .patchstackrc.json).`
9550
- );
9663
+ say(`Endpoint override: ${config.endpoint}`);
9551
9664
  }
9552
9665
  if (stats.duplicateNames.length > 0) {
9553
9666
  const sample = stats.duplicateNames.slice(0, 10).join(", ");
9554
9667
  const more = stats.duplicateNames.length > 10 ? `, +${stats.duplicateNames.length - 10} more` : "";
9555
- console.log(
9556
- `${stats.duplicateNames.length} package(s) appear at multiple versions: ${sample}${more}`
9557
- );
9668
+ say(`${stats.duplicateNames.length} package(s) at more than one version: ${sample}${more}`);
9558
9669
  }
9559
9670
  const body = buildManifestBody(config, payload);
9560
9671
  if (typeof body.url === "string") {
9561
- console.log(`Reporting this app's address as ${body.url}.`);
9672
+ say(`Reporting app address: ${body.url}`);
9562
9673
  }
9563
9674
  if (typeof body.name === "string") {
9564
- console.log(`Reporting this app's name as "${body.name}".`);
9675
+ say(`Reporting app name: "${body.name}"`);
9565
9676
  }
9566
9677
  if (typeof config.claimToken === "string" && config.claimToken !== "") {
9567
- 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.");
9568
9679
  }
9569
9680
  if (dryRun) {
9570
9681
  console.log("");
@@ -9583,15 +9694,15 @@ async function runScan(args, options = {}) {
9583
9694
  }
9584
9695
  if (isPreBundleBuildHook()) {
9585
9696
  const cleared = applyBuildStamp(process.cwd(), null);
9586
- 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}.`);
9587
9698
  }
9588
9699
  const shellFramework = detectStack(payload.packages).framework;
9589
9700
  if (config.widget) {
9590
- reportSourceMarker(shellFramework, computeManifestChecksum(payload.packages));
9701
+ reportSourceMarker(shellFramework, computeManifestChecksum(payload.packages), report);
9591
9702
  }
9592
9703
  const provisioning = config.siteUuid === null;
9593
9704
  if (provisioning) {
9594
- 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");
9595
9706
  }
9596
9707
  let response;
9597
9708
  try {
@@ -9601,113 +9712,147 @@ async function runScan(args, options = {}) {
9601
9712
  for (const line of undeliveredReportLines(err, config, process.cwd())) console.error(line);
9602
9713
  return 0;
9603
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
+ }
9604
9728
  if (provisioning && response.uuid !== void 0 && response.uuid.length > 0) {
9605
9729
  const target = await persistSiteUuid(process.cwd(), response.uuid);
9606
- 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}.`);
9607
9732
  }
9608
9733
  if (typeof response.api_key === "string" && response.api_key.length > 0) {
9609
9734
  const hadCredentialInConfig = await credentialInCommittedConfig(process.cwd());
9610
9735
  const saved = await persistApiKey(process.cwd(), response.api_key);
9611
- console.log(`Saved API key to ${saved.path}. Do not commit it.`);
9612
- console.log(
9613
- saved.ignored ? " Added to .gitignore." : ` NOT ignored by git \u2014 ${saved.reason ?? "unknown reason"}. Add \`${SECRET_CONFIG_FILENAME}\` to .gitignore yourself before committing.`
9614
- );
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
+ }
9615
9746
  if (hadCredentialInConfig) {
9616
- console.log(
9617
- "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."
9618
- );
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
+ });
9619
9753
  }
9620
9754
  }
9621
- if (response.stored) {
9622
- console.log(`Stored manifest #${response.manifest_id} (checksum ${response.checksum}).`);
9623
- } else if (response.reason === "duplicate") {
9624
- console.log("Manifest unchanged since last scan \u2014 nothing to store.");
9625
- } else {
9626
- console.log(`Server response: ${response.message ?? JSON.stringify(response)}`);
9627
- }
9628
- const claimLines = claimOutcomeLines(response.claim, config);
9629
- if (claimLines.length > 0) {
9630
- console.log("");
9631
- 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]));
9632
9760
  }
9633
9761
  const connected = response.claim?.state === "claimed" || response.claim?.state === "owned-by-you";
9634
9762
  const effectiveUuid = config.siteUuid ?? response.uuid ?? null;
9635
9763
  if (config.widget && effectiveUuid !== null && effectiveUuid.length > 0) {
9636
- reportSourceWidget(effectiveUuid, shellFramework);
9637
- }
9638
- const linkUuid = response.uuid ?? config.siteUuid;
9639
- if (!connected && (provisioning || claimLines.length > 0) && linkUuid !== null && linkUuid !== void 0 && linkUuid.length > 0) {
9640
- console.log("");
9641
- console.log("Connect this site to your Patchstack account \u2014 any one of these:");
9642
- if (config.widget) {
9643
- console.log(' 1. In the preview \u2014 the widget shows a "Connect this website" panel while the');
9644
- console.log(" site is unclaimed. Signing in there attaches it.");
9645
- console.log(" 2. Open this dashboard link in a browser:");
9646
- console.log(` ${buildClaimUrl(config.endpoint, linkUuid)}`);
9647
- console.log(" 3. From this terminal: npx @patchstack/connect claim");
9648
- } else {
9649
- console.log(" 1. Open this dashboard link in a browser:");
9650
- console.log(` ${buildClaimUrl(config.endpoint, linkUuid)}`);
9651
- console.log(" 2. From this terminal: npx @patchstack/connect claim");
9652
- }
9653
- if (config.endpoint !== DEFAULT_ENDPOINT) {
9654
- console.log(" (this URL inherits the endpoint override above)");
9655
- }
9656
- }
9657
- 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) {
9658
9775
  try {
9659
- const state = await collectGuideState(process.cwd());
9660
- const remaining = countRemainingSteps(state);
9661
- if (remaining > 0) {
9662
- const hooksMissing = !(state.prebuildWired && state.postbuildWired);
9663
- console.log("");
9664
- console.log(
9665
- `Setup not complete \u2014 ${remaining} step(s) remaining${hooksMissing ? ", including the package.json build hooks (which scan can't wire)" : ""}.`
9666
- );
9667
- console.log("Run `npx @patchstack/connect guide` for the exact steps to finish for this project.");
9668
- }
9776
+ await printScanReport(config, effectiveUuid, outcome, report, options.showRemainingSetup !== false);
9669
9777
  } catch {
9670
9778
  }
9671
9779
  }
9672
9780
  return 0;
9673
9781
  }
9674
- 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
+ };
9675
9826
  try {
9676
9827
  const hint = hasJsxShell(framework) ? resolveWidgetFileHint(process.cwd(), framework) : null;
9677
9828
  const jsxShell = hint !== null && !hint.toLowerCase().endsWith(".html") ? hint : null;
9678
9829
  const result = ensureSourceWidget(process.cwd(), siteUuid, jsxShell);
9679
9830
  switch (result.action) {
9680
9831
  case "added":
9681
- console.log(`Widget: added the disclosure widget tag to ${result.shell}. Reload your preview to see it.`);
9682
- 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}`);
9683
9833
  break;
9684
9834
  case "updated":
9685
- console.log(`Widget: updated the managed tag in ${result.shell} to site ${siteUuid}.`);
9835
+ report.done.push(`Updated the Patchstack widget in ${result.shell}`);
9686
9836
  break;
9687
9837
  case "unchanged":
9688
- console.log(`Widget: already installed in ${result.shell}.`);
9838
+ report.done.push(`The Patchstack widget is already in ${result.shell}`);
9689
9839
  break;
9690
9840
  case "manual":
9691
- 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`);
9692
9842
  break;
9693
9843
  case "no-body":
9694
- console.log(`Widget: ${result.shell} has no </body> tag to anchor on. Add this tag to your root layout manually:`);
9695
- console.log(` ${buildWidgetTag(siteUuid)}`);
9844
+ handAdd(`${result.shell} has no </body>. Add this to your main page layout, just before </body>:`);
9696
9845
  break;
9697
9846
  case "no-shell":
9698
- console.log("Widget: no root shell found to edit (index.html / public/index.html / src/app.html, or a JSX root).");
9699
- console.log("Add this tag to your root layout before </body> (run `guide` for framework-specific placement):");
9700
- console.log(` ${buildWidgetTag(siteUuid)}`);
9847
+ handAdd("Add this to your main page layout, just before </body>:");
9701
9848
  break;
9702
9849
  }
9703
- if (result.action === "added" || result.action === "updated") {
9704
- console.log(' (opt out any time with "widget": false in .patchstackrc.json)');
9705
- }
9706
9850
  } catch (err) {
9707
- console.warn(`Widget: skipped (${err.message}).`);
9851
+ handAdd("Add this to your main page layout, just before </body>:");
9852
+ detail(`Widget: skipped (${err.message}).`);
9708
9853
  }
9709
9854
  }
9710
- function reportSourceMarker(framework, checksum = null) {
9855
+ function reportSourceMarker(framework, checksum, report) {
9711
9856
  try {
9712
9857
  const shell = resolveWidgetFileHint(process.cwd(), framework);
9713
9858
  if (shell === null || shell.toLowerCase().endsWith(".html")) {
@@ -9716,30 +9861,25 @@ function reportSourceMarker(framework, checksum = null) {
9716
9861
  const result = ensureSourceMarker(process.cwd(), shell, framework, checksum);
9717
9862
  switch (result.action) {
9718
9863
  case "added":
9719
- console.log(`Production marker: added to ${shell} (guarded by ${productionGate(framework)}).`);
9720
- 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)}).`);
9721
9866
  break;
9722
9867
  case "manual":
9723
- console.log(`Production marker: already set in ${shell} \u2014 left untouched.`);
9868
+ detail(`Production marker: already in ${shell}.`);
9724
9869
  break;
9725
9870
  case "no-anchor":
9726
9871
  case "unsupported":
9727
- console.log(
9728
- `Production marker: ${shell} needs it by hand \u2014 without it the widget reads the published site as build mode.`
9729
- );
9730
- if (hasJsxShell(framework)) {
9731
- for (const line of buildSourceMarkerSnippet(framework).split("\n")) {
9732
- console.log(` ${line}`);
9733
- }
9734
- } else {
9735
- console.log(
9736
- ` Emit <script>window.__PATCHSTACK_PROD__=true;</script> only when ${productionGate(framework)}.`
9737
- );
9738
- }
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
+ });
9739
9879
  break;
9740
9880
  }
9741
9881
  } catch (err) {
9742
- console.warn(`Production marker: skipped (${err.message}).`);
9882
+ detail(`Production marker: skipped (${err.message}).`);
9743
9883
  }
9744
9884
  }
9745
9885
  async function runProtectCommand(args) {
@@ -9788,12 +9928,7 @@ async function runDemoCommand(args) {
9788
9928
  try {
9789
9929
  const scenario = resolveDemoScenario(args.positional[0]);
9790
9930
  const cwd = process.cwd();
9791
- const config = await resolveConfig({
9792
- cwd,
9793
- cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
9794
- cliEndpoint: getStringFlag(args.flags, "endpoint"),
9795
- requireSiteUuid: true
9796
- });
9931
+ const config = await resolveCliConfig(args, { requireSiteUuid: true });
9797
9932
  if (config.environment !== "production") {
9798
9933
  throw new DemoError(
9799
9934
  "The production-backed demo requires PATCHSTACK_ENVIRONMENT=production. Unset the sandbox override and try again."
@@ -9884,8 +10019,7 @@ async function runGuide(args) {
9884
10019
  let allDone = false;
9885
10020
  try {
9886
10021
  const state = await collectGuideState(process.cwd());
9887
- const useColor = process.stdout.isTTY === true && process.env.NO_COLOR === void 0;
9888
- console.log(renderGuideChecklist(state, useColor));
10022
+ console.log(renderGuideChecklist(state, useColor(), {}, { verbose }));
9889
10023
  allDone = countRemainingSteps(state) === 0;
9890
10024
  } catch {
9891
10025
  }
@@ -9903,102 +10037,87 @@ async function runGuide(args) {
9903
10037
  console.log(readFileSync19(guidePath, "utf8"));
9904
10038
  return 0;
9905
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
+ }
9906
10073
  async function runSetup(args) {
9907
10074
  if (args.flags.get("dry-run") === true) {
9908
- 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");
9909
10076
  return 1;
9910
10077
  }
9911
10078
  const before = await collectGuideState(process.cwd());
9912
10079
  if (!before.hasPackageJson) {
9913
- 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.");
9914
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.");
9915
10082
  return 1;
9916
10083
  }
9917
10084
  if (before.installed === null) {
9918
10085
  console.error(
9919
- `Error: @patchstack/connect is not declared in package.json.
10086
+ `The Patchstack connector is not installed in this project yet.
9920
10087
  Run: ${installCommand(before.packageManager)}`
9921
10088
  );
9922
10089
  return 1;
9923
10090
  }
9924
- console.log("Patchstack setup \u2014 applying bounded project changes");
9925
- console.log(" 1. Scan dependencies, provision/reuse the site, and manage the source widget");
9926
- 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
+ });
9927
10099
  if (scanCode !== 0) {
9928
10100
  return scanCode;
9929
10101
  }
9930
- console.log("");
9931
- console.log(" 2. Install and verify runtime protection");
9932
- const protection = setupProtection(process.cwd());
9933
- if (protection.install.status === "not-applicable") {
9934
- console.log("Runtime protection: not applicable to this project \u2014 nothing installed.");
9935
- console.log(` ${protection.install.reason}`);
9936
- if (protection.install.leftovers.length > 0) {
9937
- console.log(" An earlier run scaffolded a guard here before that was established. These files do");
9938
- console.log(" nothing on this project and can be deleted:");
9939
- for (const file of protection.install.leftovers) console.log(` ${file}`);
9940
- }
9941
- } else if (protection.verification.wired) {
9942
- console.log(`Runtime protection: wired (${protection.verification.stack}).`);
9943
- } else {
9944
- console.log(`Runtime protection: manual wiring remains (${protection.verification.stack}):`);
9945
- for (const check of protection.verification.checks) {
9946
- console.log(` ${check.ok ? "\u2713" : "\u2717"} ${check.label}${!check.ok && check.hint ? ` \u2014 ${check.hint}` : ""}`);
9947
- }
9948
- console.log("Run `npx @patchstack/connect protect --check` after completing the failed checks.");
9949
- }
9950
- console.log("");
9951
- console.log(" 3. Wire dependency-install and production-build scans into package.json");
10102
+ reportProtection(setupProtection(process.cwd()), report);
9952
10103
  const wired = wireBuildScripts(process.cwd(), before.packageManager);
9953
- console.log(`Build integration: ${wired.detail}`);
9954
- console.log("");
9955
- console.log(" 4. Verify setup status");
10104
+ report.done.push(buildStepsLine(wired));
10105
+ detail(`Build hooks: ${wired.detail}`);
10106
+ const config = await resolveCliConfig(args);
9956
10107
  const after = await collectGuideState(process.cwd());
9957
- const useColor = process.stdout.isTTY === true && process.env.NO_COLOR === void 0;
9958
- console.log(renderGuideChecklist(after, useColor));
9959
- const remaining = countRemainingSteps(after);
9960
- if (remaining > 0) {
9961
- console.log("");
9962
- 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);
9963
10116
  }
9964
- const outcome = setupOutcome(after, protection, wired);
9965
- console.log("");
9966
- console.log("Outcome \u2014 relay this to the user as it is:");
9967
- for (const [label, value] of outcome) console.log(` ${label}: ${value}`);
9968
10117
  return 0;
9969
10118
  }
9970
- function setupOutcome(state, protection, wired) {
9971
- const remaining = countRemainingSteps(state);
9972
- const lines = [];
9973
- 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)`]);
9974
- lines.push(["Monitoring", "starts with the first build that runs on your hosting platform"]);
9975
- if (protection.install.status === "not-applicable") {
9976
- lines.push(["Runtime protection", "not applicable \u2014 this project has no request path (static build)"]);
9977
- } else if (protection.verification.wired) {
9978
- lines.push(["Runtime protection", `wired (${protection.verification.stack}) \u2014 local wiring verified; verify against the live site after deploy with \`protect --check --runtime\``]);
9979
- } else {
9980
- lines.push(["Runtime protection", `not wired yet (${protection.verification.stack}) \u2014 finish the checks above, then \`npx @patchstack/connect protect --check\``]);
9981
- }
9982
- lines.push(["Next steps", [
9983
- "add PATCHSTACK_API_KEY (from .patchstackrc.local.json) to your hosting platform's environment variables",
9984
- "commit .patchstackrc.json, package.json and the widget change (never .patchstackrc.local.json)",
9985
- 'deploy, then check the site in the Patchstack dashboard \u2014 it reads "Deployed" once the live site is seen'
9986
- ].join("; ")]);
9987
- const warnings = [];
9988
- if (!wired.changed && wired.strategy === "postinstall-only") warnings.push("no build script, so only dependency installs are scanned");
9989
- if (protection.install.status === "not-applicable" && protection.install.leftovers.length > 0) {
9990
- warnings.push(`earlier guard scaffold does nothing here and can be deleted: ${protection.install.leftovers.join(", ")}`);
9991
- }
9992
- 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');
9993
- if (warnings.length > 0) lines.push(["Warnings", warnings.join("; ")]);
9994
- return lines;
9995
- }
9996
10119
  async function runStatus(args) {
9997
- const config = await resolveConfig({
9998
- cwd: process.cwd(),
9999
- cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
10000
- cliEndpoint: getStringFlag(args.flags, "endpoint")
10001
- });
10120
+ const config = await resolveCliConfig(args);
10002
10121
  console.log(`Site UUID: ${config.siteUuid ?? "(none yet \u2014 the next `scan` will provision one)"}`);
10003
10122
  console.log(
10004
10123
  `Endpoint: ${config.endpoint}${config.endpoint === DEFAULT_ENDPOINT ? "" : " (override)"}`
@@ -10007,24 +10126,14 @@ async function runStatus(args) {
10007
10126
  console.log(`Environment: ${config.environment}`);
10008
10127
  if (config.siteUuid !== null) {
10009
10128
  console.log(`Dashboard URL: ${buildClaimUrl(config.endpoint, config.siteUuid)}`);
10010
- console.log(` Not attached to an account yet? Sign in through the widget's "Connect this`);
10011
- console.log(' website" panel on the preview, open the link above, or run');
10012
- console.log(" `npx @patchstack/connect claim`.");
10129
+ console.log(" Not connected yet? Open the link above or run `npx @patchstack/connect claim`.");
10013
10130
  switch (await fetchSiteStatus(config)) {
10014
10131
  case "active":
10015
10132
  console.log("Site status: active on Patchstack");
10016
10133
  break;
10017
10134
  case "removed":
10018
10135
  console.log("Site status: removed from Patchstack");
10019
- console.log(
10020
- " The site record no longer exists (deleted from the dashboard or via the"
10021
- );
10022
- console.log(
10023
- " widget uninstall flow). The local integration files are still in this"
10024
- );
10025
- console.log(
10026
- ' project \u2014 see "Uninstalling" in AGENT-INSTALL.md to remove them.'
10027
- );
10136
+ console.log(' The local files are still here. See "Uninstalling" in AGENT-INSTALL.md to remove them.');
10028
10137
  break;
10029
10138
  case "unknown":
10030
10139
  console.log("Site status: could not be verified (Patchstack unreachable)");
@@ -10034,11 +10143,7 @@ async function runStatus(args) {
10034
10143
  return 0;
10035
10144
  }
10036
10145
  async function runUninstall(args) {
10037
- const config = await resolveConfig({
10038
- cwd: process.cwd(),
10039
- cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
10040
- cliEndpoint: getStringFlag(args.flags, "endpoint")
10041
- });
10146
+ const config = await resolveCliConfig(args);
10042
10147
  if (config.siteUuid === null) {
10043
10148
  console.log("No site UUID configured \u2014 there is no site record to signal about.");
10044
10149
  console.log('Continue with the local removal steps in AGENT-INSTALL.md ("Uninstalling").');
@@ -10063,8 +10168,7 @@ async function runUninstall(args) {
10063
10168
  break;
10064
10169
  }
10065
10170
  console.log("");
10066
- console.log("This command only signals Patchstack. The local integration files must still be");
10067
- 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.');
10068
10172
  return 0;
10069
10173
  }
10070
10174
  function describeStack(stack) {
@@ -10078,7 +10182,7 @@ function describeStack(stack) {
10078
10182
  }
10079
10183
  function readBuildScript(cwd) {
10080
10184
  try {
10081
- const pkg = JSON.parse(readFileSync19(path17.join(cwd, "package.json"), "utf8"));
10185
+ const pkg = JSON.parse(readFileSync19(path16.join(cwd, "package.json"), "utf8"));
10082
10186
  return pkg.scripts?.build;
10083
10187
  } catch {
10084
10188
  return void 0;
@@ -10097,7 +10201,7 @@ async function reportBuildStamp(config, payload, marker) {
10097
10201
  await postManifestWithEnvironmentFallback(bounded, payload, marker);
10098
10202
  } catch (err) {
10099
10203
  console.warn(
10100
- `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.`
10101
10205
  );
10102
10206
  }
10103
10207
  }
@@ -10159,22 +10263,14 @@ async function runMarkBuild(args) {
10159
10263
  const files = findHtmlFiles(dir);
10160
10264
  if (files.length === 0) {
10161
10265
  console.warn(`mark-build: found ${dir} but no HTML files in it.`);
10162
- console.warn(
10163
- "mark-build: this build looks server-rendered, so the production flag has nothing to stamp."
10164
- );
10165
- console.warn(
10166
- "mark-build: add the marker to your root shell instead \u2014 run `patchstack-connect guide` for the snippet."
10167
- );
10266
+ console.warn("mark-build: this looks like a server-rendered app. Run `npx @patchstack/connect guide` to see what your main layout needs.");
10168
10267
  await reportBuildStamp(reported, wirePayload, "no-pages");
10169
10268
  return 0;
10170
10269
  }
10171
10270
  const staleBy = staleBuildAge(files, startedAt);
10172
10271
  if (staleBy !== null) {
10173
10272
  console.warn(
10174
- `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.`
10175
- );
10176
- console.warn(
10177
- "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).`
10178
10274
  );
10179
10275
  }
10180
10276
  const snippet = published ? buildInjectionSnippet(checksum, stack) : "";
@@ -10197,7 +10293,8 @@ async function runMarkBuild(args) {
10197
10293
  }
10198
10294
  const stackSummary = stack !== null ? describeStack(stack) : null;
10199
10295
  if (published) {
10200
- console.log(
10296
+ console.log(`Patchstack: marked ${plural(files.length, "page", "pages")} as your live app.`);
10297
+ detail(
10201
10298
  `mark-build: marked ${marked} HTML file(s) in ${dir}${checksum !== null ? ` (build ${checksum})` : ""}${widgetTouched > 0 ? `, widget tag ensured in ${widgetTouched}` : ""}${stackSummary !== null ? ` [${stackSummary}]` : ""}.`
10202
10299
  );
10203
10300
  await reportBuildStamp(reported, wirePayload, "stamped");
@@ -10205,13 +10302,10 @@ async function runMarkBuild(args) {
10205
10302
  }
10206
10303
  const because = environmentEvidence.length > 0 ? ` (${environmentEvidence.join("; ")})` : "";
10207
10304
  console.log(
10208
- `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}]` : ""}.`
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.`
10209
10306
  );
10210
- console.log(
10211
- "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."
10212
- );
10213
- console.log(
10214
- "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}]` : ""}.`
10215
10309
  );
10216
10310
  await reportBuildStamp(reported, wirePayload, "withheld");
10217
10311
  return 0;
@@ -10225,8 +10319,28 @@ function packageVersion() {
10225
10319
  return "unknown";
10226
10320
  }
10227
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
+ }
10228
10341
  async function main() {
10229
10342
  const args = parseArgs(process.argv);
10343
+ verbose = args.flags.get("verbose") === true;
10230
10344
  if (args.flags.has("version") || args.command === "version") {
10231
10345
  console.log(packageVersion());
10232
10346
  return 0;
@@ -10271,7 +10385,9 @@ async function main() {
10271
10385
  }
10272
10386
  main().then((code) => process.exit(code)).catch((err) => {
10273
10387
  if (err instanceof PatchstackError) {
10274
- 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}`);
10275
10391
  process.exit(1);
10276
10392
  }
10277
10393
  console.error("Unexpected error:", err);