amont 1.5.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 +440 -0
- package/bin/amont.js +82 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
# amont
|
|
2
|
+
|
|
3
|
+
**Catch the bad commit before it exists — and take the whole thing back out in one command.**
|
|
4
|
+
|
|
5
|
+
[](https://github.com/fredericrous/amont/actions/workflows/ci.yaml)
|
|
6
|
+
[](https://github.com/fredericrous/amont/releases/latest)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
A single Rust binary that checks `git commit` and `git push` — no YAML to
|
|
10
|
+
write, no runtime to install, nothing to configure before it is useful.
|
|
11
|
+
|
|
12
|
+
- **Useful in the first minute.** Twenty-one built-in checks — commit-message
|
|
13
|
+
conventions, merge-conflict markers, the linters and formatters for the
|
|
14
|
+
languages your repository actually uses, branch rules, your test suite —
|
|
15
|
+
and each one fires only where the repository has opted into its tool.
|
|
16
|
+
- **A cloned repository cannot run code on your machine.** Checks a repository
|
|
17
|
+
declares for itself are inert until you review them and say `amont trust`
|
|
18
|
+
— a gate pre-commit, lefthook and husky do not have.
|
|
19
|
+
- **Nothing on the commit path but `std`.** The hook binary links no external
|
|
20
|
+
crates, and CI fails any build that changes that.
|
|
21
|
+
- **Leaving is one command.** `amont uninstall` removes exactly the four
|
|
22
|
+
shims install wrote; a hook you or another tool put there is named and left
|
|
23
|
+
alone.
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
How that stacks up against pre-commit, lefthook and husky, feature by feature:
|
|
28
|
+
[the full comparison](docs/similar-projects.md).
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
**Linux and macOS**
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
curl -fsSL https://raw.githubusercontent.com/fredericrous/amont/main/install/install.sh | sh
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Windows** (PowerShell — the line above is POSIX `sh` and only reaches
|
|
39
|
+
Windows through Git Bash)
|
|
40
|
+
|
|
41
|
+
```powershell
|
|
42
|
+
irm https://raw.githubusercontent.com/fredericrous/amont/main/install/install.ps1 | iex
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Either one downloads a release binary, verifies it against the published
|
|
46
|
+
`SHA256SUMS`, and puts it where the hooks already look — `~/.local/bin`, or
|
|
47
|
+
`%USERPROFILE%\.local\bin`. Both **enable nothing**: hooks are turned on per
|
|
48
|
+
repository, by you, afterwards.
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
cd <your-repo> && amont install # this repository only
|
|
52
|
+
amont list # what would run here, and why not
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Prefer not to pipe a script into a shell? Download a binary and its checksum
|
|
56
|
+
from [Releases](https://github.com/fredericrous/amont/releases/latest) —
|
|
57
|
+
prebuilt for Linux (gnu/musl, x86_64 and aarch64), macOS (Intel and Apple
|
|
58
|
+
silicon) and Windows. Or build from source: `cargo build --release`.
|
|
59
|
+
|
|
60
|
+
From [crates.io](https://crates.io/crates/amont):
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
cargo install amont
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Or with Homebrew:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
brew tap fredericrous/tap
|
|
70
|
+
brew trust fredericrous/tap # Homebrew asks this of every third-party tap
|
|
71
|
+
brew install amont
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**In a JavaScript project**, the binary can travel with the repository rather
|
|
75
|
+
than with the machine, so a teammate who clones it needs no install step at all:
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
npm i -D amont # or: pnpm add -D amont
|
|
79
|
+
npm pkg set scripts.prepare="amont init"
|
|
80
|
+
npm install # the hooks appear
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Six prebuilt platform packages are declared as `optionalDependencies`, so npm
|
|
84
|
+
installs exactly one and runs no install scripts at all — this survives
|
|
85
|
+
`npm ci --ignore-scripts`. `amont init` wires up that one repository and
|
|
86
|
+
nothing else: no `~/.local/bin`, no template directory, no prompts.
|
|
87
|
+
|
|
88
|
+
If the project also installs **without** its dev dependencies anywhere — `npm ci
|
|
89
|
+
--omit=dev`, the usual second stage of a Dockerfile — write
|
|
90
|
+
`"prepare": "amont init || true"` there, since `prepare` still runs and `amont`
|
|
91
|
+
will not be installed. [The details](docs/install.md#if-anything-ever-installs-without-your-dev-dependencies).
|
|
92
|
+
|
|
93
|
+
## Uninstall
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
amont uninstall # this repository
|
|
97
|
+
amont uninstall --binary # …and remove ~/.local/bin/amont too
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Uninstall is listed second on purpose. These hooks can block a commit, so the
|
|
101
|
+
honest question to answer first is how you get out — and the answer is that
|
|
102
|
+
`uninstall` removes our four shims and **nothing else**. A hook you wrote
|
|
103
|
+
yourself is left where it is and named in the output, whatever it is; a hook it
|
|
104
|
+
cannot even read is named too rather than passed over in silence. Your
|
|
105
|
+
`hook.skip` and `amont.severity` settings are never touched, because those
|
|
106
|
+
are your statements about your repository.
|
|
107
|
+
|
|
108
|
+
This is also why the README does not tell you to run
|
|
109
|
+
`rm $(git rev-parse --git-dir)/hooks/*`. That glob deletes every hook in the
|
|
110
|
+
directory — including ones other tools installed and ones you wrote — to remove
|
|
111
|
+
four files that belong to us.
|
|
112
|
+
|
|
113
|
+
To bypass a single run rather than uninstall: `git commit --no-verify`.
|
|
114
|
+
To turn off one check permanently, see [Turning a check off, or down](#turning-a-check-off-or-down).
|
|
115
|
+
|
|
116
|
+
## What actually runs
|
|
117
|
+
|
|
118
|
+
`amont list` answers that for the repository you are standing in, and it is
|
|
119
|
+
the honest answer rather than the catalogue: most checks are **inert** in most
|
|
120
|
+
repositories, because a repo with no `ruff.toml` never needs ruff.
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
pre-commit
|
|
124
|
+
● ban-terms
|
|
125
|
+
○ cargo-fmt inert here — needs .rs + Cargo.toml
|
|
126
|
+
● lint-json-yaml
|
|
127
|
+
● merge-conflict
|
|
128
|
+
○ prettier inert here — needs .prettierrc | .prettierrc.json | …
|
|
129
|
+
● usual-name
|
|
130
|
+
pre-push
|
|
131
|
+
● branch-protect
|
|
132
|
+
● branch-pattern
|
|
133
|
+
● pull-rebase
|
|
134
|
+
○ cargo-test inert here — needs .rs + Cargo.toml
|
|
135
|
+
|
|
136
|
+
● runs here ○ inert ⊘ skipped via hook.skip ✗ declaration unusable
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Twenty-one built-in checks across four git hooks, plus any your repository declares
|
|
140
|
+
itself. The full list, with what each one needs before it fires, is in
|
|
141
|
+
[the checks reference](docs/checks.md).
|
|
142
|
+
|
|
143
|
+
## Two ways to turn hooks on
|
|
144
|
+
|
|
145
|
+
**Per repository** — the default, and nothing runs anywhere you did not ask:
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
cd <your-repo> && amont install
|
|
149
|
+
amont-fleet install --root ~/Developer # or in bulk, across many repos
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`amont install --force` replaces a hook the installer would otherwise
|
|
153
|
+
refuse — one carrying no marker of ours, or a symlink. Even `--force` never
|
|
154
|
+
writes a tracked file or a directory. The full semantics:
|
|
155
|
+
[what `--force` will and will not do](docs/install.md#--force-and-what-it-will-not-do).
|
|
156
|
+
|
|
157
|
+
**Everywhere, forever** — an opt-in, and a real one:
|
|
158
|
+
|
|
159
|
+
```sh
|
|
160
|
+
git config --global init.templateDir ~/.config/git/git-templates/templates
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Git copies that directory into `.git/hooks` on every `init` **and every
|
|
164
|
+
clone**, so from then on every repository you clone runs these hooks without
|
|
165
|
+
being asked again. That is the convenience, and it is worth having.
|
|
166
|
+
|
|
167
|
+
It is also a standing grant, so it is worth stating what you granted. A cloned
|
|
168
|
+
repository can declare its own checks in `amont.conf`, and with this key
|
|
169
|
+
set those are one `amont trust` away from running on your first commit in a
|
|
170
|
+
repository you may have cloned only to read. If you set this, **trust
|
|
171
|
+
deliberately** rather than letting installation be the moment you decided.
|
|
172
|
+
|
|
173
|
+
## One view across every repo
|
|
174
|
+
|
|
175
|
+
`amont-fleet` — installed separately, on purpose — answers the questions a
|
|
176
|
+
directory full of repositories accumulates: which repos are covered, which
|
|
177
|
+
shims went stale after an upgrade, what every check is doing where, and which
|
|
178
|
+
repository is quietly carrying a `hook.skip` somebody forgot.
|
|
179
|
+
|
|
180
|
+

|
|
181
|
+
|
|
182
|
+
```sh
|
|
183
|
+
amont-fleet install --root ~/Developer # shims into every repo at once
|
|
184
|
+
amont-fleet # report the fleet
|
|
185
|
+
amont-fleet tui # the dashboard above
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Design record: [the fleet dashboard](docs/fleet-dashboard.md).
|
|
189
|
+
|
|
190
|
+
## Trust: a repository you clone cannot run its own checks
|
|
191
|
+
|
|
192
|
+
`amont.conf` is committed — that is the point, a team shares a check by
|
|
193
|
+
committing it. The consequence is that cloning a repository and committing to
|
|
194
|
+
it would otherwise run commands that repository chose, and neither of those
|
|
195
|
+
acts is one anybody performs *as a decision about trust*.
|
|
196
|
+
|
|
197
|
+
So a cloned repository's declared checks are **inert until you say otherwise**:
|
|
198
|
+
|
|
199
|
+
```sh
|
|
200
|
+
amont trust # show what this repo declares, and accept it
|
|
201
|
+
amont trust --show # what is trusted here
|
|
202
|
+
amont trust --revoke
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The record is keyed on the file's **content**, not its path, so a `git pull`
|
|
206
|
+
that adds a command does not inherit the consent given to the file before it.
|
|
207
|
+
The fingerprint is `git hash-object --no-filters`, and the `--no-filters` is
|
|
208
|
+
not decoration: plain `hash-object` applies the clean filter and eol conversion
|
|
209
|
+
that the repository's own committed `.gitattributes` asks for, which would let
|
|
210
|
+
a repository choose the transform its own consent is taken through.
|
|
211
|
+
|
|
212
|
+
Full reasoning: [the trust model](docs/trust.md).
|
|
213
|
+
|
|
214
|
+
## Turning a check off, or down
|
|
215
|
+
|
|
216
|
+
Every check has an id, `<trigger>-<name>` — `pre-commit-clippy`. Three things
|
|
217
|
+
name it, and both config surfaces read all three the same way:
|
|
218
|
+
|
|
219
|
+
```sh
|
|
220
|
+
git config --add hook.skip pre-commit-clippy # that one check
|
|
221
|
+
git config --add hook.skip clippy # that check, on either trigger
|
|
222
|
+
git config --add hook.skip pre-commit # every pre-commit check
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
`amont.severity.<key>` takes the same three, and keeps the signal — the
|
|
226
|
+
check still runs and still reports, it just stops failing the commit:
|
|
227
|
+
|
|
228
|
+
```sh
|
|
229
|
+
git config amont.severity.clippy warn
|
|
230
|
+
git config amont.severity.pre-commit warn
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Where several keys reach one check the most specific wins: full id, then short
|
|
234
|
+
name, then trigger. Nothing matches by substring — `hook.skip e` reaches
|
|
235
|
+
nothing at all, and skipping `lint-js` leaves `lint-json-yaml` alone.
|
|
236
|
+
|
|
237
|
+
A skipped check is announced on every commit, so a config line nobody remembers
|
|
238
|
+
writing cannot go on silently disabling things. More in
|
|
239
|
+
[opting out](docs/opting-out.md) and [configuration](docs/configuration.md).
|
|
240
|
+
|
|
241
|
+
## Making the commit convention yours
|
|
242
|
+
|
|
243
|
+
`commit-msg` is the one hook `hook.skip` and `--no-verify` cannot reach, so its
|
|
244
|
+
opinions are adjustable in themselves:
|
|
245
|
+
|
|
246
|
+
```sh
|
|
247
|
+
amont setup # four questions, current values as the defaults
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
It asks where the type's gitmoji goes — `none` (the default: your subject,
|
|
251
|
+
untouched), `prefix`, `suffix` or `replace` — and for the subject limit, the
|
|
252
|
+
description budget and the body wrap column. Then it prints the exact
|
|
253
|
+
`git config` lines it wrote, so you can paste them into your dotfiles or hand
|
|
254
|
+
them to a teammate.
|
|
255
|
+
|
|
256
|
+
```sh
|
|
257
|
+
git config amont.commit.gitmoji suffix # feat: add a cart ✨
|
|
258
|
+
git config amont.commit.descriptionMax 68 # still fits a 72-col subject
|
|
259
|
+
git config amont.commit.bodyWrap 0 # leave my stack traces alone
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
The limits measure what you wrote, so a gitmoji never eats your budget, and
|
|
263
|
+
re-running over an amended message changes nothing. Full reference:
|
|
264
|
+
[commit conventions](docs/commit-convention.md#if-the-defaults-do-not-fit).
|
|
265
|
+
|
|
266
|
+
## Custom checks
|
|
267
|
+
|
|
268
|
+
A repository can declare checks of its own in a committed `amont.conf`:
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
# stage name scope severity command
|
|
272
|
+
pre-commit shellcheck *.sh block scripts/lint-shell.sh
|
|
273
|
+
pre-push smoke * warn make smoke
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
They run alongside the built-ins, obey the same `hook.skip` and
|
|
277
|
+
`amont.severity` controls, and are inert until trusted. Full reference:
|
|
278
|
+
[custom checks](docs/custom-checks.md).
|
|
279
|
+
|
|
280
|
+
## Running the checks yourself
|
|
281
|
+
|
|
282
|
+
```sh
|
|
283
|
+
amont run # would my commit pass? (the staged set)
|
|
284
|
+
amont run --all-files # does my working tree pass? (git ls-files)
|
|
285
|
+
amont run pre-commit-prettier
|
|
286
|
+
amont list # what would run here, and why not
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
The two questions are different on purpose. `--all-files` on a dirty tree
|
|
290
|
+
reports on content that is not committed and may never be — which is what you
|
|
291
|
+
want when adopting a check into an existing repository, where `git add .` is
|
|
292
|
+
not an acceptable way to measure the mess.
|
|
293
|
+
|
|
294
|
+
## What a push actually tests
|
|
295
|
+
|
|
296
|
+
By default `pre-push` runs your suite against the **working tree**, and says
|
|
297
|
+
so. That is fast and usually what you want, but it is not what you are pushing:
|
|
298
|
+
an uncommitted fix makes a broken commit look green.
|
|
299
|
+
|
|
300
|
+
```sh
|
|
301
|
+
git config amont.testPushedTree true
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
turns on the accurate answer — the suite runs in a throwaway checkout of the
|
|
305
|
+
commits being pushed, and your tree is not touched. It costs a second checkout
|
|
306
|
+
and a build that cannot reuse your `target/` cache, which is why it is opt-in.
|
|
307
|
+
|
|
308
|
+
## For coding agents
|
|
309
|
+
|
|
310
|
+
`amont list --json` is the same answer as `amont list`, machine-readable:
|
|
311
|
+
every check's declared and effective severity, whether it fires here and why
|
|
312
|
+
not, and its command if it is a declared external. `--stage` filters to one
|
|
313
|
+
trigger; `--pushed` scopes to what your *next push* would carry (`@{u}..HEAD`)
|
|
314
|
+
rather than the whole tracked tree.
|
|
315
|
+
|
|
316
|
+
```sh
|
|
317
|
+
amont list --json --stage pre-push --pushed
|
|
318
|
+
amont agents-md # write a self-verifying pointer into AGENTS.md
|
|
319
|
+
amont agents-md --check # exit non-zero if it has drifted
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
`agents-md` only ever touches a `<!-- amont:start -->` / `<!-- amont:end -->`
|
|
323
|
+
span, so the rest of the file stays yours.
|
|
324
|
+
|
|
325
|
+
The block it writes also warns the agent that `git commit` and `git push`
|
|
326
|
+
run their checks first — pre-commit can mean clippy building a workspace,
|
|
327
|
+
pre-push a whole test suite — and an agent whose shell tool defaults to a
|
|
328
|
+
two-minute timeout will kill the command mid-check and read its own
|
|
329
|
+
impatience as a failure. Ten minutes is the safe floor, for both.
|
|
330
|
+
|
|
331
|
+
## Why you can let this near your commits
|
|
332
|
+
|
|
333
|
+
A prompt theme is cosmetic. This blocks commits and pushes, reads every staged
|
|
334
|
+
file, and runs with your credentials while nobody is watching — so the claim it
|
|
335
|
+
has to earn is not "delightful", it is "harmless".
|
|
336
|
+
|
|
337
|
+
- **The commit path links no external crates.** `amont` and
|
|
338
|
+
`amont-runtime` are std-only, and `scripts/check-no-deps.sh` fails a build
|
|
339
|
+
that changes that — fails *closed*, so a cargo error or an unreachable
|
|
340
|
+
registry is a failure rather than a reassuring green tick. `amont-fleet`
|
|
341
|
+
takes dependencies quite happily; it is installed separately and runs when
|
|
342
|
+
asked.
|
|
343
|
+
- **No network, ever.** The binary phones nothing home — no telemetry, no
|
|
344
|
+
update checks, no fetches. With the commit path std-only, there is not even
|
|
345
|
+
an HTTP client linked to do it with.
|
|
346
|
+
- **Over six hundred tests**, run on Linux, macOS and Windows, alongside `cargo fmt
|
|
347
|
+
--check`, `clippy -D warnings`, an MSRV floor of 1.74 compiled for the commit
|
|
348
|
+
path, and `cargo-audit`.
|
|
349
|
+
- **v1.0.0 followed a full security review**, and each finding landed with a
|
|
350
|
+
committed reproduction: a drive-by RCE via a relative path in the shim; a
|
|
351
|
+
held-store format that let a repository delete a tracked file and plant a
|
|
352
|
+
symlink outside the worktree; a trust prompt a repository could conceal
|
|
353
|
+
declarations from; guards that wrote through symlinks onto tracked source; a
|
|
354
|
+
commit subject shaped like a trailer destroying commit trailers;
|
|
355
|
+
`pre-commit-pyright` blocking every commit it ran on.
|
|
356
|
+
- **Your uncommitted work is the thing that must never be lost.** The release
|
|
357
|
+
profile deliberately omits `panic = "abort"` so that the `Drop` that restores
|
|
358
|
+
unstaged work still runs when a check panics — with a test asserting on the
|
|
359
|
+
manifest, because cargo ignores that setting for test targets and no
|
|
360
|
+
behavioural test could catch the regression.
|
|
361
|
+
|
|
362
|
+
Threat model and private reporting: [SECURITY.md](SECURITY.md).
|
|
363
|
+
|
|
364
|
+
## Windows
|
|
365
|
+
|
|
366
|
+
Everything works, and the PowerShell one-liner in [Install](#install) is all
|
|
367
|
+
the setup most machines need. One difference: there is no symlink. To build
|
|
368
|
+
from source instead — Git for Windows ships `bash` and coreutils but not
|
|
369
|
+
`make`:
|
|
370
|
+
|
|
371
|
+
```sh
|
|
372
|
+
cargo build --release
|
|
373
|
+
./target/release/amont install
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
On macOS/Linux `~/.config/git/git-templates` is usually a symlink to the
|
|
377
|
+
checkout, so `init.templateDir` can point at a stable XDG path. Windows does
|
|
378
|
+
not create symlinks without Developer Mode or elevation, so point git straight
|
|
379
|
+
at the checkout instead:
|
|
380
|
+
|
|
381
|
+
```sh
|
|
382
|
+
git config --global init.templateDir 'C:/path/to/amont/templates'
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Nothing else changes. The shims never need the symlink: they resolve the binary
|
|
386
|
+
at runtime, trying `$GIT_HOOKS_BIN`, the baked path, `~/.local/bin/amont`
|
|
387
|
+
and `~/.local/bin/amont.exe`, then `PATH`.
|
|
388
|
+
|
|
389
|
+
## Requirements
|
|
390
|
+
|
|
391
|
+
- **Git 2.31+** — `git rev-parse --path-format=absolute` landed there, and
|
|
392
|
+
three places depend on it. On an older git those return nothing rather than
|
|
393
|
+
failing loudly, which is the worst shape for a version floor: the tool
|
|
394
|
+
appears to work and quietly resolves the wrong paths.
|
|
395
|
+
|
|
396
|
+
The hooks are a single binary with no runtime dependencies. Each check brings
|
|
397
|
+
its own tool requirement only where you have opted into that check.
|
|
398
|
+
|
|
399
|
+
## Documentation
|
|
400
|
+
|
|
401
|
+
The full documentation is in [`docs/`](docs/), versioned with the code and
|
|
402
|
+
published as a book:
|
|
403
|
+
|
|
404
|
+
- [Installing and activating](docs/install.md)
|
|
405
|
+
- [The checks](docs/checks.md) · [Configuration](docs/configuration.md) ·
|
|
406
|
+
[Opting out](docs/opting-out.md)
|
|
407
|
+
- [The trust model](docs/trust.md) · [Custom checks](docs/custom-checks.md)
|
|
408
|
+
- [Where the hooks fit in your flow](docs/coding-flow.md) ·
|
|
409
|
+
[Commit conventions](docs/commit-convention.md) ·
|
|
410
|
+
[How it compares](docs/similar-projects.md) ·
|
|
411
|
+
[Ideas, not a roadmap](docs/ideas.md)
|
|
412
|
+
- Decision records for maintainers: [hook architecture](docs/hook-architecture.md),
|
|
413
|
+
[index fidelity and run modes](docs/index-fidelity-and-run-modes.md),
|
|
414
|
+
[skip management](docs/hook-skip-management.md),
|
|
415
|
+
[the fleet dashboard](docs/fleet-dashboard.md),
|
|
416
|
+
[the Rust migration](docs/rust-migration.md)
|
|
417
|
+
|
|
418
|
+
## Contributing
|
|
419
|
+
|
|
420
|
+
Everything is Rust, in `crates/`:
|
|
421
|
+
|
|
422
|
+
```
|
|
423
|
+
crates/amont-runtime/ the checks, registry and dispatchers. std only.
|
|
424
|
+
crates/amont/ the hook binary. Runs on every commit. std only.
|
|
425
|
+
crates/amont-fleet/ the dashboard and the fleet fixer. Opt-in.
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
`make check` is the CI-parity target — run it before you push. Setup, the
|
|
429
|
+
zero-dependency rule and when reopening it is legitimate, the house test style
|
|
430
|
+
and the commit convention are all in [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
431
|
+
|
|
432
|
+
Questions, "does this work with X", ideas for checks:
|
|
433
|
+
[Discussions](https://github.com/fredericrous/amont/discussions). Bugs:
|
|
434
|
+
[issues](https://github.com/fredericrous/amont/issues).
|
|
435
|
+
|
|
436
|
+
By participating you agree to the [Code of Conduct](CODE_OF_CONDUCT.md).
|
|
437
|
+
|
|
438
|
+
## License
|
|
439
|
+
|
|
440
|
+
[MIT](LICENSE).
|
package/bin/amont.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Find the native binary npm installed for this platform, and become it.
|
|
3
|
+
//
|
|
4
|
+
// This wrapper exists only because a package manager can link a `bin` that
|
|
5
|
+
// lives inside the package, and the native executable lives in a DIFFERENT
|
|
6
|
+
// package — one of six, selected by npm from `os`/`cpu`/`libc`.
|
|
7
|
+
//
|
|
8
|
+
// It is not on the hook path. `amont init` bakes `current_exe()` — the native
|
|
9
|
+
// binary this spawns, not this file — into `.git/hooks`, so the ~30ms of node
|
|
10
|
+
// start-up below is paid once during `prepare` and never again on a commit.
|
|
11
|
+
// See npm/README.md.
|
|
12
|
+
|
|
13
|
+
const { spawnSync } = require("node:child_process");
|
|
14
|
+
const { existsSync } = require("node:fs");
|
|
15
|
+
|
|
16
|
+
// The same six targets `release.yaml` builds, spelled the way npm spells them.
|
|
17
|
+
// `libc` is why linux-x64 appears twice: a musl host cannot run the glibc build,
|
|
18
|
+
// and npm will install only the package whose `libc` matches.
|
|
19
|
+
const PACKAGES = {
|
|
20
|
+
"darwin arm64": "amont-darwin-arm64",
|
|
21
|
+
"darwin x64": "amont-darwin-x64",
|
|
22
|
+
"linux arm64": "amont-linux-arm64-gnu",
|
|
23
|
+
"linux x64": ["amont-linux-x64-gnu", "amont-linux-x64-musl"],
|
|
24
|
+
"win32 x64": "amont-win32-x64",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function candidates() {
|
|
28
|
+
const found = PACKAGES[`${process.platform} ${process.arch}`];
|
|
29
|
+
if (!found) return [];
|
|
30
|
+
return Array.isArray(found) ? found : [found];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// `require.resolve` rather than a hand-built `../amont-<target>/bin/…` path.
|
|
34
|
+
// pnpm does not hoist — the real package sits under `node_modules/.pnpm/` — so a
|
|
35
|
+
// path assembled from `__dirname` is correct under npm and wrong under pnpm and
|
|
36
|
+
// yarn. Node's own resolver knows where the dependency actually is.
|
|
37
|
+
function resolveBinary() {
|
|
38
|
+
const exe = process.platform === "win32" ? "amont.exe" : "amont";
|
|
39
|
+
for (const pkg of candidates()) {
|
|
40
|
+
try {
|
|
41
|
+
const p = require.resolve(`${pkg}/bin/${exe}`);
|
|
42
|
+
if (existsSync(p)) return p;
|
|
43
|
+
} catch {
|
|
44
|
+
// Not installed: either the wrong libc for this host, or an
|
|
45
|
+
// `--ignore-scripts`-style install that skipped optional deps. Try the
|
|
46
|
+
// next candidate before giving up.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const binary = resolveBinary();
|
|
53
|
+
if (!binary) {
|
|
54
|
+
// Name the platform. "amont binary not found" sends people to reinstall;
|
|
55
|
+
// "no build for linux/ppc64" tells them the actual answer, which is that they
|
|
56
|
+
// want `cargo install amont` or the shell installer.
|
|
57
|
+
const target = `${process.platform}/${process.arch}`;
|
|
58
|
+
process.stderr.write(
|
|
59
|
+
`amont: no native binary for ${target}.\n` +
|
|
60
|
+
` npm installs one of: ${Object.values(PACKAGES).flat().join(", ")}\n` +
|
|
61
|
+
` If your platform is not among them, build from source:\n` +
|
|
62
|
+
` cargo install amont\n` +
|
|
63
|
+
` If it is, the optional dependency did not install — try:\n` +
|
|
64
|
+
` npm install --force amont\n`,
|
|
65
|
+
);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// `spawnSync` with inherited stdio rather than `execFileSync`: this forwards the
|
|
70
|
+
// child's exit CODE, and a hook runner's exit code is the whole product. It also
|
|
71
|
+
// keeps the child's stdin, which `amont run` reads.
|
|
72
|
+
const result = spawnSync(binary, process.argv.slice(2), { stdio: "inherit" });
|
|
73
|
+
if (result.error) {
|
|
74
|
+
process.stderr.write(`amont: could not run ${binary}: ${result.error.message}\n`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
// A signalled child has a null status. Report it the way a shell does, so
|
|
78
|
+
// "killed by SIGINT" does not read as a clean exit 0.
|
|
79
|
+
if (result.status === null && result.signal) {
|
|
80
|
+
process.exit(128 + (require("node:os").constants.signals[result.signal] ?? 0));
|
|
81
|
+
}
|
|
82
|
+
process.exit(result.status ?? 1);
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "amont",
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"description": "Opinionated git hooks that judge what you are committing, not what is on disk",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"git",
|
|
7
|
+
"git-hooks",
|
|
8
|
+
"pre-commit",
|
|
9
|
+
"pre-push",
|
|
10
|
+
"hooks",
|
|
11
|
+
"lint",
|
|
12
|
+
"husky"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/fredericrous/amont#readme",
|
|
15
|
+
"bugs": "https://github.com/fredericrous/amont/issues",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/fredericrous/amont.git"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": "Frederic Rousseau",
|
|
22
|
+
"type": "commonjs",
|
|
23
|
+
"bin": {
|
|
24
|
+
"amont": "bin/amont.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"bin/amont.js",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
},
|
|
33
|
+
"optionalDependencies": {
|
|
34
|
+
"amont-darwin-arm64": "1.5.0",
|
|
35
|
+
"amont-darwin-x64": "1.5.0",
|
|
36
|
+
"amont-linux-arm64-gnu": "1.5.0",
|
|
37
|
+
"amont-linux-x64-gnu": "1.5.0",
|
|
38
|
+
"amont-linux-x64-musl": "1.5.0",
|
|
39
|
+
"amont-win32-x64": "1.5.0"
|
|
40
|
+
}
|
|
41
|
+
}
|