@dinglebear/labby 1.8.6

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/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@dinglebear/labby",
3
+ "version": "1.8.6",
4
+ "description": "Rust MCP gateway with Code Mode, authentication, setup, logs, CLI, HTTP API, and operator web UI.",
5
+ "license": "MIT OR Apache-2.0",
6
+ "homepage": "https://github.com/dinglebear-ai/labby#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/dinglebear-ai/labby.git",
10
+ "directory": "packages/labby-mcp"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/dinglebear-ai/labby/issues"
14
+ },
15
+ "mcpName": "ai.dinglebear/labby",
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "bin": {
20
+ "labby-mcp": "bin/labby.js",
21
+ "labby": "bin/labby.js"
22
+ },
23
+ "files": [
24
+ "bin/",
25
+ "lib/",
26
+ "scripts/",
27
+ "README.md",
28
+ "LICENSE*",
29
+ "package.json"
30
+ ],
31
+ "scripts": {
32
+ "prepack": "node scripts/sync-readme.js",
33
+ "postinstall": "node scripts/install.js",
34
+ "test": "node --test",
35
+ "check": "node --check bin/labby.js && node --check scripts/install.js && node --check lib/platform.js && node --check scripts/sync-readme.js && node --check scripts/check-package.js && node scripts/check-package.js",
36
+ "prepublishOnly": "node scripts/check-package.js --release"
37
+ },
38
+ "engines": {
39
+ "node": ">=18"
40
+ },
41
+ "keywords": [
42
+ "labby",
43
+ "lab",
44
+ "mcp",
45
+ "mcp-gateway",
46
+ "mcp-server",
47
+ "model-context-protocol",
48
+ "ai-agents",
49
+ "homelab",
50
+ "rust",
51
+ "cli",
52
+ "http-api",
53
+ "web-ui",
54
+ "code-mode",
55
+ "oauth",
56
+ "log-search",
57
+ "local-first",
58
+ "automation"
59
+ ],
60
+ "author": {
61
+ "name": "dinglebear.ai",
62
+ "url": "https://dinglebear.ai"
63
+ }
64
+ }
@@ -0,0 +1,529 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require("node:fs");
5
+ const crypto = require("node:crypto");
6
+ const https = require("node:https");
7
+ const os = require("node:os");
8
+ const path = require("node:path");
9
+ const { spawnSync } = require("node:child_process");
10
+
11
+ const packageRoot = path.resolve(__dirname, "..");
12
+ const repoRoot = path.resolve(packageRoot, "..", "..");
13
+ const packageJsonPath = path.join(packageRoot, "package.json");
14
+ const packageJson = readJson(packageJsonPath);
15
+ const expectedPackageName = "@dinglebear/labby";
16
+ const releaseMode = process.argv.includes("--release");
17
+ const skipReleaseAssets = process.argv.includes("--skip-release-assets");
18
+ const releaseAssetDir = process.env.LABBY_RELEASE_ASSET_DIR;
19
+
20
+ const failures = [];
21
+
22
+ function readJson(file) {
23
+ return JSON.parse(fs.readFileSync(file, "utf8"));
24
+ }
25
+
26
+ function fail(message) {
27
+ failures.push(message);
28
+ }
29
+
30
+ function assert(condition, message) {
31
+ if (!condition) {
32
+ fail(message);
33
+ }
34
+ }
35
+
36
+ function normalizeRepoUrl(url) {
37
+ return String(url || "")
38
+ .replace(/^git\+/, "")
39
+ .replace(/#readme$/, "")
40
+ .replace(/\.git$/, "")
41
+ .replace(/\/$/, "");
42
+ }
43
+
44
+ function normalizeHomepage(url) {
45
+ return String(url || "").replace(/#readme$/, "").replace(/\/$/, "");
46
+ }
47
+
48
+ function repoLicenseFiles() {
49
+ return fs
50
+ .readdirSync(repoRoot)
51
+ .filter((entry) => /^licen[cs]e/i.test(entry))
52
+ .filter((entry) => fs.statSync(path.join(repoRoot, entry)).isFile())
53
+ .sort();
54
+ }
55
+
56
+ function compareFiles(left, right) {
57
+ if (!fs.existsSync(left) || !fs.existsSync(right)) {
58
+ return false;
59
+ }
60
+
61
+ return fs.readFileSync(left).equals(fs.readFileSync(right));
62
+ }
63
+
64
+ function run(command, args, options = {}) {
65
+ const env = { ...(options.env || process.env) };
66
+ delete env.npm_config_dry_run;
67
+ const result = spawnSync(command, args, {
68
+ cwd: options.cwd || packageRoot,
69
+ env,
70
+ encoding: "utf8",
71
+ stdio: options.stdio || ["ignore", "pipe", "pipe"],
72
+ });
73
+
74
+ if (result.status !== 0) {
75
+ const detail = (result.stderr || result.stdout || `${command} ${args.join(" ")} failed`).trim();
76
+ throw new Error(detail);
77
+ }
78
+
79
+ return result;
80
+ }
81
+
82
+ function checkSyncedFiles() {
83
+ assert(
84
+ compareFiles(path.join(repoRoot, "README.md"), path.join(packageRoot, "README.md")),
85
+ "package README.md must be byte-identical to the repo README.md; run npm pack or scripts/sync-readme.js",
86
+ );
87
+
88
+ const licenses = repoLicenseFiles();
89
+ assert(licenses.length > 0, "repo must have at least one LICENSE file to sync into the npm package");
90
+
91
+ for (const license of licenses) {
92
+ assert(
93
+ compareFiles(path.join(repoRoot, license), path.join(packageRoot, license)),
94
+ `package ${license} must be byte-identical to repo ${license}; run npm pack or scripts/sync-readme.js`,
95
+ );
96
+ }
97
+ }
98
+
99
+ function findNpmPackage(serverJson) {
100
+ return (serverJson.packages || []).find(
101
+ (entry) => entry.registryType === "npm" || entry.identifier === packageJson.name,
102
+ );
103
+ }
104
+
105
+ function checkMetadata() {
106
+ const serverPath = path.join(repoRoot, "server.json");
107
+ assert(fs.existsSync(serverPath), "repo must contain server.json for package metadata checks");
108
+
109
+ if (!fs.existsSync(serverPath)) {
110
+ return;
111
+ }
112
+
113
+ const serverJson = readJson(serverPath);
114
+ const npmPackage = findNpmPackage(serverJson);
115
+ const repoUrl = normalizeRepoUrl(packageJson.repository && packageJson.repository.url);
116
+ const serverRepoUrl = normalizeRepoUrl(serverJson.repository && serverJson.repository.url);
117
+ const homepage = normalizeHomepage(packageJson.homepage);
118
+ const serverWebsite = normalizeHomepage(serverJson.websiteUrl);
119
+
120
+ assert(packageJson.name, "package.json must include name");
121
+ assert(
122
+ packageJson.name === expectedPackageName,
123
+ `package.json name must be the dinglebear organization package ${expectedPackageName}`,
124
+ );
125
+ assert(packageJson.version, "package.json must include version");
126
+ assert(packageJson.description, "package.json must include description");
127
+ assert(packageJson.license, "package.json must include license");
128
+ assert(packageJson.author && packageJson.author.name === "dinglebear.ai", "package.json author must be dinglebear.ai");
129
+ assert(packageJson.engines && packageJson.engines.node, "package.json must declare engines.node");
130
+ assert(packageJson.repository && packageJson.repository.type === "git", "package.json repository.type must be git");
131
+ assert(packageJson.repository && packageJson.repository.directory, "package.json repository.directory must point at this package");
132
+ assert(packageJson.repository && packageJson.repository.directory === path.relative(repoRoot, packageRoot), "package.json repository.directory must match package path");
133
+ assert(packageJson.publishConfig && packageJson.publishConfig.access === "public", "scoped npm package must publish with public access");
134
+ assert(packageJson.bugs && packageJson.bugs.url, "package.json must include bugs.url");
135
+ assert(packageJson.mcpName === serverJson.name, "package.json mcpName must match server.json name");
136
+ assert(repoUrl === serverRepoUrl, `package repository ${repoUrl} must match server.json repository ${serverRepoUrl}`);
137
+ assert(homepage === serverWebsite, `package homepage ${homepage} must match server.json websiteUrl ${serverWebsite}`);
138
+ assert(serverJson.version === packageJson.version, `package version ${packageJson.version} must match server.json version ${serverJson.version}`);
139
+
140
+ if (npmPackage) {
141
+ assert(npmPackage.identifier === packageJson.name, "server.json npm package identifier must match package name");
142
+ assert(npmPackage.version === packageJson.version, "server.json npm package version must match package version");
143
+ } else {
144
+ fail("server.json must include an npm package entry");
145
+ }
146
+
147
+ const publisherMeta = serverJson._meta && serverJson._meta["io.modelcontextprotocol.registry/publisher-provided"];
148
+ const distribution = publisherMeta && publisherMeta.distribution;
149
+ if (distribution) {
150
+ if (distribution.npm) {
151
+ assert(
152
+ distribution.npm === `${packageJson.name}@${packageJson.version}`,
153
+ `server.json distribution.npm must be ${packageJson.name}@${packageJson.version}`,
154
+ );
155
+ }
156
+ if (distribution.nodePackage) {
157
+ assert(distribution.nodePackage === packageJson.name, "server.json distribution.nodePackage must match package name");
158
+ }
159
+ }
160
+
161
+ for (const keyword of ["mcp", "mcp-server", "model-context-protocol"]) {
162
+ assert(Array.isArray(packageJson.keywords) && packageJson.keywords.includes(keyword), `package keywords must include ${keyword}`);
163
+ }
164
+
165
+ for (const field of ["bin/", "scripts/", "README.md", "LICENSE*", "package.json"]) {
166
+ assert(Array.isArray(packageJson.files) && packageJson.files.includes(field), `package files must include ${field}`);
167
+ }
168
+
169
+ assert(packageJson.scripts && packageJson.scripts.prepack === "node scripts/sync-readme.js", "package prepack must sync README/LICENSE files");
170
+ assert(packageJson.scripts && packageJson.scripts.prepublishOnly === "node scripts/check-package.js --release", "package prepublishOnly must run the release gate");
171
+ assert(packageJson.scripts && packageJson.scripts.check && packageJson.scripts.check.includes("node scripts/check-package.js"), "package check must run the package verifier");
172
+ }
173
+
174
+ function assertRuntimeScriptsDoNotEscapePackage() {
175
+ const runtimeScripts = [packageJson.scripts && packageJson.scripts.postinstall].filter(Boolean);
176
+ for (const script of runtimeScripts) {
177
+ assert(!script.includes("../"), `runtime npm script must not reference files outside the package: ${script}`);
178
+ }
179
+
180
+ const runtimeFiles = [];
181
+ if (packageJson.bin) {
182
+ for (const relative of Object.values(packageJson.bin)) {
183
+ runtimeFiles.push(relative);
184
+ }
185
+ }
186
+ for (const relative of ["scripts/install.js", "lib/platform.js"]) {
187
+ const absolute = path.join(packageRoot, relative);
188
+ if (fs.existsSync(absolute)) {
189
+ runtimeFiles.push(relative);
190
+ }
191
+ }
192
+
193
+ for (const relative of new Set(runtimeFiles)) {
194
+ const text = fs.readFileSync(path.join(packageRoot, relative), "utf8");
195
+ assert(!text.includes('"..", ".."'), `${relative} must not resolve paths outside the package at runtime`);
196
+ assert(!text.includes("'..', '..'"), `${relative} must not resolve paths outside the package at runtime`);
197
+ }
198
+
199
+ const installer = path.join(packageRoot, "scripts", "install.js");
200
+ if (fs.existsSync(installer)) {
201
+ const text = fs.readFileSync(installer, "utf8");
202
+ assert(text.includes("verifyChecksum"), "postinstall downloader must verify release checksums");
203
+ assert(text.includes("sha256"), "postinstall downloader must validate SHA-256 checksums");
204
+ }
205
+ }
206
+
207
+ function packTarball(tempDir) {
208
+ run("npm", ["pack", packageRoot, "--pack-destination", tempDir], { cwd: packageRoot });
209
+ const tarballs = fs.readdirSync(tempDir).filter((entry) => entry.endsWith(".tgz"));
210
+ assert(tarballs.length === 1, "npm pack must produce exactly one tarball");
211
+ return path.join(tempDir, tarballs[0]);
212
+ }
213
+
214
+ function tarList(tarball) {
215
+ return run("tar", ["-tzf", tarball]).stdout.trim().split(/\n+/).filter(Boolean);
216
+ }
217
+
218
+ function tarExtract(tarball, entry) {
219
+ return run("tar", ["-xOzf", tarball, entry]).stdout;
220
+ }
221
+
222
+ function checkPacklist(tarball) {
223
+ const entries = tarList(tarball);
224
+ const allowedPrefixes = ["package/bin/", "package/lib/", "package/scripts/"];
225
+ const allowedExact = new Set(["package/package.json", "package/README.md"]);
226
+
227
+ for (const license of repoLicenseFiles()) {
228
+ allowedExact.add(`package/${license}`);
229
+ }
230
+
231
+ for (const required of allowedExact) {
232
+ assert(entries.includes(required), `packed tarball missing ${required}`);
233
+ }
234
+
235
+ const forbiddenFragments = [
236
+ "node_modules",
237
+ "vendor/",
238
+ "test/",
239
+ ".env",
240
+ "Cargo.",
241
+ "target/",
242
+ "package-lock.json",
243
+ "pnpm-lock.yaml",
244
+ ".tgz",
245
+ ];
246
+
247
+ for (const entry of entries) {
248
+ const allowed = allowedExact.has(entry) || allowedPrefixes.some((prefix) => entry.startsWith(prefix));
249
+ assert(allowed, `packed tarball contains unexpected file ${entry}`);
250
+ for (const fragment of forbiddenFragments) {
251
+ assert(!entry.includes(fragment), `packed tarball must not contain ${entry}`);
252
+ }
253
+ }
254
+
255
+ assert(
256
+ Buffer.from(tarExtract(tarball, "package/README.md")).equals(fs.readFileSync(path.join(repoRoot, "README.md"))),
257
+ "packed README.md must match repo README.md",
258
+ );
259
+
260
+ for (const license of repoLicenseFiles()) {
261
+ assert(
262
+ Buffer.from(tarExtract(tarball, `package/${license}`)).equals(fs.readFileSync(path.join(repoRoot, license))),
263
+ `packed ${license} must match repo ${license}`,
264
+ );
265
+ }
266
+ }
267
+
268
+ function writeSmokeBinary(destination) {
269
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
270
+ fs.writeFileSync(
271
+ destination,
272
+ "#!/usr/bin/env node\nprocess.stdout.write(`package-smoke-ok ${process.argv.slice(2).join(\" \")}\\n`);\n",
273
+ { mode: 0o755 },
274
+ );
275
+ fs.chmodSync(destination, 0o755);
276
+ }
277
+
278
+ function checkInstalledBins(installRoot) {
279
+ const installedPackageRoot = path.join(installRoot, "node_modules", packageJson.name);
280
+ assert(fs.existsSync(installedPackageRoot), "tarball install must create package under node_modules");
281
+
282
+ const env = { ...process.env };
283
+ if (packageJson.name === "soma-rmcp") {
284
+ const fakeBinary = path.join(installRoot, "fake-soma");
285
+ writeSmokeBinary(fakeBinary);
286
+ env.SOMA_BIN = fakeBinary;
287
+ } else {
288
+ const platformPath = path.join(installedPackageRoot, "lib", "platform.js");
289
+ assert(fs.existsSync(platformPath), "downloaded-binary packages must include lib/platform.js");
290
+ if (fs.existsSync(platformPath)) {
291
+ const platform = require(platformPath);
292
+ writeSmokeBinary(platform.binaryPath());
293
+ }
294
+ }
295
+
296
+ for (const [name, relative] of Object.entries(packageJson.bin || {})) {
297
+ const binPath = path.join(installedPackageRoot, relative);
298
+ const result = spawnSync(process.execPath, [binPath, "--package-smoke"], {
299
+ cwd: installRoot,
300
+ env,
301
+ encoding: "utf8",
302
+ stdio: ["ignore", "pipe", "pipe"],
303
+ });
304
+ const output = `${result.stdout || ""}${result.stderr || ""}`;
305
+ assert(result.status === 0, `${name} bin smoke exited ${result.status}: ${output.trim()}`);
306
+ assert(output.includes("package-smoke-ok --package-smoke"), `${name} bin smoke did not invoke the installed binary`);
307
+ }
308
+ }
309
+
310
+ function checkTarballInstallSmoke(tarball, tempDir) {
311
+ const installRoot = path.join(tempDir, "consumer");
312
+ fs.mkdirSync(installRoot, { recursive: true });
313
+ run("npm", ["install", "--ignore-scripts", "--prefix", installRoot, tarball], { cwd: tempDir });
314
+ checkInstalledBins(installRoot);
315
+ }
316
+
317
+ function supportedTargets(platform) {
318
+ const tuples = [
319
+ ["linux", "x64"],
320
+ ["win32", "x64"],
321
+ ["darwin", "x64"],
322
+ ["linux", "ppc64"],
323
+ ];
324
+ const targets = [];
325
+ for (const [osName, arch] of tuples) {
326
+ try {
327
+ const target = platform.targetFor(osName, arch);
328
+ targets.push({ osName, arch, target });
329
+ } catch (_) {
330
+ // Unsupported target; ignore.
331
+ }
332
+ }
333
+ return targets;
334
+ }
335
+
336
+ function requestHead(url, redirects = 0) {
337
+ return new Promise((resolve, reject) => {
338
+ const request = https.request(url, { method: "HEAD" }, (response) => {
339
+ if ([301, 302, 303, 307, 308].includes(response.statusCode) && response.headers.location) {
340
+ response.resume();
341
+ if (redirects > 8) {
342
+ reject(new Error(`too many redirects for ${url}`));
343
+ return;
344
+ }
345
+ requestHead(new URL(response.headers.location, url).toString(), redirects + 1).then(resolve, reject);
346
+ return;
347
+ }
348
+
349
+ response.resume();
350
+ resolve(response.statusCode);
351
+ });
352
+
353
+ request.setTimeout(15000, () => {
354
+ request.destroy(new Error(`timeout checking ${url}`));
355
+ });
356
+ request.on("error", reject);
357
+ request.end();
358
+ });
359
+ }
360
+
361
+ function requestText(url, redirects = 0) {
362
+ return new Promise((resolve, reject) => {
363
+ https
364
+ .get(url, (response) => {
365
+ if ([301, 302, 303, 307, 308].includes(response.statusCode) && response.headers.location) {
366
+ response.resume();
367
+ if (redirects > 8) {
368
+ reject(new Error(`too many redirects for ${url}`));
369
+ return;
370
+ }
371
+ requestText(new URL(response.headers.location, url).toString(), redirects + 1).then(resolve, reject);
372
+ return;
373
+ }
374
+
375
+ if (response.statusCode < 200 || response.statusCode >= 300) {
376
+ response.resume();
377
+ reject(new Error(`${url} returned ${response.statusCode}`));
378
+ return;
379
+ }
380
+
381
+ let body = "";
382
+ response.setEncoding("utf8");
383
+ response.on("data", (chunk) => {
384
+ body += chunk;
385
+ });
386
+ response.on("end", () => resolve(body));
387
+ })
388
+ .on("error", reject)
389
+ .setTimeout(15000, function onTimeout() {
390
+ this.destroy(new Error(`timeout checking ${url}`));
391
+ });
392
+ });
393
+ }
394
+
395
+ function checksumManifestUrl(assetUrl) {
396
+ return assetUrl.replace(/\/[^/]+$/, "/SHA256SUMS");
397
+ }
398
+
399
+ function checksumEntries(text) {
400
+ return text
401
+ .trim()
402
+ .split(/\r?\n/)
403
+ .map((line) => {
404
+ const parts = line.trim().split(/\s+/);
405
+ return {
406
+ hash: parts[0] && parts[0].toLowerCase(),
407
+ name: parts.slice(1).join(" ").replace(/^\*/, ""),
408
+ };
409
+ });
410
+ }
411
+
412
+ function localChecksumMatches(assetPath, asset) {
413
+ const digest = crypto.createHash("sha256").update(fs.readFileSync(assetPath)).digest("hex");
414
+ const checksumFiles = [
415
+ `${assetPath}.sha256`,
416
+ path.join(path.dirname(assetPath), "SHA256SUMS"),
417
+ ];
418
+
419
+ return checksumFiles.some((checksumFile) => {
420
+ if (!fs.existsSync(checksumFile)) {
421
+ return false;
422
+ }
423
+ return checksumEntries(fs.readFileSync(checksumFile, "utf8")).some(
424
+ (entry) =>
425
+ /^[a-f0-9]{64}$/.test(entry.hash) &&
426
+ path.basename(entry.name) === asset &&
427
+ entry.hash === digest,
428
+ );
429
+ });
430
+ }
431
+
432
+ async function hasChecksumFor(url, asset) {
433
+ const sidecarStatus = await requestHead(`${url}.sha256`);
434
+ if (sidecarStatus >= 200 && sidecarStatus < 300) {
435
+ return true;
436
+ }
437
+
438
+ const manifestUrl = checksumManifestUrl(url);
439
+ const manifest = await requestText(manifestUrl);
440
+ return manifest
441
+ .trim()
442
+ .split(/\r?\n/)
443
+ .some((line) => {
444
+ const parts = line.trim().split(/\s+/);
445
+ const hash = parts[0] && parts[0].toLowerCase();
446
+ const name = parts.slice(1).join(" ").replace(/^\*/, "");
447
+ return /^[a-f0-9]{64}$/.test(hash) && path.basename(name) === asset;
448
+ });
449
+ }
450
+
451
+ async function checkReleaseAssets() {
452
+ if (skipReleaseAssets || !releaseMode) {
453
+ return;
454
+ }
455
+
456
+ const platformPath = path.join(packageRoot, "lib", "platform.js");
457
+ if (!fs.existsSync(platformPath)) {
458
+ return;
459
+ }
460
+
461
+ const platform = require(platformPath);
462
+ if (typeof platform.downloadUrl !== "function" || typeof platform.targetFor !== "function") {
463
+ return;
464
+ }
465
+
466
+ if (releaseAssetDir) {
467
+ for (const { osName, arch, target } of supportedTargets(platform)) {
468
+ const assetPath = path.join(releaseAssetDir, target.asset);
469
+ assert(
470
+ fs.existsSync(assetPath),
471
+ `local release asset missing for ${osName}/${arch}: ${assetPath}`,
472
+ );
473
+ if (fs.existsSync(assetPath)) {
474
+ assert(
475
+ localChecksumMatches(assetPath, target.asset),
476
+ `local release checksum missing or invalid for ${osName}/${arch}: ${target.asset}`,
477
+ );
478
+ }
479
+ }
480
+ return;
481
+ }
482
+
483
+ for (const { osName, arch, target } of supportedTargets(platform)) {
484
+ const url = platform.downloadUrl(target);
485
+ const status = await requestHead(url);
486
+ assert(status >= 200 && status < 300, `release asset missing for ${osName}/${arch}: ${url} returned ${status}`);
487
+ assert(await hasChecksumFor(url, target.asset), `release checksum missing for ${osName}/${arch}: ${target.asset}`);
488
+ }
489
+ }
490
+
491
+ async function main() {
492
+ checkSyncedFiles();
493
+ checkMetadata();
494
+ assertRuntimeScriptsDoNotEscapePackage();
495
+
496
+ const packageTempLabel = packageJson.name.replace(/[^A-Za-z0-9._-]/g, "-");
497
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `${packageTempLabel}-package-check-`));
498
+ try {
499
+ const tarball = packTarball(tempDir);
500
+ checkPacklist(tarball);
501
+ checkTarballInstallSmoke(tarball, tempDir);
502
+ } finally {
503
+ fs.rmSync(tempDir, { recursive: true, force: true });
504
+ }
505
+
506
+ await checkReleaseAssets();
507
+
508
+ if (failures.length > 0) {
509
+ for (const failure of failures) {
510
+ process.stderr.write(`package-check: ${failure}\n`);
511
+ }
512
+ process.exitCode = 1;
513
+ return;
514
+ }
515
+
516
+ process.stdout.write(`package-check: ${packageJson.name} ok\n`);
517
+ }
518
+
519
+ if (require.main === module) {
520
+ main().catch((error) => {
521
+ process.stderr.write(`package-check: ${error.stack || error.message}\n`);
522
+ process.exitCode = 1;
523
+ });
524
+ }
525
+
526
+ module.exports = {
527
+ checksumEntries,
528
+ localChecksumMatches,
529
+ };