@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/src/hooks.ts CHANGED
@@ -6,43 +6,59 @@
6
6
  * notices, and two repos disagree about what a commit must satisfy. Here the repo
7
7
  * commits a delegating wrapper and the behaviour ships with the package, so changing
8
8
  * the rule is one release and a version bump rather than fourteen edits.
9
- *
10
- * ⚠️ HUSKY, NOT lefthook OR A BARE `core.hooksPath`. It is already the estate's
11
- * mechanism in the repos that have working hooks, it installs from `prepare` on a
12
- * plain `bun install`, and it keeps the hook files tracked and reviewable. A second
13
- * mechanism alongside it would mean two ways to answer "are hooks on in this repo".
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.
14
16
  *
15
17
  * Usage from a hook file — see `HUSKY_HOOK`:
16
18
  *
17
19
  * bun node_modules/@homeflare/config/bin/hooks.ts pre-commit
18
20
  */
21
+ import { activateHooks } from './hooks/activate.ts';
19
22
  import { preCommit, prePush } from './hooks/gates.ts';
20
- import { HOOK_NAMES, HUSKY_HOOK, installHooks, problemsInHooks } from './hooks/install.ts';
23
+ import { HOOK_NAMES, HUSKY_HOOK, PREPARE, installHooks, problemsInHooks } from './hooks/install.ts';
24
+ import { type Lane, planLanes } from './hooks/push-plan.ts';
21
25
  import { type Hook, note, ok } from './hooks/report.ts';
22
26
 
23
- export { HOOK_NAMES, HUSKY_HOOK, installHooks, problemsInHooks };
24
- export type { Hook };
27
+ export { HOOK_NAMES, HUSKY_HOOK, PREPARE, activateHooks, installHooks, planLanes, problemsInHooks };
28
+ export type { Hook, Lane };
25
29
 
26
30
  /** The commands `bin/hooks.ts` accepts. */
27
- export type Command = Hook | 'install';
31
+ export type Command = Hook | 'install' | 'activate';
28
32
 
29
33
  export function isCommand(value: string): value is Command {
30
- return value === 'pre-commit' || value === 'pre-push' || value === 'install';
34
+ return ['pre-commit', 'pre-push', 'install', 'activate'].includes(value);
31
35
  }
32
36
 
33
37
  /**
34
- * Run one hook, or install the wrappers.
38
+ * Run one hook, write the wrappers, or activate them.
35
39
  *
40
+ * `args` are git's own hook arguments (for `pre-push`: the remote name and URL) and
41
+ * `stdin` is what git wrote to the hook (for `pre-push`: the refs being pushed).
36
42
  * ⛔ Never exits non-zero for a reason the caller cannot act on: an unknown command is a
37
43
  * programming error in the wrapper and is reported as such, not as a failed commit.
38
44
  */
39
- export async function runCommand(command: Command, root: string): Promise<void> {
45
+ export async function runCommand(
46
+ command: Command,
47
+ root: string,
48
+ args: readonly string[] = [],
49
+ stdin = '',
50
+ ): Promise<void> {
40
51
  if (command === 'install') {
41
52
  const written = await installHooks(root);
42
53
  ok(`wrote ${written.join(', ')} — commit them`);
43
- note('they do nothing until `bun install` runs `prepare` (husky)');
54
+ note(`then set "prepare": "${PREPARE}" so every install activates them`);
55
+ return;
56
+ }
57
+ if (command === 'activate') {
58
+ const result = await activateHooks(root);
59
+ (result.active ? ok : note)(`homeflare hooks: ${result.message}`);
44
60
  return;
45
61
  }
46
62
  if (command === 'pre-commit') return await preCommit(root);
47
- return await prePush(root);
63
+ return await prePush(root, args, stdin);
48
64
  }
@@ -6,9 +6,15 @@
6
6
  * this house are the product. The rendered workflows are written as text with holes;
7
7
  * only the step lists, whose shape varies per repository, go through here.
8
8
  *
9
- * ⚠️ Bun can PARSE YAML natively (`Bun.YAML.parse`) but does not stringify it, measured
10
- * against Bun 1.4.0 on 2026-09-22. The tests parse what this writes with `Bun.YAML.parse`
11
- * and compare structures, so a malformed emission fails rather than shipping.
9
+ * ⚠️ Bun.YAML.stringify EXISTS (`Object.keys(Bun.YAML)` is `["parse", "stringify"]`,
10
+ * measured against Bun 1.4.0 on 2026-09-23) but its output does not fit these rules:
11
+ * (a) a multi-line `run:` comes out as a double-quoted string with `\n` escapes, e.g.
12
+ * `run: "echo a\necho b\n"`, not a `|` block scalar; (b) `09:00` comes out UNQUOTED,
13
+ * e.g. `cron: 09:00`, the YAML 1.1 sexagesimal trap the `scalar()` comment below guards
14
+ * against; (c) a mapping key is followed by a trailing space, e.g. `"steps: \n - ..."`;
15
+ * and (d) a plain JS object has nowhere to attach a comment, so it cannot carry one. The
16
+ * tests parse what this writes with `Bun.YAML.parse` and compare structures, so a
17
+ * malformed emission fails rather than shipping.
12
18
  */
13
19
  import type { JobStep } from './shape.ts';
14
20
 
@@ -53,13 +59,29 @@ function renderMapping(
53
59
  return Object.entries(entries).map(([key, value]) => `${indent(depth)}${key}: ${scalar(value)}`);
54
60
  }
55
61
 
62
+ /**
63
+ * Trim trailing `\n` characters the way `command.replace(/\n+$/, '')` used to, but
64
+ * linear instead of backtracking — the same pattern as `normalizeBaseUrl` in
65
+ * `packages/distilled-netbox/src/credentials.ts`. Exported so a test can compare it
66
+ * against the old regex directly.
67
+ *
68
+ * ⚠️ Linear on purpose: a `/\n+$/` regex backtracks polynomially on a long run of
69
+ * "\n" that is not at the end (CodeQL js/polynomial-redos), and `command` here is
70
+ * repository-configured step text.
71
+ */
72
+ export function trimTrailingNewlines(value: string): string {
73
+ let end = value.length;
74
+ while (end > 0 && value.charCodeAt(end - 1) === 10) end--;
75
+ return value.slice(0, end);
76
+ }
77
+
56
78
  /**
57
79
  * ★ BLOCK SCALAR FOR EVERY MULTI-LINE `run:`. A folded or quoted form would join the
58
80
  * lines, and a shell script whose `if` and `then` end up on one line is a syntax error
59
81
  * at job time rather than at lint time. `|` keeps them exactly as written.
60
82
  */
61
83
  function renderRun(command: string, depth: number): string[] {
62
- const lines = command.replace(/\n+$/, '').split('\n');
84
+ const lines = trimTrailingNewlines(command).split('\n');
63
85
  if (lines.length === 1) return [`${indent(depth)}run: ${scalar(lines[0] ?? '')}`];
64
86
  return [`${indent(depth)}run: |`, ...lines.map((line) => `${indent(depth + 1)}${line}`)];
65
87
  }