@cofferdam/cofferdam 0.2.3-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 cofferdam contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,197 @@
1
+ # cofferdam
2
+
3
+ TypeScript code-quality analyzer with a Rust core. Sorts findings by
4
+ priority across five categories (Consistency, Design, Readability,
5
+ Refactor, Warning) and gates CI on a configurable severity axis.
6
+ Inspired by Elixir's [Credo](https://github.com/rrrene/credo).
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ pnpm add -D cofferdam # pnpm
12
+ npm install -D cofferdam # npm
13
+ yarn add -D cofferdam # yarn
14
+ ```
15
+
16
+ The package downloads a pre-built binary for your platform on install
17
+ (Linux x64/arm64 gnu+musl, macOS x64/arm64, Windows x64) and runs it
18
+ through a tiny JS shim.
19
+
20
+ > **pnpm users:** pnpm v10's default sandbox blocks postinstall scripts
21
+ > unless the package is on the allowlist. `pnpm add -D cofferdam` will
22
+ > "succeed" without ever downloading the binary. Add this to your
23
+ > `package.json` so the binary install actually runs:
24
+ >
25
+ > ```json
26
+ > { "pnpm": { "onlyBuiltDependencies": ["cofferdam"] } }
27
+ > ```
28
+ >
29
+ > Then re-run `pnpm install` (or `pnpm rebuild cofferdam` for an existing
30
+ > install). Verified you're hit by this if `pnpm exec cofferdam --version`
31
+ > errors with "binary not found".
32
+
33
+ ## First run
34
+
35
+ ```bash
36
+ pnpm exec cofferdam init
37
+ ```
38
+
39
+ `init` writes three things and asks once whether to capture a baseline
40
+ of your current findings:
41
+
42
+ - **`cofferdam.toml`** — every check stanza commented out so you can
43
+ see what's tunable. Edit only the values you care about; defaults
44
+ cover the rest.
45
+ - **`.cofferdam/baseline.json`** — snapshot of findings the build
46
+ should *not* fail on (your existing tech debt). Commit this file.
47
+ - **`.gitignore`** — `.cofferdam/` is added with a
48
+ `!.cofferdam/baseline.json` negation, so the baseline stays
49
+ tracked while future cache content is ignored.
50
+
51
+ After `init`:
52
+
53
+ ```bash
54
+ pnpm exec cofferdam check src/ # exits 0 — everything baselined
55
+ ```
56
+
57
+ Add a fresh `==` to your code, re-run, and CI will fail with only the
58
+ new finding flagged.
59
+
60
+ ## Built-in checks
61
+
62
+ Cofferdam ships 20+ built-in checks across all five categories,
63
+ including project-graph rules (`Design.OrphanExport`,
64
+ `Design.ImportCycle`, `Design.LayerViolation`, `Refactor.DeadExport`)
65
+ and complexity rules (`Refactor.CyclomaticComplexity`,
66
+ `Refactor.CognitiveComplexity`). Severity gates CI; priority sorts
67
+ the report — the two are deliberately separate axes.
68
+
69
+ Full catalog with bad/good examples, options, and per-check defaults:
70
+ **<https://tajd.github.io/cofferdam/checks/>**.
71
+
72
+ ## CI integration
73
+
74
+ ### GitHub Actions
75
+
76
+ ```yaml
77
+ - uses: actions/checkout@v6
78
+ with:
79
+ # fetch-depth: 0 so --since can resolve the base branch ref
80
+ fetch-depth: 0
81
+ - run: pnpm install --frozen-lockfile
82
+ - run: pnpm exec cofferdam check src/ --since origin/${{ github.base_ref }} --fail-on=high
83
+ ```
84
+
85
+ `--since <git-ref>` runs only against files changed in `<ref>...HEAD`,
86
+ so PR checks stay fast on large repos. `--fail-on=high` exits 1 only
87
+ when at least one finding is High or Critical — Medium and below
88
+ print but don't fail CI.
89
+
90
+ ### Husky pre-commit
91
+
92
+ ```bash
93
+ # .husky/pre-commit
94
+ pnpm exec cofferdam check --since HEAD --fail-on=high
95
+ ```
96
+
97
+ ## Configuration
98
+
99
+ `cofferdam.toml` at the project root. Discovered by walking up from the
100
+ working directory until a `.git` is reached. Every key is optional —
101
+ unset values fall back to the defaults.
102
+
103
+ ```toml
104
+ # Lower a check's severity so it stops failing CI but still appears in reports
105
+ [checks."Refactor.CyclomaticComplexity"]
106
+ severity = "low"
107
+
108
+ # Tighten a limit
109
+ [checks."Readability.MaxLineLength"]
110
+ limit = 100
111
+ ```
112
+
113
+ Override per invocation: `--config <path>` points at a specific file,
114
+ `--no-config` skips discovery entirely.
115
+
116
+ ## Suppression
117
+
118
+ Inline directives let you silence a finding with an auditable reason
119
+ field. Canonical form:
120
+
121
+ ```ts
122
+ // cofferdam-ignore: Warning.NoEval: codegen bootstrap, not user input
123
+ eval(generatedCode);
124
+ ```
125
+
126
+ Range and file-scoped variants:
127
+
128
+ ```ts
129
+ // cofferdam-ignore-start: Refactor.CyclomaticComplexity
130
+ function generatedRouter(req, res) { /* ... */ }
131
+ // cofferdam-ignore-end
132
+
133
+ // cofferdam-ignore-file: Readability.MaxLineLength
134
+ ```
135
+
136
+ ESLint-style aliases (`// cofferdam-disable-next-line <CheckId>`,
137
+ `/* cofferdam-disable */ ... /* cofferdam-enable */`) are also
138
+ recognised for ergonomic continuity. Full syntax and reason-field
139
+ rules: <https://tajd.github.io/cofferdam/suppression/>.
140
+
141
+ ## Custom checks
142
+
143
+ Author project-specific checks in TypeScript with
144
+ [`@cofferdam/check-sdk`](https://www.npmjs.com/package/@cofferdam/check-sdk).
145
+ The `defineCheck` API gives you AST and line views over the same
146
+ sources cofferdam already parsed; the cofferdam binary spawns a Node
147
+ host and merges your findings into its stream. Plugin authoring guide:
148
+ <https://tajd.github.io/cofferdam/plugin-sdk/> (in progress).
149
+
150
+ ## Architectural specs
151
+
152
+ Pin the shape of your codebase in `cofferdam.invariants.toml` —
153
+ declare layer boundaries (`ui` cannot import `db`), freeze a public
154
+ surface (`packages/sdk` exports may not change without a deliberate
155
+ update), and let `Design.LayerViolation` and `Design.BoundaryFrozen`
156
+ fail CI on drift. Spec reference:
157
+ <https://tajd.github.io/cofferdam/invariants/>.
158
+
159
+ ## Exit codes
160
+
161
+ | Code | Meaning |
162
+ |------|---------|
163
+ | 0 | No findings at or above `--fail-on` (default: `medium`). Baselined findings never trigger the gate. |
164
+ | 1 | At least one finding triggers the gate. |
165
+ | 2 | Invocation, IO, or config error. |
166
+
167
+ ## Sandboxed installs / `--ignore-scripts`
168
+
169
+ If your installer disables postinstall scripts, the binary won't be
170
+ downloaded. Two recovery paths:
171
+
172
+ 1. **Manual binary** — download the release archive from
173
+ [GitHub Releases](https://github.com/TAJD/cofferdam/releases),
174
+ extract it, set `COFFERDAM_BINARY_PATH` to the binary, then
175
+ `npm rebuild cofferdam`.
176
+ 2. **Pre-baked image** — set `COFFERDAM_SKIP_DOWNLOAD=1` if the
177
+ binary is already at `node_modules/cofferdam/bin/cofferdam`
178
+ (or `cofferdam.exe` on Windows).
179
+
180
+ > **Windows + npm 6:** bare `npx cofferdam` falls back to `npm run` and
181
+ > fails with `Missing script: 'cofferdam'`. Use `npx -p cofferdam cofferdam`,
182
+ > `pnpm exec cofferdam`, or `.\node_modules\.bin\cofferdam.cmd`, or
183
+ > upgrade to npm ≥ 7.
184
+
185
+ ## Versioning
186
+
187
+ The npm package version tracks the cofferdam release version.
188
+ `cofferdam@0.1.0` downloads the binary from the `v0.1.0` GitHub
189
+ Release. Lockfile-pinned installs are deterministic. The Rust
190
+ workspace and the `cofferdam` + `@cofferdam/check-sdk` npm packages
191
+ are released in lockstep — a `cofferdam@X.Y.Z` install always pairs
192
+ with an SDK at the same `X.Y.Z`.
193
+
194
+ ## License
195
+
196
+ MIT. Full source and project documentation:
197
+ [github.com/TAJD/cofferdam](https://github.com/TAJD/cofferdam).
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ // Tiny shim: forward all argv + stdio to the platform-specific binary that
3
+ // postinstall placed alongside this script.
4
+
5
+ 'use strict';
6
+
7
+ const { spawnSync } = require('child_process');
8
+ const path = require('path');
9
+ const fs = require('fs');
10
+
11
+ const binaryName = process.platform === 'win32' ? 'cofferdam.exe' : 'cofferdam';
12
+ const binaryPath = path.join(__dirname, binaryName);
13
+
14
+ if (!fs.existsSync(binaryPath)) {
15
+ // Tailor the recovery hint to the package manager. pnpm v10's default
16
+ // sandbox silently blocks postinstall unless the package is in
17
+ // `pnpm.onlyBuiltDependencies`, so a `pnpm add -D cofferdam` 'succeeds'
18
+ // with no binary on disk. Recommending `npm rebuild` to those users is
19
+ // wrong — they need pnpm syntax + the allowlist edit (cd-iui / gh #9).
20
+ const pkgManager = detectPackageManager();
21
+ let recovery;
22
+ if (pkgManager === 'pnpm') {
23
+ recovery =
24
+ "Looks like a pnpm install. pnpm v10 blocks postinstall scripts unless\n" +
25
+ "the package is in `pnpm.onlyBuiltDependencies`. To fix:\n\n" +
26
+ ' 1. Add to your package.json:\n' +
27
+ ' { "pnpm": { "onlyBuiltDependencies": ["cofferdam"] } }\n' +
28
+ ' 2. Then run: pnpm rebuild cofferdam\n';
29
+ } else if (pkgManager === 'yarn') {
30
+ recovery =
31
+ 'Looks like a yarn install. Run:\n' +
32
+ ' yarn rebuild cofferdam\n';
33
+ } else {
34
+ recovery = 'Run:\n npm rebuild cofferdam\n';
35
+ }
36
+
37
+ console.error(
38
+ `[cofferdam] binary not found at ${binaryPath}\n\n` +
39
+ 'This usually means the postinstall step did not complete.\n\n' +
40
+ recovery + '\n' +
41
+ 'If you installed with --ignore-scripts (or your CI does), the binary\n' +
42
+ 'must be installed manually. See:\n' +
43
+ ' https://github.com/TAJD/cofferdam#install'
44
+ );
45
+ process.exit(1);
46
+ }
47
+
48
+ // Cheap heuristic: walk up from this script's location looking for the
49
+ // canonical lockfile / metadata each package manager leaves behind. Falls
50
+ // back to `process.env.npm_config_user_agent` when we're inside a fresh
51
+ // shell invocation that hasn't touched node_modules. Returns one of
52
+ // 'pnpm' | 'yarn' | 'npm' | null.
53
+ function detectPackageManager() {
54
+ const ua = process.env.npm_config_user_agent || '';
55
+ if (/^pnpm\//.test(ua)) return 'pnpm';
56
+ if (/^yarn\//.test(ua)) return 'yarn';
57
+ if (/^npm\//.test(ua)) return 'npm';
58
+
59
+ // Walk up from __dirname looking for tell-tale files. Most reliable:
60
+ // an adjacent `node_modules/.pnpm/` (pnpm's content-addressed store).
61
+ let dir = path.resolve(__dirname);
62
+ for (let i = 0; i < 20; i++) {
63
+ if (fs.existsSync(path.join(dir, 'node_modules', '.pnpm'))) return 'pnpm';
64
+ if (fs.existsSync(path.join(dir, 'pnpm-lock.yaml'))) return 'pnpm';
65
+ if (fs.existsSync(path.join(dir, 'yarn.lock'))) return 'yarn';
66
+ if (fs.existsSync(path.join(dir, 'package-lock.json'))) return 'npm';
67
+ const parent = path.dirname(dir);
68
+ if (parent === dir) break;
69
+ dir = parent;
70
+ }
71
+ return null;
72
+ }
73
+
74
+ // Guard against broken-pipe conditions (e.g. `cofferdam check --format=json | head`).
75
+ //
76
+ // On POSIX, when the consumer closes the pipe, the kernel sends SIGPIPE to the
77
+ // child process first (Rust's stdlib already handles it cleanly). The pnpm
78
+ // wrapper — this script — may still have its own writes fail with EPIPE (e.g.
79
+ // the error-not-found message path above, or any Node internal flush). Node 22+
80
+ // delivers SIGPIPE to non-default handlers; older versions silently terminate.
81
+ // Belt-and-suspenders: listen for both the signal and the stream error.
82
+ //
83
+ // On Windows there is no SIGPIPE signal; writes to a closed pipe throw EPIPE
84
+ // synchronously through the stream 'error' event. The error handlers cover that
85
+ // path — registering the signal handler on Windows is harmless (never fires).
86
+ process.on('SIGPIPE', () => process.exit(0));
87
+ process.stdout.on('error', (err) => { if (err.code === 'EPIPE') process.exit(0); else throw err; });
88
+ process.stderr.on('error', (err) => { if (err.code === 'EPIPE') process.exit(0); else throw err; });
89
+
90
+ const result = spawnSync(binaryPath, process.argv.slice(2), {
91
+ stdio: 'inherit',
92
+ windowsHide: false,
93
+ });
94
+
95
+ if (result.error) {
96
+ console.error(`[cofferdam] failed to spawn binary: ${result.error.message}`);
97
+ process.exit(1);
98
+ }
99
+
100
+ process.exit(result.status ?? 1);
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@cofferdam/cofferdam",
3
+ "version": "0.2.3-rc.0",
4
+ "description": "TypeScript code-quality analyzer — Rust core + JS plugin layer, inspired by Elixir's Credo",
5
+ "license": "MIT",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/TAJD/cofferdam.git",
12
+ "directory": "packages/cofferdam"
13
+ },
14
+ "homepage": "https://tajd.github.io/cofferdam",
15
+ "bugs": "https://github.com/TAJD/cofferdam/issues",
16
+ "keywords": [
17
+ "typescript",
18
+ "linter",
19
+ "static-analysis",
20
+ "code-quality",
21
+ "credo",
22
+ "oxc"
23
+ ],
24
+ "bin": {
25
+ "cofferdam": "bin/cofferdam.js"
26
+ },
27
+ "files": [
28
+ "bin/cofferdam.js",
29
+ "CHANGELOG.md",
30
+ "LICENSE",
31
+ "README.md",
32
+ "scripts/postinstall.js"
33
+ ],
34
+ "scripts": {
35
+ "postinstall": "node scripts/postinstall.js"
36
+ },
37
+ "engines": {
38
+ "node": ">=16",
39
+ "npm": ">=7"
40
+ },
41
+ "os": [
42
+ "linux",
43
+ "darwin",
44
+ "win32"
45
+ ],
46
+ "cpu": [
47
+ "x64",
48
+ "arm64"
49
+ ]
50
+ }
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env node
2
+ // cofferdam postinstall — downloads the matching binary from the GitHub Release
3
+ // for this package version. Stopgap until phase 4 ships @cofferdam/node via
4
+ // napi-rs prebuilt binaries through optionalDependencies.
5
+ //
6
+ // Zero runtime deps: uses only Node stdlib (https, fs, path, crypto, child_process).
7
+ // Designed to fail loudly with actionable error messages so users know exactly
8
+ // what went wrong when corp proxies, offline installs, or sandbox environments
9
+ // block the download.
10
+ //
11
+ // Override paths:
12
+ // - COFFERDAM_BINARY_PATH=/abs/path → skip download, use this binary
13
+ // - COFFERDAM_SKIP_DOWNLOAD=1 → skip postinstall entirely (CI / Docker layers)
14
+
15
+ 'use strict';
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const https = require('https');
20
+ const crypto = require('crypto');
21
+ const { execSync } = require('child_process');
22
+
23
+ const PACKAGE_DIR = path.resolve(__dirname, '..');
24
+ const PKG = require(path.join(PACKAGE_DIR, 'package.json'));
25
+ const BIN_DIR = path.join(PACKAGE_DIR, 'bin');
26
+ const VERSION = PKG.version;
27
+ const REPO = 'TAJD/cofferdam';
28
+
29
+ if (process.env.COFFERDAM_SKIP_DOWNLOAD === '1') {
30
+ console.log('[cofferdam] COFFERDAM_SKIP_DOWNLOAD=1 set, skipping binary download.');
31
+ process.exit(0);
32
+ }
33
+
34
+ // pnpm v10+ silently blocks postinstall scripts unless the package is in
35
+ // `pnpm.onlyBuiltDependencies` — when the user runs
36
+ // `pnpm add -D @cofferdam/cofferdam` without that allowlist, this script
37
+ // never executes and the binary never lands. By the time we DO run
38
+ // (typically `pnpm rebuild @cofferdam/cofferdam` after the user notices the
39
+ // missing binary), surface a one-line hint pointing at the durable fix so
40
+ // they don't hit the same trap on subsequent fresh installs (cd-iui / gh #9).
41
+ if (/^pnpm\//.test(process.env.npm_config_user_agent || '')) {
42
+ console.log(
43
+ '[cofferdam] pnpm detected. To make future installs work without `pnpm rebuild`,\n' +
44
+ ' add this to your package.json:\n' +
45
+ ' { "pnpm": { "onlyBuiltDependencies": ["@cofferdam/cofferdam"] } }'
46
+ );
47
+ }
48
+
49
+ if (process.env.COFFERDAM_BINARY_PATH) {
50
+ const src = process.env.COFFERDAM_BINARY_PATH;
51
+ if (!fs.existsSync(src)) {
52
+ console.error(`[cofferdam] COFFERDAM_BINARY_PATH=${src} does not exist.`);
53
+ process.exit(1);
54
+ }
55
+ const dest = path.join(BIN_DIR, binaryName());
56
+ fs.mkdirSync(BIN_DIR, { recursive: true });
57
+ fs.copyFileSync(src, dest);
58
+ if (process.platform !== 'win32') fs.chmodSync(dest, 0o755);
59
+ console.log(`[cofferdam] using binary from ${src}`);
60
+ process.exit(0);
61
+ }
62
+
63
+ main().catch((err) => {
64
+ console.error(`[cofferdam] postinstall failed: ${err.message || err}`);
65
+ console.error('');
66
+ console.error('Manual install fallback:');
67
+ console.error(` 1. Download from https://github.com/${REPO}/releases/tag/v${VERSION}`);
68
+ console.error(` 2. Extract the archive for your platform`);
69
+ console.error(` 3. Set COFFERDAM_BINARY_PATH to the extracted binary and run \`npm rebuild @cofferdam/cofferdam\``);
70
+ process.exit(1);
71
+ });
72
+
73
+ async function main() {
74
+ const target = detectTarget();
75
+ const archive = archiveName(target);
76
+ const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${archive}`;
77
+ const shaUrl = `${url}.sha256`;
78
+
79
+ console.log(`[cofferdam] platform: ${target}`);
80
+ console.log(`[cofferdam] downloading ${archive} ...`);
81
+
82
+ fs.mkdirSync(BIN_DIR, { recursive: true });
83
+ const archivePath = path.join(BIN_DIR, archive);
84
+
85
+ try {
86
+ await download(url, archivePath);
87
+ } catch (err) {
88
+ // Clean up partial download so a retry doesn't see a corrupt zero-byte file.
89
+ if (fs.existsSync(archivePath)) {
90
+ try { fs.unlinkSync(archivePath); } catch {}
91
+ }
92
+ throw err;
93
+ }
94
+
95
+ // SHA verification — non-fatal if .sha256 is missing (older releases),
96
+ // fatal if present and mismatched.
97
+ try {
98
+ const expectedHash = (await fetchText(shaUrl)).trim().split(/\s+/)[0];
99
+ const actualHash = sha256File(archivePath);
100
+ if (expectedHash && expectedHash.toLowerCase() !== actualHash.toLowerCase()) {
101
+ throw new Error(`SHA-256 mismatch: expected ${expectedHash}, got ${actualHash}`);
102
+ }
103
+ if (expectedHash) console.log(`[cofferdam] SHA-256 verified: ${actualHash}`);
104
+ } catch (e) {
105
+ if (/SHA-256 mismatch/.test(e.message)) throw e;
106
+ console.warn(`[cofferdam] could not verify SHA-256 (${e.message}); continuing.`);
107
+ }
108
+
109
+ extract(archivePath, BIN_DIR);
110
+
111
+ const binaryPath = path.join(BIN_DIR, binaryName());
112
+ if (!fs.existsSync(binaryPath)) {
113
+ throw new Error(`expected ${binaryPath} after extraction but did not find it`);
114
+ }
115
+ if (process.platform !== 'win32') fs.chmodSync(binaryPath, 0o755);
116
+ fs.unlinkSync(archivePath);
117
+
118
+ console.log(`[cofferdam] installed ${binaryPath}`);
119
+ }
120
+
121
+ function binaryName() {
122
+ return process.platform === 'win32' ? 'cofferdam.exe' : 'cofferdam';
123
+ }
124
+
125
+ function detectTarget() {
126
+ const platform = process.platform;
127
+ const arch = process.arch;
128
+
129
+ if (platform === 'win32' && arch === 'x64') return 'x86_64-pc-windows-msvc';
130
+ if (platform === 'darwin' && arch === 'x64') return 'x86_64-apple-darwin';
131
+ if (platform === 'darwin' && arch === 'arm64') return 'aarch64-apple-darwin';
132
+ if (platform === 'linux' && arch === 'x64') return isMusl() ? 'x86_64-unknown-linux-musl' : 'x86_64-unknown-linux-gnu';
133
+ if (platform === 'linux' && arch === 'arm64') return isMusl() ? 'aarch64-unknown-linux-musl' : 'aarch64-unknown-linux-gnu';
134
+
135
+ throw new Error(`unsupported platform/arch combination: ${platform}/${arch}`);
136
+ }
137
+
138
+ function isMusl() {
139
+ // Use process.report when available (Node 16+) — checks the running binary's
140
+ // libc, not the system's. This is what we want: even on a glibc host running
141
+ // a musl-built Node (alpine in docker), pick the musl artifact.
142
+ try {
143
+ const report = process.report.getReport();
144
+ if (report.header && report.header.glibcVersionRuntime) return false;
145
+ return true;
146
+ } catch {
147
+ // Fall back to ldd parsing on older Node.
148
+ try {
149
+ const out = execSync('ldd --version 2>&1', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
150
+ return /musl/i.test(out);
151
+ } catch {
152
+ return false; // assume gnu if we can't tell
153
+ }
154
+ }
155
+ }
156
+
157
+ function archiveName(target) {
158
+ const isWin = target.includes('windows');
159
+ return `cofferdam-v${VERSION}-${target}.${isWin ? 'zip' : 'tar.gz'}`;
160
+ }
161
+
162
+ function download(url, dest, redirectsLeft = 5) {
163
+ return new Promise((resolve, reject) => {
164
+ const file = fs.createWriteStream(dest);
165
+ const opts = {
166
+ headers: {
167
+ 'User-Agent': `cofferdam-postinstall/${VERSION}`,
168
+ Accept: 'application/octet-stream',
169
+ },
170
+ };
171
+ https.get(url, opts, (res) => {
172
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
173
+ if (redirectsLeft <= 0) return reject(new Error('too many redirects'));
174
+ file.close();
175
+ fs.unlinkSync(dest);
176
+ return resolve(download(res.headers.location, dest, redirectsLeft - 1));
177
+ }
178
+ if (res.statusCode !== 200) {
179
+ return reject(new Error(`HTTP ${res.statusCode} fetching ${url}`));
180
+ }
181
+ res.pipe(file);
182
+ file.on('finish', () => file.close(resolve));
183
+ file.on('error', reject);
184
+ }).on('error', reject);
185
+ });
186
+ }
187
+
188
+ function fetchText(url, redirectsLeft = 5) {
189
+ return new Promise((resolve, reject) => {
190
+ const opts = { headers: { 'User-Agent': `cofferdam-postinstall/${VERSION}` } };
191
+ https.get(url, opts, (res) => {
192
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
193
+ if (redirectsLeft <= 0) return reject(new Error('too many redirects'));
194
+ return resolve(fetchText(res.headers.location, redirectsLeft - 1));
195
+ }
196
+ if (res.statusCode !== 200) return reject(new Error(`HTTP ${res.statusCode}`));
197
+ let buf = '';
198
+ res.setEncoding('utf8');
199
+ res.on('data', (c) => (buf += c));
200
+ res.on('end', () => resolve(buf));
201
+ res.on('error', reject);
202
+ }).on('error', reject);
203
+ });
204
+ }
205
+
206
+ function sha256File(filePath) {
207
+ const h = crypto.createHash('sha256');
208
+ h.update(fs.readFileSync(filePath));
209
+ return h.digest('hex');
210
+ }
211
+
212
+ function extract(archivePath, dest) {
213
+ // System tar handles both .tar.gz and .zip on modern Windows / macOS / Linux.
214
+ // We could pull tar.js as a dep but it would dominate the install footprint.
215
+ if (archivePath.endsWith('.zip')) {
216
+ if (process.platform === 'win32') {
217
+ // Use PowerShell's Expand-Archive — built into every supported Windows.
218
+ execSync(`powershell -NoProfile -Command "Expand-Archive -Force -Path '${archivePath}' -DestinationPath '${dest}'"`, {
219
+ stdio: 'inherit',
220
+ });
221
+ } else {
222
+ // On unix we'd need an unzip — should never reach here because windows is the only zip target.
223
+ throw new Error(`zip extraction is only supported on Windows; got ${process.platform}`);
224
+ }
225
+ } else {
226
+ execSync(`tar -xzf "${archivePath}" -C "${dest}"`, { stdio: 'inherit' });
227
+ }
228
+ }