s9n-devops-agent 2.0.18-dev.2 → 2.0.18-dev.3
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/docs/RELEASE_NOTES.md +15 -0
- package/package.json +1 -1
- package/src/credentials-manager.js +28 -6
- package/src/session-coordinator.js +123 -22
- package/src/setup-cs-devops-agent.js +181 -47
package/docs/RELEASE_NOTES.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
# Release Notes - s9n-devops-agent v2.0.18-dev.3
|
|
2
|
+
|
|
3
|
+
## 🚀 Enhancements
|
|
4
|
+
- **Base Branch Selection**: You can now select a base branch (e.g., main, develop) when starting a session, allowing for cleaner feature branching from stable points.
|
|
5
|
+
- **Enhanced Setup Wizard**:
|
|
6
|
+
- Finds and merges contract files from subdirectories.
|
|
7
|
+
- Ensures versioning strategy is configured.
|
|
8
|
+
- Persists credentials in user home directory to survive package updates.
|
|
9
|
+
|
|
10
|
+
## 🐛 Fixes
|
|
11
|
+
- **Update Logic**: Fixed update checker to respect dev versions.
|
|
12
|
+
- **Credentials**: Fixed issue where API keys were lost during updates.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
1
16
|
# Release Notes - s9n-devops-agent v2.0.11-dev.0
|
|
2
17
|
|
|
3
18
|
## 🐛 Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "s9n-devops-agent",
|
|
3
|
-
"version": "2.0.18-dev.
|
|
3
|
+
"version": "2.0.18-dev.3",
|
|
4
4
|
"description": "CS_DevOpsAgent - Intelligent Git Automation System with multi-agent support and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/cs-devops-agent-worker.js",
|
|
@@ -3,11 +3,15 @@ import path from 'path';
|
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { dirname } from 'path';
|
|
5
5
|
|
|
6
|
+
import os from 'os';
|
|
7
|
+
|
|
6
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
9
|
const __dirname = dirname(__filename);
|
|
8
|
-
const rootDir = path.join(__dirname, '..');
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
// Use home directory for persistent storage across package updates
|
|
12
|
+
const HOME_DIR = os.homedir();
|
|
13
|
+
const CONFIG_DIR = path.join(HOME_DIR, '.devops-agent');
|
|
14
|
+
const CREDENTIALS_PATH = process.env.DEVOPS_CREDENTIALS_PATH || path.join(CONFIG_DIR, 'credentials.json');
|
|
11
15
|
|
|
12
16
|
// Simple obfuscation to prevent casual shoulder surfing
|
|
13
17
|
// NOTE: This is NOT strong encryption. In a production environment with sensitive keys,
|
|
@@ -38,15 +42,33 @@ export class CredentialsManager {
|
|
|
38
42
|
console.error('Failed to load credentials:', error.message);
|
|
39
43
|
this.credentials = {};
|
|
40
44
|
}
|
|
45
|
+
} else {
|
|
46
|
+
// Migration: Check for old local_deploy location
|
|
47
|
+
const oldPath = path.join(__dirname, '..', 'local_deploy', 'credentials.json');
|
|
48
|
+
if (fs.existsSync(oldPath)) {
|
|
49
|
+
try {
|
|
50
|
+
const rawData = fs.readFileSync(oldPath, 'utf8');
|
|
51
|
+
const data = JSON.parse(rawData);
|
|
52
|
+
// Deobfuscate sensitive values
|
|
53
|
+
if (data.groqApiKey) {
|
|
54
|
+
data.groqApiKey = deobfuscate(data.groqApiKey);
|
|
55
|
+
}
|
|
56
|
+
this.credentials = data;
|
|
57
|
+
// Save to new location immediately
|
|
58
|
+
this.save();
|
|
59
|
+
} catch (e) {
|
|
60
|
+
// Ignore migration errors
|
|
61
|
+
}
|
|
62
|
+
}
|
|
41
63
|
}
|
|
42
64
|
}
|
|
43
65
|
|
|
44
66
|
save() {
|
|
45
67
|
try {
|
|
46
|
-
// Ensure
|
|
47
|
-
const
|
|
48
|
-
if (!fs.existsSync(
|
|
49
|
-
fs.mkdirSync(
|
|
68
|
+
// Ensure config dir exists
|
|
69
|
+
const configDir = path.dirname(CREDENTIALS_PATH);
|
|
70
|
+
if (!fs.existsSync(configDir)) {
|
|
71
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
50
72
|
}
|
|
51
73
|
|
|
52
74
|
// Clone and obfuscate
|
|
@@ -165,22 +165,42 @@ export class SessionCoordinator {
|
|
|
165
165
|
// Show checking message
|
|
166
166
|
console.log(`${CONFIG.colors.dim}🔍 Checking for DevOps Agent updates...${CONFIG.colors.reset}`);
|
|
167
167
|
|
|
168
|
-
// Check npm for
|
|
169
|
-
const
|
|
168
|
+
// Check npm for dist-tags
|
|
169
|
+
const distTags = JSON.parse(execSync('npm view s9n-devops-agent dist-tags --json', {
|
|
170
170
|
encoding: 'utf8',
|
|
171
171
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
172
172
|
timeout: 5000
|
|
173
|
-
}).trim();
|
|
173
|
+
}).trim());
|
|
174
|
+
|
|
175
|
+
const latest = distTags.latest;
|
|
176
|
+
const dev = distTags.dev;
|
|
174
177
|
|
|
175
178
|
// Update last check time
|
|
176
179
|
globalSettings.lastUpdateCheck = now;
|
|
177
180
|
this.saveGlobalSettings(globalSettings);
|
|
178
181
|
|
|
179
|
-
//
|
|
180
|
-
|
|
182
|
+
// Determine which version to compare against
|
|
183
|
+
// If current is a dev version, we check dev tag as well
|
|
184
|
+
const isDev = this.currentVersion.includes('dev') || this.currentVersion.includes('-');
|
|
185
|
+
|
|
186
|
+
let updateAvailable = false;
|
|
187
|
+
let targetVersion = latest;
|
|
188
|
+
let updateTag = 'latest';
|
|
189
|
+
|
|
190
|
+
if (isDev && dev && this.compareVersions(dev, this.currentVersion) > 0) {
|
|
191
|
+
updateAvailable = true;
|
|
192
|
+
targetVersion = dev;
|
|
193
|
+
updateTag = 'dev';
|
|
194
|
+
} else if (this.compareVersions(latest, this.currentVersion) > 0) {
|
|
195
|
+
updateAvailable = true;
|
|
196
|
+
targetVersion = latest;
|
|
197
|
+
updateTag = 'latest';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (updateAvailable) {
|
|
181
201
|
console.log(`\n${CONFIG.colors.yellow}▲ Update Available!${CONFIG.colors.reset}`);
|
|
182
202
|
console.log(`${CONFIG.colors.dim}Current version: ${this.currentVersion}${CONFIG.colors.reset}`);
|
|
183
|
-
console.log(`${CONFIG.colors.bright}
|
|
203
|
+
console.log(`${CONFIG.colors.bright}New version: ${targetVersion} (${updateTag})${CONFIG.colors.reset}`);
|
|
184
204
|
console.log();
|
|
185
205
|
|
|
186
206
|
// Ask if user wants to update now
|
|
@@ -199,7 +219,7 @@ export class SessionCoordinator {
|
|
|
199
219
|
if (updateNow) {
|
|
200
220
|
console.log(`\n${CONFIG.colors.blue}Updating s9n-devops-agent...${CONFIG.colors.reset}`);
|
|
201
221
|
try {
|
|
202
|
-
execSync(
|
|
222
|
+
execSync(`npm install -g s9n-devops-agent@${updateTag}`, {
|
|
203
223
|
stdio: 'inherit',
|
|
204
224
|
cwd: process.cwd()
|
|
205
225
|
});
|
|
@@ -207,10 +227,10 @@ export class SessionCoordinator {
|
|
|
207
227
|
process.exit(0);
|
|
208
228
|
} catch (err) {
|
|
209
229
|
console.log(`\n${CONFIG.colors.red}✗ Update failed: ${err.message}${CONFIG.colors.reset}`);
|
|
210
|
-
console.log(`${CONFIG.colors.dim}You can manually update with: npm install -g s9n-devops-agent
|
|
230
|
+
console.log(`${CONFIG.colors.dim}You can manually update with: npm install -g s9n-devops-agent@${updateTag}${CONFIG.colors.reset}`);
|
|
211
231
|
}
|
|
212
232
|
} else {
|
|
213
|
-
console.log(`${CONFIG.colors.dim}You can update later with: npm install -g s9n-devops-agent
|
|
233
|
+
console.log(`${CONFIG.colors.dim}You can update later with: npm install -g s9n-devops-agent@${updateTag}${CONFIG.colors.reset}`);
|
|
214
234
|
}
|
|
215
235
|
console.log();
|
|
216
236
|
} else {
|
|
@@ -224,19 +244,22 @@ export class SessionCoordinator {
|
|
|
224
244
|
}
|
|
225
245
|
|
|
226
246
|
/**
|
|
227
|
-
* Compare semantic versions
|
|
247
|
+
* Compare semantic versions (robust to suffixes)
|
|
228
248
|
* Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if equal
|
|
229
249
|
*/
|
|
230
250
|
compareVersions(v1, v2) {
|
|
231
|
-
|
|
232
|
-
const parts2 = v2.split('.').map(Number);
|
|
251
|
+
if (!v1 || !v2) return 0;
|
|
233
252
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
253
|
+
const normalize = v => v.replace(/^v/, '').split('.').map(p => parseInt(p, 10));
|
|
254
|
+
const p1 = normalize(v1);
|
|
255
|
+
const p2 = normalize(v2);
|
|
256
|
+
|
|
257
|
+
for (let i = 0; i < Math.max(p1.length, p2.length); i++) {
|
|
258
|
+
const n1 = isNaN(p1[i]) ? 0 : p1[i];
|
|
259
|
+
const n2 = isNaN(p2[i]) ? 0 : p2[i];
|
|
237
260
|
|
|
238
|
-
if (
|
|
239
|
-
if (
|
|
261
|
+
if (n1 > n2) return 1;
|
|
262
|
+
if (n1 < n2) return -1;
|
|
240
263
|
}
|
|
241
264
|
|
|
242
265
|
return 0;
|
|
@@ -427,13 +450,18 @@ export class SessionCoordinator {
|
|
|
427
450
|
/**
|
|
428
451
|
* Ensure project-specific version settings are configured
|
|
429
452
|
*/
|
|
430
|
-
async ensureProjectSetup() {
|
|
453
|
+
async ensureProjectSetup(options = {}) {
|
|
431
454
|
const projectSettings = this.loadProjectSettings();
|
|
432
455
|
|
|
433
456
|
// Check if project setup is needed (version strategy)
|
|
434
|
-
if (!projectSettings.versioningStrategy || !projectSettings.versioningStrategy.configured) {
|
|
435
|
-
console.log(`\n${CONFIG.colors.yellow}
|
|
436
|
-
|
|
457
|
+
if (options.force || !projectSettings.versioningStrategy || !projectSettings.versioningStrategy.configured) {
|
|
458
|
+
console.log(`\n${CONFIG.colors.yellow}Project Versioning Setup${CONFIG.colors.reset}`);
|
|
459
|
+
if (options.force) {
|
|
460
|
+
console.log(`${CONFIG.colors.dim}Reconfiguring version strategy...${CONFIG.colors.reset}`);
|
|
461
|
+
} else {
|
|
462
|
+
console.log(`${CONFIG.colors.yellow}First-time project setup for this repository!${CONFIG.colors.reset}`);
|
|
463
|
+
console.log(`${CONFIG.colors.dim}Let's configure the versioning strategy for this project${CONFIG.colors.reset}`);
|
|
464
|
+
}
|
|
437
465
|
|
|
438
466
|
const versionInfo = await this.promptForStartingVersion();
|
|
439
467
|
projectSettings.versioningStrategy = {
|
|
@@ -797,6 +825,73 @@ export class SessionCoordinator {
|
|
|
797
825
|
return config;
|
|
798
826
|
}
|
|
799
827
|
|
|
828
|
+
/**
|
|
829
|
+
* Prompt for base branch (source)
|
|
830
|
+
*/
|
|
831
|
+
async promptForBaseBranch() {
|
|
832
|
+
console.log(`\n${CONFIG.colors.yellow}═══ Base Branch Selection ═══${CONFIG.colors.reset}`);
|
|
833
|
+
console.log(`${CONFIG.colors.dim}Select the branch you want to start your work FROM.${CONFIG.colors.reset}`);
|
|
834
|
+
|
|
835
|
+
// Get available branches
|
|
836
|
+
const branches = this.getAvailableBranches();
|
|
837
|
+
// Prioritize main/develop/master
|
|
838
|
+
const priorityBranches = ['main', 'master', 'develop', 'development'];
|
|
839
|
+
|
|
840
|
+
const sortedBranches = branches.sort((a, b) => {
|
|
841
|
+
const aP = priorityBranches.indexOf(a);
|
|
842
|
+
const bP = priorityBranches.indexOf(b);
|
|
843
|
+
if (aP !== -1 && bP !== -1) return aP - bP;
|
|
844
|
+
if (aP !== -1) return -1;
|
|
845
|
+
if (bP !== -1) return 1;
|
|
846
|
+
return a.localeCompare(b);
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
const uniqueBranches = [...new Set(sortedBranches)].slice(0, 10);
|
|
850
|
+
|
|
851
|
+
console.log();
|
|
852
|
+
uniqueBranches.forEach((branch, index) => {
|
|
853
|
+
const isPriority = priorityBranches.includes(branch);
|
|
854
|
+
const marker = isPriority ? ` ${CONFIG.colors.green}⭐${CONFIG.colors.reset}` : '';
|
|
855
|
+
console.log(` ${index + 1}) ${branch}${marker}`);
|
|
856
|
+
});
|
|
857
|
+
console.log(` 0) Enter a different branch name`);
|
|
858
|
+
console.log(` Hit Enter for default (HEAD)`);
|
|
859
|
+
|
|
860
|
+
const rl = readline.createInterface({
|
|
861
|
+
input: process.stdin,
|
|
862
|
+
output: process.stdout
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
return new Promise((resolve) => {
|
|
866
|
+
rl.question(`\nSelect base branch (1-${uniqueBranches.length}, 0, or Enter): `, (answer) => {
|
|
867
|
+
rl.close();
|
|
868
|
+
const choice = answer.trim();
|
|
869
|
+
|
|
870
|
+
if (choice === '') {
|
|
871
|
+
resolve('HEAD');
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const num = parseInt(choice);
|
|
876
|
+
|
|
877
|
+
if (num === 0) {
|
|
878
|
+
const rl2 = readline.createInterface({
|
|
879
|
+
input: process.stdin,
|
|
880
|
+
output: process.stdout
|
|
881
|
+
});
|
|
882
|
+
rl2.question('Enter custom branch name: ', (custom) => {
|
|
883
|
+
rl2.close();
|
|
884
|
+
resolve(custom.trim() || 'HEAD');
|
|
885
|
+
});
|
|
886
|
+
} else if (num >= 1 && num <= uniqueBranches.length) {
|
|
887
|
+
resolve(uniqueBranches[num - 1]);
|
|
888
|
+
} else {
|
|
889
|
+
resolve('HEAD');
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
|
|
800
895
|
/**
|
|
801
896
|
* Prompt for auto-merge configuration
|
|
802
897
|
*/
|
|
@@ -1172,6 +1267,9 @@ export class SessionCoordinator {
|
|
|
1172
1267
|
// Ask for auto-merge configuration
|
|
1173
1268
|
const mergeConfig = await this.promptForMergeConfig();
|
|
1174
1269
|
|
|
1270
|
+
// Ask for base branch (where to start work from)
|
|
1271
|
+
const baseBranch = await this.promptForBaseBranch();
|
|
1272
|
+
|
|
1175
1273
|
// Check for Docker configuration and ask about restart preference
|
|
1176
1274
|
let dockerConfig = null;
|
|
1177
1275
|
|
|
@@ -1321,7 +1419,10 @@ export class SessionCoordinator {
|
|
|
1321
1419
|
|
|
1322
1420
|
// Create worktree
|
|
1323
1421
|
console.log(`\n${CONFIG.colors.yellow}Creating worktree...${CONFIG.colors.reset}`);
|
|
1324
|
-
|
|
1422
|
+
const baseRef = baseBranch || 'HEAD';
|
|
1423
|
+
console.log(`${CONFIG.colors.dim}Branching off: ${baseRef}${CONFIG.colors.reset}`);
|
|
1424
|
+
|
|
1425
|
+
execSync(`git worktree add -b ${branchName} "${worktreePath}" ${baseRef}`, { stdio: 'pipe' });
|
|
1325
1426
|
console.log(`${CONFIG.colors.green}✓${CONFIG.colors.reset} Worktree created at: ${worktreePath}`);
|
|
1326
1427
|
|
|
1327
1428
|
// If we're in a submodule, set up the correct remote for the worktree
|
|
@@ -28,6 +28,7 @@ import { execSync } from 'child_process';
|
|
|
28
28
|
import { fileURLToPath } from 'url';
|
|
29
29
|
import { dirname } from 'path';
|
|
30
30
|
import { credentialsManager } from './credentials-manager.js';
|
|
31
|
+
import { SessionCoordinator } from './session-coordinator.js';
|
|
31
32
|
import {
|
|
32
33
|
colors,
|
|
33
34
|
status,
|
|
@@ -149,25 +150,9 @@ This structure is compatible with the DevOps Agent's automation tools.
|
|
|
149
150
|
return missingFolders;
|
|
150
151
|
}
|
|
151
152
|
|
|
152
|
-
function checkContractsExist(projectRoot) {
|
|
153
|
-
// Search recursively for contract folders
|
|
153
|
+
async function checkContractsExist(projectRoot) {
|
|
154
|
+
// Search recursively for contract folders and files
|
|
154
155
|
try {
|
|
155
|
-
// Find all directories named 'House_Rules_Contracts' or 'contracts'
|
|
156
|
-
// Ignoring node_modules and .git
|
|
157
|
-
const findCommand = `find "${projectRoot}" -type d \\( -name "House_Rules_Contracts" -o -name "contracts" \\) -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/local_deploy/*"`;
|
|
158
|
-
|
|
159
|
-
const output = execSync(findCommand, { encoding: 'utf8' }).trim();
|
|
160
|
-
const locations = output.split('\n').filter(Boolean);
|
|
161
|
-
|
|
162
|
-
let contractsDir = null;
|
|
163
|
-
if (locations.length > 0) {
|
|
164
|
-
// Prefer House_Rules_Contracts if available
|
|
165
|
-
contractsDir = locations.find(l => l.endsWith('House_Rules_Contracts')) || locations[0];
|
|
166
|
-
log.info(`Found contracts directory at: ${contractsDir}`);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (!contractsDir) return false;
|
|
170
|
-
|
|
171
156
|
const requiredContracts = [
|
|
172
157
|
'FEATURES_CONTRACT.md',
|
|
173
158
|
'API_CONTRACT.md',
|
|
@@ -176,21 +161,122 @@ function checkContractsExist(projectRoot) {
|
|
|
176
161
|
'THIRD_PARTY_INTEGRATIONS.md',
|
|
177
162
|
'INFRA_CONTRACT.md'
|
|
178
163
|
];
|
|
164
|
+
|
|
165
|
+
// Map to hold found files for each type
|
|
166
|
+
const contractMap = {};
|
|
167
|
+
requiredContracts.forEach(c => contractMap[c] = []);
|
|
168
|
+
|
|
169
|
+
// Find all files that look like contracts
|
|
170
|
+
// We look for files containing "CONTRACT" in the name, excluding typical ignores
|
|
171
|
+
const findCommand = `find "${projectRoot}" -type f \\( -name "*CONTRACT*.md" -o -name "*CONTRACT*.json" \\) -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/local_deploy/*"`;
|
|
172
|
+
|
|
173
|
+
let files = [];
|
|
174
|
+
try {
|
|
175
|
+
const output = execSync(findCommand, { encoding: 'utf8' }).trim();
|
|
176
|
+
files = output.split('\n').filter(Boolean);
|
|
177
|
+
} catch (e) {
|
|
178
|
+
// find might fail if no matches or other issues, just treat as empty
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Categorize found files
|
|
182
|
+
for (const file of files) {
|
|
183
|
+
const basename = path.basename(file).toUpperCase();
|
|
184
|
+
|
|
185
|
+
// Skip files in the target directory itself (House_Rules_Contracts) to avoid self-merging if we run this multiple times
|
|
186
|
+
// actually we SHOULD include them to see if we have them, but valid if we are merging duplicates from elsewhere
|
|
187
|
+
|
|
188
|
+
let matched = false;
|
|
189
|
+
|
|
190
|
+
if (basename.includes('FEATURE')) contractMap['FEATURES_CONTRACT.md'].push(file);
|
|
191
|
+
else if (basename.includes('API')) contractMap['API_CONTRACT.md'].push(file);
|
|
192
|
+
else if (basename.includes('DATABASE') || basename.includes('SCHEMA')) contractMap['DATABASE_SCHEMA_CONTRACT.md'].push(file);
|
|
193
|
+
else if (basename.includes('SQL')) contractMap['SQL_CONTRACT.json'].push(file);
|
|
194
|
+
else if (basename.includes('INFRA')) contractMap['INFRA_CONTRACT.md'].push(file);
|
|
195
|
+
else if (basename.includes('THIRD') || basename.includes('INTEGRATION')) contractMap['THIRD_PARTY_INTEGRATIONS.md'].push(file);
|
|
196
|
+
else {
|
|
197
|
+
// Fallback or ignore
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const targetDir = path.join(projectRoot, 'House_Rules_Contracts');
|
|
202
|
+
let hasChanges = false;
|
|
203
|
+
|
|
204
|
+
// Process each contract type
|
|
205
|
+
for (const [type, foundFiles] of Object.entries(contractMap)) {
|
|
206
|
+
// Filter out unique paths (resolve them)
|
|
207
|
+
const uniqueFiles = [...new Set(foundFiles.map(f => path.resolve(f)))];
|
|
208
|
+
|
|
209
|
+
if (uniqueFiles.length > 1) {
|
|
210
|
+
console.log();
|
|
211
|
+
log.info(`Found multiple files for contract type: ${colors.cyan}${type}${colors.reset}`);
|
|
212
|
+
uniqueFiles.forEach(f => console.log(` - ${path.relative(projectRoot, f)}`));
|
|
213
|
+
|
|
214
|
+
const shouldMerge = await confirm(`Do you want to merge these into House_Rules_Contracts/${type}?`, true);
|
|
215
|
+
|
|
216
|
+
if (shouldMerge) {
|
|
217
|
+
ensureDirectoryExists(targetDir);
|
|
218
|
+
const targetPath = path.join(targetDir, type);
|
|
219
|
+
|
|
220
|
+
let mergedContent = '';
|
|
221
|
+
// Handle JSON vs MD
|
|
222
|
+
if (type.endsWith('.json')) {
|
|
223
|
+
// For JSON, we try to merge arrays/objects or just list them
|
|
224
|
+
const mergedJson = [];
|
|
225
|
+
for (const file of uniqueFiles) {
|
|
226
|
+
try {
|
|
227
|
+
const content = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
228
|
+
mergedJson.push({ source: path.relative(projectRoot, file), content });
|
|
229
|
+
} catch (e) {
|
|
230
|
+
log.warn(`Skipping invalid JSON in ${path.basename(file)}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
mergedContent = JSON.stringify(mergedJson, null, 2);
|
|
234
|
+
} else {
|
|
235
|
+
// Markdown
|
|
236
|
+
mergedContent = `# Merged ${type}\n\nGenerated on ${new Date().toISOString()}\n\n`;
|
|
237
|
+
for (const file of uniqueFiles) {
|
|
238
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
239
|
+
mergedContent += `\n<!-- SOURCE: ${path.relative(projectRoot, file)} -->\n`;
|
|
240
|
+
mergedContent += `## Source: ${path.basename(file)}\n(Path: ${path.relative(projectRoot, file)})\n\n`;
|
|
241
|
+
mergedContent += `${content}\n\n---\n`;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
fs.writeFileSync(targetPath, mergedContent);
|
|
246
|
+
log.success(`Merged contracts into ${path.relative(projectRoot, targetPath)}`);
|
|
247
|
+
hasChanges = true;
|
|
248
|
+
}
|
|
249
|
+
} else if (uniqueFiles.length === 1) {
|
|
250
|
+
// If single file exists but is NOT in House_Rules_Contracts, ask to move/copy
|
|
251
|
+
const file = uniqueFiles[0];
|
|
252
|
+
const targetPath = path.join(targetDir, type);
|
|
253
|
+
|
|
254
|
+
if (file !== path.resolve(targetPath)) {
|
|
255
|
+
console.log();
|
|
256
|
+
log.info(`Found ${type} at: ${path.relative(projectRoot, file)}`);
|
|
257
|
+
const shouldCopy = await confirm(`Copy this to central House_Rules_Contracts/${type}?`, true);
|
|
258
|
+
if (shouldCopy) {
|
|
259
|
+
ensureDirectoryExists(targetDir);
|
|
260
|
+
fs.copyFileSync(file, targetPath);
|
|
261
|
+
log.success(`Copied to ${path.relative(projectRoot, targetPath)}`);
|
|
262
|
+
hasChanges = true;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Final check: Do we have all required contracts in the target directory?
|
|
269
|
+
const missing = requiredContracts.filter(file => !fs.existsSync(path.join(targetDir, file)));
|
|
179
270
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
(f.includes('FEATURE') && f !== 'FEATURES_CONTRACT.md') ||
|
|
184
|
-
(f.includes('API') && f !== 'API_CONTRACT.md')
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
if (potentialDuplicates.length > 0) {
|
|
188
|
-
log.info(`Found potential split contract files in ${contractsDir}:`);
|
|
189
|
-
potentialDuplicates.forEach(f => console.log(` - ${f}`));
|
|
190
|
-
console.log('You may want to merge these into single contract files per type.');
|
|
271
|
+
if (missing.length === 0) {
|
|
272
|
+
if (hasChanges) log.success('Contract files consolidated successfully.');
|
|
273
|
+
return true;
|
|
191
274
|
}
|
|
192
275
|
|
|
193
|
-
|
|
276
|
+
// If we are missing some, but have others, we still return false so generateContracts can run for the missing ones?
|
|
277
|
+
// Or we return false and generateContracts will run.
|
|
278
|
+
return false;
|
|
279
|
+
|
|
194
280
|
} catch (error) {
|
|
195
281
|
log.warn(`Error searching for contracts: ${error.message}`);
|
|
196
282
|
return false;
|
|
@@ -866,30 +952,50 @@ async function setupEnvFile(projectRoot) {
|
|
|
866
952
|
log.info('Creating .env file');
|
|
867
953
|
}
|
|
868
954
|
|
|
869
|
-
// Check
|
|
955
|
+
// Check if OPENAI_API_KEY is already present in memory (from credentials.json)
|
|
956
|
+
const existingKey = credentialsManager.getGroqApiKey();
|
|
957
|
+
|
|
958
|
+
// Check for OPENAI_API_KEY in .env content
|
|
870
959
|
if (!envContent.includes('OPENAI_API_KEY=')) {
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
${colors.bright}Groq API Key Setup${colors.reset}
|
|
874
|
-
The contract automation features use Groq LLM (via OpenAI compatibility).
|
|
875
|
-
You can enter your API key now, or set it later in the .env file.
|
|
876
|
-
`);
|
|
877
|
-
|
|
878
|
-
const apiKey = await prompt('Enter Groq API Key (leave empty to skip)');
|
|
879
|
-
|
|
880
|
-
if (apiKey) {
|
|
960
|
+
if (existingKey) {
|
|
961
|
+
log.info('Found existing Groq API Key in credentials store.');
|
|
881
962
|
const newLine = envContent.endsWith('\n') || envContent === '' ? '' : '\n';
|
|
882
|
-
envContent += `${newLine}# Groq API Key for Contract Automation\nOPENAI_API_KEY=${
|
|
963
|
+
envContent += `${newLine}# Groq API Key for Contract Automation\nOPENAI_API_KEY=${existingKey}\n`;
|
|
883
964
|
fs.writeFileSync(envPath, envContent);
|
|
884
|
-
log.success('
|
|
965
|
+
log.success('Restored OPENAI_API_KEY to .env');
|
|
885
966
|
} else {
|
|
886
|
-
log
|
|
887
|
-
|
|
888
|
-
|
|
967
|
+
console.log();
|
|
968
|
+
explain(`
|
|
969
|
+
${colors.bright}Groq API Key Setup${colors.reset}
|
|
970
|
+
The contract automation features use Groq LLM (via OpenAI compatibility).
|
|
971
|
+
You can enter your API key now, or set it later in the .env file.
|
|
972
|
+
`);
|
|
973
|
+
|
|
974
|
+
const apiKey = await prompt('Enter Groq API Key (leave empty to skip)');
|
|
975
|
+
|
|
976
|
+
if (apiKey) {
|
|
977
|
+
const newLine = envContent.endsWith('\n') || envContent === '' ? '' : '\n';
|
|
978
|
+
envContent += `${newLine}# Groq API Key for Contract Automation\nOPENAI_API_KEY=${apiKey}\n`;
|
|
979
|
+
fs.writeFileSync(envPath, envContent);
|
|
980
|
+
|
|
981
|
+
// Also save to credentials manager for persistence across updates
|
|
982
|
+
credentialsManager.setGroqApiKey(apiKey);
|
|
983
|
+
|
|
984
|
+
log.success('Added OPENAI_API_KEY to .env');
|
|
985
|
+
} else {
|
|
986
|
+
log.warn('Skipped Groq API Key. Contract automation features may not work.');
|
|
987
|
+
if (!fs.existsSync(envPath)) {
|
|
988
|
+
fs.writeFileSync(envPath, '# Environment Variables\n');
|
|
989
|
+
}
|
|
889
990
|
}
|
|
890
991
|
}
|
|
891
992
|
} else {
|
|
892
993
|
log.info('OPENAI_API_KEY is already configured in .env');
|
|
994
|
+
// Ensure it's backed up in credentials manager if it exists in .env
|
|
995
|
+
const match = envContent.match(/OPENAI_API_KEY=(.+)/);
|
|
996
|
+
if (match && match[1] && !existingKey) {
|
|
997
|
+
credentialsManager.setGroqApiKey(match[1].trim());
|
|
998
|
+
}
|
|
893
999
|
}
|
|
894
1000
|
}
|
|
895
1001
|
|
|
@@ -1125,7 +1231,7 @@ ${colors.bright}Security:${colors.reset} Stored locally in ${colors.yellow}local
|
|
|
1125
1231
|
}
|
|
1126
1232
|
|
|
1127
1233
|
// Check for contracts
|
|
1128
|
-
if (!checkContractsExist(projectRoot)) {
|
|
1234
|
+
if (!(await checkContractsExist(projectRoot))) {
|
|
1129
1235
|
log.header();
|
|
1130
1236
|
log.title('📜 Contract Files Missing');
|
|
1131
1237
|
|
|
@@ -1162,6 +1268,34 @@ We can scan your codebase and generate them now.
|
|
|
1162
1268
|
log.success('Created .env file');
|
|
1163
1269
|
}
|
|
1164
1270
|
}
|
|
1271
|
+
|
|
1272
|
+
// Initialize SessionCoordinator for versioning setup
|
|
1273
|
+
const coordinator = new SessionCoordinator();
|
|
1274
|
+
|
|
1275
|
+
// Check/Setup versioning strategy
|
|
1276
|
+
if (!skipPrompts) {
|
|
1277
|
+
const settings = coordinator.loadProjectSettings();
|
|
1278
|
+
if (!settings.versioningStrategy?.configured) {
|
|
1279
|
+
log.header();
|
|
1280
|
+
log.title('📅 Project Versioning Strategy');
|
|
1281
|
+
await coordinator.ensureProjectSetup();
|
|
1282
|
+
} else {
|
|
1283
|
+
// Optional reconfigure
|
|
1284
|
+
log.info('Versioning strategy is already configured.');
|
|
1285
|
+
const reconfigure = await confirm('Do you want to reconfigure versioning?', false);
|
|
1286
|
+
if (reconfigure) {
|
|
1287
|
+
await coordinator.ensureProjectSetup({ force: true });
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
} else {
|
|
1291
|
+
// In non-interactive mode, we only ensure if missing (and hope it doesn't block or has defaults)
|
|
1292
|
+
// Actually promptForStartingVersion is interactive-only, so we skip if missing in non-interactive
|
|
1293
|
+
// or we could force defaults. For now, we skip to avoid hanging.
|
|
1294
|
+
const settings = coordinator.loadProjectSettings();
|
|
1295
|
+
if (!settings.versioningStrategy?.configured) {
|
|
1296
|
+
log.warn('Skipping versioning setup (interactive-only). Run setup without --yes to configure.');
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1165
1299
|
|
|
1166
1300
|
// Clean up DevOpsAgent files to avoid duplicates
|
|
1167
1301
|
cleanupDevOpsAgentFiles(projectRoot, agentName);
|