jettypod 4.1.2 → 4.1.4

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 (179) hide show
  1. package/.nvmrc +1 -0
  2. package/docs/COMPLETE-TESTING-STRATEGY.md +970 -0
  3. package/docs/DECISIONS.md +10 -12
  4. package/docs/NODE_VERSION.md +83 -0
  5. package/docs/TDD-INFRASTRUCTURE-STRATEGY.md +1374 -0
  6. package/docs/TESTING-FOR-NON-ENGINEERS.md +1588 -0
  7. package/docs/TESTING-STRATEGY-AUDIT.md +698 -0
  8. package/hooks/post-checkout +17 -0
  9. package/hooks/post-merge +17 -0
  10. package/hooks/pre-commit +30 -0
  11. package/jettypod.js +259 -120
  12. package/lib/coverage-tracker.js +218 -0
  13. package/lib/database.js +2 -0
  14. package/lib/db-export.js +192 -0
  15. package/lib/db-import.js +193 -0
  16. package/lib/external-transition-handler.js +32 -0
  17. package/lib/git-hook-helpers.js +174 -0
  18. package/lib/git-root.js +90 -0
  19. package/lib/infrastructure-chore-generator.js +45 -0
  20. package/lib/install-hooks.js +52 -0
  21. package/lib/jettypod-backup.js +238 -0
  22. package/lib/merge-lock.js +193 -0
  23. package/lib/migrations/012-add-worktree-path.js +38 -0
  24. package/lib/migrations/013-worktrees-table.js +86 -0
  25. package/lib/migrations/014-migrate-worktree-data.js +161 -0
  26. package/lib/migrations/015-merge-locks-table.js +67 -0
  27. package/lib/pattern-finder.js +152 -0
  28. package/lib/process-manager.js +140 -0
  29. package/lib/production-standards-reader.js +13 -2
  30. package/lib/production-standards-writer.js +85 -0
  31. package/lib/skills/feature-planning/dry-run-validator.js +135 -0
  32. package/lib/skills/feature-planning/validation-formatter.js +160 -0
  33. package/lib/smart-conflict-detection.js +168 -0
  34. package/lib/smart-fetch-rebase.js +614 -0
  35. package/lib/step-definition-parser.js +76 -0
  36. package/lib/unit-test-generator.js +232 -0
  37. package/lib/verification-command-generator.js +66 -0
  38. package/lib/worktree-diagnostics.js +413 -0
  39. package/lib/worktree-facade.js +174 -0
  40. package/lib/worktree-manager.js +636 -0
  41. package/lib/worktree-reconciler.js +429 -0
  42. package/package.json +30 -3
  43. package/skills-templates/external-transition/SKILL.md +34 -3
  44. package/skills-templates/feature-planning/SKILL.md +190 -24
  45. package/skills-templates/production-mode/SKILL.md +127 -9
  46. package/skills-templates/speed-mode/SKILL.md +454 -51
  47. package/skills-templates/stable-mode/SKILL.md +285 -76
  48. package/.claude/PROTECT_SKILLS.md +0 -28
  49. package/.claude/settings.json +0 -24
  50. package/.claude/settings.local.json +0 -16
  51. package/.claude/skills/epic-planning/SKILL.md +0 -297
  52. package/.claude/skills/external-transition/SKILL.md +0 -384
  53. package/.claude/skills/feature-planning/SKILL.md +0 -464
  54. package/.claude/skills/production-mode/SKILL.md +0 -369
  55. package/.claude/skills/speed-mode/SKILL.md +0 -481
  56. package/.claude/skills/stable-mode/SKILL.md +0 -713
  57. package/.claude/skills.backup-2025-11-10T23-33-09-368Z/epic-planning/SKILL.md +0 -297
  58. package/.claude/skills.backup-2025-11-10T23-33-09-368Z/feature-planning/SKILL.md +0 -464
  59. package/.claude/skills.backup-2025-11-10T23-33-09-368Z/speed-mode/SKILL.md +0 -467
  60. package/.claude/skills.backup-2025-11-10T23-33-09-368Z/stable-mode/SKILL.md +0 -673
  61. package/.claude/skills.backup-2025-11-11T16-15-10-070Z/epic-discover/SKILL.md +0 -297
  62. package/.claude/skills.backup-2025-11-11T16-42-43-212Z/epic-planning/SKILL.md +0 -297
  63. package/.claude/skills.backup-2025-11-11T16-42-43-212Z/feature-planning/SKILL.md +0 -464
  64. package/.claude/skills.backup-2025-11-11T16-42-43-212Z/speed-mode/SKILL.md +0 -467
  65. package/.claude/skills.backup-2025-11-11T16-42-43-212Z/stable-mode/SKILL.md +0 -673
  66. package/.claude/skills.backup-2025-11-11T17-06-09-783Z/epic-planning/SKILL.md +0 -297
  67. package/.claude/skills.backup-2025-11-11T17-06-09-783Z/feature-planning/SKILL.md +0 -464
  68. package/.claude/skills.backup-2025-11-11T17-06-09-783Z/speed-mode/SKILL.md +0 -467
  69. package/.claude/skills.backup-2025-11-11T17-06-09-783Z/stable-mode/SKILL.md +0 -673
  70. package/.devpod/current-work.json +0 -10
  71. package/.devpod/work.db +0 -0
  72. package/.github/workflows/test-safety.yml +0 -85
  73. package/.jettypod/config.json +0 -5
  74. package/.jettypod/current-work.json +0 -10
  75. package/.jettypod/hooks/README.md +0 -77
  76. package/.jettypod/hooks/protect-claude-md.js +0 -338
  77. package/.jettypod/test-work.db +0 -0
  78. package/.jettypod/work.db +0 -0
  79. package/CLAUDE.md +0 -49
  80. package/SPEED-STABLE-AUDIT.md +0 -853
  81. package/SYSTEM-BEHAVIOR.md +0 -2199
  82. package/TEST_SAFETY_AUDIT.md +0 -314
  83. package/TEST_SAFETY_IMPLEMENTATION.md +0 -97
  84. package/cucumber-report.html +0 -45
  85. package/dist/devpod-linux +0 -0
  86. package/dist/devpod-macos +0 -0
  87. package/dist/devpod-win.exe +0 -0
  88. package/docs/features/jettypod-standards-explained.md +0 -543
  89. package/docs/features/standards-inventory.md +0 -257
  90. package/features/auto-generate-production-chores.feature +0 -13
  91. package/features/backlog-command.feature +0 -26
  92. package/features/backlog-filtering-production.feature +0 -10
  93. package/features/claude-md-protection/steps.js +0 -498
  94. package/features/decisions/index.js +0 -490
  95. package/features/decisions/index.test.js +0 -208
  96. package/features/fix-text-wrapping.feature +0 -42
  97. package/features/git-hooks/git-hooks.feature +0 -30
  98. package/features/git-hooks/index.js +0 -93
  99. package/features/git-hooks/index.test.js +0 -137
  100. package/features/git-hooks/post-commit +0 -56
  101. package/features/git-hooks/post-merge +0 -47
  102. package/features/git-hooks/pre-commit +0 -28
  103. package/features/git-hooks/simple-steps.js +0 -53
  104. package/features/git-hooks/simple-test.feature +0 -10
  105. package/features/git-hooks/steps.js +0 -196
  106. package/features/jettypod-update-command.feature +0 -46
  107. package/features/mode-prompts/index.js +0 -95
  108. package/features/mode-prompts/simple-steps.js +0 -44
  109. package/features/mode-prompts/simple-test.feature +0 -9
  110. package/features/mode-prompts/validation.test.js +0 -120
  111. package/features/multiple-claude-instances.feature +0 -121
  112. package/features/production-mode-skill.feature +0 -121
  113. package/features/refactor-mode/steps.js +0 -217
  114. package/features/refactor-mode.feature +0 -49
  115. package/features/simplify-external-transition.feature +0 -166
  116. package/features/skills-update/index.test.js +0 -216
  117. package/features/step_definitions/backlog-command.steps.js +0 -37
  118. package/features/step_definitions/fix-text-wrapping.steps.js +0 -271
  119. package/features/step_definitions/multiple-claude-instances.steps.js +0 -621
  120. package/features/step_definitions/production-mode-skill.steps.js +0 -862
  121. package/features/step_definitions/simplify-external-transition.steps.js +0 -370
  122. package/features/step_definitions/terminal-logo.steps.js +0 -145
  123. package/features/step_definitions/update-command.steps.js +0 -183
  124. package/features/support/hooks.js +0 -9
  125. package/features/terminal-logo/index.js +0 -39
  126. package/features/terminal-logo/terminal-logo.feature +0 -30
  127. package/features/update-command/index.js +0 -181
  128. package/features/update-command/index.test.js +0 -225
  129. package/features/work-commands/bug-workflow-display.feature +0 -22
  130. package/features/work-commands/index.js +0 -498
  131. package/features/work-commands/simple-steps.js +0 -69
  132. package/features/work-commands/stable-tests.feature +0 -57
  133. package/features/work-commands/steps.js +0 -1174
  134. package/features/work-commands/validation.test.js +0 -88
  135. package/features/work-commands/work-commands.feature +0 -13
  136. package/features/work-tracking/discovery-validation.test.js +0 -228
  137. package/features/work-tracking/index.js +0 -1921
  138. package/features/work-tracking/mode-required.feature +0 -112
  139. package/features/work-tracking/phase-tracking.test.js +0 -482
  140. package/features/work-tracking/prototype-tracking.test.js +0 -485
  141. package/features/work-tracking/tree-view.test.js +0 -310
  142. package/features/work-tracking/work-set-mode.feature +0 -71
  143. package/features/work-tracking/work-start-mode.feature +0 -88
  144. package/full-test.txt +0 -0
  145. package/lib/bug-workflow.test.js +0 -177
  146. package/lib/claudemd.test.js +0 -195
  147. package/lib/config.test.js +0 -511
  148. package/lib/constants.test.js +0 -164
  149. package/lib/current-work.test.js +0 -146
  150. package/lib/database-project-config.test.js +0 -111
  151. package/lib/database.test.js +0 -106
  152. package/lib/decisions-generator.test.js +0 -457
  153. package/lib/decisions-helpers.test.js +0 -310
  154. package/lib/git-coordinator.js +0 -167
  155. package/lib/git.test.js +0 -145
  156. package/lib/migrations/002-default-work-item-modes.test.js +0 -351
  157. package/lib/production-chore-generator.test.js +0 -432
  158. package/lib/production-context-detector.test.js +0 -277
  159. package/lib/production-scenario-appender.test.js +0 -235
  160. package/lib/production-scenario-validator.test.js +0 -246
  161. package/lib/production-standards-reader.test.js +0 -270
  162. package/lib/project-state.test.js +0 -92
  163. package/lib/push-queue.js +0 -417
  164. package/lib/queue-processor.js +0 -74
  165. package/lib/test-helpers.js +0 -202
  166. package/lib/test-helpers.test.js +0 -255
  167. package/prototypes/2025-01-11-production-mode-autonomous.js +0 -119
  168. package/prototypes/2025-01-11-production-mode-collaborative.js +0 -166
  169. package/prototypes/2025-01-11-production-mode-guided.js +0 -217
  170. package/prototypes/2025-01-11-production-mode-smart-context.js +0 -347
  171. package/prototypes/2025-01-11-production-standards-example.md +0 -204
  172. package/prototypes/2025-11-10-backlog-filtering-tree-aware.js +0 -242
  173. package/prototypes/test/index.html +0 -1
  174. package/setup-dist-repo.sh +0 -68
  175. package/test-production-standards-engine.js +0 -130
  176. package/test-results.json +0 -2195
  177. package/test-safety-check.sh +0 -80
  178. package/work-item-tracking-plan.md +0 -199
  179. /package/{.jettypod/devpod.db → jettypod.db} +0 -0
