no-yolo-commits 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ataztech910
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # no-yolo-commits
2
+
3
+ [![npm version](https://img.shields.io/npm/v/no-yolo-commits.svg)](https://www.npmjs.com/package/no-yolo-commits)
4
+ [![license](https://img.shields.io/npm/l/no-yolo-commits.svg)](./LICENSE)
5
+
6
+ You know that feeling when you `git commit -m "fix"` straight onto `main` at 2am, staged changes you half-remember writing, and hit enter before your brain finishes the sentence "wait, should I—"?
7
+
8
+ This stops that.
9
+
10
+ ```bash
11
+ npx no-yolo-commits init
12
+ ```
13
+
14
+ One command. Two things happen on every commit from now on:
15
+
16
+ 1. **An AI actually reads your diff** (via the [`claude`](https://claude.com/claude-code) CLI) and blocks the commit — but only for real, high-confidence problems. Not vibes, not "you could refactor this." Type-safety holes, bugs that will actually bite, broken framework patterns. If the reviewer is missing, slow, or having a bad day, it fails **open** — a flaky linter should never be the reason your commit is stuck.
17
+ 2. **`main` and `master` become look-but-don't-touch.** Try to commit straight there and instead of yelling at you, it just... makes you a branch. `ACME-1788716508-fix-the-thing-you-were-actually-fixing`, ready to go, commit already on it. You didn't even have to think of a branch name — the AI wrote that from your diff too.
18
+
19
+ No dashboard. No config file to argue with. No dependencies at runtime — it's a ~150-line shell script wearing a `husky` trenchcoat.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ npx no-yolo-commits init
25
+ ```
26
+
27
+ That's the whole install. It will, in order:
28
+
29
+ - add `husky` as a devDependency (if you don't have it)
30
+ - set `"scripts.prepare": "husky"` in `package.json`
31
+ - drop `.husky/pre-commit` into your repo
32
+
33
+ ## Make it yours
34
+
35
+ ```bash
36
+ npx no-yolo-commits init \
37
+ --prefix=ACME \
38
+ --stack="Next.js + TypeScript + Postgres" \
39
+ --protect=main,master,release
40
+ ```
41
+
42
+ | Flag | Default | Does what it says |
43
+ |---|---|---|
44
+ | `--prefix` | your `package.json` name, shouted in caps | prefix for the auto-branch name |
45
+ | `--stack` | `TypeScript` | tells the reviewer what it's actually looking at, so findings are relevant instead of generic |
46
+ | `--protect` | `main,master` | which branches you're not allowed to just casually commit to |
47
+ | `--force`, `-f` | off | steamroll an existing `.husky/pre-commit` |
48
+
49
+ ## The eject button
50
+
51
+ This is a guardrail, not a cage. Bad day, emergency hotfix, you know exactly what you're doing:
52
+
53
+ ```bash
54
+ git commit --no-verify
55
+ ```
56
+
57
+ No questions asked. No shame either — that's what it's there for.
58
+
59
+ ## Why this exists
60
+
61
+ Because I kept copy-pasting the same `.husky/pre-commit` into every new project and hand-editing the branch prefix like some kind of caveman. Now it's `npx` and thirty seconds.
62
+
63
+ ## License
64
+
65
+ MIT — do whatever you want with it.
package/bin/cli.js ADDED
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { execSync, spawnSync } = require('child_process');
7
+
8
+ const CWD = process.cwd();
9
+
10
+ function fail(msg) {
11
+ console.error(`✗ ${msg}`);
12
+ process.exit(1);
13
+ }
14
+
15
+ function parseArgs(argv) {
16
+ const args = { command: 'init', prefix: null, stack: 'TypeScript', protect: 'main,master', force: false };
17
+ const rest = [];
18
+ for (const a of argv) {
19
+ if (a === '--force' || a === '-f') args.force = true;
20
+ else if (a === '--help' || a === '-h') args.command = 'help';
21
+ else if (a.startsWith('--prefix=')) args.prefix = a.slice('--prefix='.length);
22
+ else if (a.startsWith('--stack=')) args.stack = a.slice('--stack='.length);
23
+ else if (a.startsWith('--protect=')) args.protect = a.slice('--protect='.length);
24
+ else rest.push(a);
25
+ }
26
+ if (rest[0]) args.command = rest[0];
27
+ return args;
28
+ }
29
+
30
+ function printHelp() {
31
+ console.log(`no-yolo-commits — an AI pre-commit reviewer + "no direct commits to main" guard
32
+
33
+ Usage:
34
+ npx no-yolo-commits init [options]
35
+
36
+ Options:
37
+ --prefix=NAME Branch prefix used when auto-creating a branch off a
38
+ protected branch, e.g. --prefix=ACME -> ACME-<ts>-<slug>
39
+ (default: derived from package.json "name")
40
+ --stack=TEXT One-line description of the project handed to the AI
41
+ reviewer, e.g. --stack="Next.js + TypeScript + Postgres"
42
+ (default: "TypeScript")
43
+ --protect=a,b,c Comma-separated branch names that block direct commits
44
+ (default: "main,master")
45
+ --force, -f Overwrite an existing .husky/pre-commit
46
+ --help, -h Show this help
47
+
48
+ Requires the \`claude\` CLI on PATH to actually run the AI review — if it's
49
+ missing, the hook warns and lets the commit through (fails open).`);
50
+ }
51
+
52
+ function readPackageJson() {
53
+ const pkgPath = path.join(CWD, 'package.json');
54
+ if (!fs.existsSync(pkgPath)) {
55
+ fail('No package.json in the current directory — run this from your project root.');
56
+ }
57
+ return { pkgPath, pkg: JSON.parse(fs.readFileSync(pkgPath, 'utf8')) };
58
+ }
59
+
60
+ function ensureGitRepo() {
61
+ const result = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], { cwd: CWD, stdio: 'pipe' });
62
+ if (result.status !== 0) {
63
+ fail('Not a git repository — run `git init` first.');
64
+ }
65
+ }
66
+
67
+ function slugifyPrefix(name) {
68
+ return (name || 'wip')
69
+ .replace(/^@[^/]+\//, '') // drop npm scope
70
+ .toUpperCase()
71
+ .replace(/[^A-Z0-9]+/g, '')
72
+ .slice(0, 12) || 'WIP';
73
+ }
74
+
75
+ function run(cmd, args) {
76
+ console.log(` $ ${cmd} ${args.join(' ')}`);
77
+ const result = spawnSync(cmd, args, { cwd: CWD, stdio: 'inherit' });
78
+ if (result.status !== 0) {
79
+ fail(`\`${cmd} ${args.join(' ')}\` failed (exit ${result.status}).`);
80
+ }
81
+ }
82
+
83
+ function init(args) {
84
+ ensureGitRepo();
85
+ const { pkgPath, pkg } = readPackageJson();
86
+
87
+ const huskyDir = path.join(CWD, '.husky');
88
+ const hookPath = path.join(huskyDir, 'pre-commit');
89
+
90
+ if (fs.existsSync(hookPath) && !args.force) {
91
+ fail(`${hookPath} already exists — pass --force to overwrite it.`);
92
+ }
93
+
94
+ const hasHusky =
95
+ (pkg.devDependencies && pkg.devDependencies.husky) || (pkg.dependencies && pkg.dependencies.husky);
96
+
97
+ if (!hasHusky) {
98
+ console.log('→ Installing husky (devDependency)...');
99
+ run('npm', ['install', '--save-dev', 'husky']);
100
+ } else {
101
+ console.log('✓ husky already a devDependency');
102
+ }
103
+
104
+ const freshPkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
105
+ freshPkg.scripts = freshPkg.scripts || {};
106
+ if (freshPkg.scripts.prepare !== 'husky') {
107
+ freshPkg.scripts.prepare = 'husky';
108
+ fs.writeFileSync(pkgPath, JSON.stringify(freshPkg, null, 2) + '\n');
109
+ console.log('✓ set "scripts.prepare": "husky" in package.json');
110
+ }
111
+
112
+ console.log('→ Wiring up husky (git hooksPath)...');
113
+ run('npx', ['husky']);
114
+
115
+ const prefix = slugifyPrefix(args.prefix || freshPkg.name);
116
+ const protectedBranches = args.protect
117
+ .split(',')
118
+ .map((b) => b.trim())
119
+ .filter(Boolean)
120
+ .join(' ');
121
+
122
+ const templatePath = path.join(__dirname, '..', 'templates', 'pre-commit.sh');
123
+ let hookScript = fs.readFileSync(templatePath, 'utf8');
124
+ hookScript = hookScript
125
+ .replace(/__STACK__/g, args.stack.replace(/"/g, '\\"'))
126
+ .replace(/__PREFIX__/g, prefix)
127
+ .replace(/__PROTECTED__/g, protectedBranches);
128
+
129
+ fs.mkdirSync(huskyDir, { recursive: true });
130
+ fs.writeFileSync(hookPath, hookScript);
131
+ fs.chmodSync(hookPath, 0o755);
132
+
133
+ console.log(`✓ wrote ${path.relative(CWD, hookPath)}`);
134
+ console.log('');
135
+ console.log('Done. New behavior on `git commit`:');
136
+ console.log(` - AI review of staged changes (needs \`claude\` on PATH) — blocks only on high-confidence issues`);
137
+ console.log(` - direct commits to [${protectedBranches}] auto-branch instead, as ${prefix}-<timestamp>-<slug>`);
138
+ console.log('');
139
+ console.log('Bypass for one commit: git commit --no-verify');
140
+ }
141
+
142
+ function main() {
143
+ const args = parseArgs(process.argv.slice(2));
144
+ if (args.command === 'help') return printHelp();
145
+ if (args.command === 'init') return init(args);
146
+ console.error(`Unknown command "${args.command}"\n`);
147
+ printHelp();
148
+ process.exit(1);
149
+ }
150
+
151
+ main();
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "no-yolo-commits",
3
+ "version": "1.0.0",
4
+ "description": "AI pre-commit review (via the claude CLI) + a guard that auto-branches you off main/master instead of letting a direct commit land there.",
5
+ "keywords": [
6
+ "husky",
7
+ "git-hooks",
8
+ "pre-commit",
9
+ "ai-code-review",
10
+ "claude",
11
+ "claude-code"
12
+ ],
13
+ "license": "MIT",
14
+ "author": "ataztech910",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/ataztech910/no-yolo-commits.git"
18
+ },
19
+ "homepage": "https://github.com/ataztech910/no-yolo-commits#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/ataztech910/no-yolo-commits/issues"
22
+ },
23
+ "bin": {
24
+ "no-yolo-commits": "./bin/cli.js"
25
+ },
26
+ "files": [
27
+ "bin",
28
+ "templates",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
32
+ "engines": {
33
+ "node": ">=18"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ }
38
+ }
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env sh
2
+
3
+ # --- AI code review on staged changes ---
4
+ # Fails the commit only on real, high-confidence findings. If Claude is
5
+ # missing, times out, or errors, this fails OPEN (warns, does not block) —
6
+ # a broken/unavailable review must never be the only thing stopping commits.
7
+ if command -v claude >/dev/null 2>&1 && ! git diff --cached --quiet; then
8
+ echo "→ Running AI code review on staged changes..."
9
+
10
+ review_prompt="You are reviewing staged git changes before they are committed, for a __STACK__ project. Inspect the staged changes (run \`git diff --cached\` yourself; read full files with the Read tool when a diff hunk needs more surrounding context) and flag ONLY real, high-confidence problems: type-safety holes (any/unsafe casts), missing error handling at real failure points, obvious bugs, broken framework patterns, dead or unused code introduced by this diff.
11
+
12
+ Do NOT flag stylistic preferences. Do NOT flag pre-existing patterns already used elsewhere in this codebase (check before flagging). Do NOT invent issues — if you are not confident something is a real problem, leave it out. Respond with ONLY JSON matching the schema, nothing else."
13
+
14
+ review_tmpfile="$(mktemp)"
15
+
16
+ (claude -p "$review_prompt" \
17
+ --output-format json \
18
+ --json-schema '{"type":"object","properties":{"blocking_issues":{"type":"array","items":{"type":"string"}}},"required":["blocking_issues"]}' \
19
+ --allowedTools "Bash(git diff*)" "Bash(git show*)" "Bash(git log*)" "Read" "Grep" "Glob" \
20
+ --disallowedTools "Write" "Edit" "MultiEdit" "NotebookEdit" \
21
+ --permission-mode bypassPermissions \
22
+ >"$review_tmpfile" 2>/dev/null; exit 0) &
23
+ review_pid=$!
24
+ (sleep 120; kill "$review_pid" >/dev/null 2>&1; exit 0) >/dev/null 2>&1 &
25
+ review_watcher_pid=$!
26
+ disown "$review_watcher_pid" >/dev/null 2>&1 || true
27
+
28
+ wait "$review_pid" >/dev/null 2>&1 || true
29
+ kill "$review_watcher_pid" >/dev/null 2>&1 || true
30
+ wait "$review_watcher_pid" >/dev/null 2>&1 || true
31
+
32
+ node -e '
33
+ const fs = require("fs")
34
+ try {
35
+ const data = JSON.parse(fs.readFileSync(process.argv[1], "utf-8"))
36
+ const issues = (data.structured_output && data.structured_output.blocking_issues) || []
37
+ if (issues.length > 0) {
38
+ console.error("")
39
+ console.error("⛔ AI review found blocking issues:")
40
+ for (const i of issues) console.error(" - " + i)
41
+ console.error("")
42
+ console.error("Fix these, or commit anyway with `git commit --no-verify`.")
43
+ process.exit(1)
44
+ }
45
+ process.exit(0)
46
+ } catch (e) {
47
+ process.exit(2)
48
+ }
49
+ ' "$review_tmpfile"
50
+ review_status=$?
51
+ rm -f "$review_tmpfile" 2>/dev/null || true
52
+
53
+ if [ "$review_status" = "1" ]; then
54
+ exit 1
55
+ elif [ "$review_status" = "2" ]; then
56
+ echo "⚠ AI review unavailable or timed out — continuing without it."
57
+ else
58
+ echo "✓ AI review found no blocking issues."
59
+ fi
60
+ fi
61
+
62
+ # --- Block direct commits to protected branches ---
63
+ branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
64
+
65
+ case " __PROTECTED__ " in
66
+ *" $branch "*) ;;
67
+ *) exit 0 ;;
68
+ esac
69
+
70
+ echo "⛔ Direct commits to '$branch' are blocked — creating a branch for this commit..."
71
+
72
+ fallback_slug() {
73
+ echo "changes-$(date +%s | tail -c 5)"
74
+ }
75
+
76
+ slug=""
77
+
78
+ if command -v claude >/dev/null 2>&1; then
79
+ diff="$(git diff --cached -- . ':(exclude)package-lock.json' ':(exclude)yarn.lock' ':(exclude)pnpm-lock.yaml' ':(exclude)*.svg' ':(exclude)*.png' 2>/dev/null | head -c 6000)"
80
+ diff="${diff:-}"
81
+
82
+ if [ -n "$diff" ]; then
83
+ tmpfile="$(mktemp)"
84
+ prompt="Summarize the following staged git diff as ONE short phrase in kebab-case (lowercase words separated by hyphens, no punctuation, no quotes), at most 8 words, describing what changed. Output ONLY the phrase, nothing else, no explanation."
85
+
86
+ (printf '%s' "$diff" | claude -p "$prompt" >"$tmpfile" 2>/dev/null; exit 0) &
87
+ claude_pid=$!
88
+ (sleep 25; kill "$claude_pid" >/dev/null 2>&1; exit 0) >/dev/null 2>&1 &
89
+ watcher_pid=$!
90
+ disown "$watcher_pid" >/dev/null 2>&1 || true
91
+
92
+ wait "$claude_pid" >/dev/null 2>&1 || true
93
+ kill "$watcher_pid" >/dev/null 2>&1 || true
94
+ wait "$watcher_pid" >/dev/null 2>&1 || true
95
+
96
+ raw="$(cat "$tmpfile" 2>/dev/null || true)"
97
+ rm -f "$tmpfile" 2>/dev/null || true
98
+
99
+ slug="$(printf '%s' "$raw" \
100
+ | tr '[:upper:]' '[:lower:]' \
101
+ | tr -cs 'a-z0-9' '-' \
102
+ | sed -e 's/^-*//' -e 's/-*$//' \
103
+ | cut -c1-60)"
104
+ slug="${slug:-}"
105
+ fi
106
+ fi
107
+
108
+ if [ -z "$slug" ]; then
109
+ slug="$(fallback_slug)"
110
+ fi
111
+
112
+ timestamp="$(date +%s)"
113
+ new_branch="__PREFIX__-${timestamp}-${slug}"
114
+ new_branch="$(printf '%s' "$new_branch" | cut -c1-100)"
115
+
116
+ if ! git checkout -b "$new_branch" 2>/tmp/no-yolo-commits-error; then
117
+ echo "✗ Failed to create branch '$new_branch':"
118
+ cat /tmp/no-yolo-commits-error 2>/dev/null || true
119
+ rm -f /tmp/no-yolo-commits-error 2>/dev/null || true
120
+ echo "Commit aborted — create/switch to a feature branch manually and retry."
121
+ exit 1
122
+ fi
123
+ rm -f /tmp/no-yolo-commits-error 2>/dev/null || true
124
+
125
+ echo "✓ Switched to '$new_branch' — continuing commit there."