no-mistakes 0.55.0 → 0.56.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/README.md CHANGED
@@ -8,6 +8,10 @@ coverage gate. See [why it exists](../../docs/why.md).
8
8
  The async N-API facade avoids subprocess parsing and reuses one prepared
9
9
  analysis for related reports.
10
10
 
11
+ Published installs select a platform-specific optional package containing both
12
+ the CLI and N-API addon. Repository contributors build and stage the local
13
+ native package explicitly with `pnpm run build:native` before running the CLI.
14
+
11
15
  ```bash
12
16
  npm install --save-dev no-mistakes
13
17
  npx no-mistakes dependencies src/main.mts --json
@@ -2,8 +2,8 @@
2
2
  "use strict";
3
3
 
4
4
  const { spawn } = require("node:child_process");
5
- const { join } = require("node:path");
6
5
  const { boundedDiagnostic } = require("../planning-impact-cli");
6
+ const { resolveNativePackage } = require("../scripts/native-package");
7
7
 
8
8
  const VALUE_OPTIONS = new Set(["--timeout", "--lock-timeout", "--jobs", "--profile", "-j"]);
9
9
  const FLAG_OPTIONS = new Set(["--fail-on-lock"]);
@@ -35,8 +35,22 @@ function planningImpactArgs(argv) {
35
35
  return [...argv.slice(0, index), ...argv.slice(index + 1)];
36
36
  }
37
37
 
38
- function launchNative(argv, spawnFn = spawn, io = process, kill = process.kill) {
39
- const child = spawnFn(join(__dirname, "no-mistakes"), argv, { stdio: "inherit" });
38
+ function launchNative(
39
+ argv,
40
+ spawnFn = spawn,
41
+ io = process,
42
+ kill = process.kill,
43
+ resolve = resolveNativePackage,
44
+ ) {
45
+ let cliPath;
46
+ try {
47
+ cliPath = resolve().cliPath;
48
+ } catch (error) {
49
+ io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
50
+ io.exitCode = 1;
51
+ return undefined;
52
+ }
53
+ const child = spawnFn(cliPath, argv, { stdio: "inherit" });
40
54
  child.on("error", (error) => {
41
55
  io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
42
56
  io.exitCode = 1;
package/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
 
3
- // CI real-addon tests point this at the freshly compiled cdylib without
4
- // overwriting the package's checked-in install placeholder.
5
- const native = require(process.env.NO_MISTAKES_TEST_NAPI_ADDON_PATH || "./bin/no-mistakes.node");
3
+ const { resolveNativePackage } = require("./scripts/native-package");
4
+ const addonPath = process.env.NO_MISTAKES_TEST_NAPI_ADDON_PATH || resolveNativePackage().addonPath;
5
+ const native = require(addonPath);
6
6
  const planning = require("./planning");
7
7
  const { writePlanningImpactArtifacts: writeArtifacts } = require("./planning-impact-artifacts");
8
8
  const { createPlanningArtifactLock } = require("./planning-impact-artifacts-lock");
@@ -150,9 +150,7 @@ async function ciTopology(options) {
150
150
  return pending.then((value) => structuredClone(value));
151
151
  }
152
152
 
153
- async function version() {
154
- return native.version();
155
- }
153
+ const version = () => native.version();
156
154
 
157
155
  module.exports.createWorkflowTopologyIndex = createWorkflowTopologyIndex;
158
156
  module.exports.version = version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "no-mistakes",
3
- "version": "0.55.0",
3
+ "version": "0.56.0",
4
4
  "description": "Deterministic impact maps, test plans, Playwright coverage, and repository checks for coding agents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -26,8 +26,7 @@
26
26
  "planning.js",
27
27
  "workflow-topology-index.js",
28
28
  "workflow-topology-index-helpers.js",
29
- "scripts/install.js",
30
- "scripts/install/",
29
+ "scripts/native-package.js",
31
30
  "README.md",
32
31
  "LICENSE"
33
32
  ],
@@ -45,7 +44,11 @@
45
44
  "publishConfig": {
46
45
  "access": "public"
47
46
  },
48
- "scripts": {
49
- "postinstall": "node scripts/install.js"
47
+ "optionalDependencies": {
48
+ "no-mistakes-darwin-arm64": "0.56.0",
49
+ "no-mistakes-darwin-x64": "0.56.0",
50
+ "no-mistakes-linux-arm64-gnu": "0.56.0",
51
+ "no-mistakes-linux-x64-gnu": "0.56.0",
52
+ "no-mistakes-win32-x64-msvc": "0.56.0"
50
53
  }
51
- }
54
+ }
package/planning.js CHANGED
@@ -3,7 +3,10 @@
3
3
  const fs = require("node:fs/promises");
4
4
  const os = require("node:os");
5
5
  const path = require("node:path");
6
- const native = require(process.env.NO_MISTAKES_TEST_NAPI_ADDON_PATH || "./bin/no-mistakes.node");
6
+ const { resolveNativePackage } = require("./scripts/native-package");
7
+ const native = require(
8
+ process.env.NO_MISTAKES_TEST_NAPI_ADDON_PATH || resolveNativePackage().addonPath,
9
+ );
7
10
 
8
11
  async function callJson(fn, options) {
9
12
  const input = Buffer.from(JSON.stringify(options || {}));
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+
3
+ const { constants, existsSync, accessSync, statSync } = require("node:fs");
4
+ const { join } = require("node:path");
5
+
6
+ const MIN_GLIBC = [2, 35];
7
+
8
+ const PACKAGE_NAMES = new Map([
9
+ ["darwin/arm64", "no-mistakes-darwin-arm64"],
10
+ ["darwin/x64", "no-mistakes-darwin-x64"],
11
+ ["linux/arm64", "no-mistakes-linux-arm64-gnu"],
12
+ ["linux/x64", "no-mistakes-linux-x64-gnu"],
13
+ ["win32/x64", "no-mistakes-win32-x64-msvc"],
14
+ ]);
15
+
16
+ function nativePackageName(platform = process.platform, arch = process.arch) {
17
+ return PACKAGE_NAMES.get(`${platform}/${arch}`);
18
+ }
19
+
20
+ function unsupportedPlatformMessage(platform = process.platform, arch = process.arch) {
21
+ return `Unsupported platform ${platform}/${arch}. Install with \`cargo install no-mistakes\` instead.`;
22
+ }
23
+
24
+ function glibcVersion(report = process.report) {
25
+ try {
26
+ const header = report?.getReport?.().header;
27
+ return header?.glibcVersionRuntime || header?.glibcVersionCompiler || undefined;
28
+ } catch {
29
+ return undefined;
30
+ }
31
+ }
32
+
33
+ function supportedGlibc(report = process.report) {
34
+ const version = glibcVersion(report);
35
+ if (!version) return false;
36
+ const [major, minor] = version.split(".").map((part) => Number.parseInt(part, 10));
37
+ return (
38
+ Number.isInteger(major) &&
39
+ Number.isInteger(minor) &&
40
+ (major > MIN_GLIBC[0] || (major === MIN_GLIBC[0] && minor >= MIN_GLIBC[1]))
41
+ );
42
+ }
43
+
44
+ function unsupportedGlibcMessage(report = process.report) {
45
+ const version = glibcVersion(report);
46
+ const detected = version
47
+ ? `Detected glibc ${version}.`
48
+ : "Could not detect a supported glibc runtime.";
49
+ return `Linux native packages require glibc 2.35 or newer. ${detected} Install with \`cargo install no-mistakes\` instead.`;
50
+ }
51
+
52
+ function missingPackageMessage(name, { staged, unusableCli = false }) {
53
+ if (staged) {
54
+ return `The native package ${name} has ${unusableCli ? "an unusable CLI artifact" : "no staged artifacts"}. From the no-mistakes repository run \`pnpm run build:native\`.`;
55
+ }
56
+ return `The optional native package ${name} is ${unusableCli ? "not executable" : "unavailable"}. Run \`npm install no-mistakes\` or \`pnpm install\` to repair optional dependencies.`;
57
+ }
58
+
59
+ function localPackagePath(name) {
60
+ const path = join(__dirname, "..", "..", name);
61
+ return existsSync(join(path, "package.json")) ? path : undefined;
62
+ }
63
+
64
+ function usableCli(path, platform = process.platform) {
65
+ try {
66
+ if (!statSync(path).isFile()) return false;
67
+ if (platform === "win32") return true;
68
+ accessSync(path, constants.X_OK);
69
+ return true;
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ function resolveNativePackage({
76
+ platform = process.platform,
77
+ arch = process.arch,
78
+ resolve = require.resolve,
79
+ resolveLocalPackage = localPackagePath,
80
+ localArtifactExists = existsSync,
81
+ isUsableCli = usableCli,
82
+ report = process.report,
83
+ } = {}) {
84
+ const name = nativePackageName(platform, arch);
85
+ if (!name) throw new Error(unsupportedPlatformMessage(platform, arch));
86
+ if (platform === "linux" && !supportedGlibc(report)) {
87
+ throw new Error(unsupportedGlibcMessage(report));
88
+ }
89
+ let unusableCli = false;
90
+ try {
91
+ const cliPath = resolve(`${name}/bin/no-mistakes${platform === "win32" ? ".exe" : ""}`);
92
+ const addonPath = resolve(name);
93
+ if (isUsableCli(cliPath, platform)) return { name, cliPath, addonPath };
94
+ unusableCli = true;
95
+ } catch {
96
+ // The package may still be available as the repository's sibling staging package.
97
+ }
98
+ const local = resolveLocalPackage(name);
99
+ if (local) {
100
+ const cliPath = join(local, "bin", `no-mistakes${platform === "win32" ? ".exe" : ""}`);
101
+ const addonPath = join(local, "bin", "no-mistakes.node");
102
+ if (!localArtifactExists(cliPath) || !localArtifactExists(addonPath)) {
103
+ throw new Error(missingPackageMessage(name, { staged: true }));
104
+ }
105
+ if (!isUsableCli(cliPath, platform)) {
106
+ throw new Error(missingPackageMessage(name, { staged: true, unusableCli: true }));
107
+ }
108
+ return { name, cliPath, addonPath };
109
+ }
110
+ let installed = false;
111
+ try {
112
+ resolve(`${name}/package.json`);
113
+ installed = true;
114
+ } catch {}
115
+ throw new Error(
116
+ missingPackageMessage(name, { staged: false, unusableCli: installed && unusableCli }),
117
+ );
118
+ }
119
+
120
+ module.exports = {
121
+ glibcVersion,
122
+ nativePackageName,
123
+ localPackagePath,
124
+ resolveNativePackage,
125
+ supportedGlibc,
126
+ unsupportedGlibcMessage,
127
+ unsupportedPlatformMessage,
128
+ usableCli,
129
+ };
package/bin/no-mistakes DELETED
@@ -1 +0,0 @@
1
- Native binary placeholder. Replaced by scripts/install.js during npm postinstall.
@@ -1 +0,0 @@
1
- Native binary placeholder. Replaced by scripts/install.js during npm postinstall.
@@ -1,60 +0,0 @@
1
- "use strict";
2
-
3
- const { basename } = require("node:path");
4
-
5
- function assetName(binNameOrOptions, version, target, assetExtension) {
6
- if (
7
- typeof binNameOrOptions === "object" &&
8
- binNameOrOptions !== null &&
9
- !Array.isArray(binNameOrOptions)
10
- ) {
11
- return assetNameFromOptions(binNameOrOptions);
12
- }
13
- return assetNameFromLegacyArgs(binNameOrOptions, version, target, assetExtension);
14
- }
15
-
16
- function assetNameFromLegacyArgs(binName, version, target, assetExtension) {
17
- return assetNameFromOptions({ binName, version, target, assetExtension });
18
- }
19
-
20
- function assetNameFromOptions({ binName, version, target, assetExtension }) {
21
- if (typeof binName !== "string" || typeof version !== "string" || typeof target !== "string") {
22
- throw new TypeError(
23
- "assetName requires (binName, version, target) or an equivalent options object.",
24
- );
25
- }
26
-
27
- const ext = assetExtension ?? (target.endsWith("windows-msvc") ? ".exe" : "");
28
- return `${binName}-v${version}-${target}${ext}`;
29
- }
30
-
31
- function releaseBaseUrl(repository, version, envVar) {
32
- return (
33
- (envVar && process.env[envVar]) ||
34
- `https://github.com/${repository}/releases/download/v${version}`
35
- );
36
- }
37
-
38
- function parseChecksum(text, expectedAsset) {
39
- for (const line of text.split(/\r?\n/)) {
40
- const trimmed = line.trim();
41
- if (!trimmed) {
42
- continue;
43
- }
44
- const [hash, file] = trimmed.split(/\s+/, 2);
45
- if (!/^[a-fA-F0-9]{64}$/.test(hash)) {
46
- continue;
47
- }
48
- const normalizedFile = file?.replace(/^\*/, "");
49
- if (!file || normalizedFile === expectedAsset || basename(normalizedFile) === expectedAsset) {
50
- return hash.toLowerCase();
51
- }
52
- }
53
- throw new Error(`No SHA-256 checksum found for ${expectedAsset}`);
54
- }
55
-
56
- module.exports = {
57
- assetName,
58
- parseChecksum,
59
- releaseBaseUrl,
60
- };
@@ -1,167 +0,0 @@
1
- "use strict";
2
-
3
- const { copyFile, readFile } = require("node:fs/promises");
4
- const { createWriteStream } = require("node:fs");
5
- const http = require("node:http");
6
- const https = require("node:https");
7
- const { pipeline } = require("node:stream/promises");
8
- const { fileURLToPath } = require("node:url");
9
-
10
- const { HttpError, withRetry } = require("./retry");
11
-
12
- const DOWNLOAD_TIMEOUT_MS = 30_000;
13
-
14
- function download(url, destination, redirects = 0, validateUrl = () => {}, retryOptions) {
15
- validateUrl(url);
16
- if (isFileUrl(url)) {
17
- return copyFile(fileURLToPath(url), destination);
18
- }
19
-
20
- return withRetry(
21
- () =>
22
- request(
23
- url,
24
- async (response) => {
25
- await pipeline(response, createWriteStream(destination));
26
- },
27
- redirects,
28
- { http, https },
29
- DOWNLOAD_TIMEOUT_MS,
30
- validateUrl,
31
- ),
32
- { ...retryOptions, describe: () => `download of ${url}` },
33
- );
34
- }
35
-
36
- function request(
37
- url,
38
- handleResponse,
39
- redirects = 0,
40
- clients = { http, https },
41
- timeoutMs = DOWNLOAD_TIMEOUT_MS,
42
- validateUrl = () => {},
43
- ) {
44
- return new Promise((resolve, reject) => {
45
- try {
46
- validateUrl(url);
47
- } catch (error) {
48
- reject(error);
49
- return;
50
- }
51
- const client = url.startsWith("http://") ? clients.http : clients.https;
52
- const req = client.get(url, (response) => {
53
- if (isRedirectStatus(response.statusCode)) {
54
- response.resume();
55
- if (redirects >= 5) {
56
- reject(new Error(`Too many redirects while downloading ${url}`));
57
- return;
58
- }
59
- if (!response.headers.location) {
60
- reject(new Error(`Redirect missing Location header while downloading ${url}`));
61
- return;
62
- }
63
-
64
- let redirectedUrl;
65
- try {
66
- redirectedUrl = new URL(response.headers.location, url);
67
- } catch {
68
- reject(
69
- new Error(
70
- `Invalid redirect Location header while downloading ${url}: ${response.headers.location}`,
71
- ),
72
- );
73
- return;
74
- }
75
-
76
- if (redirectedUrl.protocol !== "http:" && redirectedUrl.protocol !== "https:") {
77
- reject(new Error(`Unsupported redirect protocol: ${redirectedUrl.protocol}`));
78
- return;
79
- }
80
-
81
- try {
82
- validateUrl(redirectedUrl.toString());
83
- } catch (error) {
84
- reject(error);
85
- return;
86
- }
87
-
88
- request(
89
- redirectedUrl.toString(),
90
- handleResponse,
91
- redirects + 1,
92
- clients,
93
- timeoutMs,
94
- validateUrl,
95
- ).then(resolve, reject);
96
- return;
97
- }
98
-
99
- if (response.statusCode !== 200) {
100
- response.resume();
101
- reject(new HttpError(url, response.statusCode));
102
- return;
103
- }
104
-
105
- Promise.resolve(handleResponse(response)).then(resolve, reject);
106
- });
107
-
108
- req.setTimeout(timeoutMs, () => {
109
- const timeoutError = new Error(`Download timed out after ${timeoutMs}ms: ${url}`);
110
- timeoutError.retryable = true;
111
- req.destroy(timeoutError);
112
- });
113
- req.on("error", reject);
114
- });
115
- }
116
-
117
- function isRedirectStatus(statusCode) {
118
- return [301, 302, 303, 307, 308].includes(statusCode);
119
- }
120
-
121
- async function fetchText(url, validateUrl = () => {}, retryOptions) {
122
- validateUrl(url);
123
-
124
- if (isFileUrl(url)) {
125
- return readFile(fileURLToPath(url), "utf8");
126
- }
127
- const MAX_LENGTH = 1024 * 1024;
128
- return withRetry(
129
- async () => {
130
- const chunks = [];
131
- let totalLength = 0;
132
- await request(
133
- url,
134
- async (response) => {
135
- for await (const chunk of response) {
136
- totalLength += chunk.length;
137
- if (totalLength > MAX_LENGTH) {
138
- throw new Error(`Response exceeded maximum size of ${MAX_LENGTH} bytes`);
139
- }
140
- chunks.push(chunk);
141
- }
142
- },
143
- 0,
144
- { http, https },
145
- DOWNLOAD_TIMEOUT_MS,
146
- validateUrl,
147
- );
148
- return Buffer.concat(chunks).toString("utf8");
149
- },
150
- { ...retryOptions, describe: () => `fetch of ${url}` },
151
- );
152
- }
153
-
154
- function isFileUrl(url) {
155
- try {
156
- return new URL(url).protocol === "file:";
157
- } catch {
158
- return false;
159
- }
160
- }
161
-
162
- module.exports = {
163
- download,
164
- fetchText,
165
- isRedirectStatus,
166
- request,
167
- };
@@ -1,15 +0,0 @@
1
- "use strict";
2
-
3
- const assets = require("./assets");
4
- const download = require("./download");
5
- const installer = require("./installer");
6
- const platform = require("./platform");
7
- const retry = require("./retry");
8
-
9
- module.exports = {
10
- ...assets,
11
- ...download,
12
- ...installer,
13
- ...platform,
14
- ...retry,
15
- };
@@ -1,218 +0,0 @@
1
- "use strict";
2
-
3
- const { createHash, randomBytes } = require("node:crypto");
4
- const { closeSync, createReadStream, existsSync, openSync, readSync } = require("node:fs");
5
- const { chmod, mkdir, rename, rm, writeFile } = require("node:fs/promises");
6
- const { join } = require("node:path");
7
-
8
- const { assetName, parseChecksum, releaseBaseUrl } = require("./assets");
9
- const { download, fetchText } = require("./download");
10
- const { platformTarget, supportedGlibc } = require("./platform");
11
- const { installedVersion, versionMarkerPath } = require("./version-marker");
12
-
13
- const PLACEHOLDER_TEXT = "Native binary placeholder.";
14
- const PLACEHOLDER_READ_BYTES = 256;
15
-
16
- async function sha256(path) {
17
- const hash = createHash("sha256");
18
- for await (const chunk of createReadStream(path)) {
19
- hash.update(chunk);
20
- }
21
- return hash.digest("hex");
22
- }
23
-
24
- async function install(binName, repository, options = {}) {
25
- const version = options.version;
26
- if (!version) {
27
- throw new Error("version is required for install()");
28
- }
29
- const target = Object.hasOwn(options, "target") ? options.target : platformTarget();
30
- if (!target) {
31
- throw new Error(unsupportedPlatformMessage(binName));
32
- }
33
-
34
- const vendorDir = options.vendorDir;
35
- if (!vendorDir) {
36
- throw new Error("vendorDir is required for install()");
37
- }
38
- const executable = options.destinationName || binName;
39
- const destination = join(vendorDir, executable);
40
- const destinationExists = existsSync(destination);
41
- const destinationIsPlaceholder = destinationExists && isPlaceholder(destination);
42
-
43
- if (process.env.SKIP_BINARY_DOWNLOAD) {
44
- if (destinationExists && !destinationIsPlaceholder) {
45
- return destination;
46
- }
47
- throw new Error(
48
- `SKIP_BINARY_DOWNLOAD is set but ${destination} is missing or still a placeholder; unset SKIP_BINARY_DOWNLOAD to install ${binName}.`,
49
- );
50
- }
51
-
52
- if (
53
- options.checkExisting &&
54
- destinationExists &&
55
- !destinationIsPlaceholder &&
56
- installedVersion(destination) === version
57
- ) {
58
- return destination;
59
- }
60
-
61
- const asset = assetName({ binName, version, target, assetExtension: options.assetExtension });
62
- const baseUrl = normalizeBaseUrl(
63
- options.baseUrl || releaseBaseUrl(repository, version, options.envVar),
64
- );
65
- validateReleaseBaseUrl(baseUrl, repository, { enforcePath: true });
66
- const validateReleaseDownloadUrl = (url) =>
67
- validateReleaseBaseUrl(url, repository, { enforcePath: false });
68
-
69
- const temp = `${destination}.tmp-${randomBytes(8).toString("hex")}`;
70
-
71
- await mkdir(vendorDir, { recursive: true });
72
-
73
- try {
74
- console.log(`Downloading ${binName} v${version} for ${target}...`);
75
- await download(`${baseUrl}/${asset}`, temp, 0, validateReleaseDownloadUrl);
76
-
77
- let checksumText;
78
- try {
79
- checksumText = await fetchText(`${baseUrl}/${asset}.sha256`, validateReleaseDownloadUrl);
80
- } catch (e) {
81
- throw new Error(`Failed to fetch checksum for ${asset}: ${e.message}`);
82
- }
83
-
84
- const expected = parseChecksum(checksumText, asset);
85
- const actual = await sha256(temp);
86
- if (actual !== expected) {
87
- throw new Error(`Checksum mismatch for ${asset}: expected ${expected}, got ${actual}`);
88
- }
89
- if (!target.endsWith("windows-msvc")) {
90
- await chmod(temp, 0o755);
91
- }
92
- await rename(temp, destination);
93
- await writeFile(versionMarkerPath(destination), version);
94
- return destination;
95
- } catch (error) {
96
- await rm(temp, { force: true });
97
- throw new Error(`Failed to install ${binName}: ${error.message}`);
98
- }
99
- }
100
-
101
- function normalizeBaseUrl(baseUrl) {
102
- const url = String(baseUrl);
103
- if (url.endsWith("://")) {
104
- return url;
105
- }
106
- return url.replace(/\/+$/, "");
107
- }
108
-
109
- function validateReleaseBaseUrl(baseUrl, repository, options = {}) {
110
- const enforcePath = options.enforcePath ?? true;
111
- const allowedPublicHost = "github.com";
112
- const allowedRedirectHostSuffixes = ["githubusercontent.com"];
113
- const allowedLocalHosts = ["127.0.0.1", "example.test"];
114
- const publicPathPrefix = `/${repository.toLowerCase()}/releases/download`;
115
-
116
- let parsedUrl;
117
- try {
118
- parsedUrl = new URL(baseUrl);
119
- } catch {
120
- throw new Error(`Invalid release base URL: ${baseUrl}. It must be a valid absolute URL.`);
121
- }
122
-
123
- if (parsedUrl.protocol === "file:") {
124
- if (
125
- parsedUrl.username ||
126
- parsedUrl.password ||
127
- parsedUrl.pathname.startsWith("//") ||
128
- !String(baseUrl).toLowerCase().startsWith("file:///")
129
- ) {
130
- throw new Error(
131
- `Untrusted base URL: ${baseUrl}. File URLs must use canonical 'file:///path/to/asset' form and must not include credentials.`,
132
- );
133
- }
134
- return;
135
- }
136
- if (parsedUrl.username || parsedUrl.password) {
137
- throw new Error(`Untrusted base URL: ${baseUrl}. Credentials are not allowed in URLs.`);
138
- }
139
-
140
- const hostname = parsedUrl.hostname.toLowerCase();
141
- const isLocalHost = allowedLocalHosts.includes(hostname);
142
- if (isLocalHost) {
143
- if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
144
- throw new Error(`Untrusted base URL: ${baseUrl}. Expected http: or https: protocol.`);
145
- }
146
- } else if (parsedUrl.protocol !== "https:") {
147
- throw new Error(`Untrusted base URL: ${baseUrl}. Expected https: protocol.`);
148
- }
149
-
150
- if (enforcePath && hostname !== allowedPublicHost && !isLocalHost) {
151
- throw new Error(
152
- `Untrusted base URL: ${baseUrl}. When enforcePath is enabled, expected base URL host ${allowedPublicHost} unless host is a local testing host.`,
153
- );
154
- }
155
-
156
- const isAllowedHost =
157
- hostname === allowedPublicHost ||
158
- allowedLocalHosts.includes(hostname) ||
159
- allowedRedirectHostSuffixes.some(
160
- (suffix) => hostname === suffix || hostname.endsWith(`.${suffix}`),
161
- );
162
- if (!isAllowedHost) {
163
- throw new Error(
164
- `Untrusted base URL: ${baseUrl}. Allowed hosts are: ${[allowedPublicHost, ...allowedLocalHosts, ...allowedRedirectHostSuffixes.map((suffix) => `*.${suffix}`)].join(", ")}.`,
165
- );
166
- }
167
-
168
- if (
169
- enforcePath &&
170
- !isLocalHost &&
171
- !parsedUrl.pathname.toLowerCase().startsWith(`${publicPathPrefix}/`)
172
- ) {
173
- throw new Error(
174
- `Untrusted GitHub repository in base URL: ${baseUrl}. For github.com, expected base URL prefix ${publicPathPrefix}.`,
175
- );
176
- }
177
- }
178
-
179
- function isPlaceholder(path) {
180
- let fd;
181
- try {
182
- fd = openSync(path, "r");
183
- const buffer = Buffer.alloc(PLACEHOLDER_READ_BYTES);
184
- const bytesRead = readSync(fd, buffer, 0, buffer.length, 0);
185
- return buffer.subarray(0, bytesRead).toString("utf8").includes(PLACEHOLDER_TEXT);
186
- } catch (error) {
187
- if (error.code === "ENOENT") {
188
- return false;
189
- }
190
- throw new Error(`Failed to inspect native binary placeholder ${path}: ${error.message}`);
191
- } finally {
192
- if (fd !== undefined) {
193
- closeSync(fd);
194
- }
195
- }
196
- }
197
-
198
- function unsupportedPlatformMessage(
199
- binName,
200
- platform = process.platform,
201
- arch = process.arch,
202
- report = process.report,
203
- ) {
204
- if (platform === "linux" && (arch === "x64" || arch === "arm64") && !supportedGlibc(report)) {
205
- return `Linux npm installs require glibc 2.35 or newer. Install with \`cargo install ${binName}\` instead.`;
206
- }
207
- return `Unsupported platform ${platform}/${arch}. Install with \`cargo install ${binName}\` instead.`;
208
- }
209
-
210
- module.exports = {
211
- install,
212
- installedVersion,
213
- isPlaceholder,
214
- sha256,
215
- unsupportedPlatformMessage,
216
- validateReleaseBaseUrl,
217
- versionMarkerPath,
218
- };
@@ -1,50 +0,0 @@
1
- "use strict";
2
-
3
- const MIN_GLIBC = [2, 35];
4
-
5
- function platformTarget(platform = process.platform, arch = process.arch, report = process.report) {
6
- if (platform === "darwin" && arch === "x64") {
7
- return "x86_64-apple-darwin";
8
- }
9
- if (platform === "darwin" && arch === "arm64") {
10
- return "aarch64-apple-darwin";
11
- }
12
- if (platform === "win32" && arch === "x64") {
13
- return "x86_64-pc-windows-msvc";
14
- }
15
- if (platform === "linux" && (arch === "x64" || arch === "arm64")) {
16
- if (!supportedGlibc(report)) {
17
- return null;
18
- }
19
- return arch === "x64" ? "x86_64-unknown-linux-gnu" : "aarch64-unknown-linux-gnu";
20
- }
21
- return null;
22
- }
23
-
24
- function isGlibc(report = process.report) {
25
- return glibcVersion(report) !== null;
26
- }
27
-
28
- function glibcVersion(report = process.report) {
29
- const header = typeof report?.getReport === "function" ? report.getReport().header : {};
30
- return header?.glibcVersionRuntime || header?.glibcVersionCompiler || null;
31
- }
32
-
33
- function supportedGlibc(report = process.report) {
34
- const version = glibcVersion(report);
35
- if (!version) {
36
- return false;
37
- }
38
- const [major, minor] = version.split(".").map((part) => Number.parseInt(part, 10));
39
- if (!Number.isInteger(major) || !Number.isInteger(minor)) {
40
- return false;
41
- }
42
- return major > MIN_GLIBC[0] || (major === MIN_GLIBC[0] && minor >= MIN_GLIBC[1]);
43
- }
44
-
45
- module.exports = {
46
- glibcVersion,
47
- isGlibc,
48
- platformTarget,
49
- supportedGlibc,
50
- };
@@ -1,100 +0,0 @@
1
- "use strict";
2
-
3
- // A GitHub Releases asset download or its separate .sha256 checksum fetch can
4
- // hit a short-lived CDN 5xx, timeout, or connection reset. These defaults give
5
- // each network operation a bounded multi-attempt window (~15s avg / ~31s worst
6
- // case total sleep) to ride that out, rather than exhausting a tiny budget in a
7
- // few seconds. Override via NO_MISTAKES_DOWNLOAD_MAX_ATTEMPTS /
8
- // NO_MISTAKES_DOWNLOAD_RETRY_BASE_MS.
9
- const DEFAULT_MAX_ATTEMPTS = 6;
10
- const DEFAULT_RETRY_BASE_MS = 1_000;
11
- const RETRY_MAX_DELAY_MS = 30_000;
12
- const RETRYABLE_NETWORK_CODES = new Set([
13
- "ECONNRESET",
14
- "ECONNREFUSED",
15
- "ETIMEDOUT",
16
- "EAI_AGAIN",
17
- "ENETUNREACH",
18
- "EPIPE",
19
- "ERR_STREAM_PREMATURE_CLOSE",
20
- ]);
21
-
22
- class HttpError extends Error {
23
- constructor(url, statusCode) {
24
- super(`Download failed for ${url}: HTTP ${statusCode}`);
25
- this.name = "HttpError";
26
- this.statusCode = statusCode;
27
- this.retryable = isRetryableStatus(statusCode);
28
- }
29
- }
30
-
31
- function isRetryableStatus(statusCode) {
32
- if (typeof statusCode !== "number") return false;
33
- if (statusCode >= 500 && statusCode < 600) return true;
34
- return statusCode === 408 || statusCode === 429;
35
- }
36
-
37
- function isRetryableError(error) {
38
- if (!error) return false;
39
- if (error.retryable === true) return true;
40
- if (error.code && RETRYABLE_NETWORK_CODES.has(error.code)) return true;
41
- return false;
42
- }
43
-
44
- function sleep(ms) {
45
- return new Promise((resolve) => setTimeout(resolve, ms));
46
- }
47
-
48
- function parsePositiveInt(value, fallback) {
49
- if (value === undefined || value === null || value === "") return fallback;
50
- const n = Number(value);
51
- if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) return fallback;
52
- return n;
53
- }
54
-
55
- function computeBackoffMs(attempt, baseDelayMs, random = Math.random) {
56
- const exponential = baseDelayMs * 2 ** (attempt - 1);
57
- const capped = Math.min(exponential, RETRY_MAX_DELAY_MS);
58
- return Math.floor(random() * capped);
59
- }
60
-
61
- async function withRetry(fn, options = {}) {
62
- const maxAttempts = parsePositiveInt(
63
- options.maxAttempts ?? process.env.NO_MISTAKES_DOWNLOAD_MAX_ATTEMPTS,
64
- DEFAULT_MAX_ATTEMPTS,
65
- );
66
- const baseDelayMs = parsePositiveInt(
67
- options.baseDelayMs ?? process.env.NO_MISTAKES_DOWNLOAD_RETRY_BASE_MS,
68
- DEFAULT_RETRY_BASE_MS,
69
- );
70
- const delay = options.delay ?? sleep;
71
- const random = options.random ?? Math.random;
72
- const logger = options.logger ?? console;
73
- const describe = options.describe ?? (() => "download");
74
-
75
- let attempt = 0;
76
- for (;;) {
77
- attempt += 1;
78
- try {
79
- return await fn();
80
- } catch (error) {
81
- if (attempt >= maxAttempts || !isRetryableError(error)) {
82
- throw error;
83
- }
84
- const waitMs = computeBackoffMs(attempt, baseDelayMs, random);
85
- logger.warn(
86
- `no-mistakes: retrying ${describe()} after ${error.message} (attempt ${attempt + 1}/${maxAttempts}, waiting ~${waitMs}ms)`,
87
- );
88
- await delay(waitMs);
89
- }
90
- }
91
- }
92
-
93
- module.exports = {
94
- HttpError,
95
- computeBackoffMs,
96
- isRetryableError,
97
- isRetryableStatus,
98
- parsePositiveInt,
99
- withRetry,
100
- };
@@ -1,30 +0,0 @@
1
- "use strict";
2
-
3
- const { readFileSync } = require("node:fs");
4
-
5
- function versionMarkerPath(destination) {
6
- return `${destination}.version`;
7
- }
8
-
9
- // The destination binary alone can't tell us which version it is (the CLI's
10
- // own --version requires spawning a process, and the N-API .node addon isn't
11
- // executable at all), so track the installed version in a small sidecar file
12
- // written right after a successful install. A missing or mismatched marker
13
- // means we can't prove the existing file matches the requested version, so
14
- // checkExisting must fall through to a real (re)download rather than trust
15
- // stale bytes left behind by an earlier install of a different version.
16
- function installedVersion(destination) {
17
- try {
18
- return readFileSync(versionMarkerPath(destination), "utf8").trim();
19
- } catch (error) {
20
- if (error.code === "ENOENT") {
21
- return null;
22
- }
23
- throw error;
24
- }
25
- }
26
-
27
- module.exports = {
28
- installedVersion,
29
- versionMarkerPath,
30
- };
@@ -1,38 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
-
4
- const { join } = require("node:path");
5
- const { install } = require("./install/index");
6
-
7
- const PACKAGE_ROOT = join(__dirname, "..");
8
-
9
- async function main(installFn = install, io = process, logger = console) {
10
- try {
11
- const pkg = require(join(PACKAGE_ROOT, "package.json"));
12
- const binDir = join(PACKAGE_ROOT, "bin");
13
- const binaryDestination = await installFn("no-mistakes", "jonathanong/no-mistakes", {
14
- version: pkg.version,
15
- vendorDir: binDir,
16
- destinationName: "no-mistakes",
17
- envVar: "NO_MISTAKES_RELEASE_BASE_URL",
18
- checkExisting: true,
19
- });
20
- const addonDestination = await installFn("no-mistakes-napi", "jonathanong/no-mistakes", {
21
- version: pkg.version,
22
- vendorDir: binDir,
23
- destinationName: "no-mistakes.node",
24
- assetExtension: ".node",
25
- envVar: "NO_MISTAKES_RELEASE_BASE_URL",
26
- checkExisting: true,
27
- });
28
- logger.log(`Installed no-mistakes native binary to ${binaryDestination}`);
29
- logger.log(`Installed no-mistakes N-API addon to ${addonDestination}`);
30
- } catch (error) {
31
- logger.error(error instanceof Error ? error.message : String(error));
32
- io.exit(1);
33
- }
34
- }
35
-
36
- if (require.main === module) main();
37
-
38
- module.exports = { main };