s9n-devops-agent 1.1.0 → 1.2.0
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 +53 -1
- package/src/docker-utils.js +290 -0
- package/src/session-coordinator.js +121 -15
- 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,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 { restartDockerContainers } from './docker-utils.js';
|
|
116
117
|
|
|
117
118
|
// ============================================================================
|
|
118
119
|
// CONFIGURATION SECTION - All settings can be overridden via environment vars
|
|
@@ -762,7 +763,12 @@ async function commitOnce(repoRoot, msgPath) {
|
|
|
762
763
|
if (PUSH) {
|
|
763
764
|
const ok = await pushBranch(BRANCH);
|
|
764
765
|
log(`push ${ok ? "ok" : "failed"}`);
|
|
765
|
-
if (ok
|
|
766
|
+
if (ok) {
|
|
767
|
+
if (CLEAR_MSG_WHEN === "push") clearMsgFile(msgPath);
|
|
768
|
+
|
|
769
|
+
// Handle Docker restart if configured
|
|
770
|
+
await handleDockerRestart();
|
|
771
|
+
}
|
|
766
772
|
}
|
|
767
773
|
} finally {
|
|
768
774
|
busy = false;
|
|
@@ -782,6 +788,52 @@ function schedule(repoRoot, msgPath) {
|
|
|
782
788
|
}, QUIET_MS);
|
|
783
789
|
}
|
|
784
790
|
|
|
791
|
+
// ============================================================================
|
|
792
|
+
// DOCKER CONTAINER MANAGEMENT
|
|
793
|
+
// ============================================================================
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Handle Docker container restart if configured in session
|
|
797
|
+
*/
|
|
798
|
+
async function handleDockerRestart() {
|
|
799
|
+
// Check if we're in a session with Docker configuration
|
|
800
|
+
const sessionConfigPath = path.join(process.cwd(), '.devops-session.json');
|
|
801
|
+
if (!fs.existsSync(sessionConfigPath)) {
|
|
802
|
+
return; // Not in a managed session
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
try {
|
|
806
|
+
const sessionConfig = JSON.parse(fs.readFileSync(sessionConfigPath, 'utf8'));
|
|
807
|
+
|
|
808
|
+
// Check if Docker restart is enabled
|
|
809
|
+
if (!sessionConfig.dockerConfig || !sessionConfig.dockerConfig.enabled) {
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
log('🐋 Docker restart configured - initiating container restart...');
|
|
814
|
+
|
|
815
|
+
const dockerOptions = {
|
|
816
|
+
projectPath: process.cwd(),
|
|
817
|
+
composeFile: sessionConfig.dockerConfig.composeFile,
|
|
818
|
+
serviceName: sessionConfig.dockerConfig.service,
|
|
819
|
+
rebuild: sessionConfig.dockerConfig.rebuild,
|
|
820
|
+
forceRecreate: sessionConfig.dockerConfig.forceRecreate || false,
|
|
821
|
+
detach: true
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
const result = await restartDockerContainers(dockerOptions);
|
|
825
|
+
|
|
826
|
+
if (result.success) {
|
|
827
|
+
log('✅ Docker containers restarted successfully');
|
|
828
|
+
} else {
|
|
829
|
+
log(`⚠️ Docker restart failed: ${result.error}`);
|
|
830
|
+
}
|
|
831
|
+
} catch (error) {
|
|
832
|
+
// Don't fail the commit/push if Docker restart fails
|
|
833
|
+
dlog('Docker restart error:', error.message);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
785
837
|
// ============================================================================
|
|
786
838
|
// INFRASTRUCTURE CHANGE DETECTION
|
|
787
839
|
// ============================================================================
|
|
@@ -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,9 +19,10 @@
|
|
|
19
19
|
|
|
20
20
|
import fs from 'fs';
|
|
21
21
|
import path from 'path';
|
|
22
|
-
import { execSync, spawn
|
|
23
|
-
import
|
|
24
|
-
import
|
|
22
|
+
import { execSync, spawn } from 'child_process';
|
|
23
|
+
import crypto from 'crypto';
|
|
24
|
+
import readline from 'readline';
|
|
25
|
+
import { hasDockerConfiguration } from './docker-utils.js';
|
|
25
26
|
import crypto from 'crypto';
|
|
26
27
|
import readline from 'readline';
|
|
27
28
|
|
|
@@ -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
|
|
|
@@ -1193,9 +1299,9 @@ ${CONFIG.colors.blue}Examples:${CONFIG.colors.reset}
|
|
|
1193
1299
|
|
|
1194
1300
|
${CONFIG.colors.yellow}Typical Workflow:${CONFIG.colors.reset}
|
|
1195
1301
|
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.
|
|
1302
|
+
2. Copy the displayed instructions to Claude/Cline
|
|
1303
|
+
3. Claude navigates to the worktree and starts working
|
|
1304
|
+
4. Agent automatically commits and pushes changes
|
|
1199
1305
|
`);
|
|
1200
1306
|
}
|
|
1201
1307
|
}
|
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
|
|