@patchstack/connect 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,9 +17,11 @@ npm install --save-dev @patchstack/connect
17
17
  npx @patchstack/connect scan
18
18
  ```
19
19
 
20
+ > **Use your project's own package manager.** On bun-managed projects (Lovable, Bolt, most vibe-coding platforms) install with `bun add -d @patchstack/connect` instead — running `npm install` there plants a `package-lock.json` that the platform's native dependency flow never updates again, leaving a stale lockfile next to the live one. The connector detects and works around that (see *Stale lockfiles* below), but not creating the fossil is better.
21
+
20
22
  That's it. The first `scan`:
21
23
 
22
- 1. Reads your `package-lock.json`.
24
+ 1. Reads your lockfile (see *Supported lockfiles*).
23
25
  2. POSTs the package list to Patchstack with **no** UUID.
24
26
  3. Patchstack provisions a fresh site and returns its UUID.
25
27
  4. The connector writes the UUID to `.patchstackrc.json` so the next `scan` targets the same site.
@@ -123,6 +125,10 @@ That's the entire payload. No source code, no environment variables, no file pat
123
125
 
124
126
  If both a Bun lockfile and `node_modules/` are present, the connector walks `node_modules/` to enumerate the installed packages. Run `bun install` (or `npm install`) before scanning so the directory is populated.
125
127
 
128
+ ### Stale lockfiles
129
+
130
+ Every scanned source is validated against `package.json`: if the chosen lockfile is missing dependencies that `package.json` declares, it is treated as a fossil (e.g. a `package-lock.json` created by a one-off `npm install` in a bun-managed project) and the connector falls through to the next source — ultimately walking `node_modules/`, the installed truth — and prints a warning naming the stale file. Delete the stale lockfile to silence the warning. Without this, the manifest and the build fingerprint would silently freeze while the real dependency set drifts.
131
+
126
132
  ## Development
127
133
 
128
134
  ```bash
@@ -132,6 +138,22 @@ npm test
132
138
  npm run build
