s9n-devops-agent 1.1.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/README.md +30 -5
- package/package.json +1 -1
- package/src/cs-devops-agent-worker.js +118 -11
- package/src/docker-utils.js +290 -0
- package/src/session-coordinator.js +354 -19
- package/start-devops-session.sh +6 -11
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ An intelligent Git automation system with multi-agent support, session managemen
|
|
|
23
23
|
- **⚙️ VS Code Integration**: Seamlessly integrates with VS Code tasks
|
|
24
24
|
- **🔐 Safe Concurrent Development**: Multiple agents can work simultaneously without conflicts
|
|
25
25
|
- **🏷️ Smart Branching**: Automatic branch creation with configurable naming patterns
|
|
26
|
+
- **🐋 Docker Auto-Restart**: Automatically restart Docker containers after code push (v1.2.0)
|
|
26
27
|
|
|
27
28
|
## Installation 📦
|
|
28
29
|
|
|
@@ -96,9 +97,9 @@ s9n-devops-agent start
|
|
|
96
97
|
s9n-devops-agent create --task "implement-api"
|
|
97
98
|
```
|
|
98
99
|
|
|
99
|
-
### Working with AI
|
|
100
|
+
### Working with AI Assistants
|
|
100
101
|
|
|
101
|
-
When you start a session, you'll receive instructions to paste into your AI
|
|
102
|
+
When you start a session, you'll receive instructions to paste into your AI assistant:
|
|
102
103
|
|
|
103
104
|
```
|
|
104
105
|
I'm working in a DevOps-managed session with the following setup:
|
|
@@ -186,9 +187,9 @@ Configuration is stored in:
|
|
|
186
187
|
# Enter task: implement-authentication
|
|
187
188
|
```
|
|
188
189
|
|
|
189
|
-
2. **Copy Instructions to AI
|
|
190
|
-
- The tool provides instructions to paste into
|
|
191
|
-
- The AI
|
|
190
|
+
2. **Copy Instructions to AI Assistant**:
|
|
191
|
+
- The tool provides instructions to paste into Claude/Copilot
|
|
192
|
+
- The AI works in the isolated worktree
|
|
192
193
|
|
|
193
194
|
3. **AI Makes Changes**:
|
|
194
195
|
- AI writes code in the worktree directory
|
|
@@ -226,6 +227,30 @@ Add to `.vscode/tasks.json`:
|
|
|
226
227
|
|
|
227
228
|
## Advanced Features 🔬
|
|
228
229
|
|
|
230
|
+
### Docker Container Auto-Restart (v1.2.0+)
|
|
231
|
+
|
|
232
|
+
Automatically restart Docker containers after pushing code changes:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
# During session creation, if docker-compose is detected:
|
|
236
|
+
s9n-devops-agent start
|
|
237
|
+
# -> Auto-restart Docker containers after push? (y/N): y
|
|
238
|
+
# -> Rebuild containers on restart? (y/N): n
|
|
239
|
+
# -> Specific service to restart (leave empty for all): app
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Features:**
|
|
243
|
+
- Detects docker-compose files automatically
|
|
244
|
+
- Optional container rebuild on restart
|
|
245
|
+
- Target specific services or restart all
|
|
246
|
+
- Non-blocking: Docker failures don't affect git workflow
|
|
247
|
+
- Works with Docker Compose v1 and v2
|
|
248
|
+
|
|
249
|
+
**Supported docker-compose files:**
|
|
250
|
+
- docker-compose.yml / docker-compose.yaml
|
|
251
|
+
- compose.yml / compose.yaml
|
|
252
|
+
- docker-compose.dev.yml / docker-compose.local.yml
|
|
253
|
+
|
|
229
254
|
### Multi-Agent Collaboration
|
|
230
255
|
|
|
231
256
|
Multiple AI assistants can work on different features simultaneously:
|
package/package.json
CHANGED
|
@@ -113,6 +113,8 @@ 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';
|
|
117
|
+
import { restartDockerContainers } from './docker-utils.js';
|
|
116
118
|
|
|
117
119
|
// ============================================================================
|
|
118
120
|
// CONFIGURATION SECTION - All settings can be overridden via environment vars
|
|
@@ -762,7 +764,12 @@ async function commitOnce(repoRoot, msgPath) {
|
|
|
762
764
|
if (PUSH) {
|
|
763
765
|
const ok = await pushBranch(BRANCH);
|
|
764
766
|
log(`push ${ok ? "ok" : "failed"}`);
|
|
765
|
-
if (ok
|
|
767
|
+
if (ok) {
|
|
768
|
+
if (CLEAR_MSG_WHEN === "push") clearMsgFile(msgPath);
|
|
769
|
+
|
|
770
|
+
// Handle Docker restart if configured
|
|
771
|
+
await handleDockerRestart();
|
|
772
|
+
}
|
|
766
773
|
}
|
|
767
774
|
} finally {
|
|
768
775
|
busy = false;
|
|
@@ -782,6 +789,52 @@ function schedule(repoRoot, msgPath) {
|
|
|
782
789
|
}, QUIET_MS);
|
|
783
790
|
}
|
|
784
791
|
|
|
792
|
+
// ============================================================================
|
|
793
|
+
// DOCKER CONTAINER MANAGEMENT
|
|
794
|
+
// ============================================================================
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Handle Docker container restart if configured in session
|
|
798
|
+
*/
|
|
799
|
+
async function handleDockerRestart() {
|
|
800
|
+
// Check if we're in a session with Docker configuration
|
|
801
|
+
const sessionConfigPath = path.join(process.cwd(), '.devops-session.json');
|
|
802
|
+
if (!fs.existsSync(sessionConfigPath)) {
|
|
803
|
+
return; // Not in a managed session
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
try {
|
|
807
|
+
const sessionConfig = JSON.parse(fs.readFileSync(sessionConfigPath, 'utf8'));
|
|
808
|
+
|
|
809
|
+
// Check if Docker restart is enabled
|
|
810
|
+
if (!sessionConfig.dockerConfig || !sessionConfig.dockerConfig.enabled) {
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
log('🐋 Docker restart configured - initiating container restart...');
|
|
815
|
+
|
|
816
|
+
const dockerOptions = {
|
|
817
|
+
projectPath: process.cwd(),
|
|
818
|
+
composeFile: sessionConfig.dockerConfig.composeFile,
|
|
819
|
+
serviceName: sessionConfig.dockerConfig.service,
|
|
820
|
+
rebuild: sessionConfig.dockerConfig.rebuild,
|
|
821
|
+
forceRecreate: sessionConfig.dockerConfig.forceRecreate || false,
|
|
822
|
+
detach: true
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
const result = await restartDockerContainers(dockerOptions);
|
|
826
|
+
|
|
827
|
+
if (result.success) {
|
|
828
|
+
log('✅ Docker containers restarted successfully');
|
|
829
|
+
} else {
|
|
830
|
+
log(`⚠️ Docker restart failed: ${result.error}`);
|
|
831
|
+
}
|
|
832
|
+
} catch (error) {
|
|
833
|
+
// Don't fail the commit/push if Docker restart fails
|
|
834
|
+
dlog('Docker restart error:', error.message);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
785
838
|
// ============================================================================
|
|
786
839
|
// INFRASTRUCTURE CHANGE DETECTION
|
|
787
840
|
// ============================================================================
|
|
@@ -1604,17 +1657,71 @@ console.log();
|
|
|
1604
1657
|
fs.unlinkSync(msgPath);
|
|
1605
1658
|
}
|
|
1606
1659
|
|
|
1607
|
-
//
|
|
1660
|
+
// Handle worktree cleanup if in a session
|
|
1608
1661
|
if (sessionId) {
|
|
1609
|
-
const
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
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
|
+
}
|
|
1618
1725
|
}
|
|
1619
1726
|
|
|
1620
1727
|
console.log("\nGoodbye!");
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* DOCKER UTILITIES FOR DEVOPS AGENT
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* PURPOSE:
|
|
7
|
+
* Provides Docker container management functionality for automatic restarts
|
|
8
|
+
* after code pushes. Integrates with the DevOps agent session workflow.
|
|
9
|
+
*
|
|
10
|
+
* ============================================================================
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { execa } from 'execa';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check if Docker is installed and running
|
|
19
|
+
*/
|
|
20
|
+
export async function isDockerAvailable() {
|
|
21
|
+
try {
|
|
22
|
+
await execa('docker', ['version'], { stdio: 'pipe' });
|
|
23
|
+
return true;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if docker-compose is available
|
|
31
|
+
*/
|
|
32
|
+
export async function isDockerComposeAvailable() {
|
|
33
|
+
try {
|
|
34
|
+
// Try docker compose (v2)
|
|
35
|
+
await execa('docker', ['compose', 'version'], { stdio: 'pipe' });
|
|
36
|
+
return 'docker compose';
|
|
37
|
+
} catch {
|
|
38
|
+
try {
|
|
39
|
+
// Try docker-compose (v1)
|
|
40
|
+
await execa('docker-compose', ['version'], { stdio: 'pipe' });
|
|
41
|
+
return 'docker-compose';
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Find docker-compose files in the project
|
|
50
|
+
*/
|
|
51
|
+
export function findDockerComposeFiles(projectPath) {
|
|
52
|
+
const possibleFiles = [
|
|
53
|
+
'docker-compose.yml',
|
|
54
|
+
'docker-compose.yaml',
|
|
55
|
+
'compose.yml',
|
|
56
|
+
'compose.yaml',
|
|
57
|
+
'docker-compose.dev.yml',
|
|
58
|
+
'docker-compose.dev.yaml',
|
|
59
|
+
'docker-compose.local.yml',
|
|
60
|
+
'docker-compose.local.yaml'
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const found = [];
|
|
64
|
+
for (const file of possibleFiles) {
|
|
65
|
+
const filePath = path.join(projectPath, file);
|
|
66
|
+
if (fs.existsSync(filePath)) {
|
|
67
|
+
found.push({
|
|
68
|
+
name: file,
|
|
69
|
+
path: filePath
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return found;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if project has Docker configuration
|
|
79
|
+
*/
|
|
80
|
+
export function hasDockerConfiguration(projectPath) {
|
|
81
|
+
const composeFiles = findDockerComposeFiles(projectPath);
|
|
82
|
+
const dockerfilePath = path.join(projectPath, 'Dockerfile');
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
hasCompose: composeFiles.length > 0,
|
|
86
|
+
hasDockerfile: fs.existsSync(dockerfilePath),
|
|
87
|
+
composeFiles: composeFiles
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Restart Docker containers
|
|
93
|
+
*/
|
|
94
|
+
export async function restartDockerContainers(options = {}) {
|
|
95
|
+
const {
|
|
96
|
+
projectPath = process.cwd(),
|
|
97
|
+
composeFile = null,
|
|
98
|
+
serviceName = null,
|
|
99
|
+
forceRecreate = false,
|
|
100
|
+
rebuild = false,
|
|
101
|
+
detach = true
|
|
102
|
+
} = options;
|
|
103
|
+
|
|
104
|
+
console.log('\n🐋 Docker Container Restart');
|
|
105
|
+
console.log('━'.repeat(50));
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
// Check if Docker is available
|
|
109
|
+
const dockerCmd = await isDockerComposeAvailable();
|
|
110
|
+
if (!dockerCmd) {
|
|
111
|
+
throw new Error('Docker Compose is not available');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Determine compose file to use
|
|
115
|
+
let composeFilePath = composeFile;
|
|
116
|
+
if (!composeFilePath) {
|
|
117
|
+
const dockerConfig = hasDockerConfiguration(projectPath);
|
|
118
|
+
if (dockerConfig.hasCompose && dockerConfig.composeFiles.length > 0) {
|
|
119
|
+
composeFilePath = dockerConfig.composeFiles[0].path;
|
|
120
|
+
} else {
|
|
121
|
+
throw new Error('No docker-compose file found');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Build command arguments
|
|
126
|
+
const isV2 = dockerCmd === 'docker compose';
|
|
127
|
+
const baseCmd = isV2 ? ['docker', 'compose'] : ['docker-compose'];
|
|
128
|
+
const fileArgs = ['-f', composeFilePath];
|
|
129
|
+
|
|
130
|
+
// Stop containers
|
|
131
|
+
console.log('📦 Stopping containers...');
|
|
132
|
+
const stopArgs = [...fileArgs, 'stop'];
|
|
133
|
+
if (serviceName) stopArgs.push(serviceName);
|
|
134
|
+
|
|
135
|
+
const stopResult = await execa(baseCmd[0], [...baseCmd.slice(1), ...stopArgs], {
|
|
136
|
+
cwd: projectPath,
|
|
137
|
+
stdio: 'inherit'
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Optionally rebuild
|
|
141
|
+
if (rebuild) {
|
|
142
|
+
console.log('🔨 Rebuilding containers...');
|
|
143
|
+
const buildArgs = [...fileArgs, 'build'];
|
|
144
|
+
if (serviceName) buildArgs.push(serviceName);
|
|
145
|
+
|
|
146
|
+
await execa(baseCmd[0], [...baseCmd.slice(1), ...buildArgs], {
|
|
147
|
+
cwd: projectPath,
|
|
148
|
+
stdio: 'inherit'
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Start containers
|
|
153
|
+
console.log('🚀 Starting containers...');
|
|
154
|
+
const upArgs = [...fileArgs, 'up'];
|
|
155
|
+
if (forceRecreate) upArgs.push('--force-recreate');
|
|
156
|
+
if (detach) upArgs.push('-d');
|
|
157
|
+
if (serviceName) upArgs.push(serviceName);
|
|
158
|
+
|
|
159
|
+
await execa(baseCmd[0], [...baseCmd.slice(1), ...upArgs], {
|
|
160
|
+
cwd: projectPath,
|
|
161
|
+
stdio: 'inherit'
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
console.log('✅ Docker containers restarted successfully');
|
|
165
|
+
console.log('━'.repeat(50));
|
|
166
|
+
return { success: true };
|
|
167
|
+
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error('❌ Failed to restart Docker containers:', error.message);
|
|
170
|
+
console.log('━'.repeat(50));
|
|
171
|
+
return { success: false, error: error.message };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get Docker container status
|
|
177
|
+
*/
|
|
178
|
+
export async function getDockerStatus(projectPath = process.cwd(), composeFile = null) {
|
|
179
|
+
try {
|
|
180
|
+
const dockerCmd = await isDockerComposeAvailable();
|
|
181
|
+
if (!dockerCmd) {
|
|
182
|
+
return { running: false, services: [] };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let composeFilePath = composeFile;
|
|
186
|
+
if (!composeFilePath) {
|
|
187
|
+
const dockerConfig = hasDockerConfiguration(projectPath);
|
|
188
|
+
if (dockerConfig.hasCompose && dockerConfig.composeFiles.length > 0) {
|
|
189
|
+
composeFilePath = dockerConfig.composeFiles[0].path;
|
|
190
|
+
} else {
|
|
191
|
+
return { running: false, services: [] };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const isV2 = dockerCmd === 'docker compose';
|
|
196
|
+
const baseCmd = isV2 ? ['docker', 'compose'] : ['docker-compose'];
|
|
197
|
+
const psArgs = ['-f', composeFilePath, 'ps', '--format', 'json'];
|
|
198
|
+
|
|
199
|
+
const result = await execa(baseCmd[0], [...baseCmd.slice(1), ...psArgs], {
|
|
200
|
+
cwd: projectPath,
|
|
201
|
+
stdio: 'pipe'
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Parse the JSON output
|
|
205
|
+
const services = [];
|
|
206
|
+
if (result.stdout) {
|
|
207
|
+
const lines = result.stdout.split('\n').filter(line => line.trim());
|
|
208
|
+
for (const line of lines) {
|
|
209
|
+
try {
|
|
210
|
+
const service = JSON.parse(line);
|
|
211
|
+
services.push({
|
|
212
|
+
name: service.Service || service.Name,
|
|
213
|
+
state: service.State,
|
|
214
|
+
status: service.Status,
|
|
215
|
+
ports: service.Ports
|
|
216
|
+
});
|
|
217
|
+
} catch {
|
|
218
|
+
// Skip unparseable lines
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
running: services.some(s => s.state === 'running'),
|
|
225
|
+
services: services
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
} catch (error) {
|
|
229
|
+
return { running: false, services: [], error: error.message };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Monitor Docker logs
|
|
235
|
+
*/
|
|
236
|
+
export async function tailDockerLogs(options = {}) {
|
|
237
|
+
const {
|
|
238
|
+
projectPath = process.cwd(),
|
|
239
|
+
composeFile = null,
|
|
240
|
+
serviceName = null,
|
|
241
|
+
lines = 50,
|
|
242
|
+
follow = true
|
|
243
|
+
} = options;
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const dockerCmd = await isDockerComposeAvailable();
|
|
247
|
+
if (!dockerCmd) {
|
|
248
|
+
throw new Error('Docker Compose is not available');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let composeFilePath = composeFile;
|
|
252
|
+
if (!composeFilePath) {
|
|
253
|
+
const dockerConfig = hasDockerConfiguration(projectPath);
|
|
254
|
+
if (dockerConfig.hasCompose && dockerConfig.composeFiles.length > 0) {
|
|
255
|
+
composeFilePath = dockerConfig.composeFiles[0].path;
|
|
256
|
+
} else {
|
|
257
|
+
throw new Error('No docker-compose file found');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const isV2 = dockerCmd === 'docker compose';
|
|
262
|
+
const baseCmd = isV2 ? ['docker', 'compose'] : ['docker-compose'];
|
|
263
|
+
const logsArgs = ['-f', composeFilePath, 'logs'];
|
|
264
|
+
|
|
265
|
+
if (lines) logsArgs.push('--tail', lines.toString());
|
|
266
|
+
if (follow) logsArgs.push('-f');
|
|
267
|
+
if (serviceName) logsArgs.push(serviceName);
|
|
268
|
+
|
|
269
|
+
const proc = execa(baseCmd[0], [...baseCmd.slice(1), ...logsArgs], {
|
|
270
|
+
cwd: projectPath,
|
|
271
|
+
stdio: 'inherit'
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
return proc;
|
|
275
|
+
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.error('Failed to tail Docker logs:', error.message);
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export default {
|
|
283
|
+
isDockerAvailable,
|
|
284
|
+
isDockerComposeAvailable,
|
|
285
|
+
findDockerComposeFiles,
|
|
286
|
+
hasDockerConfiguration,
|
|
287
|
+
restartDockerContainers,
|
|
288
|
+
getDockerStatus,
|
|
289
|
+
tailDockerLogs
|
|
290
|
+
};
|
|
@@ -19,11 +19,12 @@
|
|
|
19
19
|
|
|
20
20
|
import fs from 'fs';
|
|
21
21
|
import path from 'path';
|
|
22
|
-
import { execSync, spawn, fork } from 'child_process';
|
|
23
22
|
import { fileURLToPath } from 'url';
|
|
24
23
|
import { dirname } from 'path';
|
|
24
|
+
import { execSync, spawn, fork } from 'child_process';
|
|
25
25
|
import crypto from 'crypto';
|
|
26
26
|
import readline from 'readline';
|
|
27
|
+
import { hasDockerConfiguration } from './docker-utils.js';
|
|
27
28
|
|
|
28
29
|
const __filename = fileURLToPath(import.meta.url);
|
|
29
30
|
const __dirname = dirname(__filename);
|
|
@@ -367,7 +368,91 @@ class SessionCoordinator {
|
|
|
367
368
|
}
|
|
368
369
|
|
|
369
370
|
/**
|
|
370
|
-
* Prompt for
|
|
371
|
+
* Prompt for Docker restart configuration
|
|
372
|
+
*/
|
|
373
|
+
async promptForDockerConfig() {
|
|
374
|
+
const rl = readline.createInterface({
|
|
375
|
+
input: process.stdin,
|
|
376
|
+
output: process.stdout
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Ask if they want automatic Docker restarts
|
|
380
|
+
const autoRestart = await new Promise((resolve) => {
|
|
381
|
+
rl.question('\nAuto-restart Docker containers after push? (y/N): ', (answer) => {
|
|
382
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
if (!autoRestart) {
|
|
387
|
+
rl.close();
|
|
388
|
+
return { enabled: false };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Ask which compose file to use if multiple
|
|
392
|
+
const dockerInfo = hasDockerConfiguration(process.cwd());
|
|
393
|
+
let selectedComposeFile = null;
|
|
394
|
+
|
|
395
|
+
if (dockerInfo.composeFiles.length > 1) {
|
|
396
|
+
console.log(`\n${CONFIG.colors.bright}Select docker-compose file:${CONFIG.colors.reset}`);
|
|
397
|
+
dockerInfo.composeFiles.forEach((file, index) => {
|
|
398
|
+
console.log(` ${index + 1}) ${file.name}`);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
const fileChoice = await new Promise((resolve) => {
|
|
402
|
+
rl.question(`Choose file (1-${dockerInfo.composeFiles.length}) [1]: `, (answer) => {
|
|
403
|
+
const choice = parseInt(answer) || 1;
|
|
404
|
+
if (choice >= 1 && choice <= dockerInfo.composeFiles.length) {
|
|
405
|
+
resolve(dockerInfo.composeFiles[choice - 1]);
|
|
406
|
+
} else {
|
|
407
|
+
resolve(dockerInfo.composeFiles[0]);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
selectedComposeFile = fileChoice.path;
|
|
413
|
+
} else if (dockerInfo.composeFiles.length === 1) {
|
|
414
|
+
selectedComposeFile = dockerInfo.composeFiles[0].path;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Ask about rebuild preference
|
|
418
|
+
const rebuild = await new Promise((resolve) => {
|
|
419
|
+
rl.question('\nRebuild containers on restart? (y/N): ', (answer) => {
|
|
420
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// Ask about specific service
|
|
425
|
+
const specificService = await new Promise((resolve) => {
|
|
426
|
+
rl.question('\nSpecific service to restart (leave empty for all): ', (answer) => {
|
|
427
|
+
resolve(answer.trim() || null);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
rl.close();
|
|
432
|
+
|
|
433
|
+
const config = {
|
|
434
|
+
enabled: true,
|
|
435
|
+
composeFile: selectedComposeFile,
|
|
436
|
+
rebuild: rebuild,
|
|
437
|
+
service: specificService,
|
|
438
|
+
forceRecreate: false
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
console.log(`\n${CONFIG.colors.green}✓${CONFIG.colors.reset} Docker restart configuration:`);
|
|
442
|
+
console.log(` ${CONFIG.colors.bright}Auto-restart:${CONFIG.colors.reset} Enabled`);
|
|
443
|
+
if (selectedComposeFile) {
|
|
444
|
+
console.log(` ${CONFIG.colors.bright}Compose file:${CONFIG.colors.reset} ${path.basename(selectedComposeFile)}`);
|
|
445
|
+
}
|
|
446
|
+
console.log(` ${CONFIG.colors.bright}Rebuild:${CONFIG.colors.reset} ${rebuild ? 'Yes' : 'No'}`);
|
|
447
|
+
if (specificService) {
|
|
448
|
+
console.log(` ${CONFIG.colors.bright}Service:${CONFIG.colors.reset} ${specificService}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return config;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Prompt for auto-merge configuration
|
|
371
456
|
*/
|
|
372
457
|
async promptForMergeConfig() {
|
|
373
458
|
const rl = readline.createInterface({
|
|
@@ -593,6 +678,27 @@ class SessionCoordinator {
|
|
|
593
678
|
// Ask for auto-merge configuration
|
|
594
679
|
const mergeConfig = await this.promptForMergeConfig();
|
|
595
680
|
|
|
681
|
+
// Check for Docker configuration and ask about restart preference
|
|
682
|
+
let dockerConfig = null;
|
|
683
|
+
const dockerInfo = hasDockerConfiguration(process.cwd());
|
|
684
|
+
|
|
685
|
+
if (dockerInfo.hasCompose || dockerInfo.hasDockerfile) {
|
|
686
|
+
console.log(`\n${CONFIG.colors.yellow}Docker Configuration Detected${CONFIG.colors.reset}`);
|
|
687
|
+
|
|
688
|
+
if (dockerInfo.hasCompose) {
|
|
689
|
+
console.log(`${CONFIG.colors.dim}Found docker-compose files:${CONFIG.colors.reset}`);
|
|
690
|
+
dockerInfo.composeFiles.forEach(file => {
|
|
691
|
+
console.log(` • ${file.name}`);
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (dockerInfo.hasDockerfile) {
|
|
696
|
+
console.log(`${CONFIG.colors.dim}Found Dockerfile${CONFIG.colors.reset}`);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
dockerConfig = await this.promptForDockerConfig();
|
|
700
|
+
}
|
|
701
|
+
|
|
596
702
|
// Create worktree with developer initials in the name
|
|
597
703
|
const worktreeName = `${agentType}-${devInitials}-${sessionId}-${task.replace(/\s+/g, '-')}`;
|
|
598
704
|
const worktreePath = path.join(this.worktreesPath, worktreeName);
|
|
@@ -615,7 +721,8 @@ class SessionCoordinator {
|
|
|
615
721
|
status: 'active',
|
|
616
722
|
pid: process.pid,
|
|
617
723
|
developerInitials: devInitials,
|
|
618
|
-
mergeConfig: mergeConfig
|
|
724
|
+
mergeConfig: mergeConfig,
|
|
725
|
+
dockerConfig: dockerConfig
|
|
619
726
|
};
|
|
620
727
|
|
|
621
728
|
const lockFile = path.join(this.locksPath, `${sessionId}.lock`);
|
|
@@ -629,7 +736,7 @@ class SessionCoordinator {
|
|
|
629
736
|
fs.writeFileSync(instructionsFile, instructions.markdown);
|
|
630
737
|
|
|
631
738
|
// Display instructions
|
|
632
|
-
this.displayInstructions(instructions, sessionId, task
|
|
739
|
+
this.displayInstructions(instructions, sessionId, task);
|
|
633
740
|
|
|
634
741
|
// Create session config in worktree
|
|
635
742
|
this.createWorktreeConfig(worktreePath, lockData);
|
|
@@ -650,7 +757,7 @@ class SessionCoordinator {
|
|
|
650
757
|
}
|
|
651
758
|
|
|
652
759
|
/**
|
|
653
|
-
* Generate instructions for
|
|
760
|
+
* Generate instructions for Claude/Cline
|
|
654
761
|
*/
|
|
655
762
|
generateClaudeInstructions(sessionData) {
|
|
656
763
|
const { sessionId, worktreePath, branchName, task } = sessionData;
|
|
@@ -677,7 +784,7 @@ INSTRUCTIONS:
|
|
|
677
784
|
- **Worktree Path:** \`${worktreePath}\`
|
|
678
785
|
- **Branch:** \`${branchName}\`
|
|
679
786
|
|
|
680
|
-
## Instructions for
|
|
787
|
+
## Instructions for Claude/Cline
|
|
681
788
|
|
|
682
789
|
### Step 1: Navigate to Your Worktree
|
|
683
790
|
\`\`\`bash
|
|
@@ -731,13 +838,12 @@ The DevOps agent will automatically:
|
|
|
731
838
|
/**
|
|
732
839
|
* Display instructions in a user-friendly format
|
|
733
840
|
*/
|
|
734
|
-
displayInstructions(instructions, sessionId, task
|
|
735
|
-
|
|
736
|
-
console.log(`\n${CONFIG.colors.bgGreen}${CONFIG.colors.bright} Instructions for ${agentName} ${CONFIG.colors.reset}\n`);
|
|
841
|
+
displayInstructions(instructions, sessionId, task) {
|
|
842
|
+
console.log(`\n${CONFIG.colors.bgGreen}${CONFIG.colors.bright} Instructions for Claude/Cline ${CONFIG.colors.reset}\n`);
|
|
737
843
|
|
|
738
844
|
// Clean separator
|
|
739
845
|
console.log(`${CONFIG.colors.yellow}══════════════════════════════════════════════════════════════${CONFIG.colors.reset}`);
|
|
740
|
-
console.log(`${CONFIG.colors.bright}COPY AND PASTE THIS ENTIRE BLOCK INTO
|
|
846
|
+
console.log(`${CONFIG.colors.bright}COPY AND PASTE THIS ENTIRE BLOCK INTO CLAUDE BEFORE YOUR PROMPT:${CONFIG.colors.reset}`);
|
|
741
847
|
console.log(`${CONFIG.colors.yellow}──────────────────────────────────────────────────────────────${CONFIG.colors.reset}`);
|
|
742
848
|
console.log();
|
|
743
849
|
|
|
@@ -1079,6 +1185,215 @@ The DevOps agent is monitoring this worktree for changes.
|
|
|
1079
1185
|
|
|
1080
1186
|
return session;
|
|
1081
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
|
+
}
|
|
1082
1397
|
}
|
|
1083
1398
|
|
|
1084
1399
|
// ============================================================================
|
|
@@ -1154,13 +1469,31 @@ async function main() {
|
|
|
1154
1469
|
break;
|
|
1155
1470
|
}
|
|
1156
1471
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
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();
|
|
1160
1485
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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: {
|
|
1164
1497
|
console.log(`
|
|
1165
1498
|
${CONFIG.colors.bright}DevOps Session Coordinator${CONFIG.colors.reset}
|
|
1166
1499
|
|
|
@@ -1173,6 +1506,8 @@ ${CONFIG.colors.blue}Commands:${CONFIG.colors.reset}
|
|
|
1173
1506
|
${CONFIG.colors.green}create-and-start${CONFIG.colors.reset} Create session and start agent (all-in-one)
|
|
1174
1507
|
${CONFIG.colors.green}request [agent]${CONFIG.colors.reset} Request a session (for Claude to call)
|
|
1175
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
|
|
1176
1511
|
${CONFIG.colors.green}help${CONFIG.colors.reset} Show this help
|
|
1177
1512
|
|
|
1178
1513
|
${CONFIG.colors.blue}Options:${CONFIG.colors.reset}
|
|
@@ -1193,9 +1528,9 @@ ${CONFIG.colors.blue}Examples:${CONFIG.colors.reset}
|
|
|
1193
1528
|
|
|
1194
1529
|
${CONFIG.colors.yellow}Typical Workflow:${CONFIG.colors.reset}
|
|
1195
1530
|
1. Run: ${CONFIG.colors.green}node session-coordinator.js create-and-start${CONFIG.colors.reset}
|
|
1196
|
-
2. Copy the displayed instructions to
|
|
1197
|
-
3.
|
|
1198
|
-
4.
|
|
1531
|
+
2. Copy the displayed instructions to Claude/Cline
|
|
1532
|
+
3. Claude navigates to the worktree and starts working
|
|
1533
|
+
4. Agent automatically commits and pushes changes
|
|
1199
1534
|
`);
|
|
1200
1535
|
}
|
|
1201
1536
|
}
|
package/start-devops-session.sh
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# This script provides a user-friendly way to start DevOps agent sessions.
|
|
8
8
|
# It handles the complete workflow:
|
|
9
9
|
# 1. Ask if using existing session or creating new
|
|
10
|
-
# 2. If new, creates session and generates instructions for
|
|
10
|
+
# 2. If new, creates session and generates instructions for Claude
|
|
11
11
|
# 3. Starts the DevOps agent monitoring the appropriate worktree
|
|
12
12
|
#
|
|
13
13
|
# ============================================================================
|
|
@@ -62,13 +62,12 @@ display_instructions() {
|
|
|
62
62
|
local worktree_path="$2"
|
|
63
63
|
local branch_name="$3"
|
|
64
64
|
local task="$4"
|
|
65
|
-
local agent_type="${5:-AI Development Agent}"
|
|
66
65
|
|
|
67
66
|
echo
|
|
68
|
-
echo -e "${BG_GREEN}${BOLD} Instructions for
|
|
67
|
+
echo -e "${BG_GREEN}${BOLD} Instructions for Claude/Cline ${NC}"
|
|
69
68
|
echo
|
|
70
69
|
echo -e "${YELLOW}══════════════════════════════════════════════════════════════${NC}"
|
|
71
|
-
echo -e "${BOLD}COPY AND PASTE THIS ENTIRE BLOCK INTO
|
|
70
|
+
echo -e "${BOLD}COPY AND PASTE THIS ENTIRE BLOCK INTO CLAUDE BEFORE YOUR PROMPT:${NC}"
|
|
72
71
|
echo -e "${YELLOW}──────────────────────────────────────────────────────────────${NC}"
|
|
73
72
|
echo
|
|
74
73
|
echo "I'm working in a DevOps-managed session with the following setup:"
|
|
@@ -216,12 +215,8 @@ select_session() {
|
|
|
216
215
|
echo
|
|
217
216
|
echo -e "${GREEN}Using existing session: ${session_id}${NC}"
|
|
218
217
|
|
|
219
|
-
#
|
|
220
|
-
|
|
221
|
-
[[ -z "$agent_type" ]] && agent_type="AI Agent"
|
|
222
|
-
|
|
223
|
-
# Display instructions for the AI agent IMMEDIATELY after selection
|
|
224
|
-
display_instructions "$session_id" "$worktree_path" "$branch_name" "$task" "$agent_type"
|
|
218
|
+
# Display instructions for Claude/Cline IMMEDIATELY after selection
|
|
219
|
+
display_instructions "$session_id" "$worktree_path" "$branch_name" "$task"
|
|
225
220
|
|
|
226
221
|
# Add a pause and visual separator before starting the agent
|
|
227
222
|
echo -e "${DIM}Press Enter to start the DevOps agent monitoring...${NC}"
|
|
@@ -265,7 +260,7 @@ main() {
|
|
|
265
260
|
echo
|
|
266
261
|
echo "This tool will:"
|
|
267
262
|
echo " 1. Help you create or select a session"
|
|
268
|
-
echo " 2. Generate instructions for
|
|
263
|
+
echo " 2. Generate instructions for Claude/Cline"
|
|
269
264
|
echo " 3. Start the DevOps agent to monitor changes"
|
|
270
265
|
echo
|
|
271
266
|
|