s9n-devops-agent 1.6.2 → 1.7.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.
@@ -0,0 +1,544 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Branch Configuration Manager
5
+ * Manages branch management settings for the DevOps Agent
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const { execSync } = require('child_process');
11
+ const readline = require('readline');
12
+
13
+ // Configuration
14
+ const CONFIG = {
15
+ colors: {
16
+ reset: '\x1b[0m',
17
+ bright: '\x1b[1m',
18
+ red: '\x1b[31m',
19
+ green: '\x1b[32m',
20
+ yellow: '\x1b[33m',
21
+ blue: '\x1b[34m',
22
+ cyan: '\x1b[36m',
23
+ dim: '\x1b[2m'
24
+ }
25
+ };
26
+
27
+ class BranchConfigManager {
28
+ constructor() {
29
+ this.repoRoot = this.getRepoRoot();
30
+ this.localDeployDir = path.join(this.repoRoot, 'local_deploy');
31
+ this.projectSettingsPath = path.join(this.localDeployDir, 'project-settings.json');
32
+ this.defaultSettings = this.getDefaultSettings();
33
+ this.ensureDirectories();
34
+ }
35
+
36
+ getRepoRoot() {
37
+ try {
38
+ return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();
39
+ } catch (error) {
40
+ console.error(`${CONFIG.colors.red}Error: Not in a git repository${CONFIG.colors.reset}`);
41
+ process.exit(1);
42
+ }
43
+ }
44
+
45
+ ensureDirectories() {
46
+ if (!fs.existsSync(this.localDeployDir)) {
47
+ fs.mkdirSync(this.localDeployDir, { recursive: true });
48
+ }
49
+ }
50
+
51
+ getDefaultSettings() {
52
+ return {
53
+ version: "1.4.0",
54
+ branchManagement: {
55
+ defaultMergeTarget: "main",
56
+ enableDualMerge: false,
57
+ enableWeeklyConsolidation: true,
58
+ orphanSessionThresholdDays: 7,
59
+ mergeStrategy: "hierarchical-first",
60
+ conflictResolution: "prompt"
61
+ },
62
+ rolloverSettings: {
63
+ enableAutoRollover: true,
64
+ rolloverTime: "00:00",
65
+ timezone: "UTC",
66
+ preserveRunningAgent: true
67
+ },
68
+ cleanup: {
69
+ autoCleanupOrphans: false,
70
+ weeklyCleanupDay: "sunday",
71
+ retainWeeklyBranches: 12
72
+ }
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Load current project settings
78
+ */
79
+ loadSettings() {
80
+ try {
81
+ if (fs.existsSync(this.projectSettingsPath)) {
82
+ const settings = JSON.parse(fs.readFileSync(this.projectSettingsPath, 'utf8'));
83
+ // Merge with defaults to ensure all properties exist
84
+ return this.mergeWithDefaults(settings);
85
+ }
86
+ } catch (error) {
87
+ console.warn(`${CONFIG.colors.yellow}Warning: Could not load project settings: ${error.message}${CONFIG.colors.reset}`);
88
+ }
89
+ return this.defaultSettings;
90
+ }
91
+
92
+ /**
93
+ * Merge loaded settings with defaults
94
+ */
95
+ mergeWithDefaults(settings) {
96
+ const merged = JSON.parse(JSON.stringify(this.defaultSettings));
97
+
98
+ // Deep merge function
99
+ const deepMerge = (target, source) => {
100
+ for (const key in source) {
101
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
102
+ if (!target[key]) target[key] = {};
103
+ deepMerge(target[key], source[key]);
104
+ } else {
105
+ target[key] = source[key];
106
+ }
107
+ }
108
+ };
109
+
110
+ deepMerge(merged, settings);
111
+ return merged;
112
+ }
113
+
114
+ /**
115
+ * Save settings to file
116
+ */
117
+ saveSettings(settings) {
118
+ try {
119
+ // Ensure version is updated
120
+ settings.version = this.defaultSettings.version;
121
+
122
+ fs.writeFileSync(this.projectSettingsPath, JSON.stringify(settings, null, 2));
123
+ console.log(`${CONFIG.colors.green}✓ Settings saved to ${this.projectSettingsPath}${CONFIG.colors.reset}`);
124
+ return true;
125
+ } catch (error) {
126
+ console.error(`${CONFIG.colors.red}✗ Failed to save settings: ${error.message}${CONFIG.colors.reset}`);
127
+ return false;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Get a nested property value using dot notation
133
+ */
134
+ getNestedValue(obj, path) {
135
+ return path.split('.').reduce((current, key) => current && current[key], obj);
136
+ }
137
+
138
+ /**
139
+ * Set a nested property value using dot notation
140
+ */
141
+ setNestedValue(obj, path, value) {
142
+ const keys = path.split('.');
143
+ const lastKey = keys.pop();
144
+ const target = keys.reduce((current, key) => {
145
+ if (!current[key]) current[key] = {};
146
+ return current[key];
147
+ }, obj);
148
+
149
+ // Convert string values to appropriate types
150
+ if (value === 'true') value = true;
151
+ else if (value === 'false') value = false;
152
+ else if (!isNaN(value) && !isNaN(parseFloat(value))) value = parseFloat(value);
153
+
154
+ target[lastKey] = value;
155
+ }
156
+
157
+ /**
158
+ * Display current settings
159
+ */
160
+ displaySettings(settings = null) {
161
+ if (!settings) {
162
+ settings = this.loadSettings();
163
+ }
164
+
165
+ console.log(`\n${CONFIG.colors.bright}${CONFIG.colors.blue}Current Branch Management Settings${CONFIG.colors.reset}`);
166
+ console.log(`${CONFIG.colors.dim}Repository: ${this.repoRoot}${CONFIG.colors.reset}\n`);
167
+
168
+ // Branch Management
169
+ console.log(`${CONFIG.colors.bright}Branch Management:${CONFIG.colors.reset}`);
170
+ console.log(` Default merge target: ${CONFIG.colors.cyan}${settings.branchManagement.defaultMergeTarget}${CONFIG.colors.reset}`);
171
+ console.log(` Dual merge enabled: ${settings.branchManagement.enableDualMerge ? CONFIG.colors.green + 'Yes' : CONFIG.colors.yellow + 'No'}${CONFIG.colors.reset}`);
172
+ console.log(` Weekly consolidation: ${settings.branchManagement.enableWeeklyConsolidation ? CONFIG.colors.green + 'Yes' : CONFIG.colors.yellow + 'No'}${CONFIG.colors.reset}`);
173
+ console.log(` Orphan threshold: ${CONFIG.colors.cyan}${settings.branchManagement.orphanSessionThresholdDays} days${CONFIG.colors.reset}`);
174
+ console.log(` Merge strategy: ${CONFIG.colors.cyan}${settings.branchManagement.mergeStrategy}${CONFIG.colors.reset}`);
175
+ console.log(` Conflict resolution: ${CONFIG.colors.cyan}${settings.branchManagement.conflictResolution}${CONFIG.colors.reset}`);
176
+
177
+ // Rollover Settings
178
+ console.log(`\n${CONFIG.colors.bright}Rollover Settings:${CONFIG.colors.reset}`);
179
+ console.log(` Auto rollover: ${settings.rolloverSettings.enableAutoRollover ? CONFIG.colors.green + 'Yes' : CONFIG.colors.yellow + 'No'}${CONFIG.colors.reset}`);
180
+ console.log(` Rollover time: ${CONFIG.colors.cyan}${settings.rolloverSettings.rolloverTime}${CONFIG.colors.reset}`);
181
+ console.log(` Timezone: ${CONFIG.colors.cyan}${settings.rolloverSettings.timezone}${CONFIG.colors.reset}`);
182
+ console.log(` Preserve running agent: ${settings.rolloverSettings.preserveRunningAgent ? CONFIG.colors.green + 'Yes' : CONFIG.colors.yellow + 'No'}${CONFIG.colors.reset}`);
183
+
184
+ // Cleanup Settings
185
+ console.log(`\n${CONFIG.colors.bright}Cleanup Settings:${CONFIG.colors.reset}`);
186
+ console.log(` Auto cleanup orphans: ${settings.cleanup.autoCleanupOrphans ? CONFIG.colors.green + 'Yes' : CONFIG.colors.yellow + 'No'}${CONFIG.colors.reset}`);
187
+ console.log(` Weekly cleanup day: ${CONFIG.colors.cyan}${settings.cleanup.weeklyCleanupDay}${CONFIG.colors.reset}`);
188
+ console.log(` Retain weekly branches: ${CONFIG.colors.cyan}${settings.cleanup.retainWeeklyBranches}${CONFIG.colors.reset}`);
189
+ }
190
+
191
+ /**
192
+ * Interactive configuration wizard
193
+ */
194
+ async runConfigWizard() {
195
+ console.log(`\n${CONFIG.colors.bright}${CONFIG.colors.blue}Branch Management Configuration Wizard${CONFIG.colors.reset}`);
196
+ console.log(`${CONFIG.colors.dim}This wizard will help you configure branch management settings${CONFIG.colors.reset}\n`);
197
+
198
+ const settings = this.loadSettings();
199
+ const rl = readline.createInterface({
200
+ input: process.stdin,
201
+ output: process.stdout
202
+ });
203
+
204
+ const prompt = (question, defaultValue) => {
205
+ return new Promise((resolve) => {
206
+ const displayDefault = defaultValue !== undefined ? ` (${defaultValue})` : '';
207
+ rl.question(`${question}${displayDefault}: `, (answer) => {
208
+ resolve(answer.trim() || defaultValue);
209
+ });
210
+ });
211
+ };
212
+
213
+ const promptYesNo = (question, defaultValue = false) => {
214
+ return new Promise((resolve) => {
215
+ const defaultDisplay = defaultValue ? 'Y/n' : 'y/N';
216
+ rl.question(`${question} (${defaultDisplay}): `, (answer) => {
217
+ const normalized = answer.toLowerCase().trim();
218
+ if (normalized === '') {
219
+ resolve(defaultValue);
220
+ } else {
221
+ resolve(normalized === 'y' || normalized === 'yes');
222
+ }
223
+ });
224
+ });
225
+ };
226
+
227
+ try {
228
+ // Branch Management Settings
229
+ console.log(`${CONFIG.colors.bright}Branch Management Settings:${CONFIG.colors.reset}`);
230
+
231
+ settings.branchManagement.defaultMergeTarget = await prompt(
232
+ 'Default merge target branch',
233
+ settings.branchManagement.defaultMergeTarget
234
+ );
235
+
236
+ settings.branchManagement.enableDualMerge = await promptYesNo(
237
+ 'Enable dual merge (merge to both daily and target branches)',
238
+ settings.branchManagement.enableDualMerge
239
+ );
240
+
241
+ if (settings.branchManagement.enableDualMerge) {
242
+ console.log('\nMerge strategy options:');
243
+ console.log(' 1. hierarchical-first - Merge to daily branch first, then target');
244
+ console.log(' 2. target-first - Merge to target branch first, then daily');
245
+ console.log(' 3. parallel - Merge to both branches simultaneously');
246
+
247
+ const strategyChoice = await prompt('Choose merge strategy (1-3)', '1');
248
+ const strategies = ['hierarchical-first', 'target-first', 'parallel'];
249
+ settings.branchManagement.mergeStrategy = strategies[parseInt(strategyChoice) - 1] || 'hierarchical-first';
250
+ }
251
+
252
+ settings.branchManagement.enableWeeklyConsolidation = await promptYesNo(
253
+ 'Enable weekly branch consolidation',
254
+ settings.branchManagement.enableWeeklyConsolidation
255
+ );
256
+
257
+ const orphanDays = await prompt(
258
+ 'Days before session considered orphaned',
259
+ settings.branchManagement.orphanSessionThresholdDays
260
+ );
261
+ settings.branchManagement.orphanSessionThresholdDays = parseInt(orphanDays) || 7;
262
+
263
+ // Rollover Settings
264
+ console.log(`\n${CONFIG.colors.bright}Rollover Settings:${CONFIG.colors.reset}`);
265
+
266
+ settings.rolloverSettings.enableAutoRollover = await promptYesNo(
267
+ 'Enable automatic daily rollover',
268
+ settings.rolloverSettings.enableAutoRollover
269
+ );
270
+
271
+ if (settings.rolloverSettings.enableAutoRollover) {
272
+ settings.rolloverSettings.rolloverTime = await prompt(
273
+ 'Rollover time (HH:MM format)',
274
+ settings.rolloverSettings.rolloverTime
275
+ );
276
+
277
+ settings.rolloverSettings.timezone = await prompt(
278
+ 'Timezone for rollover',
279
+ settings.rolloverSettings.timezone
280
+ );
281
+ }
282
+
283
+ settings.rolloverSettings.preserveRunningAgent = await promptYesNo(
284
+ 'Preserve running agent during rollover',
285
+ settings.rolloverSettings.preserveRunningAgent
286
+ );
287
+
288
+ // Cleanup Settings
289
+ console.log(`\n${CONFIG.colors.bright}Cleanup Settings:${CONFIG.colors.reset}`);
290
+
291
+ settings.cleanup.autoCleanupOrphans = await promptYesNo(
292
+ 'Automatically cleanup orphaned sessions',
293
+ settings.cleanup.autoCleanupOrphans
294
+ );
295
+
296
+ if (settings.branchManagement.enableWeeklyConsolidation) {
297
+ console.log('\nWeekly cleanup day options: sunday, monday, tuesday, wednesday, thursday, friday, saturday');
298
+ settings.cleanup.weeklyCleanupDay = await prompt(
299
+ 'Day of week for weekly cleanup',
300
+ settings.cleanup.weeklyCleanupDay
301
+ );
302
+
303
+ const retainWeeks = await prompt(
304
+ 'Number of weekly branches to retain',
305
+ settings.cleanup.retainWeeklyBranches
306
+ );
307
+ settings.cleanup.retainWeeklyBranches = parseInt(retainWeeks) || 12;
308
+ }
309
+
310
+ rl.close();
311
+
312
+ // Display final configuration
313
+ console.log(`\n${CONFIG.colors.bright}Configuration Summary:${CONFIG.colors.reset}`);
314
+ this.displaySettings(settings);
315
+
316
+ // Confirm save
317
+ const rl2 = readline.createInterface({
318
+ input: process.stdin,
319
+ output: process.stdout
320
+ });
321
+
322
+ const shouldSave = await new Promise((resolve) => {
323
+ rl2.question('\nSave this configuration? (Y/n): ', (answer) => {
324
+ rl2.close();
325
+ resolve(answer.toLowerCase() !== 'n');
326
+ });
327
+ });
328
+
329
+ if (shouldSave) {
330
+ this.saveSettings(settings);
331
+ console.log(`\n${CONFIG.colors.green}✅ Configuration saved successfully${CONFIG.colors.reset}`);
332
+ } else {
333
+ console.log(`\n${CONFIG.colors.yellow}Configuration not saved${CONFIG.colors.reset}`);
334
+ }
335
+
336
+ } catch (error) {
337
+ rl.close();
338
+ console.error(`\n${CONFIG.colors.red}❌ Configuration wizard failed: ${error.message}${CONFIG.colors.reset}`);
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Get a specific setting value
344
+ */
345
+ getSetting(settingPath) {
346
+ const settings = this.loadSettings();
347
+ const value = this.getNestedValue(settings, settingPath);
348
+
349
+ if (value !== undefined) {
350
+ console.log(`${settingPath}: ${CONFIG.colors.cyan}${value}${CONFIG.colors.reset}`);
351
+ } else {
352
+ console.log(`${CONFIG.colors.red}Setting not found: ${settingPath}${CONFIG.colors.reset}`);
353
+ }
354
+
355
+ return value;
356
+ }
357
+
358
+ /**
359
+ * Set a specific setting value
360
+ */
361
+ setSetting(settingPath, value) {
362
+ const settings = this.loadSettings();
363
+
364
+ try {
365
+ this.setNestedValue(settings, settingPath, value);
366
+
367
+ if (this.saveSettings(settings)) {
368
+ console.log(`${CONFIG.colors.green}✓ Updated ${settingPath} = ${value}${CONFIG.colors.reset}`);
369
+ return true;
370
+ }
371
+ } catch (error) {
372
+ console.error(`${CONFIG.colors.red}✗ Failed to set ${settingPath}: ${error.message}${CONFIG.colors.reset}`);
373
+ }
374
+
375
+ return false;
376
+ }
377
+
378
+ /**
379
+ * Reset settings to defaults
380
+ */
381
+ resetSettings() {
382
+ const rl = readline.createInterface({
383
+ input: process.stdin,
384
+ output: process.stdout
385
+ });
386
+
387
+ return new Promise((resolve) => {
388
+ console.log(`${CONFIG.colors.yellow}⚠️ This will reset all branch management settings to defaults${CONFIG.colors.reset}`);
389
+ rl.question('Are you sure? (y/N): ', (answer) => {
390
+ rl.close();
391
+
392
+ if (answer.toLowerCase() === 'y') {
393
+ if (this.saveSettings(this.defaultSettings)) {
394
+ console.log(`${CONFIG.colors.green}✅ Settings reset to defaults${CONFIG.colors.reset}`);
395
+ resolve(true);
396
+ } else {
397
+ resolve(false);
398
+ }
399
+ } else {
400
+ console.log('Reset cancelled');
401
+ resolve(false);
402
+ }
403
+ });
404
+ });
405
+ }
406
+
407
+ /**
408
+ * Validate current settings
409
+ */
410
+ validateSettings() {
411
+ const settings = this.loadSettings();
412
+ const issues = [];
413
+
414
+ console.log(`\n${CONFIG.colors.bright}${CONFIG.colors.blue}Validating Settings${CONFIG.colors.reset}\n`);
415
+
416
+ // Validate merge target branch exists
417
+ if (settings.branchManagement.defaultMergeTarget) {
418
+ try {
419
+ execSync(`git show-ref --verify --quiet refs/remotes/origin/${settings.branchManagement.defaultMergeTarget}`, { stdio: 'ignore' });
420
+ console.log(`${CONFIG.colors.green}✓ Target branch '${settings.branchManagement.defaultMergeTarget}' exists${CONFIG.colors.reset}`);
421
+ } catch {
422
+ issues.push(`Target branch '${settings.branchManagement.defaultMergeTarget}' does not exist`);
423
+ console.log(`${CONFIG.colors.red}✗ Target branch '${settings.branchManagement.defaultMergeTarget}' does not exist${CONFIG.colors.reset}`);
424
+ }
425
+ }
426
+
427
+ // Validate merge strategy
428
+ const validStrategies = ['hierarchical-first', 'target-first', 'parallel'];
429
+ if (!validStrategies.includes(settings.branchManagement.mergeStrategy)) {
430
+ issues.push(`Invalid merge strategy: ${settings.branchManagement.mergeStrategy}`);
431
+ console.log(`${CONFIG.colors.red}✗ Invalid merge strategy: ${settings.branchManagement.mergeStrategy}${CONFIG.colors.reset}`);
432
+ } else {
433
+ console.log(`${CONFIG.colors.green}✓ Merge strategy '${settings.branchManagement.mergeStrategy}' is valid${CONFIG.colors.reset}`);
434
+ }
435
+
436
+ // Validate orphan threshold
437
+ if (settings.branchManagement.orphanSessionThresholdDays < 1) {
438
+ issues.push('Orphan threshold must be at least 1 day');
439
+ console.log(`${CONFIG.colors.red}✗ Orphan threshold must be at least 1 day${CONFIG.colors.reset}`);
440
+ } else {
441
+ console.log(`${CONFIG.colors.green}✓ Orphan threshold (${settings.branchManagement.orphanSessionThresholdDays} days) is valid${CONFIG.colors.reset}`);
442
+ }
443
+
444
+ // Validate rollover time format
445
+ const timeRegex = /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/;
446
+ if (!timeRegex.test(settings.rolloverSettings.rolloverTime)) {
447
+ issues.push(`Invalid rollover time format: ${settings.rolloverSettings.rolloverTime}`);
448
+ console.log(`${CONFIG.colors.red}✗ Invalid rollover time format: ${settings.rolloverSettings.rolloverTime}${CONFIG.colors.reset}`);
449
+ } else {
450
+ console.log(`${CONFIG.colors.green}✓ Rollover time format is valid${CONFIG.colors.reset}`);
451
+ }
452
+
453
+ // Validate weekly cleanup day
454
+ const validDays = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
455
+ if (!validDays.includes(settings.cleanup.weeklyCleanupDay.toLowerCase())) {
456
+ issues.push(`Invalid weekly cleanup day: ${settings.cleanup.weeklyCleanupDay}`);
457
+ console.log(`${CONFIG.colors.red}✗ Invalid weekly cleanup day: ${settings.cleanup.weeklyCleanupDay}${CONFIG.colors.reset}`);
458
+ } else {
459
+ console.log(`${CONFIG.colors.green}✓ Weekly cleanup day is valid${CONFIG.colors.reset}`);
460
+ }
461
+
462
+ // Summary
463
+ if (issues.length === 0) {
464
+ console.log(`\n${CONFIG.colors.green}✅ All settings are valid${CONFIG.colors.reset}`);
465
+ } else {
466
+ console.log(`\n${CONFIG.colors.red}❌ Found ${issues.length} issue(s):${CONFIG.colors.reset}`);
467
+ issues.forEach(issue => {
468
+ console.log(` • ${issue}`);
469
+ });
470
+ }
471
+
472
+ return issues.length === 0;
473
+ }
474
+
475
+ /**
476
+ * Main execution function
477
+ */
478
+ async run(command, ...args) {
479
+ try {
480
+ switch (command) {
481
+ case 'show':
482
+ case 'display':
483
+ this.displaySettings();
484
+ break;
485
+
486
+ case 'wizard':
487
+ case 'setup':
488
+ await this.runConfigWizard();
489
+ break;
490
+
491
+ case 'get':
492
+ if (args.length === 0) {
493
+ console.log(`${CONFIG.colors.red}Usage: get <setting.path>${CONFIG.colors.reset}`);
494
+ return;
495
+ }
496
+ this.getSetting(args[0]);
497
+ break;
498
+
499
+ case 'set':
500
+ if (args.length < 2) {
501
+ console.log(`${CONFIG.colors.red}Usage: set <setting.path> <value>${CONFIG.colors.reset}`);
502
+ return;
503
+ }
504
+ this.setSetting(args[0], args[1]);
505
+ break;
506
+
507
+ case 'reset':
508
+ await this.resetSettings();
509
+ break;
510
+
511
+ case 'validate':
512
+ this.validateSettings();
513
+ break;
514
+
515
+ default:
516
+ console.log(`${CONFIG.colors.red}Unknown command: ${command}${CONFIG.colors.reset}`);
517
+ console.log('\nAvailable commands:');
518
+ console.log(' show/display - Display current settings');
519
+ console.log(' wizard/setup - Run interactive configuration wizard');
520
+ console.log(' get <path> - Get a specific setting value');
521
+ console.log(' set <path> <value> - Set a specific setting value');
522
+ console.log(' reset - Reset all settings to defaults');
523
+ console.log(' validate - Validate current settings');
524
+ console.log('\nExample setting paths:');
525
+ console.log(' branchManagement.enableDualMerge');
526
+ console.log(' branchManagement.defaultMergeTarget');
527
+ console.log(' cleanup.retainWeeklyBranches');
528
+ }
529
+ } catch (error) {
530
+ console.error(`${CONFIG.colors.red}❌ Operation failed: ${error.message}${CONFIG.colors.reset}`);
531
+ process.exit(1);
532
+ }
533
+ }
534
+ }
535
+
536
+ // CLI execution
537
+ if (require.main === module) {
538
+ const command = process.argv[2] || 'show';
539
+ const args = process.argv.slice(3);
540
+ const manager = new BranchConfigManager();
541
+ manager.run(command, ...args);
542
+ }
543
+
544
+ module.exports = BranchConfigManager;