133
139
  ```
134
140
 
141
+ ### Manifest endpoint testing
142
+
143
+ To post the current lockfile manifest to a local Patchstack API endpoint and provision a new site:
144
+
145
+ ```bash
146
+ bun run test:manifest -- --endpoint http://localhost:8000/monitor/pulse/manifest
147
+ ```
148
+
149
+ The response should include the new site UUID. To re-test an existing site, pass that UUID explicitly:
150
+
151
+ ```bash
152
+ bun run test:manifest -- --endpoint http://localhost:8000/monitor/pulse/manifest --site-uuid YOUR_REAL_UUID
153
+ ```
154
+
155
+ Use `--dry-run` to preview the payload without posting.
156
+
135
157
  ## Release process
136
158
 
137
159
  Pull requests run typecheck, tests, build, package verification, and a production dependency audit in GitHub Actions.
package/dist/cli.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { readFileSync, writeFileSync } from "fs";
4
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
5
5
 
6
6
  // src/parsers/index.ts
7
7
  import { access } from "fs/promises";
8
- import path4 from "path";
8
+ import path5 from "path";
9
9
 
10
10
  // src/types.ts
11
11
  var PatchstackError = class extends Error {
@@ -19,13 +19,52 @@ var PatchstackError = class extends Error {
19
19
  cause;
20
20
  };
21
21
 
22
- // src/parsers/npm.ts
22
+ // src/parsers/consistency.ts
23
23
  import { readFile } from "fs/promises";
24
24
  import path from "path";
25
+ async function readDeclaredDependencyNames(cwd) {
26
+ let raw;
27
+ try {
28
+ raw = await readFile(path.join(cwd, "package.json"), "utf8");
29
+ } catch {
30
+ return [];
31
+ }
32
+ let parsed;
33
+ try {
34
+ parsed = JSON.parse(raw);
35
+ } catch {
36
+ return [];
37
+ }
38
+ if (typeof parsed !== "object" || parsed === null) {
39
+ return [];
40
+ }
41
+ const names = /* @__PURE__ */ new Set();
42
+ for (const group of ["dependencies", "devDependencies"]) {
43
+ const deps = parsed[group];
44
+ if (typeof deps !== "object" || deps === null) {
45
+ continue;
46
+ }
47
+ for (const [name, spec] of Object.entries(deps)) {
48
+ if (typeof spec === "string" && /^(file|link|workspace|portal):/.test(spec)) {
49
+ continue;
50
+ }
51
+ names.add(name);
52
+ }
53
+ }
54
+ return [...names];
55
+ }
56
+ function missingDependencies(declared, packages) {
57
+ const present = new Set(packages.map((entry) => entry.name));
58
+ return declared.filter((name) => !present.has(name));
59
+ }
60
+
61
+ // src/parsers/npm.ts
62
+ import { readFile as readFile2 } from "fs/promises";
63
+ import path2 from "path";
25
64
  async function parseNpmLockfile(lockfilePath) {
26
65
  let raw;
27
66
  try {
28
- raw = await readFile(lockfilePath, "utf8");
67
+ raw = await readFile2(lockfilePath, "utf8");
29
68
  } catch (cause) {
30
69
  throw new PatchstackError(`Could not read lockfile at ${lockfilePath}`, "LOCKFILE_NOT_FOUND", cause);
31
70
  }
@@ -83,7 +122,7 @@ function extractFromV1(deps, acc = [], depth = 0) {
83
122
  return acc;
84
123
  }
85
124
  function extractNameFromPath(pkgPath) {
86
- const segments = pkgPath.split("node_modules" + path.sep === pkgPath ? path.sep : "/");
125
+ const segments = pkgPath.split("node_modules" + path2.sep === pkgPath ? path2.sep : "/");
87
126
  const parts = pkgPath.split("/");
88
127
  const nmIndex = parts.lastIndexOf("node_modules");
89
128
  if (nmIndex === -1 || nmIndex >= parts.length - 1) {
@@ -106,10 +145,10 @@ function isDirectV2(pkgPath) {
106
145
  }
107
146
 
108
147
  // src/parsers/node_modules.ts
109
- import { lstat, readFile as readFile2, readdir, stat } from "fs/promises";
110
- import path2 from "path";
148
+ import { lstat, readFile as readFile3, readdir, stat } from "fs/promises";
149
+ import path3 from "path";
111
150
  async function walkNodeModules(cwd) {
112
- const root = path2.join(cwd, "node_modules");
151
+ const root = path3.join(cwd, "node_modules");
113
152
  try {
114
153
  const info = await stat(root);
115
154
  if (!info.isDirectory()) {
@@ -143,7 +182,7 @@ async function walk(dir, acc, depth) {
143
182
  if (name.startsWith(".")) {
144
183
  continue;
145
184
  }
146
- const fullPath = path2.join(dir, name);
185
+ const fullPath = path3.join(dir, name);
147
186
  if (!await isPlainDirectory(fullPath)) {
148
187
  continue;
149
188
  }
@@ -158,7 +197,7 @@ async function walk(dir, acc, depth) {
158
197
  if (sub.startsWith(".")) {
159
198
  continue;
160
199
  }
161
- const scopedDir = path2.join(fullPath, sub);
200
+ const scopedDir = path3.join(fullPath, sub);
162
201
  if (!await isPlainDirectory(scopedDir)) {
163
202
  continue;
164
203
  }
@@ -174,7 +213,7 @@ async function walk(dir, acc, depth) {
174
213
  async function readPackage(pkgDir, depth, acc) {
175
214
  let raw;
176
215
  try {
177
- raw = await readFile2(path2.join(pkgDir, "package.json"), "utf8");
216
+ raw = await readFile3(path3.join(pkgDir, "package.json"), "utf8");
178
217
  } catch {
179
218
  return;
180
219
  }
@@ -197,7 +236,7 @@ async function readPackage(pkgDir, depth, acc) {
197
236
  });
198
237
  }
199
238
  async function walkNested(pkgDir, acc, depth) {
200
- const nested = path2.join(pkgDir, "node_modules");
239
+ const nested = path3.join(pkgDir, "node_modules");
201
240
  if (!await isPlainDirectory(nested)) {
202
241
  return;
203
242
  }
@@ -213,11 +252,11 @@ async function isPlainDirectory(dir) {
213
252
  }
214
253
 
215
254
  // src/parsers/pnpm.ts
216
- import { readFile as readFile3 } from "fs/promises";
255
+ import { readFile as readFile4 } from "fs/promises";
217
256
  async function parsePnpmLockfile(lockfilePath) {
218
257
  let raw;
219
258
  try {
220
- raw = await readFile3(lockfilePath, "utf8");
259
+ raw = await readFile4(lockfilePath, "utf8");
221
260
  } catch (cause) {
222
261
  throw new PatchstackError(
223
262
  `Could not read lockfile at ${lockfilePath}`,
@@ -465,12 +504,12 @@ function extractLeafName(trimmed) {
465
504
  }
466
505
 
467
506
  // src/parsers/yarn.ts
468
- import { readFile as readFile4 } from "fs/promises";
469
- import path3 from "path";
507
+ import { readFile as readFile5 } from "fs/promises";
508
+ import path4 from "path";
470
509
  async function parseYarnLockfile(lockfilePath) {
471
510
  let raw;
472
511
  try {
473
- raw = await readFile4(lockfilePath, "utf8");
512
+ raw = await readFile5(lockfilePath, "utf8");
474
513
  } catch (cause) {
475
514
  throw new PatchstackError(
476
515
  `Could not read lockfile at ${lockfilePath}`,
@@ -485,7 +524,7 @@ async function parseYarnLockfile(lockfilePath) {
485
524
  "LOCKFILE_PARSE_ERROR"
486
525
  );
487
526
  }
488
- const directNames = await readDirectDepNames(path3.dirname(lockfilePath));
527
+ const directNames = await readDirectDepNames(path4.dirname(lockfilePath));
489
528
  const entries = [];
490
529
  const seen = /* @__PURE__ */ new Set();
491
530
  for (const block of blocks) {
@@ -629,7 +668,7 @@ async function readDirectDepNames(cwd) {
629
668
  const names = /* @__PURE__ */ new Set();
630
669
  let raw;
631
670
  try {
632
- raw = await readFile4(path3.join(cwd, "package.json"), "utf8");
671
+ raw = await readFile5(path4.join(cwd, "package.json"), "utf8");
633
672
  } catch {
634
673
  return names;
635
674
  }
@@ -666,7 +705,7 @@ var LOCKFILE_CANDIDATES = [
666
705
  async function detectLockfile(cwd) {
667
706
  const probed = await Promise.all(
668
707
  LOCKFILE_CANDIDATES.map(async (candidate) => {
669
- const filePath = path4.join(cwd, candidate.filename);
708
+ const filePath = path5.join(cwd, candidate.filename);
670
709
  return { ...candidate, filePath, present: await exists(filePath) };
671
710
  })
672
711
  );
@@ -686,9 +725,69 @@ async function detectLockfile(cwd) {
686
725
  );
687
726
  }
688
727
  async function scanLockfile(cwd) {
689
- const detected = await detectLockfile(cwd);
690
- const packages = await runStrategy(detected, cwd);
691
- return { ecosystem: detected.ecosystem, packages };
728
+ const candidates = await presentLockfiles(cwd);
729
+ if (candidates.length === 0) {
730
+ await detectLockfile(cwd);
731
+ }
732
+ const declared = await readDeclaredDependencyNames(cwd);
733
+ const warnings = [];
734
+ let firstParsed = null;
735
+ let walkTried = false;
736
+ for (const candidate of candidates) {
737
+ let packages;
738
+ try {
739
+ packages = await runStrategy(candidate, cwd);
740
+ } catch {
741
+ continue;
742
+ }
743
+ walkTried ||= candidate.strategy === "node-modules-walk";
744
+ firstParsed ??= { packages, filename: candidate.filename };
745
+ const missing = missingDependencies(declared, packages);
746
+ if (missing.length === 0) {
747
+ return manifestWith(packages, warnings);
748
+ }
749
+ warnings.push(staleWarning(candidate.filename, missing));
750
+ }
751
+ if (!walkTried) {
752
+ try {
753
+ const packages = await walkNodeModules(cwd);
754
+ const missing = missingDependencies(declared, packages);
755
+ if (missing.length > 0) {
756
+ warnings.push(staleWarning("node_modules/", missing));
757
+ }
758
+ warnings.push("Scanned node_modules/ instead. Delete the stale lockfile to silence this warning.");
759
+ return manifestWith(packages, warnings);
760
+ } catch {
761
+ }
762
+ }
763
+ if (firstParsed === null) {
764
+ const expected = LOCKFILE_CANDIDATES.map((candidate) => candidate.filename).join(", ");
765
+ throw new PatchstackError(
766
+ `No readable lockfile found in ${cwd}. Expected one of: ${expected}.`,
767
+ "LOCKFILE_NOT_FOUND"
768
+ );
769
+ }
770
+ warnings.push(
771
+ `No fully-consistent source found; reporting ${firstParsed.filename}. The manifest may understate the real dependency set.`
772
+ );
773
+ return manifestWith(firstParsed.packages, warnings);
774
+ }
775
+ function manifestWith(packages, warnings) {
776
+ return warnings.length > 0 ? { ecosystem: "npm", packages, warnings } : { ecosystem: "npm", packages };
777
+ }
778
+ function staleWarning(source, missing) {
779
+ const sample = missing.slice(0, 3).join(", ");
780
+ const suffix = missing.length > 3 ? `, +${missing.length - 3} more` : "";
781
+ return `${source} looks stale: package.json declares ${missing.length} dependenc${missing.length === 1 ? "y" : "ies"} it does not contain (${sample}${suffix}).`;
782
+ }
783
+ async function presentLockfiles(cwd) {
784
+ const probed = await Promise.all(
785
+ LOCKFILE_CANDIDATES.map(async (candidate) => {
786
+ const filePath = path5.join(cwd, candidate.filename);
787
+ return { ...candidate, filePath, present: await exists(filePath) };
788
+ })
789
+ );
790
+ return probed.filter((candidate) => candidate.present).map(({ filename, strategy, filePath }) => ({ ecosystem: "npm", filename, strategy, filePath }));
692
791
  }
693
792
  async function runStrategy(detected, cwd) {
694
793
  switch (detected.strategy) {
@@ -906,8 +1005,8 @@ function isTimeoutError(cause) {
906
1005
  }
907
1006
 
908
1007
  // src/config.ts
909
- import { readFile as readFile5, writeFile } from "fs/promises";
910
- import path5 from "path";
1008
+ import { readFile as readFile6, writeFile } from "fs/promises";
1009
+ import path6 from "path";
911
1010
  var CONFIG_FILENAME = ".patchstackrc.json";
912
1011
  var DEFAULT_ENVIRONMENT = "production";
913
1012
  async function resolveConfig(options) {
@@ -944,7 +1043,7 @@ async function resolveConfig(options) {
944
1043
  };
945
1044
  }
946
1045
  async function writeConfigFile(cwd, config) {
947
- const target = path5.join(cwd, CONFIG_FILENAME);
1046
+ const target = path6.join(cwd, CONFIG_FILENAME);
948
1047
  const content = JSON.stringify(config, null, 2) + "\n";
949
1048
  await writeFile(target, content, "utf8");
950
1049
  return target;
@@ -954,10 +1053,10 @@ async function persistSiteUuid(cwd, siteUuid) {
954
1053
  return writeConfigFile(cwd, { ...existing, siteUuid });
955
1054
  }
956
1055
  async function readConfigFile(cwd) {
957
- const target = path5.join(cwd, CONFIG_FILENAME);
1056
+ const target = path6.join(cwd, CONFIG_FILENAME);
958
1057
  let raw;
959
1058
  try {
960
- raw = await readFile5(target, "utf8");
1059
+ raw = await readFile6(target, "utf8");
961
1060
  } catch (err) {
962
1061
  if (err.code === "ENOENT") {
963
1062
  return {};
@@ -1008,16 +1107,16 @@ function isEnvironment(value) {
1008
1107
 
1009
1108
  // src/mark-build.ts
1010
1109
  import { existsSync, readdirSync, statSync } from "fs";
1011
- import path6 from "path";
1110
+ import path7 from "path";
1012
1111
  var MARKER_ATTR = "data-patchstack-build";
1013
1112
  var BUILD_DIR_CANDIDATES = ["dist", "build", "out", ".output/public"];
1014
1113
  function resolveBuildDir(cwd, override) {
1015
1114
  if (override !== void 0 && override !== "") {
1016
- const abs = path6.resolve(cwd, override);
1115
+ const abs = path7.resolve(cwd, override);
1017
1116
  return existsSync(abs) && statSync(abs).isDirectory() ? abs : null;
1018
1117
  }
1019
1118
  for (const candidate of BUILD_DIR_CANDIDATES) {
1020
- const abs = path6.resolve(cwd, candidate);
1119
+ const abs = path7.resolve(cwd, candidate);
1021
1120
  if (existsSync(abs) && statSync(abs).isDirectory()) {
1022
1121
  return abs;
1023
1122
  }
@@ -1028,7 +1127,7 @@ function findHtmlFiles(dir) {
1028
1127
  const out = [];
1029
1128
  const walk2 = (current) => {
1030
1129
  for (const entry of readdirSync(current, { withFileTypes: true })) {
1031
- const full = path6.join(current, entry.name);
1130
+ const full = path7.join(current, entry.name);
1032
1131
  if (entry.isDirectory()) {
1033
1132
  walk2(full);
1034
1133
  } else if (entry.isFile() && entry.name.toLowerCase().endsWith(".html")) {
@@ -1060,6 +1159,151 @@ function injectMarker(html, snippet) {
1060
1159
  return stripped + snippet;
1061
1160
  }
1062
1161
 
1162
+ // src/protect/install.ts
1163
+ import { readFileSync, writeFileSync, existsSync as existsSync2, mkdirSync, copyFileSync } from "fs";
1164
+ import { join } from "path";
1165
+ import { fileURLToPath } from "url";
1166
+ var TEMPLATES = fileURLToPath(new URL("./protect/templates/", import.meta.url));
1167
+ var CLIENT_TUNNEL = [
1168
+ "",
1169
+ " // PATCHSTACK auto-guard: in the browser, tunnel Supabase traffic through the app's own",
1170
+ " // server guard (same-origin) so payloads are inspected before they reach Supabase.",
1171
+ " if (typeof window !== 'undefined') {",
1172
+ " const target = typeof input === 'string' ? input : input instanceof Request ? input.url : String(input);",
1173
+ " const method = init?.method ?? (input instanceof Request ? input.method : 'GET');",
1174
+ " headers.set('x-ps-target', target);",
1175
+ " const guardUrl = new URL('/_patchstack/guard', window.location.origin).toString();",
1176
+ " return fetch(guardUrl, { ...init, method, headers });",
1177
+ " }",
1178
+ ""
1179
+ ].join("\n");
1180
+ var START_IMPORTS = [
1181
+ 'import { getRequest } from "@tanstack/react-start/server";',
1182
+ 'import { GUARD_PATH, handleGuardRequest } from "@/integrations/patchstack/guard";'
1183
+ ].join("\n");
1184
+ var START_MIDDLEWARE = [
1185
+ "",
1186
+ "// Patchstack auto-guard: intercept the tunneled data traffic before anything else runs.",
1187
+ "const patchstackGuard = createMiddleware().server(async ({ next }) => {",
1188
+ " const request = getRequest();",
1189
+ " if (request) {",
1190
+ " const { pathname } = new URL(request.url);",
1191
+ " if (pathname === GUARD_PATH) return handleGuardRequest(request);",
1192
+ " }",
1193
+ " return next();",
1194
+ "});",
1195
+ ""
1196
+ ].join("\n");
1197
+ var PS_DIR_REL = "src/integrations/patchstack";
1198
+ function log(msg) {
1199
+ console.log(`patchstack protect: ${msg}`);
1200
+ }
1201
+ function detectSupportedStack(cwd) {
1202
+ const pkgPath = join(cwd, "package.json");
1203
+ if (!existsSync2(pkgPath)) return false;
1204
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
1205
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1206
+ return Boolean(deps["@tanstack/react-start"]) && existsSync2(join(cwd, "src/start.ts")) && existsSync2(join(cwd, "src/integrations/supabase/client.ts"));
1207
+ }
1208
+ function generateManifest(cwd) {
1209
+ const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
1210
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1211
+ const packages = {};
1212
+ for (const name of Object.keys(deps)) {
1213
+ let version = String(deps[name]).replace(/^[^\d]*/, "");
1214
+ const installed = join(cwd, "node_modules", name, "package.json");
1215
+ if (existsSync2(installed)) {
1216
+ try {
1217
+ version = JSON.parse(readFileSync(installed, "utf8")).version;
1218
+ } catch {
1219
+ }
1220
+ }
1221
+ packages[name] = version;
1222
+ }
1223
+ mkdirSync(join(cwd, PS_DIR_REL), { recursive: true });
1224
+ 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";
1225
+ writeFileSync(join(cwd, PS_DIR_REL, "manifest.js"), body);
1226
+ log(`wrote manifest.js (${Object.keys(packages).length} packages)`);
1227
+ }
1228
+ function scaffold(cwd) {
1229
+ const dst = join(cwd, PS_DIR_REL);
1230
+ mkdirSync(dst, { recursive: true });
1231
+ copyFileSync(join(TEMPLATES, "engine.js"), join(dst, "engine.js"));
1232
+ copyFileSync(join(TEMPLATES, "engine.d.ts"), join(dst, "engine.d.ts"));
1233
+ copyFileSync(join(TEMPLATES, "manifest.d.ts"), join(dst, "manifest.d.ts"));
1234
+ copyFileSync(join(TEMPLATES, "rules.json"), join(dst, "rules.json"));
1235
+ copyFileSync(join(TEMPLATES, "guard.ts"), join(dst, "guard.ts"));
1236
+ log("scaffolded engine.js (+types), rules.json, guard.ts");
1237
+ }
1238
+ function patchClient(cwd) {
1239
+ const p = join(cwd, "src/integrations/supabase/client.ts");
1240
+ let s = readFileSync(p, "utf8");
1241
+ if (s.includes("x-ps-target")) {
1242
+ log("client.ts already wired");
1243
+ return;
1244
+ }
1245
+ const anchor = "headers.set('apikey', supabaseKey);";
1246
+ if (!s.includes(anchor)) {
1247
+ log("client.ts anchor not found (template changed?) \u2014 skipping client patch");
1248
+ return;
1249
+ }
1250
+ s = s.replace(anchor, anchor + "\n" + CLIENT_TUNNEL);
1251
+ writeFileSync(p, s);
1252
+ log("patched client.ts (tunnel Supabase through the guard)");
1253
+ }
1254
+ function patchStart(cwd) {
1255
+ const p = join(cwd, "src/start.ts");
1256
+ let s = readFileSync(p, "utf8");
1257
+ if (s.includes("patchstackGuard")) {
1258
+ log("start.ts already wired");
1259
+ return;
1260
+ }
1261
+ const importAnchor = 'import { createStart, createMiddleware } from "@tanstack/react-start";';
1262
+ const exportAnchor = "export const startInstance";
1263
+ const rmAnchor = "requestMiddleware: [";
1264
+ if (!s.includes(importAnchor) || !s.includes(exportAnchor) || !s.includes(rmAnchor)) {
1265
+ log("start.ts anchors not found (template changed?) \u2014 skipping start patch");
1266
+ return;
1267
+ }
1268
+ s = s.replace(importAnchor, importAnchor + "\n" + START_IMPORTS);
1269
+ s = s.replace(exportAnchor, START_MIDDLEWARE + "\n" + exportAnchor);
1270
+ s = s.replace(rmAnchor, rmAnchor + "patchstackGuard, ");
1271
+ writeFileSync(p, s);
1272
+ log("patched start.ts (registered the guard as request middleware)");
1273
+ }
1274
+ function wirePrebuild(cwd) {
1275
+ const p = join(cwd, "package.json");
1276
+ const pkg = JSON.parse(readFileSync(p, "utf8"));
1277
+ pkg.scripts = pkg.scripts || {};
1278
+ const step = "patchstack-connect protect --manifest";
1279
+ const prebuild = pkg.scripts.prebuild || "";
1280
+ if (prebuild.includes(step)) {
1281
+ log("prebuild already refreshes the manifest");
1282
+ return;
1283
+ }
1284
+ pkg.scripts.prebuild = prebuild ? prebuild + " && " + step : step;
1285
+ writeFileSync(p, JSON.stringify(pkg, null, 2) + "\n");
1286
+ log("wired manifest refresh into prebuild");
1287
+ }
1288
+ function runProtect(cwd, opts = {}) {
1289
+ if (opts.manifestOnly) {
1290
+ generateManifest(cwd);
1291
+ return;
1292
+ }
1293
+ if (!detectSupportedStack(cwd)) {
1294
+ log(
1295
+ "runtime protection currently supports TanStack Start + Supabase apps; stack not detected \u2014 skipping."
1296
+ );
1297
+ return;
1298
+ }
1299
+ scaffold(cwd);
1300
+ patchClient(cwd);
1301
+ patchStart(cwd);
1302
+ generateManifest(cwd);
1303
+ wirePrebuild(cwd);
1304
+ log("done \u2014 guard wired. Set PATCHSTACK_MODE=block to enforce (default dry-run).");
1305
+ }
1306
+
1063
1307
  // src/cli.ts
1064
1308
  var HELP = `@patchstack/connect \u2014 scan your lockfile and report packages to Patchstack.
