start-command 0.7.6 → 0.9.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/ARCHITECTURE.md +297 -0
- package/CHANGELOG.md +10 -0
- package/README.md +28 -7
- package/REQUIREMENTS.md +26 -1
- package/package.json +1 -1
- package/src/bin/cli.js +8 -0
- package/src/lib/args-parser.js +33 -5
- package/src/lib/isolation.js +97 -12
- package/test/args-parser.test.js +130 -0
- package/test/docker-autoremove.test.js +169 -0
- package/test/isolation-cleanup.test.js +377 -0
- package/test/isolation.test.js +200 -0
package/src/lib/isolation.js
CHANGED
|
@@ -299,7 +299,7 @@ function runScreenWithLogCapture(command, sessionName, shellInfo) {
|
|
|
299
299
|
/**
|
|
300
300
|
* Run command in GNU Screen
|
|
301
301
|
* @param {string} command - Command to execute
|
|
302
|
-
* @param {object} options - Options (session, detached)
|
|
302
|
+
* @param {object} options - Options (session, detached, keepAlive)
|
|
303
303
|
* @returns {Promise<{success: boolean, sessionName: string, message: string}>}
|
|
304
304
|
*/
|
|
305
305
|
function runInScreen(command, options = {}) {
|
|
@@ -319,10 +319,28 @@ function runInScreen(command, options = {}) {
|
|
|
319
319
|
try {
|
|
320
320
|
if (options.detached) {
|
|
321
321
|
// Detached mode: screen -dmS <session> <shell> -c '<command>'
|
|
322
|
-
|
|
322
|
+
// By default (keepAlive=false), the session will exit after command completes
|
|
323
|
+
// With keepAlive=true, we start a shell that runs the command but stays alive
|
|
324
|
+
let effectiveCommand = command;
|
|
325
|
+
|
|
326
|
+
if (options.keepAlive) {
|
|
327
|
+
// With keep-alive: run command, then keep shell open
|
|
328
|
+
// Use exec to replace the shell, but first run command
|
|
329
|
+
effectiveCommand = `${command}; exec ${shell}`;
|
|
330
|
+
}
|
|
331
|
+
// Without keep-alive: command runs and session exits naturally when done
|
|
332
|
+
|
|
333
|
+
const screenArgs = [
|
|
334
|
+
'-dmS',
|
|
335
|
+
sessionName,
|
|
336
|
+
shell,
|
|
337
|
+
shellArg,
|
|
338
|
+
effectiveCommand,
|
|
339
|
+
];
|
|
323
340
|
|
|
324
341
|
if (DEBUG) {
|
|
325
342
|
console.log(`[DEBUG] Running: screen ${screenArgs.join(' ')}`);
|
|
343
|
+
console.log(`[DEBUG] keepAlive: ${options.keepAlive || false}`);
|
|
326
344
|
}
|
|
327
345
|
|
|
328
346
|
// Use spawnSync with array arguments to avoid shell quoting issues
|
|
@@ -336,10 +354,18 @@ function runInScreen(command, options = {}) {
|
|
|
336
354
|
throw result.error;
|
|
337
355
|
}
|
|
338
356
|
|
|
357
|
+
let message = `Command started in detached screen session: ${sessionName}`;
|
|
358
|
+
if (options.keepAlive) {
|
|
359
|
+
message += `\nSession will stay alive after command completes.`;
|
|
360
|
+
} else {
|
|
361
|
+
message += `\nSession will exit automatically after command completes.`;
|
|
362
|
+
}
|
|
363
|
+
message += `\nReattach with: screen -r ${sessionName}`;
|
|
364
|
+
|
|
339
365
|
return Promise.resolve({
|
|
340
366
|
success: true,
|
|
341
367
|
sessionName,
|
|
342
|
-
message
|
|
368
|
+
message,
|
|
343
369
|
});
|
|
344
370
|
} else {
|
|
345
371
|
// Attached mode: always use detached mode with log capture
|
|
@@ -371,7 +397,7 @@ function runInScreen(command, options = {}) {
|
|
|
371
397
|
/**
|
|
372
398
|
* Run command in tmux
|
|
373
399
|
* @param {string} command - Command to execute
|
|
374
|
-
* @param {object} options - Options (session, detached)
|
|
400
|
+
* @param {object} options - Options (session, detached, keepAlive)
|
|
375
401
|
* @returns {Promise<{success: boolean, sessionName: string, message: string}>}
|
|
376
402
|
*/
|
|
377
403
|
function runInTmux(command, options = {}) {
|
|
@@ -385,24 +411,48 @@ function runInTmux(command, options = {}) {
|
|
|
385
411
|
}
|
|
386
412
|
|
|
387
413
|
const sessionName = options.session || generateSessionName('tmux');
|
|
414
|
+
const shellInfo = getShell();
|
|
415
|
+
const { shell } = shellInfo;
|
|
388
416
|
|
|
389
417
|
try {
|
|
390
418
|
if (options.detached) {
|
|
391
419
|
// Detached mode: tmux new-session -d -s <session> '<command>'
|
|
420
|
+
// By default (keepAlive=false), the session will exit after command completes
|
|
421
|
+
// With keepAlive=true, we keep the shell alive after the command
|
|
422
|
+
let effectiveCommand = command;
|
|
423
|
+
|
|
424
|
+
if (options.keepAlive) {
|
|
425
|
+
// With keep-alive: run command, then keep shell open
|
|
426
|
+
effectiveCommand = `${command}; exec ${shell}`;
|
|
427
|
+
}
|
|
428
|
+
// Without keep-alive: command runs and session exits naturally when done
|
|
429
|
+
|
|
392
430
|
if (DEBUG) {
|
|
393
431
|
console.log(
|
|
394
|
-
`[DEBUG] Running: tmux new-session -d -s "${sessionName}" "${
|
|
432
|
+
`[DEBUG] Running: tmux new-session -d -s "${sessionName}" "${effectiveCommand}"`
|
|
395
433
|
);
|
|
434
|
+
console.log(`[DEBUG] keepAlive: ${options.keepAlive || false}`);
|
|
396
435
|
}
|
|
397
436
|
|
|
398
|
-
execSync(
|
|
399
|
-
|
|
400
|
-
|
|
437
|
+
execSync(
|
|
438
|
+
`tmux new-session -d -s "${sessionName}" "${effectiveCommand}"`,
|
|
439
|
+
{
|
|
440
|
+
stdio: 'inherit',
|
|
441
|
+
}
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
let message = `Command started in detached tmux session: ${sessionName}`;
|
|
445
|
+
if (options.keepAlive) {
|
|
446
|
+
message += `\nSession will stay alive after command completes.`;
|
|
447
|
+
} else {
|
|
448
|
+
message += `\nSession will exit automatically after command completes.`;
|
|
449
|
+
}
|
|
450
|
+
message += `\nReattach with: tmux attach -t ${sessionName}`;
|
|
401
451
|
|
|
402
452
|
return Promise.resolve({
|
|
403
453
|
success: true,
|
|
404
454
|
sessionName,
|
|
405
|
-
message
|
|
455
|
+
message,
|
|
406
456
|
});
|
|
407
457
|
} else {
|
|
408
458
|
// Attached mode: tmux new-session -s <session> '<command>'
|
|
@@ -451,7 +501,7 @@ function runInTmux(command, options = {}) {
|
|
|
451
501
|
/**
|
|
452
502
|
* Run command in Docker container
|
|
453
503
|
* @param {string} command - Command to execute
|
|
454
|
-
* @param {object} options - Options (image, session/name, detached)
|
|
504
|
+
* @param {object} options - Options (image, session/name, detached, keepAlive, autoRemoveDockerContainer)
|
|
455
505
|
* @returns {Promise<{success: boolean, containerName: string, message: string}>}
|
|
456
506
|
*/
|
|
457
507
|
function runInDocker(command, options = {}) {
|
|
@@ -477,6 +527,16 @@ function runInDocker(command, options = {}) {
|
|
|
477
527
|
try {
|
|
478
528
|
if (options.detached) {
|
|
479
529
|
// Detached mode: docker run -d --name <name> <image> <shell> -c '<command>'
|
|
530
|
+
// By default (keepAlive=false), the container exits after command completes
|
|
531
|
+
// With keepAlive=true, we keep the container running with a shell
|
|
532
|
+
let effectiveCommand = command;
|
|
533
|
+
|
|
534
|
+
if (options.keepAlive) {
|
|
535
|
+
// With keep-alive: run command, then keep shell alive
|
|
536
|
+
effectiveCommand = `${command}; exec /bin/sh`;
|
|
537
|
+
}
|
|
538
|
+
// Without keep-alive: container exits naturally when command completes
|
|
539
|
+
|
|
480
540
|
const dockerArgs = [
|
|
481
541
|
'run',
|
|
482
542
|
'-d',
|
|
@@ -485,22 +545,47 @@ function runInDocker(command, options = {}) {
|
|
|
485
545
|
options.image,
|
|
486
546
|
'/bin/sh',
|
|
487
547
|
'-c',
|
|
488
|
-
|
|
548
|
+
effectiveCommand,
|
|
489
549
|
];
|
|
490
550
|
|
|
551
|
+
// Add --rm flag if autoRemoveDockerContainer is true
|
|
552
|
+
// Note: --rm must come before the image name
|
|
553
|
+
if (options.autoRemoveDockerContainer) {
|
|
554
|
+
dockerArgs.splice(2, 0, '--rm');
|
|
555
|
+
}
|
|
556
|
+
|
|
491
557
|
if (DEBUG) {
|
|
492
558
|
console.log(`[DEBUG] Running: docker ${dockerArgs.join(' ')}`);
|
|
559
|
+
console.log(`[DEBUG] keepAlive: ${options.keepAlive || false}`);
|
|
560
|
+
console.log(
|
|
561
|
+
`[DEBUG] autoRemoveDockerContainer: ${options.autoRemoveDockerContainer || false}`
|
|
562
|
+
);
|
|
493
563
|
}
|
|
494
564
|
|
|
495
565
|
const containerId = execSync(`docker ${dockerArgs.join(' ')}`, {
|
|
496
566
|
encoding: 'utf8',
|
|
497
567
|
}).trim();
|
|
498
568
|
|
|
569
|
+
let message = `Command started in detached docker container: ${containerName}`;
|
|
570
|
+
message += `\nContainer ID: ${containerId.substring(0, 12)}`;
|
|
571
|
+
if (options.keepAlive) {
|
|
572
|
+
message += `\nContainer will stay alive after command completes.`;
|
|
573
|
+
} else {
|
|
574
|
+
message += `\nContainer will exit automatically after command completes.`;
|
|
575
|
+
}
|
|
576
|
+
if (options.autoRemoveDockerContainer) {
|
|
577
|
+
message += `\nContainer will be automatically removed after exit.`;
|
|
578
|
+
} else {
|
|
579
|
+
message += `\nContainer filesystem will be preserved after exit.`;
|
|
580
|
+
}
|
|
581
|
+
message += `\nAttach with: docker attach ${containerName}`;
|
|
582
|
+
message += `\nView logs: docker logs ${containerName}`;
|
|
583
|
+
|
|
499
584
|
return Promise.resolve({
|
|
500
585
|
success: true,
|
|
501
586
|
containerName,
|
|
502
587
|
containerId,
|
|
503
|
-
message
|
|
588
|
+
message,
|
|
504
589
|
});
|
|
505
590
|
} else {
|
|
506
591
|
// Attached mode: docker run -it --name <name> <image> <shell> -c '<command>'
|
package/test/args-parser.test.js
CHANGED
|
@@ -219,6 +219,136 @@ describe('parseArgs', () => {
|
|
|
219
219
|
});
|
|
220
220
|
});
|
|
221
221
|
|
|
222
|
+
describe('keep-alive option', () => {
|
|
223
|
+
it('should parse --keep-alive flag', () => {
|
|
224
|
+
const result = parseArgs([
|
|
225
|
+
'--isolated',
|
|
226
|
+
'tmux',
|
|
227
|
+
'--keep-alive',
|
|
228
|
+
'--',
|
|
229
|
+
'npm',
|
|
230
|
+
'test',
|
|
231
|
+
]);
|
|
232
|
+
assert.strictEqual(result.wrapperOptions.keepAlive, true);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should parse -k shorthand', () => {
|
|
236
|
+
const result = parseArgs(['-i', 'screen', '-k', '--', 'npm', 'start']);
|
|
237
|
+
assert.strictEqual(result.wrapperOptions.keepAlive, true);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should default keepAlive to false', () => {
|
|
241
|
+
const result = parseArgs(['-i', 'tmux', '--', 'npm', 'test']);
|
|
242
|
+
assert.strictEqual(result.wrapperOptions.keepAlive, false);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should throw error for keep-alive without isolation', () => {
|
|
246
|
+
assert.throws(() => {
|
|
247
|
+
parseArgs(['--keep-alive', '--', 'npm', 'test']);
|
|
248
|
+
}, /--keep-alive option is only valid with --isolated/);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should work with detached mode', () => {
|
|
252
|
+
const result = parseArgs([
|
|
253
|
+
'-i',
|
|
254
|
+
'screen',
|
|
255
|
+
'-d',
|
|
256
|
+
'-k',
|
|
257
|
+
'--',
|
|
258
|
+
'npm',
|
|
259
|
+
'start',
|
|
260
|
+
]);
|
|
261
|
+
assert.strictEqual(result.wrapperOptions.isolated, 'screen');
|
|
262
|
+
assert.strictEqual(result.wrapperOptions.detached, true);
|
|
263
|
+
assert.strictEqual(result.wrapperOptions.keepAlive, true);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('should work with docker', () => {
|
|
267
|
+
const result = parseArgs([
|
|
268
|
+
'-i',
|
|
269
|
+
'docker',
|
|
270
|
+
'--image',
|
|
271
|
+
'node:20',
|
|
272
|
+
'-k',
|
|
273
|
+
'--',
|
|
274
|
+
'npm',
|
|
275
|
+
'test',
|
|
276
|
+
]);
|
|
277
|
+
assert.strictEqual(result.wrapperOptions.isolated, 'docker');
|
|
278
|
+
assert.strictEqual(result.wrapperOptions.image, 'node:20');
|
|
279
|
+
assert.strictEqual(result.wrapperOptions.keepAlive, true);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe('auto-remove-docker-container option', () => {
|
|
284
|
+
it('should parse --auto-remove-docker-container flag', () => {
|
|
285
|
+
const result = parseArgs([
|
|
286
|
+
'--isolated',
|
|
287
|
+
'docker',
|
|
288
|
+
'--image',
|
|
289
|
+
'alpine',
|
|
290
|
+
'--auto-remove-docker-container',
|
|
291
|
+
'--',
|
|
292
|
+
'npm',
|
|
293
|
+
'test',
|
|
294
|
+
]);
|
|
295
|
+
assert.strictEqual(result.wrapperOptions.autoRemoveDockerContainer, true);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should default autoRemoveDockerContainer to false', () => {
|
|
299
|
+
const result = parseArgs([
|
|
300
|
+
'-i',
|
|
301
|
+
'docker',
|
|
302
|
+
'--image',
|
|
303
|
+
'alpine',
|
|
304
|
+
'--',
|
|
305
|
+
'npm',
|
|
306
|
+
'test',
|
|
307
|
+
]);
|
|
308
|
+
assert.strictEqual(
|
|
309
|
+
result.wrapperOptions.autoRemoveDockerContainer,
|
|
310
|
+
false
|
|
311
|
+
);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should throw error for auto-remove-docker-container without docker isolation', () => {
|
|
315
|
+
assert.throws(() => {
|
|
316
|
+
parseArgs([
|
|
317
|
+
'-i',
|
|
318
|
+
'tmux',
|
|
319
|
+
'--auto-remove-docker-container',
|
|
320
|
+
'--',
|
|
321
|
+
'npm',
|
|
322
|
+
'test',
|
|
323
|
+
]);
|
|
324
|
+
}, /--auto-remove-docker-container option is only valid with --isolated docker/);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should throw error for auto-remove-docker-container without isolation', () => {
|
|
328
|
+
assert.throws(() => {
|
|
329
|
+
parseArgs(['--auto-remove-docker-container', '--', 'npm', 'test']);
|
|
330
|
+
}, /--auto-remove-docker-container option is only valid with --isolated docker/);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('should work with keep-alive and auto-remove-docker-container', () => {
|
|
334
|
+
const result = parseArgs([
|
|
335
|
+
'-i',
|
|
336
|
+
'docker',
|
|
337
|
+
'--image',
|
|
338
|
+
'node:20',
|
|
339
|
+
'-k',
|
|
340
|
+
'--auto-remove-docker-container',
|
|
341
|
+
'--',
|
|
342
|
+
'npm',
|
|
343
|
+
'test',
|
|
344
|
+
]);
|
|
345
|
+
assert.strictEqual(result.wrapperOptions.isolated, 'docker');
|
|
346
|
+
assert.strictEqual(result.wrapperOptions.image, 'node:20');
|
|
347
|
+
assert.strictEqual(result.wrapperOptions.keepAlive, true);
|
|
348
|
+
assert.strictEqual(result.wrapperOptions.autoRemoveDockerContainer, true);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
222
352
|
describe('command without separator', () => {
|
|
223
353
|
it('should parse command after options without separator', () => {
|
|
224
354
|
const result = parseArgs(['-i', 'tmux', '-d', 'npm', 'start']);
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Tests for Docker auto-remove container feature
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { describe, it } = require('node:test');
|
|
7
|
+
const assert = require('assert');
|
|
8
|
+
const { isCommandAvailable } = require('../src/lib/isolation');
|
|
9
|
+
const { runInDocker } = require('../src/lib/isolation');
|
|
10
|
+
const { execSync } = require('child_process');
|
|
11
|
+
|
|
12
|
+
// Helper to wait for a condition with timeout
|
|
13
|
+
async function waitFor(conditionFn, timeout = 5000, interval = 100) {
|
|
14
|
+
const startTime = Date.now();
|
|
15
|
+
while (Date.now() - startTime < timeout) {
|
|
16
|
+
if (conditionFn()) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Helper function to check if docker daemon is running
|
|
25
|
+
function isDockerRunning() {
|
|
26
|
+
if (!isCommandAvailable('docker')) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
execSync('docker info', { stdio: 'ignore', timeout: 5000 });
|
|
31
|
+
return true;
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('Docker Auto-Remove Container Feature', () => {
|
|
38
|
+
// These tests verify the --auto-remove-docker-container option
|
|
39
|
+
// which automatically removes the container after exit (disabled by default)
|
|
40
|
+
|
|
41
|
+
describe('auto-remove enabled', () => {
|
|
42
|
+
it('should automatically remove container when autoRemoveDockerContainer is true', async () => {
|
|
43
|
+
if (!isDockerRunning()) {
|
|
44
|
+
console.log(' Skipping: docker not available or daemon not running');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const containerName = `test-autoremove-${Date.now()}`;
|
|
49
|
+
|
|
50
|
+
// Run command with autoRemoveDockerContainer enabled
|
|
51
|
+
const result = await runInDocker('echo "test" && sleep 0.5', {
|
|
52
|
+
image: 'alpine:latest',
|
|
53
|
+
session: containerName,
|
|
54
|
+
detached: true,
|
|
55
|
+
keepAlive: false,
|
|
56
|
+
autoRemoveDockerContainer: true,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
assert.strictEqual(result.success, true);
|
|
60
|
+
assert.ok(
|
|
61
|
+
result.message.includes('automatically removed'),
|
|
62
|
+
'Message should indicate auto-removal'
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Wait for container to finish and be removed
|
|
66
|
+
const containerRemoved = await waitFor(() => {
|
|
67
|
+
try {
|
|
68
|
+
execSync(`docker inspect -f '{{.State.Status}}' ${containerName}`, {
|
|
69
|
+
encoding: 'utf8',
|
|
70
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
71
|
+
});
|
|
72
|
+
return false; // Container still exists
|
|
73
|
+
} catch {
|
|
74
|
+
return true; // Container does not exist (removed)
|
|
75
|
+
}
|
|
76
|
+
}, 10000);
|
|
77
|
+
|
|
78
|
+
assert.ok(
|
|
79
|
+
containerRemoved,
|
|
80
|
+
'Container should be automatically removed after exit with --auto-remove-docker-container'
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Double-check with docker ps -a that container is completely removed
|
|
84
|
+
try {
|
|
85
|
+
const allContainers = execSync('docker ps -a', {
|
|
86
|
+
encoding: 'utf8',
|
|
87
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
88
|
+
});
|
|
89
|
+
assert.ok(
|
|
90
|
+
!allContainers.includes(containerName),
|
|
91
|
+
'Container should NOT appear in docker ps -a (completely removed)'
|
|
92
|
+
);
|
|
93
|
+
console.log(
|
|
94
|
+
' ✓ Docker container auto-removed after exit (filesystem not preserved)'
|
|
95
|
+
);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
assert.fail(`Failed to verify container removal: ${err.message}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// No cleanup needed - container should already be removed
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('auto-remove disabled (default)', () => {
|
|
105
|
+
it('should preserve container filesystem by default (without autoRemoveDockerContainer)', async () => {
|
|
106
|
+
if (!isDockerRunning()) {
|
|
107
|
+
console.log(' Skipping: docker not available or daemon not running');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const containerName = `test-preserve-${Date.now()}`;
|
|
112
|
+
|
|
113
|
+
// Run command without autoRemoveDockerContainer
|
|
114
|
+
const result = await runInDocker('echo "test" && sleep 0.1', {
|
|
115
|
+
image: 'alpine:latest',
|
|
116
|
+
session: containerName,
|
|
117
|
+
detached: true,
|
|
118
|
+
keepAlive: false,
|
|
119
|
+
autoRemoveDockerContainer: false,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
assert.strictEqual(result.success, true);
|
|
123
|
+
assert.ok(
|
|
124
|
+
result.message.includes('filesystem will be preserved'),
|
|
125
|
+
'Message should indicate filesystem preservation'
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// Wait for container to exit
|
|
129
|
+
await waitFor(() => {
|
|
130
|
+
try {
|
|
131
|
+
const status = execSync(
|
|
132
|
+
`docker inspect -f '{{.State.Status}}' ${containerName}`,
|
|
133
|
+
{
|
|
134
|
+
encoding: 'utf8',
|
|
135
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
136
|
+
}
|
|
137
|
+
).trim();
|
|
138
|
+
return status === 'exited';
|
|
139
|
+
} catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}, 10000);
|
|
143
|
+
|
|
144
|
+
// Container should still exist (in exited state)
|
|
145
|
+
try {
|
|
146
|
+
const allContainers = execSync('docker ps -a', {
|
|
147
|
+
encoding: 'utf8',
|
|
148
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
149
|
+
});
|
|
150
|
+
assert.ok(
|
|
151
|
+
allContainers.includes(containerName),
|
|
152
|
+
'Container should appear in docker ps -a (filesystem preserved)'
|
|
153
|
+
);
|
|
154
|
+
console.log(
|
|
155
|
+
' ✓ Docker container filesystem preserved by default (can be re-entered)'
|
|
156
|
+
);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
assert.fail(`Failed to verify container preservation: ${err.message}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Clean up
|
|
162
|
+
try {
|
|
163
|
+
execSync(`docker rm -f ${containerName}`, { stdio: 'ignore' });
|
|
164
|
+
} catch {
|
|
165
|
+
// Ignore cleanup errors
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|