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 +21 -0
- package/README.md +65 -0
- package/bin/cli.js +151 -0
- package/package.json +38 -0
- package/templates/pre-commit.sh +125 -0
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
|
+
[](https://www.npmjs.com/package/no-yolo-commits)
|
|
4
|
+
[](./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."
|