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.
- package/README.md +29 -18
- package/docs/AUTOMATED_TESTING_STRATEGY.md +316 -0
- package/docs/branch-management.md +101 -0
- package/package.json +1 -1
- package/src/branch-config-manager.js +544 -0
- package/src/enhanced-close-session.js +492 -0
- package/src/orphan-cleaner.js +538 -0
- package/src/session-coordinator.js +7 -0
- package/src/weekly-consolidator.js +403 -0
- package/start-devops-session.sh +1 -1
|
@@ -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
|
|