s9n-devops-agent 1.2.0 → 1.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "s9n-devops-agent",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
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",
@@ -113,6 +113,7 @@ import chokidar from "chokidar";
113
113
  import { execa } from "execa";
114
114
  import readline from "node:readline";
115
115
  import { stdin as input, stdout as output } from 'node:process';
116
+ import { execSync } from 'child_process';
116
117
  import { restartDockerContainers } from './docker-utils.js';
117
118
 
118
119
  // ============================================================================
@@ -1656,17 +1657,71 @@ console.log();
1656
1657
  fs.unlinkSync(msgPath);
1657
1658
  }
1658
1659
 
1659
- // Create cleanup marker if in a session
1660
+ // Handle worktree cleanup if in a session
1660
1661
  if (sessionId) {
1661
- const cleanupMarker = path.join(process.cwd(), '.session-cleanup-requested');
1662
- fs.writeFileSync(cleanupMarker, JSON.stringify({
1663
- sessionId: sessionId,
1664
- timestamp: new Date().toISOString(),
1665
- branch: await currentBranch(),
1666
- worktree: process.cwd()
1667
- }, null, 2));
1668
- console.log("\n✓ Session cleanup complete.");
1669
- console.log("Run 'npm run devops:close' from the main repo to remove the worktree.");
1662
+ const currentDir = process.cwd();
1663
+ const isWorktree = currentDir.includes('local_deploy/worktrees');
1664
+
1665
+ if (isWorktree) {
1666
+ console.log("\n" + "=".repeat(60));
1667
+ console.log("WORKTREE CLEANUP");
1668
+ console.log("=".repeat(60));
1669
+ console.log("\nThis session is running in a worktree:");
1670
+ console.log(` ${currentDir}`);
1671
+ console.log("\nWould you like to remove this worktree now?");
1672
+ console.log(" y/yes - Remove worktree and close session");
1673
+ console.log(" n/no - Keep worktree for later use");
1674
+ console.log(" (You can remove it later with: node src/session-coordinator.js close " + sessionId + ")");
1675
+
1676
+ rl.prompt();
1677
+ const cleanupAnswer = await new Promise(resolve => {
1678
+ rl.once('line', resolve);
1679
+ });
1680
+
1681
+ if (cleanupAnswer.toLowerCase() === 'y' || cleanupAnswer.toLowerCase() === 'yes') {
1682
+ console.log("\nRemoving worktree...");
1683
+
1684
+ // Get the main repo root (parent of worktree)
1685
+ const repoRoot = path.resolve(currentDir, '../../../');
1686
+
1687
+ try {
1688
+ // Switch to main repo directory first
1689
+ process.chdir(repoRoot);
1690
+
1691
+ // Remove the worktree
1692
+ execSync(`git worktree remove "${currentDir}" --force`, { stdio: 'inherit' });
1693
+ console.log("✓ Worktree removed successfully");
1694
+
1695
+ // Remove session lock file
1696
+ const lockFile = path.join(repoRoot, 'local_deploy', 'session-locks', `${sessionId}.lock`);
1697
+ if (fs.existsSync(lockFile)) {
1698
+ fs.unlinkSync(lockFile);
1699
+ console.log("✓ Session closed");
1700
+ }
1701
+ } catch (err) {
1702
+ console.error("\nCould not remove worktree automatically.");
1703
+ console.error("Error: " + err.message);
1704
+ console.log("\nTo remove it manually, run from the main repo:");
1705
+ console.log(` git worktree remove "${currentDir}" --force`);
1706
+ console.log(" OR");
1707
+ console.log(` node src/session-coordinator.js close ${sessionId}`);
1708
+ }
1709
+ } else {
1710
+ // Create cleanup marker for later
1711
+ const cleanupMarker = path.join(currentDir, '.session-cleanup-requested');
1712
+ fs.writeFileSync(cleanupMarker, JSON.stringify({
1713
+ sessionId: sessionId,
1714
+ timestamp: new Date().toISOString(),
1715
+ branch: await currentBranch(),
1716
+ worktree: currentDir
1717
+ }, null, 2));
1718
+ console.log("\n✓ Session marked for later cleanup.");
1719
+ console.log("To remove the worktree later, run from the main repo:");
1720
+ console.log(` node src/session-coordinator.js close ${sessionId}`);
1721
+ }
1722
+ } else {
1723
+ console.log("\n✓ Session cleanup complete.");
1724
+ }
1670
1725
  }
