start-command 0.16.0 → 0.17.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/CHANGELOG.md +10 -0
- package/package.json +1 -1
- package/src/bin/cli.js +63 -57
- package/src/lib/docker-utils.js +89 -0
- package/src/lib/isolation.js +4 -0
- package/src/lib/output-blocks.js +16 -0
- package/test/isolation.test.js +32 -0
- package/test/output-blocks.test.js +40 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# start-command
|
|
2
2
|
|
|
3
|
+
## 0.17.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 1da275c: feat: Improve output block uniformity and add OS-based Docker image detection
|
|
8
|
+
- Move isolation info lines into start block instead of printing them separately
|
|
9
|
+
- Move exit/result messages into finish block instead of printing them separately
|
|
10
|
+
- Add getDefaultDockerImage() to detect host OS and select matching Docker image
|
|
11
|
+
- Default Docker images: ubuntu, debian, archlinux, fedora, centos based on host OS
|
|
12
|
+
|
|
3
13
|
## 0.16.0
|
|
4
14
|
|
|
5
15
|
### Minor Changes
|
package/package.json
CHANGED
package/src/bin/cli.js
CHANGED
|
@@ -22,6 +22,7 @@ const {
|
|
|
22
22
|
createLogFooter,
|
|
23
23
|
writeLogFile,
|
|
24
24
|
createLogPath,
|
|
25
|
+
getDefaultDockerImage,
|
|
25
26
|
} = require('../lib/isolation');
|
|
26
27
|
const {
|
|
27
28
|
createIsolatedUser,
|
|
@@ -286,6 +287,12 @@ async function runWithIsolation(
|
|
|
286
287
|
const startTime = getTimestamp();
|
|
287
288
|
const startTimeMs = Date.now();
|
|
288
289
|
|
|
290
|
+
// Use default Docker image if docker isolation is selected but no image specified
|
|
291
|
+
let effectiveImage = options.image;
|
|
292
|
+
if (environment === 'docker' && !effectiveImage) {
|
|
293
|
+
effectiveImage = getDefaultDockerImage();
|
|
294
|
+
}
|
|
295
|
+
|
|
289
296
|
// Create log file path
|
|
290
297
|
const logFilePath = createLogPath(environment || 'direct');
|
|
291
298
|
|
|
@@ -298,38 +305,8 @@ async function runWithIsolation(
|
|
|
298
305
|
const isWindows = process.platform === 'win32';
|
|
299
306
|
const shell = isWindows ? 'powershell.exe' : process.env.SHELL || '/bin/sh';
|
|
300
307
|
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
const store = getExecutionStore();
|
|
304
|
-
if (store) {
|
|
305
|
-
executionRecord = new ExecutionRecord({
|
|
306
|
-
uuid: sessionId, // Use the provided session ID
|
|
307
|
-
command: cmd,
|
|
308
|
-
logPath: logFilePath,
|
|
309
|
-
shell,
|
|
310
|
-
workingDirectory: process.cwd(),
|
|
311
|
-
options: {
|
|
312
|
-
isolated: environment,
|
|
313
|
-
isolationMode: mode,
|
|
314
|
-
sessionName,
|
|
315
|
-
image: options.image,
|
|
316
|
-
endpoint: options.endpoint,
|
|
317
|
-
user: options.user,
|
|
318
|
-
keepAlive: options.keepAlive,
|
|
319
|
-
useCommandStream,
|
|
320
|
-
},
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Print start block with session ID
|
|
325
|
-
console.log(
|
|
326
|
-
createStartBlock({
|
|
327
|
-
sessionId,
|
|
328
|
-
timestamp: startTime,
|
|
329
|
-
command: cmd,
|
|
330
|
-
})
|
|
331
|
-
);
|
|
332
|
-
console.log('');
|
|
308
|
+
// Collect isolation info lines for start block
|
|
309
|
+
const extraLines = [];
|
|
333
310
|
|
|
334
311
|
// Handle --isolated-user option: create a new user with same permissions
|
|
335
312
|
let createdUser = null;
|
|
@@ -352,9 +329,9 @@ async function runWithIsolation(
|
|
|
352
329
|
currentGroups.includes(g)
|
|
353
330
|
);
|
|
354
331
|
|
|
355
|
-
|
|
332
|
+
extraLines.push('[User Isolation] Creating new user...');
|
|
356
333
|
if (importantGroups.length > 0) {
|
|
357
|
-
|
|
334
|
+
extraLines.push(
|
|
358
335
|
`[User Isolation] Inheriting groups: ${importantGroups.join(', ')}`
|
|
359
336
|
);
|
|
360
337
|
}
|
|
@@ -369,37 +346,69 @@ async function runWithIsolation(
|
|
|
369
346
|
}
|
|
370
347
|
|
|
371
348
|
createdUser = userResult.username;
|
|
372
|
-
|
|
349
|
+
extraLines.push(`[User Isolation] Created user: ${createdUser}`);
|
|
373
350
|
if (userResult.groups && userResult.groups.length > 0) {
|
|
374
|
-
|
|
351
|
+
extraLines.push(
|
|
375
352
|
`[User Isolation] User groups: ${userResult.groups.join(', ')}`
|
|
376
353
|
);
|
|
377
354
|
}
|
|
378
355
|
if (options.keepUser) {
|
|
379
|
-
|
|
356
|
+
extraLines.push('[User Isolation] User will be kept after completion');
|
|
380
357
|
}
|
|
381
|
-
console.log('');
|
|
382
358
|
}
|
|
383
359
|
|
|
384
|
-
//
|
|
360
|
+
// Add isolation info to extra lines
|
|
385
361
|
if (environment) {
|
|
386
|
-
|
|
362
|
+
extraLines.push(`[Isolation] Environment: ${environment}, Mode: ${mode}`);
|
|
387
363
|
}
|
|
388
364
|
if (options.session) {
|
|
389
|
-
|
|
365
|
+
extraLines.push(`[Isolation] Session: ${options.session}`);
|
|
390
366
|
}
|
|
391
|
-
if (
|
|
392
|
-
|
|
367
|
+
if (effectiveImage) {
|
|
368
|
+
extraLines.push(`[Isolation] Image: ${effectiveImage}`);
|
|
393
369
|
}
|
|
394
370
|
if (options.endpoint) {
|
|
395
|
-
|
|
371
|
+
extraLines.push(`[Isolation] Endpoint: ${options.endpoint}`);
|
|
396
372
|
}
|
|
397
373
|
if (createdUser) {
|
|
398
|
-
|
|
374
|
+
extraLines.push(`[Isolation] User: ${createdUser} (isolated)`);
|
|
399
375
|
}
|
|
400
376
|
if (useCommandStream) {
|
|
401
|
-
|
|
377
|
+
extraLines.push('[Isolation] Using command-stream library');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Create execution record for tracking with provided session ID
|
|
381
|
+
let executionRecord = null;
|
|
382
|
+
const store = getExecutionStore();
|
|
383
|
+
if (store) {
|
|
384
|
+
executionRecord = new ExecutionRecord({
|
|
385
|
+
uuid: sessionId, // Use the provided session ID
|
|
386
|
+
command: cmd,
|
|
387
|
+
logPath: logFilePath,
|
|
388
|
+
shell,
|
|
389
|
+
workingDirectory: process.cwd(),
|
|
390
|
+
options: {
|
|
391
|
+
isolated: environment,
|
|
392
|
+
isolationMode: mode,
|
|
393
|
+
sessionName,
|
|
394
|
+
image: effectiveImage,
|
|
395
|
+
endpoint: options.endpoint,
|
|
396
|
+
user: options.user,
|
|
397
|
+
keepAlive: options.keepAlive,
|
|
398
|
+
useCommandStream,
|
|
399
|
+
},
|
|
400
|
+
});
|
|
402
401
|
}
|
|
402
|
+
|
|
403
|
+
// Print start block with session ID and isolation info
|
|
404
|
+
console.log(
|
|
405
|
+
createStartBlock({
|
|
406
|
+
sessionId,
|
|
407
|
+
timestamp: startTime,
|
|
408
|
+
command: cmd,
|
|
409
|
+
extraLines,
|
|
410
|
+
})
|
|
411
|
+
);
|
|
403
412
|
console.log('');
|
|
404
413
|
|
|
405
414
|
// Save initial execution record
|
|
@@ -421,7 +430,7 @@ async function runWithIsolation(
|
|
|
421
430
|
environment: environment || 'direct',
|
|
422
431
|
mode,
|
|
423
432
|
sessionName,
|
|
424
|
-
image:
|
|
433
|
+
image: effectiveImage,
|
|
425
434
|
user: createdUser,
|
|
426
435
|
startTime,
|
|
427
436
|
});
|
|
@@ -442,7 +451,7 @@ async function runWithIsolation(
|
|
|
442
451
|
// Future: Add command-stream support with raw() function for multiplexers
|
|
443
452
|
result = await runIsolated(environment, cmd, {
|
|
444
453
|
session: options.session,
|
|
445
|
-
image:
|
|
454
|
+
image: effectiveImage,
|
|
446
455
|
endpoint: options.endpoint,
|
|
447
456
|
detached: mode === 'detached',
|
|
448
457
|
user: createdUser,
|
|
@@ -483,30 +492,26 @@ async function runWithIsolation(
|
|
|
483
492
|
}
|
|
484
493
|
}
|
|
485
494
|
|
|
486
|
-
// Print result
|
|
487
|
-
console.log('');
|
|
488
|
-
console.log(result.message);
|
|
489
|
-
|
|
490
495
|
// Cleanup: delete the created user if we created one (unless --keep-user)
|
|
496
|
+
// This output goes to stdout but NOT inside the boxes - it's operational info
|
|
491
497
|
if (createdUser && !options.keepUser) {
|
|
492
|
-
console.log('');
|
|
493
498
|
console.log(`[User Isolation] Cleaning up user: ${createdUser}`);
|
|
494
499
|
const deleteResult = deleteUser(createdUser, { removeHome: true });
|
|
495
500
|
if (deleteResult.success) {
|
|
496
|
-
console.log(
|
|
501
|
+
console.log('[User Isolation] User deleted successfully');
|
|
497
502
|
} else {
|
|
498
503
|
console.log(`[User Isolation] Warning: ${deleteResult.message}`);
|
|
499
504
|
}
|
|
500
|
-
} else if (createdUser && options.keepUser) {
|
|
501
505
|
console.log('');
|
|
506
|
+
} else if (createdUser && options.keepUser) {
|
|
502
507
|
console.log(
|
|
503
508
|
`[User Isolation] Keeping user: ${createdUser} (use 'sudo userdel -r ${createdUser}' to delete)`
|
|
504
509
|
);
|
|
510
|
+
console.log('');
|
|
505
511
|
}
|
|
506
512
|
|
|
507
|
-
// Print finish block
|
|
513
|
+
// Print finish block with result message inside
|
|
508
514
|
const durationMs = Date.now() - startTimeMs;
|
|
509
|
-
console.log('');
|
|
510
515
|
console.log(
|
|
511
516
|
createFinishBlock({
|
|
512
517
|
sessionId,
|
|
@@ -514,6 +519,7 @@ async function runWithIsolation(
|
|
|
514
519
|
exitCode,
|
|
515
520
|
logPath: logFilePath,
|
|
516
521
|
durationMs,
|
|
522
|
+
resultMessage: result.message,
|
|
517
523
|
})
|
|
518
524
|
);
|
|
519
525
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker utilities for the start-command CLI
|
|
3
|
+
*
|
|
4
|
+
* Provides Docker-related helper functions like detecting the default
|
|
5
|
+
* Docker image based on the host operating system.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get the default Docker image based on the host operating system
|
|
12
|
+
* Returns an image that matches the current OS as closely as possible:
|
|
13
|
+
* - macOS: Uses alpine (since macOS cannot run in Docker)
|
|
14
|
+
* - Ubuntu/Debian: Uses ubuntu:latest
|
|
15
|
+
* - Arch Linux: Uses archlinux:latest
|
|
16
|
+
* - Other Linux: Uses the detected distro or alpine as fallback
|
|
17
|
+
* - Windows: Uses alpine (Windows containers have limited support)
|
|
18
|
+
* @returns {string} Docker image name
|
|
19
|
+
*/
|
|
20
|
+
function getDefaultDockerImage() {
|
|
21
|
+
const platform = process.platform;
|
|
22
|
+
|
|
23
|
+
if (platform === 'darwin') {
|
|
24
|
+
// macOS cannot run in Docker containers, use alpine as lightweight alternative
|
|
25
|
+
return 'alpine:latest';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (platform === 'win32') {
|
|
29
|
+
// Windows containers have limited support, use alpine for Linux containers
|
|
30
|
+
return 'alpine:latest';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (platform === 'linux') {
|
|
34
|
+
// Try to detect the Linux distribution
|
|
35
|
+
try {
|
|
36
|
+
const osRelease = fs.readFileSync('/etc/os-release', 'utf8');
|
|
37
|
+
|
|
38
|
+
// Check for Ubuntu
|
|
39
|
+
if (
|
|
40
|
+
osRelease.includes('ID=ubuntu') ||
|
|
41
|
+
osRelease.includes('ID_LIKE=ubuntu') ||
|
|
42
|
+
osRelease.includes('ID_LIKE=debian ubuntu')
|
|
43
|
+
) {
|
|
44
|
+
return 'ubuntu:latest';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check for Debian
|
|
48
|
+
if (
|
|
49
|
+
osRelease.includes('ID=debian') ||
|
|
50
|
+
osRelease.includes('ID_LIKE=debian')
|
|
51
|
+
) {
|
|
52
|
+
return 'debian:latest';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check for Arch Linux
|
|
56
|
+
if (osRelease.includes('ID=arch') || osRelease.includes('ID_LIKE=arch')) {
|
|
57
|
+
return 'archlinux:latest';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check for Fedora
|
|
61
|
+
if (osRelease.includes('ID=fedora')) {
|
|
62
|
+
return 'fedora:latest';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check for CentOS/RHEL
|
|
66
|
+
if (
|
|
67
|
+
osRelease.includes('ID=centos') ||
|
|
68
|
+
osRelease.includes('ID=rhel') ||
|
|
69
|
+
osRelease.includes('ID_LIKE=rhel')
|
|
70
|
+
) {
|
|
71
|
+
return 'centos:latest';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check for Alpine
|
|
75
|
+
if (osRelease.includes('ID=alpine')) {
|
|
76
|
+
return 'alpine:latest';
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// Cannot read /etc/os-release, fall through to default
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Default fallback: use alpine as a lightweight, universal option
|
|
84
|
+
return 'alpine:latest';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = {
|
|
88
|
+
getDefaultDockerImage,
|
|
89
|
+
};
|
package/src/lib/isolation.js
CHANGED
|
@@ -964,6 +964,9 @@ function canRunLinuxDockerImages() {
|
|
|
964
964
|
}
|
|
965
965
|
}
|
|
966
966
|
|
|
967
|
+
// Re-export getDefaultDockerImage from docker-utils for backwards compatibility
|
|
968
|
+
const { getDefaultDockerImage } = require('./docker-utils');
|
|
969
|
+
|
|
967
970
|
module.exports = {
|
|
968
971
|
isCommandAvailable,
|
|
969
972
|
hasTTY,
|
|
@@ -985,4 +988,5 @@ module.exports = {
|
|
|
985
988
|
supportsLogfileOption,
|
|
986
989
|
resetScreenVersionCache,
|
|
987
990
|
canRunLinuxDockerImages,
|
|
991
|
+
getDefaultDockerImage,
|
|
988
992
|
};
|
package/src/lib/output-blocks.js
CHANGED
|
@@ -144,6 +144,7 @@ function createBottomBorder(width, style) {
|
|
|
144
144
|
* @param {string} options.sessionId - Session UUID
|
|
145
145
|
* @param {string} options.timestamp - Timestamp string
|
|
146
146
|
* @param {string} options.command - Command being executed
|
|
147
|
+
* @param {string[]} [options.extraLines] - Additional lines to show after the command line
|
|
147
148
|
* @param {string} [options.style] - Box style name
|
|
148
149
|
* @param {number} [options.width] - Box width
|
|
149
150
|
* @returns {string} Formatted start block
|
|
@@ -153,6 +154,7 @@ function createStartBlock(options) {
|
|
|
153
154
|
sessionId,
|
|
154
155
|
timestamp,
|
|
155
156
|
command,
|
|
157
|
+
extraLines = [],
|
|
156
158
|
style: styleName = DEFAULT_STYLE,
|
|
157
159
|
width = DEFAULT_WIDTH,
|
|
158
160
|
} = options;
|
|
@@ -165,6 +167,12 @@ function createStartBlock(options) {
|
|
|
165
167
|
lines.push(
|
|
166
168
|
createBorderedLine(`Starting at ${timestamp}: ${command}`, width, style)
|
|
167
169
|
);
|
|
170
|
+
|
|
171
|
+
// Add extra lines (e.g., isolation info, docker image, etc.)
|
|
172
|
+
for (const line of extraLines) {
|
|
173
|
+
lines.push(createBorderedLine(line, width, style));
|
|
174
|
+
}
|
|
175
|
+
|
|
168
176
|
lines.push(createBottomBorder(width, style));
|
|
169
177
|
|
|
170
178
|
return lines.join('\n');
|
|
@@ -197,6 +205,7 @@ function formatDuration(durationMs) {
|
|
|
197
205
|
* @param {number} options.exitCode - Exit code
|
|
198
206
|
* @param {string} options.logPath - Path to log file
|
|
199
207
|
* @param {number} [options.durationMs] - Duration in milliseconds
|
|
208
|
+
* @param {string} [options.resultMessage] - Result message (e.g., "Screen session exited...")
|
|
200
209
|
* @param {string} [options.style] - Box style name
|
|
201
210
|
* @param {number} [options.width] - Box width
|
|
202
211
|
* @returns {string} Formatted finish block
|
|
@@ -208,6 +217,7 @@ function createFinishBlock(options) {
|
|
|
208
217
|
exitCode,
|
|
209
218
|
logPath,
|
|
210
219
|
durationMs,
|
|
220
|
+
resultMessage,
|
|
211
221
|
style: styleName = DEFAULT_STYLE,
|
|
212
222
|
width = DEFAULT_WIDTH,
|
|
213
223
|
} = options;
|
|
@@ -222,6 +232,12 @@ function createFinishBlock(options) {
|
|
|
222
232
|
}
|
|
223
233
|
|
|
224
234
|
lines.push(createTopBorder(width, style));
|
|
235
|
+
|
|
236
|
+
// Add result message first if provided (e.g., "Docker container exited...")
|
|
237
|
+
if (resultMessage) {
|
|
238
|
+
lines.push(createBorderedLine(resultMessage, width, style));
|
|
239
|
+
}
|
|
240
|
+
|
|
225
241
|
lines.push(createBorderedLine(finishedMsg, width, style));
|
|
226
242
|
lines.push(createBorderedLine(`Exit code: ${exitCode}`, width, style));
|
|
227
243
|
lines.push(createBorderedLine(`Log: ${logPath}`, width, style));
|
package/test/isolation.test.js
CHANGED
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
getScreenVersion,
|
|
14
14
|
supportsLogfileOption,
|
|
15
15
|
resetScreenVersionCache,
|
|
16
|
+
getDefaultDockerImage,
|
|
16
17
|
} = require('../src/lib/isolation');
|
|
17
18
|
|
|
18
19
|
describe('Isolation Module', () => {
|
|
@@ -723,3 +724,34 @@ describe('Isolation Runner with Available Backends', () => {
|
|
|
723
724
|
});
|
|
724
725
|
});
|
|
725
726
|
});
|
|
727
|
+
|
|
728
|
+
describe('Default Docker Image Detection', () => {
|
|
729
|
+
describe('getDefaultDockerImage', () => {
|
|
730
|
+
it('should return a valid Docker image string', () => {
|
|
731
|
+
const image = getDefaultDockerImage();
|
|
732
|
+
assert.ok(image, 'Should return a non-empty string');
|
|
733
|
+
assert.ok(typeof image === 'string', 'Should return a string');
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
it('should return an image with a tag', () => {
|
|
737
|
+
const image = getDefaultDockerImage();
|
|
738
|
+
assert.ok(image.includes(':'), 'Image should have a tag (e.g., :latest)');
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
it('should return a known base image', () => {
|
|
742
|
+
const image = getDefaultDockerImage();
|
|
743
|
+
const knownImages = [
|
|
744
|
+
'alpine:latest',
|
|
745
|
+
'ubuntu:latest',
|
|
746
|
+
'debian:latest',
|
|
747
|
+
'archlinux:latest',
|
|
748
|
+
'fedora:latest',
|
|
749
|
+
'centos:latest',
|
|
750
|
+
];
|
|
751
|
+
assert.ok(
|
|
752
|
+
knownImages.includes(image),
|
|
753
|
+
`Image '${image}' should be one of the known base images: ${knownImages.join(', ')}`
|
|
754
|
+
);
|
|
755
|
+
});
|
|
756
|
+
});
|
|
757
|
+
});
|
|
@@ -77,6 +77,26 @@ describe('output-blocks module', () => {
|
|
|
77
77
|
expect(block).toContain('+');
|
|
78
78
|
expect(block).toContain('-');
|
|
79
79
|
});
|
|
80
|
+
|
|
81
|
+
it('should include extra lines when provided', () => {
|
|
82
|
+
const block = createStartBlock({
|
|
83
|
+
sessionId: 'test-uuid',
|
|
84
|
+
timestamp: '2025-01-01 00:00:00',
|
|
85
|
+
command: 'echo hello',
|
|
86
|
+
extraLines: [
|
|
87
|
+
'[Isolation] Environment: screen, Mode: attached',
|
|
88
|
+
'[Isolation] Session: my-session',
|
|
89
|
+
],
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(block).toContain('╭');
|
|
93
|
+
expect(block).toContain('╰');
|
|
94
|
+
expect(block).toContain('Session ID: test-uuid');
|
|
95
|
+
expect(block).toContain(
|
|
96
|
+
'[Isolation] Environment: screen, Mode: attached'
|
|
97
|
+
);
|
|
98
|
+
expect(block).toContain('[Isolation] Session: my-session');
|
|
99
|
+
});
|
|
80
100
|
});
|
|
81
101
|
|
|
82
102
|
describe('createFinishBlock', () => {
|
|
@@ -110,6 +130,26 @@ describe('output-blocks module', () => {
|
|
|
110
130
|
expect(block).toContain('Finished at 2025-01-01 00:00:01');
|
|
111
131
|
expect(block).not.toContain('seconds');
|
|
112
132
|
});
|
|
133
|
+
|
|
134
|
+
it('should include result message when provided', () => {
|
|
135
|
+
const block = createFinishBlock({
|
|
136
|
+
sessionId: 'test-uuid-1234',
|
|
137
|
+
timestamp: '2025-01-01 00:00:01',
|
|
138
|
+
exitCode: 0,
|
|
139
|
+
logPath: '/tmp/test.log',
|
|
140
|
+
durationMs: 17,
|
|
141
|
+
resultMessage: 'Screen session "my-session" exited with code 0',
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(block).toContain('╭');
|
|
145
|
+
expect(block).toContain('╰');
|
|
146
|
+
expect(block).toContain('Screen session');
|
|
147
|
+
expect(block).toContain('exited with code 0');
|
|
148
|
+
expect(block).toContain('Session ID: test-uuid-1234');
|
|
149
|
+
expect(block).toContain(
|
|
150
|
+
'Finished at 2025-01-01 00:00:01 in 0.017 seconds'
|
|
151
|
+
);
|
|
152
|
+
});
|
|
113
153
|
});
|
|
114
154
|
|
|
115
155
|
describe('formatDuration', () => {
|