@patchstack/connect 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Patchstack
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # @patchstack/connect
2
+
3
+ Connect a JavaScript / Node.js application to [Patchstack](https://patchstack.com) for continuous vulnerability monitoring. Scans your `package-lock.json` and reports installed packages so Patchstack can match them against its vulnerability database and notify you when something needs patching.
4
+
5
+ ## Install prompt (for AI coding tools)
6
+
7
+ Copy and paste this prompt into any AI coding assistant (Cursor, v0, Bolt, Lovable, Claude Code, etc.):
8
+
9
+ > Add `@patchstack/connect` to this project as a dev dependency. Run `npx @patchstack/connect init <SITE_UUID>` once to save the site UUID, then add `"prebuild": "patchstack-connect scan"` to `package.json` `scripts` so dependencies are reported to Patchstack on every build. Replace `<SITE_UUID>` with the UUID from the Patchstack dashboard.
10
+
11
+ ## Quick start
12
+
13
+ ```bash
14
+ npm install --save-dev @patchstack/connect
15
+ npx @patchstack/connect init <your-site-uuid>
16
+ npx @patchstack/connect scan
17
+ ```
18
+
19
+ Get your site UUID from the Patchstack dashboard: create a new site with type **Application**, then copy the UUID shown on the site's settings page.
20
+
21
+ ## CLI
22
+
23
+ ```
24
+ patchstack-connect init <site-uuid> Save the site UUID to .patchstackrc.json
25
+ patchstack-connect scan [options] Scan the lockfile and POST to Patchstack
26
+ patchstack-connect status [options] Show current configuration
27
+ patchstack-connect help Print help
28
+
29
+ Options (for scan and status):
30
+ --site-uuid <uuid> Override the configured site UUID
31
+ --endpoint <url> Override the API endpoint
32
+ --dry-run (scan only) Print the payload without posting
33
+ ```
34
+
35
+ ## Configuration
36
+
37
+ Precedence (highest wins):
38
+
39
+ 1. CLI flag (`--site-uuid`, `--endpoint`)
40
+ 2. Environment variable
41
+ 3. `.patchstackrc.json` in the current directory
42
+
43
+ Environment variables:
44
+
45
+ - `PATCHSTACK_SITE_UUID` — the site UUID from your Patchstack dashboard
46
+ - `PATCHSTACK_ENDPOINT` — override the API endpoint (default `https://app.patchstack.com/monitor/pulse/manifest`)
47
+ - `PATCHSTACK_TIMEOUT_MS` — request timeout in milliseconds (default `30000`)
48
+
49
+ `.patchstackrc.json` example:
50
+
51
+ ```json
52
+ {
53
+ "siteUuid": "550e8400-e29b-41d4-a716-446655440000"
54
+ }
55
+ ```
56
+
57
+ The site UUID is the only credential. Possession of it grants the right to submit manifests for that site, so treat it like an API token: keep it out of public repos, and prefer the environment variable in CI.
58
+
59
+ ## Programmatic API
60
+
61
+ ```ts
62
+ import { scanAndReport } from '@patchstack/connect';
63
+
64
+ const result = await scanAndReport();
65
+ console.log(result.response.stored ? 'Reported' : 'Unchanged');
66
+ ```
67
+
68
+ Lower-level pieces are also exported: `scanLockfile`, `buildWirePayload`, `postManifest`, `resolveConfig`.
69
+
70
+ ## What gets sent
71
+
72
+ ```json
73
+ {
74
+ "ecosystem": "npm",
75
+ "packages": [
76
+ { "name": "axios", "version": "1.6.0" },
77
+ { "name": "lodash", "version": "4.17.15" },
78
+ { "name": "lodash", "version": "4.17.21" }
79
+ ]
80
+ }
81
+ ```
82
+
83
+ That's the entire payload. No source code, no environment variables, no file paths — just the package names and versions from your lockfile. Duplicate names with different versions are preserved so transitive vulnerabilities aren't missed.
84
+
85
+ ## Supported lockfiles
86
+
87
+ - ✅ `package-lock.json` (npm v6 / v2 / v3)
88
+ - ❌ `yarn.lock` — coming soon
89
+ - ❌ `pnpm-lock.yaml` — coming soon
90
+
91
+ ## Development
92
+
93
+ ```bash
94
+ npm install
95
+ npm run typecheck
96
+ npm test
97
+ npm run build
98
+ ```
99
+
100
+ ## License
101
+
102
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,561 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/parsers/index.ts
4
+ import { access } from "fs/promises";
5
+ import path2 from "path";
6
+
7
+ // src/types.ts
8
+ var PatchstackError = class extends Error {
9
+ constructor(message, code, cause) {
10
+ super(message);
11
+ this.code = code;
12
+ this.cause = cause;
13
+ this.name = "PatchstackError";
14
+ }
15
+ code;
16
+ cause;
17
+ };
18
+
19
+ // src/parsers/npm.ts
20
+ import { readFile } from "fs/promises";
21
+ import path from "path";
22
+ async function parseNpmLockfile(lockfilePath) {
23
+ let raw;
24
+ try {
25
+ raw = await readFile(lockfilePath, "utf8");
26
+ } catch (cause) {
27
+ throw new PatchstackError(`Could not read lockfile at ${lockfilePath}`, "LOCKFILE_NOT_FOUND", cause);
28
+ }
29
+ let parsed;
30
+ try {
31
+ parsed = JSON.parse(raw);
32
+ } catch (cause) {
33
+ throw new PatchstackError(`Lockfile at ${lockfilePath} is not valid JSON`, "LOCKFILE_PARSE_ERROR", cause);
34
+ }
35
+ if (parsed.packages) {
36
+ return extractFromV2(parsed.packages);
37
+ }
38
+ if (parsed.dependencies) {
39
+ return extractFromV1(parsed.dependencies);
40
+ }
41
+ throw new PatchstackError(
42
+ `Lockfile at ${lockfilePath} has no "packages" or "dependencies" key`,
43
+ "LOCKFILE_PARSE_ERROR"
44
+ );
45
+ }
46
+ function extractFromV2(packages) {
47
+ const entries = [];
48
+ for (const [pkgPath, pkg] of Object.entries(packages)) {
49
+ if (pkgPath === "") {
50
+ continue;
51
+ }
52
+ if (pkg.link === true) {
53
+ continue;
54
+ }
55
+ if (typeof pkg.version !== "string" || pkg.version.length === 0) {
56
+ continue;
57
+ }
58
+ const name = pkg.name ?? extractNameFromPath(pkgPath);
59
+ if (name === null) {
60
+ continue;
61
+ }
62
+ entries.push({
63
+ name,
64
+ version: pkg.version,
65
+ path: pkgPath,
66
+ direct: isDirectV2(pkgPath)
67
+ });
68
+ }
69
+ return entries;
70
+ }
71
+ function extractFromV1(deps, acc = [], depth = 0) {
72
+ for (const [name, dep] of Object.entries(deps)) {
73
+ if (typeof dep.version === "string" && dep.version.length > 0) {
74
+ acc.push({ name, version: dep.version, direct: depth === 0 });
75
+ }
76
+ if (dep.dependencies) {
77
+ extractFromV1(dep.dependencies, acc, depth + 1);
78
+ }
79
+ }
80
+ return acc;
81
+ }
82
+ function extractNameFromPath(pkgPath) {
83
+ const segments = pkgPath.split("node_modules" + path.sep === pkgPath ? path.sep : "/");
84
+ const parts = pkgPath.split("/");
85
+ const nmIndex = parts.lastIndexOf("node_modules");
86
+ if (nmIndex === -1 || nmIndex >= parts.length - 1) {
87
+ return segments[segments.length - 1] ?? null;
88
+ }
89
+ const tail = parts.slice(nmIndex + 1);
90
+ if (tail.length === 0) {
91
+ return null;
92
+ }
93
+ const first = tail[0];
94
+ if (first !== void 0 && first.startsWith("@") && tail.length >= 2) {
95
+ return `${first}/${tail[1]}`;
96
+ }
97
+ return first ?? null;
98
+ }
99
+ function isDirectV2(pkgPath) {
100
+ const parts = pkgPath.split("/");
101
+ const nmCount = parts.filter((p) => p === "node_modules").length;
102
+ return nmCount === 1;
103
+ }
104
+
105
+ // src/parsers/index.ts
106
+ async function detectLockfile(cwd) {
107
+ const npmLock = path2.join(cwd, "package-lock.json");
108
+ if (await exists(npmLock)) {
109
+ return { ecosystem: "npm", filePath: npmLock, filename: "package-lock.json" };
110
+ }
111
+ const yarnLock = path2.join(cwd, "yarn.lock");
112
+ if (await exists(yarnLock)) {
113
+ throw new PatchstackError(
114
+ "yarn.lock detected but not yet supported. Run `npm install` to generate a package-lock.json, or open an issue at github.com/patchstack/connect.",
115
+ "LOCKFILE_UNSUPPORTED"
116
+ );
117
+ }
118
+ const pnpmLock = path2.join(cwd, "pnpm-lock.yaml");
119
+ if (await exists(pnpmLock)) {
120
+ throw new PatchstackError(
121
+ "pnpm-lock.yaml detected but not yet supported. Open an issue at github.com/patchstack/connect to request support.",
122
+ "LOCKFILE_UNSUPPORTED"
123
+ );
124
+ }
125
+ throw new PatchstackError(
126
+ `No lockfile found in ${cwd}. Expected one of: package-lock.json, yarn.lock, pnpm-lock.yaml.`,
127
+ "LOCKFILE_NOT_FOUND"
128
+ );
129
+ }
130
+ async function scanLockfile(cwd) {
131
+ const detected = await detectLockfile(cwd);
132
+ const packages = await parseNpmLockfile(detected.filePath);
133
+ return { ecosystem: detected.ecosystem, packages };
134
+ }
135
+ async function exists(filePath) {
136
+ try {
137
+ await access(filePath);
138
+ return true;
139
+ } catch {
140
+ return false;
141
+ }
142
+ }
143
+
144
+ // src/normalize.ts
145
+ function buildWirePayload(manifest) {
146
+ const seen = /* @__PURE__ */ new Map();
147
+ const wirePackages = [];
148
+ for (const entry of manifest.packages) {
149
+ const versions = seen.get(entry.name);
150
+ if (versions) {
151
+ if (versions.has(entry.version)) {
152
+ continue;
153
+ }
154
+ versions.add(entry.version);
155
+ } else {
156
+ seen.set(entry.name, /* @__PURE__ */ new Set([entry.version]));
157
+ }
158
+ wirePackages.push({ name: entry.name, version: entry.version });
159
+ }
160
+ wirePackages.sort((a, b) => {
161
+ if (a.name === b.name) {
162
+ return compareVersions(a.version, b.version);
163
+ }
164
+ return a.name < b.name ? -1 : 1;
165
+ });
166
+ const duplicateNames = [];
167
+ for (const [name, versions] of seen) {
168
+ if (versions.size > 1) {
169
+ duplicateNames.push(name);
170
+ }
171
+ }
172
+ return {
173
+ payload: { ecosystem: manifest.ecosystem, packages: wirePackages },
174
+ stats: {
175
+ uniqueNames: seen.size,
176
+ duplicateNames,
177
+ totalEntries: manifest.packages.length
178
+ }
179
+ };
180
+ }
181
+ function compareVersions(a, b) {
182
+ if (a === b) {
183
+ return 0;
184
+ }
185
+ const [aBase, aPre] = splitPrerelease(a);
186
+ const [bBase, bPre] = splitPrerelease(b);
187
+ const baseCmp = compareSegments(aBase.split("."), bBase.split("."));
188
+ if (baseCmp !== 0) {
189
+ return baseCmp;
190
+ }
191
+ if (aPre === null && bPre === null) {
192
+ return 0;
193
+ }
194
+ if (aPre === null) {
195
+ return 1;
196
+ }
197
+ if (bPre === null) {
198
+ return -1;
199
+ }
200
+ return compareSegments(aPre.split("."), bPre.split("."));
201
+ }
202
+ function splitPrerelease(version) {
203
+ const cleaned = version.replace(/^[v=]+/, "").split("+")[0];
204
+ const dashIndex = cleaned.indexOf("-");
205
+ if (dashIndex === -1) {
206
+ return [cleaned, null];
207
+ }
208
+ return [cleaned.slice(0, dashIndex), cleaned.slice(dashIndex + 1)];
209
+ }
210
+ function compareSegments(a, b) {
211
+ const max = Math.max(a.length, b.length);
212
+ for (let i = 0; i < max; i++) {
213
+ const aPart = a[i];
214
+ const bPart = b[i];
215
+ if (aPart === void 0) {
216
+ return -1;
217
+ }
218
+ if (bPart === void 0) {
219
+ return 1;
220
+ }
221
+ const aNum = /^\d+$/.test(aPart);
222
+ const bNum = /^\d+$/.test(bPart);
223
+ if (aNum && bNum) {
224
+ const diff = Number(aPart) - Number(bPart);
225
+ if (diff !== 0) {
226
+ return diff < 0 ? -1 : 1;
227
+ }
228
+ continue;
229
+ }
230
+ if (aNum) {
231
+ return -1;
232
+ }
233
+ if (bNum) {
234
+ return 1;
235
+ }
236
+ if (aPart < bPart) {
237
+ return -1;
238
+ }
239
+ if (aPart > bPart) {
240
+ return 1;
241
+ }
242
+ }
243
+ return 0;
244
+ }
245
+
246
+ // src/client.ts
247
+ var DEFAULT_ENDPOINT = "https://app.patchstack.com/monitor/pulse/manifest";
248
+ var DEFAULT_TIMEOUT_MS = 3e4;
249
+ function buildEndpointUrl(base, siteUuid) {
250
+ const trimmed = base.replace(/\/$/, "");
251
+ return `${trimmed}/${encodeURIComponent(siteUuid)}`;
252
+ }
253
+ async function postManifest(config, payload) {
254
+ const url = buildEndpointUrl(config.endpoint, config.siteUuid);
255
+ const timeoutMs = config.timeoutMs;
256
+ let response;
257
+ try {
258
+ response = await fetch(url, {
259
+ method: "POST",
260
+ headers: {
261
+ "Content-Type": "application/json",
262
+ Accept: "application/json",
263
+ "User-Agent": "@patchstack/connect"
264
+ },
265
+ body: JSON.stringify(payload),
266
+ signal: AbortSignal.timeout(timeoutMs)
267
+ });
268
+ } catch (cause) {
269
+ if (isTimeoutError(cause)) {
270
+ throw new PatchstackError(
271
+ `Patchstack request to ${url} timed out after ${timeoutMs}ms. Override with PATCHSTACK_TIMEOUT_MS.`,
272
+ "NETWORK_TIMEOUT",
273
+ cause
274
+ );
275
+ }
276
+ throw new PatchstackError(
277
+ `Could not reach Patchstack at ${url}. Check your network connection.`,
278
+ "NETWORK_ERROR",
279
+ cause
280
+ );
281
+ }
282
+ const text = await response.text();
283
+ let body = null;
284
+ try {
285
+ body = text.length > 0 ? JSON.parse(text) : null;
286
+ } catch {
287
+ body = null;
288
+ }
289
+ if (response.status === 404) {
290
+ throw new PatchstackError(
291
+ body?.error ?? "Site not found. Check that your site UUID is correct and that the app is registered as a Pulse app in your Patchstack dashboard.",
292
+ "SITE_NOT_FOUND"
293
+ );
294
+ }
295
+ if (response.status === 422) {
296
+ throw new PatchstackError(
297
+ body?.message ?? "Patchstack rejected the manifest payload (validation failed).",
298
+ "VALIDATION_ERROR"
299
+ );
300
+ }
301
+ if (response.status < 200 || response.status >= 300) {
302
+ throw new PatchstackError(
303
+ `Patchstack returned ${response.status}: ${text.slice(0, 200)}`,
304
+ "SERVER_ERROR"
305
+ );
306
+ }
307
+ if (body === null) {
308
+ throw new PatchstackError("Patchstack returned an empty response.", "SERVER_ERROR");
309
+ }
310
+ return body;
311
+ }
312
+ function isTimeoutError(cause) {
313
+ if (cause instanceof Error) {
314
+ return cause.name === "TimeoutError" || cause.name === "AbortError";
315
+ }
316
+ return false;
317
+ }
318
+
319
+ // src/config.ts
320
+ import { readFile as readFile2, writeFile } from "fs/promises";
321
+ import path3 from "path";
322
+ var CONFIG_FILENAME = ".patchstackrc.json";
323
+ async function resolveConfig(options) {
324
+ const fromFile = await readConfigFile(options.cwd);
325
+ const fromEnv = readEnv();
326
+ const siteUuid = options.cliSiteUuid ?? fromEnv.siteUuid ?? fromFile.siteUuid ?? null;
327
+ const endpoint = options.cliEndpoint ?? fromEnv.endpoint ?? fromFile.endpoint ?? DEFAULT_ENDPOINT;
328
+ const timeoutMs = fromEnv.timeoutMs ?? fromFile.timeoutMs ?? DEFAULT_TIMEOUT_MS;
329
+ if (siteUuid === null || siteUuid.length === 0) {
330
+ throw new PatchstackError(
331
+ "No site UUID configured. Run `patchstack-connect init <site-uuid>` or set PATCHSTACK_SITE_UUID.",
332
+ "CONFIG_MISSING"
333
+ );
334
+ }
335
+ if (!isUuid(siteUuid)) {
336
+ throw new PatchstackError(
337
+ `Site UUID "${siteUuid}" does not look like a valid UUID.`,
338
+ "CONFIG_INVALID"
339
+ );
340
+ }
341
+ return { siteUuid, endpoint, timeoutMs };
342
+ }
343
+ async function writeConfigFile(cwd, config) {
344
+ const target = path3.join(cwd, CONFIG_FILENAME);
345
+ const content = JSON.stringify(config, null, 2) + "\n";
346
+ await writeFile(target, content, "utf8");
347
+ return target;
348
+ }
349
+ async function readConfigFile(cwd) {
350
+ const target = path3.join(cwd, CONFIG_FILENAME);
351
+ let raw;
352
+ try {
353
+ raw = await readFile2(target, "utf8");
354
+ } catch (err) {
355
+ if (err.code === "ENOENT") {
356
+ return {};
357
+ }
358
+ throw new PatchstackError(
359
+ `Could not read ${target}: ${err.message}`,
360
+ "CONFIG_INVALID",
361
+ err
362
+ );
363
+ }
364
+ try {
365
+ return JSON.parse(raw);
366
+ } catch (err) {
367
+ throw new PatchstackError(
368
+ `Config file ${target} contains invalid JSON.`,
369
+ "CONFIG_INVALID",
370
+ err
371
+ );
372
+ }
373
+ }
374
+ function readEnv() {
375
+ const timeoutRaw = process.env.PATCHSTACK_TIMEOUT_MS;
376
+ let timeoutMs;
377
+ if (timeoutRaw !== void 0 && timeoutRaw.length > 0) {
378
+ const parsed = Number(timeoutRaw);
379
+ if (!Number.isFinite(parsed) || parsed <= 0) {
380
+ throw new PatchstackError(
381
+ `PATCHSTACK_TIMEOUT_MS must be a positive number; got "${timeoutRaw}".`,
382
+ "CONFIG_INVALID"
383
+ );
384
+ }
385
+ timeoutMs = parsed;
386
+ }
387
+ return {
388
+ siteUuid: process.env.PATCHSTACK_SITE_UUID ?? void 0,
389
+ endpoint: process.env.PATCHSTACK_ENDPOINT ?? void 0,
390
+ timeoutMs
391
+ };
392
+ }
393
+ function isUuid(value) {
394
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
395
+ }
396
+
397
+ // src/cli.ts
398
+ var HELP = `@patchstack/connect \u2014 scan your lockfile and report packages to Patchstack.
399
+
400
+ Usage:
401
+ patchstack-connect init <site-uuid> Save the site UUID to .patchstackrc.json
402
+ patchstack-connect scan [options] Scan lockfile and POST to Patchstack
403
+ patchstack-connect status [options] Show current configuration
404
+ patchstack-connect help Print this message
405
+
406
+ Options (for scan and status):
407
+ --site-uuid <uuid> Override the configured site UUID
408
+ --endpoint <url> Override the API endpoint
409
+ --dry-run (scan only) Show the payload without posting
410
+
411
+ Environment:
412
+ PATCHSTACK_SITE_UUID Site UUID
413
+ PATCHSTACK_ENDPOINT API endpoint (default: https://app.patchstack.com/monitor/pulse/manifest)
414
+ PATCHSTACK_TIMEOUT_MS Request timeout in ms (default: 30000)
415
+
416
+ Precedence: CLI flag > environment variable > .patchstackrc.json.
417
+
418
+ Examples:
419
+ npx @patchstack/connect init 550e8400-e29b-41d4-a716-446655440000
420
+ npx @patchstack/connect scan
421
+ npx @patchstack/connect scan --dry-run
422
+ npx @patchstack/connect scan --site-uuid 550e8400-...-446655440000
423
+ `;
424
+ var VALUE_FLAGS = /* @__PURE__ */ new Set(["site-uuid", "endpoint"]);
425
+ function parseArgs(argv) {
426
+ const args = argv.slice(2);
427
+ const positional = [];
428
+ const flags = /* @__PURE__ */ new Map();
429
+ for (let i = 0; i < args.length; i++) {
430
+ const arg = args[i];
431
+ if (!arg.startsWith("--")) {
432
+ positional.push(arg);
433
+ continue;
434
+ }
435
+ const stripped = arg.slice(2);
436
+ const eqIdx = stripped.indexOf("=");
437
+ if (eqIdx !== -1) {
438
+ flags.set(stripped.slice(0, eqIdx), stripped.slice(eqIdx + 1));
439
+ continue;
440
+ }
441
+ const next = args[i + 1];
442
+ if (VALUE_FLAGS.has(stripped) && next !== void 0 && !next.startsWith("--")) {
443
+ flags.set(stripped, next);
444
+ i++;
445
+ } else {
446
+ flags.set(stripped, true);
447
+ }
448
+ }
449
+ return {
450
+ command: positional.shift() ?? null,
451
+ positional,
452
+ flags
453
+ };
454
+ }
455
+ function getStringFlag(flags, name) {
456
+ const value = flags.get(name);
457
+ return typeof value === "string" ? value : void 0;
458
+ }
459
+ async function runInit(args) {
460
+ const uuid = args.positional[0];
461
+ if (!uuid) {
462
+ console.error("Error: site UUID is required.\n");
463
+ console.error("Usage: patchstack-connect init <site-uuid>");
464
+ return 1;
465
+ }
466
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(uuid)) {
467
+ console.error(`Error: "${uuid}" does not look like a valid UUID.`);
468
+ return 1;
469
+ }
470
+ const target = await writeConfigFile(process.cwd(), { siteUuid: uuid });
471
+ console.log(`Wrote ${target}`);
472
+ console.log("");
473
+ console.log("Next: run `npx @patchstack/connect scan` to send your first manifest.");
474
+ return 0;
475
+ }
476
+ async function runScan(args) {
477
+ const dryRun = args.flags.get("dry-run") === true;
478
+ const config = await resolveConfig({
479
+ cwd: process.cwd(),
480
+ cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
481
+ cliEndpoint: getStringFlag(args.flags, "endpoint")
482
+ });
483
+ const manifest = await scanLockfile(process.cwd());
484
+ const { payload, stats } = buildWirePayload(manifest);
485
+ console.log(
486
+ `Found ${payload.packages.length} unique package versions across ${stats.uniqueNames} package names in ${manifest.ecosystem} lockfile.`
487
+ );
488
+ if (stats.duplicateNames.length > 0) {
489
+ console.log(`${stats.duplicateNames.length} package(s) appear at multiple versions:`);
490
+ if (stats.duplicateNames.length <= 10) {
491
+ console.log(` ${stats.duplicateNames.join(", ")}`);
492
+ }
493
+ }
494
+ if (dryRun) {
495
+ console.log("");
496
+ console.log("--dry-run: not posting to Patchstack. Payload preview:");
497
+ const preview = JSON.stringify(payload, null, 2).split("\n");
498
+ console.log(preview.slice(0, Math.min(preview.length, 30)).join("\n"));
499
+ if (preview.length > 30) {
500
+ console.log(` ... (${preview.length - 30} more lines)`);
501
+ }
502
+ return 0;
503
+ }
504
+ const response = await postManifest(config, payload);
505
+ if (response.stored) {
506
+ console.log(`Stored manifest #${response.manifest_id} (checksum ${response.checksum}).`);
507
+ } else if (response.reason === "duplicate") {
508
+ console.log("Manifest unchanged since last scan \u2014 nothing to store.");
509
+ } else {
510
+ console.log(`Server response: ${response.message ?? JSON.stringify(response)}`);
511
+ }
512
+ return 0;
513
+ }
514
+ async function runStatus(args) {
515
+ try {
516
+ const config = await resolveConfig({
517
+ cwd: process.cwd(),
518
+ cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
519
+ cliEndpoint: getStringFlag(args.flags, "endpoint")
520
+ });
521
+ console.log(`Site UUID: ${config.siteUuid}`);
522
+ console.log(`Endpoint: ${config.endpoint}`);
523
+ console.log(`Timeout: ${config.timeoutMs}ms`);
524
+ return 0;
525
+ } catch (err) {
526
+ if (err instanceof PatchstackError && err.code === "CONFIG_MISSING") {
527
+ console.log("Not configured. Run `patchstack-connect init <site-uuid>` to get started.");
528
+ return 0;
529
+ }
530
+ throw err;
531
+ }
532
+ }
533
+ async function main() {
534
+ const args = parseArgs(process.argv);
535
+ if (args.flags.has("help") || args.command === "help" || args.command === null) {
536
+ console.log(HELP);
537
+ return 0;
538
+ }
539
+ switch (args.command) {
540
+ case "init":
541
+ return runInit(args);
542
+ case "scan":
543
+ return runScan(args);
544
+ case "status":
545
+ return runStatus(args);
546
+ default:
547
+ console.error(`Unknown command: ${args.command}
548
+ `);
549
+ console.error(HELP);
550
+ return 1;
551
+ }
552
+ }
553
+ main().then((code) => process.exit(code)).catch((err) => {
554
+ if (err instanceof PatchstackError) {
555
+ console.error(`Error (${err.code}): ${err.message}`);
556
+ process.exit(1);
557
+ }
558
+ console.error("Unexpected error:", err);
559
+ process.exit(2);
560
+ });
561
+ //# sourceMappingURL=cli.js.map