1671
1726
 
1672
1727
  console.log("\nGoodbye!");
@@ -19,12 +19,12 @@
19
19
 
20
20
  import fs from 'fs';
21
21
  import path from 'path';
22
- import { execSync, spawn } from 'child_process';
22
+ import { fileURLToPath } from 'url';
23
+ import { dirname } from 'path';
24
+ import { execSync, spawn, fork } from 'child_process';
23
25
  import crypto from 'crypto';
24
26
  import readline from 'readline';
25
27
  import { hasDockerConfiguration } from './docker-utils.js';
26
- import crypto from 'crypto';
27
- import readline from 'readline';
28
28
 
29
29
  const __filename = fileURLToPath(import.meta.url);
30
30
  const __dirname = dirname(__filename);
@@ -1185,6 +1185,215 @@ The DevOps agent is monitoring this worktree for changes.
1185
1185
 
1186
1186
  return session;
1187
1187
  }
1188
+
1189
+ /**
1190
+ * Close a specific session
1191
+ */
1192
+ async closeSession(sessionId) {
1193
+ const lockFile = path.join(this.locksPath, `${sessionId}.lock`);
1194
+
1195
+ if (!fs.existsSync(lockFile)) {
1196
+ console.error(`${CONFIG.colors.red}Session not found: ${sessionId}${CONFIG.colors.reset}`);
1197
+ return false;
1198
+ }
1199
+
1200
+ const session = JSON.parse(fs.readFileSync(lockFile, 'utf8'));
1201
+ console.log(`\n${CONFIG.colors.yellow}Closing session: ${sessionId}${CONFIG.colors.reset}`);
1202
+ console.log(`${CONFIG.colors.dim}Task: ${session.task}${CONFIG.colors.reset}`);
1203
+ console.log(`${CONFIG.colors.dim}Branch: ${session.branchName}${CONFIG.colors.reset}`);
1204
+
1205
+ // Kill agent if running
1206
+ if (session.agentPid) {
1207
+ try {
1208
+ process.kill(session.agentPid, 'SIGTERM');
1209
+ console.log(`${CONFIG.colors.green}✓${CONFIG.colors.reset} Agent process stopped`);
1210
+ } catch (err) {
1211
+ // Process might already be dead
1212
+ }
1213
+ }
1214
+
1215
+ // Check for uncommitted changes
1216
+ if (fs.existsSync(session.worktreePath)) {
1217
+ try {
1218
+ const status = execSync(`git -C "${session.worktreePath}" status --porcelain`, { encoding: 'utf8' });
1219
+ if (status.trim()) {
1220
+ console.log(`\n${CONFIG.colors.yellow}Warning: Uncommitted changes found${CONFIG.colors.reset}`);
1221
+ console.log(status);
1222
+
1223
+ const rl = readline.createInterface({
1224
+ input: process.stdin,
1225
+ output: process.stdout
1226
+ });
1227
+
1228
+ const answer = await new Promise(resolve => {
1229
+ rl.question('Commit these changes before closing? (y/N): ', resolve);
1230
+ });
1231
+ rl.close();
1232
+
1233
+ if (answer.toLowerCase() === 'y') {
1234
+ execSync(`git -C "${session.worktreePath}" add -A`, { stdio: 'pipe' });
1235
+ execSync(`git -C "${session.worktreePath}" commit -m "chore: final session cleanup for ${sessionId}"`, { stdio: 'pipe' });
1236
+ execSync(`git -C "${session.worktreePath}" push origin ${session.branchName}`, { stdio: 'pipe' });
1237
+ console.log(`${CONFIG.colors.green}✓${CONFIG.colors.reset} Changes committed and pushed`);
1238
+ }
1239
+ }
1240
+ } catch (err) {
1241
+ console.log(`${CONFIG.colors.dim}Could not check git status${CONFIG.colors.reset}`);
1242
+ }
1243
+
1244
+ // Ask about removing worktree
1245
+ const rl = readline.createInterface({
1246
+ input: process.stdin,
1247
+ output: process.stdout
1248
+ });
1249
+
1250
+ const removeWorktree = await new Promise(resolve => {
1251
+ rl.question(`\nRemove worktree at ${session.worktreePath}? (Y/n): `, resolve);
1252
+ });
1253
+ rl.close();
1254
+
1255
+ if (removeWorktree.toLowerCase() !== 'n') {
1256
+ try {
1257
+ // Remove worktree
1258
+ execSync(`git worktree remove "${session.worktreePath}" --force`, { stdio: 'pipe' });
1259
+ console.log(`${CONFIG.colors.green}✓${CONFIG.colors.reset} Worktree removed`);
1260
+
1261
+ // Prune worktree list
1262
+ execSync('git worktree prune', { stdio: 'pipe' });
1263
+ } catch (err) {
1264
+ console.error(`${CONFIG.colors.red}Failed to remove worktree: ${err.message}${CONFIG.colors.reset}`);
1265
+ }
1266
+ }
1267
+ }
1268
+
1269
+ // Remove lock file
1270
+ fs.unlinkSync(lockFile);
1271
+ console.log(`${CONFIG.colors.green}✓${CONFIG.colors.reset} Session closed successfully`);
1272
+
1273
+ return true;
1274
+ }
1275
+
1276
+ /**
1277
+ * Interactive session selection and close
1278
+ */
1279
+ async selectAndCloseSession() {
1280
+ if (!fs.existsSync(this.locksPath)) {
1281
+ console.log(`${CONFIG.colors.yellow}No active sessions${CONFIG.colors.reset}`);
1282
+ return;
1283
+ }
1284
+
1285
+ const locks = fs.readdirSync(this.locksPath);
1286
+ if (locks.length === 0) {
1287
+ console.log(`${CONFIG.colors.yellow}No active sessions${CONFIG.colors.reset}`);
1288
+ return;
1289
+ }
1290
+
1291
+ const sessions = [];
1292
+ locks.forEach(lockFile => {
1293
+ const lockPath = path.join(this.locksPath, lockFile);
1294
+ const session = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
1295
+ sessions.push(session);
1296
+ });
1297
+
1298
+ console.log(`\n${CONFIG.colors.bright}Select session to close:${CONFIG.colors.reset}\n`);
1299
+
1300
+ sessions.forEach((session, index) => {
1301
+ const status = session.status === 'active' ?
1302
+ `${CONFIG.colors.green}●${CONFIG.colors.reset}` :
1303
+ `${CONFIG.colors.yellow}○${CONFIG.colors.reset}`;
1304
+
1305
+ console.log(`${status} ${CONFIG.colors.bright}${index + 1})${CONFIG.colors.reset} ${session.sessionId}`);
1306
+ console.log(` Task: ${session.task}`);
1307
+ console.log(` Branch: ${session.branchName}`);
1308
+ console.log(` Created: ${session.created}`);
1309
+ console.log();
1310
+ });
1311
+
1312
+ const rl = readline.createInterface({
1313
+ input: process.stdin,
1314
+ output: process.stdout
1315
+ });
1316
+
1317
+ const answer = await new Promise(resolve => {
1318
+ rl.question(`Select session (1-${sessions.length}) or 'q' to quit: `, resolve);
1319
+ });
1320
+ rl.close();
1321
+
1322
+ if (answer.toLowerCase() === 'q') {
1323
+ return;
1324
+ }
1325
+
1326
+ const index = parseInt(answer) - 1;
1327
+ if (index >= 0 && index < sessions.length) {
1328
+ await this.closeSession(sessions[index].sessionId);
1329
+ } else {
1330
+ console.log(`${CONFIG.colors.red}Invalid selection${CONFIG.colors.reset}`);
1331
+ }
1332
+ }
1333
+
1334
+ /**
1335
+ * Clean up all stale sessions and worktrees
1336
+ */
1337
+ async cleanupAll() {
1338
+ console.log(`\n${CONFIG.colors.yellow}Cleaning up stale sessions and worktrees...${CONFIG.colors.reset}`);
1339
+
1340
+ // Clean up old lock files (older than 24 hours)
1341
+ const oneDayAgo = Date.now() - 86400000;
1342
+ let cleanedLocks = 0;
1343
+
1344
+ if (fs.existsSync(this.locksPath)) {
1345
+ const locks = fs.readdirSync(this.locksPath);
1346
+ locks.forEach(lockFile => {
1347
+ const lockPath = path.join(this.locksPath, lockFile);
1348
+ const stats = fs.statSync(lockPath);
1349
+ if (stats.mtimeMs < oneDayAgo) {
1350
+ fs.unlinkSync(lockPath);
1351
+ cleanedLocks++;
1352
+ }
1353
+ });
1354
+ }
1355
+
1356
+ if (cleanedLocks > 0) {
1357
+ console.log(`${CONFIG.colors.green}✓${CONFIG.colors.reset} Removed ${cleanedLocks} stale lock files`);
1358
+ }
1359
+
1360
+ // Prune git worktrees
1361
+ try {
1362
+ execSync('git worktree prune', { stdio: 'pipe' });
1363
+ console.log(`${CONFIG.colors.green}✓${CONFIG.colors.reset} Pruned git worktrees`);
1364
+ } catch (err) {
1365
+ console.log(`${CONFIG.colors.dim}Could not prune worktrees${CONFIG.colors.reset}`);
1366
+ }
1367
+
1368
+ // Clean up orphaned worktree directories
1369
+ if (fs.existsSync(this.worktreesPath)) {
1370
+ const worktrees = fs.readdirSync(this.worktreesPath);
1371
+ let cleanedWorktrees = 0;
1372
+
1373
+ for (const dir of worktrees) {
1374
+ const worktreePath = path.join(this.worktreesPath, dir);
1375
+
1376
+ // Check if this worktree is still valid
1377
+ try {
1378
+ execSync(`git worktree list | grep "${worktreePath}"`, { stdio: 'pipe' });
1379
+ } catch (err) {
1380
+ // Worktree not in git list, it's orphaned
1381
+ try {
1382
+ fs.rmSync(worktreePath, { recursive: true, force: true });
1383
+ cleanedWorktrees++;
1384
+ } catch (err) {
1385
+ console.log(`${CONFIG.colors.dim}Could not remove ${dir}${CONFIG.colors.reset}`);
1386
+ }
1387
+ }
1388
+ }
1389
+
1390
+ if (cleanedWorktrees > 0) {
1391
+ console.log(`${CONFIG.colors.green}✓${CONFIG.colors.reset} Removed ${cleanedWorktrees} orphaned worktree directories`);
1392
+ }
1393
+ }
1394
+
1395
+ console.log(`${CONFIG.colors.green}✓${CONFIG.colors.reset} Cleanup complete`);
1396
+ }
1188
1397
  }
