Skip to content

Commit 343ef91

Browse files
committed
fix[PREPARE]: Conditionally install Git hooks
1 parent 593a6e9 commit 343ef91

6 files changed

Lines changed: 199 additions & 130 deletions

File tree

.github/copilot-instructions.md

Lines changed: 0 additions & 127 deletions
This file was deleted.

bun.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "lumen",
3-
"version": "0.4.115",
3+
"version": "0.4.116",
44
"devDependencies": {
55
"@biomejs/biome": "^2.4.8",
66
"@changesets/cli": "^2.30.0",
@@ -15,6 +15,7 @@
1515
"@types/react": "^19.2.14",
1616
"@types/react-dom": "^19.2.3",
1717
"babel-plugin-react-compiler": "^1.0.0",
18+
"bun-types": "^1.3.11",
1819
"concurrently": "^9.2.1",
1920
"fake-indexeddb": "^6.2.5",
2021
"happy-dom": "^20.8.7",
@@ -37,7 +38,7 @@
3738
"changeset": "changeset",
3839
"changeset:version": "changeset version",
3940
"postinstall": "bun run db:gen",
40-
"prepare": "lefthook install",
41+
"prepare": "bun ./scripts/install/prepare.ts",
4142
"dev": "turbo run dev",
4243
"dev:web": "concurrently --names \"web,workers\" --prefix-colors \"blue,red\" \"cd apps/web && bun run dev\" \"cd apps/workers && bun run dev\"",
4344
"dev:linux": "concurrently --names \"native,workers\" --prefix-colors \"yellow,red\" \"cd apps/native && bun run dev:linux\" \"cd apps/workers && bun run dev\"",

scripts/install/prepare.test.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { describe, expect, mock, test } from "bun:test";
2+
3+
import { detectGitState, installHooks, runPrepare } from "./prepare";
4+
5+
describe("detectGitState", () => {
6+
test("returns worktree when git rev-parse succeeds", () => {
7+
expect(
8+
detectGitState(() => ({
9+
status: 0,
10+
}))
11+
).toBe("worktree");
12+
});
13+
14+
test("returns missing when git rev-parse fails", () => {
15+
expect(
16+
detectGitState(() => ({
17+
status: 128,
18+
}))
19+
).toBe("missing");
20+
});
21+
22+
test("returns unavailable when git cannot be executed", () => {
23+
expect(
24+
detectGitState(() => ({
25+
error: new Error("spawn git ENOENT"),
26+
status: null,
27+
}))
28+
).toBe("unavailable");
29+
});
30+
});
31+
32+
describe("installHooks", () => {
33+
test("returns the lefthook exit code", () => {
34+
expect(
35+
installHooks((command, args, options) => {
36+
expect(command).toBe("lefthook");
37+
expect(args).toEqual(["install"]);
38+
expect(options).toEqual({ stdio: "inherit" });
39+
40+
return {
41+
status: 0,
42+
};
43+
})
44+
).toBe(0);
45+
});
46+
47+
test("throws when lefthook cannot be executed", () => {
48+
expect(() =>
49+
installHooks(() => ({
50+
error: new Error("spawn lefthook ENOENT"),
51+
status: null,
52+
}))
53+
).toThrow("spawn lefthook ENOENT");
54+
});
55+
});
56+
57+
describe("runPrepare", () => {
58+
test("skips hook installation outside a git worktree", () => {
59+
const log = mock(() => undefined);
60+
const runInstallHooks = mock(() => 1);
61+
62+
expect(
63+
runPrepare({
64+
detectGitState: () => "missing",
65+
installHooks: runInstallHooks,
66+
log,
67+
})
68+
).toBe(0);
69+
70+
expect(runInstallHooks).not.toHaveBeenCalled();
71+
expect(log).toHaveBeenCalledWith(
72+
"Skipping lefthook install: no git worktree detected."
73+
);
74+
});
75+
76+
test("skips hook installation when git is unavailable", () => {
77+
const log = mock(() => undefined);
78+
const runInstallHooks = mock(() => 1);
79+
80+
expect(
81+
runPrepare({
82+
detectGitState: () => "unavailable",
83+
installHooks: runInstallHooks,
84+
log,
85+
})
86+
).toBe(0);
87+
88+
expect(runInstallHooks).not.toHaveBeenCalled();
89+
expect(log).toHaveBeenCalledWith(
90+
"Skipping lefthook install: git is unavailable in this environment."
91+
);
92+
});
93+
94+
test("installs hooks inside a git worktree", () => {
95+
const log = mock(() => undefined);
96+
const runInstallHooks = mock(() => 7);
97+
98+
expect(
99+
runPrepare({
100+
detectGitState: () => "worktree",
101+
installHooks: runInstallHooks,
102+
log,
103+
})
104+
).toBe(7);
105+
106+
expect(runInstallHooks).toHaveBeenCalledTimes(1);
107+
expect(log).not.toHaveBeenCalled();
108+
});
109+
});

scripts/install/prepare.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { spawnSync } from "node:child_process";
2+
3+
type GitState = "missing" | "unavailable" | "worktree";
4+
5+
interface CommandOptions {
6+
stdio: "ignore" | "inherit";
7+
}
8+
9+
interface CommandResult {
10+
error?: Error;
11+
status: number | null;
12+
}
13+
14+
type CommandRunner = (
15+
command: string,
16+
args: string[],
17+
options: CommandOptions
18+
) => CommandResult;
19+
20+
interface PrepareDependencies {
21+
detectGitState: () => GitState;
22+
installHooks: () => number;
23+
log: (message: string) => void;
24+
}
25+
26+
export const spawnCommand: CommandRunner = (command, args, options) => {
27+
const result = spawnSync(command, args, options);
28+
29+
return {
30+
error: result.error,
31+
status: result.status,
32+
};
33+
};
34+
35+
export function detectGitState(
36+
commandRunner: CommandRunner = spawnCommand
37+
): GitState {
38+
const result = commandRunner("git", ["rev-parse", "--is-inside-work-tree"], {
39+
stdio: "ignore",
40+
});
41+
42+
if (result.error) {
43+
return "unavailable";
44+
}
45+
46+
return result.status === 0 ? "worktree" : "missing";
47+
}
48+
49+
export function installHooks(
50+
commandRunner: CommandRunner = spawnCommand
51+
): number {
52+
const result = commandRunner("lefthook", ["install"], {
53+
stdio: "inherit",
54+
});
55+
56+
if (result.error) {
57+
throw result.error;
58+
}
59+
60+
return result.status ?? 1;
61+
}
62+
63+
export function runPrepare({
64+
detectGitState: getGitState = detectGitState,
65+
installHooks: runInstallHooks = installHooks,
66+
log = console.log,
67+
}: Partial<PrepareDependencies> = {}): number {
68+
const gitState = getGitState();
69+
70+
if (gitState === "missing") {
71+
log("Skipping lefthook install: no git worktree detected.");
72+
return 0;
73+
}
74+
75+
if (gitState === "unavailable") {
76+
log("Skipping lefthook install: git is unavailable in this environment.");
77+
return 0;
78+
}
79+
80+
return runInstallHooks();
81+
}
82+
83+
if (import.meta.main) {
84+
process.exit(runPrepare());
85+
}

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"target": "ES2021",
44
"module": "preserve",
55
"moduleResolution": "bundler",
6-
"types": ["bun"],
6+
"types": ["node", "bun-types"],
77
"strict": true,
88
"noEmit": true,
99
"skipLibCheck": true,

0 commit comments

Comments
 (0)