boru 1.0.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.
- package/README.md +97 -0
- package/bin/cli.js +243 -0
- package/package.json +20 -0
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# ⚡ Boru
|
|
2
|
+
|
|
3
|
+
> **Zero dependencies. No magic. Just native Git hooks.**
|
|
4
|
+
> Scaffolds git hooks directly into your project, inspired by the [shadcn/ui](https://ui.shadcn.com/) philosophy.
|
|
5
|
+
|
|
6
|
+
[](https://www.npmjs.com/package/boru)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
|
|
9
|
+
## 🤔 Why?
|
|
10
|
+
|
|
11
|
+
Traditional tools like Husky are powerful but often introduce:
|
|
12
|
+
|
|
13
|
+
1. **Hidden Abstractions:** Logic buried in `node_modules` or complex configs.
|
|
14
|
+
2. **Runtime Overhead:** Node.js is required just to run a shell command.
|
|
15
|
+
3. **Breaking Changes:** Updates to the tool can break your workflow.
|
|
16
|
+
|
|
17
|
+
**Boru** takes a different approach. It creates simple, editable shell scripts in a `.boru` folder. Once installed, **you own the code**. You can delete this tool, and your hooks will keep working.
|
|
18
|
+
|
|
19
|
+
## ✨ Features
|
|
20
|
+
|
|
21
|
+
- 📦 **Zero Runtime Dependencies:** Your hooks are just shell scripts.
|
|
22
|
+
- 🚀 **Native Speed:** Uses Git's native `core.hooksPath`.
|
|
23
|
+
- 📝 **Smart Templates:** Includes ready-to-use templates for `pre-commit`, `pre-push` (branch protection), and `commit-msg`.
|
|
24
|
+
- 🛠️ **Fully Customizable:** Edit your hooks directly in your editor.
|
|
25
|
+
- 🎛️ **Interactive CLI:** Select only the hooks you need (Space to toggle).
|
|
26
|
+
- 💾 **Git Control:** The CLI asks if you want to stage the new files to Git automatically.
|
|
27
|
+
|
|
28
|
+
## 🚀 Quick Start
|
|
29
|
+
|
|
30
|
+
Run the command in the root of your git repository:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx boru
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 📂 How it works
|
|
37
|
+
|
|
38
|
+
The CLI guides you through a simple process:
|
|
39
|
+
|
|
40
|
+
1. **Selection:** Choose which hooks you want to generate.
|
|
41
|
+
2. **Scaffolding:** Creates the `.boru` folder with executable shell scripts.
|
|
42
|
+
3. **Configuration:** Runs `git config core.hooksPath .boru` locally.
|
|
43
|
+
4. **Staging (Optional):** Asks if you want to run `git add .boru` immediately.
|
|
44
|
+
|
|
45
|
+
## 📂 Directory Structure
|
|
46
|
+
|
|
47
|
+
After running the command, your project will look like this:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
├── .boru
|
|
51
|
+
│ ├── pre-commit <-- Edit this file!
|
|
52
|
+
│ ├── pre-push
|
|
53
|
+
│ └── commit-msg
|
|
54
|
+
├── package.json
|
|
55
|
+
└── README.md
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 🛠️ Usage Example
|
|
59
|
+
|
|
60
|
+
Open `.boru/pre-commit` and add your logic. It's just a shell script!
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
#!/bin/sh
|
|
64
|
+
|
|
65
|
+
echo "⚡ Running pre-commit checks..."
|
|
66
|
+
|
|
67
|
+
# 1. Run Linting
|
|
68
|
+
npm run lint
|
|
69
|
+
|
|
70
|
+
# 2. Run Tests
|
|
71
|
+
# If this fails (exit code != 0), the commit is aborted.
|
|
72
|
+
npm test
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 🤝 Team Workflow
|
|
76
|
+
|
|
77
|
+
Since the hooks are files in your repo, it is recommended to commit them to Git so your team shares the same rules. To ensure every developer on your team has the hooks activated when they clone the repo, add a simple `prepare` script to your `package.json`:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"scripts": {
|
|
82
|
+
"prepare": "git config core.hooksPath .boru || true"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## 🗑️ Uninstallation
|
|
88
|
+
|
|
89
|
+
Because there is no magic, "uninstalling" is trivial:
|
|
90
|
+
|
|
91
|
+
1. Delete the `.boru` folder.
|
|
92
|
+
2. Run `git config --unset core.hooksPath`.
|
|
93
|
+
|
|
94
|
+
## 📄 License
|
|
95
|
+
|
|
96
|
+
MIT © Borja Muñoz
|
|
97
|
+
# boru
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require("node:fs");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const { execSync } = require("node:child_process");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 1. CONFIGURATION & TEMPLATES
|
|
8
|
+
*/
|
|
9
|
+
const HOOKS_DIR = ".boru";
|
|
10
|
+
|
|
11
|
+
const TEMPLATES = {
|
|
12
|
+
"pre-commit": {
|
|
13
|
+
desc: "Runs before creating a commit (tests, linting)",
|
|
14
|
+
content: `#!/bin/sh
|
|
15
|
+
# ------------------------------------------------------------------
|
|
16
|
+
# PRE-COMMIT HOOK
|
|
17
|
+
# This script runs before the commit is created.
|
|
18
|
+
# If it exits with a non-zero status, the commit is aborted.
|
|
19
|
+
# ------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
echo "→ ⚡ Running pre-commit checks..."
|
|
22
|
+
|
|
23
|
+
# Example: Run linter
|
|
24
|
+
# npm run lint
|
|
25
|
+
|
|
26
|
+
# Example: Run tests
|
|
27
|
+
# npm test
|
|
28
|
+
`,
|
|
29
|
+
},
|
|
30
|
+
"pre-push": {
|
|
31
|
+
desc: "Runs before pushing (e.g., protect main branch)",
|
|
32
|
+
content: `#!/bin/sh
|
|
33
|
+
# ------------------------------------------------------------------
|
|
34
|
+
# PRE-PUSH HOOK
|
|
35
|
+
# This script runs before changes are pushed to the remote.
|
|
36
|
+
# ------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
echo "→ 🚀 Running pre-push checks..."
|
|
39
|
+
|
|
40
|
+
# Example: Prevent pushing directly to main
|
|
41
|
+
# Note: The double backslash below is required for JS string escaping.
|
|
42
|
+
current_branch=$(git symbolic-ref HEAD | sed -e 's,.*/\\(.*\\),\\1,')
|
|
43
|
+
|
|
44
|
+
if [ "$current_branch" = "main" ]; then
|
|
45
|
+
echo "⚠️ Direct push to 'main' is restricted."
|
|
46
|
+
# Uncomment the next line to enforce the block:
|
|
47
|
+
# exit 1
|
|
48
|
+
fi
|
|
49
|
+
`,
|
|
50
|
+
},
|
|
51
|
+
"commit-msg": {
|
|
52
|
+
desc: "Validates the commit message (Conventional Commits)",
|
|
53
|
+
content: `#!/bin/sh
|
|
54
|
+
# ------------------------------------------------------------------
|
|
55
|
+
# COMMIT-MSG HOOK
|
|
56
|
+
# This script validates the commit message.
|
|
57
|
+
# The message file path is passed as the first argument ($1).
|
|
58
|
+
# ------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
echo "→ 📝 Checking commit message format..."
|
|
61
|
+
|
|
62
|
+
# Example: Use commitlint
|
|
63
|
+
# npx commitlint --edit "$1"
|
|
64
|
+
`,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* 2. UI COMPONENTS (Zero dependencies)
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
// Menú de selección múltiple (código previo)
|
|
72
|
+
async function multiselect(options) {
|
|
73
|
+
const { stdin, stdout } = process;
|
|
74
|
+
let cursor = 0;
|
|
75
|
+
const selected = new Set([0]);
|
|
76
|
+
|
|
77
|
+
stdout.write("\x1b[?25l");
|
|
78
|
+
|
|
79
|
+
const render = () => {
|
|
80
|
+
stdout.moveCursor(0, -options.length);
|
|
81
|
+
options.forEach((opt, i) => {
|
|
82
|
+
const isSelected = selected.has(i);
|
|
83
|
+
const isCursor = i === cursor;
|
|
84
|
+
const checkbox = isSelected ? "\x1b[32m◉\x1b[0m" : "\x1b[2m◯\x1b[0m";
|
|
85
|
+
const pointer = isCursor ? "\x1b[36m❯\x1b[0m" : " ";
|
|
86
|
+
const label = isCursor ? `\x1b[1m${opt.name}\x1b[0m` : opt.name;
|
|
87
|
+
const desc = `\x1b[2m- ${opt.desc}\x1b[0m`;
|
|
88
|
+
stdout.clearLine(0);
|
|
89
|
+
stdout.write(`${pointer} ${checkbox} ${label} ${desc}\n`);
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
stdout.write("\n".repeat(options.length));
|
|
94
|
+
render();
|
|
95
|
+
|
|
96
|
+
stdin.setRawMode(true);
|
|
97
|
+
stdin.resume();
|
|
98
|
+
stdin.setEncoding("utf8");
|
|
99
|
+
|
|
100
|
+
return new Promise((resolve) => {
|
|
101
|
+
const handleKey = (key) => {
|
|
102
|
+
if (key === "\u0003") {
|
|
103
|
+
stdout.write("\x1b[?25h");
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
if (key === "\u001b[A") {
|
|
107
|
+
cursor = cursor > 0 ? cursor - 1 : options.length - 1;
|
|
108
|
+
render();
|
|
109
|
+
}
|
|
110
|
+
if (key === "\u001b[B") {
|
|
111
|
+
cursor = cursor < options.length - 1 ? cursor + 1 : 0;
|
|
112
|
+
render();
|
|
113
|
+
}
|
|
114
|
+
if (key === " ") {
|
|
115
|
+
if (selected.has(cursor)) selected.delete(cursor);
|
|
116
|
+
else selected.add(cursor);
|
|
117
|
+
render();
|
|
118
|
+
}
|
|
119
|
+
if (key === "\r") {
|
|
120
|
+
stdin.removeListener("data", handleKey);
|
|
121
|
+
stdin.setRawMode(false);
|
|
122
|
+
stdin.pause();
|
|
123
|
+
stdout.write("\x1b[?25h");
|
|
124
|
+
resolve(Array.from(selected).map((i) => options[i].key));
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
stdin.on("data", handleKey);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function confirm(question) {
|
|
132
|
+
const { stdin, stdout } = process;
|
|
133
|
+
|
|
134
|
+
stdout.write(`${question} \x1b[2m(Y/n)\x1b[0m `);
|
|
135
|
+
|
|
136
|
+
stdin.setRawMode(true);
|
|
137
|
+
stdin.resume();
|
|
138
|
+
stdin.setEncoding("utf8");
|
|
139
|
+
|
|
140
|
+
return new Promise((resolve) => {
|
|
141
|
+
const handleKey = (key) => {
|
|
142
|
+
if (key === "\u0003") {
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (key === "\r" || key.toLowerCase() === "y") {
|
|
147
|
+
stdout.write("\x1b[32mYes\x1b[0m\n");
|
|
148
|
+
cleanup(true);
|
|
149
|
+
} else if (key.toLowerCase() === "n") {
|
|
150
|
+
stdout.write("\x1b[31mNo\x1b[0m\n");
|
|
151
|
+
cleanup(false);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const cleanup = (result) => {
|
|
156
|
+
stdin.removeListener("data", handleKey);
|
|
157
|
+
stdin.setRawMode(false);
|
|
158
|
+
stdin.pause();
|
|
159
|
+
resolve(result);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
stdin.on("data", handleKey);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 3. MAIN LOGIC
|
|
168
|
+
*/
|
|
169
|
+
async function main() {
|
|
170
|
+
console.clear();
|
|
171
|
+
console.log(
|
|
172
|
+
`\x1b[1m⚡ Setup Git Hooks\x1b[0m \x1b[2m(Space to select, Enter to confirm)\x1b[0m\n`
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
execSync("git rev-parse --is-inside-work-tree", { stdio: "ignore" });
|
|
177
|
+
} catch {
|
|
178
|
+
console.error("❌ Error: Not inside a Git repository.");
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const choices = Object.keys(TEMPLATES).map((key) => ({
|
|
183
|
+
key,
|
|
184
|
+
name: key,
|
|
185
|
+
desc: TEMPLATES[key].desc,
|
|
186
|
+
}));
|
|
187
|
+
|
|
188
|
+
const selectedKeys = await multiselect(choices);
|
|
189
|
+
|
|
190
|
+
console.log("\n");
|
|
191
|
+
|
|
192
|
+
if (selectedKeys.length === 0) {
|
|
193
|
+
console.log("⚠ No hooks selected. Exiting.");
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const targetDir = path.join(process.cwd(), HOOKS_DIR);
|
|
198
|
+
if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir);
|
|
199
|
+
|
|
200
|
+
// Crear archivos
|
|
201
|
+
selectedKeys.forEach((key) => {
|
|
202
|
+
const filePath = path.join(targetDir, key);
|
|
203
|
+
if (!fs.existsSync(filePath)) {
|
|
204
|
+
fs.writeFileSync(filePath, TEMPLATES[key].content);
|
|
205
|
+
try {
|
|
206
|
+
fs.chmodSync(filePath, "755");
|
|
207
|
+
} catch {}
|
|
208
|
+
console.log(` \x1b[32m✔\x1b[0m Created \x1b[1m${key}\x1b[0m`);
|
|
209
|
+
} else {
|
|
210
|
+
console.log(
|
|
211
|
+
` \x1b[33m•\x1b[0m Skipped \x1b[1m${key}\x1b[0m (already exists)`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Configurar Git
|
|
217
|
+
try {
|
|
218
|
+
execSync(`git config core.hooksPath ${HOOKS_DIR}`);
|
|
219
|
+
console.log(`\n\x1b[2m✔ Git configured to use ${HOOKS_DIR}\x1b[0m`);
|
|
220
|
+
} catch (e) {
|
|
221
|
+
console.error("❌ Failed to configure git.");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
console.log();
|
|
225
|
+
const shouldStage = await confirm(
|
|
226
|
+
`\x1b[36m?\x1b[0m Do you want to stage these files to Git?`
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
if (shouldStage) {
|
|
230
|
+
try {
|
|
231
|
+
execSync(`git add ${HOOKS_DIR}`);
|
|
232
|
+
console.log(` \x1b[32m✔\x1b[0m Files added to staging area.`);
|
|
233
|
+
} catch (e) {
|
|
234
|
+
console.log(` \x1b[31m❌\x1b[0m Failed to git add.`);
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
console.log(` \x1b[2m•\x1b[0m Skipping git add.`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
console.log(`\n\x1b[32mDone! Edit your files inside /${HOOKS_DIR}\x1b[0m`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "boru",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Own your hooks",
|
|
5
|
+
"main": "bin/cli.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"boru": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"git",
|
|
11
|
+
"hooks",
|
|
12
|
+
"husky-alternative",
|
|
13
|
+
"dx"
|
|
14
|
+
],
|
|
15
|
+
"author": "Borja Muñoz",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
}
|
|
20
|
+
}
|