@patchstack/connect 0.2.5 → 0.2.7

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
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/cli.ts
4
+ import { readFileSync, writeFileSync } from "fs";
5
+
3
6
  // src/parsers/index.ts
4
7
  import { access } from "fs/promises";
5
8
  import path4 from "path";
@@ -653,54 +656,32 @@ async function readDirectDepNames(cwd) {
653
656
  }
654
657
 
655
658
  // src/parsers/index.ts
659
+ var LOCKFILE_CANDIDATES = [
660
+ { filename: "package-lock.json", strategy: "npm-lockfile" },
661
+ { filename: "bun.lock", strategy: "node-modules-walk" },
662
+ { filename: "bun.lockb", strategy: "node-modules-walk" },
663
+ { filename: "pnpm-lock.yaml", strategy: "pnpm-lockfile" },
664
+ { filename: "yarn.lock", strategy: "yarn-lockfile" }
665
+ ];
656
666
  async function detectLockfile(cwd) {
657
- const npmLock = path4.join(cwd, "package-lock.json");
658
- if (await exists(npmLock)) {
659
- return {
660
- ecosystem: "npm",
661
- filePath: npmLock,
662
- filename: "package-lock.json",
663
- strategy: "npm-lockfile"
664
- };
665
- }
666
- const bunLock = path4.join(cwd, "bun.lock");
667
- if (await exists(bunLock)) {
668
- return {
669
- ecosystem: "npm",
670
- filePath: bunLock,
671
- filename: "bun.lock",
672
- strategy: "node-modules-walk"
673
- };
674
- }
675
- const bunLockB = path4.join(cwd, "bun.lockb");
676
- if (await exists(bunLockB)) {
677
- return {
678
- ecosystem: "npm",
679
- filePath: bunLockB,
680
- filename: "bun.lockb",
681
- strategy: "node-modules-walk"
682
- };
683
- }
684
- const pnpmLock = path4.join(cwd, "pnpm-lock.yaml");
685
- if (await exists(pnpmLock)) {
686
- return {
687
- ecosystem: "npm",
688
- filePath: pnpmLock,
689
- filename: "pnpm-lock.yaml",
690
- strategy: "pnpm-lockfile"
691
- };
692
- }
693
- const yarnLock = path4.join(cwd, "yarn.lock");
694
- if (await exists(yarnLock)) {
667
+ const probed = await Promise.all(
668
+ LOCKFILE_CANDIDATES.map(async (candidate) => {
669
+ const filePath = path4.join(cwd, candidate.filename);
670
+ return { ...candidate, filePath, present: await exists(filePath) };
671
+ })
672
+ );
673
+ const match = probed.find((candidate) => candidate.present);
674
+ if (match) {
695
675
  return {
696
676
  ecosystem: "npm",
697
- filePath: yarnLock,
698
- filename: "yarn.lock",
699
- strategy: "yarn-lockfile"
677
+ filePath: match.filePath,
678
+ filename: match.filename,
679
+ strategy: match.strategy
700
680
  };
701
681
  }
682
+ const expected = LOCKFILE_CANDIDATES.map((candidate) => candidate.filename).join(", ");
702
683
  throw new PatchstackError(
703
- `No lockfile found in ${cwd}. Expected one of: package-lock.json, bun.lock, bun.lockb, yarn.lock, pnpm-lock.yaml.`,
684
+ `No lockfile found in ${cwd}. Expected one of: ${expected}.`,
704
685
  "LOCKFILE_NOT_FOUND"
705
686
  );
706
687
  }
@@ -832,6 +813,21 @@ function compareSegments(a, b) {
832
813
  return 0;
833
814
  }
834
815
 
816
+ // src/checksum.ts
817
+ import { createHash } from "crypto";
818
+ function computeManifestChecksum(packages) {
819
+ const canonical = packages.map((pkg) => ({ name: pkg.name, version: pkg.version })).sort((a, b) => {
820
+ if (a.name !== b.name) {
821
+ return a.name < b.name ? -1 : 1;
822
+ }
823
+ if (a.version !== b.version) {
824
+ return a.version < b.version ? -1 : 1;
825
+ }
826
+ return 0;
827
+ });
828
+ return createHash("sha256").update(JSON.stringify(canonical)).digest("hex").slice(0, 12);
829
+ }
830
+
835
831
  // src/client.ts
836
832
  var DEFAULT_ENDPOINT = "https://api.patchstack.com/monitor/pulse/manifest";
837
833
  var DEFAULT_TIMEOUT_MS = 3e4;
@@ -855,7 +851,7 @@ async function postManifest(config, payload) {
855
851
  Accept: "application/json",
856
852
  "User-Agent": "@patchstack/connect"
857
853
  },
858
- body: JSON.stringify(payload),
854
+ body: JSON.stringify({ ...payload, environment: config.environment }),
859
855
  signal: AbortSignal.timeout(timeoutMs)
860
856
  });
