pumuki-ast-hooks 5.5.30 → 5.5.32
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki-ast-hooks",
|
|
3
|
-
"version": "5.5.
|
|
3
|
+
"version": "5.5.32",
|
|
4
4
|
"description": "Enterprise-grade AST Intelligence System with multi-platform support (iOS, Android, Backend, Frontend) and Feature-First + DDD + Clean Architecture enforcement. Includes dynamic violations API for intelligent querying.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"ast-install": "./bin/install.js",
|
|
11
11
|
"ast-violations": "./bin/violations-api.js",
|
|
12
12
|
"ast-check-version": "./bin/check-version.js",
|
|
13
|
+
"ast-gitflow": "./scripts/hooks-system/bin/gitflow-cycle.js",
|
|
13
14
|
"ai-commit": "./bin/ai-commit.sh",
|
|
14
15
|
"hook-run-orchestrator": "./bin/run-orchestrator.js",
|
|
15
16
|
"hook-watch": "./bin/watch-hooks.js",
|
package/scripts/hooks-system/application/services/installation/ConfigurationGeneratorService.js
CHANGED
|
@@ -116,6 +116,7 @@ class ConfigurationGeneratorService {
|
|
|
116
116
|
packageJson.scripts['ast:guard:status'] = 'bash scripts/hooks-system/bin/evidence-guard status';
|
|
117
117
|
packageJson.scripts['ast:guard:logs'] = 'bash scripts/hooks-system/bin/evidence-guard logs';
|
|
118
118
|
packageJson.scripts['ast:check-version'] = 'node scripts/hooks-system/bin/check-version.js';
|
|
119
|
+
packageJson.scripts['ast:gitflow'] = 'node scripts/hooks-system/bin/gitflow-cycle.js';
|
|
119
120
|
|
|
120
121
|
fs.writeFileSync(projectPackageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
121
122
|
this.logSuccess('npm scripts added');
|
|
@@ -79,12 +79,19 @@ class GitEnvironmentService {
|
|
|
79
79
|
return;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
// Install pre-commit hook
|
|
82
83
|
const preCommitHook = this.getPreCommitHookContent();
|
|
83
84
|
const preCommitPath = path.join(gitHooksDir, 'pre-commit');
|
|
84
|
-
|
|
85
85
|
fs.writeFileSync(preCommitPath, preCommitHook);
|
|
86
86
|
fs.chmodSync(preCommitPath, '755');
|
|
87
87
|
this.logSuccess(' ✅ Installed pre-commit hook');
|
|
88
|
+
|
|
89
|
+
// Install pre-push hook (Git Flow enforcement)
|
|
90
|
+
const prePushHook = this.getPrePushHookContent();
|
|
91
|
+
const prePushPath = path.join(gitHooksDir, 'pre-push');
|
|
92
|
+
fs.writeFileSync(prePushPath, prePushHook);
|
|
93
|
+
fs.chmodSync(prePushPath, '755');
|
|
94
|
+
this.logSuccess(' ✅ Installed pre-push hook (Git Flow enforcement)');
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
getPreCommitHookContent() {
|
|
@@ -101,6 +108,26 @@ fi
|
|
|
101
108
|
# Change to project root (where package.json is)
|
|
102
109
|
cd "$(git rev-parse --show-toplevel)" || exit 1
|
|
103
110
|
|
|
111
|
+
# ═══════════════════════════════════════════════════════════════
|
|
112
|
+
# GIT FLOW ENFORCEMENT: Block commits on protected branches
|
|
113
|
+
# ═══════════════════════════════════════════════════════════════
|
|
114
|
+
CURRENT_BRANCH=$(git branch --show-current 2>/dev/null || echo "")
|
|
115
|
+
|
|
116
|
+
if [[ "$CURRENT_BRANCH" == "main" ]] || [[ "$CURRENT_BRANCH" == "master" ]] || [[ "$CURRENT_BRANCH" == "develop" ]]; then
|
|
117
|
+
echo ""
|
|
118
|
+
echo "❌ COMMIT BLOCKED: Cannot commit directly to protected branch: $CURRENT_BRANCH"
|
|
119
|
+
echo ""
|
|
120
|
+
echo "🐈 Pumuki says: Create a feature branch first!"
|
|
121
|
+
echo ""
|
|
122
|
+
echo " git checkout -b feature/my-feature"
|
|
123
|
+
echo " git checkout -b fix/my-fix"
|
|
124
|
+
echo " git checkout -b hotfix/urgent-fix"
|
|
125
|
+
echo ""
|
|
126
|
+
echo "Then run: npm run ast:gitflow"
|
|
127
|
+
echo ""
|
|
128
|
+
exit 1
|
|
129
|
+
fi
|
|
130
|
+
|
|
104
131
|
# Check if there are staged files
|
|
105
132
|
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E '\\.(ts|tsx|js|jsx|swift|kt)$' || true)
|
|
106
133
|
if [ -z "$STAGED_FILES" ]; then
|
|
@@ -146,6 +173,107 @@ fi
|
|
|
146
173
|
|
|
147
174
|
# Fallback: direct execution logic here...
|
|
148
175
|
echo "⚠️ ast-intelligence-hooks not found"
|
|
176
|
+
exit 0
|
|
177
|
+
`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
getPrePushHookContent() {
|
|
181
|
+
return `#!/bin/bash
|
|
182
|
+
# AST Intelligence Hooks - Pre-push (Git Flow Enforcement)
|
|
183
|
+
# Auto-generated by pumuki-ast-hooks v${this.version}
|
|
184
|
+
|
|
185
|
+
# Check for bypass
|
|
186
|
+
if [[ -n "\${GIT_BYPASS_HOOK}" ]]; then
|
|
187
|
+
echo "⚠️ Bypassing Git Flow enforcement (GIT_BYPASS_HOOK=1)"
|
|
188
|
+
exit 0
|
|
189
|
+
fi
|
|
190
|
+
|
|
191
|
+
# Change to project root
|
|
192
|
+
cd "$(git rev-parse --show-toplevel)" || exit 1
|
|
193
|
+
|
|
194
|
+
# Check if we're pushing only tags (read from stdin)
|
|
195
|
+
while read local_ref local_sha remote_ref remote_sha; do
|
|
196
|
+
# If pushing a tag (refs/tags/*), allow it
|
|
197
|
+
if [[ "$local_ref" =~ ^refs/tags/ ]]; then
|
|
198
|
+
echo ""
|
|
199
|
+
echo "✅ Tag push detected - allowing release workflow"
|
|
200
|
+
echo ""
|
|
201
|
+
exit 0
|
|
202
|
+
fi
|
|
203
|
+
done
|
|
204
|
+
|
|
205
|
+
echo ""
|
|
206
|
+
echo "🔍 Validating Git Flow compliance before push..."
|
|
207
|
+
echo ""
|
|
208
|
+
|
|
209
|
+
# Get current branch
|
|
210
|
+
CURRENT_BRANCH=$(git branch --show-current 2>/dev/null || echo "")
|
|
211
|
+
|
|
212
|
+
if [[ -z "$CURRENT_BRANCH" ]]; then
|
|
213
|
+
echo "❌ Detached HEAD - cannot validate Git Flow"
|
|
214
|
+
exit 1
|
|
215
|
+
fi
|
|
216
|
+
|
|
217
|
+
# Protected branches - cannot push directly
|
|
218
|
+
PROTECTED_BRANCHES=("main" "master" "develop")
|
|
219
|
+
for protected in "\${PROTECTED_BRANCHES[@]}"; do
|
|
220
|
+
if [[ "$CURRENT_BRANCH" == "$protected" ]]; then
|
|
221
|
+
echo "❌ Cannot push directly to protected branch: $CURRENT_BRANCH"
|
|
222
|
+
echo ""
|
|
223
|
+
echo "Create a feature branch instead:"
|
|
224
|
+
echo " git checkout -b feature/my-feature"
|
|
225
|
+
echo ""
|
|
226
|
+
exit 1
|
|
227
|
+
fi
|
|
228
|
+
done
|
|
229
|
+
|
|
230
|
+
# Validate branch naming convention
|
|
231
|
+
if [[ ! "$CURRENT_BRANCH" =~ ^(feature|fix|hotfix|chore|docs|refactor|test|ci)/ ]]; then
|
|
232
|
+
echo "⚠️ Branch name '$CURRENT_BRANCH' doesn't follow Git Flow convention"
|
|
233
|
+
echo ""
|
|
234
|
+
echo "Expected patterns:"
|
|
235
|
+
echo " feature/description"
|
|
236
|
+
echo " fix/description"
|
|
237
|
+
echo " hotfix/description"
|
|
238
|
+
echo " chore/description"
|
|
239
|
+
echo ""
|
|
240
|
+
echo "This is a WARNING - push will continue"
|
|
241
|
+
echo ""
|
|
242
|
+
fi
|
|
243
|
+
|
|
244
|
+
# Check evidence freshness (optional - warn only)
|
|
245
|
+
EVIDENCE_FILE=".AI_EVIDENCE.json"
|
|
246
|
+
if [[ -f "$EVIDENCE_FILE" ]]; then
|
|
247
|
+
EVIDENCE_TS=$(jq -r '.timestamp // .severity_metrics.last_updated // empty' "$EVIDENCE_FILE" 2>/dev/null || echo "")
|
|
248
|
+
if [[ -n "$EVIDENCE_TS" ]]; then
|
|
249
|
+
# Parse timestamp and check age
|
|
250
|
+
CLEAN_TS=$(echo "$EVIDENCE_TS" | sed 's/\\.[0-9]*Z$/Z/')
|
|
251
|
+
EVIDENCE_EPOCH=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$CLEAN_TS" +%s 2>/dev/null || echo "0")
|
|
252
|
+
NOW_EPOCH=$(date +%s)
|
|
253
|
+
AGE=$((NOW_EPOCH - EVIDENCE_EPOCH))
|
|
254
|
+
|
|
255
|
+
if [[ $AGE -gt 300 ]]; then
|
|
256
|
+
echo "⚠️ Evidence is stale (\${AGE}s old, recommended <5min)"
|
|
257
|
+
echo " Consider running: npm run ast:refresh"
|
|
258
|
+
echo ""
|
|
259
|
+
fi
|
|
260
|
+
fi
|
|
261
|
+
fi
|
|
262
|
+
|
|
263
|
+
# Run gitflow-enforcer if available (optional validation)
|
|
264
|
+
ENFORCER_SCRIPT="scripts/hooks-system/infrastructure/shell/gitflow/gitflow-enforcer.sh"
|
|
265
|
+
if [[ -f "$ENFORCER_SCRIPT" ]]; then
|
|
266
|
+
if ! bash "$ENFORCER_SCRIPT" check 2>/dev/null; then
|
|
267
|
+
echo ""
|
|
268
|
+
echo "⚠️ Git Flow check completed with warnings (non-blocking)"
|
|
269
|
+
echo ""
|
|
270
|
+
fi
|
|
271
|
+
fi
|
|
272
|
+
|
|
273
|
+
echo ""
|
|
274
|
+
echo "✅ Git Flow validation passed. Proceeding with push..."
|
|
275
|
+
echo ""
|
|
276
|
+
|
|
149
277
|
exit 0
|
|
150
278
|
`;
|
|
151
279
|
}
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Git Flow Cycle - Complete automation
|
|
4
|
+
*
|
|
5
|
+
* Executes the full Git Flow cycle:
|
|
6
|
+
* 1. Validates current branch (must be feature/fix/hotfix/chore)
|
|
7
|
+
* 2. Commits uncommitted changes (optional)
|
|
8
|
+
* 3. Pushes to origin
|
|
9
|
+
* 4. Creates PR (requires gh CLI)
|
|
10
|
+
* 5. Optionally merges PR
|
|
11
|
+
* 6. Cleans up merged branches
|
|
12
|
+
* 7. Syncs local branches with remote
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { execSync } = require('child_process');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
const COLORS = {
|
|
20
|
+
reset: '\x1b[0m',
|
|
21
|
+
red: '\x1b[31m',
|
|
22
|
+
green: '\x1b[32m',
|
|
23
|
+
yellow: '\x1b[33m',
|
|
24
|
+
blue: '\x1b[34m',
|
|
25
|
+
cyan: '\x1b[36m',
|
|
26
|
+
magenta: '\x1b[35m'
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function log(color, message) {
|
|
30
|
+
console.log(`${color}${message}${COLORS.reset}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function exec(cmd, options = {}) {
|
|
34
|
+
try {
|
|
35
|
+
return execSync(cmd, {
|
|
36
|
+
encoding: 'utf-8',
|
|
37
|
+
stdio: options.silent ? 'pipe' : 'inherit',
|
|
38
|
+
...options
|
|
39
|
+
});
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (options.ignoreError) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function execSilent(cmd) {
|
|
49
|
+
try {
|
|
50
|
+
return execSync(cmd, { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getCurrentBranch() {
|
|
57
|
+
return execSilent('git branch --show-current') || 'unknown';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isProtectedBranch(branch) {
|
|
61
|
+
const protected = ['main', 'master', 'develop'];
|
|
62
|
+
return protected.includes(branch);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isValidFeatureBranch(branch) {
|
|
66
|
+
return /^(feature|fix|hotfix|chore|docs|refactor|test|ci)\//.test(branch);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function hasUncommittedChanges() {
|
|
70
|
+
const status = execSilent('git status --porcelain');
|
|
71
|
+
return status && status.length > 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function hasStagedChanges() {
|
|
75
|
+
const staged = execSilent('git diff --cached --name-only');
|
|
76
|
+
return staged && staged.length > 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getBaseBranch() {
|
|
80
|
+
// Check if develop exists
|
|
81
|
+
const hasDevelop = execSilent('git show-ref --verify --quiet refs/heads/develop');
|
|
82
|
+
return hasDevelop !== null ? 'develop' : 'main';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getMergedBranches(baseBranch) {
|
|
86
|
+
const output = execSilent(`git branch --merged ${baseBranch}`);
|
|
87
|
+
if (!output) return [];
|
|
88
|
+
return output.split('\n')
|
|
89
|
+
.map(b => b.replace(/^\*?\s*/, '').trim())
|
|
90
|
+
.filter(b => b && !isProtectedBranch(b));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isGitHubCliAvailable() {
|
|
94
|
+
return execSilent('gh auth status') !== null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function prExists(branch) {
|
|
98
|
+
return execSilent(`gh pr view ${branch}`) !== null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ════════════════════════════════════════════════════════════════
|
|
102
|
+
// MAIN CYCLE STEPS
|
|
103
|
+
// ════════════════════════════════════════════════════════════════
|
|
104
|
+
|
|
105
|
+
function step1_validateBranch() {
|
|
106
|
+
log(COLORS.cyan, '\n═══════════════════════════════════════════════════════════════');
|
|
107
|
+
log(COLORS.cyan, '📍 Step 1: Validate Branch');
|
|
108
|
+
log(COLORS.cyan, '═══════════════════════════════════════════════════════════════\n');
|
|
109
|
+
|
|
110
|
+
const branch = getCurrentBranch();
|
|
111
|
+
log(COLORS.blue, `Current branch: ${branch}`);
|
|
112
|
+
|
|
113
|
+
if (isProtectedBranch(branch)) {
|
|
114
|
+
log(COLORS.red, `\n❌ Cannot run Git Flow cycle on protected branch: ${branch}`);
|
|
115
|
+
log(COLORS.yellow, '\nCreate a feature branch first:');
|
|
116
|
+
log(COLORS.yellow, ' git checkout -b feature/my-feature');
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!isValidFeatureBranch(branch)) {
|
|
121
|
+
log(COLORS.yellow, `\n⚠️ Branch '${branch}' doesn't follow naming convention`);
|
|
122
|
+
log(COLORS.yellow, 'Expected: feature/, fix/, hotfix/, chore/, docs/, refactor/, test/, ci/');
|
|
123
|
+
log(COLORS.yellow, '\nContinuing anyway...');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
log(COLORS.green, '✅ Branch validation passed');
|
|
127
|
+
return branch;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function step2_commitChanges(commitMessage) {
|
|
131
|
+
log(COLORS.cyan, '\n═══════════════════════════════════════════════════════════════');
|
|
132
|
+
log(COLORS.cyan, '📝 Step 2: Commit Changes');
|
|
133
|
+
log(COLORS.cyan, '═══════════════════════════════════════════════════════════════\n');
|
|
134
|
+
|
|
135
|
+
if (!hasUncommittedChanges()) {
|
|
136
|
+
log(COLORS.green, '✅ No uncommitted changes');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
log(COLORS.yellow, '⚠️ Uncommitted changes detected');
|
|
141
|
+
|
|
142
|
+
if (!hasStagedChanges()) {
|
|
143
|
+
log(COLORS.blue, 'Staging all changes...');
|
|
144
|
+
exec('git add -A');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const message = commitMessage || `chore: auto-commit changes on ${getCurrentBranch()}`;
|
|
148
|
+
log(COLORS.blue, `Committing: ${message}`);
|
|
149
|
+
exec(`git commit -m "${message}"`);
|
|
150
|
+
log(COLORS.green, '✅ Changes committed');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function step3_pushToOrigin(branch) {
|
|
154
|
+
log(COLORS.cyan, '\n═══════════════════════════════════════════════════════════════');
|
|
155
|
+
log(COLORS.cyan, '🚀 Step 3: Push to Origin');
|
|
156
|
+
log(COLORS.cyan, '═══════════════════════════════════════════════════════════════\n');
|
|
157
|
+
|
|
158
|
+
log(COLORS.blue, `Pushing ${branch} to origin...`);
|
|
159
|
+
exec(`git push -u origin ${branch}`);
|
|
160
|
+
log(COLORS.green, '✅ Pushed to origin');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function step4_createPR(branch, baseBranch, prTitle, prBody) {
|
|
164
|
+
log(COLORS.cyan, '\n═══════════════════════════════════════════════════════════════');
|
|
165
|
+
log(COLORS.cyan, '📋 Step 4: Create Pull Request');
|
|
166
|
+
log(COLORS.cyan, '═══════════════════════════════════════════════════════════════\n');
|
|
167
|
+
|
|
168
|
+
if (!isGitHubCliAvailable()) {
|
|
169
|
+
log(COLORS.yellow, '⚠️ GitHub CLI (gh) not available or not authenticated');
|
|
170
|
+
log(COLORS.yellow, ' Install: https://cli.github.com/');
|
|
171
|
+
log(COLORS.yellow, ' Auth: gh auth login');
|
|
172
|
+
log(COLORS.yellow, '\n Create PR manually at GitHub.');
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (prExists(branch)) {
|
|
177
|
+
log(COLORS.green, '✅ PR already exists for this branch');
|
|
178
|
+
const prUrl = execSilent(`gh pr view ${branch} --json url --jq .url`);
|
|
179
|
+
return prUrl;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const title = prTitle || execSilent('git log -1 --pretty=%s') || `Merge ${branch}`;
|
|
183
|
+
const body = prBody || `## Summary\nAutomated PR for ${branch}\n\n## Changes\n${execSilent(`git log --oneline origin/${baseBranch}..${branch}`) || '- Updates'}`;
|
|
184
|
+
|
|
185
|
+
log(COLORS.blue, `Creating PR: ${title}`);
|
|
186
|
+
log(COLORS.blue, `Base: ${baseBranch} ← Head: ${branch}`);
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const output = execSilent(`gh pr create --base ${baseBranch} --head ${branch} --title "${title}" --body "${body.replace(/"/g, '\\"')}"`);
|
|
190
|
+
if (output) {
|
|
191
|
+
const urlMatch = output.match(/https:\/\/github\.com\/.*\/pull\/\d+/);
|
|
192
|
+
const prUrl = urlMatch ? urlMatch[0] : output;
|
|
193
|
+
log(COLORS.green, `✅ PR created: ${prUrl}`);
|
|
194
|
+
return prUrl;
|
|
195
|
+
}
|
|
196
|
+
} catch (error) {
|
|
197
|
+
log(COLORS.red, `❌ Failed to create PR: ${error.message}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function step5_mergePR(prUrl, autoMerge) {
|
|
204
|
+
log(COLORS.cyan, '\n═══════════════════════════════════════════════════════════════');
|
|
205
|
+
log(COLORS.cyan, '🔀 Step 5: Merge Pull Request');
|
|
206
|
+
log(COLORS.cyan, '═══════════════════════════════════════════════════════════════\n');
|
|
207
|
+
|
|
208
|
+
if (!prUrl) {
|
|
209
|
+
log(COLORS.yellow, '⚠️ No PR URL available, skipping merge');
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!autoMerge) {
|
|
214
|
+
log(COLORS.yellow, '⚠️ Auto-merge disabled. Merge PR manually.');
|
|
215
|
+
log(COLORS.blue, ` PR: ${prUrl}`);
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
log(COLORS.blue, 'Enabling auto-merge (squash)...');
|
|
220
|
+
try {
|
|
221
|
+
exec(`gh pr merge --auto --squash "${prUrl}"`, { ignoreError: true });
|
|
222
|
+
log(COLORS.green, '✅ Auto-merge enabled');
|
|
223
|
+
return true;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
log(COLORS.yellow, '⚠️ Could not enable auto-merge (may require admin approval)');
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function step6_cleanupBranches(baseBranch) {
|
|
231
|
+
log(COLORS.cyan, '\n═══════════════════════════════════════════════════════════════');
|
|
232
|
+
log(COLORS.cyan, '🧹 Step 6: Cleanup Merged Branches');
|
|
233
|
+
log(COLORS.cyan, '═══════════════════════════════════════════════════════════════\n');
|
|
234
|
+
|
|
235
|
+
// Fetch latest
|
|
236
|
+
exec('git fetch --prune', { silent: true, ignoreError: true });
|
|
237
|
+
|
|
238
|
+
const mergedBranches = getMergedBranches(baseBranch);
|
|
239
|
+
|
|
240
|
+
if (mergedBranches.length === 0) {
|
|
241
|
+
log(COLORS.green, '✅ No merged branches to clean');
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
log(COLORS.blue, `Found ${mergedBranches.length} merged branches:`);
|
|
246
|
+
mergedBranches.forEach(b => log(COLORS.yellow, ` - ${b}`));
|
|
247
|
+
|
|
248
|
+
// Delete local merged branches
|
|
249
|
+
for (const branch of mergedBranches) {
|
|
250
|
+
log(COLORS.blue, `Deleting local: ${branch}`);
|
|
251
|
+
exec(`git branch -d "${branch}"`, { ignoreError: true, silent: true });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Delete remote merged branches (if gh available)
|
|
255
|
+
if (isGitHubCliAvailable()) {
|
|
256
|
+
for (const branch of mergedBranches) {
|
|
257
|
+
log(COLORS.blue, `Deleting remote: origin/${branch}`);
|
|
258
|
+
exec(`git push origin --delete "${branch}"`, { ignoreError: true, silent: true });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
log(COLORS.green, `✅ Cleaned ${mergedBranches.length} merged branches`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function step7_syncBranches(baseBranch) {
|
|
266
|
+
log(COLORS.cyan, '\n═══════════════════════════════════════════════════════════════');
|
|
267
|
+
log(COLORS.cyan, '🔄 Step 7: Sync Branches');
|
|
268
|
+
log(COLORS.cyan, '═══════════════════════════════════════════════════════════════\n');
|
|
269
|
+
|
|
270
|
+
const currentBranch = getCurrentBranch();
|
|
271
|
+
|
|
272
|
+
log(COLORS.blue, 'Fetching from origin...');
|
|
273
|
+
exec('git fetch origin', { ignoreError: true, silent: true });
|
|
274
|
+
|
|
275
|
+
// Sync develop
|
|
276
|
+
if (execSilent('git show-ref --verify --quiet refs/heads/develop') !== null) {
|
|
277
|
+
log(COLORS.blue, 'Syncing develop...');
|
|
278
|
+
exec('git checkout develop && git pull origin develop', { ignoreError: true, silent: true });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Sync main
|
|
282
|
+
log(COLORS.blue, 'Syncing main...');
|
|
283
|
+
exec('git checkout main && git pull origin main', { ignoreError: true, silent: true });
|
|
284
|
+
|
|
285
|
+
// Return to original branch
|
|
286
|
+
log(COLORS.blue, `Returning to ${currentBranch}...`);
|
|
287
|
+
exec(`git checkout ${currentBranch}`, { ignoreError: true, silent: true });
|
|
288
|
+
|
|
289
|
+
log(COLORS.green, '✅ Branches synchronized');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ════════════════════════════════════════════════════════════════
|
|
293
|
+
// MAIN
|
|
294
|
+
// ════════════════════════════════════════════════════════════════
|
|
295
|
+
|
|
296
|
+
function printUsage() {
|
|
297
|
+
console.log(`
|
|
298
|
+
${COLORS.cyan}═══════════════════════════════════════════════════════════════
|
|
299
|
+
🐈 Pumuki Git Flow Cycle
|
|
300
|
+
═══════════════════════════════════════════════════════════════${COLORS.reset}
|
|
301
|
+
|
|
302
|
+
${COLORS.blue}Usage:${COLORS.reset} npm run ast:gitflow [options]
|
|
303
|
+
|
|
304
|
+
${COLORS.blue}Options:${COLORS.reset}
|
|
305
|
+
--commit-message, -m <msg> Custom commit message
|
|
306
|
+
--pr-title <title> Custom PR title
|
|
307
|
+
--auto-merge Enable auto-merge after PR creation
|
|
308
|
+
--skip-cleanup Skip branch cleanup step
|
|
309
|
+
--skip-sync Skip branch sync step
|
|
310
|
+
--help, -h Show this help
|
|
311
|
+
|
|
312
|
+
${COLORS.blue}Examples:${COLORS.reset}
|
|
313
|
+
npm run ast:gitflow
|
|
314
|
+
npm run ast:gitflow -- -m "feat: add new feature"
|
|
315
|
+
npm run ast:gitflow -- --auto-merge
|
|
316
|
+
npm run ast:gitflow -- --pr-title "My PR Title" --auto-merge
|
|
317
|
+
|
|
318
|
+
${COLORS.blue}What it does:${COLORS.reset}
|
|
319
|
+
1. Validates branch (must be feature/, fix/, etc.)
|
|
320
|
+
2. Commits uncommitted changes
|
|
321
|
+
3. Pushes to origin
|
|
322
|
+
4. Creates PR (requires gh CLI)
|
|
323
|
+
5. Optionally auto-merges PR
|
|
324
|
+
6. Cleans up merged branches
|
|
325
|
+
7. Syncs local branches with remote
|
|
326
|
+
|
|
327
|
+
${COLORS.yellow}⚠️ Requires:${COLORS.reset}
|
|
328
|
+
- Git repository
|
|
329
|
+
- GitHub CLI (gh) for PR operations: https://cli.github.com/
|
|
330
|
+
`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function parseArgs(args) {
|
|
334
|
+
const options = {
|
|
335
|
+
commitMessage: null,
|
|
336
|
+
prTitle: null,
|
|
337
|
+
prBody: null,
|
|
338
|
+
autoMerge: false,
|
|
339
|
+
skipCleanup: false,
|
|
340
|
+
skipSync: false,
|
|
341
|
+
help: false
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
for (let i = 0; i < args.length; i++) {
|
|
345
|
+
const arg = args[i];
|
|
346
|
+
switch (arg) {
|
|
347
|
+
case '--commit-message':
|
|
348
|
+
case '-m':
|
|
349
|
+
options.commitMessage = args[++i];
|
|
350
|
+
break;
|
|
351
|
+
case '--pr-title':
|
|
352
|
+
options.prTitle = args[++i];
|
|
353
|
+
break;
|
|
354
|
+
case '--pr-body':
|
|
355
|
+
options.prBody = args[++i];
|
|
356
|
+
break;
|
|
357
|
+
case '--auto-merge':
|
|
358
|
+
options.autoMerge = true;
|
|
359
|
+
break;
|
|
360
|
+
case '--skip-cleanup':
|
|
361
|
+
options.skipCleanup = true;
|
|
362
|
+
break;
|
|
363
|
+
case '--skip-sync':
|
|
364
|
+
options.skipSync = true;
|
|
365
|
+
break;
|
|
366
|
+
case '--help':
|
|
367
|
+
case '-h':
|
|
368
|
+
options.help = true;
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return options;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function main() {
|
|
377
|
+
const args = process.argv.slice(2);
|
|
378
|
+
const options = parseArgs(args);
|
|
379
|
+
|
|
380
|
+
if (options.help) {
|
|
381
|
+
printUsage();
|
|
382
|
+
process.exit(0);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
log(COLORS.magenta, '\n🐈 Pumuki Git Flow Cycle - Starting...\n');
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
// Step 1: Validate branch
|
|
389
|
+
const branch = step1_validateBranch();
|
|
390
|
+
const baseBranch = getBaseBranch();
|
|
391
|
+
|
|
392
|
+
// Step 2: Commit changes
|
|
393
|
+
step2_commitChanges(options.commitMessage);
|
|
394
|
+
|
|
395
|
+
// Step 3: Push to origin
|
|
396
|
+
step3_pushToOrigin(branch);
|
|
397
|
+
|
|
398
|
+
// Step 4: Create PR
|
|
399
|
+
const prUrl = step4_createPR(branch, baseBranch, options.prTitle, options.prBody);
|
|
400
|
+
|
|
401
|
+
// Step 5: Merge PR (optional)
|
|
402
|
+
step5_mergePR(prUrl, options.autoMerge);
|
|
403
|
+
|
|
404
|
+
// Step 6: Cleanup branches
|
|
405
|
+
if (!options.skipCleanup) {
|
|
406
|
+
step6_cleanupBranches(baseBranch);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Step 7: Sync branches
|
|
410
|
+
if (!options.skipSync) {
|
|
411
|
+
step7_syncBranches(baseBranch);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
log(COLORS.green, '\n═══════════════════════════════════════════════════════════════');
|
|
415
|
+
log(COLORS.green, '✅ Git Flow Cycle Complete!');
|
|
416
|
+
log(COLORS.green, '═══════════════════════════════════════════════════════════════\n');
|
|
417
|
+
|
|
418
|
+
} catch (error) {
|
|
419
|
+
log(COLORS.red, `\n❌ Git Flow Cycle failed: ${error.message}`);
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
main();
|