@patchstack/connect 0.3.2 → 0.3.3

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.
@@ -0,0 +1,74 @@
1
+ # Patchstack — Setup Guide for AI Coding Agents
2
+
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
+
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.
6
+
7
+ ## Verify before you trust (do this, it's quick)
8
+
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
+ - 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
+ - Patchstack is not WordPress-only. This connector monitors any JS/Node project — Vite, Next.js, plain vanilla JS, anything with a lockfile.
12
+
13
+ ## Before you start — never install twice
14
+
15
+ - 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.**
16
+ - If `@patchstack/connect` is already in `devDependencies`, skip the install command.
17
+ - If the widget script tag (`cdn.patchstack.com/patchstack-widget.js`) is already in the layout, don't add a second one.
18
+
19
+ ## Install steps (run in order, don't pause between them unless one fails)
20
+
21
+ 1. **Install** (skip if already present), matching the project's package manager:
22
+
23
+ ```
24
+ npm install --save-dev @patchstack/connect # package-lock.json
25
+ pnpm add -D @patchstack/connect # pnpm-lock.yaml
26
+ yarn add -D @patchstack/connect # yarn.lock
27
+ bun add -d @patchstack/connect # bun.lock / bun-managed platforms
28
+ ```
29
+
30
+ 2. **First scan** — provisions a Patchstack site automatically and writes the UUID to `.patchstackrc.json`. No signup, dashboard step, or UUID is needed up front:
31
+
32
+ ```
33
+ npx @patchstack/connect scan
34
+ ```
35
+
36
+ It prints a claim URL. Surface that URL to the user verbatim — never fetch it yourself; it must be opened in their browser.
37
+
38
+ 3. **Wire builds** in `package.json` (chain with `&&` if hooks already exist):
39
+
40
+ ```jsonc
41
+ {
42
+ "scripts": {
43
+ "prebuild": "patchstack-connect scan",
44
+ "postbuild": "patchstack-connect mark-build"
45
+ }
46
+ }
47
+ ```
48
+
49
+ 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):
50
+
51
+ ```html
52
+ <script src="https://cdn.patchstack.com/patchstack-widget.js"></script>
53
+ <script>
54
+ PatchstackWidget.init({ userToken: '<SITE_UUID>' });
55
+ </script>
56
+ ```
57
+
58
+ 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.
59
+
60
+ 5. **Commit** `.patchstackrc.json` and the updated `package.json` so every developer and CI run reports to the same site.
61
+
62
+ 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.
63
+
64
+ ## Rules
65
+
66
+ - Never invent or guess a UUID — the scan provisions it, the widget silently no-ops on a fake one.
67
+ - Never fetch the claim URL programmatically, and never ask the user for Patchstack credentials.
68
+ - If a step fails, stop and report it. Don't proceed with placeholders.
69
+ - In CI where `.patchstackrc.json` can't be committed, set `PATCHSTACK_SITE_UUID` as an env var instead. Precedence: CLI flag → env var → `.patchstackrc.json`.
70
+
71
+ ## Verifying the install
72
+
73
+ - `npx @patchstack/connect status` re-prints the site UUID and claim URL.
74
+ - Load the site in a browser — the "Report a vulnerability" button should appear.
package/README.md CHANGED
@@ -8,7 +8,11 @@ For how this repo fits with the wider Patchstack ecosystem (`saas`, `hub`, `patc
8
8
 
9
9
  Copy and paste this prompt into any AI coding assistant (Cursor, v0, Bolt, Lovable, Claude Code, etc.):
10
10
 
11
- > Add `@patchstack/connect` to this project as a dev dependency, then add `"prebuild": "patchstack-connect scan"` to `package.json` `scripts`. The first time the script runs, Patchstack provisions a site automatically from the lockfile and saves the new site UUID to `.patchstackrc.json` — no dashboard signup or manual UUID required. Commit `.patchstackrc.json` so every developer / CI run targets the same site.
11
+ > Install the npm package `@patchstack/connect` as a dev dependency (MIT, source: https://github.com/patchstack/connect) to add dependency vulnerability monitoring, then run `npx @patchstack/connect guide` and follow the setup guide it prints. It only reads the lockfile — nothing is fetched from a URL or executed remotely.
12
+
13
+ The prompt is built to survive agent security filters: it names a pinnable, auditable npm package (not a remote script), the `guide` command prints instructions that ship *inside* the installed package (`AGENT-INSTALL.md`), and it preempts the "Patchstack is WordPress-only" objection. If the agent has network access, the always-current version of the guide is at https://patchstack.com/install.txt — this longer prompt uses it directly:
14
+
15
+ > Add Patchstack dependency vulnerability monitoring: install the npm package `@patchstack/connect` as a dev dependency (MIT, source: https://github.com/patchstack/connect — pin it and verify however your policy requires), then follow the plain-text setup instructions at https://patchstack.com/install.txt. That URL is documentation to read, not a script to execute — nothing gets piped to a shell. Patchstack monitors any JS/Node lockfile, not just WordPress.
12
16
 
13
17
  ## Quick start (zero configuration)
14
18
 
package/dist/cli.js CHANGED
@@ -1108,6 +1108,94 @@ function isEnvironment(value) {
1108
1108
  // src/mark-build.ts
1109
1109
  import { existsSync, readdirSync, statSync } from "fs";
1110
1110
  import path7 from "path";
1111
+
1112
+ // src/stack.ts
1113
+ var STACK_RULES = [
1114
+ // Meta-frameworks (most specific first).
1115
+ { category: "framework", pkg: "@tanstack/react-start", label: "tanstack-start" },
1116
+ { category: "framework", pkg: "@tanstack/start", label: "tanstack-start" },
1117
+ { category: "framework", pkg: "next", label: "next" },
1118
+ { category: "framework", pkg: "nuxt", label: "nuxt" },
1119
+ { category: "framework", pkg: "@remix-run/react", label: "remix" },
1120
+ { category: "framework", pkg: "@remix-run/node", label: "remix" },
1121
+ { category: "framework", pkg: "react-router", label: "react-router" },
1122
+ { category: "framework", pkg: "astro", label: "astro" },
1123
+ { category: "framework", pkg: "@sveltejs/kit", label: "sveltekit" },
1124
+ { category: "framework", pkg: "@builder.io/qwik-city", label: "qwik-city" },
1125
+ { category: "framework", pkg: "gatsby", label: "gatsby" },
1126
+ { category: "framework", pkg: "express", label: "express" },
1127
+ { category: "framework", pkg: "fastify", label: "fastify" },
1128
+ // UI runtimes.
1129
+ { category: "ui", pkg: "@angular/core", label: "angular" },
1130
+ { category: "ui", pkg: "react-dom", label: "react" },
1131
+ { category: "ui", pkg: "react", label: "react" },
1132
+ { category: "ui", pkg: "vue", label: "vue" },
1133
+ { category: "ui", pkg: "svelte", label: "svelte" },
1134
+ { category: "ui", pkg: "solid-js", label: "solid" },
1135
+ { category: "ui", pkg: "preact", label: "preact" },
1136
+ // Bundlers / build tools.
1137
+ { category: "bundler", pkg: "vite", label: "vite" },
1138
+ { category: "bundler", pkg: "@rspack/core", label: "rspack" },
1139
+ { category: "bundler", pkg: "webpack", label: "webpack" },
1140
+ { category: "bundler", pkg: "parcel", label: "parcel" },
1141
+ { category: "bundler", pkg: "rollup", label: "rollup" },
1142
+ { category: "bundler", pkg: "esbuild", label: "esbuild" },
1143
+ // Deployment-runtime hints from build deps.
1144
+ { category: "runtime", pkg: "wrangler", label: "cloudflare-workers" },
1145
+ { category: "runtime", pkg: "@cloudflare/workers-types", label: "cloudflare-workers" },
1146
+ { category: "runtime", pkg: "@cloudflare/vite-plugin", label: "cloudflare-workers" },
1147
+ { category: "runtime", pkg: "@vercel/node", label: "vercel" },
1148
+ { category: "runtime", pkg: "@netlify/functions", label: "netlify" },
1149
+ { category: "runtime", pkg: "@netlify/blobs", label: "netlify" },
1150
+ // Vibe / builder platforms — the "learn from each platform" signal.
1151
+ { category: "builder", pkg: "lovable-tagger", label: "lovable" },
1152
+ { category: "builder", pkg: "@replit/vite-plugin-runtime-error-modal", label: "replit" },
1153
+ { category: "builder", pkg: "@replit/vite-plugin-cartographer", label: "replit" }
1154
+ ];
1155
+ var HOSTING_ENV_PATTERNS = [
1156
+ /^CF_/,
1157
+ /^CLOUDFLARE_/,
1158
+ /^VERCEL/,
1159
+ /^NETLIFY/,
1160
+ /^AWS_(LAMBDA|REGION|EXECUTION)/,
1161
+ /^FLY_/,
1162
+ /^RENDER/,
1163
+ /^RAILWAY_/,
1164
+ /^DENO_/,
1165
+ /^EDGE_RUNTIME/,
1166
+ /^DYNO$/,
1167
+ /^K_SERVICE$/,
1168
+ /^GAE_/,
1169
+ /^FUNCTION_/
1170
+ ];
1171
+ function collectHostingEnvKeys(env = process.env) {
1172
+ return Object.keys(env).filter((key) => HOSTING_ENV_PATTERNS.some((pattern) => pattern.test(key))).sort();
1173
+ }
1174
+ function detectStack(packages, env = process.env) {
1175
+ const present = new Set(packages.map((pkg) => pkg.name));
1176
+ const firstMatch = (category) => {
1177
+ for (const rule of STACK_RULES) {
1178
+ if (rule.category === category && present.has(rule.pkg)) {
1179
+ return rule.label;
1180
+ }
1181
+ }
1182
+ return null;
1183
+ };
1184
+ return {
1185
+ framework: firstMatch("framework"),
1186
+ ui: firstMatch("ui"),
1187
+ bundler: firstMatch("bundler"),
1188
+ runtime: firstMatch("runtime"),
1189
+ builder: firstMatch("builder"),
1190
+ ecosystem: "npm",
1191
+ hostingEnvKeys: collectHostingEnvKeys(env)
1192
+ };
1193
+ }
1194
+ function isEmptyStack(stack) {
1195
+ return stack.framework === null && stack.ui === null && stack.bundler === null && stack.runtime === null && stack.builder === null && stack.hostingEnvKeys.length === 0;
1196
+ }
1197
+
1198
+ // src/mark-build.ts
1111
1199
  var MARKER_ATTR = "data-patchstack-build";
1112
1200
  var BUILD_DIR_CANDIDATES = ["dist", "build", "out", ".output/public"];
1113
1201
  function resolveBuildDir(cwd, override) {
@@ -1138,11 +1226,14 @@ function findHtmlFiles(dir) {
1138
1226
  walk2(dir);
1139
1227
  return out;
1140
1228
  }
1141
- function buildInjectionSnippet(checksum) {
1229
+ function buildInjectionSnippet(checksum, stack) {
1142
1230
  const statements = ["window.__PATCHSTACK_PROD__=true;"];
1143
1231
  if (checksum !== null && checksum !== "") {
1144
1232
  statements.push(`window.__PATCHSTACK_BUILD__=${JSON.stringify(checksum)};`);
1145
1233
  }
1234
+ if (stack != null && !isEmptyStack(stack)) {
1235
+ statements.push(`window.__PATCHSTACK_STACK__=${JSON.stringify(stack)};`);
1236
+ }
1146
1237
  return `<script ${MARKER_ATTR}>${statements.join("")}</script>`;
1147
1238
  }
1148
1239
  function injectMarker(html, snippet) {
@@ -1161,17 +1252,13 @@ function injectMarker(html, snippet) {
1161
1252
 
1162
1253
  // src/protect/install.ts
1163
1254
  import { readFileSync, writeFileSync, existsSync as existsSync2, mkdirSync, copyFileSync } from "fs";
1164
- import { join, dirname } from "path";
1255
+ import { join } from "path";
1165
1256
  import { fileURLToPath } from "url";
1166
- var TEMPLATES = join(dirname(fileURLToPath(import.meta.url)), "protect", "templates");
1167
- var APP = process.cwd();
1168
- var PS_DIR = join(APP, "src/integrations/patchstack");
1169
- var read = (p) => readFileSync(p, "utf8");
1170
- var log = (msg) => console.log(`patchstack protect: ${msg}`);
1257
+ var TEMPLATES = fileURLToPath(new URL("./protect/templates/", import.meta.url));
1171
1258
  var CLIENT_TUNNEL = [
1172
1259
  "",
1173
- " // PATCHSTACK: in the browser, tunnel Supabase traffic through the app's own server guard",
1174
- " // (same-origin) so payloads are inspected before they reach Supabase.",
1260
+ " // PATCHSTACK auto-guard: in the browser, tunnel Supabase traffic through the app's own",
1261
+ " // server guard (same-origin) so payloads are inspected before they reach Supabase.",
1175
1262
  " if (typeof window !== 'undefined') {",
1176
1263
  " const target = typeof input === 'string' ? input : input instanceof Request ? input.url : String(input);",
1177
1264
  " const method = init?.method ?? (input instanceof Request ? input.method : 'GET');",
@@ -1183,11 +1270,11 @@ var CLIENT_TUNNEL = [
1183
1270
  ].join("\n");
1184
1271
  var START_IMPORTS = [
1185
1272
  'import { getRequest } from "@tanstack/react-start/server";',
1186
- 'import { GUARD_PATH, handleGuardRequest, inspectServerFn } from "@/integrations/patchstack/guard";'
1273
+ 'import { GUARD_PATH, handleGuardRequest } from "@/integrations/patchstack/guard";'
1187
1274
  ].join("\n");
1188
- var REQUEST_MIDDLEWARE_DEF = [
1275
+ var START_MIDDLEWARE = [
1189
1276
  "",
1190
- "// Patchstack guard (browser tunnel): intercept tunneled Supabase traffic before anything else.",
1277
+ "// Patchstack auto-guard: intercept the tunneled data traffic before anything else runs.",
1191
1278
  "const patchstackGuard = createMiddleware().server(async ({ next }) => {",
1192
1279
  " const request = getRequest();",
1193
1280
  " if (request) {",
@@ -1198,87 +1285,113 @@ var REQUEST_MIDDLEWARE_DEF = [
1198
1285
  "});",
1199
1286
  ""
1200
1287
  ].join("\n");
1201
- var FUNCTION_MIDDLEWARE_DEF = [
1202
- "",
1203
- "// Patchstack guard (server functions): inspect server-fn args before they reach the database,",
1204
- "// covering apps that mutate via TanStack server functions (which bypass the browser tunnel).",
1205
- 'const patchstackFunctionGuard = createMiddleware({ type: "function" }).server(async ({ next, data }) => {',
1206
- " const blocked = await inspectServerFn(data);",
1207
- " if (blocked) throw new Error(blocked.message);",
1208
- " return next();",
1209
- "});",
1210
- ""
1211
- ].join("\n");
1288
+ var PS_DIR_REL = "src/integrations/patchstack";
1289
+ function log(msg) {
1290
+ console.log(`patchstack protect: ${msg}`);
1291
+ }
1212
1292
  function detectSupportedStack(cwd) {
1213
1293
  const pkgPath = join(cwd, "package.json");
1214
1294
  if (!existsSync2(pkgPath)) return false;
1215
- const pkg = JSON.parse(read(pkgPath));
1295
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
1216
1296
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1217
1297
  return Boolean(deps["@tanstack/react-start"]) && existsSync2(join(cwd, "src/start.ts")) && existsSync2(join(cwd, "src/integrations/supabase/client.ts"));
1218
1298
  }
1299
+ function generateManifest(cwd) {
1300
+ const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
1301
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1302
+ const packages = {};
1303
+ for (const name of Object.keys(deps)) {
1304
+ let version = String(deps[name]).replace(/^[^\d]*/, "");
1305
+ const installed = join(cwd, "node_modules", name, "package.json");
1306
+ if (existsSync2(installed)) {
1307
+ try {
1308
+ version = JSON.parse(readFileSync(installed, "utf8")).version;
1309
+ } catch {
1310
+ }
1311
+ }
1312
+ packages[name] = version;
1313
+ }
1314
+ mkdirSync(join(cwd, PS_DIR_REL), { recursive: true });
1315
+ const body = "// Patchstack auto-guard manifest \u2014 generated from the lockfile.\n// Do not edit; regenerated by `patchstack-connect protect --manifest` (wired into prebuild).\nexport const manifest = " + JSON.stringify({ packages }, null, 2) + ";\n";
1316
+ writeFileSync(join(cwd, PS_DIR_REL, "manifest.js"), body);
1317
+ log(`wrote manifest.js (${Object.keys(packages).length} packages)`);
1318
+ }
1219
1319
  function scaffold(cwd) {
1220
- const dst = join(cwd, "src/integrations/patchstack");
1320
+ const dst = join(cwd, PS_DIR_REL);
1221
1321
  mkdirSync(dst, { recursive: true });
1222
- copyFileSync(join(TEMPLATES, "guard.ts"), join(dst, "guard.ts"));
1322
+ copyFileSync(join(TEMPLATES, "engine.js"), join(dst, "engine.js"));
1323
+ copyFileSync(join(TEMPLATES, "engine.d.ts"), join(dst, "engine.d.ts"));
1324
+ copyFileSync(join(TEMPLATES, "manifest.d.ts"), join(dst, "manifest.d.ts"));
1223
1325
  copyFileSync(join(TEMPLATES, "rules.json"), join(dst, "rules.json"));
1224
- log("scaffolded guard.ts + rules.json");
1326
+ copyFileSync(join(TEMPLATES, "guard.ts"), join(dst, "guard.ts"));
1327
+ log("scaffolded engine.js (+types), rules.json, guard.ts");
1225
1328
  }
1226
1329
  function patchClient(cwd) {
1227
1330
  const p = join(cwd, "src/integrations/supabase/client.ts");
1228
- let s = read(p);
1229
- if (s.includes("x-ps-target")) return log("client.ts already wired");
1331
+ let s = readFileSync(p, "utf8");
1332
+ if (s.includes("x-ps-target")) {
1333
+ log("client.ts already wired");
1334
+ return;
1335
+ }
1230
1336
  const anchor = "headers.set('apikey', supabaseKey);";
1231
- if (!s.includes(anchor)) return log("client.ts anchor not found \u2014 skipping (template changed?)");
1232
- writeFileSync(p, s.replace(anchor, anchor + "\n" + CLIENT_TUNNEL));
1337
+ if (!s.includes(anchor)) {
1338
+ log("client.ts anchor not found (template changed?) \u2014 skipping client patch");
1339
+ return;
1340
+ }
1341
+ s = s.replace(anchor, anchor + "\n" + CLIENT_TUNNEL);
1342
+ writeFileSync(p, s);
1233
1343
  log("patched client.ts (tunnel Supabase through the guard)");
1234
1344
  }
1235
1345
  function patchStart(cwd) {
1236
1346
  const p = join(cwd, "src/start.ts");
1237
- let s = read(p);
1347
+ let s = readFileSync(p, "utf8");
1348
+ if (s.includes("patchstackGuard")) {
1349
+ log("start.ts already wired");
1350
+ return;
1351
+ }
1238
1352
  const importAnchor = 'import { createStart, createMiddleware } from "@tanstack/react-start";';
1239
1353
  const exportAnchor = "export const startInstance";
1240
1354
  const rmAnchor = "requestMiddleware: [";
1241
- if (!s.includes(importAnchor) || !s.includes(exportAnchor)) {
1242
- return log("start.ts anchors not found \u2014 skipping (template changed?)");
1243
- }
1244
- const original = s;
1245
- if (!s.includes("@/integrations/patchstack/guard")) {
1246
- s = s.replace(importAnchor, importAnchor + "\n" + START_IMPORTS);
1247
- } else if (!s.includes("inspectServerFn")) {
1248
- s = s.replace(
1249
- 'import { GUARD_PATH, handleGuardRequest } from "@/integrations/patchstack/guard";',
1250
- 'import { GUARD_PATH, handleGuardRequest, inspectServerFn } from "@/integrations/patchstack/guard";'
1251
- );
1252
- }
1253
- if (!s.includes("const patchstackGuard =")) {
1254
- s = s.replace(exportAnchor, REQUEST_MIDDLEWARE_DEF + "\n" + exportAnchor);
1255
- }
1256
- if (!s.includes("const patchstackFunctionGuard =")) {
1257
- s = s.replace(exportAnchor, FUNCTION_MIDDLEWARE_DEF + "\n" + exportAnchor);
1258
- }
1259
- if (s.includes(rmAnchor) && !s.includes("requestMiddleware: [patchstackGuard")) {
1260
- s = s.replace(rmAnchor, rmAnchor + "patchstackGuard, ");
1261
- }
1262
- if (!s.includes("functionMiddleware: [patchstackFunctionGuard")) {
1263
- const fmAnchor = "functionMiddleware: [";
1264
- if (s.includes(fmAnchor)) {
1265
- s = s.replace(fmAnchor, fmAnchor + "patchstackFunctionGuard, ");
1266
- } else if (s.includes(rmAnchor)) {
1267
- s = s.replace(rmAnchor, "functionMiddleware: [patchstackFunctionGuard],\n " + rmAnchor);
1268
- }
1355
+ if (!s.includes(importAnchor) || !s.includes(exportAnchor) || !s.includes(rmAnchor)) {
1356
+ log("start.ts anchors not found (template changed?) \u2014 skipping start patch");
1357
+ return;
1269
1358
  }
1270
- if (s === original) return log("start.ts already wired");
1359
+ s = s.replace(importAnchor, importAnchor + "\n" + START_IMPORTS);
1360
+ s = s.replace(exportAnchor, START_MIDDLEWARE + "\n" + exportAnchor);
1361
+ s = s.replace(rmAnchor, rmAnchor + "patchstackGuard, ");
1271
1362
  writeFileSync(p, s);
1272
- log("patched start.ts (guard registered as request + function middleware)");
1363
+ log("patched start.ts (registered the guard as request middleware)");
1364
+ }
1365
+ function wirePrebuild(cwd) {
1366
+ const p = join(cwd, "package.json");
1367
+ const pkg = JSON.parse(readFileSync(p, "utf8"));
1368
+ pkg.scripts = pkg.scripts || {};
1369
+ const step = "patchstack-connect protect --manifest";
1370
+ const prebuild = pkg.scripts.prebuild || "";
1371
+ if (prebuild.includes(step)) {
1372
+ log("prebuild already refreshes the manifest");
1373
+ return;
1374
+ }
1375
+ pkg.scripts.prebuild = prebuild ? prebuild + " && " + step : step;
1376
+ writeFileSync(p, JSON.stringify(pkg, null, 2) + "\n");
1377
+ log("wired manifest refresh into prebuild");
1273
1378
  }
1274
- function runProtect(cwd) {
1379
+ function runProtect(cwd, opts = {}) {
1380
+ if (opts.manifestOnly) {
1381
+ generateManifest(cwd);
1382
+ return;
1383
+ }
1275
1384
  if (!detectSupportedStack(cwd)) {
1276
- log("runtime protection currently supports TanStack Start + Supabase apps; stack not detected \u2014 skipping.");
1385
+ log(
1386
+ "runtime protection currently supports TanStack Start + Supabase apps; stack not detected \u2014 skipping."
1387
+ );
1277
1388
  return;
1278
1389
  }
1279
1390
  scaffold(cwd);
1280
1391
  patchClient(cwd);
1281
1392
  patchStart(cwd);
1393
+ generateManifest(cwd);
1394
+ wirePrebuild(cwd);
1282
1395
  log("done \u2014 guard wired and always-on (blocks by default). Set PATCHSTACK_MODE=dry-run for log-only.");
1283
1396
  }
1284
1397
 
@@ -1294,8 +1407,11 @@ Usage:
1294
1407
  patchstack-connect status [options] Show current configuration
1295
1408
  patchstack-connect mark-build [options] Stamp built HTML with a production flag +
1296
1409
  build fingerprint (run as a postbuild step)
1297
- patchstack-connect protect Install always-on runtime protection (the
1298
- guard) into a TanStack Start + Supabase app.
1410
+ patchstack-connect protect [--manifest] Install runtime protection (the guard) into
1411
+ a server-side JS app. --manifest only
1412
+ regenerates the package manifest (prebuild).
1413
+ patchstack-connect guide Print the full setup guide for AI coding
1414
+ agents (also at https://patchstack.com/install.txt)
1299
1415
  patchstack-connect help Print this message
1300
1416
 
1301
1417
  Options (for scan and status):
@@ -1433,14 +1549,19 @@ async function runScan(args) {
1433
1549
  }
1434
1550
  return 0;
1435
1551
  }
1436
- async function runProtectCommand(_args) {
1552
+ async function runProtectCommand(args) {
1437
1553
  try {
1438
- runProtect(process.cwd());
1554
+ runProtect(process.cwd(), { manifestOnly: args.flags.get("manifest") === true });
1439
1555
  } catch (err) {
1440
1556
  console.warn(`patchstack protect: skipped (${err.message}).`);
1441
1557
  }
1442
1558
  return 0;
1443
1559
  }
1560
+ async function runGuide() {
1561
+ const guidePath = new URL("../AGENT-INSTALL.md", import.meta.url);
1562
+ console.log(readFileSync2(guidePath, "utf8"));
1563
+ return 0;
1564
+ }
1444
1565
  async function runStatus(args) {
1445
1566
  const config = await resolveConfig({
1446
1567
  cwd: process.cwd(),
@@ -1456,9 +1577,19 @@ async function runStatus(args) {
1456
1577
  }
1457
1578
  return 0;
1458
1579
  }
1580
+ function describeStack(stack) {
1581
+ const parts = [stack.builder, stack.framework, stack.ui, stack.runtime].filter(
1582
+ (part) => part !== null
1583
+ );
1584
+ if (stack.hostingEnvKeys.length > 0) {
1585
+ parts.push(`${stack.hostingEnvKeys.length} hosting env key(s)`);
1586
+ }
1587
+ return parts.length > 0 ? parts.join(" \xB7 ") : null;
1588
+ }
1459
1589
  async function runMarkBuild(args) {
1460
1590
  const cwd = process.cwd();
1461
1591
  let checksum = null;
1592
+ let stack = null;
1462
1593
  try {
1463
1594
  const manifest = await scanLockfile(cwd);
1464
1595
  for (const warning of manifest.warnings ?? []) {
@@ -1466,6 +1597,7 @@ async function runMarkBuild(args) {
1466
1597
  }
1467
1598
  const { payload } = buildWirePayload(manifest);
1468
1599
  checksum = computeManifestChecksum(payload.packages);
1600
+ stack = detectStack(payload.packages);
1469
1601
  } catch (err) {
1470
1602
  console.warn(
1471
1603
  `mark-build: could not compute the build fingerprint (${err.message}). Stamping the production flag only.`
@@ -1483,7 +1615,7 @@ async function runMarkBuild(args) {
1483
1615
  console.warn(`mark-build: no HTML files found under ${dir}. Nothing to mark.`);
1484
1616
  return 0;
1485
1617
  }
1486
- const snippet = buildInjectionSnippet(checksum);
1618
+ const snippet = buildInjectionSnippet(checksum, stack);
1487
1619
  let marked = 0;
1488
1620
  for (const file of files) {
1489
1621
  const before = readFileSync2(file, "utf8");
@@ -1493,8 +1625,9 @@ async function runMarkBuild(args) {
1493
1625
  marked += 1;
1494
1626
  }
1495
1627
  }
1628
+ const stackSummary = stack !== null ? describeStack(stack) : null;
1496
1629
  console.log(
1497
- `mark-build: marked ${marked} HTML file(s) in ${dir}${checksum !== null ? ` (build ${checksum})` : ""}.`
1630
+ `mark-build: marked ${marked} HTML file(s) in ${dir}${checksum !== null ? ` (build ${checksum})` : ""}${stackSummary !== null ? ` [${stackSummary}]` : ""}.`
1498
1631
  );
1499
1632
  return 0;
1500
1633
  }
@@ -1515,6 +1648,8 @@ async function main() {
1515
1648
  return runMarkBuild(args);
1516
1649
  case "protect":
1517
1650
  return runProtectCommand(args);
1651
+ case "guide":
1652
+ return runGuide();
1518
1653
  default:
1519
1654
  console.error(`Unknown command: ${args.command}
1520
1655
  `);