s9n-devops-agent 2.0.18-dev.1 → 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.
@@ -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.1",
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
- const CREDENTIALS_PATH = process.env.DEVOPS_CREDENTIALS_PATH || path.join(rootDir, 'local_deploy', 'credentials.json');
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 local_deploy exists
47
- const localDeployDir = path.dirname(CREDENTIALS_PATH);
48
- if (!fs.existsSync(localDeployDir)) {
49
- fs.mkdirSync(localDeployDir, { recursive: true });
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 latest version
169
- const result = execSync('npm view s9n-devops-agent version', {
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
- // Compare versions
180
- if (result && this.compareVersions(result, this.currentVersion) > 0) {
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}Latest version: ${result}${CONFIG.colors.reset}`);
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('npm install -g s9n-devops-agent@latest', {
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@latest${CONFIG.colors.reset}`);
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@latest${CONFIG.colors.reset}`);
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
- const parts1 = v1.split('.').map(Number);
232
- const parts2 = v2.split('.').map(Number);
251
+ if (!v1 || !v2) return 0;
233
252
 
234
- for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
235
- const p1 = parts1[i] || 0;
236
- const p2 = parts2[i] || 0;
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 (p1 > p2) return 1;
239
- if (p1 < p2) return -1;
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}First-time project setup for this repository!${CONFIG.colors.reset}`);
436
- console.log(`${CONFIG.colors.dim}Let's configure the versioning strategy for this project${CONFIG.colors.reset}`);
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
- execSync(`git worktree add -b ${branchName} "${worktreePath}" HEAD`, { stdio: 'pipe' });
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,20 +150,137 @@ This structure is compatible with the DevOps Agent's automation tools.
149
150
  return missingFolders;
150
151
  }
151
152
 
152
- function checkContractsExist(projectRoot) {
153
- const contractsDir = path.join(projectRoot, 'House_Rules_Contracts');
154
- if (!fs.existsSync(contractsDir)) return false;
155
-
156
- const requiredContracts = [
157
- 'FEATURES_CONTRACT.md',
158
- 'API_CONTRACT.md',
159
- 'DATABASE_SCHEMA_CONTRACT.md',
160
- 'SQL_CONTRACT.json',
161
- 'THIRD_PARTY_INTEGRATIONS.md',
162
- 'INFRA_CONTRACT.md'
163
- ];
164
-
165
- return requiredContracts.every(file => fs.existsSync(path.join(contractsDir, file)));
153
+ async function checkContractsExist(projectRoot) {
154
+ // Search recursively for contract folders and files
155
+ try {
156
+ const requiredContracts = [
157
+ 'FEATURES_CONTRACT.md',
158
+ 'API_CONTRACT.md',
159
+ 'DATABASE_SCHEMA_CONTRACT.md',
160
+ 'SQL_CONTRACT.json',
161
+ 'THIRD_PARTY_INTEGRATIONS.md',
162
+ 'INFRA_CONTRACT.md'
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)));
270
+
271
+ if (missing.length === 0) {
272
+ if (hasChanges) log.success('Contract files consolidated successfully.');
273
+ return true;
274
+ }
275
+
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
+
280
+ } catch (error) {
281
+ log.warn(`Error searching for contracts: ${error.message}`);
282
+ return false;
283
+ }
166
284
  }
167
285
 
168
286
  async function generateContracts(projectRoot) {
@@ -834,30 +952,50 @@ async function setupEnvFile(projectRoot) {
834
952
  log.info('Creating .env file');
835
953
  }
836
954
 
837
- // Check for OPENAI_API_KEY
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
838
959
  if (!envContent.includes('OPENAI_API_KEY=')) {
839
- console.log();
840
- explain(`
841
- ${colors.bright}Groq API Key Setup${colors.reset}
842
- The contract automation features use Groq LLM (via OpenAI compatibility).
843
- You can enter your API key now, or set it later in the .env file.
844
- `);
845
-
846
- const apiKey = await prompt('Enter Groq API Key (leave empty to skip)');
847
-
848
- if (apiKey) {
960
+ if (existingKey) {
961
+ log.info('Found existing Groq API Key in credentials store.');
849
962
  const newLine = envContent.endsWith('\n') || envContent === '' ? '' : '\n';
850
- envContent += `${newLine}# Groq API Key for Contract Automation\nOPENAI_API_KEY=${apiKey}\n`;
963
+ envContent += `${newLine}# Groq API Key for Contract Automation\nOPENAI_API_KEY=${existingKey}\n`;
851
964
  fs.writeFileSync(envPath, envContent);
852
- log.success('Added OPENAI_API_KEY to .env');
965
+ log.success('Restored OPENAI_API_KEY to .env');
853
966
  } else {
854
- log.warn('Skipped Groq API Key. Contract automation features may not work.');
855
- if (!fs.existsSync(envPath)) {
856
- fs.writeFileSync(envPath, '# Environment Variables\n');
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
+ }
857
990
  }
858
991
  }
859
992
  } else {
860
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
+ }
861
999
  }
862
1000
  }
863
1001
 
@@ -1093,7 +1231,7 @@ ${colors.bright}Security:${colors.reset} Stored locally in ${colors.yellow}local
1093
1231
  }
1094
1232
 
1095
1233
  // Check for contracts
1096
- if (!checkContractsExist(projectRoot)) {
1234
+ if (!(await checkContractsExist(projectRoot))) {
1097
1235
  log.header();
1098
1236
  log.title('📜 Contract Files Missing');
1099
1237
 
@@ -1130,6 +1268,34 @@ We can scan your codebase and generate them now.
1130
1268
  log.success('Created .env file');
1131
1269
  }
1132
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
+ }
1133
1299
 
1134
1300
  // Clean up DevOpsAgent files to avoid duplicates
1135
1301
  cleanupDevOpsAgentFiles(projectRoot, agentName);