vibecodingmachine-core 2025.12.25-25 → 2026.1.22-1441
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/ERROR_REPORTING_API.md +212 -0
- package/ERROR_REPORTING_USAGE.md +380 -0
- package/__tests__/provider-manager-fallback.test.js +43 -0
- package/__tests__/provider-manager-rate-limit.test.js +61 -0
- package/__tests__/utils/git-branch-manager.test.js +61 -0
- package/package.json +1 -1
- package/src/beta-request.js +160 -0
- package/src/compliance/compliance-manager.js +5 -2
- package/src/database/migrations.js +135 -12
- package/src/database/user-database-client.js +127 -8
- package/src/database/user-schema.js +28 -0
- package/src/health-tracking/__tests__/ide-health-tracker.test.js +420 -0
- package/src/health-tracking/__tests__/interaction-recorder.test.js +392 -0
- package/src/health-tracking/errors.js +50 -0
- package/src/health-tracking/health-reporter.js +331 -0
- package/src/health-tracking/ide-health-tracker.js +446 -0
- package/src/health-tracking/interaction-recorder.js +161 -0
- package/src/health-tracking/json-storage.js +276 -0
- package/src/health-tracking/storage-interface.js +63 -0
- package/src/health-tracking/validators.js +277 -0
- package/src/ide-integration/applescript-manager.cjs +1087 -9
- package/src/ide-integration/applescript-manager.js +565 -15
- package/src/ide-integration/applescript-utils.js +26 -18
- package/src/ide-integration/provider-manager.cjs +158 -28
- package/src/ide-integration/quota-detector.cjs +339 -16
- package/src/ide-integration/quota-detector.js +6 -1
- package/src/index.cjs +36 -1
- package/src/index.js +20 -0
- package/src/localization/translations/en.js +15 -1
- package/src/localization/translations/es.js +14 -0
- package/src/requirement-numbering.js +164 -0
- package/src/sync/aws-setup.js +4 -4
- package/src/utils/admin-utils.js +33 -0
- package/src/utils/error-reporter.js +117 -0
- package/src/utils/git-branch-manager.js +278 -0
- package/src/utils/requirement-helpers.js +44 -5
- package/src/utils/requirements-parser.js +28 -3
- package/tests/health-tracking/health-reporter.test.js +329 -0
- package/tests/health-tracking/ide-health-tracker.test.js +368 -0
- package/tests/health-tracking/interaction-recorder.test.js +309 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get metadata file path for storing requirement counter
|
|
6
|
+
* @param {string} repoPath - Path to repository
|
|
7
|
+
* @returns {string} Path to metadata file
|
|
8
|
+
*/
|
|
9
|
+
function getMetadataPath(repoPath) {
|
|
10
|
+
return path.join(repoPath, '.vibecodingmachine', 'requirement-metadata.json');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get the next requirement number for this repository
|
|
15
|
+
* @param {string} repoPath - Path to repository
|
|
16
|
+
* @returns {Promise<number>} Next requirement number
|
|
17
|
+
*/
|
|
18
|
+
async function getNextRequirementNumber(repoPath) {
|
|
19
|
+
const metadataPath = getMetadataPath(repoPath);
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await fs.ensureFile(metadataPath);
|
|
23
|
+
const metadata = await fs.readJson(metadataPath).catch(() => ({ lastRequirementNumber: 0 }));
|
|
24
|
+
|
|
25
|
+
const nextNumber = (metadata.lastRequirementNumber || 0) + 1;
|
|
26
|
+
|
|
27
|
+
// Save the incremented number
|
|
28
|
+
metadata.lastRequirementNumber = nextNumber;
|
|
29
|
+
await fs.writeJson(metadataPath, metadata, { spaces: 2 });
|
|
30
|
+
|
|
31
|
+
return nextNumber;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
// Default to 1 if there's any error
|
|
34
|
+
console.error('Error getting next requirement number:', error.message);
|
|
35
|
+
return 1;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Set the last requirement number (used during initialization)
|
|
41
|
+
* @param {string} repoPath - Path to repository
|
|
42
|
+
* @param {number} lastNumber - Last requirement number
|
|
43
|
+
*/
|
|
44
|
+
async function setLastRequirementNumber(repoPath, lastNumber) {
|
|
45
|
+
const metadataPath = getMetadataPath(repoPath);
|
|
46
|
+
|
|
47
|
+
await fs.ensureFile(metadataPath);
|
|
48
|
+
const metadata = await fs.readJson(metadataPath).catch(() => ({}));
|
|
49
|
+
metadata.lastRequirementNumber = lastNumber;
|
|
50
|
+
await fs.writeJson(metadataPath, metadata, { spaces: 2 });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Extract requirement number from requirement title
|
|
55
|
+
* @param {string} title - Requirement title
|
|
56
|
+
* @returns {number|null} Requirement number or null if not found
|
|
57
|
+
*/
|
|
58
|
+
function extractRequirementNumber(title) {
|
|
59
|
+
const match = title.match(/^R(\d+):/);
|
|
60
|
+
return match ? parseInt(match[1], 10) : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Number all existing requirements in a requirements file
|
|
65
|
+
* @param {string} reqPath - Path to requirements file
|
|
66
|
+
* @param {string} repoPath - Path to repository
|
|
67
|
+
* @returns {Promise<number>} Number of requirements numbered
|
|
68
|
+
*/
|
|
69
|
+
async function numberAllRequirements(reqPath, repoPath) {
|
|
70
|
+
if (!await fs.pathExists(reqPath)) {
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const content = await fs.readFile(reqPath, 'utf8');
|
|
75
|
+
const lines = content.split('\n');
|
|
76
|
+
const newLines = [];
|
|
77
|
+
|
|
78
|
+
let requirementCount = 0;
|
|
79
|
+
let maxExistingNumber = 0;
|
|
80
|
+
|
|
81
|
+
// First pass: collect existing numbers and count requirements
|
|
82
|
+
for (const line of lines) {
|
|
83
|
+
if (line.trim().startsWith('###')) {
|
|
84
|
+
const title = line.trim().substring(3).trim();
|
|
85
|
+
const existingNumber = extractRequirementNumber(title);
|
|
86
|
+
|
|
87
|
+
if (existingNumber) {
|
|
88
|
+
maxExistingNumber = Math.max(maxExistingNumber, existingNumber);
|
|
89
|
+
} else {
|
|
90
|
+
// This requirement needs numbering
|
|
91
|
+
requirementCount++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Start numbering from max existing + 1
|
|
97
|
+
let currentNumber = maxExistingNumber + 1;
|
|
98
|
+
let numbered = 0;
|
|
99
|
+
|
|
100
|
+
// Second pass: add numbers to requirements that don't have them
|
|
101
|
+
for (const line of lines) {
|
|
102
|
+
if (line.trim().startsWith('###')) {
|
|
103
|
+
const title = line.trim().substring(3).trim();
|
|
104
|
+
const existingNumber = extractRequirementNumber(title);
|
|
105
|
+
|
|
106
|
+
if (!existingNumber) {
|
|
107
|
+
// Add number to this requirement
|
|
108
|
+
newLines.push(`### R${currentNumber}: ${title}`);
|
|
109
|
+
currentNumber++;
|
|
110
|
+
numbered++;
|
|
111
|
+
} else {
|
|
112
|
+
// Keep existing numbered requirement as-is
|
|
113
|
+
newLines.push(line);
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
newLines.push(line);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Save the updated file
|
|
121
|
+
await fs.writeFile(reqPath, newLines.join('\n'));
|
|
122
|
+
|
|
123
|
+
// Update metadata with the highest number used
|
|
124
|
+
await setLastRequirementNumber(repoPath, currentNumber - 1);
|
|
125
|
+
|
|
126
|
+
return numbered;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Create a git branch name from requirement number and title
|
|
131
|
+
* @param {number} reqNumber - Requirement number
|
|
132
|
+
* @param {string} title - Requirement title
|
|
133
|
+
* @returns {string} Branch name (e.g., "R1_email_system")
|
|
134
|
+
*/
|
|
135
|
+
function createBranchName(reqNumber, title) {
|
|
136
|
+
// Remove requirement number from title if present
|
|
137
|
+
let cleanTitle = title.replace(/^R\d+:\s*/, '').trim();
|
|
138
|
+
|
|
139
|
+
// Take first few words (up to 3-4 words, max 30 chars)
|
|
140
|
+
const words = cleanTitle.split(/\s+/);
|
|
141
|
+
let branchSuffix = '';
|
|
142
|
+
|
|
143
|
+
for (const word of words) {
|
|
144
|
+
const candidate = branchSuffix ? `${branchSuffix}_${word}` : word;
|
|
145
|
+
if (candidate.length > 30) break;
|
|
146
|
+
branchSuffix = candidate;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Clean up branch suffix: lowercase, remove special chars
|
|
150
|
+
branchSuffix = branchSuffix
|
|
151
|
+
.toLowerCase()
|
|
152
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
153
|
+
.replace(/^_+|_+$/g, '');
|
|
154
|
+
|
|
155
|
+
return branchSuffix ? `R${reqNumber}_${branchSuffix}` : `R${reqNumber}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = {
|
|
159
|
+
getNextRequirementNumber,
|
|
160
|
+
setLastRequirementNumber,
|
|
161
|
+
extractRequirementNumber,
|
|
162
|
+
numberAllRequirements,
|
|
163
|
+
createBranchName
|
|
164
|
+
};
|
package/src/sync/aws-setup.js
CHANGED
|
@@ -116,12 +116,12 @@ async function setupCognitoWithGoogle() {
|
|
|
116
116
|
|
|
117
117
|
// Add Google OAuth provider if not already configured
|
|
118
118
|
try {
|
|
119
|
-
const googleClientId = process.env.
|
|
119
|
+
const googleClientId = process.env.REACT_APP_GOOGLE_CLIENT_ID;
|
|
120
120
|
const googleClientSecret = process.env.GOOGLE_CLIENT_SECRET;
|
|
121
121
|
|
|
122
122
|
if (!googleClientId || !googleClientSecret) {
|
|
123
123
|
console.log('⚠ Google OAuth credentials not provided. Skipping Google provider setup.');
|
|
124
|
-
console.log(' Set
|
|
124
|
+
console.log(' Set REACT_APP_GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET environment variables.');
|
|
125
125
|
return { userPoolId: existingPoolId, status: 'existing' };
|
|
126
126
|
}
|
|
127
127
|
|
|
@@ -198,7 +198,7 @@ async function setupCognitoWithGoogle() {
|
|
|
198
198
|
console.log(`✓ Created Cognito User Pool: ${userPoolId}`);
|
|
199
199
|
|
|
200
200
|
// Add Google OAuth provider first (if credentials provided)
|
|
201
|
-
const googleClientId = process.env.
|
|
201
|
+
const googleClientId = process.env.REACT_APP_GOOGLE_CLIENT_ID;
|
|
202
202
|
const googleClientSecret = process.env.GOOGLE_CLIENT_SECRET;
|
|
203
203
|
|
|
204
204
|
let supportedProviders = ['COGNITO'];
|
|
@@ -231,7 +231,7 @@ async function setupCognitoWithGoogle() {
|
|
|
231
231
|
}
|
|
232
232
|
} else {
|
|
233
233
|
console.log('⚠ Google OAuth credentials not provided');
|
|
234
|
-
console.log(' Set
|
|
234
|
+
console.log(' Set REACT_APP_GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET to enable Google login');
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
// Create app client
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin Utilities for VibeCodingMachine
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const ADMIN_EMAILS = [
|
|
6
|
+
'jesse.d.olsen@gmail.com'
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Check if an email belongs to an administrator
|
|
11
|
+
* @param {string} email - The email to check
|
|
12
|
+
* @returns {boolean} True if the email is an admin email
|
|
13
|
+
*/
|
|
14
|
+
function isAdmin(email) {
|
|
15
|
+
if (!email) return false;
|
|
16
|
+
return ADMIN_EMAILS.includes(email.toLowerCase().trim());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if the application should automatically start implementation
|
|
21
|
+
* based on the user's email (admin feedback)
|
|
22
|
+
* @param {string} email - The email of the feedback sender
|
|
23
|
+
* @returns {boolean} True if auto-start should be triggered
|
|
24
|
+
*/
|
|
25
|
+
function shouldAutoStart(email) {
|
|
26
|
+
return isAdmin(email);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = {
|
|
30
|
+
isAdmin,
|
|
31
|
+
shouldAutoStart,
|
|
32
|
+
ADMIN_EMAILS
|
|
33
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global Error Reporter for VibeCodingMachine
|
|
3
|
+
*
|
|
4
|
+
* Automatically reports errors to the admin database for tracking and fixing.
|
|
5
|
+
* This helps the admin know about issues and use VibeCodingMachine to fix itself.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
class ErrorReporter {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.enabled = true;
|
|
11
|
+
this.db = null;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const UserDatabase = require('../database/user-schema');
|
|
15
|
+
this.db = new UserDatabase();
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.warn('ErrorReporter: Failed to initialize UserDatabase:', error.message);
|
|
18
|
+
this.enabled = false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Set authentication token for error reporting
|
|
24
|
+
*/
|
|
25
|
+
setAuthToken(token) {
|
|
26
|
+
if (this.db) {
|
|
27
|
+
this.db.setAuthToken(token);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Enable or disable error reporting
|
|
33
|
+
*/
|
|
34
|
+
setEnabled(enabled) {
|
|
35
|
+
this.enabled = enabled;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Report an error to the admin database
|
|
40
|
+
*/
|
|
41
|
+
async reportError(error, context = {}) {
|
|
42
|
+
if (!this.enabled) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const errorData = {
|
|
48
|
+
message: error.message || String(error),
|
|
49
|
+
stack: error.stack || '',
|
|
50
|
+
name: error.name || 'Error',
|
|
51
|
+
code: error.code || '',
|
|
52
|
+
context: {
|
|
53
|
+
...context,
|
|
54
|
+
cwd: process.cwd(),
|
|
55
|
+
argv: process.argv.slice(2).filter(arg =>
|
|
56
|
+
!arg.includes('password') &&
|
|
57
|
+
!arg.includes('token') &&
|
|
58
|
+
!arg.includes('secret')
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
await this.db.reportError(errorData);
|
|
64
|
+
return true;
|
|
65
|
+
} catch (reportError) {
|
|
66
|
+
// Silently fail - don't block the application
|
|
67
|
+
console.warn('Failed to report error:', reportError.message);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Wrap a function to automatically report errors
|
|
74
|
+
*/
|
|
75
|
+
wrapFunction(fn, context = {}) {
|
|
76
|
+
return async (...args) => {
|
|
77
|
+
try {
|
|
78
|
+
return await fn(...args);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
await this.reportError(error, {
|
|
81
|
+
...context,
|
|
82
|
+
functionName: fn.name,
|
|
83
|
+
args: args.filter(arg =>
|
|
84
|
+
typeof arg !== 'object' ||
|
|
85
|
+
!JSON.stringify(arg).match(/password|token|secret/i)
|
|
86
|
+
)
|
|
87
|
+
});
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create a global error handler for uncaught exceptions
|
|
95
|
+
*/
|
|
96
|
+
setupGlobalHandlers() {
|
|
97
|
+
process.on('uncaughtException', async (error) => {
|
|
98
|
+
await this.reportError(error, {
|
|
99
|
+
type: 'uncaughtException'
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
process.on('unhandledRejection', async (error) => {
|
|
104
|
+
await this.reportError(error, {
|
|
105
|
+
type: 'unhandledRejection'
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Singleton instance
|
|
112
|
+
const errorReporter = new ErrorReporter();
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
ErrorReporter,
|
|
116
|
+
errorReporter
|
|
117
|
+
};
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { createBranchName, extractRequirementNumber } = require('../requirement-numbering');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Check if a directory is a git repository
|
|
7
|
+
* @param {string} repoPath - Path to check
|
|
8
|
+
* @returns {boolean} True if it's a git repo
|
|
9
|
+
*/
|
|
10
|
+
function isGitRepo(repoPath) {
|
|
11
|
+
try {
|
|
12
|
+
execSync('git rev-parse --git-dir', {
|
|
13
|
+
cwd: repoPath,
|
|
14
|
+
stdio: 'ignore'
|
|
15
|
+
});
|
|
16
|
+
return true;
|
|
17
|
+
} catch (error) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get current git branch name
|
|
24
|
+
* @param {string} repoPath - Path to repository
|
|
25
|
+
* @returns {string|null} Current branch name or null
|
|
26
|
+
*/
|
|
27
|
+
function getCurrentBranch(repoPath) {
|
|
28
|
+
try {
|
|
29
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
30
|
+
cwd: repoPath,
|
|
31
|
+
encoding: 'utf8'
|
|
32
|
+
}).trim();
|
|
33
|
+
return branch;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create a feature branch for a requirement
|
|
41
|
+
* @param {string} repoPath - Path to repository
|
|
42
|
+
* @param {string} requirementTitle - Full requirement title (e.g., "R1: Email System")
|
|
43
|
+
* @returns {Promise<{success: boolean, branchName: string|null, parentBranch: string|null, error: string|null}>}
|
|
44
|
+
*/
|
|
45
|
+
async function createRequirementBranch(repoPath, requirementTitle) {
|
|
46
|
+
try {
|
|
47
|
+
if (!isGitRepo(repoPath)) {
|
|
48
|
+
return {
|
|
49
|
+
success: false,
|
|
50
|
+
branchName: null,
|
|
51
|
+
parentBranch: null,
|
|
52
|
+
error: 'Not a git repository'
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Extract requirement number from title
|
|
57
|
+
const reqNumber = extractRequirementNumber(requirementTitle);
|
|
58
|
+
if (!reqNumber) {
|
|
59
|
+
return {
|
|
60
|
+
success: false,
|
|
61
|
+
branchName: null,
|
|
62
|
+
parentBranch: null,
|
|
63
|
+
error: 'Requirement number not found in title'
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Get parent branch (current branch before creating new one)
|
|
68
|
+
const parentBranch = getCurrentBranch(repoPath);
|
|
69
|
+
if (!parentBranch) {
|
|
70
|
+
return {
|
|
71
|
+
success: false,
|
|
72
|
+
branchName: null,
|
|
73
|
+
parentBranch: null,
|
|
74
|
+
error: 'Could not determine current branch'
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Create branch name
|
|
79
|
+
const branchName = createBranchName(reqNumber, requirementTitle);
|
|
80
|
+
|
|
81
|
+
// Check if branch already exists
|
|
82
|
+
try {
|
|
83
|
+
execSync(`git rev-parse --verify ${branchName}`, {
|
|
84
|
+
cwd: repoPath,
|
|
85
|
+
stdio: 'ignore'
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Branch exists, check it out
|
|
89
|
+
execSync(`git checkout ${branchName}`, {
|
|
90
|
+
cwd: repoPath,
|
|
91
|
+
stdio: 'ignore'
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
success: true,
|
|
96
|
+
branchName,
|
|
97
|
+
parentBranch,
|
|
98
|
+
error: null,
|
|
99
|
+
alreadyExists: true
|
|
100
|
+
};
|
|
101
|
+
} catch (error) {
|
|
102
|
+
// Branch doesn't exist, create it
|
|
103
|
+
execSync(`git checkout -b ${branchName}`, {
|
|
104
|
+
cwd: repoPath,
|
|
105
|
+
stdio: 'ignore'
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
success: true,
|
|
110
|
+
branchName,
|
|
111
|
+
parentBranch,
|
|
112
|
+
error: null,
|
|
113
|
+
alreadyExists: false
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return {
|
|
118
|
+
success: false,
|
|
119
|
+
branchName: null,
|
|
120
|
+
parentBranch: null,
|
|
121
|
+
error: error.message
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Merge a requirement branch back to parent and delete it
|
|
128
|
+
* @param {string} repoPath - Path to repository
|
|
129
|
+
* @param {string} requirementTitle - Full requirement title
|
|
130
|
+
* @param {string} parentBranch - Parent branch to merge into
|
|
131
|
+
* @returns {Promise<{success: boolean, error: string|null}>}
|
|
132
|
+
*/
|
|
133
|
+
async function mergeRequirementBranch(repoPath, requirementTitle, parentBranch = 'main') {
|
|
134
|
+
try {
|
|
135
|
+
if (!isGitRepo(repoPath)) {
|
|
136
|
+
return { success: false, error: 'Not a git repository' };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Extract requirement number
|
|
140
|
+
const reqNumber = extractRequirementNumber(requirementTitle);
|
|
141
|
+
if (!reqNumber) {
|
|
142
|
+
return { success: false, error: 'Requirement number not found in title' };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Create branch name
|
|
146
|
+
const branchName = createBranchName(reqNumber, requirementTitle);
|
|
147
|
+
|
|
148
|
+
// Checkout parent branch
|
|
149
|
+
execSync(`git checkout ${parentBranch}`, {
|
|
150
|
+
cwd: repoPath,
|
|
151
|
+
stdio: 'ignore'
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Merge the feature branch
|
|
155
|
+
execSync(`git merge --no-ff ${branchName} -m "Merge ${branchName}"`, {
|
|
156
|
+
cwd: repoPath,
|
|
157
|
+
stdio: 'ignore'
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Delete the feature branch
|
|
161
|
+
execSync(`git branch -d ${branchName}`, {
|
|
162
|
+
cwd: repoPath,
|
|
163
|
+
stdio: 'ignore'
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return { success: true, error: null };
|
|
167
|
+
} catch (error) {
|
|
168
|
+
return { success: false, error: error.message };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Remove a feature by reverting its merge commit
|
|
174
|
+
* @param {string} repoPath - Path to repository
|
|
175
|
+
* @param {string} requirementTitle - Full requirement title
|
|
176
|
+
* @returns {Promise<{success: boolean, error: string|null}>}
|
|
177
|
+
*/
|
|
178
|
+
async function removeRequirementFeature(repoPath, requirementTitle) {
|
|
179
|
+
try {
|
|
180
|
+
if (!isGitRepo(repoPath)) {
|
|
181
|
+
return { success: false, error: 'Not a git repository' };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Extract requirement number
|
|
185
|
+
const reqNumber = extractRequirementNumber(requirementTitle);
|
|
186
|
+
if (!reqNumber) {
|
|
187
|
+
return { success: false, error: 'Requirement number not found in title' };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Create branch name to search for
|
|
191
|
+
const branchName = createBranchName(reqNumber, requirementTitle);
|
|
192
|
+
|
|
193
|
+
// Find the merge commit for this branch
|
|
194
|
+
const mergeCommit = execSync(
|
|
195
|
+
`git log --grep="Merge ${branchName}" --oneline --format="%H" -1`,
|
|
196
|
+
{
|
|
197
|
+
cwd: repoPath,
|
|
198
|
+
encoding: 'utf8'
|
|
199
|
+
}
|
|
200
|
+
).trim();
|
|
201
|
+
|
|
202
|
+
if (!mergeCommit) {
|
|
203
|
+
return { success: false, error: 'Merge commit not found for this requirement' };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Revert the merge commit
|
|
207
|
+
execSync(`git revert -m 1 ${mergeCommit} --no-edit`, {
|
|
208
|
+
cwd: repoPath,
|
|
209
|
+
stdio: 'ignore'
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
return { success: true, error: null };
|
|
213
|
+
} catch (error) {
|
|
214
|
+
return { success: false, error: error.message };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Get list of all requirement branches
|
|
220
|
+
* @param {string} repoPath - Path to repository
|
|
221
|
+
* @returns {Array<{branchName: string, reqNumber: number}>}
|
|
222
|
+
*/
|
|
223
|
+
function listRequirementBranches(repoPath) {
|
|
224
|
+
try {
|
|
225
|
+
if (!isGitRepo(repoPath)) {
|
|
226
|
+
return [];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const branches = execSync('git branch --list "R*"', {
|
|
230
|
+
cwd: repoPath,
|
|
231
|
+
encoding: 'utf8'
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
return branches
|
|
235
|
+
.split('\n')
|
|
236
|
+
.map(b => b.trim().replace(/^\*\s*/, ''))
|
|
237
|
+
.filter(b => b && b.startsWith('R'))
|
|
238
|
+
.map(b => {
|
|
239
|
+
const match = b.match(/^R(\d+)/);
|
|
240
|
+
return {
|
|
241
|
+
branchName: b,
|
|
242
|
+
reqNumber: match ? parseInt(match[1], 10) : 0
|
|
243
|
+
};
|
|
244
|
+
})
|
|
245
|
+
.filter(b => b.reqNumber > 0)
|
|
246
|
+
.sort((a, b) => a.reqNumber - b.reqNumber);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
return [];
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Check if there are any uncommitted changes in the repository
|
|
254
|
+
* @param {string} repoPath - Path to repository
|
|
255
|
+
* @returns {boolean} True if there are uncommitted changes
|
|
256
|
+
*/
|
|
257
|
+
function hasUncommittedChanges(repoPath) {
|
|
258
|
+
try {
|
|
259
|
+
// Check for staged and unstaged changes
|
|
260
|
+
const status = execSync('git status --porcelain', {
|
|
261
|
+
cwd: repoPath,
|
|
262
|
+
encoding: 'utf8'
|
|
263
|
+
}).trim();
|
|
264
|
+
return status.length > 0;
|
|
265
|
+
} catch (error) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
module.exports = {
|
|
271
|
+
isGitRepo,
|
|
272
|
+
getCurrentBranch,
|
|
273
|
+
hasUncommittedChanges,
|
|
274
|
+
createRequirementBranch,
|
|
275
|
+
mergeRequirementBranch,
|
|
276
|
+
removeRequirementFeature,
|
|
277
|
+
listRequirementBranches
|
|
278
|
+
};
|