@@ -0,0 +1,614 @@
1
+ /**
2
+ * Smart Fetch & Rebase Module
3
+ *
4
+ * Automatically fetches latest changes from origin and rebases feature branches
5
+ * on updated main before merging. Implements retry strategy with exponential
6
+ * backoff to handle concurrent merge operations.
7
+ */
8
+
9
+ const { execSync } = require('child_process');
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ /**
14
+ * Smart fetch and rebase operation for merging feature branches
15
+ *
16
+ * @param {string} repoPath - Path to git repository
17
+ * @param {string} featureBranch - Name of feature branch to merge
18
+ * @param {Object} options - Optional configuration
19
+ * @param {number} options.maxRetries - Maximum retry attempts (default: 3)
20
+ * @param {number} options.initialDelay - Initial retry delay in ms (default: 1000)
21
+ * @param {boolean} options.cleanupWorktree - Cleanup worktree after successful merge (default: true)
22
+ * @param {Object} options.db - Database connection for worktree cleanup (optional)
23
+ * @returns {Promise<Object>} Result object with operation status flags
24
+ * @throws {Error} If operation fails or divergence detected
25
+ */
26
+ async function smartFetchAndRebase(repoPath, featureBranch, options = {}) {
27
+ const maxRetries = options.maxRetries || 3;
28
+ const initialDelay = options.initialDelay || 1000;
29
+ const cleanupWorktree = options.cleanupWorktree !== false; // Default true
30
+
31
+ const result = {
32
+ fetchedFromOrigin: false,
33
+ mainFastForwarded: false,
34
+ rebaseCompleted: false,
35
+ mergeCompleted: false,
36
+ pushedToOrigin: false,
37
+ retryAttempted: false,
38
+ succeededOnRetry: false,
39
+ divergenceDetected: false,
40
+ aborted: false,
41
+ worktreeCleaned: false,
42
+ error: null
43
+ };
44
+
45
+ try {
46
+ // Pre-flight validation: check git repository health
47
+ await validateGitRepo(repoPath);
48
+
49
+ // Fetch latest changes from origin
50
+ await fetchOrigin(repoPath);
51
+ result.fetchedFromOrigin = true;
52
+
53
+ // Detect if main has diverged (cannot be fast-forwarded)
54
+ const diverged = await detectDivergence(repoPath);
55
+ if (diverged) {
56
+ result.divergenceDetected = true;
57
+ result.aborted = true;
58
+ result.error = 'Local main has diverged from origin/main. Cannot fast-forward. Manual intervention required.';
59
+ throw new Error(result.error);
60
+ }
61
+
62
+ // Fast-forward local main to match origin/main
63
+ await fastForwardMain(repoPath);
64
+ result.mainFastForwarded = true;
65
+
66
+ // Rebase feature branch onto updated main
67
+ await rebaseFeatureBranch(repoPath, featureBranch);
68
+ result.rebaseCompleted = true;
69
+
70
+ // Merge feature branch to main
71
+ await mergeFeatureBranch(repoPath, featureBranch);
72
+ result.mergeCompleted = true;
73
+
74
+ // Push to origin with retry logic
75
+ let retryCount = 0;
76
+ const retryAttempts = [];
77
+
78
+ while (retryCount <= maxRetries) {
79
+ try {
80
+ await pushToOrigin(repoPath);
81
+ result.pushedToOrigin = true;
82
+ if (retryCount > 0) {
83
+ result.succeededOnRetry = true;
84
+ }
85
+
86
+ // Cleanup worktree after successful merge and push
87
+ if (cleanupWorktree) {
88
+ try {
89
+ // Extract work item ID from branch name (format: feature/work-{id}-{slug})
90
+ const branchMatch = featureBranch.match(/work-(\d+)-/);
91
+ if (branchMatch) {
92
+ const workItemId = parseInt(branchMatch[1]);
93
+
94
+ // Get database connection
95
+ const { getDb } = require('./database');
96
+ const db = options.db || getDb();
97
+
98
+ // Find worktree for this work item
99
+ const worktree = await new Promise((resolve, reject) => {
100
+ db.get(
101
+ 'SELECT * FROM worktrees WHERE work_item_id = ? AND status = ?',
102
+ [workItemId, 'active'],
103
+ (err, row) => {
104
+ if (err) reject(err);
105
+ else resolve(row);
106
+ }
107
+ );
108
+ });
109
+
110
+ if (worktree) {
111
+ // Cleanup using worktree-facade
112
+ const worktreeFacade = require('./worktree-facade');
113
+ const cleanupResult = await worktreeFacade.stopWork(worktree.id, {
114
+ repoPath: repoPath,
115
+ db: db,
116
+ deleteBranch: true // Delete branch after successful merge
117
+ });
118
+
119
+ if (cleanupResult.success) {
120
+ result.worktreeCleaned = true;
121
+ }
122
+ }
123
+ }
124
+ } catch (cleanupErr) {
125
+ // Non-fatal - log warning but don't fail the merge
126
+ console.warn(`Warning: Worktree cleanup failed: ${cleanupErr.message}`);
127
+ }
128
+ }
129
+
130
+ break;
131
+ } catch (pushErr) {
132
+ // Log retry attempt with details
133
+ retryAttempts.push({
134
+ attempt: retryCount + 1,
135
+ error: pushErr.message,
136
+ timestamp: new Date().toISOString()
137
+ });
138
+
139
+ if (retryCount === maxRetries) {
140
+ // Build detailed error message for retry exhaustion
141
+ const attemptsLog = retryAttempts.map((a, i) =>
142
+ ` Attempt ${a.attempt}/${maxRetries + 1}: ${a.error}`
143
+ ).join('\n');
144
+
145
+ const errorMessage = [
146
+ `Push to origin failed after ${maxRetries + 1} attempts.`,
147
+ '',
148
+ 'Retry attempts:',
149
+ attemptsLog,
150
+ '',
151
+ 'Possible causes:',
152
+ ' • Another instance is continuously pushing changes',
153
+ ' • Network connectivity issues',
154
+ ' • Git server is under heavy load or unavailable',
155
+ ' • Repository has been locked or has restrictions',
156
+ '',
157
+ 'Manual resolution steps:',
158
+ ' 1. Check if other instances are running: ps aux | grep jettypod',
159
+ ' 2. Verify network connectivity: git fetch origin',
160
+ ' 3. Check remote repository status on GitHub/GitLab',
161
+ ' 4. Wait a few minutes and try merging again',
162
+ ' 5. If issue persists, push manually: git push origin main'
163
+ ].join('\n');
164
+
165
+ throw new Error(errorMessage);
166
+ }
167
+
168
+ // Retry with exponential backoff
169
+ retryCount++;
170
+ result.retryAttempted = true;
171
+ const delay = initialDelay * Math.pow(2, retryCount - 1);
172
+
173
+ console.log(`Push failed (attempt ${retryCount}/${maxRetries + 1}), retrying in ${delay}ms...`);
174
+ await sleep(delay);
175
+
176
+ // Re-fetch and rebase before retry
177
+ await fetchOrigin(repoPath);
178
+ await fastForwardMain(repoPath);
179
+ await rebaseFeatureBranch(repoPath, featureBranch);
180
+ }
181
+ }
182
+
183
+ return result;
184
+ } catch (err) {
185
+ if (!result.error) {
186
+ result.error = err.message;
187
+ }
188
+ throw err;
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Fetch latest changes from origin
194
+ *
195
+ * @param {string} repoPath - Path to git repository
196
+ * @returns {Promise<void>}
197
+ * @throws {Error} If fetch fails
198
+ */
199
+ async function fetchOrigin(repoPath) {
200
+ try {
201
+ execSync('git fetch origin', { cwd: repoPath, stdio: 'pipe' });
202
+ } catch (err) {
203
+ // Detect specific network error types
204
+ const errorMsg = err.message.toLowerCase();
205
+
206
+ // DNS/hostname resolution failures
207
+ if (errorMsg.includes('could not resolve host') ||
208
+ errorMsg.includes('temporary failure in name resolution') ||
209
+ errorMsg.includes('nodename nor servname provided')) {
210
+ throw new Error([
211
+ 'Network error: Unable to resolve remote repository hostname.',
212
+ '',
213
+ 'Possible causes:',
214
+ ' • No internet connection',
215
+ ' • DNS server is unavailable',
216
+ ' • Hostname is incorrect',
217
+ '',
218
+ 'Resolution steps:',
219
+ ' 1. Check internet connection: ping 8.8.8.8',
220
+ ' 2. Verify DNS: nslookup github.com',
221
+ ' 3. Check remote URL: git remote -v',
222
+ ' 4. Try using mobile hotspot or different network'
223
+ ].join('\n'));
224
+ }
225
+
226
+ // Connection refused / timeout
227
+ if (errorMsg.includes('connection refused') ||
228
+ errorMsg.includes('connection timed out') ||
229
+ errorMsg.includes('failed to connect')) {
230
+ throw new Error([
231
+ 'Network error: Cannot connect to remote repository.',
232
+ '',
233
+ 'Possible causes:',
234
+ ' • Git server is down or unreachable',
235
+ ' • Firewall blocking git protocol',
236
+ ' • Network timeout',
237
+ ' • VPN or proxy issues',
238
+ '',
239
+ 'Resolution steps:',
240
+ ' 1. Check server status (e.g., https://www.githubstatus.com/)',
241
+ ' 2. Try using HTTPS instead of SSH: git remote set-url origin https://...',
242
+ ' 3. Check firewall settings',
243
+ ' 4. Disable VPN temporarily and retry'
244
+ ].join('\n'));
245
+ }
246
+
247
+ // Authentication failures
248
+ if (errorMsg.includes('authentication failed') ||
249
+ errorMsg.includes('permission denied') ||
250
+ errorMsg.includes('could not read from remote')) {
251
+ throw new Error([
252
+ 'Network error: Authentication to remote repository failed.',
253
+ '',
254
+ 'Possible causes:',
255
+ ' • SSH key not configured or expired',
256
+ ' • Invalid credentials',
257
+ ' • Token expired',
258
+ ' • Insufficient repository permissions',
259
+ '',
260
+ 'Resolution steps:',
261
+ ' 1. Check SSH key: ssh -T git@github.com',
262
+ ' 2. Verify credentials are current',
263
+ ' 3. Regenerate personal access token if needed',
264
+ ' 4. Ensure you have push access to repository'
265
+ ].join('\n'));
266
+ }
267
+
268
+ // Generic network error
269
+ throw new Error([
270
+ `Failed to fetch from origin: ${err.message}`,
271
+ '',
272
+ 'This may be a network connectivity issue.',
273
+ '',
274
+ 'Try:',
275
+ ' 1. Check internet connection',
276
+ ' 2. Verify git remote: git remote -v',
277
+ ' 3. Test fetch manually: git fetch origin',
278
+ ' 4. Check git server status'
279
+ ].join('\n'));
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Fast-forward local main to match origin/main
285
+ *
286
+ * @param {string} repoPath - Path to git repository
287
+ * @returns {Promise<void>}
288
+ * @throws {Error} If fast-forward fails
289
+ */
290
+ async function fastForwardMain(repoPath) {
291
+ try {
292
+ // Checkout main
293
+ execSync('git checkout main', { cwd: repoPath, stdio: 'pipe' });
294
+
295
+ // Fast-forward merge from origin/main
296
+ execSync('git merge --ff-only origin/main', { cwd: repoPath, stdio: 'pipe' });
297
+ } catch (err) {
298
+ throw new Error(`Failed to fast-forward main: ${err.message}`);
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Rebase feature branch onto main
304
+ *
305
+ * @param {string} repoPath - Path to git repository
306
+ * @param {string} featureBranch - Name of feature branch
307
+ * @returns {Promise<void>}
308
+ * @throws {Error} If rebase fails
309
+ */
310
+ async function rebaseFeatureBranch(repoPath, featureBranch) {
311
+ try {
312
+ // Checkout feature branch
313
+ execSync(`git checkout ${featureBranch}`, { cwd: repoPath, stdio: 'pipe' });
314
+
315
+ // Rebase onto main
316
+ execSync('git rebase main', { cwd: repoPath, stdio: 'pipe' });
317
+ } catch (err) {
318
+ const errorMsg = err.message.toLowerCase();
319
+
320
+ // Detect conflict state
321
+ const isConflict = errorMsg.includes('conflict') ||
322
+ errorMsg.includes('merge conflict') ||
323
+ errorMsg.includes('needs merge');
324
+
325
+ // Check if rebase is in progress
326
+ let rebaseInProgress = false;
327
+ try {
328
+ const gitDir = execSync('git rev-parse --git-dir', { cwd: repoPath, encoding: 'utf-8' }).trim();
329
+ const rebaseDir = path.join(repoPath, gitDir, 'rebase-merge');
330
+ rebaseInProgress = fs.existsSync(rebaseDir) || fs.existsSync(path.join(repoPath, gitDir, 'rebase-apply'));
331
+ } catch (checkErr) {
332
+ // Ignore errors checking for rebase state
333
+ }
334
+
335
+ if (isConflict && rebaseInProgress) {
336
+ // Get list of conflicted files
337
+ let conflictedFiles = [];
338
+ try {
339
+ const status = execSync('git status --porcelain', { cwd: repoPath, encoding: 'utf-8' });
340
+ conflictedFiles = status
341
+ .split('\n')
342
+ .filter(line => line.startsWith('UU ') || line.startsWith('AA ') || line.startsWith('DD '))
343
+ .map(line => line.substring(3).trim());
344
+ } catch (statusErr) {
345
+ // Ignore status errors
346
+ }
347
+
348
+ const filesMsg = conflictedFiles.length > 0
349
+ ? `\nConflicted files:\n${conflictedFiles.map(f => ` • ${f}`).join('\n')}`
350
+ : '';
351
+
352
+ // Abort rebase to clean state
353
+ try {
354
+ execSync('git rebase --abort', { cwd: repoPath, stdio: 'pipe' });
355
+ } catch (abortErr) {
356
+ // Continue even if abort fails
357
+ }
358
+
359
+ throw new Error([
360
+ 'Rebase conflict detected: Feature branch has changes that conflict with main.',
361
+ filesMsg,
362
+ '',
363
+ 'Your work has been preserved - the rebase was aborted and your feature branch is intact.',
364
+ '',
365
+ 'Resolution steps:',
366
+ ' 1. Manually rebase in the repository:',
367
+ ` cd ${repoPath}`,
368
+ ` git checkout ${featureBranch}`,
369
+ ' git rebase main',
370
+ '',
371
+ ' 2. Resolve each conflict:',
372
+ ' • Edit conflicted files (marked with <<<<<<< and >>>>>>>)',
373
+ ' • Choose which changes to keep',
374
+ ' • Stage resolved files: git add <file>',
375
+ ' • Continue: git rebase --continue',
376
+ '',
377
+ ' 3. After resolving all conflicts:',
378
+ ' • Verify tests still pass',
379
+ ' • Push the rebased branch: git push --force-with-lease',
380
+ '',
381
+ ' 4. Then retry the merge operation',
382
+ '',
383
+ 'Tip: Use a merge tool for easier conflict resolution: git mergetool'
384
+ ].join('\n'));
385
+ }
386
+
387
+ // Other rebase errors - still abort to clean state
388
+ try {
389
+ execSync('git rebase --abort', { cwd: repoPath, stdio: 'pipe' });
390
+ } catch (abortErr) {
391
+ // Ignore abort errors
392
+ }
393
+
394
+ throw new Error([
395
+ `Failed to rebase feature branch: ${err.message}`,
396
+ '',
397
+ 'The rebase was aborted and your feature branch is intact.',
398
+ '',
399
+ 'Possible causes:',
400
+ ' • Commits cannot be automatically applied',
401
+ ' • Invalid git state',
402
+ ' • Repository corruption',
403
+ '',
404
+ 'Try:',
405
+ ' 1. Manually rebase: git checkout ${featureBranch} && git rebase main',
406
+ ' 2. Check git status: git status',
407
+ ' 3. Verify repository health: git fsck'
408
+ ].join('\n'));
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Merge feature branch to main
414
+ *
415
+ * @param {string} repoPath - Path to git repository
416
+ * @param {string} featureBranch - Name of feature branch
417
+ * @returns {Promise<void>}
418
+ * @throws {Error} If merge fails
419
+ */
420
+ async function mergeFeatureBranch(repoPath, featureBranch) {
421
+ try {
422
+ // Checkout main
423
+ execSync('git checkout main', { cwd: repoPath, stdio: 'pipe' });
424
+
425
+ // Merge feature branch with --no-ff to preserve merge history
426
+ execSync(`git merge --no-ff ${featureBranch} -m "Merge ${featureBranch}"`, { cwd: repoPath, stdio: 'pipe' });
427
+ } catch (err) {
428
+ throw new Error(`Failed to merge feature branch: ${err.message}`);
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Push to origin
434
+ *
435
+ * @param {string} repoPath - Path to git repository
436
+ * @returns {Promise<void>}
437
+ * @throws {Error} If push fails
438
+ */
439
+ async function pushToOrigin(repoPath) {
440
+ try {
441
+ execSync('git push origin main', { cwd: repoPath, stdio: 'pipe' });
442
+ } catch (err) {
443
+ throw new Error(`Failed to push to origin: ${err.message}`);
444
+ }
445
+ }
446
+
447
+ /**
448
+ * Detect if local main has diverged from origin/main
449
+ *
450
+ * @param {string} repoPath - Path to git repository
451
+ * @returns {Promise<boolean>} True if branches have diverged
452
+ */
453
+ async function detectDivergence(repoPath) {
454
+ try {
455
+ // Get merge-base between main and origin/main
456
+ const mergeBase = execSync('git merge-base main origin/main', { cwd: repoPath, encoding: 'utf-8' }).trim();
457
+
458
+ // Get current commit of local main
459
+ const localMain = execSync('git rev-parse main', { cwd: repoPath, encoding: 'utf-8' }).trim();
460
+
461
+ // If merge-base is not equal to local main, then main has commits that origin doesn't
462
+ // Check if we can fast-forward
463
+ if (mergeBase !== localMain) {
464
+ // Local main has commits that origin/main doesn't have
465
+ return true;
466
+ }
467
+
468
+ return false;
469
+ } catch (err) {
470
+ throw new Error(`Failed to detect divergence: ${err.message}`);
471
+ }
472
+ }
473
+
474
+ /**
475
+ * Validate git repository health before operations
476
+ *
477
+ * @param {string} repoPath - Path to git repository
478
+ * @returns {Promise<void>}
479
+ * @throws {Error} If repository is invalid or corrupted
480
+ */
481
+ async function validateGitRepo(repoPath) {
482
+ // Check if path exists
483
+ if (!fs.existsSync(repoPath)) {
484
+ throw new Error([
485
+ 'Git repository path does not exist.',
486
+ '',
487
+ `Path: ${repoPath}`,
488
+ '',
489
+ 'Ensure the repository path is correct.'
490
+ ].join('\n'));
491
+ }
492
+
493
+ // Check if it's a git repository
494
+ try {
495
+ execSync('git rev-parse --git-dir', { cwd: repoPath, stdio: 'pipe' });
496
+ } catch (err) {
497
+ throw new Error([
498
+ 'Not a valid git repository.',
499
+ '',
500
+ `Path: ${repoPath}`,
501
+ '',
502
+ 'Initialize a git repository first:',
503
+ ' git init'
504
+ ].join('\n'));
505
+ }
506
+
507
+ // Check for ongoing git operations that would block our operations
508
+ const gitDir = execSync('git rev-parse --git-dir', { cwd: repoPath, encoding: 'utf-8' }).trim();
509
+ const gitDirPath = path.isAbsolute(gitDir) ? gitDir : path.join(repoPath, gitDir);
510
+
511
+ const blockingFiles = [
512
+ 'rebase-merge',
513
+ 'rebase-apply',
514
+ 'MERGE_HEAD',
515
+ 'CHERRY_PICK_HEAD',
516
+ 'REVERT_HEAD',
517
+ 'BISECT_LOG'
518
+ ];
519
+
520
+ for (const file of blockingFiles) {
521
+ const filePath = path.join(gitDirPath, file);
522
+ if (fs.existsSync(filePath)) {
523
+ const operation = file.replace(/[-_]/g, ' ').toLowerCase();
524
+ throw new Error([
525
+ `Git repository has an ongoing ${operation} operation.`,
526
+ '',
527
+ 'Complete or abort the current operation first:',
528
+ ' • For rebase: git rebase --abort or git rebase --continue',
529
+ ' • For merge: git merge --abort or complete the merge',
530
+ ' • For cherry-pick: git cherry-pick --abort',
531
+ ' • For revert: git revert --abort',
532
+ '',
533
+ 'Then retry the merge operation.'
534
+ ].join('\n'));
535
+ }
536
+ }
537
+
538
+ // Run git fsck to check for corruption
539
+ try {
540
+ const fsckOutput = execSync('git fsck --no-progress 2>&1', {
541
+ cwd: repoPath,
542
+ encoding: 'utf-8',
543
+ stdio: 'pipe'
544
+ });
545
+
546
+ // Check for critical errors (ignore warnings)
547
+ if (fsckOutput.includes('error:') || fsckOutput.includes('fatal:')) {
548
+ throw new Error([
549
+ 'Git repository corruption detected.',
550
+ '',
551
+ 'Repository integrity check failed:',
552
+ fsckOutput.split('\n').filter(line => line.includes('error:') || line.includes('fatal:')).join('\n'),
553
+ '',
554
+ 'Recovery steps:',
555
+ ' 1. Backup your work immediately',
556
+ ' 2. Try: git fsck --full',
557
+ ' 3. Attempt recovery: git reflog',
558
+ ' 4. If needed, clone fresh from origin',
559
+ '',
560
+ 'WARNING: Repository may need manual repair or re-cloning.'
561
+ ].join('\n'));
562
+ }
563
+ } catch (err) {
564
+ // fsck failed to run
565
+ if (err.message.includes('error:') || err.message.includes('fatal:')) {
566
+ throw new Error([
567
+ 'Git repository validation failed.',
568
+ '',
569
+ `Error: ${err.message}`,
570
+ '',
571
+ 'The repository may be corrupted or in an invalid state.',
572
+ '',
573
+ 'Try:',
574
+ ' 1. Check repository status: git status',
575
+ ' 2. Verify repository: git fsck',
576
+ ' 3. If needed, clone fresh from origin'
577
+ ].join('\n'));
578
+ }
579
+ // Ignore other fsck errors (exit code != 0 but no critical errors)
580
+ }
581
+
582
+ // Check if remote 'origin' exists
583
+ try {
584
+ execSync('git remote get-url origin', { cwd: repoPath, stdio: 'pipe' });
585
+ } catch (err) {
586
+ throw new Error([
587
+ 'Git remote "origin" is not configured.',
588
+ '',
589
+ 'Configure the remote first:',
590
+ ' git remote add origin <repository-url>',
591
+ '',
592
+ 'Or verify existing remotes:',
593
+ ' git remote -v'
594
+ ].join('\n'));
595
+ }
596
+ }
597
+
598
+ /**
599
+ * Sleep for specified milliseconds
600
+ *
601
+ * @param {number} ms - Milliseconds to sleep
602
+ * @returns {Promise<void>}
603
+ */
604
+ function sleep(ms) {
605
+ return new Promise(resolve => setTimeout(resolve, ms));
606
+ }
607
+
608
+ module.exports = {
609
+ smartFetchAndRebase,
610
+ fetchOrigin,
611
+ fastForwardMain,
612
+ rebaseFeatureBranch,
613
+ detectDivergence
614
+ };
@@ -0,0 +1,76 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Parse a step definition file to extract step definitions with their line numbers
6
+ * @param {string} filePath - Absolute path to step definition file
7
+ * @returns {Map} Map of step text to {file, lineNumber}
8
+ */
9
+ function parseStepDefinitions(filePath) {
10
+ const stepMap = new Map();
11
+
12
+ if (!fs.existsSync(filePath)) {
13
+ return stepMap;
14
+ }
15
+
16
+ const content = fs.readFileSync(filePath, 'utf8');
17
+ const lines = content.split('\n');
18
+
19
+ // Regex to match Given/When/Then/And statements
20
+ // Matches: Given('step text'), When("step text"), etc.
21
+ const stepRegex = /(?:Given|When|Then|And|But)\s*\(\s*['"](.+?)['"]/;
22
+
23
+ lines.forEach((line, index) => {
24
+ const lineNumber = index + 1; // Line numbers start at 1
25
+
26
+ // Skip comments
27
+ if (line.trim().startsWith('//') || line.trim().startsWith('/*')) {
28
+ return;
29
+ }
30
+
31
+ const match = line.match(stepRegex);
32
+ if (match) {
33
+ const stepText = match[1];
34
+ stepMap.set(stepText, {
35
+ file: filePath,
36
+ lineNumber: lineNumber
37
+ });
38
+ }
39
+ });
40
+
41
+ return stepMap;
42
+ }
43
+
44
+ /**
45
+ * Parse all step definition files in a directory
46
+ * @param {string} dirPath - Path to step_definitions directory
47
+ * @returns {Map} Combined map of all step definitions
48
+ */
49
+ function parseAllStepDefinitions(dirPath) {
50
+ const combinedMap = new Map();
51
+
52
+ if (!fs.existsSync(dirPath)) {
53
+ return combinedMap;
54
+ }
55
+
56
+ const files = fs.readdirSync(dirPath);
57
+
58
+ files.forEach(file => {
59
+ if (file.endsWith('.steps.js')) {
60
+ const filePath = path.join(dirPath, file);
61
+ const fileSteps = parseStepDefinitions(filePath);
62
+
63
+ // Merge into combined map
64
+ fileSteps.forEach((value, key) => {
65
+ combinedMap.set(key, value);
66
+ });
67
+ }
68
+ });
69
+
70
+ return combinedMap;
71
+ }
72
+
73
+ module.exports = {
74
+ parseStepDefinitions,
75
+ parseAllStepDefinitions
76
+ };