1065
1309
 
@@ -1072,6 +1316,9 @@ Usage:
1072
1316
  patchstack-connect status [options] Show current configuration
1073
1317
  patchstack-connect mark-build [options] Stamp built HTML with a production flag +
1074
1318
  build fingerprint (run as a postbuild step)
1319
+ patchstack-connect protect [--manifest] Install runtime protection (the guard) into
1320
+ a server-side JS app. --manifest only
1321
+ regenerates the package manifest (prebuild).
1075
1322
  patchstack-connect help Print this message
1076
1323
 
1077
1324
  Options (for scan and status):
@@ -1157,6 +1404,9 @@ async function runScan(args) {
1157
1404
  cliEndpoint: getStringFlag(args.flags, "endpoint")
1158
1405
  });
1159
1406
  const manifest = await scanLockfile(process.cwd());
1407
+ for (const warning of manifest.warnings ?? []) {
1408
+ console.warn(`patchstack: ${warning}`);
1409
+ }
1160
1410
  const { payload, stats } = buildWirePayload(manifest);
1161
1411
  console.log(
1162
1412
  `Found ${payload.packages.length} unique package versions across ${stats.uniqueNames} package names in ${manifest.ecosystem} lockfile.`
@@ -1206,6 +1456,14 @@ async function runScan(args) {
1206
1456
  }
1207
1457
  return 0;
1208
1458
  }
