@homeflare/config 0.9.0 → 0.11.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.
Files changed (59) hide show
  1. package/README.md +5 -40
  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/push-plan.d.ts +59 -0
  10. package/dist/hooks/push-plan.d.ts.map +1 -0
  11. package/dist/hooks/push-range.d.ts +51 -0
  12. package/dist/hooks/push-range.d.ts.map +1 -0
  13. package/dist/hooks/report.d.ts +29 -2
  14. package/dist/hooks/report.d.ts.map +1 -1
  15. package/dist/hooks/secrets.d.ts +2 -0
  16. package/dist/hooks/secrets.d.ts.map +1 -0
  17. package/dist/hooks.d.ts +30 -6
  18. package/dist/hooks.d.ts.map +1 -1
  19. package/dist/hooks.js +339 -31
  20. package/dist/hooks.js.map +11 -7
  21. package/dist/repo-shape/automerge.d.ts +17 -0
  22. package/dist/repo-shape/automerge.d.ts.map +1 -0
  23. package/dist/repo-shape/ci.d.ts.map +1 -1
  24. package/dist/repo-shape/companions.d.ts +2 -6
  25. package/dist/repo-shape/companions.d.ts.map +1 -1
  26. package/dist/repo-shape/dependabot.d.ts +35 -0
  27. package/dist/repo-shape/dependabot.d.ts.map +1 -0
  28. package/dist/repo-shape/guards.d.ts +25 -0
  29. package/dist/repo-shape/guards.d.ts.map +1 -0
  30. package/dist/repo-shape/render.d.ts.map +1 -1
  31. package/dist/repo-shape/shape.d.ts +40 -2
  32. package/dist/repo-shape/shape.d.ts.map +1 -1
  33. package/dist/repo-shape.d.ts +5 -2
  34. package/dist/repo-shape.d.ts.map +1 -1
  35. package/dist/repo-shape.js +269 -105
  36. package/dist/repo-shape.js.map +10 -7
  37. package/dist/versions.js +68 -31
  38. package/dist/versions.js.map +6 -5
  39. package/docs/hooks.md +99 -0
  40. package/docs/repo-shape-dependabot.md +126 -0
  41. package/docs/repo-shape-inputs.md +83 -0
  42. package/docs/repo-shape.md +5 -23
  43. package/package.json +1 -1
  44. package/src/hooks/activate.ts +71 -0
  45. package/src/hooks/gates.ts +87 -28
  46. package/src/hooks/install.ts +39 -20
  47. package/src/hooks/push-plan.ts +167 -0
  48. package/src/hooks/push-range.ts +177 -0
  49. package/src/hooks/report.ts +31 -6
  50. package/src/hooks/secrets.ts +42 -0
  51. package/src/hooks.ts +30 -14
  52. package/src/repo-shape/automerge.ts +144 -0
  53. package/src/repo-shape/ci.ts +35 -16
  54. package/src/repo-shape/companions.ts +2 -77
  55. package/src/repo-shape/dependabot.ts +136 -0
  56. package/src/repo-shape/guards.ts +68 -0
  57. package/src/repo-shape/render.ts +5 -1
  58. package/src/repo-shape/shape.ts +51 -35
  59. package/src/repo-shape.ts +8 -4
package/dist/hooks.js CHANGED
@@ -16,10 +16,23 @@ async function run(cmd, isolated = false) {
16
16
  return await proc.exited;
17
17
  }
18
18
  async function capture(cmd) {
19
+ return (await probe(cmd)).stdout;
20
+ }
21
+ async function probe(cmd) {
19
22
  const proc = Bun.spawn([...cmd], { stdout: "pipe", stderr: "ignore" });
20
- const text = await new Response(proc.stdout).text();
21
- await proc.exited;
22
- return text;
23
+ const stdout = await new Response(proc.stdout).text();
24
+ return { code: await proc.exited, stdout };
25
+ }
26
+ async function runLane(command, root) {
27
+ const env = withoutGitEnv();
28
+ env["PATH"] = `${root}/node_modules/.bin:${env["PATH"] ?? ""}`;
29
+ const proc = Bun.spawn(["sh", "-c", command], {
30
+ cwd: root,
31
+ env,
32
+ stdout: "inherit",
33
+ stderr: "inherit"
34
+ });
35
+ return await proc.exited;
23
36
  }
