@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 +21 -0
- package/README.md +102 -0
- package/dist/cli.js +561 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +470 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +89 -0
- package/dist/index.d.ts +89 -0
- package/dist/index.js +423 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
type Ecosystem = 'npm' | 'composer';
|
|
2
|
+
interface PackageEntry {
|
|
3
|
+
name: string;
|
|
4
|
+
version: string;
|
|
5
|
+
path?: string;
|
|
6
|
+
direct?: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface Manifest {
|
|
9
|
+
ecosystem: Ecosystem;
|
|
10
|
+
packages: PackageEntry[];
|
|
11
|
+
}
|
|
12
|
+
interface Config {
|
|
13
|
+
siteUuid: string;
|
|
14
|
+
endpoint: string;
|
|
15
|
+
timeoutMs: number;
|
|
16
|
+
}
|
|
17
|
+
interface StoreManifestResponse {
|
|
18
|
+
stored: boolean;
|
|
19
|
+
manifest_id?: number;
|
|
20
|
+
checksum?: string;
|
|
21
|
+
reason?: string;
|
|
22
|
+
message?: string;
|
|
23
|
+
error?: string;
|
|
24
|
+
}
|
|
25
|
+
declare class PatchstackError extends Error {
|
|
26
|
+
readonly code: 'CONFIG_MISSING' | 'CONFIG_INVALID' | 'LOCKFILE_NOT_FOUND' | 'LOCKFILE_UNSUPPORTED' | 'LOCKFILE_PARSE_ERROR' | 'NETWORK_ERROR' | 'NETWORK_TIMEOUT' | 'SITE_NOT_FOUND' | 'VALIDATION_ERROR' | 'SERVER_ERROR';
|
|
27
|
+
readonly cause?: unknown | undefined;
|
|
28
|
+
constructor(message: string, code: 'CONFIG_MISSING' | 'CONFIG_INVALID' | 'LOCKFILE_NOT_FOUND' | 'LOCKFILE_UNSUPPORTED' | 'LOCKFILE_PARSE_ERROR' | 'NETWORK_ERROR' | 'NETWORK_TIMEOUT' | 'SITE_NOT_FOUND' | 'VALIDATION_ERROR' | 'SERVER_ERROR', cause?: unknown | undefined);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface DetectedLockfile {
|
|
32
|
+
ecosystem: 'npm';
|
|
33
|
+
filePath: string;
|
|
34
|
+
filename: 'package-lock.json' | 'yarn.lock' | 'pnpm-lock.yaml';
|
|
35
|
+
}
|
|
36
|
+
declare function detectLockfile(cwd: string): Promise<DetectedLockfile>;
|
|
37
|
+
declare function scanLockfile(cwd: string): Promise<Manifest>;
|
|
38
|
+
|
|
39
|
+
interface WirePackage {
|
|
40
|
+
name: string;
|
|
41
|
+
version: string;
|
|
42
|
+
}
|
|
43
|
+
interface WirePayload {
|
|
44
|
+
ecosystem: Manifest['ecosystem'];
|
|
45
|
+
packages: WirePackage[];
|
|
46
|
+
}
|
|
47
|
+
interface NormalizeStats {
|
|
48
|
+
uniqueNames: number;
|
|
49
|
+
duplicateNames: string[];
|
|
50
|
+
totalEntries: number;
|
|
51
|
+
}
|
|
52
|
+
interface NormalizeResult {
|
|
53
|
+
payload: WirePayload;
|
|
54
|
+
stats: NormalizeStats;
|
|
55
|
+
}
|
|
56
|
+
declare function buildWirePayload(manifest: Manifest): NormalizeResult;
|
|
57
|
+
declare function compareVersions(a: string, b: string): number;
|
|
58
|
+
|
|
59
|
+
declare const DEFAULT_ENDPOINT = "https://app.patchstack.com/monitor/pulse/manifest";
|
|
60
|
+
declare function buildEndpointUrl(base: string, siteUuid: string): string;
|
|
61
|
+
declare function postManifest(config: Config, payload: WirePayload): Promise<StoreManifestResponse>;
|
|
62
|
+
|
|
63
|
+
interface ConfigFile {
|
|
64
|
+
siteUuid?: string;
|
|
65
|
+
endpoint?: string;
|
|
66
|
+
timeoutMs?: number;
|
|
67
|
+
}
|
|
68
|
+
interface ResolveConfigOptions {
|
|
69
|
+
cwd: string;
|
|
70
|
+
cliSiteUuid?: string;
|
|
71
|
+
cliEndpoint?: string;
|
|
72
|
+
}
|
|
73
|
+
declare function resolveConfig(options: ResolveConfigOptions): Promise<Config>;
|
|
74
|
+
declare function writeConfigFile(cwd: string, config: ConfigFile): Promise<string>;
|
|
75
|
+
|
|
76
|
+
interface ScanAndReportOptions {
|
|
77
|
+
cwd?: string;
|
|
78
|
+
config?: Config;
|
|
79
|
+
}
|
|
80
|
+
interface ScanAndReportResult {
|
|
81
|
+
manifest: Manifest;
|
|
82
|
+
response: StoreManifestResponse;
|
|
83
|
+
duplicateNames: string[];
|
|
84
|
+
uniqueNames: number;
|
|
85
|
+
totalEntries: number;
|
|
86
|
+
}
|
|
87
|
+
declare function scanAndReport(options?: ScanAndReportOptions): Promise<ScanAndReportResult>;
|
|
88
|
+
|
|
89
|
+
export { type Config, DEFAULT_ENDPOINT, type Ecosystem, type Manifest, type PackageEntry, PatchstackError, type ScanAndReportOptions, type ScanAndReportResult, type StoreManifestResponse, buildEndpointUrl, buildWirePayload, compareVersions, detectLockfile, postManifest, resolveConfig, scanAndReport, scanLockfile, writeConfigFile };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
type Ecosystem = 'npm' | 'composer';
|
|
2
|
+
interface PackageEntry {
|
|
3
|
+
name: string;
|
|
4
|
+
version: string;
|
|
5
|
+
path?: string;
|
|
6
|
+
direct?: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface Manifest {
|
|
9
|
+
ecosystem: Ecosystem;
|
|
10
|
+
packages: PackageEntry[];
|
|
11
|
+
}
|
|
12
|
+
interface Config {
|
|
13
|
+
siteUuid: string;
|
|
14
|
+
endpoint: string;
|
|
15
|
+
timeoutMs: number;
|
|
16
|
+
}
|
|
17
|
+
interface StoreManifestResponse {
|
|
18
|
+
stored: boolean;
|
|
19
|
+
manifest_id?: number;
|
|
20
|
+
checksum?: string;
|
|
21
|
+
reason?: string;
|
|
22
|
+
message?: string;
|
|
23
|
+
error?: string;
|
|
24
|
+
}
|
|
25
|
+
declare class PatchstackError extends Error {
|
|
26
|
+
readonly code: 'CONFIG_MISSING' | 'CONFIG_INVALID' | 'LOCKFILE_NOT_FOUND' | 'LOCKFILE_UNSUPPORTED' | 'LOCKFILE_PARSE_ERROR' | 'NETWORK_ERROR' | 'NETWORK_TIMEOUT' | 'SITE_NOT_FOUND' | 'VALIDATION_ERROR' | 'SERVER_ERROR';
|
|
27
|
+
readonly cause?: unknown | undefined;
|
|
28
|
+
constructor(message: string, code: 'CONFIG_MISSING' | 'CONFIG_INVALID' | 'LOCKFILE_NOT_FOUND' | 'LOCKFILE_UNSUPPORTED' | 'LOCKFILE_PARSE_ERROR' | 'NETWORK_ERROR' | 'NETWORK_TIMEOUT' | 'SITE_NOT_FOUND' | 'VALIDATION_ERROR' | 'SERVER_ERROR', cause?: unknown | undefined);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface DetectedLockfile {
|
|
32
|
+
ecosystem: 'npm';
|
|
33
|
+
filePath: string;
|
|
34
|
+
filename: 'package-lock.json' | 'yarn.lock' | 'pnpm-lock.yaml';
|
|
35
|
+
}
|
|
36
|
+
declare function detectLockfile(cwd: string): Promise<DetectedLockfile>;
|
|
37
|
+
declare function scanLockfile(cwd: string): Promise<Manifest>;
|
|
38
|
+
|
|
39
|
+
interface WirePackage {
|
|
40
|
+
name: string;
|
|
41
|
+
version: string;
|
|
42
|
+
}
|
|
43
|
+
interface WirePayload {
|
|
44
|
+
ecosystem: Manifest['ecosystem'];
|
|
45
|
+
packages: WirePackage[];
|
|
46
|
+
}
|
|
47
|
+
interface NormalizeStats {
|
|
48
|
+
uniqueNames: number;
|
|
49
|
+
duplicateNames: string[];
|
|
50
|
+
totalEntries: number;
|
|
51
|
+
}
|
|
52
|
+
interface NormalizeResult {
|
|
53
|
+
payload: WirePayload;
|
|
54
|
+
stats: NormalizeStats;
|
|
55
|
+
}
|
|
56
|
+
declare function buildWirePayload(manifest: Manifest): NormalizeResult;
|
|
57
|
+
declare function compareVersions(a: string, b: string): number;
|
|
58
|
+
|
|
59
|
+
declare const DEFAULT_ENDPOINT = "https://app.patchstack.com/monitor/pulse/manifest";
|
|
60
|
+
declare function buildEndpointUrl(base: string, siteUuid: string): string;
|
|
61
|
+
declare function postManifest(config: Config, payload: WirePayload): Promise<StoreManifestResponse>;
|
|
62
|
+
|
|
63
|
+
interface ConfigFile {
|
|
64
|
+
siteUuid?: string;
|
|
65
|
+
endpoint?: string;
|
|
66
|
+
timeoutMs?: number;
|
|
67
|
+
}
|
|
68
|
+
interface ResolveConfigOptions {
|
|
69
|
+
cwd: string;
|
|
70
|
+
cliSiteUuid?: string;
|
|
71
|
+
cliEndpoint?: string;
|
|
72
|
+
}
|
|
73
|
+
declare function resolveConfig(options: ResolveConfigOptions): Promise<Config>;
|
|
74
|
+
declare function writeConfigFile(cwd: string, config: ConfigFile): Promise<string>;
|
|
75
|
+
|
|
76
|
+
interface ScanAndReportOptions {
|
|
77
|
+
cwd?: string;
|
|
78
|
+
config?: Config;
|
|
79
|
+
}
|
|
80
|
+
interface ScanAndReportResult {
|
|
81
|
+
manifest: Manifest;
|
|
82
|
+
response: StoreManifestResponse;
|
|
83
|
+
duplicateNames: string[];
|
|
84
|
+
uniqueNames: number;
|
|
85
|
+
totalEntries: number;
|
|
86
|
+
}
|
|
87
|
+
declare function scanAndReport(options?: ScanAndReportOptions): Promise<ScanAndReportResult>;
|
|
88
|
+
|
|
89
|
+
export { type Config, DEFAULT_ENDPOINT, type Ecosystem, type Manifest, type PackageEntry, PatchstackError, type ScanAndReportOptions, type ScanAndReportResult, type StoreManifestResponse, buildEndpointUrl, buildWirePayload, compareVersions, detectLockfile, postManifest, resolveConfig, scanAndReport, scanLockfile, writeConfigFile };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
// src/parsers/index.ts
|
|
2
|
+
import { access } from "fs/promises";
|
|
3
|
+
import path2 from "path";
|
|
4
|
+
|
|
5
|
+
// src/types.ts
|
|
6
|
+
var PatchstackError = class extends Error {
|
|
7
|
+
constructor(message, code, cause) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.cause = cause;
|
|
11
|
+
this.name = "PatchstackError";
|
|
12
|
+
}
|
|
13
|
+
code;
|
|
14
|
+
cause;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/parsers/npm.ts
|
|
18
|
+
import { readFile } from "fs/promises";
|
|
19
|
+
import path from "path";
|
|
20
|
+
async function parseNpmLockfile(lockfilePath) {
|
|
21
|
+
let raw;
|
|
22
|
+
try {
|
|
23
|
+
raw = await readFile(lockfilePath, "utf8");
|
|
24
|
+
} catch (cause) {
|
|
25
|
+
throw new PatchstackError(`Could not read lockfile at ${lockfilePath}`, "LOCKFILE_NOT_FOUND", cause);
|
|
26
|
+
}
|
|
27
|
+
let parsed;
|
|
28
|
+
try {
|
|
29
|
+
parsed = JSON.parse(raw);
|
|
30
|
+
} catch (cause) {
|
|
31
|
+
throw new PatchstackError(`Lockfile at ${lockfilePath} is not valid JSON`, "LOCKFILE_PARSE_ERROR", cause);
|
|
32
|
+
}
|
|
33
|
+
if (parsed.packages) {
|
|
34
|
+
return extractFromV2(parsed.packages);
|
|
35
|
+
}
|
|
36
|
+
if (parsed.dependencies) {
|
|
37
|
+
return extractFromV1(parsed.dependencies);
|
|
38
|
+
}
|
|
39
|
+
throw new PatchstackError(
|
|
40
|
+
`Lockfile at ${lockfilePath} has no "packages" or "dependencies" key`,
|
|
41
|
+
"LOCKFILE_PARSE_ERROR"
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
function extractFromV2(packages) {
|
|
45
|
+
const entries = [];
|
|
46
|
+
for (const [pkgPath, pkg] of Object.entries(packages)) {
|
|
47
|
+
if (pkgPath === "") {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (pkg.link === true) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (typeof pkg.version !== "string" || pkg.version.length === 0) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const name = pkg.name ?? extractNameFromPath(pkgPath);
|
|
57
|
+
if (name === null) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
entries.push({
|
|
61
|
+
name,
|
|
62
|
+
version: pkg.version,
|
|
63
|
+
path: pkgPath,
|
|
64
|
+
direct: isDirectV2(pkgPath)
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return entries;
|
|
68
|
+
}
|
|
69
|
+
function extractFromV1(deps, acc = [], depth = 0) {
|
|
70
|
+
for (const [name, dep] of Object.entries(deps)) {
|
|
71
|
+
if (typeof dep.version === "string" && dep.version.length > 0) {
|
|
72
|
+
acc.push({ name, version: dep.version, direct: depth === 0 });
|
|
73
|
+
}
|
|
74
|
+
if (dep.dependencies) {
|
|
75
|
+
extractFromV1(dep.dependencies, acc, depth + 1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return acc;
|
|
79
|
+
}
|
|
80
|
+
function extractNameFromPath(pkgPath) {
|
|
81
|
+
const segments = pkgPath.split("node_modules" + path.sep === pkgPath ? path.sep : "/");
|
|
82
|
+
const parts = pkgPath.split("/");
|
|
83
|
+
const nmIndex = parts.lastIndexOf("node_modules");
|
|
84
|
+
if (nmIndex === -1 || nmIndex >= parts.length - 1) {
|
|
85
|
+
return segments[segments.length - 1] ?? null;
|
|
86
|
+
}
|
|
87
|
+
const tail = parts.slice(nmIndex + 1);
|
|
88
|
+
if (tail.length === 0) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const first = tail[0];
|
|
92
|
+
if (first !== void 0 && first.startsWith("@") && tail.length >= 2) {
|
|
93
|
+
return `${first}/${tail[1]}`;
|
|
94
|
+
}
|
|
95
|
+
return first ?? null;
|
|
96
|
+
}
|
|
97
|
+
function isDirectV2(pkgPath) {
|
|
98
|
+
const parts = pkgPath.split("/");
|
|
99
|
+
const nmCount = parts.filter((p) => p === "node_modules").length;
|
|
100
|
+
return nmCount === 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/parsers/index.ts
|
|
104
|
+
async function detectLockfile(cwd) {
|
|
105
|
+
const npmLock = path2.join(cwd, "package-lock.json");
|
|
106
|
+
if (await exists(npmLock)) {
|
|
107
|
+
return { ecosystem: "npm", filePath: npmLock, filename: "package-lock.json" };
|
|
108
|
+
}
|
|
109
|
+
const yarnLock = path2.join(cwd, "yarn.lock");
|
|
110
|
+
if (await exists(yarnLock)) {
|
|
111
|
+
throw new PatchstackError(
|
|
112
|
+
"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.",
|
|
113
|
+
"LOCKFILE_UNSUPPORTED"
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
const pnpmLock = path2.join(cwd, "pnpm-lock.yaml");
|
|
117
|
+
if (await exists(pnpmLock)) {
|
|
118
|
+
throw new PatchstackError(
|
|
119
|
+
"pnpm-lock.yaml detected but not yet supported. Open an issue at github.com/patchstack/connect to request support.",
|
|
120
|
+
"LOCKFILE_UNSUPPORTED"
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
throw new PatchstackError(
|
|
124
|
+
`No lockfile found in ${cwd}. Expected one of: package-lock.json, yarn.lock, pnpm-lock.yaml.`,
|
|
125
|
+
"LOCKFILE_NOT_FOUND"
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
async function scanLockfile(cwd) {
|
|
129
|
+
const detected = await detectLockfile(cwd);
|
|
130
|
+
const packages = await parseNpmLockfile(detected.filePath);
|
|
131
|
+
return { ecosystem: detected.ecosystem, packages };
|
|
132
|
+
}
|
|
133
|
+
async function exists(filePath) {
|
|
134
|
+
try {
|
|
135
|
+
await access(filePath);
|
|
136
|
+
return true;
|
|
137
|
+
} catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/normalize.ts
|
|
143
|
+
function buildWirePayload(manifest) {
|
|
144
|
+
const seen = /* @__PURE__ */ new Map();
|
|
145
|
+
const wirePackages = [];
|
|
146
|
+
for (const entry of manifest.packages) {
|
|
147
|
+
const versions = seen.get(entry.name);
|
|
148
|
+
if (versions) {
|
|
149
|
+
if (versions.has(entry.version)) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
versions.add(entry.version);
|
|
153
|
+
} else {
|
|
154
|
+
seen.set(entry.name, /* @__PURE__ */ new Set([entry.version]));
|
|
155
|
+
}
|
|
156
|
+
wirePackages.push({ name: entry.name, version: entry.version });
|
|
157
|
+
}
|
|
158
|
+
wirePackages.sort((a, b) => {
|
|
159
|
+
if (a.name === b.name) {
|
|
160
|
+
return compareVersions(a.version, b.version);
|
|
161
|
+
}
|
|
162
|
+
return a.name < b.name ? -1 : 1;
|
|
163
|
+
});
|
|
164
|
+
const duplicateNames = [];
|
|
165
|
+
for (const [name, versions] of seen) {
|
|
166
|
+
if (versions.size > 1) {
|
|
167
|
+
duplicateNames.push(name);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
payload: { ecosystem: manifest.ecosystem, packages: wirePackages },
|
|
172
|
+
stats: {
|
|
173
|
+
uniqueNames: seen.size,
|
|
174
|
+
duplicateNames,
|
|
175
|
+
totalEntries: manifest.packages.length
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function compareVersions(a, b) {
|
|
180
|
+
if (a === b) {
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
const [aBase, aPre] = splitPrerelease(a);
|
|
184
|
+
const [bBase, bPre] = splitPrerelease(b);
|
|
185
|
+
const baseCmp = compareSegments(aBase.split("."), bBase.split("."));
|
|
186
|
+
if (baseCmp !== 0) {
|
|
187
|
+
return baseCmp;
|
|
188
|
+
}
|
|
189
|
+
if (aPre === null && bPre === null) {
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
if (aPre === null) {
|
|
193
|
+
return 1;
|
|
194
|
+
}
|
|
195
|
+
if (bPre === null) {
|
|
196
|
+
return -1;
|
|
197
|
+
}
|
|
198
|
+
return compareSegments(aPre.split("."), bPre.split("."));
|
|
199
|
+
}
|
|
200
|
+
function splitPrerelease(version) {
|
|
201
|
+
const cleaned = version.replace(/^[v=]+/, "").split("+")[0];
|
|
202
|
+
const dashIndex = cleaned.indexOf("-");
|
|
203
|
+
if (dashIndex === -1) {
|
|
204
|
+
return [cleaned, null];
|
|
205
|
+
}
|
|
206
|
+
return [cleaned.slice(0, dashIndex), cleaned.slice(dashIndex + 1)];
|
|
207
|
+
}
|
|
208
|
+
function compareSegments(a, b) {
|
|
209
|
+
const max = Math.max(a.length, b.length);
|
|
210
|
+
for (let i = 0; i < max; i++) {
|
|
211
|
+
const aPart = a[i];
|
|
212
|
+
const bPart = b[i];
|
|
213
|
+
if (aPart === void 0) {
|
|
214
|
+
return -1;
|
|
215
|
+
}
|
|
216
|
+
if (bPart === void 0) {
|
|
217
|
+
return 1;
|
|
218
|
+
}
|
|
219
|
+
const aNum = /^\d+$/.test(aPart);
|
|
220
|
+
const bNum = /^\d+$/.test(bPart);
|
|
221
|
+
if (aNum && bNum) {
|
|
222
|
+
const diff = Number(aPart) - Number(bPart);
|
|
223
|
+
if (diff !== 0) {
|
|
224
|
+
return diff < 0 ? -1 : 1;
|
|
225
|
+
}
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (aNum) {
|
|
229
|
+
return -1;
|
|
230
|
+
}
|
|
231
|
+
if (bNum) {
|
|
232
|
+
return 1;
|
|
233
|
+
}
|
|
234
|
+
if (aPart < bPart) {
|
|
235
|
+
return -1;
|
|
236
|
+
}
|
|
237
|
+
if (aPart > bPart) {
|
|
238
|
+
return 1;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/client.ts
|
|
245
|
+
var DEFAULT_ENDPOINT = "https://app.patchstack.com/monitor/pulse/manifest";
|
|
246
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
247
|
+
function buildEndpointUrl(base, siteUuid) {
|
|
248
|
+
const trimmed = base.replace(/\/$/, "");
|
|
249
|
+
return `${trimmed}/${encodeURIComponent(siteUuid)}`;
|
|
250
|
+
}
|
|
251
|
+
async function postManifest(config, payload) {
|
|
252
|
+
const url = buildEndpointUrl(config.endpoint, config.siteUuid);
|
|
253
|
+
const timeoutMs = config.timeoutMs;
|
|
254
|
+
let response;
|
|
255
|
+
try {
|
|
256
|
+
response = await fetch(url, {
|
|
257
|
+
method: "POST",
|
|
258
|
+
headers: {
|
|
259
|
+
"Content-Type": "application/json",
|
|
260
|
+
Accept: "application/json",
|
|
261
|
+
"User-Agent": "@patchstack/connect"
|
|
262
|
+
},
|
|
263
|
+
body: JSON.stringify(payload),
|
|
264
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
265
|
+
});
|
|
266
|
+
} catch (cause) {
|
|
267
|
+
if (isTimeoutError(cause)) {
|
|
268
|
+
throw new PatchstackError(
|
|
269
|
+
`Patchstack request to ${url} timed out after ${timeoutMs}ms. Override with PATCHSTACK_TIMEOUT_MS.`,
|
|
270
|
+
"NETWORK_TIMEOUT",
|
|
271
|
+
cause
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
throw new PatchstackError(
|
|
275
|
+
`Could not reach Patchstack at ${url}. Check your network connection.`,
|
|
276
|
+
"NETWORK_ERROR",
|
|
277
|
+
cause
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
const text = await response.text();
|
|
281
|
+
let body = null;
|
|
282
|
+
try {
|
|
283
|
+
body = text.length > 0 ? JSON.parse(text) : null;
|
|
284
|
+
} catch {
|
|
285
|
+
body = null;
|
|
286
|
+
}
|
|
287
|
+
if (response.status === 404) {
|
|
288
|
+
throw new PatchstackError(
|
|
289
|
+
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.",
|
|
290
|
+
"SITE_NOT_FOUND"
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
if (response.status === 422) {
|
|
294
|
+
throw new PatchstackError(
|
|
295
|
+
body?.message ?? "Patchstack rejected the manifest payload (validation failed).",
|
|
296
|
+
"VALIDATION_ERROR"
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
if (response.status < 200 || response.status >= 300) {
|
|
300
|
+
throw new PatchstackError(
|
|
301
|
+
`Patchstack returned ${response.status}: ${text.slice(0, 200)}`,
|
|
302
|
+
"SERVER_ERROR"
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
if (body === null) {
|
|
306
|
+
throw new PatchstackError("Patchstack returned an empty response.", "SERVER_ERROR");
|
|
307
|
+
}
|
|
308
|
+
return body;
|
|
309
|
+
}
|
|
310
|
+
function isTimeoutError(cause) {
|
|
311
|
+
if (cause instanceof Error) {
|
|
312
|
+
return cause.name === "TimeoutError" || cause.name === "AbortError";
|
|
313
|
+
}
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/config.ts
|
|
318
|
+
import { readFile as readFile2, writeFile } from "fs/promises";
|
|
319
|
+
import path3 from "path";
|
|
320
|
+
var CONFIG_FILENAME = ".patchstackrc.json";
|
|
321
|
+
async function resolveConfig(options) {
|
|
322
|
+
const fromFile = await readConfigFile(options.cwd);
|
|
323
|
+
const fromEnv = readEnv();
|
|
324
|
+
const siteUuid = options.cliSiteUuid ?? fromEnv.siteUuid ?? fromFile.siteUuid ?? null;
|
|
325
|
+
const endpoint = options.cliEndpoint ?? fromEnv.endpoint ?? fromFile.endpoint ?? DEFAULT_ENDPOINT;
|
|
326
|
+
const timeoutMs = fromEnv.timeoutMs ?? fromFile.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
327
|
+
if (siteUuid === null || siteUuid.length === 0) {
|
|
328
|
+
throw new PatchstackError(
|
|
329
|
+
"No site UUID configured. Run `patchstack-connect init <site-uuid>` or set PATCHSTACK_SITE_UUID.",
|
|
330
|
+
"CONFIG_MISSING"
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
if (!isUuid(siteUuid)) {
|
|
334
|
+
throw new PatchstackError(
|
|
335
|
+
`Site UUID "${siteUuid}" does not look like a valid UUID.`,
|
|
336
|
+
"CONFIG_INVALID"
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
return { siteUuid, endpoint, timeoutMs };
|
|
340
|
+
}
|
|
341
|
+
async function writeConfigFile(cwd, config) {
|
|
342
|
+
const target = path3.join(cwd, CONFIG_FILENAME);
|
|
343
|
+
const content = JSON.stringify(config, null, 2) + "\n";
|
|
344
|
+
await writeFile(target, content, "utf8");
|
|
345
|
+
return target;
|
|
346
|
+
}
|
|
347
|
+
async function readConfigFile(cwd) {
|
|
348
|
+
const target = path3.join(cwd, CONFIG_FILENAME);
|
|
349
|
+
let raw;
|
|
350
|
+
try {
|
|
351
|
+
raw = await readFile2(target, "utf8");
|
|
352
|
+
} catch (err) {
|
|
353
|
+
if (err.code === "ENOENT") {
|
|
354
|
+
return {};
|
|
355
|
+
}
|
|
356
|
+
throw new PatchstackError(
|
|
357
|
+
`Could not read ${target}: ${err.message}`,
|
|
358
|
+
"CONFIG_INVALID",
|
|
359
|
+
err
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
return JSON.parse(raw);
|
|
364
|
+
} catch (err) {
|
|
365
|
+
throw new PatchstackError(
|
|
366
|
+
`Config file ${target} contains invalid JSON.`,
|
|
367
|
+
"CONFIG_INVALID",
|
|
368
|
+
err
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function readEnv() {
|
|
373
|
+
const timeoutRaw = process.env.PATCHSTACK_TIMEOUT_MS;
|
|
374
|
+
let timeoutMs;
|
|
375
|
+
if (timeoutRaw !== void 0 && timeoutRaw.length > 0) {
|
|
376
|
+
const parsed = Number(timeoutRaw);
|
|
377
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
378
|
+
throw new PatchstackError(
|
|
379
|
+
`PATCHSTACK_TIMEOUT_MS must be a positive number; got "${timeoutRaw}".`,
|
|
380
|
+
"CONFIG_INVALID"
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
timeoutMs = parsed;
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
siteUuid: process.env.PATCHSTACK_SITE_UUID ?? void 0,
|
|
387
|
+
endpoint: process.env.PATCHSTACK_ENDPOINT ?? void 0,
|
|
388
|
+
timeoutMs
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
function isUuid(value) {
|
|
392
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/index.ts
|
|
396
|
+
async function scanAndReport(options = {}) {
|
|
397
|
+
const cwd = options.cwd ?? process.cwd();
|
|
398
|
+
const config = options.config ?? await resolveConfig({ cwd });
|
|
399
|
+
const manifest = await scanLockfile(cwd);
|
|
400
|
+
const { payload, stats } = buildWirePayload(manifest);
|
|
401
|
+
const response = await postManifest(config, payload);
|
|
402
|
+
return {
|
|
403
|
+
manifest,
|
|
404
|
+
response,
|
|
405
|
+
duplicateNames: stats.duplicateNames,
|
|
406
|
+
uniqueNames: stats.uniqueNames,
|
|
407
|
+
totalEntries: stats.totalEntries
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
export {
|
|
411
|
+
DEFAULT_ENDPOINT,
|
|
412
|
+
PatchstackError,
|
|
413
|
+
buildEndpointUrl,
|
|
414
|
+
buildWirePayload,
|
|
415
|
+
compareVersions,
|
|
416
|
+
detectLockfile,
|
|
417
|
+
postManifest,
|
|
418
|
+
resolveConfig,
|
|
419
|
+
scanAndReport,
|
|
420
|
+
scanLockfile,
|
|
421
|
+
writeConfigFile
|
|
422
|
+
};
|
|
423
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/parsers/index.ts","../src/types.ts","../src/parsers/npm.ts","../src/normalize.ts","../src/client.ts","../src/config.ts","../src/index.ts"],"sourcesContent":["import { access } from 'node:fs/promises';\nimport path from 'node:path';\nimport { PatchstackError, type Manifest } from '../types.js';\nimport { parseNpmLockfile } from './npm.js';\n\ninterface DetectedLockfile {\n ecosystem: 'npm';\n filePath: string;\n filename: 'package-lock.json' | 'yarn.lock' | 'pnpm-lock.yaml';\n}\n\nexport async function detectLockfile(cwd: string): Promise<DetectedLockfile> {\n const npmLock = path.join(cwd, 'package-lock.json');\n if (await exists(npmLock)) {\n return { ecosystem: 'npm', filePath: npmLock, filename: 'package-lock.json' };\n }\n\n const yarnLock = path.join(cwd, 'yarn.lock');\n if (await exists(yarnLock)) {\n throw new PatchstackError(\n '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.',\n 'LOCKFILE_UNSUPPORTED',\n );\n }\n\n const pnpmLock = path.join(cwd, 'pnpm-lock.yaml');\n if (await exists(pnpmLock)) {\n throw new PatchstackError(\n 'pnpm-lock.yaml detected but not yet supported. Open an issue at github.com/patchstack/connect to request support.',\n 'LOCKFILE_UNSUPPORTED',\n );\n }\n\n throw new PatchstackError(\n `No lockfile found in ${cwd}. Expected one of: package-lock.json, yarn.lock, pnpm-lock.yaml.`,\n 'LOCKFILE_NOT_FOUND',\n );\n}\n\nexport async function scanLockfile(cwd: string): Promise<Manifest> {\n const detected = await detectLockfile(cwd);\n const packages = await parseNpmLockfile(detected.filePath);\n return { ecosystem: detected.ecosystem, packages };\n}\n\nasync function exists(filePath: string): Promise<boolean> {\n try {\n await access(filePath);\n return true;\n } catch {\n return false;\n }\n}\n","export type Ecosystem = 'npm' | 'composer';\n\nexport interface PackageEntry {\n name: string;\n version: string;\n path?: string;\n direct?: boolean;\n}\n\nexport interface Manifest {\n ecosystem: Ecosystem;\n packages: PackageEntry[];\n}\n\nexport interface Config {\n siteUuid: string;\n endpoint: string;\n timeoutMs: number;\n}\n\nexport interface StoreManifestResponse {\n stored: boolean;\n manifest_id?: number;\n checksum?: string;\n reason?: string;\n message?: string;\n error?: string;\n}\n\nexport class PatchstackError extends Error {\n constructor(\n message: string,\n public readonly code:\n | 'CONFIG_MISSING'\n | 'CONFIG_INVALID'\n | 'LOCKFILE_NOT_FOUND'\n | 'LOCKFILE_UNSUPPORTED'\n | 'LOCKFILE_PARSE_ERROR'\n | 'NETWORK_ERROR'\n | 'NETWORK_TIMEOUT'\n | 'SITE_NOT_FOUND'\n | 'VALIDATION_ERROR'\n | 'SERVER_ERROR',\n public readonly cause?: unknown,\n ) {\n super(message);\n this.name = 'PatchstackError';\n }\n}\n","import { readFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport { PatchstackError, type PackageEntry } from '../types.js';\n\ninterface LockfileV2Package {\n name?: string;\n version?: string;\n link?: boolean;\n resolved?: string;\n}\n\ninterface LockfileV2 {\n lockfileVersion: number;\n packages?: Record<string, LockfileV2Package>;\n dependencies?: Record<string, LockfileV1Dependency>;\n}\n\ninterface LockfileV1Dependency {\n version: string;\n dependencies?: Record<string, LockfileV1Dependency>;\n}\n\nexport async function parseNpmLockfile(lockfilePath: string): Promise<PackageEntry[]> {\n let raw: string;\n try {\n raw = await readFile(lockfilePath, 'utf8');\n } catch (cause) {\n throw new PatchstackError(`Could not read lockfile at ${lockfilePath}`, 'LOCKFILE_NOT_FOUND', cause);\n }\n\n let parsed: LockfileV2;\n try {\n parsed = JSON.parse(raw) as LockfileV2;\n } catch (cause) {\n throw new PatchstackError(`Lockfile at ${lockfilePath} is not valid JSON`, 'LOCKFILE_PARSE_ERROR', cause);\n }\n\n if (parsed.packages) {\n return extractFromV2(parsed.packages);\n }\n\n if (parsed.dependencies) {\n return extractFromV1(parsed.dependencies);\n }\n\n throw new PatchstackError(\n `Lockfile at ${lockfilePath} has no \"packages\" or \"dependencies\" key`,\n 'LOCKFILE_PARSE_ERROR',\n );\n}\n\nfunction extractFromV2(packages: Record<string, LockfileV2Package>): PackageEntry[] {\n const entries: PackageEntry[] = [];\n\n for (const [pkgPath, pkg] of Object.entries(packages)) {\n if (pkgPath === '') {\n continue;\n }\n if (pkg.link === true) {\n continue;\n }\n if (typeof pkg.version !== 'string' || pkg.version.length === 0) {\n continue;\n }\n\n const name = pkg.name ?? extractNameFromPath(pkgPath);\n if (name === null) {\n continue;\n }\n\n entries.push({\n name,\n version: pkg.version,\n path: pkgPath,\n direct: isDirectV2(pkgPath),\n });\n }\n\n return entries;\n}\n\nfunction extractFromV1(\n deps: Record<string, LockfileV1Dependency>,\n acc: PackageEntry[] = [],\n depth = 0,\n): PackageEntry[] {\n for (const [name, dep] of Object.entries(deps)) {\n if (typeof dep.version === 'string' && dep.version.length > 0) {\n acc.push({ name, version: dep.version, direct: depth === 0 });\n }\n if (dep.dependencies) {\n extractFromV1(dep.dependencies, acc, depth + 1);\n }\n }\n return acc;\n}\n\nfunction extractNameFromPath(pkgPath: string): string | null {\n const segments = pkgPath.split('node_modules' + path.sep === pkgPath ? path.sep : '/');\n const parts = pkgPath.split('/');\n const nmIndex = parts.lastIndexOf('node_modules');\n if (nmIndex === -1 || nmIndex >= parts.length - 1) {\n return segments[segments.length - 1] ?? null;\n }\n const tail = parts.slice(nmIndex + 1);\n if (tail.length === 0) {\n return null;\n }\n const first = tail[0];\n if (first !== undefined && first.startsWith('@') && tail.length >= 2) {\n return `${first}/${tail[1]}`;\n }\n return first ?? null;\n}\n\nfunction isDirectV2(pkgPath: string): boolean {\n const parts = pkgPath.split('/');\n const nmCount = parts.filter((p) => p === 'node_modules').length;\n return nmCount === 1;\n}\n","import type { Manifest, PackageEntry } from './types.js';\n\nexport interface WirePackage {\n name: string;\n version: string;\n}\n\nexport interface WirePayload {\n ecosystem: Manifest['ecosystem'];\n packages: WirePackage[];\n}\n\nexport interface NormalizeStats {\n uniqueNames: number;\n duplicateNames: string[];\n totalEntries: number;\n}\n\nexport interface NormalizeResult {\n payload: WirePayload;\n stats: NormalizeStats;\n}\n\nexport function buildWirePayload(manifest: Manifest): NormalizeResult {\n const seen = new Map<string, Set<string>>();\n const wirePackages: WirePackage[] = [];\n\n for (const entry of manifest.packages) {\n const versions = seen.get(entry.name);\n if (versions) {\n if (versions.has(entry.version)) {\n continue;\n }\n versions.add(entry.version);\n } else {\n seen.set(entry.name, new Set([entry.version]));\n }\n wirePackages.push({ name: entry.name, version: entry.version });\n }\n\n wirePackages.sort((a, b) => {\n if (a.name === b.name) {\n return compareVersions(a.version, b.version);\n }\n return a.name < b.name ? -1 : 1;\n });\n\n const duplicateNames: string[] = [];\n for (const [name, versions] of seen) {\n if (versions.size > 1) {\n duplicateNames.push(name);\n }\n }\n\n return {\n payload: { ecosystem: manifest.ecosystem, packages: wirePackages },\n stats: {\n uniqueNames: seen.size,\n duplicateNames,\n totalEntries: manifest.packages.length,\n },\n };\n}\n\nexport function compareVersions(a: string, b: string): number {\n if (a === b) {\n return 0;\n }\n\n const [aBase, aPre] = splitPrerelease(a);\n const [bBase, bPre] = splitPrerelease(b);\n\n const baseCmp = compareSegments(aBase.split('.'), bBase.split('.'));\n if (baseCmp !== 0) {\n return baseCmp;\n }\n\n if (aPre === null && bPre === null) {\n return 0;\n }\n if (aPre === null) {\n return 1;\n }\n if (bPre === null) {\n return -1;\n }\n return compareSegments(aPre.split('.'), bPre.split('.'));\n}\n\nfunction splitPrerelease(version: string): [string, string | null] {\n const cleaned = version.replace(/^[v=]+/, '').split('+')[0]!;\n const dashIndex = cleaned.indexOf('-');\n if (dashIndex === -1) {\n return [cleaned, null];\n }\n return [cleaned.slice(0, dashIndex), cleaned.slice(dashIndex + 1)];\n}\n\nfunction compareSegments(a: string[], b: string[]): number {\n const max = Math.max(a.length, b.length);\n for (let i = 0; i < max; i++) {\n const aPart = a[i];\n const bPart = b[i];\n if (aPart === undefined) {\n return -1;\n }\n if (bPart === undefined) {\n return 1;\n }\n const aNum = /^\\d+$/.test(aPart);\n const bNum = /^\\d+$/.test(bPart);\n if (aNum && bNum) {\n const diff = Number(aPart) - Number(bPart);\n if (diff !== 0) {\n return diff < 0 ? -1 : 1;\n }\n continue;\n }\n if (aNum) {\n return -1;\n }\n if (bNum) {\n return 1;\n }\n if (aPart < bPart) {\n return -1;\n }\n if (aPart > bPart) {\n return 1;\n }\n }\n return 0;\n}\n\nexport function findPackageInManifest(\n manifest: Manifest,\n name: string,\n): PackageEntry[] {\n return manifest.packages.filter((p) => p.name === name);\n}\n","import { PatchstackError, type Config, type StoreManifestResponse } from './types.js';\nimport type { WirePayload } from './normalize.js';\n\nexport const DEFAULT_ENDPOINT = 'https://app.patchstack.com/monitor/pulse/manifest';\nexport const DEFAULT_TIMEOUT_MS = 30_000;\n\nexport function buildEndpointUrl(base: string, siteUuid: string): string {\n const trimmed = base.replace(/\\/$/, '');\n return `${trimmed}/${encodeURIComponent(siteUuid)}`;\n}\n\nexport async function postManifest(\n config: Config,\n payload: WirePayload,\n): Promise<StoreManifestResponse> {\n const url = buildEndpointUrl(config.endpoint, config.siteUuid);\n const timeoutMs = config.timeoutMs;\n\n let response: Response;\n try {\n response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n 'User-Agent': '@patchstack/connect',\n },\n body: JSON.stringify(payload),\n signal: AbortSignal.timeout(timeoutMs),\n });\n } catch (cause) {\n if (isTimeoutError(cause)) {\n throw new PatchstackError(\n `Patchstack request to ${url} timed out after ${timeoutMs}ms. Override with PATCHSTACK_TIMEOUT_MS.`,\n 'NETWORK_TIMEOUT',\n cause,\n );\n }\n throw new PatchstackError(\n `Could not reach Patchstack at ${url}. Check your network connection.`,\n 'NETWORK_ERROR',\n cause,\n );\n }\n\n const text = await response.text();\n let body: StoreManifestResponse | null = null;\n try {\n body = text.length > 0 ? (JSON.parse(text) as StoreManifestResponse) : null;\n } catch {\n body = null;\n }\n\n if (response.status === 404) {\n throw new PatchstackError(\n 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.',\n 'SITE_NOT_FOUND',\n );\n }\n\n if (response.status === 422) {\n throw new PatchstackError(\n body?.message ?? 'Patchstack rejected the manifest payload (validation failed).',\n 'VALIDATION_ERROR',\n );\n }\n\n if (response.status < 200 || response.status >= 300) {\n throw new PatchstackError(\n `Patchstack returned ${response.status}: ${text.slice(0, 200)}`,\n 'SERVER_ERROR',\n );\n }\n\n if (body === null) {\n throw new PatchstackError('Patchstack returned an empty response.', 'SERVER_ERROR');\n }\n\n return body;\n}\n\nfunction isTimeoutError(cause: unknown): boolean {\n if (cause instanceof Error) {\n return cause.name === 'TimeoutError' || cause.name === 'AbortError';\n }\n return false;\n}\n","import { readFile, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport { PatchstackError, type Config } from './types.js';\nimport { DEFAULT_ENDPOINT, DEFAULT_TIMEOUT_MS } from './client.js';\n\nconst CONFIG_FILENAME = '.patchstackrc.json';\n\ninterface ConfigFile {\n siteUuid?: string;\n endpoint?: string;\n timeoutMs?: number;\n}\n\nexport interface ResolveConfigOptions {\n cwd: string;\n cliSiteUuid?: string;\n cliEndpoint?: string;\n}\n\nexport async function resolveConfig(options: ResolveConfigOptions): Promise<Config> {\n const fromFile = await readConfigFile(options.cwd);\n const fromEnv = readEnv();\n\n const siteUuid =\n options.cliSiteUuid ??\n fromEnv.siteUuid ??\n fromFile.siteUuid ??\n null;\n\n const endpoint =\n options.cliEndpoint ??\n fromEnv.endpoint ??\n fromFile.endpoint ??\n DEFAULT_ENDPOINT;\n\n const timeoutMs = fromEnv.timeoutMs ?? fromFile.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n if (siteUuid === null || siteUuid.length === 0) {\n throw new PatchstackError(\n 'No site UUID configured. Run `patchstack-connect init <site-uuid>` or set PATCHSTACK_SITE_UUID.',\n 'CONFIG_MISSING',\n );\n }\n\n if (!isUuid(siteUuid)) {\n throw new PatchstackError(\n `Site UUID \"${siteUuid}\" does not look like a valid UUID.`,\n 'CONFIG_INVALID',\n );\n }\n\n return { siteUuid, endpoint, timeoutMs };\n}\n\nexport async function writeConfigFile(cwd: string, config: ConfigFile): Promise<string> {\n const target = path.join(cwd, CONFIG_FILENAME);\n const content = JSON.stringify(config, null, 2) + '\\n';\n await writeFile(target, content, 'utf8');\n return target;\n}\n\nasync function readConfigFile(cwd: string): Promise<ConfigFile> {\n const target = path.join(cwd, CONFIG_FILENAME);\n let raw: string;\n try {\n raw = await readFile(target, 'utf8');\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n return {};\n }\n throw new PatchstackError(\n `Could not read ${target}: ${(err as Error).message}`,\n 'CONFIG_INVALID',\n err,\n );\n }\n\n try {\n return JSON.parse(raw) as ConfigFile;\n } catch (err) {\n throw new PatchstackError(\n `Config file ${target} contains invalid JSON.`,\n 'CONFIG_INVALID',\n err,\n );\n }\n}\n\nfunction readEnv(): ConfigFile {\n const timeoutRaw = process.env.PATCHSTACK_TIMEOUT_MS;\n let timeoutMs: number | undefined;\n if (timeoutRaw !== undefined && timeoutRaw.length > 0) {\n const parsed = Number(timeoutRaw);\n if (!Number.isFinite(parsed) || parsed <= 0) {\n throw new PatchstackError(\n `PATCHSTACK_TIMEOUT_MS must be a positive number; got \"${timeoutRaw}\".`,\n 'CONFIG_INVALID',\n );\n }\n timeoutMs = parsed;\n }\n return {\n siteUuid: process.env.PATCHSTACK_SITE_UUID ?? undefined,\n endpoint: process.env.PATCHSTACK_ENDPOINT ?? undefined,\n timeoutMs,\n };\n}\n\nfunction isUuid(value: string): boolean {\n return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);\n}\n","import { scanLockfile } from './parsers/index.js';\nimport { buildWirePayload } from './normalize.js';\nimport { postManifest } from './client.js';\nimport { resolveConfig } from './config.js';\nimport type { Config, Manifest, StoreManifestResponse } from './types.js';\n\nexport { scanLockfile, detectLockfile } from './parsers/index.js';\nexport { buildWirePayload, compareVersions } from './normalize.js';\nexport { postManifest, buildEndpointUrl, DEFAULT_ENDPOINT } from './client.js';\nexport { resolveConfig, writeConfigFile } from './config.js';\nexport {\n PatchstackError,\n type Config,\n type Ecosystem,\n type Manifest,\n type PackageEntry,\n type StoreManifestResponse,\n} from './types.js';\n\nexport interface ScanAndReportOptions {\n cwd?: string;\n config?: Config;\n}\n\nexport interface ScanAndReportResult {\n manifest: Manifest;\n response: StoreManifestResponse;\n duplicateNames: string[];\n uniqueNames: number;\n totalEntries: number;\n}\n\nexport async function scanAndReport(\n options: ScanAndReportOptions = {},\n): Promise<ScanAndReportResult> {\n const cwd = options.cwd ?? process.cwd();\n const config = options.config ?? (await resolveConfig({ cwd }));\n const manifest = await scanLockfile(cwd);\n const { payload, stats } = buildWirePayload(manifest);\n const response = await postManifest(config, payload);\n return {\n manifest,\n response,\n duplicateNames: stats.duplicateNames,\n uniqueNames: stats.uniqueNames,\n totalEntries: stats.totalEntries,\n };\n}\n"],"mappings":"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","names":["path","path","readFile","path","path","readFile"]}
|