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.
Files changed (3) hide show
  1. package/README.md +97 -0
  2. package/bin/cli.js +243 -0
  3. 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
+ [![npm version](https://img.shields.io/npm/v/boru.svg?style=flat-square)](https://www.npmjs.com/package/boru)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](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
+ }