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 +1 -1
- package/src/cs-devops-agent-worker.js +65 -10
- package/src/session-coordinator.js +238 -9
package/package.json
CHANGED
|
@@ -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
|
-
//
|
|
1660
|
+
// Handle worktree cleanup if in a session
|
|
1660
1661
|
if (sessionId) {
|
|
1661
|
-
const
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
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 {
|
|
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
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
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
|
-
|
|
1269
|
-
|
|
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}
|