@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 +21 -0
- package/README.md +197 -0
- package/bin/cofferdam.js +100 -0
- package/package.json +50 -0
- package/scripts/postinstall.js +228 -0
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).
|
package/bin/cofferdam.js
ADDED
|
@@ -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
|
+
}
|