1459
+ async function runProtectCommand(args) {
1460
+ try {
1461
+ runProtect(process.cwd(), { manifestOnly: args.flags.get("manifest") === true });
1462
+ } catch (err) {
1463
+ console.warn(`patchstack protect: skipped (${err.message}).`);
1464
+ }
1465
+ return 0;
1466
+ }
1209
1467
  async function runStatus(args) {
1210
1468
  const config = await resolveConfig({
1211
1469
  cwd: process.cwd(),
@@ -1226,6 +1484,9 @@ async function runMarkBuild(args) {
1226
1484
  let checksum = null;
1227
1485
  try {
1228
1486
  const manifest = await scanLockfile(cwd);
1487
+ for (const warning of manifest.warnings ?? []) {
1488
+ console.warn(`mark-build: ${warning}`);
1489
+ }
1229
1490
  const { payload } = buildWirePayload(manifest);
1230
1491
  checksum = computeManifestChecksum(payload.packages);
1231
1492
  } catch (err) {
@@ -1248,10 +1509,10 @@ async function runMarkBuild(args) {
1248
1509
  const snippet = buildInjectionSnippet(checksum);
1249
1510
  let marked = 0;
1250
1511
  for (const file of files) {
1251
- const before = readFileSync(file, "utf8");
1512
+ const before = readFileSync2(file, "utf8");
1252
1513
  const after = injectMarker(before, snippet);
1253
1514
  if (after !== before) {
1254
- writeFileSync(file, after);
1515
+ writeFileSync2(file, after);
1255
1516
  marked += 1;
1256
1517
  }
1257
1518
  }
@@ -1275,6 +1536,8 @@ async function main() {
1275
1536
  return runStatus(args);
1276
1537
  case "mark-build":
1277
1538
  return runMarkBuild(args);
1539
+ case "protect":
1540
+ return runProtectCommand(args);
1278
1541
  default:
1279
1542
  console.error(`Unknown command: ${args.command}
1280
1543
  `);