s9n-devops-agent 1.0.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 +25 -0
- 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 +112 -5
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
|
|
|
@@ -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`);
|