24
37
  function tool(root, name) {
25
38
  const local = `${root}/node_modules/.bin/${name}`;
@@ -44,12 +57,243 @@ function fail(hook, what, fix) {
44
57
  process.exit(1);
45
58
  }
46
59
 
60
+ // src/hooks/activate.ts
61
+ function inCi(env) {
62
+ const ci = env["CI"];
63
+ return ci !== undefined && ci !== "" && ci !== "0" && ci.toLowerCase() !== "false";
64
+ }
65
+ async function activateHooks(root, env = process.env) {
66
+ if (inCi(env))
67
+ return { active: false, message: "CI is set \u2014 hooks stay off; CI runs the gate" };
68
+ const where = await probe(["git", "-C", root, "rev-parse", "--show-prefix"]);
69
+ if (where.code !== 0)
70
+ return { active: false, message: "not a git work tree \u2014 nothing to do" };
71
+ const tracked = await Promise.all(["pre-commit", "pre-push"].map((name) => Bun.file(`${root}/.husky/${name}`).exists()));
72
+ if (!tracked.some(Boolean)) {
73
+ return { active: false, message: "no .husky/pre-commit or .husky/pre-push to point git at" };
74
+ }
75
+ const want = `${where.stdout.trim()}.husky`;
76
+ const current = (await probe(["git", "-C", root, "config", "--get", "core.hooksPath"])).stdout.trim();
77
+ if (current === want)
78
+ return { active: true, message: `core.hooksPath is already ${want}` };
79
+ const set = await probe(["git", "-C", root, "config", "core.hooksPath", want]);
80
+ if (set.code !== 0)
81
+ return { active: false, message: "git config core.hooksPath failed" };
82
+ const was = current === "" ? "unset" : current;
83
+ return {
84
+ active: true,
85
+ message: `core.hooksPath ${was} \u2192 ${want}, for every worktree of this clone`
86
+ };
87
+ }
88
+
89
+ // src/hooks/gates.ts
90
+ import { existsSync } from "fs";
91
+
92
+ // src/hooks/push-plan.ts
93
+ var LEFT_TO_CI = /^(build|smoke)(:|$)/;
94
+ var MAX_DEPTH = 8;
95
+ function andChain(script) {
96
+ const parts = [];
97
+ let quote;
98
+ let current = "";
99
+ for (let i = 0;i < script.length; i++) {
100
+ const char = script[i] ?? "";
101
+ if (quote !== undefined) {
102
+ if (char === quote)
103
+ quote = undefined;
104
+ current += char;
105
+ continue;
106
+ }
107
+ if (char === '"' || char === "'") {
108
+ quote = char;
109
+ current += char;
110
+ continue;
111
+ }
112
+ if (char === "&" && script[i + 1] === "&") {
113
+ parts.push(current.trim());
114
+ current = "";
115
+ i += 1;
116
+ continue;
117
+ }
118
+ if ("|;&<>`".includes(char) || char === "$" && script[i + 1] === "(")
119
+ return;
120
+ current += char;
121
+ }
122
+ if (quote !== undefined)
123
+ return;
124
+ parts.push(current.trim());
125
+ return parts.some((part) => part === "") ? undefined : parts;
126
+ }
127
+ function scriptRef(segment, scripts) {
128
+ const words = segment.split(/\s+/);
129
+ if (words.length === 2 && words[0] === "npm" && words[1] === "test")
130
+ return "test";
131
+ const [runner, verb, name] = words;
132
+ if (words.length !== 3 || verb !== "run" || name === undefined)
133
+ return;
134
+ if (runner !== "bun" && runner !== "npm")
135
+ return;
136
+ return Object.hasOwn(scripts, name) ? name : undefined;
137
+ }
138
+ var isBunTest = (segment) => /^bun\s+test(\s|$)/.test(segment);
139
+ function testLane(segment, base) {
140
+ return base === undefined ? { kind: "test", label: segment, command: segment, scoped: false } : { kind: "test", label: segment, command: `${segment} --changed=${base}`, scoped: true };
141
+ }
142
+ function expand(script, scripts, base, depth) {
143
+ const segments = andChain(script);
144
+ if (segments === undefined || depth > MAX_DEPTH)
145
+ return;
146
+ const lanes = [];
147
+ for (const segment of segments) {
148
+ if (isBunTest(segment)) {
149
+ lanes.push(testLane(segment, base));
150
+ continue;
151
+ }
152
+ const name = scriptRef(segment, scripts);
153
+ if (name === undefined) {
154
+ lanes.push({ kind: "run", label: segment, command: segment });
155
+ continue;
156
+ }
157
+ if (LEFT_TO_CI.test(name)) {
158
+ lanes.push({ kind: "skip", label: segment, why: "CI runs it on every pull request" });
159
+ continue;
160
+ }
161
+ const inner = expand(scripts[name] ?? "", scripts, base, depth + 1);
162
+ if (inner !== undefined && inner.some((lane) => lane.kind !== "run")) {
163
+ lanes.push(...inner);
164
+ } else if (/^test(:|$)/.test(name)) {
165
+ lanes.push({ kind: "test", label: segment, command: segment, scoped: false });
166
+ } else {
167
+ lanes.push({ kind: "run", label: segment, command: segment });
168
+ }
169
+ }
170
+ return lanes;
171
+ }
172
+ function planLanes(scripts, base) {
173
+ const check = scripts["check"];
174
+ if (check === undefined)
175
+ return [];
176
+ const whole = {
177
+ kind: "test",
178
+ label: "bun run check",
179
+ command: "bun run check",
180
+ scoped: false
181
+ };
182
+ return expand(check, scripts, base, 0) ?? [whole];
183
+ }
184
+
185
+ // src/hooks/push-range.ts
186
+ var ZERO = /^0+$/;
187
+ function parsePushRefs(stdin) {
188
+ const refs = [];
189
+ for (const line2 of stdin.split(`
190
+ `)) {
191
+ const [localRef, localSha, remoteRef, remoteSha] = line2.trim().split(/\s+/);
192
+ if (localRef && localSha && remoteRef && remoteSha) {
193
+ refs.push({ localRef, localSha, remoteRef, remoteSha });
194
+ }
195
+ }
196
+ return refs;
197
+ }
198
+ var short = (sha) => sha.slice(0, 9);
199
+ async function ok2(root, args) {
200
+ return (await probe(["git", "-C", root, ...args])).code === 0;
201
+ }
202
+ async function out(root, args) {
203
+ const result = await probe(["git", "-C", root, ...args]);
204
+ return result.code === 0 ? result.stdout.trim() : undefined;
205
+ }
206
+ async function defaultBranch(root, remote) {
207
+ const head = await out(root, ["symbolic-ref", "--quiet", `refs/remotes/${remote}/HEAD`]);
208
+ if (head !== undefined && head !== "")
209
+ return head;
210
+ for (const name of ["main", "master"]) {
211
+ const ref = `refs/remotes/${remote}/${name}`;
212
+ if (await ok2(root, ["rev-parse", "--verify", "--quiet", ref]))
213
+ return ref;
214
+ }
215
+ return;
216
+ }
217
+ async function baseFor(root, remote, ref) {
218
+ const { localSha, remoteSha } = ref;
219
+ if (!ZERO.test(remoteSha) && await ok2(root, ["cat-file", "-e", `${remoteSha}^{commit}`]) && await ok2(root, ["merge-base", "--is-ancestor", remoteSha, localSha])) {
220
+ return { base: remoteSha, why: `since ${short(remoteSha)}, what ${remote} has now` };
221
+ }
222
+ const branch = await defaultBranch(root, remote);
223
+ if (branch === undefined)
224
+ return { why: `no ${remote}/main or ${remote}/master to measure from` };
225
+ const mergeBase = await out(root, ["merge-base", localSha, branch]);
226
+ if (mergeBase === undefined || mergeBase === "") {
227
+ return { why: `no merge base with ${branch} (a shallow clone, or unrelated history)` };
228
+ }
229
+ const name = branch.replace(/^refs\/remotes\//, "");
230
+ return { base: mergeBase, why: `since ${short(mergeBase)}, where this branch left ${name}` };
231
+ }
232
+ async function pushScope(root, remote, refs) {
233
+ const pushed = refs.filter((ref2) => !ZERO.test(ref2.localSha));
234
+ if (refs.length > 0 && pushed.length === 0) {
235
+ return { kind: "empty", why: "this push only deletes refs" };
236
+ }
237
+ const head = await out(root, ["rev-parse", "HEAD"]);
238
+ const atHead = pushed.find((ref2) => ref2.localSha === head);
239
+ const others = pushed.filter((ref2) => ref2 !== atHead).map((ref2) => ref2.localRef);
240
+ if (pushed.length > 0 && atHead === undefined) {
241
+ return {
242
+ kind: "elsewhere",
243
+ why: `pushing ${others.join(", ")}, but the checkout is at ${short(head ?? "?")}`
244
+ };
245
+ }
246
+ const ref = atHead ?? {
247
+ localRef: "HEAD",
248
+ localSha: head ?? "HEAD",
249
+ remoteRef: "",
250
+ remoteSha: "0".repeat(40)
251
+ };
252
+ const found = await baseFor(root, remote, ref);
253
+ const also = others.length > 0 ? `; ${others.join(", ")} not checked here` : "";
254
+ if (!("base" in found))
255
+ return { kind: "unscoped", why: `${found.why}${also}` };
256
+ const diff = await probe([
257
+ "git",
258
+ "-C",
259
+ root,
260
+ "diff",
261
+ "--name-only",
262
+ "-z",
263
+ found.base,
264
+ ref.localSha
265
+ ]);
266
+ if (diff.code !== 0)
267
+ return { kind: "unscoped", why: `git diff ${short(found.base)} failed` };
268
+ const changed = diff.stdout.split("\x00").filter((path) => path !== "");
269
+ if (changed.length === 0)
270
+ return { kind: "empty", why: `no file differs ${found.why}${also}` };
271
+ return { kind: "scoped", base: found.base, changed, why: `${found.why}${also}` };
272
+ }
273
+ function changesEverything(path) {
274
+ const name = path.split("/").pop() ?? "";
275
+ return /^(package\.json|bun\.lockb?|bunfig\.toml|tsconfig.*\.json)$/.test(name);
276
+ }
277
+
278
+ // src/hooks/secrets.ts
279
+ var INSTALL = "brew install gitleaks \u2014 Linux: https://github.com/gitleaks/gitleaks/releases";
280
+ async function scanStagedSecrets() {
281
+ if (Bun.which("gitleaks") === null) {
282
+ fail("pre-commit", "gitleaks is not installed, so the staged changes were NOT scanned", INSTALL);
283
+ }
284
+ const code = await run(["gitleaks", "git", "--staged", "--redact", "--no-banner", "."]);
285
+ if (code !== 0) {
286
+ fail("pre-commit", "gitleaks found a secret in the staged changes", "remove it, then ROTATE it \u2014 assume anything committed is already compromised");
287
+ }
288
+ ok("pre-commit: gitleaks found no secret in the staged changes");
289
+ }
290
+
47
291
  // src/hooks/staged.ts
48
292
  var FORMATTABLE = /\.(ts|tsx|js|jsx|mjs|cjs|json|jsonc|md)$/;
49
293
  var CODE = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
50
294
  async function names(args) {
51
- const out = await capture(["git", ...args, "-z"]);
52
- return out.split("\x00").filter((line2) => line2.length > 0);
295
+ const out2 = await capture(["git", ...args, "-z"]);
296
+ return out2.split("\x00").filter((line2) => line2.length > 0);
53
297
  }
54
298
  async function staged() {
55
299
  const indexed = await names(["diff", "--cached", "--name-only", "--diff-filter=ACMR"]);
@@ -62,18 +306,27 @@ async function staged() {
62
306
  };
63
307
  }
64
308
  async function fingerprints(files) {
65
- const out = new Map;
309
+ const out2 = new Map;
66
310
  for (const file of files) {
67
311
  const handle = Bun.file(file);
68
312
  if (!await handle.exists())
69
313
  continue;
70
- out.set(file, String(Bun.hash(await handle.arrayBuffer())));
314
+ out2.set(file, String(Bun.hash(await handle.arrayBuffer())));
71
315
  }
72
- return out;
316
+ return out2;
73
317
  }
74
318
 
75
319
  // src/hooks/gates.ts
320
+ function installed(root, hook) {
321
+ if (existsSync(`${root}/node_modules`))
322
+ return true;
323
+ note(`${hook}: no node_modules in this worktree \u2014 run 'bun install'; skipping the rest`);
324
+ return false;
325
+ }
76
326
  async function preCommit(root) {
327
+ await scanStagedSecrets();
328
+ if (!installed(root, "pre-commit"))
329
+ return;
77
330
  const oxfmt = tool(root, "oxfmt");
78
331
  const { formattable, code, partial } = await staged();
79
332
  if (partial.length > 0) {
@@ -103,40 +356,82 @@ async function preCommit(root) {
103
356
  }
104
357
  ok(`pre-commit: ${formattable.length} staged file(s) formatted and linted`);
105
358
  }
106
- async function prePush(root) {
359
+ function describe(lane) {
360
+ if (lane.kind === "skip")
361
+ return `skip ${lane.label} \u2014 ${lane.why}`;
362
+ if (lane.kind === "test" && lane.scoped)
363
+ return `run ${lane.command} (only the tests the push can reach)`;
364
+ if (lane.kind === "test")
365
+ return `run ${lane.command} (IN FULL)`;
366
+ return `run ${lane.command}`;
367
+ }
368
+ async function prePush(root, args, stdin) {
107
369
  const manifest = Bun.file(`${root}/package.json`);
108
370
  const pkg = await manifest.exists() ? await manifest.json() : {};
109
- if (pkg.scripts?.["check"] === undefined) {
371
+ const scripts = pkg.scripts ?? {};
372
+ if (scripts["check"] === undefined) {
110
373
  note("pre-push: no `check` script declared in package.json; nothing to run");
111
374
  return;
112
375
  }
113
- note("pre-push: running `bun run check` \u2014 the same gate CI runs");
376
+ if (!installed(root, "pre-push"))
377
+ return;
378
+ const scope = await pushScope(root, args[0] ?? "origin", parsePushRefs(stdin));
379
+ if (scope.kind === "empty") {
380
+ ok(`pre-push: ${scope.why} \u2014 nothing to check`);
381
+ return;
382
+ }
383
+ if (scope.kind === "elsewhere") {
384
+ note(`pre-push: ${scope.why} \u2014 the working tree is not what is being pushed`);
385
+ note(" NOT CHECKED here; CI checks it. To check it locally, check it out and push from there.");
386
+ return;
387
+ }
388
+ let base;
389
+ if (scope.kind === "unscoped") {
390
+ note(`pre-push: ${scope.why} \u2014 every lane runs, tests in full`);
391
+ } else {
392
+ const global = scope.changed.filter(changesEverything);
393
+ note(`pre-push: ${String(scope.changed.length)} file(s) changed ${scope.why}`);
394
+ if (global.length > 0)
395
+ note(` ${global.join(", ")} changes what every test runs on \u2014 tests in full`);
396
+ else
397
+ base = scope.base;
398
+ }
399
+ const lanes = planLanes(scripts, base);
114
400
  const started = Bun.nanoseconds();
115
- if (await run(["bun", "run", "check"], true) !== 0) {
116
- fail("pre-push", "bun run check failed \u2014 CI would fail the same way, on a shared runner", "bun run lint:fix, then bun run check until it is green");
401
+ let ran = 0;
402
+ for (const lane of lanes) {
403
+ note(describe(lane));
404
+ if (lane.kind === "skip")
405
+ continue;
406
+ if (await runLane(lane.command, root) !== 0) {
407
+ fail("pre-push", `\`${lane.label}\` failed`, `${lane.command} \u2014 until it is green`);
408
+ }
409
+ ran += 1;
117
410
  }
118
- ok(`pre-push: bun run check passed in ${((Bun.nanoseconds() - started) / 1e9).toFixed(1)}s`);
411
+ const seconds = ((Bun.nanoseconds() - started) / 1e9).toFixed(1);
412
+ ok(`pre-push: ${String(ran)} lane(s) of \`check\` passed in ${seconds}s \u2014 CI runs the full gate`);
119
413
  }
120
414
 
121
415
  // src/hooks/install.ts
122
- import { chmod, mkdir } from "fs/promises";
416
+ import { chmod, mkdir, stat } from "fs/promises";
123
417
  var HOOK_NAMES = ["pre-commit", "pre-push"];
124
418
  var RUNNER = "node_modules/@homeflare/config/bin/hooks.ts";
419
+ var PREPARE = `bun ${RUNNER} activate`;
125
420
  var HUSKY_HOOK = `# HomeFlare shared git hook. The behaviour lives in @homeflare/config, not in this file,
126
421
  # and the same bytes are installed as .husky/pre-commit and .husky/pre-push \u2014 the hook
127
- # name comes from $0.
422
+ # name comes from $0, and git's arguments and stdin pass straight through.
128
423
  #
129
- # \u26A0\uFE0F A hook is a local convenience, not a gate: it is skippable with --no-verify and does
130
- # not exist in a fresh clone until \`bun install\` runs the \`prepare\` script. The
131
- # required checks on main stay the gate.
424
+ # \u26A0\uFE0F A hook is a local convenience, not a gate: it is skippable with --no-verify, and a
425
+ # worktree runs it only once \`bun install\` has run there. The required checks on main
426
+ # stay the gate.
132
427
  #
133
428
  # Regenerate this file with: bun ${RUNNER} install
134
429
  hook="${RUNNER}"
135
430
  if [ ! -f "$hook" ]; then
136
- echo "husky: $hook is missing \u2014 run 'bun install' to enable the HomeFlare hooks; skipping"
431
+ echo "homeflare hooks: $hook is missing \u2014 run 'bun install' in this worktree; skipping" >&2
137
432
  exit 0
138
433
  fi
139
- exec bun "$hook" "$(basename "$0")"
434
+ exec bun "$hook" "$(basename "$0")" "$@"
140
435
  `;
141
436
  async function installHooks(projectDir) {
142
437
  await mkdir(`${projectDir}/.husky`, { recursive: true });
@@ -155,14 +450,16 @@ async function problemsInHooks(projectDir) {
155
450
  if (!await manifest.exists())
156
451
  return ["package.json: missing"];
157
452
  const pkg = await manifest.json();
158
- if (!(pkg.scripts?.["prepare"] ?? "").includes("husky")) {
159
- problems.push('package.json: no "prepare": "husky" script \u2014 a fresh clone installs no hooks');
453
+ const prepare = pkg.scripts?.["prepare"] ?? "";
454
+ if (!/bin\/hooks\.ts activate/.test(prepare)) {
455
+ problems.push(`package.json: "prepare" does not run \`${PREPARE}\` \u2014 no clone or worktree gets hooks`);
160
456
  }
161
- if (pkg.devDependencies?.["husky"] === undefined) {
162
- problems.push("package.json: husky is not a devDependency");
457
+ if (/\bhusky\b/.test(prepare)) {
458
+ problems.push('package.json: "prepare" still runs husky, which re-points core.hooksPath at .husky/_');
163
459
  }
164
460
  for (const name of HOOK_NAMES) {
165
- const file = Bun.file(`${projectDir}/.husky/${name}`);
461
+ const path = `${projectDir}/.husky/${name}`;
462
+ const file = Bun.file(path);
166
463
  if (!await file.exists()) {
167
464
  problems.push(`.husky/${name}: missing \u2014 run \`bun ${RUNNER} install\``);
168
465
  continue;
@@ -170,33 +467,44 @@ async function problemsInHooks(projectDir) {
170
467
  if (await file.text() !== HUSKY_HOOK) {
171
468
  problems.push(`.husky/${name}: differs from the @homeflare/config wrapper \u2014 run \`bun ${RUNNER} install\`, or change it in the package`);
172
469
  }
470
+ if (((await stat(path)).mode & 73) === 0) {
471
+ problems.push(`.husky/${name}: not executable, so git ignores it \u2014 chmod +x it and commit`);
472
+ }
173
473
  }
174
474
  return problems;
175
475
  }
176
476
 
177
477
  // src/hooks.ts
178
478
  function isCommand(value) {
179
- return value === "pre-commit" || value === "pre-push" || value === "install";
479
+ return ["pre-commit", "pre-push", "install", "activate"].includes(value);
180
480
  }
181
- async function runCommand(command, root) {
481
+ async function runCommand(command, root, args = [], stdin = "") {
182
482
  if (command === "install") {
183
483
  const written = await installHooks(root);
184
484
  ok(`wrote ${written.join(", ")} \u2014 commit them`);
185
- note("they do nothing until `bun install` runs `prepare` (husky)");
485
+ note(`then set "prepare": "${PREPARE}" so every install activates them`);
486
+ return;
487
+ }
488
+ if (command === "activate") {
489
+ const result = await activateHooks(root);
490
+ (result.active ? ok : note)(`homeflare hooks: ${result.message}`);
186
491
  return;
187
492
  }
188
493
  if (command === "pre-commit")
189
494
  return await preCommit(root);
190
- return await prePush(root);
495
+ return await prePush(root, args, stdin);
191
496
  }
192
497
  export {
193
498
  HOOK_NAMES,
194
499
  HUSKY_HOOK,
500
+ PREPARE,
501
+ activateHooks,
195
502
  installHooks,
196
503
  isCommand,
504
+ planLanes,
197
505
  problemsInHooks,
198
506
  runCommand
199
507
  };
200
508
 
201
- //# debugId=3480C5F48A0E1AD864756E2164756E21
509
+ //# debugId=F70ED1E9F00E0B1964756E2164756E21
202
510
  //# sourceMappingURL=hooks.js.map