s9n-devops-agent 1.6.2 → 1.7.1

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,538 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Orphaned Session Cleaner
5
+ * Detects and cleans up sessions that have been inactive for a specified period
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 OrphanedSessionCleaner {
28
+ constructor() {
29
+ this.repoRoot = this.getRepoRoot();
30
+ this.locksPath = path.join(this.repoRoot, 'local_deploy', 'session-locks');
31
+ this.worktreesPath = path.join(this.repoRoot, 'local_deploy', 'worktrees');
32
+ this.projectSettingsPath = path.join(this.repoRoot, 'local_deploy', 'project-settings.json');
33
+ this.projectSettings = this.loadProjectSettings();
34
+ this.thresholdDays = this.projectSettings?.branchManagement?.orphanSessionThresholdDays || 7;
35
+ }
36
+
37
+ getRepoRoot() {
38
+ try {
39
+ return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();
40
+ } catch (error) {
41
+ console.error(`${CONFIG.colors.red}Error: Not in a git repository${CONFIG.colors.reset}`);
42
+ process.exit(1);
43
+ }
44
+ }
45
+
46
+ loadProjectSettings() {
47
+ try {
48
+ if (fs.existsSync(this.projectSettingsPath)) {
49
+ return JSON.parse(fs.readFileSync(this.projectSettingsPath, 'utf8'));
50
+ }
51
+ } catch (error) {
52
+ console.warn(`${CONFIG.colors.yellow}Warning: Could not load project settings${CONFIG.colors.reset}`);
53
+ }
54
+ return {};
55
+ }
56
+
57
+ /**
58
+ * Get the last commit date for a branch
59
+ */
60
+ async getLastCommitDate(branchName) {
61
+ try {
62
+ const timestamp = execSync(`git log -1 --format="%ct" origin/${branchName}`, { encoding: 'utf8' }).trim();
63
+ return new Date(parseInt(timestamp) * 1000);
64
+ } catch (error) {
65
+ // Branch might not exist or have no commits
66
+ throw new Error(`Cannot get last commit date for branch ${branchName}`);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Check if a branch exists locally or remotely
72
+ */
73
+ async branchExists(branchName) {
74
+ try {
75
+ // Check remote first (more reliable)
76
+ execSync(`git show-ref --verify --quiet refs/remotes/origin/${branchName}`, { stdio: 'ignore' });
77
+ return true;
78
+ } catch {
79
+ try {
80
+ // Check local
81
+ execSync(`git show-ref --verify --quiet refs/heads/${branchName}`, { stdio: 'ignore' });
82
+ return true;
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Get current daily branch name
91
+ */
92
+ getDailyBranch() {
93
+ const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
94
+ return `daily/${today}`;
95
+ }
96
+
97
+ /**
98
+ * Find all orphaned sessions
99
+ */
100
+ async findOrphanedSessions() {
101
+ if (!fs.existsSync(this.locksPath)) {
102
+ return [];
103
+ }
104
+
105
+ console.log(`${CONFIG.colors.blue}Scanning for orphaned sessions (threshold: ${this.thresholdDays} days)...${CONFIG.colors.reset}`);
106
+
107
+ const orphans = [];
108
+ const lockFiles = fs.readdirSync(this.locksPath).filter(f => f.endsWith('.lock'));
109
+ const thresholdDate = new Date();
110
+ thresholdDate.setDate(thresholdDate.getDate() - this.thresholdDays);
111
+
112
+ // Fetch latest remote information
113
+ try {
114
+ execSync('git fetch --all --prune', { stdio: 'ignore' });
115
+ } catch (error) {
116
+ console.warn(`${CONFIG.colors.yellow}Warning: Could not fetch remote branches${CONFIG.colors.reset}`);
117
+ }
118
+
119
+ for (const lockFile of lockFiles) {
120
+ const lockPath = path.join(this.locksPath, lockFile);
121
+
122
+ try {
123
+ const sessionData = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
124
+ const sessionId = sessionData.sessionId;
125
+ const branchName = sessionData.branchName;
126
+
127
+ console.log(`${CONFIG.colors.dim}Checking session: ${sessionId} (${branchName})${CONFIG.colors.reset}`);
128
+
129
+ let lastActivity = null;
130
+ let daysSinceLastActivity = 0;
131
+ let branchMissing = false;
132
+ let reason = '';
133
+
134
+ try {
135
+ if (await this.branchExists(branchName)) {
136
+ // Branch exists, check last commit date
137
+ lastActivity = await this.getLastCommitDate(branchName);
138
+ daysSinceLastActivity = Math.floor((new Date() - lastActivity) / (1000 * 60 * 60 * 24));
139
+
140
+ if (lastActivity < thresholdDate) {
141
+ reason = `No commits for ${daysSinceLastActivity} days`;
142
+ }
143
+ } else {
144
+ // Branch doesn't exist, use session creation date
145
+ branchMissing = true;
146
+ lastActivity = new Date(sessionData.created);
147
+ daysSinceLastActivity = Math.floor((new Date() - lastActivity) / (1000 * 60 * 60 * 24));
148
+ reason = `Branch missing, created ${daysSinceLastActivity} days ago`;
149
+ }
150
+ } catch (error) {
151
+ // Error getting branch info, consider it orphaned
152
+ branchMissing = true;
153
+ lastActivity = new Date(sessionData.created);
154
+ daysSinceLastActivity = Math.floor((new Date() - lastActivity) / (1000 * 60 * 60 * 24));
155
+ reason = `Branch error: ${error.message}`;
156
+ }
157
+
158
+ // Check if session should be considered orphaned
159
+ if (daysSinceLastActivity >= this.thresholdDays) {
160
+ orphans.push({
161
+ ...sessionData,
162
+ lastActivity,
163
+ daysSinceLastActivity,
164
+ branchMissing,
165
+ reason,
166
+ lockFile: lockPath
167
+ });
168
+ }
169
+ } catch (error) {
170
+ console.error(`${CONFIG.colors.red}Error processing session lock ${lockFile}: ${error.message}${CONFIG.colors.reset}`);
171
+ }
172
+ }
173
+
174
+ return orphans;
175
+ }
176
+
177
+ /**
178
+ * Display orphaned sessions and prompt for cleanup
179
+ */
180
+ async promptForOrphanCleanup(orphans) {
181
+ console.log(`\n${CONFIG.colors.bright}🧹 Found ${orphans.length} orphaned session(s):${CONFIG.colors.reset}\n`);
182
+
183
+ orphans.forEach((orphan, index) => {
184
+ const statusIcon = orphan.branchMissing ? '❌' : '⏰';
185
+ console.log(`${statusIcon} ${CONFIG.colors.bright}${index + 1}. ${orphan.sessionId}${CONFIG.colors.reset}`);
186
+ console.log(` Task: ${orphan.task}`);
187
+ console.log(` Agent: ${orphan.agentType || 'unknown'}`);
188
+ console.log(` Branch: ${orphan.branchName}${orphan.branchMissing ? ' (missing)' : ''}`);
189
+ console.log(` Inactive: ${orphan.daysSinceLastActivity} days`);
190
+ console.log(` Reason: ${orphan.reason}`);
191
+ console.log(` Created: ${orphan.created}`);
192
+ if (orphan.lastActivity) {
193
+ console.log(` Last activity: ${orphan.lastActivity.toISOString().split('T')[0]}`);
194
+ }
195
+ console.log('');
196
+ });
197
+
198
+ const rl = readline.createInterface({
199
+ input: process.stdin,
200
+ output: process.stdout
201
+ });
202
+
203
+ return new Promise((resolve) => {
204
+ console.log(`${CONFIG.colors.bright}Cleanup Options:${CONFIG.colors.reset}`);
205
+ console.log(` ${CONFIG.colors.green}(a) Clean up all orphaned sessions${CONFIG.colors.reset}`);
206
+ console.log(` ${CONFIG.colors.yellow}(s) Select specific sessions to clean up${CONFIG.colors.reset}`);
207
+ console.log(` ${CONFIG.colors.dim}(n) No cleanup, just report${CONFIG.colors.reset}`);
208
+
209
+ rl.question('\nChoose action (a/s/n): ', (answer) => {
210
+ rl.close();
211
+ const action = answer.toLowerCase();
212
+ if (['a', 's', 'n'].includes(action)) {
213
+ resolve(action);
214
+ } else {
215
+ console.log('Invalid choice, defaulting to no cleanup');
216
+ resolve('n');
217
+ }
218
+ });
219
+ });
220
+ }
221
+
222
+ /**
223
+ * Select specific sessions for cleanup
224
+ */
225
+ async selectSessionsForCleanup(orphans) {
226
+ const rl = readline.createInterface({
227
+ input: process.stdin,
228
+ output: process.stdout
229
+ });
230
+
231
+ return new Promise((resolve) => {
232
+ console.log(`\n${CONFIG.colors.bright}Select sessions to clean up:${CONFIG.colors.reset}`);
233
+ console.log('Enter session numbers separated by commas (e.g., 1,3,5) or "all" for all sessions:');
234
+
235
+ rl.question('Selection: ', (answer) => {
236
+ rl.close();
237
+
238
+ if (answer.toLowerCase() === 'all') {
239
+ resolve(orphans);
240
+ } else {
241
+ const indices = answer.split(',')
242
+ .map(s => parseInt(s.trim()) - 1)
243
+ .filter(i => i >= 0 && i < orphans.length);
244
+
245
+ const selectedOrphans = indices.map(i => orphans[i]);
246
+ resolve(selectedOrphans);
247
+ }
248
+ });
249
+ });
250
+ }
251
+
252
+ /**
253
+ * Merge branch using the same logic as enhanced-close-session.js
254
+ */
255
+ async mergeBranch(sourceBranch, targetBranch) {
256
+ try {
257
+ console.log(`${CONFIG.colors.blue} Merging ${sourceBranch} → ${targetBranch}${CONFIG.colors.reset}`);
258
+
259
+ // Ensure target branch exists
260
+ await this.ensureBranch(targetBranch);
261
+
262
+ // Checkout target branch
263
+ execSync(`git checkout ${targetBranch}`, { stdio: 'ignore' });
264
+
265
+ // Attempt merge
266
+ execSync(`git merge --no-ff origin/${sourceBranch} -m "Merge orphaned session branch ${sourceBranch}"`, { stdio: 'ignore' });
267
+
268
+ // Push the merge
269
+ execSync(`git push origin ${targetBranch}`, { stdio: 'ignore' });
270
+
271
+ console.log(`${CONFIG.colors.green} ✓ Successfully merged ${sourceBranch} → ${targetBranch}${CONFIG.colors.reset}`);
272
+ return true;
273
+ } catch (error) {
274
+ console.error(`${CONFIG.colors.red} ✗ Failed to merge ${sourceBranch} → ${targetBranch}${CONFIG.colors.reset}`);
275
+ console.error(`${CONFIG.colors.dim} Error: ${error.message}${CONFIG.colors.reset}`);
276
+
277
+ // Reset any partial merge state
278
+ try {
279
+ execSync('git merge --abort', { stdio: 'ignore' });
280
+ } catch {}
281
+
282
+ return false;
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Ensure a branch exists
288
+ */
289
+ async ensureBranch(branchName, baseBranch = 'main') {
290
+ if (await this.branchExists(branchName)) {
291
+ return true;
292
+ }
293
+
294
+ try {
295
+ console.log(`${CONFIG.colors.blue} Creating branch: ${branchName} from ${baseBranch}${CONFIG.colors.reset}`);
296
+ execSync(`git checkout -b ${branchName} origin/${baseBranch}`, { stdio: 'ignore' });
297
+ execSync(`git push -u origin ${branchName}`, { stdio: 'ignore' });
298
+ return true;
299
+ } catch (error) {
300
+ console.error(`${CONFIG.colors.red} Failed to create branch ${branchName}: ${error.message}${CONFIG.colors.reset}`);
301
+ return false;
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Perform dual merge for orphaned session
307
+ */
308
+ async performDualMerge(orphan) {
309
+ const sessionBranch = orphan.branchName;
310
+ const dailyBranch = this.getDailyBranch();
311
+ const targetBranch = this.projectSettings?.branchManagement?.defaultMergeTarget || 'main';
312
+ const enableDualMerge = this.projectSettings?.branchManagement?.enableDualMerge;
313
+
314
+ if (!enableDualMerge || !targetBranch) {
315
+ // Single merge to daily branch
316
+ return await this.mergeBranch(sessionBranch, dailyBranch);
317
+ }
318
+
319
+ console.log(`${CONFIG.colors.blue} Performing dual merge...${CONFIG.colors.reset}`);
320
+
321
+ const dailySuccess = await this.mergeBranch(sessionBranch, dailyBranch);
322
+ const targetSuccess = await this.mergeBranch(sessionBranch, targetBranch);
323
+
324
+ if (dailySuccess && targetSuccess) {
325
+ console.log(`${CONFIG.colors.green} ✓ Dual merge completed successfully${CONFIG.colors.reset}`);
326
+ return true;
327
+ } else if (dailySuccess || targetSuccess) {
328
+ console.log(`${CONFIG.colors.yellow} ⚠️ Partial merge completed${CONFIG.colors.reset}`);
329
+ return true; // Partial success is still success
330
+ } else {
331
+ console.log(`${CONFIG.colors.red} ❌ Both merges failed${CONFIG.colors.reset}`);
332
+ return false;
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Clean up session files and worktree
338
+ */
339
+ async cleanupSessionFiles(orphan) {
340
+ const sessionId = orphan.sessionId;
341
+ const branchName = orphan.branchName;
342
+ const worktreePath = orphan.worktreePath;
343
+
344
+ try {
345
+ // Remove worktree if it exists
346
+ if (worktreePath && fs.existsSync(worktreePath)) {
347
+ console.log(`${CONFIG.colors.blue} Removing worktree: ${worktreePath}${CONFIG.colors.reset}`);
348
+ execSync(`git worktree remove --force "${worktreePath}"`, { stdio: 'ignore' });
349
+ }
350
+
351
+ // Delete session branch (only if it exists and was successfully merged)
352
+ if (!orphan.branchMissing && await this.branchExists(branchName)) {
353
+ console.log(`${CONFIG.colors.blue} Deleting session branch: ${branchName}${CONFIG.colors.reset}`);
354
+
355
+ // Delete local branch
356
+ try {
357
+ execSync(`git branch -D ${branchName}`, { stdio: 'ignore' });
358
+ } catch {}
359
+
360
+ // Delete remote branch
361
+ try {
362
+ execSync(`git push origin --delete ${branchName}`, { stdio: 'ignore' });
363
+ } catch {}
364
+ }
365
+
366
+ // Remove session lock file
367
+ if (fs.existsSync(orphan.lockFile)) {
368
+ console.log(`${CONFIG.colors.blue} Removing session lock file${CONFIG.colors.reset}`);
369
+ fs.unlinkSync(orphan.lockFile);
370
+ }
371
+
372
+ // Remove any commit message files
373
+ const commitMsgFile = path.join(this.repoRoot, `.devops-commit-${sessionId}.msg`);
374
+ if (fs.existsSync(commitMsgFile)) {
375
+ fs.unlinkSync(commitMsgFile);
376
+ }
377
+
378
+ console.log(`${CONFIG.colors.green} ✓ Session cleanup completed${CONFIG.colors.reset}`);
379
+ } catch (error) {
380
+ console.error(`${CONFIG.colors.red} ✗ Session cleanup failed: ${error.message}${CONFIG.colors.reset}`);
381
+ throw error;
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Clean up a single orphaned session
387
+ */
388
+ async cleanupOrphanedSession(orphan) {
389
+ console.log(`\n${CONFIG.colors.bright}Cleaning up session: ${orphan.sessionId}${CONFIG.colors.reset}`);
390
+ console.log(`Task: ${orphan.task}`);
391
+ console.log(`Branch: ${orphan.branchName}${orphan.branchMissing ? ' (missing)' : ''}`);
392
+ console.log(`Inactive for: ${orphan.daysSinceLastActivity} days`);
393
+
394
+ try {
395
+ // Only attempt merge if branch exists and has content
396
+ if (!orphan.branchMissing) {
397
+ console.log(`${CONFIG.colors.blue}Attempting to merge session work...${CONFIG.colors.reset}`);
398
+ const mergeSuccess = await this.performDualMerge(orphan);
399
+
400
+ if (!mergeSuccess) {
401
+ console.log(`${CONFIG.colors.yellow}⚠️ Merge failed, but continuing with cleanup${CONFIG.colors.reset}`);
402
+ console.log(`${CONFIG.colors.dim}Branch ${orphan.branchName} will be preserved for manual review${CONFIG.colors.reset}`);
403
+
404
+ // Don't delete the branch if merge failed
405
+ orphan.branchMissing = true;
406
+ }
407
+ } else {
408
+ console.log(`${CONFIG.colors.yellow}Branch missing or inaccessible, skipping merge${CONFIG.colors.reset}`);
409
+ }
410
+
411
+ // Clean up session files
412
+ await this.cleanupSessionFiles(orphan);
413
+
414
+ console.log(`${CONFIG.colors.green}✅ Successfully cleaned up session: ${orphan.sessionId}${CONFIG.colors.reset}`);
415
+ return true;
416
+ } catch (error) {
417
+ console.error(`${CONFIG.colors.red}❌ Failed to cleanup session ${orphan.sessionId}: ${error.message}${CONFIG.colors.reset}`);
418
+ return false;
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Main cleanup process
424
+ */
425
+ async cleanupOrphans() {
426
+ console.log(`\n${CONFIG.colors.bright}${CONFIG.colors.blue}Orphaned Session Cleanup${CONFIG.colors.reset}`);
427
+ console.log(`${CONFIG.colors.dim}Repository: ${this.repoRoot}${CONFIG.colors.reset}`);
428
+ console.log(`${CONFIG.colors.dim}Threshold: ${this.thresholdDays} days${CONFIG.colors.reset}\n`);
429
+
430
+ const orphans = await this.findOrphanedSessions();
431
+
432
+ if (orphans.length === 0) {
433
+ console.log(`${CONFIG.colors.green}✅ No orphaned sessions found${CONFIG.colors.reset}`);
434
+ return;
435
+ }
436
+
437
+ const action = await this.promptForOrphanCleanup(orphans);
438
+
439
+ if (action === 'n') {
440
+ console.log('Orphan cleanup cancelled');
441
+ return;
442
+ }
443
+
444
+ let sessionsToCleanup = orphans;
445
+ if (action === 's') {
446
+ sessionsToCleanup = await this.selectSessionsForCleanup(orphans);
447
+ if (sessionsToCleanup.length === 0) {
448
+ console.log('No sessions selected for cleanup');
449
+ return;
450
+ }
451
+ }
452
+
453
+ console.log(`\n${CONFIG.colors.bright}🧹 Cleaning up ${sessionsToCleanup.length} orphaned session(s)...${CONFIG.colors.reset}`);
454
+
455
+ let successCount = 0;
456
+ let failureCount = 0;
457
+
458
+ for (const orphan of sessionsToCleanup) {
459
+ const success = await this.cleanupOrphanedSession(orphan);
460
+ if (success) {
461
+ successCount++;
462
+ } else {
463
+ failureCount++;
464
+ }
465
+ }
466
+
467
+ console.log(`\n${CONFIG.colors.bright}Cleanup Summary:${CONFIG.colors.reset}`);
468
+ console.log(`${CONFIG.colors.green}✅ Successfully cleaned: ${successCount}${CONFIG.colors.reset}`);
469
+ if (failureCount > 0) {
470
+ console.log(`${CONFIG.colors.red}❌ Failed to clean: ${failureCount}${CONFIG.colors.reset}`);
471
+ }
472
+
473
+ if (successCount > 0) {
474
+ console.log(`\n${CONFIG.colors.green}✅ Orphan cleanup completed${CONFIG.colors.reset}`);
475
+ }
476
+ }
477
+
478
+ /**
479
+ * List orphaned sessions without cleanup
480
+ */
481
+ async listOrphans() {
482
+ console.log(`\n${CONFIG.colors.bright}${CONFIG.colors.blue}Orphaned Session Report${CONFIG.colors.reset}`);
483
+ console.log(`${CONFIG.colors.dim}Repository: ${this.repoRoot}${CONFIG.colors.reset}`);
484
+ console.log(`${CONFIG.colors.dim}Threshold: ${this.thresholdDays} days${CONFIG.colors.reset}\n`);
485
+
486
+ const orphans = await this.findOrphanedSessions();
487
+
488
+ if (orphans.length === 0) {
489
+ console.log(`${CONFIG.colors.green}✅ No orphaned sessions found${CONFIG.colors.reset}`);
490
+ return;
491
+ }
492
+
493
+ console.log(`${CONFIG.colors.bright}Found ${orphans.length} orphaned session(s):${CONFIG.colors.reset}\n`);
494
+
495
+ orphans.forEach((orphan, index) => {
496
+ const statusIcon = orphan.branchMissing ? '❌' : '⏰';
497
+ console.log(`${statusIcon} ${CONFIG.colors.bright}${index + 1}. ${orphan.sessionId}${CONFIG.colors.reset}`);
498
+ console.log(` Task: ${orphan.task}`);
499
+ console.log(` Branch: ${orphan.branchName}${orphan.branchMissing ? ' (missing)' : ''}`);
500
+ console.log(` Inactive: ${orphan.daysSinceLastActivity} days`);
501
+ console.log(` Reason: ${orphan.reason}`);
502
+ console.log('');
503
+ });
504
+
505
+ console.log(`${CONFIG.colors.dim}Run with 'cleanup' command to clean up these sessions${CONFIG.colors.reset}`);
506
+ }
507
+
508
+ /**
509
+ * Main execution function
510
+ */
511
+ async run(command = 'cleanup') {
512
+ try {
513
+ switch (command) {
514
+ case 'cleanup':
515
+ await this.cleanupOrphans();
516
+ break;
517
+ case 'list':
518
+ await this.listOrphans();
519
+ break;
520
+ default:
521
+ console.log(`${CONFIG.colors.red}Unknown command: ${command}${CONFIG.colors.reset}`);
522
+ console.log('Available commands: cleanup, list');
523
+ }
524
+ } catch (error) {
525
+ console.error(`${CONFIG.colors.red}❌ Operation failed: ${error.message}${CONFIG.colors.reset}`);
526
+ process.exit(1);
527
+ }
528
+ }
529
+ }
530
+
531
+ // CLI execution
532
+ if (require.main === module) {
533
+ const command = process.argv[2] || 'cleanup';
534
+ const cleaner = new OrphanedSessionCleaner();
535
+ cleaner.run(command);
536
+ }
537
+
538
+ module.exports = OrphanedSessionCleaner;
@@ -158,6 +158,9 @@ class SessionCoordinator {
158
158
  }
159
159
 
160
160
  try {
161
+ // Show checking message
162
+ console.log(`${CONFIG.colors.dim}🔍 Checking for DevOps Agent updates...${CONFIG.colors.reset}`);
163
+
161
164
  // Check npm for latest version
162
165
  const result = execSync('npm view s9n-devops-agent version', {
163
166
  encoding: 'utf8',
@@ -206,9 +209,13 @@ class SessionCoordinator {
206
209
  console.log(`${CONFIG.colors.dim}You can update later with: npm install -g s9n-devops-agent@latest${CONFIG.colors.reset}`);
207
210
  }
208
211
  console.log();
212
+ } else {
213
+ // Version is up to date
214
+ console.log(`${CONFIG.colors.dim}✓ DevOps Agent is up to date (v${this.currentVersion})${CONFIG.colors.reset}`);
209
215
  }
210
216
  } catch (err) {
211
217
  // Silently fail - don't block execution on update check
218
+ console.log(`${CONFIG.colors.dim}✗ Could not check for updates (offline or npm unavailable)${CONFIG.colors.reset}`);
212
219
  }
213
220
  }
214
221