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,492 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Enhanced Session Cleanup Tool with Dual Merge Support
|
|
5
|
+
* Safely closes and cleans up DevOps agent sessions with hierarchical and target branch merging
|
|
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 EnhancedSessionCloser {
|
|
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
|
+
}
|
|
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
|
+
loadProjectSettings() {
|
|
46
|
+
try {
|
|
47
|
+
if (fs.existsSync(this.projectSettingsPath)) {
|
|
48
|
+
return JSON.parse(fs.readFileSync(this.projectSettingsPath, 'utf8'));
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.warn(`${CONFIG.colors.yellow}Warning: Could not load project settings${CONFIG.colors.reset}`);
|
|
52
|
+
}
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get current daily branch name
|
|
58
|
+
*/
|
|
59
|
+
getDailyBranch() {
|
|
60
|
+
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
|
61
|
+
return `daily/${today}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if a branch exists
|
|
66
|
+
*/
|
|
67
|
+
async branchExists(branchName) {
|
|
68
|
+
try {
|
|
69
|
+
execSync(`git show-ref --verify --quiet refs/heads/${branchName}`, { stdio: 'ignore' });
|
|
70
|
+
return true;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create a branch if it doesn't exist
|
|
78
|
+
*/
|
|
79
|
+
async ensureBranch(branchName, baseBranch = 'main') {
|
|
80
|
+
if (await this.branchExists(branchName)) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
console.log(`${CONFIG.colors.blue}Creating branch: ${branchName} from ${baseBranch}${CONFIG.colors.reset}`);
|
|
86
|
+
execSync(`git checkout -b ${branchName} ${baseBranch}`, { stdio: 'ignore' });
|
|
87
|
+
execSync(`git push -u origin ${branchName}`, { stdio: 'ignore' });
|
|
88
|
+
return true;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error(`${CONFIG.colors.red}Failed to create branch ${branchName}: ${error.message}${CONFIG.colors.reset}`);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Merge one branch into another
|
|
97
|
+
*/
|
|
98
|
+
async mergeBranch(sourceBranch, targetBranch) {
|
|
99
|
+
try {
|
|
100
|
+
console.log(`${CONFIG.colors.blue}Merging ${sourceBranch} → ${targetBranch}${CONFIG.colors.reset}`);
|
|
101
|
+
|
|
102
|
+
// Ensure target branch exists
|
|
103
|
+
await this.ensureBranch(targetBranch);
|
|
104
|
+
|
|
105
|
+
// Checkout target branch
|
|
106
|
+
execSync(`git checkout ${targetBranch}`, { stdio: 'ignore' });
|
|
107
|
+
|
|
108
|
+
// Attempt merge
|
|
109
|
+
execSync(`git merge --no-ff ${sourceBranch} -m "Merge session branch ${sourceBranch}"`, { stdio: 'ignore' });
|
|
110
|
+
|
|
111
|
+
// Push the merge
|
|
112
|
+
execSync(`git push origin ${targetBranch}`, { stdio: 'ignore' });
|
|
113
|
+
|
|
114
|
+
console.log(`${CONFIG.colors.green}✓ Successfully merged ${sourceBranch} → ${targetBranch}${CONFIG.colors.reset}`);
|
|
115
|
+
return true;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error(`${CONFIG.colors.red}✗ Failed to merge ${sourceBranch} → ${targetBranch}${CONFIG.colors.reset}`);
|
|
118
|
+
console.error(`${CONFIG.colors.dim}Error: ${error.message}${CONFIG.colors.reset}`);
|
|
119
|
+
|
|
120
|
+
// Reset any partial merge state
|
|
121
|
+
try {
|
|
122
|
+
execSync('git merge --abort', { stdio: 'ignore' });
|
|
123
|
+
} catch {}
|
|
124
|
+
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Perform dual merge: session → daily AND session → target
|
|
131
|
+
*/
|
|
132
|
+
async performDualMerge(sessionData) {
|
|
133
|
+
const sessionBranch = sessionData.branchName;
|
|
134
|
+
const dailyBranch = this.getDailyBranch();
|
|
135
|
+
const targetBranch = this.projectSettings?.branchManagement?.defaultMergeTarget || 'main';
|
|
136
|
+
const mergeStrategy = this.projectSettings?.branchManagement?.mergeStrategy || 'hierarchical-first';
|
|
137
|
+
|
|
138
|
+
console.log(`\n${CONFIG.colors.bright}Performing dual merge with strategy: ${mergeStrategy}${CONFIG.colors.reset}`);
|
|
139
|
+
console.log(`Session: ${sessionBranch}`);
|
|
140
|
+
console.log(`Daily: ${dailyBranch}`);
|
|
141
|
+
console.log(`Target: ${targetBranch}\n`);
|
|
142
|
+
|
|
143
|
+
let dailySuccess = false;
|
|
144
|
+
let targetSuccess = false;
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
if (mergeStrategy === 'hierarchical-first') {
|
|
148
|
+
// Merge to daily first, then to target
|
|
149
|
+
dailySuccess = await this.mergeBranch(sessionBranch, dailyBranch);
|
|
150
|
+
if (dailySuccess) {
|
|
151
|
+
targetSuccess = await this.mergeBranch(sessionBranch, targetBranch);
|
|
152
|
+
}
|
|
153
|
+
} else if (mergeStrategy === 'target-first') {
|
|
154
|
+
// Merge to target first, then to daily
|
|
155
|
+
targetSuccess = await this.mergeBranch(sessionBranch, targetBranch);
|
|
156
|
+
if (targetSuccess) {
|
|
157
|
+
dailySuccess = await this.mergeBranch(sessionBranch, dailyBranch);
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
// Parallel merge (both at once)
|
|
161
|
+
dailySuccess = await this.mergeBranch(sessionBranch, dailyBranch);
|
|
162
|
+
targetSuccess = await this.mergeBranch(sessionBranch, targetBranch);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Report results
|
|
166
|
+
if (dailySuccess && targetSuccess) {
|
|
167
|
+
console.log(`${CONFIG.colors.green}✅ Dual merge completed successfully${CONFIG.colors.reset}`);
|
|
168
|
+
return true;
|
|
169
|
+
} else if (dailySuccess || targetSuccess) {
|
|
170
|
+
console.log(`${CONFIG.colors.yellow}⚠️ Partial merge completed:${CONFIG.colors.reset}`);
|
|
171
|
+
console.log(` Daily merge: ${dailySuccess ? '✓' : '✗'}`);
|
|
172
|
+
console.log(` Target merge: ${targetSuccess ? '✓' : '✗'}`);
|
|
173
|
+
return true; // Partial success is still success
|
|
174
|
+
} else {
|
|
175
|
+
console.log(`${CONFIG.colors.red}❌ Both merges failed${CONFIG.colors.reset}`);
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error(`${CONFIG.colors.red}❌ Dual merge failed: ${error.message}${CONFIG.colors.reset}`);
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Perform single merge to configured target or daily branch
|
|
186
|
+
*/
|
|
187
|
+
async performSingleMerge(sessionData) {
|
|
188
|
+
const sessionBranch = sessionData.branchName;
|
|
189
|
+
const targetBranch = this.projectSettings?.branchManagement?.defaultMergeTarget || this.getDailyBranch();
|
|
190
|
+
|
|
191
|
+
console.log(`\n${CONFIG.colors.bright}Performing single merge${CONFIG.colors.reset}`);
|
|
192
|
+
console.log(`Session: ${sessionBranch} → ${targetBranch}\n`);
|
|
193
|
+
|
|
194
|
+
return await this.mergeBranch(sessionBranch, targetBranch);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Clean up session files and worktree
|
|
199
|
+
*/
|
|
200
|
+
async cleanupSessionFiles(sessionData) {
|
|
201
|
+
const sessionId = sessionData.sessionId;
|
|
202
|
+
const branchName = sessionData.branchName;
|
|
203
|
+
const worktreePath = sessionData.worktreePath;
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
// Remove worktree if it exists
|
|
207
|
+
if (worktreePath && fs.existsSync(worktreePath)) {
|
|
208
|
+
console.log(`${CONFIG.colors.blue}Removing worktree: ${worktreePath}${CONFIG.colors.reset}`);
|
|
209
|
+
execSync(`git worktree remove --force "${worktreePath}"`, { stdio: 'ignore' });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Delete session branch
|
|
213
|
+
if (branchName && await this.branchExists(branchName)) {
|
|
214
|
+
console.log(`${CONFIG.colors.blue}Deleting session branch: ${branchName}${CONFIG.colors.reset}`);
|
|
215
|
+
execSync(`git branch -D ${branchName}`, { stdio: 'ignore' });
|
|
216
|
+
|
|
217
|
+
// Also delete remote branch if it exists
|
|
218
|
+
try {
|
|
219
|
+
execSync(`git push origin --delete ${branchName}`, { stdio: 'ignore' });
|
|
220
|
+
} catch {
|
|
221
|
+
// Remote branch might not exist, ignore
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Remove session lock file
|
|
226
|
+
const lockFile = path.join(this.locksPath, `${sessionId}.lock`);
|
|
227
|
+
if (fs.existsSync(lockFile)) {
|
|
228
|
+
console.log(`${CONFIG.colors.blue}Removing session lock file${CONFIG.colors.reset}`);
|
|
229
|
+
fs.unlinkSync(lockFile);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Remove any commit message files
|
|
233
|
+
const commitMsgFile = path.join(this.repoRoot, `.devops-commit-${sessionId}.msg`);
|
|
234
|
+
if (fs.existsSync(commitMsgFile)) {
|
|
235
|
+
fs.unlinkSync(commitMsgFile);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
console.log(`${CONFIG.colors.green}✓ Session cleanup completed${CONFIG.colors.reset}`);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.error(`${CONFIG.colors.red}✗ Session cleanup failed: ${error.message}${CONFIG.colors.reset}`);
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* List all active sessions
|
|
247
|
+
*/
|
|
248
|
+
listSessions() {
|
|
249
|
+
if (!fs.existsSync(this.locksPath)) {
|
|
250
|
+
console.log(`${CONFIG.colors.yellow}No active sessions found${CONFIG.colors.reset}`);
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const sessions = [];
|
|
255
|
+
const lockFiles = fs.readdirSync(this.locksPath).filter(f => f.endsWith('.lock'));
|
|
256
|
+
|
|
257
|
+
lockFiles.forEach(file => {
|
|
258
|
+
const lockPath = path.join(this.locksPath, file);
|
|
259
|
+
const sessionData = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
260
|
+
sessions.push(sessionData);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
return sessions;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Display sessions for selection
|
|
268
|
+
*/
|
|
269
|
+
async selectSession() {
|
|
270
|
+
const sessions = this.listSessions();
|
|
271
|
+
|
|
272
|
+
if (sessions.length === 0) {
|
|
273
|
+
console.log(`${CONFIG.colors.yellow}No active sessions to close${CONFIG.colors.reset}`);
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
console.log(`\n${CONFIG.colors.bright}Active Sessions:${CONFIG.colors.reset}\n`);
|
|
278
|
+
|
|
279
|
+
sessions.forEach((session, index) => {
|
|
280
|
+
const status = session.status === 'active' ?
|
|
281
|
+
`${CONFIG.colors.green}●${CONFIG.colors.reset}` :
|
|
282
|
+
`${CONFIG.colors.yellow}○${CONFIG.colors.reset}`;
|
|
283
|
+
|
|
284
|
+
console.log(`${status} ${CONFIG.colors.bright}${index + 1})${CONFIG.colors.reset} ${session.sessionId}`);
|
|
285
|
+
console.log(` Task: ${session.task}`);
|
|
286
|
+
console.log(` Branch: ${session.branchName}`);
|
|
287
|
+
console.log(` Created: ${session.created}`);
|
|
288
|
+
console.log();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const rl = readline.createInterface({
|
|
292
|
+
input: process.stdin,
|
|
293
|
+
output: process.stdout
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
return new Promise((resolve) => {
|
|
297
|
+
rl.question(`Select session to close (1-${sessions.length}) or 'q' to quit: `, (answer) => {
|
|
298
|
+
rl.close();
|
|
299
|
+
|
|
300
|
+
if (answer.toLowerCase() === 'q') {
|
|
301
|
+
resolve(null);
|
|
302
|
+
} else {
|
|
303
|
+
const index = parseInt(answer) - 1;
|
|
304
|
+
if (index >= 0 && index < sessions.length) {
|
|
305
|
+
resolve(sessions[index]);
|
|
306
|
+
} else {
|
|
307
|
+
console.log(`${CONFIG.colors.red}Invalid selection${CONFIG.colors.reset}`);
|
|
308
|
+
resolve(null);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Prompt for close action
|
|
317
|
+
*/
|
|
318
|
+
async promptForCloseAction() {
|
|
319
|
+
const rl = readline.createInterface({
|
|
320
|
+
input: process.stdin,
|
|
321
|
+
output: process.stdout
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
return new Promise((resolve) => {
|
|
325
|
+
console.log('\nWhat would you like to do with this session?');
|
|
326
|
+
console.log(` ${CONFIG.colors.green}(m) Merge changes and cleanup${CONFIG.colors.reset}`);
|
|
327
|
+
console.log(` ${CONFIG.colors.yellow}(k) Keep session active, just cleanup worktree${CONFIG.colors.reset}`);
|
|
328
|
+
console.log(` ${CONFIG.colors.red}(d) Delete session and all changes${CONFIG.colors.reset}`);
|
|
329
|
+
console.log(` ${CONFIG.colors.dim}(c) Cancel${CONFIG.colors.reset}`);
|
|
330
|
+
|
|
331
|
+
rl.question('\nChoose action (m/k/d/c): ', (answer) => {
|
|
332
|
+
rl.close();
|
|
333
|
+
const action = answer.toLowerCase();
|
|
334
|
+
if (['m', 'k', 'd', 'c'].includes(action)) {
|
|
335
|
+
resolve(action);
|
|
336
|
+
} else {
|
|
337
|
+
console.log('Invalid choice, defaulting to cancel');
|
|
338
|
+
resolve('c');
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Close a session with the specified action
|
|
346
|
+
*/
|
|
347
|
+
async closeSession(sessionData) {
|
|
348
|
+
console.log(`\n${CONFIG.colors.bright}Closing session: ${sessionData.sessionId}${CONFIG.colors.reset}`);
|
|
349
|
+
console.log(`Task: ${sessionData.task}`);
|
|
350
|
+
console.log(`Branch: ${sessionData.branchName}`);
|
|
351
|
+
|
|
352
|
+
const action = await this.promptForCloseAction();
|
|
353
|
+
|
|
354
|
+
switch (action) {
|
|
355
|
+
case 'm':
|
|
356
|
+
await this.mergeAndCleanup(sessionData);
|
|
357
|
+
break;
|
|
358
|
+
case 'k':
|
|
359
|
+
await this.keepSessionCleanupWorktree(sessionData);
|
|
360
|
+
break;
|
|
361
|
+
case 'd':
|
|
362
|
+
await this.deleteSession(sessionData);
|
|
363
|
+
break;
|
|
364
|
+
case 'c':
|
|
365
|
+
console.log('Session close cancelled');
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Merge session and cleanup
|
|
372
|
+
*/
|
|
373
|
+
async mergeAndCleanup(sessionData) {
|
|
374
|
+
console.log(`\n${CONFIG.colors.bright}Merging and cleaning up session...${CONFIG.colors.reset}`);
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
const enableDualMerge = this.projectSettings?.branchManagement?.enableDualMerge;
|
|
378
|
+
const targetBranch = this.projectSettings?.branchManagement?.defaultMergeTarget;
|
|
379
|
+
|
|
380
|
+
let mergeSuccess = false;
|
|
381
|
+
|
|
382
|
+
if (enableDualMerge && targetBranch) {
|
|
383
|
+
console.log(`${CONFIG.colors.blue}Dual merge enabled${CONFIG.colors.reset}`);
|
|
384
|
+
mergeSuccess = await this.performDualMerge(sessionData);
|
|
385
|
+
} else {
|
|
386
|
+
console.log(`${CONFIG.colors.blue}Single merge mode${CONFIG.colors.reset}`);
|
|
387
|
+
mergeSuccess = await this.performSingleMerge(sessionData);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (mergeSuccess) {
|
|
391
|
+
await this.cleanupSessionFiles(sessionData);
|
|
392
|
+
console.log(`\n${CONFIG.colors.green}✅ Session successfully merged and cleaned up${CONFIG.colors.reset}`);
|
|
393
|
+
} else {
|
|
394
|
+
console.log(`\n${CONFIG.colors.red}❌ Merge failed. Session preserved for manual resolution.${CONFIG.colors.reset}`);
|
|
395
|
+
console.log(`${CONFIG.colors.dim}You can manually resolve conflicts and try again later.${CONFIG.colors.reset}`);
|
|
396
|
+
}
|
|
397
|
+
} catch (error) {
|
|
398
|
+
console.error(`\n${CONFIG.colors.red}❌ Error during merge and cleanup: ${error.message}${CONFIG.colors.reset}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Keep session active but cleanup worktree
|
|
404
|
+
*/
|
|
405
|
+
async keepSessionCleanupWorktree(sessionData) {
|
|
406
|
+
console.log(`\n${CONFIG.colors.bright}Keeping session active, cleaning up worktree...${CONFIG.colors.reset}`);
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
// Only remove worktree, keep branch and session
|
|
410
|
+
if (sessionData.worktreePath && fs.existsSync(sessionData.worktreePath)) {
|
|
411
|
+
console.log(`${CONFIG.colors.blue}Removing worktree: ${sessionData.worktreePath}${CONFIG.colors.reset}`);
|
|
412
|
+
execSync(`git worktree remove --force "${sessionData.worktreePath}"`, { stdio: 'ignore' });
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Update session status
|
|
416
|
+
const lockFile = path.join(this.locksPath, `${sessionData.sessionId}.lock`);
|
|
417
|
+
if (fs.existsSync(lockFile)) {
|
|
418
|
+
sessionData.status = 'inactive';
|
|
419
|
+
sessionData.worktreePath = null;
|
|
420
|
+
sessionData.inactiveAt = new Date().toISOString();
|
|
421
|
+
fs.writeFileSync(lockFile, JSON.stringify(sessionData, null, 2));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
console.log(`\n${CONFIG.colors.green}✅ Session marked as inactive, worktree cleaned up${CONFIG.colors.reset}`);
|
|
425
|
+
console.log(`${CONFIG.colors.dim}Session branch ${sessionData.branchName} is preserved${CONFIG.colors.reset}`);
|
|
426
|
+
} catch (error) {
|
|
427
|
+
console.error(`\n${CONFIG.colors.red}❌ Error during worktree cleanup: ${error.message}${CONFIG.colors.reset}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Delete session completely
|
|
433
|
+
*/
|
|
434
|
+
async deleteSession(sessionData) {
|
|
435
|
+
console.log(`\n${CONFIG.colors.bright}Deleting session completely...${CONFIG.colors.reset}`);
|
|
436
|
+
console.log(`${CONFIG.colors.red}⚠️ This will permanently delete all work in this session!${CONFIG.colors.reset}`);
|
|
437
|
+
|
|
438
|
+
const rl = readline.createInterface({
|
|
439
|
+
input: process.stdin,
|
|
440
|
+
output: process.stdout
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
const confirmed = await new Promise((resolve) => {
|
|
444
|
+
rl.question('Are you sure? Type "DELETE" to confirm: ', (answer) => {
|
|
445
|
+
rl.close();
|
|
446
|
+
resolve(answer === 'DELETE');
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
if (!confirmed) {
|
|
451
|
+
console.log('Deletion cancelled');
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
try {
|
|
456
|
+
await this.cleanupSessionFiles(sessionData);
|
|
457
|
+
console.log(`\n${CONFIG.colors.green}✅ Session completely deleted${CONFIG.colors.reset}`);
|
|
458
|
+
} catch (error) {
|
|
459
|
+
console.error(`\n${CONFIG.colors.red}❌ Error during session deletion: ${error.message}${CONFIG.colors.reset}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Main execution function
|
|
465
|
+
*/
|
|
466
|
+
async run() {
|
|
467
|
+
console.log(`\n${CONFIG.colors.bright}${CONFIG.colors.blue}DevOps Agent - Enhanced Session Closer${CONFIG.colors.reset}`);
|
|
468
|
+
console.log(`${CONFIG.colors.dim}Repository: ${this.repoRoot}${CONFIG.colors.reset}\n`);
|
|
469
|
+
|
|
470
|
+
// Show current configuration
|
|
471
|
+
const enableDualMerge = this.projectSettings?.branchManagement?.enableDualMerge;
|
|
472
|
+
const targetBranch = this.projectSettings?.branchManagement?.defaultMergeTarget;
|
|
473
|
+
|
|
474
|
+
console.log(`${CONFIG.colors.bright}Current Configuration:${CONFIG.colors.reset}`);
|
|
475
|
+
console.log(` Dual merge: ${enableDualMerge ? CONFIG.colors.green + 'enabled' : CONFIG.colors.yellow + 'disabled'}${CONFIG.colors.reset}`);
|
|
476
|
+
console.log(` Target branch: ${targetBranch || CONFIG.colors.dim + 'not set'}${CONFIG.colors.reset}`);
|
|
477
|
+
console.log(` Daily branch: ${this.getDailyBranch()}`);
|
|
478
|
+
|
|
479
|
+
const sessionData = await this.selectSession();
|
|
480
|
+
if (sessionData) {
|
|
481
|
+
await this.closeSession(sessionData);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// CLI execution
|
|
487
|
+
if (require.main === module) {
|
|
488
|
+
const closer = new EnhancedSessionCloser();
|
|
489
|
+
closer.run().catch(console.error);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
module.exports = EnhancedSessionCloser;
|