pumuki-ast-hooks 5.5.31 → 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.31",
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",
@@ -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');
@@ -108,6 +108,26 @@ fi
108
108
  # Change to project root (where package.json is)
109
109
  cd "$(git rev-parse --show-toplevel)" || exit 1
110
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
+
111
131
  # Check if there are staged files
112
132
  STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E '\\.(ts|tsx|js|jsx|swift|kt)$' || true)
113
133
  if [ -z "$STAGED_FILES" ]; then
@@ -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();