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.
- package/.nvmrc +1 -0
- package/docs/COMPLETE-TESTING-STRATEGY.md +970 -0
- package/docs/DECISIONS.md +10 -12
- package/docs/NODE_VERSION.md +83 -0
- package/docs/TDD-INFRASTRUCTURE-STRATEGY.md +1374 -0
- package/docs/TESTING-FOR-NON-ENGINEERS.md +1588 -0
- package/docs/TESTING-STRATEGY-AUDIT.md +698 -0
- package/hooks/post-checkout +17 -0
- package/hooks/post-merge +17 -0
- package/hooks/pre-commit +30 -0
- package/jettypod.js +259 -120
- package/lib/coverage-tracker.js +218 -0
- package/lib/database.js +2 -0
- package/lib/db-export.js +192 -0
- package/lib/db-import.js +193 -0
- package/lib/external-transition-handler.js +32 -0
- package/lib/git-hook-helpers.js +174 -0
- package/lib/git-root.js +90 -0
- package/lib/infrastructure-chore-generator.js +45 -0
- package/lib/install-hooks.js +52 -0
- package/lib/jettypod-backup.js +238 -0
- package/lib/merge-lock.js +193 -0
- package/lib/migrations/012-add-worktree-path.js +38 -0
- package/lib/migrations/013-worktrees-table.js +86 -0
- package/lib/migrations/014-migrate-worktree-data.js +161 -0
- package/lib/migrations/015-merge-locks-table.js +67 -0
- package/lib/pattern-finder.js +152 -0
- package/lib/process-manager.js +140 -0
- package/lib/production-standards-reader.js +13 -2
- package/lib/production-standards-writer.js +85 -0
- package/lib/skills/feature-planning/dry-run-validator.js +135 -0
- package/lib/skills/feature-planning/validation-formatter.js +160 -0
- package/lib/smart-conflict-detection.js +168 -0
- package/lib/smart-fetch-rebase.js +614 -0
- package/lib/step-definition-parser.js +76 -0
- package/lib/unit-test-generator.js +232 -0
- package/lib/verification-command-generator.js +66 -0
- package/lib/worktree-diagnostics.js +413 -0
- package/lib/worktree-facade.js +174 -0
- package/lib/worktree-manager.js +636 -0
- package/lib/worktree-reconciler.js +429 -0
- package/package.json +30 -3
- package/skills-templates/external-transition/SKILL.md +34 -3
- package/skills-templates/feature-planning/SKILL.md +190 -24
- package/skills-templates/production-mode/SKILL.md +127 -9
- package/skills-templates/speed-mode/SKILL.md +454 -51
- package/skills-templates/stable-mode/SKILL.md +285 -76
- package/.claude/PROTECT_SKILLS.md +0 -28
- package/.claude/settings.json +0 -24
- package/.claude/settings.local.json +0 -16
- package/.claude/skills/epic-planning/SKILL.md +0 -297
- package/.claude/skills/external-transition/SKILL.md +0 -384
- package/.claude/skills/feature-planning/SKILL.md +0 -464
- package/.claude/skills/production-mode/SKILL.md +0 -369
- package/.claude/skills/speed-mode/SKILL.md +0 -481
- package/.claude/skills/stable-mode/SKILL.md +0 -713
- package/.claude/skills.backup-2025-11-10T23-33-09-368Z/epic-planning/SKILL.md +0 -297
- package/.claude/skills.backup-2025-11-10T23-33-09-368Z/feature-planning/SKILL.md +0 -464
- package/.claude/skills.backup-2025-11-10T23-33-09-368Z/speed-mode/SKILL.md +0 -467
- package/.claude/skills.backup-2025-11-10T23-33-09-368Z/stable-mode/SKILL.md +0 -673
- package/.claude/skills.backup-2025-11-11T16-15-10-070Z/epic-discover/SKILL.md +0 -297
- package/.claude/skills.backup-2025-11-11T16-42-43-212Z/epic-planning/SKILL.md +0 -297
- package/.claude/skills.backup-2025-11-11T16-42-43-212Z/feature-planning/SKILL.md +0 -464
- package/.claude/skills.backup-2025-11-11T16-42-43-212Z/speed-mode/SKILL.md +0 -467
- package/.claude/skills.backup-2025-11-11T16-42-43-212Z/stable-mode/SKILL.md +0 -673
- package/.claude/skills.backup-2025-11-11T17-06-09-783Z/epic-planning/SKILL.md +0 -297
- package/.claude/skills.backup-2025-11-11T17-06-09-783Z/feature-planning/SKILL.md +0 -464
- package/.claude/skills.backup-2025-11-11T17-06-09-783Z/speed-mode/SKILL.md +0 -467
- package/.claude/skills.backup-2025-11-11T17-06-09-783Z/stable-mode/SKILL.md +0 -673
- package/.devpod/current-work.json +0 -10
- package/.devpod/work.db +0 -0
- package/.github/workflows/test-safety.yml +0 -85
- package/.jettypod/config.json +0 -5
- package/.jettypod/current-work.json +0 -10
- package/.jettypod/hooks/README.md +0 -77
- package/.jettypod/hooks/protect-claude-md.js +0 -338
- package/.jettypod/test-work.db +0 -0
- package/.jettypod/work.db +0 -0
- package/CLAUDE.md +0 -49
- package/SPEED-STABLE-AUDIT.md +0 -853
- package/SYSTEM-BEHAVIOR.md +0 -2199
- package/TEST_SAFETY_AUDIT.md +0 -314
- package/TEST_SAFETY_IMPLEMENTATION.md +0 -97
- package/cucumber-report.html +0 -45
- package/dist/devpod-linux +0 -0
- package/dist/devpod-macos +0 -0
- package/dist/devpod-win.exe +0 -0
- package/docs/features/jettypod-standards-explained.md +0 -543
- package/docs/features/standards-inventory.md +0 -257
- package/features/auto-generate-production-chores.feature +0 -13
- package/features/backlog-command.feature +0 -26
- package/features/backlog-filtering-production.feature +0 -10
- package/features/claude-md-protection/steps.js +0 -498
- package/features/decisions/index.js +0 -490
- package/features/decisions/index.test.js +0 -208
- package/features/fix-text-wrapping.feature +0 -42
- package/features/git-hooks/git-hooks.feature +0 -30
- package/features/git-hooks/index.js +0 -93
- package/features/git-hooks/index.test.js +0 -137
- package/features/git-hooks/post-commit +0 -56
- package/features/git-hooks/post-merge +0 -47
- package/features/git-hooks/pre-commit +0 -28
- package/features/git-hooks/simple-steps.js +0 -53
- package/features/git-hooks/simple-test.feature +0 -10
- package/features/git-hooks/steps.js +0 -196
- package/features/jettypod-update-command.feature +0 -46
- package/features/mode-prompts/index.js +0 -95
- package/features/mode-prompts/simple-steps.js +0 -44
- package/features/mode-prompts/simple-test.feature +0 -9
- package/features/mode-prompts/validation.test.js +0 -120
- package/features/multiple-claude-instances.feature +0 -121
- package/features/production-mode-skill.feature +0 -121
- package/features/refactor-mode/steps.js +0 -217
- package/features/refactor-mode.feature +0 -49
- package/features/simplify-external-transition.feature +0 -166
- package/features/skills-update/index.test.js +0 -216
- package/features/step_definitions/backlog-command.steps.js +0 -37
- package/features/step_definitions/fix-text-wrapping.steps.js +0 -271
- package/features/step_definitions/multiple-claude-instances.steps.js +0 -621
- package/features/step_definitions/production-mode-skill.steps.js +0 -862
- package/features/step_definitions/simplify-external-transition.steps.js +0 -370
- package/features/step_definitions/terminal-logo.steps.js +0 -145
- package/features/step_definitions/update-command.steps.js +0 -183
- package/features/support/hooks.js +0 -9
- package/features/terminal-logo/index.js +0 -39
- package/features/terminal-logo/terminal-logo.feature +0 -30
- package/features/update-command/index.js +0 -181
- package/features/update-command/index.test.js +0 -225
- package/features/work-commands/bug-workflow-display.feature +0 -22
- package/features/work-commands/index.js +0 -498
- package/features/work-commands/simple-steps.js +0 -69
- package/features/work-commands/stable-tests.feature +0 -57
- package/features/work-commands/steps.js +0 -1174
- package/features/work-commands/validation.test.js +0 -88
- package/features/work-commands/work-commands.feature +0 -13
- package/features/work-tracking/discovery-validation.test.js +0 -228
- package/features/work-tracking/index.js +0 -1921
- package/features/work-tracking/mode-required.feature +0 -112
- package/features/work-tracking/phase-tracking.test.js +0 -482
- package/features/work-tracking/prototype-tracking.test.js +0 -485
- package/features/work-tracking/tree-view.test.js +0 -310
- package/features/work-tracking/work-set-mode.feature +0 -71
- package/features/work-tracking/work-start-mode.feature +0 -88
- package/full-test.txt +0 -0
- package/lib/bug-workflow.test.js +0 -177
- package/lib/claudemd.test.js +0 -195
- package/lib/config.test.js +0 -511
- package/lib/constants.test.js +0 -164
- package/lib/current-work.test.js +0 -146
- package/lib/database-project-config.test.js +0 -111
- package/lib/database.test.js +0 -106
- package/lib/decisions-generator.test.js +0 -457
- package/lib/decisions-helpers.test.js +0 -310
- package/lib/git-coordinator.js +0 -167
- package/lib/git.test.js +0 -145
- package/lib/migrations/002-default-work-item-modes.test.js +0 -351
- package/lib/production-chore-generator.test.js +0 -432
- package/lib/production-context-detector.test.js +0 -277
- package/lib/production-scenario-appender.test.js +0 -235
- package/lib/production-scenario-validator.test.js +0 -246
- package/lib/production-standards-reader.test.js +0 -270
- package/lib/project-state.test.js +0 -92
- package/lib/push-queue.js +0 -417
- package/lib/queue-processor.js +0 -74
- package/lib/test-helpers.js +0 -202
- package/lib/test-helpers.test.js +0 -255
- package/prototypes/2025-01-11-production-mode-autonomous.js +0 -119
- package/prototypes/2025-01-11-production-mode-collaborative.js +0 -166
- package/prototypes/2025-01-11-production-mode-guided.js +0 -217
- package/prototypes/2025-01-11-production-mode-smart-context.js +0 -347
- package/prototypes/2025-01-11-production-standards-example.md +0 -204
- package/prototypes/2025-11-10-backlog-filtering-tree-aware.js +0 -242
- package/prototypes/test/index.html +0 -1
- package/setup-dist-repo.sh +0 -68
- package/test-production-standards-engine.js +0 -130
- package/test-results.json +0 -2195
- package/test-safety-check.sh +0 -80
- package/work-item-tracking-plan.md +0 -199
- /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
|
+
};
|