1189
1398
 
1190
1399
  // ============================================================================
@@ -1260,13 +1469,31 @@ async function main() {
1260
1469
  break;
1261
1470
  }
1262
1471
 
1263
- case 'list': {
1264
- coordinator.listSessions();
1265
- break;
1472
+ case 'list': {
1473
+ coordinator.listSessions();
1474
+ break;
1475
+ }
1476
+
1477
+ case 'close': {
1478
+ // Close a session and clean up
1479
+ const sessionId = args[1];
1480
+ if (sessionId) {
1481
+ await coordinator.closeSession(sessionId);
1482
+ } else {
1483
+ // Interactive selection
1484
+ await coordinator.selectAndCloseSession();
1266
1485
  }
1267
-
1268
- case 'help':
1269
- default: {
1486
+ break;
1487
+ }
1488
+
1489
+ case 'cleanup': {
1490
+ // Clean up stale sessions and worktrees
1491
+ await coordinator.cleanupAll();
1492
+ break;
1493
+ }
1494
+
1495
+ case 'help':
1496
+ default: {
1270
1497
  console.log(`
1271
1498
  ${CONFIG.colors.bright}DevOps Session Coordinator${CONFIG.colors.reset}
1272
1499
 
@@ -1279,6 +1506,8 @@ ${CONFIG.colors.blue}Commands:${CONFIG.colors.reset}
1279
1506
  ${CONFIG.colors.green}create-and-start${CONFIG.colors.reset} Create session and start agent (all-in-one)
1280
1507
  ${CONFIG.colors.green}request [agent]${CONFIG.colors.reset} Request a session (for Claude to call)
1281
1508
  ${CONFIG.colors.green}list${CONFIG.colors.reset} List all active sessions
1509
+ ${CONFIG.colors.green}close [id]${CONFIG.colors.reset} Close session and clean up worktree
1510
+ ${CONFIG.colors.green}cleanup${CONFIG.colors.reset} Clean up all stale sessions
1282
1511
  ${CONFIG.colors.green}help${CONFIG.colors.reset} Show this help
1283
1512
 
1284
1513
  ${CONFIG.colors.blue}Options:${CONFIG.colors.reset}