861
857
  } catch (cause) {
@@ -913,12 +909,21 @@ function isTimeoutError(cause) {
913
909
  import { readFile as readFile5, writeFile } from "fs/promises";
914
910
  import path5 from "path";
915
911
  var CONFIG_FILENAME = ".patchstackrc.json";
912
+ var DEFAULT_ENVIRONMENT = "production";
916
913
  async function resolveConfig(options) {
917
914
  const fromFile = await readConfigFile(options.cwd);
918
915
  const fromEnv = readEnv();
919
916
  const siteUuid = options.cliSiteUuid ?? fromEnv.siteUuid ?? fromFile.siteUuid ?? null;
920
917
  const endpoint = options.cliEndpoint ?? fromEnv.endpoint ?? fromFile.endpoint ?? DEFAULT_ENDPOINT;
921
918
  const timeoutMs = fromEnv.timeoutMs ?? fromFile.timeoutMs ?? DEFAULT_TIMEOUT_MS;
919
+ const environmentRaw = fromEnv.environment ?? fromFile.environment;
920
+ if (environmentRaw !== void 0 && !isEnvironment(environmentRaw)) {
921
+ throw new PatchstackError(
922
+ `Environment must be "production" or "sandbox"; got "${environmentRaw}".`,
923
+ "CONFIG_INVALID"
924
+ );
925
+ }
926
+ const environment = environmentRaw ?? DEFAULT_ENVIRONMENT;
922
927
  if (siteUuid !== null && siteUuid.length > 0 && !isUuid(siteUuid)) {
923
928
  throw new PatchstackError(
924
929
  `Site UUID "${siteUuid}" does not look like a valid UUID.`,
@@ -934,7 +939,8 @@ async function resolveConfig(options) {
934
939
  return {
935
940
  siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
936
941
  endpoint,
937
- timeoutMs
942
+ timeoutMs,
943
+ environment
938
944
  };
939
945
  }
940
946
  async function writeConfigFile(cwd, config) {
@@ -985,15 +991,74 @@ function readEnv() {
985
991
  }
986
992
  timeoutMs = parsed;
987
993
  }
994
+ const environmentRaw = process.env.PATCHSTACK_ENVIRONMENT;
988
995
  return {
989
996
  siteUuid: process.env.PATCHSTACK_SITE_UUID ?? void 0,
990
997
  endpoint: process.env.PATCHSTACK_ENDPOINT ?? void 0,
991
- timeoutMs
998
+ timeoutMs,
999
+ environment: environmentRaw !== void 0 && environmentRaw.length > 0 ? environmentRaw : void 0
992
1000
  };
993
1001
  }
994
1002
  function isUuid(value) {
995
1003
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
996
1004
  }
1005
+ function isEnvironment(value) {
1006
+ return value === "production" || value === "sandbox";
1007
+ }
1008
+
1009
+ // src/mark-build.ts
1010
+ import { existsSync, readdirSync, statSync } from "fs";
1011
+ import path6 from "path";
1012
+ var MARKER_ATTR = "data-patchstack-build";
1013
+ var BUILD_DIR_CANDIDATES = ["dist", "build", "out", ".output/public"];
1014
+ function resolveBuildDir(cwd, override) {
1015
+ if (override !== void 0 && override !== "") {
1016
+ const abs = path6.resolve(cwd, override);
1017
+ return existsSync(abs) && statSync(abs).isDirectory() ? abs : null;
1018
+ }
1019
+ for (const candidate of BUILD_DIR_CANDIDATES) {
1020
+ const abs = path6.resolve(cwd, candidate);
1021
+ if (existsSync(abs) && statSync(abs).isDirectory()) {
1022
+ return abs;
1023
+ }
1024
+ }
1025
+ return null;
1026
+ }
1027
+ function findHtmlFiles(dir) {
1028
+ const out = [];
1029
+ const walk2 = (current) => {
1030
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
1031
+ const full = path6.join(current, entry.name);
1032
+ if (entry.isDirectory()) {
1033
+ walk2(full);
1034
+ } else if (entry.isFile() && entry.name.toLowerCase().endsWith(".html")) {
1035
+ out.push(full);
1036
+ }
1037
+ }
1038
+ };
1039
+ walk2(dir);
1040
+ return out;
1041
+ }
1042
+ function buildInjectionSnippet(checksum) {
1043
+ const statements = ["window.__PATCHSTACK_PROD__=true;"];
1044
+ if (checksum !== null && checksum !== "") {
1045
+ statements.push(`window.__PATCHSTACK_BUILD__=${JSON.stringify(checksum)};`);
1046
+ }
1047
+ return `<script ${MARKER_ATTR}>${statements.join("")}</script>`;
1048
+ }
1049
+ function injectMarker(html, snippet) {
1050
+ const stripped = html.replace(
1051
+ new RegExp(`\\s*<script ${MARKER_ATTR}[^>]*>[\\s\\S]*?</script>`, "gi"),
1052
+ ""
1053
+ );
1054
+ if (/<\/head>/i.test(stripped)) {
1055
+ return stripped.replace(/<\/head>/i, `${snippet}</head>`);
1056
+ }
1057
+ if (/<\/body>/i.test(stripped)) {
1058
+ return stripped.replace(/<\/body>/i, `${snippet}</body>`);
1059
+ }
1060
+ return stripped + snippet;
1061
+ }
997
1062
 
998
1063
  // src/cli.ts
999
1064
  var HELP = `@patchstack/connect \u2014 scan your lockfile and report packages to Patchstack.
@@ -1005,6 +1070,8 @@ Usage:
1005
1070
  patchstack-connect init <site-uuid> Optional: pre-seed .patchstackrc.json
1006
1071
  with an existing site UUID
1007
1072
  patchstack-connect status [options] Show current configuration
1073
+ patchstack-connect mark-build [options] Stamp built HTML with a production flag +
1074
+ build fingerprint (run as a postbuild step)
1008
1075
  patchstack-connect help Print this message
1009
1076
 
1010
1077
  Options (for scan and status):
@@ -1012,10 +1079,15 @@ Options (for scan and status):
1012
1079
  --endpoint <url> Override the API endpoint
1013
1080
  --dry-run (scan only) Show the payload without posting
1014
1081
 
1082
+ Options (for mark-build):
1083
+ --dir <path> Build output directory (default: auto-detect
1084
+ dist/ build/ out/ .output/public)
1085
+
1015
1086
  Environment:
1016
1087
  PATCHSTACK_SITE_UUID Site UUID
1017
1088
  PATCHSTACK_ENDPOINT API endpoint (default: https://api.patchstack.com/monitor/pulse/manifest)
1018
1089
  PATCHSTACK_TIMEOUT_MS Request timeout in ms (default: 30000)
1090
+ PATCHSTACK_ENVIRONMENT Manifest environment: production | sandbox (default: production)
1019
1091
 
1020
1092
  Precedence: CLI flag > environment variable > .patchstackrc.json.
1021
1093
 
@@ -1025,7 +1097,7 @@ Examples:
1025
1097
  npx @patchstack/connect init 550e8400-e29b-41d4-a716-446655440000
1026
1098
  npx @patchstack/connect scan --site-uuid 550e8400-...-446655440000
1027
1099
  `;
1028
- var VALUE_FLAGS = /* @__PURE__ */ new Set(["site-uuid", "endpoint"]);
1100
+ var VALUE_FLAGS = /* @__PURE__ */ new Set(["site-uuid", "endpoint", "dir"]);
1029
1101
  function parseArgs(argv) {
1030
1102
  const args = argv.slice(2);
1031
1103
  const positional = [];
@@ -1089,6 +1161,7 @@ async function runScan(args) {
1089
1161
  console.log(
1090
1162
  `Found ${payload.packages.length} unique package versions across ${stats.uniqueNames} package names in ${manifest.ecosystem} lockfile.`
1091
1163
  );
1164
+ console.log(`Reporting under the ${config.environment} environment.`);
1092
1165
  if (stats.duplicateNames.length > 0) {
1093
1166
  console.log(`${stats.duplicateNames.length} package(s) appear at multiple versions:`);
1094
1167
  if (stats.duplicateNames.length <= 10) {
@@ -1139,14 +1212,54 @@ async function runStatus(args) {
1139
1212
  cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
1140
1213
  cliEndpoint: getStringFlag(args.flags, "endpoint")
1141
1214
  });
1142
- console.log(`Site UUID: ${config.siteUuid ?? "(none yet \u2014 the next `scan` will provision one)"}`);
1143
- console.log(`Endpoint: ${config.endpoint}`);
1144
- console.log(`Timeout: ${config.timeoutMs}ms`);
1215
+ console.log(`Site UUID: ${config.siteUuid ?? "(none yet \u2014 the next `scan` will provision one)"}`);
1216
+ console.log(`Endpoint: ${config.endpoint}`);
1217
+ console.log(`Timeout: ${config.timeoutMs}ms`);
1218
+ console.log(`Environment: ${config.environment}`);
1145
1219
  if (config.siteUuid !== null) {
1146
1220
  console.log(`Claim URL: ${buildClaimUrl(config.endpoint, config.siteUuid)}`);
1147
1221
  }
1148
1222
  return 0;
1149
1223
  }
1224
+ async function runMarkBuild(args) {
1225
+ const cwd = process.cwd();
1226
+ let checksum = null;
1227
+ try {
1228
+ const manifest = await scanLockfile(cwd);
1229
+ const { payload } = buildWirePayload(manifest);
1230
+ checksum = computeManifestChecksum(payload.packages);
1231
+ } catch (err) {
1232
+ console.warn(
1233
+ `mark-build: could not compute the build fingerprint (${err.message}). Stamping the production flag only.`
1234
+ );
1235
+ }
1236
+ const dir = resolveBuildDir(cwd, getStringFlag(args.flags, "dir"));
1237
+ if (dir === null) {
1238
+ console.warn(
1239
+ "mark-build: no build output directory found (looked for dist/, build/, out/, .output/public). Pass --dir <path> if it is elsewhere. Nothing to mark."
1240
+ );
1241
+ return 0;
1242
+ }
1243
+ const files = findHtmlFiles(dir);
1244
+ if (files.length === 0) {
1245
+ console.warn(`mark-build: no HTML files found under ${dir}. Nothing to mark.`);
1246
+ return 0;
1247
+ }
1248
+ const snippet = buildInjectionSnippet(checksum);
1249
+ let marked = 0;
1250
+ for (const file of files) {
1251
+ const before = readFileSync(file, "utf8");
1252
+ const after = injectMarker(before, snippet);
1253
+ if (after !== before) {
1254
+ writeFileSync(file, after);
1255
+ marked += 1;
1256
+ }
1257
+ }
1258
+ console.log(
1259
+ `mark-build: marked ${marked} HTML file(s) in ${dir}${checksum !== null ? ` (build ${checksum})` : ""}.`
1260
+ );
1261
+ return 0;
1262
+ }
1150
1263
  async function main() {
1151
1264
  const args = parseArgs(process.argv);
1152
1265
  if (args.flags.has("help") || args.command === "help" || args.command === null) {
@@ -1160,6 +1273,8 @@ async function main() {
1160
1273
  return runScan(args);
1161
1274
  case "status":
1162
1275
  return runStatus(args);
1276
+ case "mark-build":
1277
+ return runMarkBuild(args);
1163
1278
  default:
1164
1279
  console.error(`Unknown command: ${args.command}
1165
1280
  `);