@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 +164 -49
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +38 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +38 -45
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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:
|
|
698
|
-
filename:
|
|
699
|
-
strategy:
|
|
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:
|
|
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:
|
|
1143
|
-
console.log(`Endpoint:
|
|
1144
|
-
console.log(`Timeout:
|
|
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
|
`);
|