s9n-devops-agent 2.0.9 → 2.0.11-dev.0
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/bin/cs-devops-agent +28 -0
- package/package.json +4 -3
- package/scripts/contract-automation/README.md +515 -0
- package/scripts/contract-automation/analyze-with-llm.js +497 -0
- package/scripts/contract-automation/check-compliance.js +567 -0
- package/scripts/contract-automation/generate-contracts.js +592 -0
- package/scripts/contract-automation/validate-commit.js +422 -0
- package/src/credentials-manager.js +108 -0
- package/src/session-coordinator.js +20 -18
- package/src/setup-cs-devops-agent.js +175 -3
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* COMMIT MESSAGE VALIDATOR WITH CONTRACT FLAGS
|
|
6
|
+
* ============================================================================
|
|
7
|
+
*
|
|
8
|
+
* This script validates commit messages and checks if contract files were
|
|
9
|
+
* updated when the commit claims to modify contracts.
|
|
10
|
+
*
|
|
11
|
+
* New Commit Format:
|
|
12
|
+
*
|
|
13
|
+
* ```
|
|
14
|
+
* feat(api): add user profile endpoint
|
|
15
|
+
*
|
|
16
|
+
* Contracts: [SQL:T, API:T, DB:F, 3RD:F, FEAT:T, INFRA:F]
|
|
17
|
+
*
|
|
18
|
+
* [WHY section...]
|
|
19
|
+
* [WHAT section...]
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Contract Flags:
|
|
23
|
+
* SQL:T/F - SQL_CONTRACT.json modified
|
|
24
|
+
* API:T/F - API_CONTRACT.md modified
|
|
25
|
+
* DB:T/F - DATABASE_SCHEMA_CONTRACT.md modified
|
|
26
|
+
* 3RD:T/F - THIRD_PARTY_INTEGRATIONS.md modified
|
|
27
|
+
* FEAT:T/F - FEATURES_CONTRACT.md modified
|
|
28
|
+
* INFRA:T/F - INFRA_CONTRACT.md modified
|
|
29
|
+
*
|
|
30
|
+
* Usage:
|
|
31
|
+
* node scripts/contract-automation/validate-commit.js
|
|
32
|
+
* node scripts/contract-automation/validate-commit.js --commit-msg=.claude-commit-msg
|
|
33
|
+
* node scripts/contract-automation/validate-commit.js --check-staged
|
|
34
|
+
*
|
|
35
|
+
* Options:
|
|
36
|
+
* --commit-msg=<path> Path to commit message file (default: .claude-commit-msg)
|
|
37
|
+
* --check-staged Check staged files in git
|
|
38
|
+
* --strict Fail on warnings
|
|
39
|
+
* --auto-fix Suggest correct contract flags
|
|
40
|
+
*
|
|
41
|
+
* ============================================================================
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import fs from 'fs';
|
|
45
|
+
import path from 'path';
|
|
46
|
+
import { execSync } from 'child_process';
|
|
47
|
+
import { fileURLToPath } from 'url';
|
|
48
|
+
|
|
49
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
50
|
+
const __dirname = path.dirname(__filename);
|
|
51
|
+
|
|
52
|
+
// Configuration
|
|
53
|
+
const CONFIG = {
|
|
54
|
+
rootDir: process.cwd(),
|
|
55
|
+
commitMsgFile: getArgValue('--commit-msg') || '.claude-commit-msg',
|
|
56
|
+
checkStaged: process.argv.includes('--check-staged'),
|
|
57
|
+
strict: process.argv.includes('--strict'),
|
|
58
|
+
autoFix: process.argv.includes('--auto-fix'),
|
|
59
|
+
contractsDir: 'House_Rules_Contracts'
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Contract file mapping
|
|
63
|
+
const CONTRACT_FILES = {
|
|
64
|
+
SQL: 'SQL_CONTRACT.json',
|
|
65
|
+
API: 'API_CONTRACT.md',
|
|
66
|
+
DB: 'DATABASE_SCHEMA_CONTRACT.md',
|
|
67
|
+
'3RD': 'THIRD_PARTY_INTEGRATIONS.md',
|
|
68
|
+
FEAT: 'FEATURES_CONTRACT.md',
|
|
69
|
+
INFRA: 'INFRA_CONTRACT.md'
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Helper: Get command line argument value
|
|
73
|
+
function getArgValue(argName) {
|
|
74
|
+
const arg = process.argv.find(a => a.startsWith(argName + '='));
|
|
75
|
+
return arg ? arg.split('=')[1] : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Helper: Log with colors
|
|
79
|
+
function log(message, level = 'info') {
|
|
80
|
+
const colors = {
|
|
81
|
+
info: '\x1b[36m', // Cyan
|
|
82
|
+
warn: '\x1b[33m', // Yellow
|
|
83
|
+
error: '\x1b[31m', // Red
|
|
84
|
+
success: '\x1b[32m', // Green
|
|
85
|
+
reset: '\x1b[0m'
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const prefix = {
|
|
89
|
+
info: '[INFO]',
|
|
90
|
+
warn: '[WARN]',
|
|
91
|
+
error: '[ERROR]',
|
|
92
|
+
success: '[SUCCESS]'
|
|
93
|
+
}[level];
|
|
94
|
+
|
|
95
|
+
const color = colors[level] || colors.reset;
|
|
96
|
+
console.log(`${color}${prefix} ${message}${colors.reset}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Helper: Read file safely
|
|
100
|
+
function readFileSafe(filePath) {
|
|
101
|
+
try {
|
|
102
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return '';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ============================================================================
|
|
109
|
+
// COMMIT MESSAGE PARSING
|
|
110
|
+
// ============================================================================
|
|
111
|
+
|
|
112
|
+
function parseCommitMessage(content) {
|
|
113
|
+
const lines = content.split('\n');
|
|
114
|
+
|
|
115
|
+
// Extract subject line
|
|
116
|
+
const subject = lines[0] || '';
|
|
117
|
+
|
|
118
|
+
// Extract contract flags
|
|
119
|
+
const contractLine = lines.find(l => l.trim().startsWith('Contracts:'));
|
|
120
|
+
let contractFlags = {};
|
|
121
|
+
|
|
122
|
+
if (contractLine) {
|
|
123
|
+
const flagsMatch = contractLine.match(/\[(.*?)\]/);
|
|
124
|
+
if (flagsMatch) {
|
|
125
|
+
const flagsStr = flagsMatch[1];
|
|
126
|
+
const flags = flagsStr.split(',').map(f => f.trim());
|
|
127
|
+
|
|
128
|
+
for (const flag of flags) {
|
|
129
|
+
const [key, value] = flag.split(':').map(s => s.trim());
|
|
130
|
+
contractFlags[key] = value === 'T' || value === 'true';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Extract WHY and WHAT sections
|
|
136
|
+
const whyIndex = content.indexOf('[WHY');
|
|
137
|
+
const whatIndex = content.indexOf('[WHAT');
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
subject,
|
|
141
|
+
contractFlags,
|
|
142
|
+
hasContractLine: !!contractLine,
|
|
143
|
+
hasWhy: whyIndex !== -1,
|
|
144
|
+
hasWhat: whatIndex !== -1,
|
|
145
|
+
raw: content
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ============================================================================
|
|
150
|
+
// GIT FILE CHECKING
|
|
151
|
+
// ============================================================================
|
|
152
|
+
|
|
153
|
+
function getStagedFiles() {
|
|
154
|
+
try {
|
|
155
|
+
const result = execSync('git diff --cached --name-only', { encoding: 'utf8' });
|
|
156
|
+
return result.trim().split('\n').filter(Boolean);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
log('Failed to get staged files. Not in a git repository?', 'warn');
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getModifiedContractFiles(stagedFiles) {
|
|
164
|
+
const modified = {};
|
|
165
|
+
|
|
166
|
+
for (const [key, filename] of Object.entries(CONTRACT_FILES)) {
|
|
167
|
+
const filePath = path.join(CONFIG.contractsDir, filename);
|
|
168
|
+
modified[key] = stagedFiles.includes(filePath);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return modified;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ============================================================================
|
|
175
|
+
// VALIDATION
|
|
176
|
+
// ============================================================================
|
|
177
|
+
|
|
178
|
+
function validateCommitMessage(parsed) {
|
|
179
|
+
const issues = [];
|
|
180
|
+
const warnings = [];
|
|
181
|
+
|
|
182
|
+
// Check subject line format
|
|
183
|
+
if (!parsed.subject.match(/^(feat|fix|refactor|docs|test|chore|style)\(/)) {
|
|
184
|
+
issues.push('Subject line must start with type(scope): (feat|fix|refactor|docs|test|chore|style)');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Check for contract flags line
|
|
188
|
+
if (!parsed.hasContractLine) {
|
|
189
|
+
warnings.push('Missing "Contracts:" line. Add contract flags to enable automatic validation.');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Check for WHY section
|
|
193
|
+
if (!parsed.hasWhy) {
|
|
194
|
+
warnings.push('Missing [WHY] section explaining motivation for changes.');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check for WHAT section
|
|
198
|
+
if (!parsed.hasWhat) {
|
|
199
|
+
warnings.push('Missing [WHAT] section listing specific file changes.');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return { issues, warnings };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function validateContractFlags(claimedFlags, actualModified) {
|
|
206
|
+
const mismatches = [];
|
|
207
|
+
const suggestions = [];
|
|
208
|
+
|
|
209
|
+
// Check each contract type
|
|
210
|
+
for (const [key, filename] of Object.entries(CONTRACT_FILES)) {
|
|
211
|
+
const claimed = claimedFlags[key] || false;
|
|
212
|
+
const actual = actualModified[key] || false;
|
|
213
|
+
|
|
214
|
+
if (claimed && !actual) {
|
|
215
|
+
mismatches.push({
|
|
216
|
+
type: 'false_positive',
|
|
217
|
+
contract: key,
|
|
218
|
+
message: `Commit claims ${key}:T but ${filename} was NOT modified`
|
|
219
|
+
});
|
|
220
|
+
suggestions.push(`Change ${key}:T to ${key}:F`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!claimed && actual) {
|
|
224
|
+
mismatches.push({
|
|
225
|
+
type: 'false_negative',
|
|
226
|
+
contract: key,
|
|
227
|
+
message: `${filename} was modified but commit claims ${key}:F or missing`
|
|
228
|
+
});
|
|
229
|
+
suggestions.push(`Change ${key}:F to ${key}:T (or add if missing)`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return { mismatches, suggestions };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ============================================================================
|
|
237
|
+
// REPORTING
|
|
238
|
+
// ============================================================================
|
|
239
|
+
|
|
240
|
+
function generateReport(parsed, validation, contractValidation, stagedFiles) {
|
|
241
|
+
log('='.repeat(80));
|
|
242
|
+
log('COMMIT MESSAGE VALIDATION REPORT');
|
|
243
|
+
log('='.repeat(80));
|
|
244
|
+
|
|
245
|
+
// Subject line
|
|
246
|
+
log(`Subject: ${parsed.subject}`);
|
|
247
|
+
log('');
|
|
248
|
+
|
|
249
|
+
// Contract flags
|
|
250
|
+
if (parsed.hasContractLine) {
|
|
251
|
+
log('Contract Flags:');
|
|
252
|
+
for (const [key, value] of Object.entries(parsed.contractFlags)) {
|
|
253
|
+
const status = value ? '✅ TRUE' : '❌ FALSE';
|
|
254
|
+
log(` ${key}: ${status}`);
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
log('Contract Flags: ⚠️ NOT SPECIFIED', 'warn');
|
|
258
|
+
}
|
|
259
|
+
log('');
|
|
260
|
+
|
|
261
|
+
// Validation issues
|
|
262
|
+
if (validation.issues.length > 0) {
|
|
263
|
+
log('ERRORS:', 'error');
|
|
264
|
+
validation.issues.forEach(issue => log(` ❌ ${issue}`, 'error'));
|
|
265
|
+
log('');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (validation.warnings.length > 0) {
|
|
269
|
+
log('WARNINGS:', 'warn');
|
|
270
|
+
validation.warnings.forEach(warning => log(` ⚠️ ${warning}`, 'warn'));
|
|
271
|
+
log('');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Contract flag validation
|
|
275
|
+
if (contractValidation.mismatches.length > 0) {
|
|
276
|
+
log('CONTRACT FLAG MISMATCHES:', 'error');
|
|
277
|
+
contractValidation.mismatches.forEach(mismatch => {
|
|
278
|
+
log(` ❌ ${mismatch.message}`, 'error');
|
|
279
|
+
});
|
|
280
|
+
log('');
|
|
281
|
+
|
|
282
|
+
if (CONFIG.autoFix && contractValidation.suggestions.length > 0) {
|
|
283
|
+
log('SUGGESTED FIXES:', 'info');
|
|
284
|
+
contractValidation.suggestions.forEach(suggestion => {
|
|
285
|
+
log(` 💡 ${suggestion}`, 'info');
|
|
286
|
+
});
|
|
287
|
+
log('');
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Staged files
|
|
292
|
+
if (CONFIG.checkStaged) {
|
|
293
|
+
log('Staged Contract Files:');
|
|
294
|
+
let anyStaged = false;
|
|
295
|
+
for (const [key, filename] of Object.entries(CONTRACT_FILES)) {
|
|
296
|
+
const filePath = path.join(CONFIG.contractsDir, filename);
|
|
297
|
+
if (stagedFiles.includes(filePath)) {
|
|
298
|
+
log(` ✅ ${filename}`, 'success');
|
|
299
|
+
anyStaged = true;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (!anyStaged) {
|
|
303
|
+
log(' (none)', 'info');
|
|
304
|
+
}
|
|
305
|
+
log('');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Final result
|
|
309
|
+
log('='.repeat(80));
|
|
310
|
+
|
|
311
|
+
const hasErrors = validation.issues.length > 0 || contractValidation.mismatches.length > 0;
|
|
312
|
+
const hasWarnings = validation.warnings.length > 0;
|
|
313
|
+
|
|
314
|
+
if (hasErrors) {
|
|
315
|
+
log('VALIDATION FAILED ❌', 'error');
|
|
316
|
+
log('Please fix the errors above before committing.', 'error');
|
|
317
|
+
return false;
|
|
318
|
+
} else if (hasWarnings && CONFIG.strict) {
|
|
319
|
+
log('VALIDATION FAILED ⚠️ (strict mode)', 'warn');
|
|
320
|
+
log('Fix warnings or remove --strict flag.', 'warn');
|
|
321
|
+
return false;
|
|
322
|
+
} else if (hasWarnings) {
|
|
323
|
+
log('VALIDATION PASSED WITH WARNINGS ⚠️', 'warn');
|
|
324
|
+
log('Consider addressing warnings for better documentation.', 'warn');
|
|
325
|
+
return true;
|
|
326
|
+
} else {
|
|
327
|
+
log('VALIDATION PASSED ✅', 'success');
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ============================================================================
|
|
333
|
+
// AUTO-FIX
|
|
334
|
+
// ============================================================================
|
|
335
|
+
|
|
336
|
+
function generateCorrectedCommitMessage(parsed, actualModified) {
|
|
337
|
+
const lines = parsed.raw.split('\n');
|
|
338
|
+
|
|
339
|
+
// Generate correct contract flags
|
|
340
|
+
const flags = [];
|
|
341
|
+
for (const [key, _] of Object.entries(CONTRACT_FILES)) {
|
|
342
|
+
const value = actualModified[key] ? 'T' : 'F';
|
|
343
|
+
flags.push(`${key}:${value}`);
|
|
344
|
+
}
|
|
345
|
+
const contractLine = `Contracts: [${flags.join(', ')}]`;
|
|
346
|
+
|
|
347
|
+
// Find and replace contract line, or insert after subject
|
|
348
|
+
const contractLineIndex = lines.findIndex(l => l.trim().startsWith('Contracts:'));
|
|
349
|
+
|
|
350
|
+
if (contractLineIndex !== -1) {
|
|
351
|
+
lines[contractLineIndex] = contractLine;
|
|
352
|
+
} else {
|
|
353
|
+
// Insert after subject line (index 0) and blank line
|
|
354
|
+
lines.splice(2, 0, contractLine);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return lines.join('\n');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ============================================================================
|
|
361
|
+
// MAIN EXECUTION
|
|
362
|
+
// ============================================================================
|
|
363
|
+
|
|
364
|
+
function main() {
|
|
365
|
+
log('='.repeat(80));
|
|
366
|
+
log('COMMIT MESSAGE VALIDATOR');
|
|
367
|
+
log('='.repeat(80));
|
|
368
|
+
log('');
|
|
369
|
+
|
|
370
|
+
// Read commit message
|
|
371
|
+
const commitMsgPath = path.join(CONFIG.rootDir, CONFIG.commitMsgFile);
|
|
372
|
+
if (!fs.existsSync(commitMsgPath)) {
|
|
373
|
+
log(`Commit message file not found: ${commitMsgPath}`, 'error');
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const commitMsg = readFileSafe(commitMsgPath);
|
|
378
|
+
if (!commitMsg) {
|
|
379
|
+
log('Commit message is empty', 'error');
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Parse commit message
|
|
384
|
+
const parsed = parseCommitMessage(commitMsg);
|
|
385
|
+
|
|
386
|
+
// Validate commit message format
|
|
387
|
+
const validation = validateCommitMessage(parsed);
|
|
388
|
+
|
|
389
|
+
// Get staged files and check contract modifications
|
|
390
|
+
const stagedFiles = CONFIG.checkStaged ? getStagedFiles() : [];
|
|
391
|
+
const actualModified = CONFIG.checkStaged ? getModifiedContractFiles(stagedFiles) : {};
|
|
392
|
+
|
|
393
|
+
// Validate contract flags against actual changes
|
|
394
|
+
const contractValidation = CONFIG.checkStaged
|
|
395
|
+
? validateContractFlags(parsed.contractFlags, actualModified)
|
|
396
|
+
: { mismatches: [], suggestions: [] };
|
|
397
|
+
|
|
398
|
+
// Generate report
|
|
399
|
+
const passed = generateReport(parsed, validation, contractValidation, stagedFiles);
|
|
400
|
+
|
|
401
|
+
// Auto-fix if requested
|
|
402
|
+
if (CONFIG.autoFix && contractValidation.mismatches.length > 0) {
|
|
403
|
+
log('='.repeat(80));
|
|
404
|
+
log('AUTO-FIX ENABLED', 'info');
|
|
405
|
+
log('='.repeat(80));
|
|
406
|
+
|
|
407
|
+
const corrected = generateCorrectedCommitMessage(parsed, actualModified);
|
|
408
|
+
const correctedPath = commitMsgPath + '.corrected';
|
|
409
|
+
|
|
410
|
+
fs.writeFileSync(correctedPath, corrected, 'utf8');
|
|
411
|
+
log(`Corrected commit message saved to: ${correctedPath}`, 'success');
|
|
412
|
+
log('Review and replace original if correct.', 'info');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
log('='.repeat(80));
|
|
416
|
+
|
|
417
|
+
// Exit with appropriate code
|
|
418
|
+
process.exit(passed ? 0 : 1);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Run
|
|
422
|
+
main();
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { dirname } from 'path';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = dirname(__filename);
|
|
8
|
+
const rootDir = path.join(__dirname, '..');
|
|
9
|
+
|
|
10
|
+
const CREDENTIALS_PATH = path.join(rootDir, 'local_deploy', 'credentials.json');
|
|
11
|
+
|
|
12
|
+
// Simple obfuscation to prevent casual shoulder surfing
|
|
13
|
+
// NOTE: This is NOT strong encryption. In a production environment with sensitive keys,
|
|
14
|
+
// one should rely on system keychains or proper secret management services.
|
|
15
|
+
// Since this is a local dev tool, this prevents accidental plain text commits/reads.
|
|
16
|
+
const obfuscate = (text) => Buffer.from(text).toString('base64');
|
|
17
|
+
const deobfuscate = (text) => Buffer.from(text, 'base64').toString('utf8');
|
|
18
|
+
|
|
19
|
+
export class CredentialsManager {
|
|
20
|
+
constructor() {
|
|
21
|
+
this.credentials = {};
|
|
22
|
+
this.load();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
load() {
|
|
26
|
+
if (fs.existsSync(CREDENTIALS_PATH)) {
|
|
27
|
+
try {
|
|
28
|
+
const rawData = fs.readFileSync(CREDENTIALS_PATH, 'utf8');
|
|
29
|
+
const data = JSON.parse(rawData);
|
|
30
|
+
|
|
31
|
+
// Deobfuscate sensitive values
|
|
32
|
+
if (data.groqApiKey) {
|
|
33
|
+
data.groqApiKey = deobfuscate(data.groqApiKey);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.credentials = data;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Failed to load credentials:', error.message);
|
|
39
|
+
this.credentials = {};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
save() {
|
|
45
|
+
try {
|
|
46
|
+
// Ensure local_deploy exists
|
|
47
|
+
const localDeployDir = path.dirname(CREDENTIALS_PATH);
|
|
48
|
+
if (!fs.existsSync(localDeployDir)) {
|
|
49
|
+
fs.mkdirSync(localDeployDir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Clone and obfuscate
|
|
53
|
+
const dataToSave = { ...this.credentials };
|
|
54
|
+
if (dataToSave.groqApiKey) {
|
|
55
|
+
dataToSave.groqApiKey = obfuscate(dataToSave.groqApiKey);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fs.writeFileSync(CREDENTIALS_PATH, JSON.stringify(dataToSave, null, 2));
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Failed to save credentials:', error.message);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
setGroqApiKey(key) {
|
|
65
|
+
if (!key) return;
|
|
66
|
+
this.credentials.groqApiKey = key;
|
|
67
|
+
this.credentials.updatedAt = new Date().toISOString();
|
|
68
|
+
this.save();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getGroqApiKey() {
|
|
72
|
+
return this.credentials.groqApiKey || null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
hasGroqApiKey() {
|
|
76
|
+
return !!this.credentials.groqApiKey;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
clearAll() {
|
|
80
|
+
this.credentials = {};
|
|
81
|
+
if (fs.existsSync(CREDENTIALS_PATH)) {
|
|
82
|
+
fs.unlinkSync(CREDENTIALS_PATH);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Injects the Groq API Key into process.env
|
|
88
|
+
* Sets both OPENAI_API_KEY (legacy/compat) and GROQ_API_KEY (native)
|
|
89
|
+
* @returns {boolean} true if key was injected or already existed
|
|
90
|
+
*/
|
|
91
|
+
injectEnv() {
|
|
92
|
+
const key = this.getGroqApiKey();
|
|
93
|
+
if (!key && !process.env.OPENAI_API_KEY && !process.env.GROQ_API_KEY) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (key) {
|
|
98
|
+
if (!process.env.OPENAI_API_KEY) process.env.OPENAI_API_KEY = key;
|
|
99
|
+
if (!process.env.GROQ_API_KEY) process.env.GROQ_API_KEY = key;
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return true; // Env vars existed
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Singleton instance
|
|
108
|
+
export const credentialsManager = new CredentialsManager();
|
|
@@ -21,9 +21,13 @@ import fs from 'fs';
|
|
|
21
21
|
import path from 'path';
|
|
22
22
|
import { fileURLToPath } from 'url';
|
|
23
23
|
import { dirname } from 'path';
|
|
24
|
-
import {
|
|
25
|
-
import
|
|
26
|
-
|
|
24
|
+
import { spawn, execSync, exec } from 'child_process';
|
|
25
|
+
import { credentialsManager } from './credentials-manager.js';
|
|
26
|
+
|
|
27
|
+
// Inject credentials immediately
|
|
28
|
+
credentialsManager.injectEnv();
|
|
29
|
+
|
|
30
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
27
31
|
import { hasDockerConfiguration } from './docker-utils.js';
|
|
28
32
|
import HouseRulesManager from './house-rules-manager.js';
|
|
29
33
|
|
|
@@ -1276,21 +1280,19 @@ The DevOps agent will automatically:
|
|
|
1276
1280
|
console.log(`Please switch to this directory before making any changes:`);
|
|
1277
1281
|
console.log(`cd "${instructions.worktreePath}"`);
|
|
1278
1282
|
console.log(``);
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
console.log(``);
|
|
1293
|
-
}
|
|
1283
|
+
console.log(`📋 IMPORTANT - READ PROJECT RULES FIRST:`);
|
|
1284
|
+
console.log(`Before making ANY changes, you MUST read the project's house rules at:`);
|
|
1285
|
+
console.log(`${houseRulesPath}`);
|
|
1286
|
+
console.log(``);
|
|
1287
|
+
console.log(`The house rules file contains:`);
|
|
1288
|
+
console.log(`- Project coding conventions and standards`);
|
|
1289
|
+
console.log(`- Required commit message formats`);
|
|
1290
|
+
console.log(`- File coordination protocols`);
|
|
1291
|
+
console.log(`- Branch naming and workflow rules`);
|
|
1292
|
+
console.log(`- Testing and review requirements`);
|
|
1293
|
+
console.log(``);
|
|
1294
|
+
console.log(`You must follow ALL rules in this file. Read it carefully before proceeding.`);
|
|
1295
|
+
console.log(``);
|
|
1294
1296
|
|
|
1295
1297
|
console.log(`⚠️ FILE COORDINATION (MANDATORY):`);
|
|
1296
1298
|
console.log(`Shared coordination directory: local_deploy/.file-coordination/`);
|