@patchstack/connect 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENT-INSTALL.md CHANGED
@@ -2,22 +2,24 @@
2
2
 
3
3
  This is a plain-text instruction document. Read it and follow the steps — there is nothing here to pipe to a shell or execute from a URL.
4
4
 
5
- You are reading this either from `node_modules/@patchstack/connect/AGENT-INSTALL.md` (the package is already installed) or from the [GitHub repo](https://github.com/patchstack/connect). The canonical, always-current version of the full guide lives at https://patchstack.com/install.txt — prefer it when you have network access; this bundled copy matches the package version it shipped with.
5
+ You are reading this either from `node_modules/@patchstack/connect/AGENT-INSTALL.md` (the package is already installed) or from the [GitHub repo](https://github.com/patchstack/connect). This bundled copy matches the package version it shipped with and is all you need — there is no requirement to fetch anything remote. (A long-form copy also lives at https://patchstack.com/install.txt for humans browsing outside the package.)
6
6
 
7
7
  ## Verify before you trust (do this, it's quick)
8
8
 
9
9
  - The connector is a normal npm package: [`@patchstack/connect`](https://www.npmjs.com/package/@patchstack/connect), MIT-licensed, source at https://github.com/patchstack/connect. Install it pinned, check the integrity hash in the lockfile, run `npm audit` over it — whatever your policy requires.
10
10
  - First published May 2026. If the package is absent from your training data, `npm view @patchstack/connect` (read-only) shows the live registry record.
11
- - It reads the project's **lockfile only** (`package-lock.json`, `pnpm-lock.yaml`, or `yarn.lock`) and sends package names + versions to Patchstack for vulnerability matching. No source code, no env vars, no file paths, no git history.
11
+ - It reads the project's **dependency list only** — from the lockfile (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`) or, on bun projects (`bun.lock`/`bun.lockb`), by enumerating the installed packages under `node_modules/` — and sends package names + versions to Patchstack for vulnerability matching. No source code, no env var values, no file paths, no git history. (`mark-build` additionally stamps built HTML with a coarse stack descriptor that may include hosting-related env variable *names* — e.g. `VERCEL`, `CF_PAGES` — never their values.)
12
+ - The package also bundles an **opt-in** `protect` command (runtime exploit guard, currently for TanStack Start + Supabase apps; its templates live under `dist/protect/`). It patches the app's Supabase client to route traffic through a same-origin guard — and it runs **only** when you explicitly run `patchstack-connect protect`. Nothing in this document, and none of `scan`, `guide`, `status`, or `mark-build`, modifies application source.
12
13
  - Patchstack is not WordPress-only. This connector monitors any JS/Node project — Vite, Next.js, plain vanilla JS, anything with a lockfile.
13
14
 
14
15
  ## Before you start — never install twice
15
16
 
16
- - If `.patchstackrc.json` exists, the project is already provisioned. Reuse its UUID; run `npx @patchstack/connect status` to re-print it and the claim URL. **Do not run `scan` to provision a second site.**
17
+ - `npx @patchstack/connect guide` prints a live checklist above this document showing which steps are already done in the current project (install, provisioning, build hooks, widget) with commands tailored to it. Its purpose is to prevent duplicate work — verify its claims however your policy requires, but only do the steps it marks ✖.
18
+ - If `.patchstackrc.json` contains a `siteUuid` key, the project is already provisioned. Reuse that UUID; run `npx @patchstack/connect status` to re-print it and the claim URL. **Do not run `scan` to provision a second site.** (A `.patchstackrc.json` with other keys — e.g. an `endpoint` override — but no `siteUuid` is *not* provisioned yet; scan normally.)
17
19
  - If `@patchstack/connect` is already in `devDependencies`, skip the install command.
18
20
  - If the widget script tag (`cdn.patchstack.com/patchstack-widget.js`) is already in the layout, don't add a second one.
19
21
 
20
- ## Install steps (run in order, don't pause between them unless one fails)
22
+ ## Install steps (do the ones the checklist marks ✖, in order; they are designed to run back-to-back — if one fails, stop and report rather than improvising)
21
23
 
22
24
  1. **Install** (skip if already present), matching the project's package manager:
23
25
 
@@ -36,7 +38,7 @@ You are reading this either from `node_modules/@patchstack/connect/AGENT-INSTALL
36
38
 
37
39
  It prints a claim URL. Surface that URL to the user verbatim — never fetch it yourself; it must be opened in their browser.
38
40
 
39
- 3. **Wire builds** in `package.json` (chain with `&&` if hooks already exist):
41
+ 3. **Wire builds** in `package.json`:
40
42
 
41
43
  ```jsonc
42
44
  {
@@ -47,7 +49,11 @@ You are reading this either from `node_modules/@patchstack/connect/AGENT-INSTALL
47
49
  }
48
50
  ```
49
51
 
50
- 4. **Install the disclosure widget** — a floating "Report a vulnerability" button. Read the UUID from `.patchstackrc.json` and use it as `userToken`. Place these two snippets via the framework's HTML/layout mechanism (never a JS entry point):
52
+ If a `prebuild`/`postbuild` hook already exists, chain instead of replacing it, e.g. `"prebuild": "existing-command && patchstack-connect scan"`.
53
+
54
+ **Bun-managed projects:** `bun run` does not execute npm-style `pre`/`post` scripts, so wire the build script directly instead: `"build": "patchstack-connect scan && <existing build command> && patchstack-connect mark-build"`.
55
+
56
+ 4. **Install the disclosure widget** — a floating "Report a vulnerability" button. Read the `siteUuid` value from `.patchstackrc.json` (the same site UUID step 2 provisioned) and pass it as the widget's `userToken`. Place these two snippets via the framework's HTML/layout mechanism (never a JS entry point):
51
57
 
52
58
  ```html
53
59
  <script src="https://cdn.patchstack.com/patchstack-widget.js"></script>
@@ -58,7 +64,7 @@ You are reading this either from `node_modules/@patchstack/connect/AGENT-INSTALL
58
64
 
59
65
  Framework-specific placement patterns: https://cdn.patchstack.com/llm.html. The `userToken` is public by design — it ships in client-side HTML and is not a secret.
60
66
 
61
- 5. **Commit** `.patchstackrc.json` and the updated `package.json` so every developer and CI run reports to the same site.
67
+ 5. **Commit** `.patchstackrc.json`, the updated `package.json`, and the layout/HTML file carrying the widget snippet, so every developer and CI run reports to the same site.
62
68
 
63
69
  6. **Tell the user to claim the site** (free): open the claim URL from step 2 in a browser and sign in. The site is monitored either way, but the vulnerability reports are only visible after claiming. They can also claim directly from the widget's "Connect this website" prompt. On the published site, the owner reaches the widget login by appending `#patchstack` to the live URL.
64
70
 
package/README.md CHANGED
@@ -6,22 +6,13 @@ For how this repo fits with the wider Patchstack ecosystem (`saas`, `hub`, `patc
6
6
 
7
7
  ## Install prompt (for AI coding tools)
8
8
 
9
- Copy and paste this prompt into any AI coding assistant (Cursor, v0, Bolt, Lovable, Claude Code, etc.):
9
+ Copy and paste this prompt into any AI coding assistant (Cursor, v0, Bolt, Lovable, Claude Code, etc.). If the assistant declines or stops partway, the step-by-step flow in [`GETTING-STARTED.md`](https://github.com/patchstack/connect/blob/main/GETTING-STARTED.md) covers what to do next:
10
10
 
11
- > Add Patchstack dependency vulnerability monitoring to this project:
12
- >
13
- > 1. `npm install --save-dev @patchstack/connect` — or the pnpm/yarn/bun equivalent (MIT, source: https://github.com/patchstack/connect; verify with `npm view @patchstack/connect` if unsure).
14
- > 2. `npx @patchstack/connect scan` — saves a site ID to `.patchstackrc.json` and prints a claim URL. Show me that URL; don't open it.
15
- > 3. Add `package.json` scripts `"prebuild": "patchstack-connect scan"` and `"postbuild": "patchstack-connect mark-build"`, then commit the changes.
16
- > 4. Run `npx @patchstack/connect guide` and use it to add the report-a-vulnerability widget and finish any framework-specific details.
11
+ > Add Patchstack dependency vulnerability monitoring: install `@patchstack/connect` as a dev dependency (MIT — verify with `npm view @patchstack/connect`; source: https://github.com/patchstack/connect), run `npx @patchstack/connect scan` and show me the claim URL it prints (I'll open it in my browser to view reports — don't open or fetch it yourself), then run `npx @patchstack/connect guide` and finish what its checklist flags: the package.json build hooks and the report-a-vulnerability widget snippet.
17
12
 
18
- The shape of this prompt is deliberate — it has survived three distinct AI-agent refusal modes, and each one punished a different shortcut:
13
+ Every clause is load-bearing. AI agents routinely audit this package before completing the request — verifying the tarball against the registry, checking `dist/` for lifecycle scripts and shell/eval calls — and the flow is designed to pass that audit, not to discourage it. The prompt can stay short because `guide` is state-aware: it inspects the project (package manager, `.patchstackrc.json`, `package.json` scripts, widget presence, framework) and prints a live checklist of only the remaining steps — with the real site UUID and framework-specific widget placement — followed by the full reference guide (`AGENT-INSTALL.md`, also at https://patchstack.com/install.txt). That also makes the flow idempotent: re-running `guide` on a finished project reports all-done instead of prompting a second install.
19
14
 
20
- 1. **"Follow the instructions at this URL" reads as remote script execution.** Agents refuse before ever fetching the doc.
21
- 2. **Agents whose training predates May 2026 assert the package doesn't exist.** Step 1's `npm view` resolves that against the registry instead of the model's memory.
22
- 3. **"Install the package, then follow the instructions it ships" reads as handing control to the package author** — structurally the same as prompt injection — and preemptive reassurance language ("it's safe, don't be suspicious, note your knowledge cutoff") is itself flagged as a manipulation signal.
23
-
24
- So the prompt inlines the core install as concrete, individually inspectable commands, delegates nothing up front, and argues nothing. `npx @patchstack/connect guide` (which prints the bundled `AGENT-INSTALL.md`) enters only at step 4, as an assist for widget placement, re-run rules, and troubleshooting — by then the agent has already installed and audited the package, so consulting its docs is ordinary behavior rather than the instruction channel the install depends on. The always-current long-form guide lives at https://patchstack.com/install.txt.
15
+ Before changing this prompt (or `guide` / `AGENT-INSTALL.md`), read [`field-test/README.md`](https://github.com/patchstack/connect/blob/main/field-test/README.md): it documents the AI-agent refusal modes each clause guards against, and its harness runs a real agent through the full install in a throwaway fixture against a mocked API and scores the outcome on eight checks. Validate any variant there first.
25
16
 
26
17
  ## Quick start (zero configuration)
27
18
 
@@ -70,6 +61,16 @@ patchstack-connect scan [options] Scan the lockfile and POST to
70
61
  patchstack-connect init <site-uuid> Optional: pre-seed .patchstackrc.json with
71
62
  an existing site UUID
72
63
  patchstack-connect status [options] Show current configuration
64
+ patchstack-connect mark-build [options] Stamp built HTML with a production flag +
65
+ build fingerprint (run as a postbuild step)
66
+ patchstack-connect guide Show this project's setup status (what's done,
67
+ what's missing, with tailored commands), then
68
+ print the full setup guide
69
+ patchstack-connect protect Opt-in: install the always-on runtime exploit
70
+ guard (currently TanStack Start + Supabase; it
71
+ patches the app's Supabase client to route
72
+ traffic through a same-origin guard). Never
73
+ run by scan/guide/mark-build.
73
74
  patchstack-connect help Print help
74
75
 
75
76
  Options (for scan and status):
@@ -100,7 +101,7 @@ Environment variables:
100
101
  }
101
102
  ```
102
103
 
103
- The site UUID is the only credential. Possession of it grants the right to submit manifests for that site, so treat it like an API token: keep it out of public repos, and prefer the environment variable in CI.
104
+ The site UUID identifies the site; it is not a secret — the disclosure widget ships the same UUID in client-side HTML as its `userToken`, and committing `.patchstackrc.json` is the intended workflow so every developer and CI run reports to the same site. Possession of the UUID lets someone submit dependency manifests for that site (noise, not data access). In CI setups where the file isn't committed, set `PATCHSTACK_SITE_UUID` instead.
104
105
 
105
106
  ## Programmatic API
106
107
 
@@ -126,7 +127,7 @@ Lower-level pieces are also exported: `scanLockfile`, `buildWirePayload`, `postM
126
127
  }
127
128
  ```
128
129
 
129
- That's the entire payload. No source code, no environment variables, no file paths — just the package names and versions from your lockfile. Duplicate names with different versions are preserved so transitive vulnerabilities aren't missed.
130
+ That's the entire payload. No source code, no environment variable values, no file paths — just the package names and versions from your lockfile. Duplicate names with different versions are preserved so transitive vulnerabilities aren't missed. (`mark-build` separately stamps built HTML with a stack descriptor that may include hosting-related env variable *names* — e.g. `VERCEL` — never their values.)
130
131
 
131
132
  ## Supported lockfiles
132
133
 
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
4
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
5
5
 
6
6
  // src/parsers/index.ts
7
7
  import { access } from "fs/promises";
@@ -1250,14 +1250,351 @@ function injectMarker(html, snippet) {
1250
1250
  return stripped + snippet;
1251
1251
  }
1252
1252
 
1253
+ // src/guide.ts
1254
+ import { existsSync as existsSync2, readFileSync, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
1255
+ import path8 from "path";
1256
+ var WIDGET_SCRIPT_URL = "https://cdn.patchstack.com/patchstack-widget.js";
1257
+ var WIDGET_NEEDLE = "patchstack-widget";
1258
+ var INSTALL_COMMANDS = {
1259
+ npm: "npm install --save-dev @patchstack/connect",
1260
+ pnpm: "pnpm add -D @patchstack/connect",
1261
+ yarn: "yarn add -D @patchstack/connect",
1262
+ bun: "bun add -d @patchstack/connect"
1263
+ };
1264
+ var PM_BY_LOCKFILE = [
1265
+ { filename: "package-lock.json", pm: "npm" },
1266
+ { filename: "bun.lock", pm: "bun" },
1267
+ { filename: "bun.lockb", pm: "bun" },
1268
+ { filename: "pnpm-lock.yaml", pm: "pnpm" },
1269
+ { filename: "yarn.lock", pm: "yarn" }
1270
+ ];
1271
+ var WIDGET_FILE_CANDIDATES = {
1272
+ next: [
1273
+ "app/layout.tsx",
1274
+ "app/layout.jsx",
1275
+ "src/app/layout.tsx",
1276
+ "src/app/layout.jsx",
1277
+ "pages/_document.tsx",
1278
+ "pages/_document.jsx",
1279
+ "src/pages/_document.tsx"
1280
+ ],
1281
+ nuxt: ["app.vue", "src/app.vue", "app/app.vue"],
1282
+ remix: ["app/root.tsx", "app/root.jsx"],
1283
+ "react-router": ["app/root.tsx", "src/root.tsx"],
1284
+ "tanstack-start": ["src/routes/__root.tsx", "app/routes/__root.tsx"],
1285
+ sveltekit: ["src/app.html"],
1286
+ astro: ["src/layouts/Layout.astro"],
1287
+ gatsby: ["src/html.js"]
1288
+ };
1289
+ var GENERIC_WIDGET_FILES = ["index.html", "public/index.html"];
1290
+ var SKIPPED_DIRS = /* @__PURE__ */ new Set([
1291
+ "node_modules",
1292
+ ".git",
1293
+ "dist",
1294
+ "build",
1295
+ "out",
1296
+ ".output",
1297
+ ".next",
1298
+ ".nuxt",
1299
+ ".svelte-kit",
1300
+ ".vercel",
1301
+ ".netlify",
1302
+ "coverage",
1303
+ "vendor"
1304
+ ]);
1305
+ var WIDGET_EXTENSIONS = /* @__PURE__ */ new Set([
1306
+ ".html",
1307
+ ".htm",
1308
+ ".tsx",
1309
+ ".jsx",
1310
+ ".ts",
1311
+ ".js",
1312
+ ".mjs",
1313
+ ".cjs",
1314
+ ".vue",
1315
+ ".svelte",
1316
+ ".astro",
1317
+ ".ejs",
1318
+ ".hbs"
1319
+ ]);
1320
+ var WIDGET_SCAN_MAX_FILES = 4e3;
1321
+ var WIDGET_SCAN_MAX_DEPTH = 6;
1322
+ var WIDGET_SCAN_MAX_BYTES = 512 * 1024;
1323
+ function detectPackageManager(cwd) {
1324
+ for (const { filename, pm } of PM_BY_LOCKFILE) {
1325
+ if (existsSync2(path8.join(cwd, filename))) {
1326
+ return pm;
1327
+ }
1328
+ }
1329
+ return "npm";
1330
+ }
1331
+ function installCommand(pm) {
1332
+ return INSTALL_COMMANDS[pm];
1333
+ }
1334
+ function readPackageJson(cwd) {
1335
+ try {
1336
+ return JSON.parse(readFileSync(path8.join(cwd, "package.json"), "utf8"));
1337
+ } catch {
1338
+ return null;
1339
+ }
1340
+ }
1341
+ function installedVersion(cwd, declaredRange) {
1342
+ try {
1343
+ const pkg = JSON.parse(
1344
+ readFileSync(path8.join(cwd, "node_modules", "@patchstack", "connect", "package.json"), "utf8")
1345
+ );
1346
+ if (typeof pkg.version === "string" && pkg.version.length > 0) {
1347
+ return pkg.version;
1348
+ }
1349
+ } catch {
1350
+ }
1351
+ return declaredRange;
1352
+ }
1353
+ function findWidgetMarker(cwd, siteUuid) {
1354
+ let budget = WIDGET_SCAN_MAX_FILES;
1355
+ let sawWidget = false;
1356
+ let sawTokenMatch = false;
1357
+ const settled = () => sawTokenMatch || sawWidget && siteUuid == null;
1358
+ const walk2 = (dir, depth) => {
1359
+ if (depth > WIDGET_SCAN_MAX_DEPTH || budget <= 0) {
1360
+ return;
1361
+ }
1362
+ let entries;
1363
+ try {
1364
+ entries = readdirSync2(dir, { withFileTypes: true });
1365
+ } catch {
1366
+ return;
1367
+ }
1368
+ for (const entry of entries) {
1369
+ if (budget <= 0 || settled()) {
1370
+ return;
1371
+ }
1372
+ const full = path8.join(dir, entry.name);
1373
+ if (entry.isDirectory()) {
1374
+ if (SKIPPED_DIRS.has(entry.name) || entry.name.startsWith(".")) {
1375
+ continue;
1376
+ }
1377
+ walk2(full, depth + 1);
1378
+ continue;
1379
+ }
1380
+ if (!entry.isFile() || !WIDGET_EXTENSIONS.has(path8.extname(entry.name).toLowerCase())) {
1381
+ continue;
1382
+ }
1383
+ budget -= 1;
1384
+ try {
1385
+ if (statSync2(full).size > WIDGET_SCAN_MAX_BYTES) {
1386
+ continue;
1387
+ }
1388
+ const content = readFileSync(full, "utf8");
1389
+ if (!content.includes(WIDGET_NEEDLE)) {
1390
+ continue;
1391
+ }
1392
+ sawWidget = true;
1393
+ if (siteUuid != null && content.includes(siteUuid)) {
1394
+ sawTokenMatch = true;
1395
+ }
1396
+ } catch {
1397
+ }
1398
+ }
1399
+ };
1400
+ walk2(cwd, 0);
1401
+ return {
1402
+ found: sawWidget,
1403
+ uuidMatches: sawWidget && siteUuid != null ? sawTokenMatch : null
1404
+ };
1405
+ }
1406
+ function resolveWidgetFileHint(cwd, framework) {
1407
+ const candidates = [
1408
+ ...framework !== null ? WIDGET_FILE_CANDIDATES[framework] ?? [] : [],
1409
+ ...GENERIC_WIDGET_FILES
1410
+ ];
1411
+ for (const candidate of candidates) {
1412
+ if (existsSync2(path8.join(cwd, candidate))) {
1413
+ return candidate;
1414
+ }
1415
+ }
1416
+ return null;
1417
+ }
1418
+ async function collectGuideState(cwd) {
1419
+ const pkg = readPackageJson(cwd);
1420
+ const packageManager = detectPackageManager(cwd);
1421
+ let installed = null;
1422
+ if (pkg?.devDependencies?.["@patchstack/connect"] !== void 0) {
1423
+ installed = {
1424
+ version: installedVersion(cwd, pkg.devDependencies["@patchstack/connect"]),
1425
+ section: "devDependencies"
1426
+ };
1427
+ } else if (pkg?.dependencies?.["@patchstack/connect"] !== void 0) {
1428
+ installed = {
1429
+ version: installedVersion(cwd, pkg.dependencies["@patchstack/connect"]),
1430
+ section: "dependencies"
1431
+ };
1432
+ }
1433
+ let siteUuid = null;
1434
+ let claimUrl = null;
1435
+ let endpointOverride = null;
1436
+ try {
1437
+ const config = await resolveConfig({ cwd });
1438
+ siteUuid = config.siteUuid;
1439
+ if (siteUuid !== null) {
1440
+ claimUrl = buildClaimUrl(config.endpoint, siteUuid);
1441
+ }
1442
+ if (config.endpoint !== DEFAULT_ENDPOINT) {
1443
+ endpointOverride = config.endpoint;
1444
+ }
1445
+ } catch {
1446
+ }
1447
+ const declaredNames = Object.keys({ ...pkg?.dependencies, ...pkg?.devDependencies });
1448
+ const stack = detectStack(
1449
+ declaredNames.map((name) => ({ name, version: "" })),
1450
+ {}
1451
+ );
1452
+ const widget = findWidgetMarker(cwd, siteUuid);
1453
+ return {
1454
+ projectName: pkg?.name ?? null,
1455
+ hasPackageJson: pkg !== null,
1456
+ packageManager,
1457
+ installed,
1458
+ siteUuid,
1459
+ claimUrl,
1460
+ endpointOverride,
1461
+ // bun run doesn't execute npm-style pre/post scripts, so chaining inside
1462
+ // the build script itself also counts as wired (and is what we suggest on bun).
1463
+ prebuildWired: (pkg?.scripts?.prebuild ?? "").includes("patchstack-connect scan") || (pkg?.scripts?.build ?? "").includes("patchstack-connect scan"),
1464
+ postbuildWired: (pkg?.scripts?.postbuild ?? "").includes("patchstack-connect mark-build") || (pkg?.scripts?.build ?? "").includes("patchstack-connect mark-build"),
1465
+ widgetInstalled: widget.found,
1466
+ widgetTokenMatches: widget.uuidMatches,
1467
+ framework: stack.framework,
1468
+ widgetFileHint: resolveWidgetFileHint(cwd, stack.framework)
1469
+ };
1470
+ }
1471
+ var ANSI = {
1472
+ reset: "\x1B[0m",
1473
+ bold: "\x1B[1m",
1474
+ dim: "\x1B[2m",
1475
+ green: "\x1B[32m",
1476
+ yellow: "\x1B[33m",
1477
+ cyan: "\x1B[36m"
1478
+ };
1479
+ function countRemainingSteps(state) {
1480
+ return [
1481
+ state.installed !== null,
1482
+ state.siteUuid !== null,
1483
+ state.prebuildWired && state.postbuildWired,
1484
+ state.widgetInstalled && state.widgetTokenMatches !== false
1485
+ ].filter((step) => !step).length;
1486
+ }
1487
+ function renderGuideChecklist(state, useColor) {
1488
+ const paint = (code, text) => useColor ? `${code}${text}${ANSI.reset}` : text;
1489
+ const done = (text) => ` ${paint(ANSI.green, "\u2714")} ${text}`;
1490
+ const todo = (text) => ` ${paint(ANSI.yellow, "\u2716")} ${paint(ANSI.bold, text)}`;
1491
+ const detail = (text) => ` ${paint(ANSI.dim, text)}`;
1492
+ const lines = [];
1493
+ const headerParts = [state.framework, state.packageManager].filter(
1494
+ (part) => part !== null
1495
+ );
1496
+ const name = state.projectName ?? path8.basename(process.cwd());
1497
+ lines.push(paint(ANSI.bold, `Patchstack setup status \u2014 ${name} (${headerParts.join(" \xB7 ")})`));
1498
+ if (state.endpointOverride !== null) {
1499
+ lines.push(
1500
+ detail(
1501
+ `endpoint override in effect: ${state.endpointOverride} (set via .patchstackrc.json, PATCHSTACK_ENDPOINT, or --endpoint)`
1502
+ )
1503
+ );
1504
+ }
1505
+ lines.push("");
1506
+ if (!state.hasPackageJson) {
1507
+ lines.push(todo("No package.json found in this directory."));
1508
+ lines.push(detail("Run the guide from the project root."));
1509
+ return lines.join("\n");
1510
+ }
1511
+ if (state.installed !== null) {
1512
+ lines.push(done(`@patchstack/connect installed (${state.installed.version}, ${state.installed.section})`));
1513
+ } else {
1514
+ lines.push(todo("Install @patchstack/connect as a dev dependency"));
1515
+ lines.push(detail(`\u2192 ${installCommand(state.packageManager)}`));
1516
+ if (state.packageManager === "bun") {
1517
+ lines.push(detail(`(if bun isn't available here, ${INSTALL_COMMANDS.npm} works too)`));
1518
+ }
1519
+ }
1520
+ if (state.siteUuid !== null) {
1521
+ lines.push(done(`Site provisioned (${state.siteUuid})`));
1522
+ } else {
1523
+ lines.push(todo("Provision the site \u2014 run the first scan"));
1524
+ lines.push(detail("\u2192 npx @patchstack/connect scan"));
1525
+ lines.push(detail("Reads the lockfile, registers the project, writes .patchstackrc.json,"));
1526
+ lines.push(detail("and prints a claim URL \u2014 show that URL to the user; never open it yourself."));
1527
+ }
1528
+ if (state.prebuildWired && state.postbuildWired) {
1529
+ lines.push(done("Build hooks wired (scan before builds, mark-build after)"));
1530
+ } else if (state.packageManager === "bun") {
1531
+ lines.push(todo("Wire builds \u2014 chain into the package.json build script (bun skips pre/post hooks)"));
1532
+ lines.push(detail('\u2192 "build": "patchstack-connect scan && <existing build command> && patchstack-connect mark-build"'));
1533
+ } else {
1534
+ lines.push(todo("Wire builds \u2014 add to package.json scripts (chain with && if a hook exists)"));
1535
+ if (!state.prebuildWired) {
1536
+ lines.push(detail('\u2192 "prebuild": "patchstack-connect scan"'));
1537
+ }
1538
+ if (!state.postbuildWired) {
1539
+ lines.push(detail('\u2192 "postbuild": "patchstack-connect mark-build"'));
1540
+ }
1541
+ }
1542
+ const widgetOk = state.widgetInstalled && state.widgetTokenMatches !== false;
1543
+ if (widgetOk) {
1544
+ lines.push(done("Disclosure widget installed"));
1545
+ } else if (state.widgetInstalled) {
1546
+ lines.push(todo("Disclosure widget found, but its userToken doesn't match this project's site UUID"));
1547
+ lines.push(detail(`\u2192 a wrong userToken makes the widget silently no-op; set it to '${state.siteUuid}'`));
1548
+ } else {
1549
+ lines.push(todo('Add the "Report a vulnerability" widget'));
1550
+ const placement = state.widgetFileHint !== null ? `\u2192 add to ${state.widgetFileHint}, just before </body> (via the framework's HTML/layout mechanism):` : "\u2192 add just before </body> via the framework's HTML/layout mechanism (never a JS entry point):";
1551
+ lines.push(detail(placement));
1552
+ lines.push(detail(` <script src="${WIDGET_SCRIPT_URL}"></script>`));
1553
+ const token = state.siteUuid ?? "<SITE_UUID from .patchstackrc.json \u2014 run scan first>";
1554
+ lines.push(detail(` <script>PatchstackWidget.init({ userToken: '${token}' });</script>`));
1555
+ lines.push(detail("The userToken is public by design \u2014 it ships in client-side HTML."));
1556
+ }
1557
+ lines.push("");
1558
+ if (state.claimUrl !== null) {
1559
+ lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Claim the site (free, opens the dashboard):")}`);
1560
+ lines.push(` ${paint(ANSI.cyan, state.claimUrl)}`);
1561
+ lines.push(detail("Open in a browser. AI agents: show this URL to the user verbatim."));
1562
+ if (state.endpointOverride !== null) {
1563
+ lines.push(detail("(this URL inherits the endpoint override above)"));
1564
+ }
1565
+ } else {
1566
+ lines.push(detail("The claim URL appears after the first scan (re-print any time with `status`)."));
1567
+ }
1568
+ const remaining = countRemainingSteps(state);
1569
+ lines.push("");
1570
+ if (remaining === 0) {
1571
+ lines.push(
1572
+ done(
1573
+ paint(
1574
+ ANSI.bold,
1575
+ "All setup steps complete. Commit .patchstackrc.json, package.json, and the file carrying the widget snippet."
1576
+ )
1577
+ )
1578
+ );
1579
+ if (state.claimUrl !== null) {
1580
+ lines.push(detail("The only manual action left is claiming the site via the URL above (if not already claimed)."));
1581
+ }
1582
+ } else {
1583
+ lines.push(
1584
+ ` ${paint(ANSI.yellow, String(remaining))} step(s) remaining \u2014 details in the reference guide below.`
1585
+ );
1586
+ }
1587
+ return lines.join("\n");
1588
+ }
1589
+
1253
1590
  // src/protect/install.ts
1254
- import { readFileSync, writeFileSync, existsSync as existsSync2, mkdirSync, copyFileSync } from "fs";
1591
+ import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3, mkdirSync, copyFileSync } from "fs";
1255
1592
  import { join, dirname } from "path";
1256
1593
  import { fileURLToPath } from "url";
1257
1594
  var TEMPLATES = join(dirname(fileURLToPath(import.meta.url)), "protect", "templates");
1258
1595
  var APP = process.cwd();
1259
1596
  var PS_DIR = join(APP, "src/integrations/patchstack");
1260
- var read = (p) => readFileSync(p, "utf8");
1597
+ var read = (p) => readFileSync2(p, "utf8");
1261
1598
  var log = (msg) => console.log(`patchstack protect: ${msg}`);
1262
1599
  var CLIENT_TUNNEL = [
1263
1600
  "",
@@ -1302,10 +1639,10 @@ var FUNCTION_MIDDLEWARE_DEF = [
1302
1639
  ].join("\n");
1303
1640
  function detectSupportedStack(cwd) {
1304
1641
  const pkgPath = join(cwd, "package.json");
1305
- if (!existsSync2(pkgPath)) return false;
1642
+ if (!existsSync3(pkgPath)) return false;
1306
1643
  const pkg = JSON.parse(read(pkgPath));
1307
1644
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1308
- return Boolean(deps["@tanstack/react-start"]) && existsSync2(join(cwd, "src/start.ts")) && existsSync2(join(cwd, "src/integrations/supabase/client.ts"));
1645
+ return Boolean(deps["@tanstack/react-start"]) && existsSync3(join(cwd, "src/start.ts")) && existsSync3(join(cwd, "src/integrations/supabase/client.ts"));
1309
1646
  }
1310
1647
  function scaffold(cwd) {
1311
1648
  const dst = join(cwd, "src/integrations/patchstack");
@@ -1388,8 +1725,11 @@ Usage:
1388
1725
  patchstack-connect protect Install always-on runtime protection (the
1389
1726
  guard) into a TanStack Start + Supabase app.
1390
1727
  Covers the browser + server-function paths.
1391
- patchstack-connect guide Print the full setup guide for AI coding
1392
- agents (also at https://patchstack.com/install.txt)
1728
+ patchstack-connect guide [--full] Show this project's setup status (what's done,
1729
+ what's missing, with tailored commands), then
1730
+ print the full setup guide. --full prints the
1731
+ guide even when setup is complete
1732
+ (also at https://patchstack.com/install.txt)
1393
1733
  patchstack-connect help Print this message
1394
1734
 
1395
1735
  Options (for scan and status):
@@ -1480,14 +1820,22 @@ async function runScan(args) {
1480
1820
  }
1481
1821
  const { payload, stats } = buildWirePayload(manifest);
1482
1822
  console.log(
1483
- `Found ${payload.packages.length} unique package versions across ${stats.uniqueNames} package names in ${manifest.ecosystem} lockfile.`
1823
+ `Found ${payload.packages.length} unique package versions across ${stats.uniqueNames} package names (${manifest.ecosystem} ecosystem).`
1824
+ );
1825
+ console.log(
1826
+ `Reporting under the ${config.environment} environment (override with PATCHSTACK_ENVIRONMENT).`
1484
1827
  );
1485
- console.log(`Reporting under the ${config.environment} environment.`);
1828
+ if (config.endpoint !== DEFAULT_ENDPOINT) {
1829
+ console.log(
1830
+ `Using endpoint override: ${config.endpoint} (set via --endpoint, PATCHSTACK_ENDPOINT, or .patchstackrc.json).`
1831
+ );
1832
+ }
1486
1833
  if (stats.duplicateNames.length > 0) {
1487
- console.log(`${stats.duplicateNames.length} package(s) appear at multiple versions:`);
1488
- if (stats.duplicateNames.length <= 10) {
1489
- console.log(` ${stats.duplicateNames.join(", ")}`);
1490
- }
1834
+ const sample = stats.duplicateNames.slice(0, 10).join(", ");
1835
+ const more = stats.duplicateNames.length > 10 ? `, +${stats.duplicateNames.length - 10} more` : "";
1836
+ console.log(
1837
+ `${stats.duplicateNames.length} package(s) appear at multiple versions: ${sample}${more}`
1838
+ );
1491
1839
  }
1492
1840
  if (dryRun) {
1493
1841
  console.log("");
@@ -1524,6 +1872,9 @@ async function runScan(args) {
1524
1872
  console.log("");
1525
1873
  console.log("Claim this site to view vulnerability reports in your Patchstack dashboard:");
1526
1874
  console.log(` ${buildClaimUrl(config.endpoint, response.uuid)}`);
1875
+ if (config.endpoint !== DEFAULT_ENDPOINT) {
1876
+ console.log(" (this URL inherits the endpoint override above)");
1877
+ }
1527
1878
  }
1528
1879
  return 0;
1529
1880
  }
@@ -1535,9 +1886,27 @@ async function runProtectCommand(_args) {
1535
1886
  }
1536
1887
  return 0;
1537
1888
  }
1538
- async function runGuide() {
1889
+ async function runGuide(args) {
1890
+ let allDone = false;
1891
+ try {
1892
+ const state = await collectGuideState(process.cwd());
1893
+ const useColor = process.stdout.isTTY === true && process.env.NO_COLOR === void 0;
1894
+ console.log(renderGuideChecklist(state, useColor));
1895
+ allDone = countRemainingSteps(state) === 0;
1896
+ } catch {
1897
+ }
1898
+ if (allDone && args.flags.get("full") !== true) {
1899
+ console.log("");
1900
+ console.log(
1901
+ "Full reference guide: `npx @patchstack/connect guide --full` (or read node_modules/@patchstack/connect/AGENT-INSTALL.md)."
1902
+ );
1903
+ return 0;
1904
+ }
1905
+ console.log("");
1906
+ console.log("\u2014\u2014\u2014\u2014 Full reference guide (ships as AGENT-INSTALL.md) \u2014\u2014\u2014\u2014");
1907
+ console.log("");
1539
1908
  const guidePath = new URL("../AGENT-INSTALL.md", import.meta.url);
1540
- console.log(readFileSync2(guidePath, "utf8"));
1909
+ console.log(readFileSync3(guidePath, "utf8"));
1541
1910
  return 0;
1542
1911
  }
1543
1912
  async function runStatus(args) {
@@ -1547,7 +1916,9 @@ async function runStatus(args) {
1547
1916
  cliEndpoint: getStringFlag(args.flags, "endpoint")
1548
1917
  });
1549
1918
  console.log(`Site UUID: ${config.siteUuid ?? "(none yet \u2014 the next `scan` will provision one)"}`);
1550
- console.log(`Endpoint: ${config.endpoint}`);
1919
+ console.log(
1920
+ `Endpoint: ${config.endpoint}${config.endpoint === DEFAULT_ENDPOINT ? "" : " (override)"}`
1921
+ );
1551
1922
  console.log(`Timeout: ${config.timeoutMs}ms`);
1552
1923
  console.log(`Environment: ${config.environment}`);
1553
1924
  if (config.siteUuid !== null) {
@@ -1596,7 +1967,7 @@ async function runMarkBuild(args) {
1596
1967
  const snippet = buildInjectionSnippet(checksum, stack);
1597
1968
  let marked = 0;
1598
1969
  for (const file of files) {
1599
- const before = readFileSync2(file, "utf8");
1970
+ const before = readFileSync3(file, "utf8");
1600
1971
  const after = injectMarker(before, snippet);
1601
1972
  if (after !== before) {
1602
1973
  writeFileSync2(file, after);
@@ -1627,7 +1998,7 @@ async function main() {
1627
1998
  case "protect":
1628
1999
  return runProtectCommand(args);
1629
2000
  case "guide":
1630
- return runGuide();
2001
+ return runGuide(args);
1631
2002
  default:
1632
2003
  console.error(`Unknown command: ${args.command}
1633
2004
  `);