@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.
Files changed (40) hide show
  1. package/README.md +11 -41
  2. package/bin/hooks.ts +8 -4
  3. package/dist/hooks/activate.d.ts +7 -0
  4. package/dist/hooks/activate.d.ts.map +1 -0
  5. package/dist/hooks/gates.d.ts +7 -14
  6. package/dist/hooks/gates.d.ts.map +1 -1
  7. package/dist/hooks/install.d.ts +7 -1
  8. package/dist/hooks/install.d.ts.map +1 -1
  9. package/dist/hooks/oxfmt-config.d.ts +25 -0
  10. package/dist/hooks/oxfmt-config.d.ts.map +1 -0
  11. package/dist/hooks/push-plan.d.ts +59 -0
  12. package/dist/hooks/push-plan.d.ts.map +1 -0
  13. package/dist/hooks/push-range.d.ts +51 -0
  14. package/dist/hooks/push-range.d.ts.map +1 -0
  15. package/dist/hooks/report.d.ts +39 -2
  16. package/dist/hooks/report.d.ts.map +1 -1
  17. package/dist/hooks/secrets.d.ts +2 -0
  18. package/dist/hooks/secrets.d.ts.map +1 -0
  19. package/dist/hooks.d.ts +30 -6
  20. package/dist/hooks.d.ts.map +1 -1
  21. package/dist/hooks.js +408 -41
  22. package/dist/hooks.js.map +12 -7
  23. package/dist/repo-shape/yaml.d.ts +20 -3
  24. package/dist/repo-shape/yaml.d.ts.map +1 -1
  25. package/dist/repo-shape.js +8 -2
  26. package/dist/repo-shape.js.map +3 -3
  27. package/dist/versions.js +8 -2
  28. package/dist/versions.js.map +3 -3
  29. package/docs/hooks.md +118 -0
  30. package/package.json +1 -1
  31. package/src/hooks/activate.ts +71 -0
  32. package/src/hooks/gates.ts +156 -39
  33. package/src/hooks/install.ts +39 -20
  34. package/src/hooks/oxfmt-config.ts +67 -0
  35. package/src/hooks/push-plan.ts +167 -0
  36. package/src/hooks/push-range.ts +177 -0
  37. package/src/hooks/report.ts +46 -6
  38. package/src/hooks/secrets.ts +42 -0
  39. package/src/hooks.ts +30 -14
  40. 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. The behaviour ships here; a repo commits a
149
- delegating wrapper and nothing else.
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 add -D @homeflare/config husky
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[2] ?? '';
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
- await runCommand(command, process.cwd());
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"}
@@ -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 repo's own declared gate before the push reaches the runner.
3
+ * Run the repository's own `check`, narrowed to what the push can affect.
11
4
  *
12
- * ★ IT CALLS `bun run check` RATHER THAN NAMING TOOLS. Every repo's `check` is the
13
- * command CI runs; hard-coding `tsc` and `bun test` here would drift from whichever
14
- * repo added a step, and the hook would certify a push CI rejects.
15
- * ⛔ It does not widen a narrow `check`. If a repo's gate only looks at part of the
16
- * tree, this hook inherits exactly that blind spot — fix the script, not the hook.
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":"AAeA;;;;;;GAMG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA8C3D;AAED;;;;;;;;GAQG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAwBzD"}
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"}
@@ -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 checkout with no `node_modules` would
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":"AAYA,mFAAmF;AACnF,eAAO,MAAM,UAAU,YAAI,YAAY,EAAE,UAAU,CAAU,CAAC;AAK9D;;;;;GAKG;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,CA+BpF"}
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"}
@@ -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
- * husky puts `node_modules/.bin` on PATH, but this runs outside husky in tests, so
26
- * the local binary is named outright when it exists and `bunx` is only the fallback.
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;AAwB7C;;;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,CAKrE;AAED;;;;;;;GAOG;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"}
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,2 @@
1
+ export declare function scanStagedSecrets(): Promise<void>;
2
+ //# sourceMappingURL=secrets.d.ts.map
@@ -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
- import { HOOK_NAMES, HUSKY_HOOK, installHooks, problemsInHooks } from './hooks/install.ts';
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, or install the wrappers.
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
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC3F,OAAO,EAAE,KAAK,IAAI,EAAY,MAAM,mBAAmB,CAAC;AAExD,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,eAAe,EAAE,CAAC;AACjE,YAAY,EAAE,IAAI,EAAE,CAAC;AAErB,2CAA2C;AAC3C,MAAM,MAAM,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC;AAEvC,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,OAAO,CAEzD;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAS9E"}
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"}