@homeflare/config 0.10.0 → 0.11.1
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 +11 -41
- package/bin/hooks.ts +8 -4
- package/dist/hooks/activate.d.ts +7 -0
- package/dist/hooks/activate.d.ts.map +1 -0
- package/dist/hooks/gates.d.ts +7 -14
- package/dist/hooks/gates.d.ts.map +1 -1
- package/dist/hooks/install.d.ts +7 -1
- package/dist/hooks/install.d.ts.map +1 -1
- package/dist/hooks/oxfmt-config.d.ts +25 -0
- package/dist/hooks/oxfmt-config.d.ts.map +1 -0
- package/dist/hooks/push-plan.d.ts +59 -0
- package/dist/hooks/push-plan.d.ts.map +1 -0
- package/dist/hooks/push-range.d.ts +51 -0
- package/dist/hooks/push-range.d.ts.map +1 -0
- package/dist/hooks/report.d.ts +39 -2
- package/dist/hooks/report.d.ts.map +1 -1
- package/dist/hooks/secrets.d.ts +2 -0
- package/dist/hooks/secrets.d.ts.map +1 -0
- package/dist/hooks.d.ts +30 -6
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +408 -41
- package/dist/hooks.js.map +12 -7
- package/dist/repo-shape/yaml.d.ts +20 -3
- package/dist/repo-shape/yaml.d.ts.map +1 -1
- package/dist/repo-shape.js +8 -2
- package/dist/repo-shape.js.map +3 -3
- package/dist/versions.js +8 -2
- package/dist/versions.js.map +3 -3
- package/docs/hooks.md +118 -0
- package/package.json +1 -1
- package/src/hooks/activate.ts +71 -0
- package/src/hooks/gates.ts +156 -39
- package/src/hooks/install.ts +39 -20
- package/src/hooks/oxfmt-config.ts +67 -0
- package/src/hooks/push-plan.ts +167 -0
- package/src/hooks/push-range.ts +177 -0
- package/src/hooks/report.ts +46 -6
- package/src/hooks/secrets.ts +42 -0
- package/src/hooks.ts +30 -14
- package/src/repo-shape/yaml.ts +26 -4
package/README.md
CHANGED
|
@@ -83,7 +83,12 @@ cp node_modules/@homeflare/config/bunfig.toml bunfig.toml
|
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
⚠️ `.oxfmtrc.json` needs the **leading dot**. Without it oxfmt silently uses its defaults,
|
|
86
|
-
and the symptom is a formatter that rewrites your quotes.
|
|
86
|
+
and the symptom is a formatter that rewrites your quotes. The same silent-defaults trap
|
|
87
|
+
applies to `.oxfmtrc.mjs`/`.ts`/`.js`/`.cjs`/`.mts`/`.cts` in a bare `oxfmt`/`oxfmt .`
|
|
88
|
+
invocation — `oxfmt`'s own auto-discovery finds only `.json`/`.jsonc`, even though `-c`
|
|
89
|
+
accepts the rest. Prefer `.oxfmtrc.json`; the shared pre-commit hook resolves the other
|
|
90
|
+
extensions and passes `--config` itself (`docs/hooks.md`), but your own `bun run format`
|
|
91
|
+
script still needs to name the file explicitly if it isn't `.oxfmtrc.json`.
|
|
87
92
|
|
|
88
93
|
⛔ oxfmt has no `extends`. Extra `ignorePatterns` are a **merge**: generated OpenAPI,
|
|
89
94
|
`vendor/`, `**/generated/**`, `**/*.gen.ts` stay out of the formatter. ⛔ Identity
|
|
@@ -145,50 +150,15 @@ Alchemy resource — see `docs/github-hygiene.md`.
|
|
|
145
150
|
|
|
146
151
|
## git hooks
|
|
147
152
|
|
|
148
|
-
One hook layer for the whole estate
|
|
149
|
-
|
|
153
|
+
One hook layer for the whole estate: a secret scan and staged format/lint on commit, and
|
|
154
|
+
the repo's own `check` narrowed to what a push can affect on push. They are active in
|
|
155
|
+
every worktree of a clone. Adoption, what runs, and why:
|
|
156
|
+
[docs/hooks.md](./docs/hooks.md).
|
|
150
157
|
|
|
151
158
|
```sh
|
|
152
|
-
bun
|
|
153
|
-
npm pkg set scripts.prepare=husky
|
|
154
|
-
bun install # husky writes .husky/_
|
|
155
|
-
bun node_modules/@homeflare/config/bin/hooks.ts install
|
|
156
|
-
git add .husky/pre-commit .husky/pre-push package.json
|
|
159
|
+
bun node_modules/@homeflare/config/bin/hooks.ts install # then "prepare": "bun node_modules/@homeflare/config/bin/hooks.ts activate"
|
|
157
160
|
```
|
|
158
161
|
|
|
159
|
-
| hook | runs | cost |
|
|
160
|
-
| ------------ | ----------------------------------------------- | ----------------- |
|
|
161
|
-
| `pre-commit` | `oxfmt` + `oxlint --deny-warnings`, staged only | sub-second |
|
|
162
|
-
| `pre-push` | the repo's own `bun run check` | whatever CI costs |
|
|
163
|
-
|
|
164
|
-
⚠️ **pre-commit rewrites files.** It formats the staged formattable files (`.md`
|
|
165
|
-
included — house `oxfmt` formats markdown, and a hook that skipped it would let an
|
|
166
|
-
unformatted changeset through), names the ones it changed, and restages exactly those.
|
|
167
|
-
Without the restage the commit would capture the unformatted bytes and CI would fail a
|
|
168
|
-
file that reads as clean locally.
|
|
169
|
-
|
|
170
|
-
⛔ **A file with unstaged edits on top is checked, never rewritten.** Restaging it would
|
|
171
|
-
sweep work in progress into a commit nobody asked for.
|
|
172
|
-
|
|
173
|
-
⛔ **pre-push always calls `check`, never `verify`.** `verify` means the consumer smoke
|
|
174
|
-
test in this repo and a _live_ adoption verifier in `homeflare-proxmox`; a hook that
|
|
175
|
-
guessed would run credentials-backed live checks on a push.
|
|
176
|
-
|
|
177
|
-
⚠️ **A hook is a local convenience, not a gate.** It is skippable with `--no-verify`,
|
|
178
|
-
absent from a fresh clone until `bun install` runs `prepare`, and silently inert when
|
|
179
|
-
`core.hooksPath` points at a `.husky/_` that no install has created yet. The required
|
|
180
|
-
checks on `main` stay the gate; this makes the cheap mistakes cheap to find.
|
|
181
|
-
|
|
182
|
-
```ts
|
|
183
|
-
import { problemsInHooks } from '@homeflare/config/hooks';
|
|
184
|
-
|
|
185
|
-
expect(await problemsInHooks(process.cwd())).toEqual([]);
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
⛔ `problemsInHooks` is deliberately **not** part of `checkProject`. Every repo runs that
|
|
189
|
-
checker from a test, so folding hook conformance in would turn every repo that has not
|
|
190
|
-
adopted yet red on `main` in one commit. A repo opts in by calling this.
|
|
191
|
-
|
|
192
162
|
## License
|
|
193
163
|
|
|
194
164
|
MIT © Timothy Schneider
|
package/bin/hooks.ts
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* The entry a repo's `.husky/` wrapper calls.
|
|
3
|
+
* The entry a repo's `.husky/` wrapper calls, and its `prepare` script.
|
|
4
4
|
*
|
|
5
5
|
* ⚠️ IT IMPORTS `../src`, NOT `../dist`. `src` is published, Bun runs TypeScript
|
|
6
6
|
* directly, and a hook that depended on a build step would be dead in a fresh clone
|
|
7
7
|
* of the repo that HOSTS this package — the one place `dist/` does not exist yet.
|
|
8
|
+
* ⚠️ STDIN IS READ FOR `pre-push` ONLY, AND NEVER FROM A TERMINAL. git pipes the pushed refs
|
|
9
|
+
* to that hook; a person running it by hand has a TTY there, and reading it would hang.
|
|
8
10
|
*/
|
|
9
11
|
import { isCommand, runCommand } from '../src/hooks.ts';
|
|
10
12
|
|
|
11
|
-
const command = process.argv
|
|
13
|
+
const [command = '', ...args] = process.argv.slice(2);
|
|
12
14
|
|
|
13
15
|
if (!isCommand(command)) {
|
|
14
16
|
process.stderr.write(`homeflare hooks: unknown command ${JSON.stringify(command)}\n`);
|
|
15
|
-
process.stderr.write(' expected one of: pre-commit, pre-push, install\n');
|
|
17
|
+
process.stderr.write(' expected one of: pre-commit, pre-push, install, activate\n');
|
|
16
18
|
process.exit(2);
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
const stdin = command === 'pre-push' && !process.stdin.isTTY ? await Bun.stdin.text() : '';
|
|
22
|
+
|
|
23
|
+
await runCommand(command, process.cwd(), args, stdin);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type Activation = {
|
|
2
|
+
/** The hooks path is set (now or already). */
|
|
3
|
+
readonly active: boolean;
|
|
4
|
+
readonly message: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function activateHooks(root: string, env?: Readonly<Record<string, string | undefined>>): Promise<Activation>;
|
|
7
|
+
//# sourceMappingURL=activate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"activate.d.ts","sourceRoot":"","sources":["../../src/hooks/activate.ts"],"names":[],"mappings":"AAoBA,MAAM,MAAM,UAAU,GAAG;IACvB,8CAA8C;IAC9C,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,CAAC;AAYF,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,GAAG,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAe,GAC9D,OAAO,CAAC,UAAU,CAAC,CA+BrB"}
|
package/dist/hooks/gates.d.ts
CHANGED
|
@@ -1,19 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Format and lint what is staged, restaging only what the formatter rewrote.
|
|
3
|
-
*
|
|
4
|
-
* ⚠️ THIS HOOK REWRITES FILES DURING A COMMIT, and it says so on the line where it
|
|
5
|
-
* happens. Without the restage, oxfmt would fix the worktree while the commit kept
|
|
6
|
-
* the unformatted bytes — CI then fails on a file that reads as correct locally.
|
|
7
|
-
*/
|
|
8
1
|
export declare function preCommit(root: string): Promise<void>;
|
|
9
2
|
/**
|
|
10
|
-
* Run the
|
|
3
|
+
* Run the repository's own `check`, narrowed to what the push can affect.
|
|
11
4
|
*
|
|
12
|
-
* ★
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
5
|
+
* ★ `args` ARE GIT'S: the remote name and URL. `stdin` is git's ref list — see
|
|
6
|
+
* push-range.ts for how the base is chosen and why it never narrows to nothing.
|
|
7
|
+
* ⛔ IT DOES NOT CERTIFY WHAT CI WILL SAY. It certifies that `check`'s own lint and type
|
|
8
|
+
* lanes pass and that every test the pushed files can reach passes. The build, the smoke
|
|
9
|
+
* test and the unreachable tests are CI's, and the success line says so.
|
|
17
10
|
*/
|
|
18
|
-
export declare function prePush(root: string): Promise<void>;
|
|
11
|
+
export declare function prePush(root: string, args: readonly string[], stdin: string): Promise<void>;
|
|
19
12
|
//# sourceMappingURL=gates.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gates.d.ts","sourceRoot":"","sources":["../../src/hooks/gates.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"gates.d.ts","sourceRoot":"","sources":["../../src/hooks/gates.ts"],"names":[],"mappings":"AAqDA,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA+F3D;AAWD;;;;;;;;GAQG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoDjG"}
|
package/dist/hooks/install.d.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
/** The hook files this package installs, in the order a contributor meets them. */
|
|
2
2
|
export declare const HOOK_NAMES: readonly ['pre-commit', 'pre-push'];
|
|
3
|
+
/** What a consumer's `prepare` script runs, so every `bun install` activates the hooks. */
|
|
4
|
+
export declare const PREPARE: string;
|
|
3
5
|
/**
|
|
4
|
-
* ⚠️ IT EXITS 0 WHEN THE RUNNER IS ABSENT. A
|
|
6
|
+
* ⚠️ IT EXITS 0 WHEN THE RUNNER IS ABSENT. A worktree with no `node_modules` would
|
|
5
7
|
* otherwise fail every commit with a module-resolution error, and the first thing
|
|
6
8
|
* anyone would do is delete the hook. Failing open is the right trade for a
|
|
7
9
|
* convenience; the required checks on `main` are what must fail closed.
|
|
10
|
+
* ★ `"$@"` AND STDIN PASS THROUGH. `pre-push` reads the remote name from its first argument
|
|
11
|
+
* and the pushed refs from stdin (push-range.ts); `exec` keeps both.
|
|
12
|
+
* ⚠️ NO SHEBANG, AND THAT IS MEASURED, NOT FORGOTTEN: git 2.55 runs an executable hook that
|
|
13
|
+
* has none through `sh` (2026-09-23), and the husky-era files in the estate have none.
|
|
8
14
|
*/
|
|
9
15
|
export declare const HUSKY_HOOK: string;
|
|
10
16
|
/** Write the wrapper into `.husky/`. Returns the paths written, relative to the project. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/hooks/install.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/hooks/install.ts"],"names":[],"mappings":"AAgBA,mFAAmF;AACnF,eAAO,MAAM,UAAU,YAAI,YAAY,EAAE,UAAU,CAAU,CAAC;AAK9D,2FAA2F;AAC3F,eAAO,MAAM,OAAO,EAAE,MAAiC,CAAC;AAExD;;;;;;;;;GASG;AACH,eAAO,MAAM,UAAU,EAAE,MAexB,CAAC;AAEF,4FAA4F;AAC5F,wBAAsB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAYjF;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAuCpF"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type OxfmtConfigResolution =
|
|
2
|
+
/** `.oxfmtrc.json` or `.oxfmtrc.jsonc` exists — bare oxfmt already finds it. */
|
|
3
|
+
{
|
|
4
|
+
readonly kind: 'auto';
|
|
5
|
+
}
|
|
6
|
+
/** No `.oxfmtrc.*` at all — let oxfmt use its built-in defaults, as today. */
|
|
7
|
+
| {
|
|
8
|
+
readonly kind: 'none';
|
|
9
|
+
}
|
|
10
|
+
/** Exactly one config oxfmt would not find unaided — pass it with `-c`. */
|
|
11
|
+
| {
|
|
12
|
+
readonly kind: 'explicit';
|
|
13
|
+
readonly path: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Two or more configs, none of them auto-discovered — bare oxfmt (and a repo's own
|
|
17
|
+
* bare `bun run format`) would silently fall back to defaults too, honouring neither.
|
|
18
|
+
* Guessing which one the repo meant would just move the silent-defaults bug here.
|
|
19
|
+
*/
|
|
20
|
+
| {
|
|
21
|
+
readonly kind: 'ambiguous';
|
|
22
|
+
readonly files: readonly string[];
|
|
23
|
+
};
|
|
24
|
+
export declare function resolveOxfmtConfig(root: string): Promise<OxfmtConfigResolution>;
|
|
25
|
+
//# sourceMappingURL=oxfmt-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oxfmt-config.d.ts","sourceRoot":"","sources":["../../src/hooks/oxfmt-config.ts"],"names":[],"mappings":"AAsCA,MAAM,MAAM,qBAAqB;AAC/B,gFAAgF;AAC9E;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AAC3B,8EAA8E;GAC5E;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AAC3B,2EAA2E;GACzE;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AACtD;;;;GAIG;GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC;AAQtE,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAQrF"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `pre-push` runs: the repository's own `check`, with the expensive lanes narrowed to
|
|
3
|
+
* the push or left to CI.
|
|
4
|
+
*
|
|
5
|
+
* ★ IT READS `check` RATHER THAN NAMING TOOLS. `check` is the command CI runs, so the lanes
|
|
6
|
+
* here are that command's lanes — a repository that adds a step to `check` gets it in the
|
|
7
|
+
* hook with no change to this package. Hard-coding `tsc` and `bun test` would drift from
|
|
8
|
+
* whichever repository added a step first, and certify a push CI rejects.
|
|
9
|
+
* ★ THREE THINGS CHANGE, AND ONLY THREE:
|
|
10
|
+
* 1. every `bun test …` becomes `bun test … --changed=<base>` — Bun's own import-graph
|
|
11
|
+
* answer to "which test files can these changed files reach" (bun 1.4, measured
|
|
12
|
+
* 2026-09-23: 11 changed files ran 2 of 246 test files in homeflare-kit);
|
|
13
|
+
* 2. `build` and `smoke` scripts are skipped — minutes in a big workspace, and CI runs
|
|
14
|
+
* both on every pull request;
|
|
15
|
+
* 3. everything else (lint, types, a `--check` script) runs exactly as `check` spells it.
|
|
16
|
+
* They are seconds, whole-program by nature, and deterministic.
|
|
17
|
+
* ⛔ PURE. No git, no filesystem, no process: the whole contract is a function of the
|
|
18
|
+
* scripts table and the base, so the estate's real `check` shapes are pinned by a table
|
|
19
|
+
* test (tests/hooks-push-plan.test.ts) instead of by a live push.
|
|
20
|
+
*/
|
|
21
|
+
/** One step of the pre-push run. */
|
|
22
|
+
export type Lane =
|
|
23
|
+
/** Run `command` through `sh -c` at the repository root. */
|
|
24
|
+
{
|
|
25
|
+
readonly kind: 'run';
|
|
26
|
+
readonly label: string;
|
|
27
|
+
readonly command: string;
|
|
28
|
+
}
|
|
29
|
+
/** A test runner; `scoped` says whether it was narrowed to the push. */
|
|
30
|
+
| {
|
|
31
|
+
readonly kind: 'test';
|
|
32
|
+
readonly label: string;
|
|
33
|
+
readonly command: string;
|
|
34
|
+
readonly scoped: boolean;
|
|
35
|
+
}
|
|
36
|
+
/** Not run here, and why. */
|
|
37
|
+
| {
|
|
38
|
+
readonly kind: 'skip';
|
|
39
|
+
readonly label: string;
|
|
40
|
+
readonly why: string;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Split a script on top-level `&&`, or return undefined when it is anything else.
|
|
44
|
+
*
|
|
45
|
+
* ⛔ UNDEFINED MEANS "RUN IT WHOLE". `||`, `;`, a pipe, a redirect, a background `&` or a
|
|
46
|
+
* substitution each change what the pieces mean together, and a hook that re-plumbed them
|
|
47
|
+
* would run something the repository never wrote. Quoted text is left alone, so
|
|
48
|
+
* `--path-ignore-patterns="homeflare-*\/**"` survives intact.
|
|
49
|
+
*/
|
|
50
|
+
export declare function andChain(script: string): readonly string[] | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* The lanes `pre-push` runs for this `scripts` table.
|
|
53
|
+
*
|
|
54
|
+
* `base` is the commit the push is measured from; `undefined` runs every test lane in full
|
|
55
|
+
* (an unknown base, or a push that changes what every test runs on).
|
|
56
|
+
* ⚠️ AN EMPTY LIST MEANS NO `check` SCRIPT — the caller reports that; it is not a pass.
|
|
57
|
+
*/
|
|
58
|
+
export declare function planLanes(scripts: Readonly<Record<string, string>>, base: string | undefined): readonly Lane[];
|
|
59
|
+
//# sourceMappingURL=push-plan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push-plan.d.ts","sourceRoot":"","sources":["../../src/hooks/push-plan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,oCAAoC;AACpC,MAAM,MAAM,IAAI;AACd,4DAA4D;AAC1D;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AAC5E,wEAAwE;GACtE;IACE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AACH,6BAA6B;GAC3B;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAY5E;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,CA4BtE;AA8DD;;;;;;GAMG;AACH,wBAAgB,SAAS,CACvB,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EACzC,IAAI,EAAE,MAAM,GAAG,SAAS,GACvB,SAAS,IAAI,EAAE,CAajB"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** One line of git's pre-push stdin. */
|
|
2
|
+
export type PushRef = {
|
|
3
|
+
readonly localRef: string;
|
|
4
|
+
readonly localSha: string;
|
|
5
|
+
readonly remoteRef: string;
|
|
6
|
+
readonly remoteSha: string;
|
|
7
|
+
};
|
|
8
|
+
export type PushScope =
|
|
9
|
+
/** The push changes no file (a deletion, or a branch at its base): nothing to check. */
|
|
10
|
+
{
|
|
11
|
+
readonly kind: 'empty';
|
|
12
|
+
readonly why: string;
|
|
13
|
+
}
|
|
14
|
+
/** Measured from `base`: `changed` is every path that differs between it and the tip. */
|
|
15
|
+
| {
|
|
16
|
+
readonly kind: 'scoped';
|
|
17
|
+
readonly base: string;
|
|
18
|
+
readonly changed: readonly string[];
|
|
19
|
+
readonly why: string;
|
|
20
|
+
}
|
|
21
|
+
/** No usable base: run every lane, tests in full. */
|
|
22
|
+
| {
|
|
23
|
+
readonly kind: 'unscoped';
|
|
24
|
+
readonly why: string;
|
|
25
|
+
}
|
|
26
|
+
/** The pushed commit is not what is checked out, so the working tree cannot vouch for it. */
|
|
27
|
+
| {
|
|
28
|
+
readonly kind: 'elsewhere';
|
|
29
|
+
readonly why: string;
|
|
30
|
+
};
|
|
31
|
+
export declare function parsePushRefs(stdin: string): readonly PushRef[];
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the push git described on stdin. With no stdin (a manual run) the push is
|
|
34
|
+
* `HEAD` to a new branch — measured from the merge base with the default branch.
|
|
35
|
+
* 🔴 THE LANES RUN ON THE WORKING TREE, SO ONLY A PUSH OF `HEAD` CAN BE CHECKED HERE. Found
|
|
36
|
+
* in review 2026-09-23 and reproduced: `git push origin broken` from a clean `main`
|
|
37
|
+
* measured the right files, then ran `bun test --changed` against `main`'s tree, found
|
|
38
|
+
* nothing, and printed "passed". A ref that is not checked out now comes back
|
|
39
|
+
* `elsewhere`, which the caller reports as NOT CHECKED — never as a pass. (The old
|
|
40
|
+
* whole-`check` hook had the same blind spot; it just ran unrelated tests while in it.)
|
|
41
|
+
* ⚠️ WITH SEVERAL REFS, THE ONE AT `HEAD` IS MEASURED and the rest are named as unchecked.
|
|
42
|
+
*/
|
|
43
|
+
export declare function pushScope(root: string, remote: string, refs: readonly PushRef[]): Promise<PushScope>;
|
|
44
|
+
/**
|
|
45
|
+
* A path that changes what EVERY test runs on: a manifest, a lockfile, Bun's config, a
|
|
46
|
+
* tsconfig. Bun's `--changed` follows imports and none of these is imported — measured
|
|
47
|
+
* 2026-09-23, editing package.json selected 0 of 246 test files — so a push touching one
|
|
48
|
+
* runs the tests in full rather than none of them.
|
|
49
|
+
*/
|
|
50
|
+
export declare function changesEverything(path: string): boolean;
|
|
51
|
+
//# sourceMappingURL=push-range.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push-range.d.ts","sourceRoot":"","sources":["../../src/hooks/push-range.ts"],"names":[],"mappings":"AAiBA,wCAAwC;AACxC,MAAM,MAAM,OAAO,GAAG;IACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,SAAS;AACnB,wFAAwF;AACtF;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE;AAClD,yFAAyF;GACvF;IACE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AACH,qDAAqD;GACnD;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE;AACrD,6FAA6F;GAC3F;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAKzD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,OAAO,EAAE,CAS/D;AA4DD;;;;;;;;;;GAUG;AACH,wBAAsB,SAAS,CAC7B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,SAAS,OAAO,EAAE,GACvB,OAAO,CAAC,SAAS,CAAC,CAsCpB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGvD"}
|
package/dist/hooks/report.d.ts
CHANGED
|
@@ -10,6 +10,18 @@
|
|
|
10
10
|
*/
|
|
11
11
|
/** The two hooks this package implements. Named exactly as the git hook files are. */
|
|
12
12
|
export type Hook = 'pre-commit' | 'pre-push';
|
|
13
|
+
/**
|
|
14
|
+
* 🔴 A GIT HOOK EXPORTS `GIT_DIR` AND `GIT_INDEX_FILE`, AND EVERYTHING IT SPAWNS
|
|
15
|
+
* INHERITS THEM. Measured 2026-09-22 at the cost of two junk files and a stray commit
|
|
16
|
+
* on this repository's `main`: `pre-push` runs `bun run check`, `check` runs the test
|
|
17
|
+
* suite, and a test that builds a throwaway git repository and commits in it commits
|
|
18
|
+
* into THIS repository instead — `cwd` is ignored once `GIT_DIR` is set. The estate's
|
|
19
|
+
* own `packages/site/tests/checkout.test.ts` already carried a comment warning about
|
|
20
|
+
* exactly this, which is how much a convention is worth.
|
|
21
|
+
* ⛔ So the hook strips them rather than trusting fourteen test suites to remember. The
|
|
22
|
+
* gate must see the repository through `cwd`, the way CI does.
|
|
23
|
+
*/
|
|
24
|
+
export declare function withoutGitEnv(): Record<string, string | undefined>;
|
|
13
25
|
/**
|
|
14
26
|
* Run a command, streaming its output. Returns its exit code.
|
|
15
27
|
* `isolated` drops the inherited `GIT_*` variables — see `withoutGitEnv`.
|
|
@@ -17,13 +29,38 @@ export type Hook = 'pre-commit' | 'pre-push';
|
|
|
17
29
|
export declare function run(cmd: readonly string[], isolated?: boolean): Promise<number>;
|
|
18
30
|
/** Capture a command's stdout. Used for git plumbing only. */
|
|
19
31
|
export declare function capture(cmd: readonly string[]): Promise<string>;
|
|
32
|
+
/**
|
|
33
|
+
* Run a command, relaying its stdout live-enough (after it exits) while also handing the
|
|
34
|
+
* caller the text — gates.ts reads oxfmt's own "on N files" summary from it. `stderr` stays
|
|
35
|
+
* `inherit`: diagnostics (a parse error, "no files matched") must show immediately, and nothing
|
|
36
|
+
* here needs to inspect them.
|
|
37
|
+
*/
|
|
38
|
+
export declare function runCaptured(cmd: readonly string[]): Promise<{
|
|
39
|
+
readonly code: number;
|
|
40
|
+
readonly stdout: string;
|
|
41
|
+
}>;
|
|
42
|
+
/** Capture a command's stdout AND its exit code — git plumbing that answers by status. */
|
|
43
|
+
export declare function probe(cmd: readonly string[]): Promise<{
|
|
44
|
+
code: number;
|
|
45
|
+
stdout: string;
|
|
46
|
+
}>;
|
|
47
|
+
/**
|
|
48
|
+
* Run one pre-push lane through `sh -c` at `root`, as `bun run` would run a script line.
|
|
49
|
+
*
|
|
50
|
+
* ⚠️ `node_modules/.bin` FIRST ON PATH, because a lane opened up from a script is no longer
|
|
51
|
+
* run by `bun run`, which is what used to put it there — `vitest` or `oxfmt` in an expanded
|
|
52
|
+
* script would otherwise be "command not found" on a machine without a global copy.
|
|
53
|
+
* 🔴 AND WITHOUT THE HOOK'S `GIT_*` — see `withoutGitEnv`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function runLane(command: string, root: string): Promise<number>;
|
|
20
56
|
/**
|
|
21
57
|
* Resolve a dev tool to the project's own copy.
|
|
22
58
|
*
|
|
23
59
|
* ⚠️ NOT `bunx` BY DEFAULT. On a cache miss `bunx` downloads from the registry, and a
|
|
24
60
|
* git hook that reaches the network mid-commit is a hang waiting for a flaky link.
|
|
25
|
-
*
|
|
26
|
-
* the
|
|
61
|
+
* Git runs the hook with the caller's PATH, which has no `node_modules/.bin` (husky
|
|
62
|
+
* used to add it; the hooks no longer run through husky), so the local binary is named
|
|
63
|
+
* outright when it exists and `bunx` is only the fallback.
|
|
27
64
|
*/
|
|
28
65
|
export declare function tool(root: string, name: string): readonly string[];
|
|
29
66
|
export declare function ok(what: string): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../src/hooks/report.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,sFAAsF;AACtF,MAAM,MAAM,IAAI,GAAG,YAAY,GAAG,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../src/hooks/report.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,sFAAsF;AACtF,MAAM,MAAM,IAAI,GAAG,YAAY,GAAG,UAAU,CAAC;AAO7C;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAIlE;AAED;;;GAGG;AACH,wBAAsB,GAAG,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,EAAE,QAAQ,UAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAOnF;AAED,8DAA8D;AAC9D,wBAAsB,OAAO,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAErE;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,SAAS,MAAM,EAAE,GACrB,OAAO,CAAC;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAK7D;AAED,0FAA0F;AAC1F,wBAAsB,KAAK,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAI7F;AAED;;;;;;;GAOG;AACH,wBAAsB,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAU5E;AAED;;;;;;;;GAQG;AACH,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAGlE;AAaD,wBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAErC;AAED,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAEvC;AAED,8FAA8F;AAC9F,wBAAgB,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,KAAK,CAKjE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../../src/hooks/secrets.ts"],"names":[],"mappings":"AAoBA,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAqBvD"}
|
package/dist/hooks.d.ts
CHANGED
|
@@ -1,15 +1,39 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The HomeFlare git hooks, as a package.
|
|
3
|
+
*
|
|
4
|
+
* ★ WHY THIS IS NOT A SCRIPT IN EVERY REPO. Fourteen copies of a hook script is the
|
|
5
|
+
* exact drift `@homeflare/config` exists to prevent: the copies diverge, nobody
|
|
6
|
+
* notices, and two repos disagree about what a commit must satisfy. Here the repo
|
|
7
|
+
* commits a delegating wrapper and the behaviour ships with the package, so changing
|
|
8
|
+
* the rule is one release and a version bump rather than fourteen edits.
|
|
9
|
+
* ★ TRACKED `.husky/`, RUN BY GIT THROUGH `core.hooksPath` — NOT husky's `.husky/_`. Since
|
|
10
|
+
* 2026-09-23 the mechanism is the one that reaches every worktree; activate.ts has the
|
|
11
|
+
* measurement. The files keep the directory name the estate already uses.
|
|
12
|
+
* ★ PRE-PUSH IS SCOPED TO THE PUSH (push-range.ts, push-plan.ts): the repository's own
|
|
13
|
+
* `check`, with `bun test` narrowed to the tests the pushed files can reach and the build
|
|
14
|
+
* and smoke test left to CI. Many agents push to many repositories at once; a pre-push
|
|
15
|
+
* that re-ran every suite was the one people learned to skip.
|
|
16
|
+
*
|
|
17
|
+
* Usage from a hook file — see `HUSKY_HOOK`:
|
|
18
|
+
*
|
|
19
|
+
* bun node_modules/@homeflare/config/bin/hooks.ts pre-commit
|
|
20
|
+
*/
|
|
21
|
+
import { activateHooks } from './hooks/activate.ts';
|
|
22
|
+
import { HOOK_NAMES, HUSKY_HOOK, PREPARE, installHooks, problemsInHooks } from './hooks/install.ts';
|
|
23
|
+
import { type Lane, planLanes } from './hooks/push-plan.ts';
|
|
2
24
|
import { type Hook } from './hooks/report.ts';
|
|
3
|
-
export { HOOK_NAMES, HUSKY_HOOK, installHooks, problemsInHooks };
|
|
4
|
-
export type { Hook };
|
|
25
|
+
export { HOOK_NAMES, HUSKY_HOOK, PREPARE, activateHooks, installHooks, planLanes, problemsInHooks };
|
|
26
|
+
export type { Hook, Lane };
|
|
5
27
|
/** The commands `bin/hooks.ts` accepts. */
|
|
6
|
-
export type Command = Hook | 'install';
|
|
28
|
+
export type Command = Hook | 'install' | 'activate';
|
|
7
29
|
export declare function isCommand(value: string): value is Command;
|
|
8
30
|
/**
|
|
9
|
-
* Run one hook,
|
|
31
|
+
* Run one hook, write the wrappers, or activate them.
|
|
10
32
|
*
|
|
33
|
+
* `args` are git's own hook arguments (for `pre-push`: the remote name and URL) and
|
|
34
|
+
* `stdin` is what git wrote to the hook (for `pre-push`: the refs being pushed).
|
|
11
35
|
* ⛔ Never exits non-zero for a reason the caller cannot act on: an unknown command is a
|
|
12
36
|
* programming error in the wrapper and is reported as such, not as a failed commit.
|
|
13
37
|
*/
|
|
14
|
-
export declare function runCommand(command: Command, root: string): Promise<void>;
|
|
38
|
+
export declare function runCommand(command: Command, root: string, args?: readonly string[], stdin?: string): Promise<void>;
|
|
15
39
|
//# sourceMappingURL=hooks.d.ts.map
|
package/dist/hooks.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACpG,OAAO,EAAE,KAAK,IAAI,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,KAAK,IAAI,EAAY,MAAM,mBAAmB,CAAC;AAExD,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,SAAS,EAAE,eAAe,EAAE,CAAC;AACpG,YAAY,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AAE3B,2CAA2C;AAC3C,MAAM,MAAM,OAAO,GAAG,IAAI,GAAG,SAAS,GAAG,UAAU,CAAC;AAEpD,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,OAAO,CAEzD;AAED;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,SAAS,MAAM,EAAO,EAC5B,KAAK,SAAK,GACT,OAAO,CAAC,IAAI,CAAC,CAcf"}
|