@patchstack/connect 0.3.28 → 0.3.30
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 +37 -21
- package/README.md +24 -10
- package/dist/{chunk-MJOTFUDE.js → chunk-LLKP5EJS.js} +1 -1
- package/dist/chunk-LLKP5EJS.js.map +1 -0
- package/dist/cli.js +1267 -224
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +419 -104
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -9
- package/dist/index.d.ts +28 -9
- package/dist/index.js +412 -97
- package/dist/index.js.map +1 -1
- package/dist/protect/templates/astro-middleware.ts +33 -13
- package/dist/protect/templates/demo-rules.json +2 -3
- package/dist/protect/templates/express-guard.cjs +26 -15
- package/dist/protect/templates/express-guard.js +26 -15
- package/dist/protect/templates/express-guard.ts +33 -16
- package/dist/protect/templates/fastify-plugin.cjs +33 -11
- package/dist/protect/templates/fastify-plugin.js +33 -11
- package/dist/protect/templates/fastify-plugin.ts +40 -12
- package/dist/protect/templates/generic-guard.cjs +28 -12
- package/dist/protect/templates/generic-guard.js +28 -13
- package/dist/protect/templates/generic-guard.ts +41 -20
- package/dist/protect/templates/guard.ts +47 -27
- package/dist/protect/templates/next-middleware.ts +29 -12
- package/dist/protect/templates/nuxt-middleware.ts +29 -12
- package/dist/protect/templates/rules.json +2 -2
- package/dist/protect/templates/sveltekit-hooks.ts +33 -13
- package/dist/protect.cjs +1039 -235
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.ts +39 -11
- package/dist/protect.edge.js +663 -132
- package/dist/protect.edge.js.map +4 -4
- package/dist/protect.js +665 -134
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-VRBE6RH6.js → refresh-manifest-ZSP76JWQ.js} +351 -94
- package/dist/refresh-manifest-ZSP76JWQ.js.map +1 -0
- package/package.json +5 -2
- package/dist/chunk-MJOTFUDE.js.map +0 -1
- package/dist/refresh-manifest-VRBE6RH6.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -34,7 +34,7 @@ async function readDeclaredDependencyNames(cwd) {
|
|
|
34
34
|
return [];
|
|
35
35
|
}
|
|
36
36
|
const names = /* @__PURE__ */ new Set();
|
|
37
|
-
for (const group of ["dependencies", "devDependencies"]) {
|
|
37
|
+
for (const group of ["dependencies", "devDependencies", "optionalDependencies"]) {
|
|
38
38
|
const deps = parsed[group];
|
|
39
39
|
if (typeof deps !== "object" || deps === null) {
|
|
40
40
|
continue;
|
|
@@ -52,14 +52,171 @@ function missingDependencies(declared, packages) {
|
|
|
52
52
|
const present = new Set(packages.map((entry) => entry.name));
|
|
53
53
|
return declared.filter((name) => !present.has(name));
|
|
54
54
|
}
|
|
55
|
+
function disagreements(packages, other) {
|
|
56
|
+
const otherVersions = /* @__PURE__ */ new Map();
|
|
57
|
+
for (const entry of other) {
|
|
58
|
+
if (!otherVersions.has(entry.name)) otherVersions.set(entry.name, entry.version);
|
|
59
|
+
}
|
|
60
|
+
const seen = /* @__PURE__ */ new Set();
|
|
61
|
+
const conflicts = [];
|
|
62
|
+
for (const entry of packages) {
|
|
63
|
+
if (seen.has(entry.name)) continue;
|
|
64
|
+
seen.add(entry.name);
|
|
65
|
+
const otherVersion = otherVersions.get(entry.name);
|
|
66
|
+
if (otherVersion !== void 0 && otherVersion !== entry.version) {
|
|
67
|
+
conflicts.push({ name: entry.name, version: entry.version, otherVersion });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return conflicts;
|
|
71
|
+
}
|
|
55
72
|
|
|
56
|
-
// src/parsers/
|
|
73
|
+
// src/parsers/bun.ts
|
|
57
74
|
import { readFile as readFile2 } from "fs/promises";
|
|
75
|
+
|
|
76
|
+
// src/parsers/report.ts
|
|
77
|
+
var MAX_SAMPLES = 3;
|
|
78
|
+
function newReport() {
|
|
79
|
+
return { unreadable: 0, samples: [] };
|
|
80
|
+
}
|
|
81
|
+
function recordUnreadable(report, sample) {
|
|
82
|
+
if (!report) return;
|
|
83
|
+
report.unreadable++;
|
|
84
|
+
if (report.samples.length < MAX_SAMPLES) report.samples.push(sample);
|
|
85
|
+
}
|
|
86
|
+
function unreadableWarning(filename, report) {
|
|
87
|
+
if (report.unreadable === 0) return null;
|
|
88
|
+
const shown = report.samples.join(", ");
|
|
89
|
+
const one = report.unreadable === 1;
|
|
90
|
+
return `${filename}: ${report.unreadable} entr${one ? "y was" : "ies were"} not in a form this scanner reads (${shown}${report.unreadable > report.samples.length ? ", \u2026" : ""}), so ${one ? "it is" : "they are"} absent from this manifest. Anything installed only through ${one ? "that entry" : "those entries"} is not being checked.`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/parsers/bun.ts
|
|
94
|
+
async function parseBunLockfile(lockfilePath, report) {
|
|
95
|
+
let raw;
|
|
96
|
+
try {
|
|
97
|
+
raw = await readFile2(lockfilePath, "utf8");
|
|
98
|
+
} catch (cause) {
|
|
99
|
+
throw new PatchstackError(`Could not read lockfile at ${lockfilePath}`, "LOCKFILE_NOT_FOUND", cause);
|
|
100
|
+
}
|
|
101
|
+
let parsed;
|
|
102
|
+
try {
|
|
103
|
+
parsed = JSON.parse(stripTrailingCommas(raw));
|
|
104
|
+
} catch (cause) {
|
|
105
|
+
throw new PatchstackError(`Lockfile at ${lockfilePath} is not valid JSON`, "LOCKFILE_PARSE_ERROR", cause);
|
|
106
|
+
}
|
|
107
|
+
const packages = parsed?.packages;
|
|
108
|
+
if (typeof packages !== "object" || packages === null) {
|
|
109
|
+
throw new PatchstackError(`Lockfile at ${lockfilePath} has no "packages" entries`, "LOCKFILE_PARSE_ERROR");
|
|
110
|
+
}
|
|
111
|
+
const direct = directDependencyNames(parsed);
|
|
112
|
+
const entries = [];
|
|
113
|
+
let skipped = 0;
|
|
114
|
+
for (const [key, value] of Object.entries(packages)) {
|
|
115
|
+
const descriptor = Array.isArray(value) ? value[0] : value;
|
|
116
|
+
if (typeof descriptor !== "string") {
|
|
117
|
+
skipped++;
|
|
118
|
+
recordUnreadable(report, key);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const split = splitDescriptor(descriptor);
|
|
122
|
+
if (split === null) {
|
|
123
|
+
skipped++;
|
|
124
|
+
if (!isDeliberatelyExcluded(descriptor)) recordUnreadable(report, key);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
entries.push({
|
|
128
|
+
name: split.name,
|
|
129
|
+
version: split.version,
|
|
130
|
+
...direct.has(split.name) ? { direct: true } : {},
|
|
131
|
+
...key === split.name ? {} : { path: key }
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
if (entries.length === 0) {
|
|
135
|
+
throw new PatchstackError(
|
|
136
|
+
`Lockfile at ${lockfilePath} lists no registry packages${skipped > 0 ? ` (${skipped} non-registry entr${skipped === 1 ? "y" : "ies"} skipped)` : ""}`,
|
|
137
|
+
"LOCKFILE_PARSE_ERROR"
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return entries;
|
|
141
|
+
}
|
|
142
|
+
function splitDescriptor(descriptor) {
|
|
143
|
+
const at = descriptor.lastIndexOf("@");
|
|
144
|
+
if (at <= 0) return null;
|
|
145
|
+
const name = descriptor.slice(0, at);
|
|
146
|
+
const version = descriptor.slice(at + 1);
|
|
147
|
+
if (name === "" || version === "") return null;
|
|
148
|
+
if (NON_REGISTRY_VERSION.test(version)) return null;
|
|
149
|
+
return { name, version };
|
|
150
|
+
}
|
|
151
|
+
var NON_REGISTRY_VERSION = /^[a-z][a-z0-9+.-]*:|\//i;
|
|
152
|
+
function isDeliberatelyExcluded(descriptor) {
|
|
153
|
+
const at = descriptor.lastIndexOf("@");
|
|
154
|
+
if (at <= 0) return false;
|
|
155
|
+
return NON_REGISTRY_VERSION.test(descriptor.slice(at + 1));
|
|
156
|
+
}
|
|
157
|
+
function directDependencyNames(parsed) {
|
|
158
|
+
const names = /* @__PURE__ */ new Set();
|
|
159
|
+
const workspaces = parsed?.workspaces;
|
|
160
|
+
if (typeof workspaces !== "object" || workspaces === null) return names;
|
|
161
|
+
for (const workspace of Object.values(workspaces)) {
|
|
162
|
+
if (typeof workspace !== "object" || workspace === null) continue;
|
|
163
|
+
for (const group of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) {
|
|
164
|
+
const deps = workspace[group];
|
|
165
|
+
if (typeof deps !== "object" || deps === null) continue;
|
|
166
|
+
for (const name of Object.keys(deps)) names.add(name);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return names;
|
|
170
|
+
}
|
|
171
|
+
function stripTrailingCommas(source) {
|
|
172
|
+
const out = [];
|
|
173
|
+
let inString = false;
|
|
174
|
+
let pendingComma = -1;
|
|
175
|
+
for (let i = 0; i < source.length; i++) {
|
|
176
|
+
const ch = source[i];
|
|
177
|
+
if (inString) {
|
|
178
|
+
out.push(ch);
|
|
179
|
+
if (ch === "\\") {
|
|
180
|
+
const next = source[i + 1];
|
|
181
|
+
if (next !== void 0) {
|
|
182
|
+
out.push(next);
|
|
183
|
+
i++;
|
|
184
|
+
}
|
|
185
|
+
} else if (ch === '"') {
|
|
186
|
+
inString = false;
|
|
187
|
+
}
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (ch === '"') {
|
|
191
|
+
inString = true;
|
|
192
|
+
pendingComma = -1;
|
|
193
|
+
out.push(ch);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (ch === ",") {
|
|
197
|
+
pendingComma = out.length;
|
|
198
|
+
out.push(ch);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (ch === "}" || ch === "]") {
|
|
202
|
+
if (pendingComma !== -1) out[pendingComma] = " ";
|
|
203
|
+
pendingComma = -1;
|
|
204
|
+
out.push(ch);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (!/\s/.test(ch)) pendingComma = -1;
|
|
208
|
+
out.push(ch);
|
|
209
|
+
}
|
|
210
|
+
return out.join("");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/parsers/npm.ts
|
|
214
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
58
215
|
import path2 from "path";
|
|
59
216
|
async function parseNpmLockfile(lockfilePath) {
|
|
60
217
|
let raw;
|
|
61
218
|
try {
|
|
62
|
-
raw = await
|
|
219
|
+
raw = await readFile3(lockfilePath, "utf8");
|
|
63
220
|
} catch (cause) {
|
|
64
221
|
throw new PatchstackError(`Could not read lockfile at ${lockfilePath}`, "LOCKFILE_NOT_FOUND", cause);
|
|
65
222
|
}
|
|
@@ -140,8 +297,9 @@ function isDirectV2(pkgPath) {
|
|
|
140
297
|
}
|
|
141
298
|
|
|
142
299
|
// src/parsers/node_modules.ts
|
|
143
|
-
import { lstat, readFile as
|
|
300
|
+
import { lstat, readFile as readFile4, readdir, realpath, stat } from "fs/promises";
|
|
144
301
|
import path3 from "path";
|
|
302
|
+
var PACKAGE_STORE_DIRS = /* @__PURE__ */ new Set([".bun", ".pnpm", ".store"]);
|
|
145
303
|
async function walkNodeModules(cwd) {
|
|
146
304
|
const root = path3.join(cwd, "node_modules");
|
|
147
305
|
try {
|
|
@@ -162,96 +320,122 @@ async function walkNodeModules(cwd) {
|
|
|
162
320
|
cause
|
|
163
321
|
);
|
|
164
322
|
}
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
323
|
+
const seen = /* @__PURE__ */ new Map();
|
|
324
|
+
try {
|
|
325
|
+
seen.set(await realpath(cwd), -1);
|
|
326
|
+
} catch {
|
|
327
|
+
}
|
|
328
|
+
return dedupeByShallowest(await walk(root, 0, seen));
|
|
329
|
+
}
|
|
330
|
+
function dedupeByShallowest(found) {
|
|
331
|
+
const best = /* @__PURE__ */ new Map();
|
|
332
|
+
for (const item of found) {
|
|
333
|
+
const current = best.get(item.realpath);
|
|
334
|
+
if (current === void 0 || item.depth < current.depth) best.set(item.realpath, item);
|
|
335
|
+
}
|
|
336
|
+
return [...best.values()].map(({ entry }) => entry);
|
|
168
337
|
}
|
|
169
|
-
|
|
338
|
+
var CONCURRENCY = 32;
|
|
339
|
+
async function walk(dir, depth, seen) {
|
|
170
340
|
let names;
|
|
171
341
|
try {
|
|
172
342
|
names = await readdir(dir);
|
|
173
343
|
} catch {
|
|
174
|
-
return;
|
|
344
|
+
return [];
|
|
175
345
|
}
|
|
176
|
-
|
|
177
|
-
if (name.startsWith("."))
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
346
|
+
const perEntry = await mapLimit(names, async (name) => {
|
|
347
|
+
if (name.startsWith(".") && !PACKAGE_STORE_DIRS.has(name)) return [];
|
|
180
348
|
const fullPath = path3.join(dir, name);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
349
|
+
const resolved = await claim(fullPath, seen, depth);
|
|
350
|
+
if (resolved === null) return [];
|
|
351
|
+
if (PACKAGE_STORE_DIRS.has(name)) return walk(fullPath, depth, seen);
|
|
184
352
|
if (name.startsWith("@")) {
|
|
185
353
|
let subNames;
|
|
186
354
|
try {
|
|
187
355
|
subNames = await readdir(fullPath);
|
|
188
356
|
} catch {
|
|
189
|
-
|
|
357
|
+
return [];
|
|
190
358
|
}
|
|
191
|
-
|
|
192
|
-
if (sub.startsWith("."))
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
359
|
+
const scoped = await mapLimit(subNames, async (sub) => {
|
|
360
|
+
if (sub.startsWith(".")) return [];
|
|
195
361
|
const scopedDir = path3.join(fullPath, sub);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
continue;
|
|
362
|
+
const scopedReal = await claim(scopedDir, seen, depth);
|
|
363
|
+
if (scopedReal === null) return [];
|
|
364
|
+
return packageAndNested(scopedDir, scopedReal, depth, seen);
|
|
365
|
+
});
|
|
366
|
+
return scoped.flat();
|
|
203
367
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
368
|
+
return packageAndNested(fullPath, resolved, depth, seen);
|
|
369
|
+
});
|
|
370
|
+
return perEntry.flat();
|
|
371
|
+
}
|
|
372
|
+
async function packageAndNested(pkgDir, resolved, depth, seen) {
|
|
373
|
+
const [own, nested] = await Promise.all([readPackage(pkgDir, depth), walkNested(pkgDir, depth, seen)]);
|
|
374
|
+
return own === null ? nested : [{ entry: own, realpath: resolved, depth }, ...nested];
|
|
207
375
|
}
|
|
208
|
-
async function
|
|
376
|
+
async function mapLimit(items, fn) {
|
|
377
|
+
const results = new Array(items.length);
|
|
378
|
+
let next = 0;
|
|
379
|
+
const workers = Array.from({ length: Math.min(CONCURRENCY, items.length) }, async () => {
|
|
380
|
+
while (next < items.length) {
|
|
381
|
+
const index = next++;
|
|
382
|
+
results[index] = await fn(items[index]);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
await Promise.all(workers);
|
|
386
|
+
return results;
|
|
387
|
+
}
|
|
388
|
+
async function readPackage(pkgDir, depth) {
|
|
209
389
|
let raw;
|
|
210
390
|
try {
|
|
211
|
-
raw = await
|
|
391
|
+
raw = await readFile4(path3.join(pkgDir, "package.json"), "utf8");
|
|
212
392
|
} catch {
|
|
213
|
-
return;
|
|
393
|
+
return null;
|
|
214
394
|
}
|
|
215
395
|
let parsed;
|
|
216
396
|
try {
|
|
217
397
|
parsed = JSON.parse(raw);
|
|
218
398
|
} catch {
|
|
219
|
-
return;
|
|
399
|
+
return null;
|
|
220
400
|
}
|
|
221
401
|
if (typeof parsed.name !== "string" || parsed.name.length === 0) {
|
|
222
|
-
return;
|
|
402
|
+
return null;
|
|
223
403
|
}
|
|
224
404
|
if (typeof parsed.version !== "string" || parsed.version.length === 0) {
|
|
225
|
-
return;
|
|
405
|
+
return null;
|
|
226
406
|
}
|
|
227
|
-
|
|
228
|
-
name: parsed.name,
|
|
229
|
-
version: parsed.version,
|
|
230
|
-
direct: depth === 0
|
|
231
|
-
});
|
|
407
|
+
return { name: parsed.name, version: parsed.version, direct: depth === 0 };
|
|
232
408
|
}
|
|
233
|
-
async function walkNested(pkgDir,
|
|
409
|
+
async function walkNested(pkgDir, depth, seen) {
|
|
234
410
|
const nested = path3.join(pkgDir, "node_modules");
|
|
235
|
-
if (
|
|
236
|
-
return;
|
|
411
|
+
if (await claim(nested, seen, depth + 1) === null) {
|
|
412
|
+
return [];
|
|
237
413
|
}
|
|
238
|
-
|
|
414
|
+
return walk(nested, depth + 1, seen);
|
|
239
415
|
}
|
|
240
|
-
async function
|
|
416
|
+
async function claim(dir, seen, depth) {
|
|
417
|
+
let resolved;
|
|
241
418
|
try {
|
|
242
419
|
const info = await lstat(dir);
|
|
243
|
-
|
|
420
|
+
if (!info.isDirectory() && !info.isSymbolicLink()) return null;
|
|
421
|
+
resolved = await realpath(dir);
|
|
422
|
+
const target = await stat(resolved);
|
|
423
|
+
if (!target.isDirectory()) return null;
|
|
244
424
|
} catch {
|
|
245
|
-
return
|
|
425
|
+
return null;
|
|
246
426
|
}
|
|
427
|
+
const claimed = seen.get(resolved);
|
|
428
|
+
if (claimed !== void 0 && claimed <= depth) return null;
|
|
429
|
+
seen.set(resolved, depth);
|
|
430
|
+
return resolved;
|
|
247
431
|
}
|
|
248
432
|
|
|
249
433
|
// src/parsers/pnpm.ts
|
|
250
|
-
import { readFile as
|
|
251
|
-
async function parsePnpmLockfile(lockfilePath) {
|
|
434
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
435
|
+
async function parsePnpmLockfile(lockfilePath, report) {
|
|
252
436
|
let raw;
|
|
253
437
|
try {
|
|
254
|
-
raw = await
|
|
438
|
+
raw = await readFile5(lockfilePath, "utf8");
|
|
255
439
|
} catch (cause) {
|
|
256
440
|
throw new PatchstackError(
|
|
257
441
|
`Could not read lockfile at ${lockfilePath}`,
|
|
@@ -272,6 +456,7 @@ async function parsePnpmLockfile(lockfilePath) {
|
|
|
272
456
|
for (const key of packageKeys) {
|
|
273
457
|
const parsed = parsePackageKey(key);
|
|
274
458
|
if (parsed === null) {
|
|
459
|
+
recordUnreadable(report, key);
|
|
275
460
|
continue;
|
|
276
461
|
}
|
|
277
462
|
entries.push({
|
|
@@ -499,12 +684,12 @@ function extractLeafName(trimmed) {
|
|
|
499
684
|
}
|
|
500
685
|
|
|
501
686
|
// src/parsers/yarn.ts
|
|
502
|
-
import { readFile as
|
|
687
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
503
688
|
import path4 from "path";
|
|
504
|
-
async function parseYarnLockfile(lockfilePath) {
|
|
689
|
+
async function parseYarnLockfile(lockfilePath, report) {
|
|
505
690
|
let raw;
|
|
506
691
|
try {
|
|
507
|
-
raw = await
|
|
692
|
+
raw = await readFile6(lockfilePath, "utf8");
|
|
508
693
|
} catch (cause) {
|
|
509
694
|
throw new PatchstackError(
|
|
510
695
|
`Could not read lockfile at ${lockfilePath}`,
|
|
@@ -512,7 +697,7 @@ async function parseYarnLockfile(lockfilePath) {
|
|
|
512
697
|
cause
|
|
513
698
|
);
|
|
514
699
|
}
|
|
515
|
-
const blocks = parseBlocks(raw);
|
|
700
|
+
const blocks = parseBlocks(raw, report);
|
|
516
701
|
if (blocks.length === 0) {
|
|
517
702
|
throw new PatchstackError(
|
|
518
703
|
`Lockfile at ${lockfilePath} contains no package entries`,
|
|
@@ -541,13 +726,19 @@ async function parseYarnLockfile(lockfilePath) {
|
|
|
541
726
|
}
|
|
542
727
|
return entries;
|
|
543
728
|
}
|
|
544
|
-
|
|
729
|
+
var NON_PACKAGE_KEYS = /* @__PURE__ */ new Set(["__metadata"]);
|
|
730
|
+
function parseBlocks(raw, report) {
|
|
545
731
|
const lines = raw.split(/\r?\n/);
|
|
546
732
|
const blocks = [];
|
|
547
733
|
let current = null;
|
|
548
734
|
const finalize = () => {
|
|
549
|
-
if (current
|
|
735
|
+
if (current === null) {
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
if (current.version.length > 0 && current.names.size > 0) {
|
|
550
739
|
blocks.push(current);
|
|
740
|
+
} else if (current.unreadable) {
|
|
741
|
+
recordUnreadable(report, current.key);
|
|
551
742
|
}
|
|
552
743
|
current = null;
|
|
553
744
|
};
|
|
@@ -564,13 +755,13 @@ function parseBlocks(raw) {
|
|
|
564
755
|
}
|
|
565
756
|
const keyLine = trimmed.slice(0, -1);
|
|
566
757
|
const names = /* @__PURE__ */ new Set();
|
|
758
|
+
let unreadable = false;
|
|
567
759
|
for (const spec of splitDescriptors(keyLine)) {
|
|
568
|
-
const
|
|
569
|
-
if (name !==
|
|
570
|
-
|
|
571
|
-
}
|
|
760
|
+
const classified = classifyDescriptor(spec);
|
|
761
|
+
if (classified.name !== void 0) names.add(classified.name);
|
|
762
|
+
else if (!classified.excluded) unreadable = true;
|
|
572
763
|
}
|
|
573
|
-
current = { names, version: "" };
|
|
764
|
+
current = { names, version: "", key: keyLine, unreadable: unreadable && names.size === 0 };
|
|
574
765
|
continue;
|
|
575
766
|
}
|
|
576
767
|
if (current === null) {
|
|
@@ -625,20 +816,37 @@ function splitDescriptors(keyLine) {
|
|
|
625
816
|
}
|
|
626
817
|
return parts;
|
|
627
818
|
}
|
|
628
|
-
|
|
819
|
+
var NON_REGISTRY_PROTOCOLS = /^(?:workspace|patch|virtual|portal|file|link|exec|git|github|https?|ssh):/i;
|
|
820
|
+
function classifyDescriptor(rawSpec) {
|
|
629
821
|
let s = rawSpec.trim();
|
|
630
822
|
if (s.length === 0) {
|
|
631
|
-
return
|
|
823
|
+
return { excluded: true };
|
|
632
824
|
}
|
|
633
825
|
if (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'")) {
|
|
634
826
|
s = s.slice(1, -1);
|
|
635
827
|
}
|
|
636
|
-
|
|
828
|
+
if (NON_PACKAGE_KEYS.has(s)) {
|
|
829
|
+
return { excluded: true };
|
|
830
|
+
}
|
|
831
|
+
const atIdx = s.indexOf("@", 1);
|
|
637
832
|
if (atIdx <= 0) {
|
|
638
|
-
return
|
|
833
|
+
return {};
|
|
639
834
|
}
|
|
640
835
|
const name = s.slice(0, atIdx);
|
|
641
|
-
|
|
836
|
+
const range = s.slice(atIdx + 1);
|
|
837
|
+
if (name.length === 0) {
|
|
838
|
+
return {};
|
|
839
|
+
}
|
|
840
|
+
if (NON_REGISTRY_PROTOCOLS.test(range)) {
|
|
841
|
+
return { excluded: true };
|
|
842
|
+
}
|
|
843
|
+
const alias = /^npm:(.+)$/i.exec(range);
|
|
844
|
+
if (alias !== null) {
|
|
845
|
+
const target = alias[1] ?? "";
|
|
846
|
+
const targetAt = target.indexOf("@", 1);
|
|
847
|
+
return { name: targetAt > 0 ? target.slice(0, targetAt) : name };
|
|
848
|
+
}
|
|
849
|
+
return { name };
|
|
642
850
|
}
|
|
643
851
|
function parseVersionField(content) {
|
|
644
852
|
if (!content.startsWith("version")) {
|
|
@@ -663,7 +871,7 @@ async function readDirectDepNames(cwd) {
|
|
|
663
871
|
const names = /* @__PURE__ */ new Set();
|
|
664
872
|
let raw;
|
|
665
873
|
try {
|
|
666
|
-
raw = await
|
|
874
|
+
raw = await readFile6(path4.join(cwd, "package.json"), "utf8");
|
|
667
875
|
} catch {
|
|
668
876
|
return names;
|
|
669
877
|
}
|
|
@@ -692,7 +900,10 @@ async function readDirectDepNames(cwd) {
|
|
|
692
900
|
// src/parsers/index.ts
|
|
693
901
|
var LOCKFILE_CANDIDATES = [
|
|
694
902
|
{ filename: "package-lock.json", strategy: "npm-lockfile" },
|
|
695
|
-
|
|
903
|
+
// Read directly, not walked. Bun's isolated layout puts packages under a dot-directory and makes the
|
|
904
|
+
// top level symlinks into it, so a `node_modules` walk finds nothing there — and isolated is the default
|
|
905
|
+
// for new workspaces.
|
|
906
|
+
{ filename: "bun.lock", strategy: "bun-lockfile" },
|
|
696
907
|
{ filename: "bun.lockb", strategy: "node-modules-walk" },
|
|
697
908
|
{ filename: "pnpm-lock.yaml", strategy: "pnpm-lockfile" },
|
|
698
909
|
{ filename: "yarn.lock", strategy: "yarn-lockfile" }
|
|
@@ -727,33 +938,57 @@ async function scanLockfile(cwd) {
|
|
|
727
938
|
const declared = await readDeclaredDependencyNames(cwd);
|
|
728
939
|
const warnings = [];
|
|
729
940
|
let firstParsed = null;
|
|
730
|
-
let
|
|
941
|
+
let walked = null;
|
|
942
|
+
let walkWasCandidate = false;
|
|
943
|
+
const parsedSources = [];
|
|
731
944
|
for (const candidate of candidates) {
|
|
732
945
|
let packages;
|
|
946
|
+
const report = newReport();
|
|
733
947
|
try {
|
|
734
|
-
packages = await runStrategy(candidate, cwd);
|
|
948
|
+
packages = await runStrategy(candidate, cwd, report);
|
|
735
949
|
} catch {
|
|
736
950
|
continue;
|
|
737
951
|
}
|
|
738
|
-
|
|
952
|
+
const unreadable = unreadableWarning(candidate.filename, report);
|
|
953
|
+
if (unreadable !== null) warnings.push(unreadable);
|
|
954
|
+
if (candidate.strategy === "node-modules-walk") {
|
|
955
|
+
walked = packages;
|
|
956
|
+
walkWasCandidate = true;
|
|
957
|
+
}
|
|
739
958
|
firstParsed ??= { packages, filename: candidate.filename };
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
959
|
+
parsedSources.push({ packages, filename: candidate.filename });
|
|
960
|
+
}
|
|
961
|
+
for (const source of parsedSources) {
|
|
962
|
+
const missing = missingDependencies(declared, source.packages);
|
|
963
|
+
if (missing.length > 0) {
|
|
964
|
+
warnings.push(staleWarning(source.filename, missing));
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
const conflicts = otherCompleteSources(parsedSources, source, declared).flatMap(
|
|
968
|
+
(other) => disagreements(source.packages, other.packages).map((conflict) => ({ other: other.filename, conflict }))
|
|
969
|
+
);
|
|
970
|
+
if (conflicts.length === 0) {
|
|
971
|
+
return manifestWith(source.packages, warnings);
|
|
743
972
|
}
|
|
744
|
-
warnings.push(
|
|
973
|
+
warnings.push(conflictWarning(source.filename, conflicts));
|
|
974
|
+
break;
|
|
745
975
|
}
|
|
746
|
-
if (
|
|
976
|
+
if (walked === null) {
|
|
747
977
|
try {
|
|
748
|
-
|
|
749
|
-
const missing = missingDependencies(declared, packages);
|
|
750
|
-
if (missing.length > 0) {
|
|
751
|
-
warnings.push(staleWarning("node_modules/", missing));
|
|
752
|
-
}
|
|
753
|
-
warnings.push("Scanned node_modules/ instead. Delete the stale lockfile to silence this warning.");
|
|
754
|
-
return manifestWith(packages, warnings);
|
|
978
|
+
walked = await walkNodeModules(cwd);
|
|
755
979
|
} catch {
|
|
980
|
+
walked = null;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
if (walked !== null) {
|
|
984
|
+
const missing = missingDependencies(declared, walked);
|
|
985
|
+
if (missing.length > 0) {
|
|
986
|
+
warnings.push(staleWarning("node_modules/", missing));
|
|
756
987
|
}
|
|
988
|
+
warnings.push(
|
|
989
|
+
walkWasCandidate ? "Reporting node_modules/, which is what the build loads." : "Scanned node_modules/ instead. Delete the stale lockfile to silence this warning."
|
|
990
|
+
);
|
|
991
|
+
return manifestWith(walked, warnings);
|
|
757
992
|
}
|
|
758
993
|
if (firstParsed === null) {
|
|
759
994
|
const expected = LOCKFILE_CANDIDATES.map((candidate) => candidate.filename).join(", ");
|
|
@@ -770,6 +1005,15 @@ async function scanLockfile(cwd) {
|
|
|
770
1005
|
function manifestWith(packages, warnings) {
|
|
771
1006
|
return warnings.length > 0 ? { ecosystem: "npm", packages, warnings } : { ecosystem: "npm", packages };
|
|
772
1007
|
}
|
|
1008
|
+
function otherCompleteSources(all, current, declared) {
|
|
1009
|
+
return all.filter(
|
|
1010
|
+
(source) => source.filename !== current.filename && missingDependencies(declared, source.packages).length === 0
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
function conflictWarning(filename, conflicts) {
|
|
1014
|
+
const shown = conflicts.slice(0, 3).map(({ other, conflict }) => `${conflict.name} ${conflict.version} vs ${conflict.otherVersion} in ${other}`).join("; ");
|
|
1015
|
+
return `${filename} and another lockfile disagree about installed versions (${shown}${conflicts.length > 3 ? `, +${conflicts.length - 3} more` : ""}). Scanned node_modules/ instead. Remove whichever lockfile your package manager does not maintain.`;
|
|
1016
|
+
}
|
|
773
1017
|
function staleWarning(source, missing) {
|
|
774
1018
|
const sample = missing.slice(0, 3).join(", ");
|
|
775
1019
|
const suffix = missing.length > 3 ? `, +${missing.length - 3} more` : "";
|
|
@@ -784,14 +1028,18 @@ async function presentLockfiles(cwd) {
|
|
|
784
1028
|
);
|
|
785
1029
|
return probed.filter((candidate) => candidate.present).map(({ filename, strategy, filePath }) => ({ ecosystem: "npm", filename, strategy, filePath }));
|
|
786
1030
|
}
|
|
787
|
-
async function runStrategy(detected, cwd) {
|
|
1031
|
+
async function runStrategy(detected, cwd, report) {
|
|
788
1032
|
switch (detected.strategy) {
|
|
789
1033
|
case "npm-lockfile":
|
|
790
1034
|
return parseNpmLockfile(detected.filePath);
|
|
1035
|
+
case "bun-lockfile":
|
|
1036
|
+
return parseBunLockfile(detected.filePath, report);
|
|
1037
|
+
// The hand-written scanners. Each meets entries it was not written for, and each is asked to say how
|
|
1038
|
+
// many it dropped, because a manifest short a package reads exactly like a project without it.
|
|
791
1039
|
case "pnpm-lockfile":
|
|
792
|
-
return parsePnpmLockfile(detected.filePath);
|
|
1040
|
+
return parsePnpmLockfile(detected.filePath, report);
|
|
793
1041
|
case "yarn-lockfile":
|
|
794
|
-
return parseYarnLockfile(detected.filePath);
|
|
1042
|
+
return parseYarnLockfile(detected.filePath, report);
|
|
795
1043
|
case "node-modules-walk":
|
|
796
1044
|
return walkNodeModules(cwd);
|
|
797
1045
|
}
|
|
@@ -1084,12 +1332,14 @@ function isTimeoutError(cause) {
|
|
|
1084
1332
|
}
|
|
1085
1333
|
|
|
1086
1334
|
// src/config.ts
|
|
1087
|
-
import { readFile as
|
|
1335
|
+
import { readFile as readFile7, writeFile, chmod } from "fs/promises";
|
|
1088
1336
|
import path6 from "path";
|
|
1089
1337
|
var CONFIG_FILENAME = ".patchstackrc.json";
|
|
1338
|
+
var SECRET_FILENAME = ".patchstackrc.local.json";
|
|
1090
1339
|
var DEFAULT_ENVIRONMENT = "production";
|
|
1091
1340
|
async function resolveConfig(options) {
|
|
1092
1341
|
const fromFile = await readConfigFile(options.cwd);
|
|
1342
|
+
const fromSecretFile = await readSecretFile(options.cwd);
|
|
1093
1343
|
const fromEnv = readEnv();
|
|
1094
1344
|
const siteUuid = options.cliSiteUuid ?? fromEnv.siteUuid ?? fromFile.siteUuid ?? null;
|
|
1095
1345
|
const endpoint = options.cliEndpoint ?? fromEnv.endpoint ?? fromFile.endpoint ?? DEFAULT_ENDPOINT;
|
|
@@ -1114,8 +1364,8 @@ async function resolveConfig(options) {
|
|
|
1114
1364
|
"CONFIG_MISSING"
|
|
1115
1365
|
);
|
|
1116
1366
|
}
|
|
1117
|
-
const apiKeyRaw = fromEnv.apiKey ?? fromFile.apiKey ?? null;
|
|
1118
|
-
const pulseAuthRaw = fromEnv.pulseAuth ?? fromFile.pulseAuth ?? apiKeyRaw;
|
|
1367
|
+
const apiKeyRaw = fromEnv.apiKey ?? fromSecretFile.apiKey ?? fromFile.apiKey ?? null;
|
|
1368
|
+
const pulseAuthRaw = fromEnv.pulseAuth ?? fromSecretFile.pulseAuth ?? fromFile.pulseAuth ?? apiKeyRaw;
|
|
1119
1369
|
return {
|
|
1120
1370
|
siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
|
|
1121
1371
|
apiKey: apiKeyRaw === null || apiKeyRaw.length === 0 ? null : apiKeyRaw,
|
|
@@ -1132,23 +1382,88 @@ async function writeConfigFile(cwd, config) {
|
|
|
1132
1382
|
await writeFile(target, content, "utf8");
|
|
1133
1383
|
return target;
|
|
1134
1384
|
}
|
|
1385
|
+
async function writeSecretFile(cwd, secrets) {
|
|
1386
|
+
const target = path6.join(cwd, SECRET_FILENAME);
|
|
1387
|
+
await writeFile(target, JSON.stringify(secrets, null, 2) + "\n", { encoding: "utf8", mode: 384 });
|
|
1388
|
+
try {
|
|
1389
|
+
await chmod(target, 384);
|
|
1390
|
+
} catch {
|
|
1391
|
+
}
|
|
1392
|
+
const ignore = await ensureIgnored(cwd, SECRET_FILENAME);
|
|
1393
|
+
return { path: target, ...ignore };
|
|
1394
|
+
}
|
|
1395
|
+
async function ensureIgnored(cwd, entry) {
|
|
1396
|
+
const target = path6.join(cwd, ".gitignore");
|
|
1397
|
+
let existing = "";
|
|
1398
|
+
try {
|
|
1399
|
+
existing = await readFile7(target, "utf8");
|
|
1400
|
+
} catch (err) {
|
|
1401
|
+
const code = err.code;
|
|
1402
|
+
if (code !== "ENOENT") {
|
|
1403
|
+
return { ignored: false, reason: `.gitignore could not be read (${code ?? "unknown error"})` };
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
if (ignoresEntry(existing, entry)) return { ignored: true };
|
|
1407
|
+
const separator = existing === "" || existing.endsWith("\n") ? "" : "\n";
|
|
1408
|
+
const block = `${separator}
|
|
1409
|
+
# Patchstack credential \u2014 never commit this
|
|
1410
|
+
${entry}
|
|
1411
|
+
`;
|
|
1412
|
+
try {
|
|
1413
|
+
await writeFile(target, existing + block, "utf8");
|
|
1414
|
+
} catch (err) {
|
|
1415
|
+
const code = err.code;
|
|
1416
|
+
return { ignored: false, reason: `.gitignore could not be written (${code ?? "unknown error"})` };
|
|
1417
|
+
}
|
|
1418
|
+
try {
|
|
1419
|
+
const written = await readFile7(target, "utf8");
|
|
1420
|
+
if (ignoresEntry(written, entry)) return { ignored: true };
|
|
1421
|
+
return { ignored: false, reason: ".gitignore does not cover the file after writing it" };
|
|
1422
|
+
} catch (err) {
|
|
1423
|
+
const code = err.code;
|
|
1424
|
+
return { ignored: false, reason: `.gitignore could not be read back (${code ?? "unknown error"})` };
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
function ignoresEntry(contents, entry) {
|
|
1428
|
+
let ignored = false;
|
|
1429
|
+
for (const raw of contents.split("\n")) {
|
|
1430
|
+
const line = raw.trim();
|
|
1431
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
1432
|
+
const negated = line.startsWith("!");
|
|
1433
|
+
const pattern = negated ? line.slice(1).trim() : line;
|
|
1434
|
+
if (pattern === entry || pattern === `/${entry}`) ignored = !negated;
|
|
1435
|
+
}
|
|
1436
|
+
return ignored;
|
|
1437
|
+
}
|
|
1135
1438
|
async function persistSiteUuid(cwd, siteUuid) {
|
|
1136
1439
|
const existing = await readConfigFile(cwd);
|
|
1137
1440
|
return writeConfigFile(cwd, { ...existing, siteUuid });
|
|
1138
1441
|
}
|
|
1139
1442
|
async function persistApiKey(cwd, apiKey) {
|
|
1140
|
-
const { pulseAuth:
|
|
1141
|
-
|
|
1443
|
+
const { apiKey: _movedKey, pulseAuth: _movedPulse, ...publicConfig } = await readConfigFile(cwd);
|
|
1444
|
+
const existingSecrets = await readSecretFile(cwd);
|
|
1445
|
+
const target = await writeSecretFile(cwd, { ...existingSecrets, apiKey, pulseAuth: void 0 });
|
|
1446
|
+
if (_movedKey !== void 0 || _movedPulse !== void 0) {
|
|
1447
|
+
await writeConfigFile(cwd, publicConfig);
|
|
1448
|
+
}
|
|
1449
|
+
return target;
|
|
1142
1450
|
}
|
|
1143
1451
|
async function persistPulseAuth(cwd, pulseAuth) {
|
|
1144
|
-
const existing = await
|
|
1145
|
-
return
|
|
1452
|
+
const existing = await readSecretFile(cwd);
|
|
1453
|
+
return writeSecretFile(cwd, { ...existing, pulseAuth });
|
|
1454
|
+
}
|
|
1455
|
+
async function readSecretFile(cwd) {
|
|
1456
|
+
const file = await readJsonFile(cwd, SECRET_FILENAME);
|
|
1457
|
+
return { apiKey: file.apiKey, pulseAuth: file.pulseAuth };
|
|
1146
1458
|
}
|
|
1147
1459
|
async function readConfigFile(cwd) {
|
|
1148
|
-
|
|
1460
|
+
return readJsonFile(cwd, CONFIG_FILENAME);
|
|
1461
|
+
}
|
|
1462
|
+
async function readJsonFile(cwd, filename) {
|
|
1463
|
+
const target = path6.join(cwd, filename);
|
|
1149
1464
|
let raw;
|
|
1150
1465
|
try {
|
|
1151
|
-
raw = await
|
|
1466
|
+
raw = await readFile7(target, "utf8");
|
|
1152
1467
|
} catch (err) {
|
|
1153
1468
|
if (err.code === "ENOENT") {
|
|
1154
1469
|
return {};
|