start-command 0.17.4 → 0.19.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 +34 -0
- package/package.json +1 -1
- package/src/bin/cli.js +2 -2
- package/src/lib/args-parser.js +5 -5
- package/src/lib/output-blocks.js +211 -177
- package/test/args-parser.test.js +20 -4
- package/test/echo-integration.test.js +106 -97
- package/test/isolation.test.js +13 -7
- package/test/output-blocks.test.js +215 -74
- package/test/ssh-integration.test.js +5 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# start-command
|
|
2
2
|
|
|
3
|
+
## 0.19.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 112a78e: Replace fixed-width box output with status spine format
|
|
8
|
+
- Width-independent output that doesn't truncate or create jagged boxes
|
|
9
|
+
- All metadata visible and copy-pasteable (log paths, session IDs)
|
|
10
|
+
- Works uniformly in TTY, tmux, SSH, CI, and log files
|
|
11
|
+
- Clear visual distinction: │ for metadata, $ for command, no prefix for output
|
|
12
|
+
- Result markers ✓ and ✗ for success/failure
|
|
13
|
+
- Isolation metadata repeated in footer for context
|
|
14
|
+
|
|
15
|
+
## 0.18.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- c918e82: feat: Use OS-matched default Docker image when --image is not specified
|
|
20
|
+
|
|
21
|
+
When using `$ --isolated docker -- command`, instead of requiring the `--image` option,
|
|
22
|
+
the system now automatically selects an appropriate default Docker image based on the
|
|
23
|
+
host operating system:
|
|
24
|
+
- macOS/Windows: `alpine:latest` (lightweight, portable)
|
|
25
|
+
- Ubuntu: `ubuntu:latest`
|
|
26
|
+
- Debian: `debian:latest`
|
|
27
|
+
- Arch Linux: `archlinux:latest`
|
|
28
|
+
- Fedora: `fedora:latest`
|
|
29
|
+
- CentOS/RHEL: `centos:latest`
|
|
30
|
+
- Other Linux/Fallback: `alpine:latest`
|
|
31
|
+
|
|
32
|
+
This allows users to use Docker isolation with a simple command like:
|
|
33
|
+
`$ --isolated docker -- echo 'hi'`
|
|
34
|
+
|
|
35
|
+
Fixes #62
|
|
36
|
+
|
|
3
37
|
## 0.17.4
|
|
4
38
|
|
|
5
39
|
### Patch Changes
|
package/package.json
CHANGED
package/src/bin/cli.js
CHANGED
|
@@ -623,7 +623,7 @@ async function runWithIsolation(
|
|
|
623
623
|
console.log('');
|
|
624
624
|
}
|
|
625
625
|
|
|
626
|
-
// Print finish block with
|
|
626
|
+
// Print finish block with isolation metadata repeated
|
|
627
627
|
// Add empty line before finish block for visual separation
|
|
628
628
|
console.log('');
|
|
629
629
|
const durationMs = Date.now() - startTimeMs;
|
|
@@ -634,7 +634,7 @@ async function runWithIsolation(
|
|
|
634
634
|
exitCode,
|
|
635
635
|
logPath: logFilePath,
|
|
636
636
|
durationMs,
|
|
637
|
-
|
|
637
|
+
extraLines, // Pass extraLines for isolation metadata repetition in footer
|
|
638
638
|
})
|
|
639
639
|
);
|
|
640
640
|
|
package/src/lib/args-parser.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* --attached, -a Run in attached mode (foreground)
|
|
11
11
|
* --detached, -d Run in detached mode (background)
|
|
12
12
|
* --session, -s <name> Session name for isolation
|
|
13
|
-
* --image <image> Docker image (
|
|
13
|
+
* --image <image> Docker image (optional, defaults to OS-matched image)
|
|
14
14
|
* --endpoint <endpoint> SSH endpoint (required for ssh isolation, e.g., user@host)
|
|
15
15
|
* --isolated-user, -u [username] Create isolated user with same permissions (auto-generated name if not specified)
|
|
16
16
|
* --keep-user Keep isolated user after command completes (don't delete)
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
* --cleanup-dry-run Show stale records that would be cleaned up (without actually cleaning)
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
+
const { getDefaultDockerImage } = require('./docker-utils');
|
|
27
|
+
|
|
26
28
|
// Debug mode from environment
|
|
27
29
|
const DEBUG =
|
|
28
30
|
process.env.START_DEBUG === '1' || process.env.START_DEBUG === 'true';
|
|
@@ -381,11 +383,9 @@ function validateOptions(options) {
|
|
|
381
383
|
);
|
|
382
384
|
}
|
|
383
385
|
|
|
384
|
-
// Docker
|
|
386
|
+
// Docker uses --image or defaults to OS-matched image
|
|
385
387
|
if (options.isolated === 'docker' && !options.image) {
|
|
386
|
-
|
|
387
|
-
'Docker isolation requires --image option to specify the container image'
|
|
388
|
-
);
|
|
388
|
+
options.image = getDefaultDockerImage();
|
|
389
389
|
}
|
|
390
390
|
|
|
391
391
|
// SSH requires --endpoint
|
package/src/lib/output-blocks.js
CHANGED
|
@@ -1,189 +1,205 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Output formatting utilities for nicely rendered command blocks
|
|
3
3
|
*
|
|
4
|
-
* Provides
|
|
5
|
-
*
|
|
4
|
+
* Provides "status spine" format: a width-independent, lossless output format
|
|
5
|
+
* that works in TTY, tmux, SSH, CI, and logs.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* 5. 'ascii': Pure ASCII compatible (-------- +------+)
|
|
7
|
+
* Core concepts:
|
|
8
|
+
* - `│` prefix → tool metadata
|
|
9
|
+
* - `$` → executed command
|
|
10
|
+
* - No prefix → program output (stdout/stderr)
|
|
11
|
+
* - Result marker (`✓` / `✗`) appears after output
|
|
13
12
|
*/
|
|
14
13
|
|
|
15
|
-
//
|
|
16
|
-
const
|
|
17
|
-
rounded: {
|
|
18
|
-
topLeft: '╭',
|
|
19
|
-
topRight: '╮',
|
|
20
|
-
bottomLeft: '╰',
|
|
21
|
-
bottomRight: '╯',
|
|
22
|
-
horizontal: '─',
|
|
23
|
-
vertical: '│',
|
|
24
|
-
},
|
|
25
|
-
heavy: {
|
|
26
|
-
topLeft: '┏',
|
|
27
|
-
topRight: '┓',
|
|
28
|
-
bottomLeft: '┗',
|
|
29
|
-
bottomRight: '┛',
|
|
30
|
-
horizontal: '━',
|
|
31
|
-
vertical: '┃',
|
|
32
|
-
},
|
|
33
|
-
double: {
|
|
34
|
-
topLeft: '╔',
|
|
35
|
-
topRight: '╗',
|
|
36
|
-
bottomLeft: '╚',
|
|
37
|
-
bottomRight: '╝',
|
|
38
|
-
horizontal: '═',
|
|
39
|
-
vertical: '║',
|
|
40
|
-
},
|
|
41
|
-
simple: {
|
|
42
|
-
topLeft: '',
|
|
43
|
-
topRight: '',
|
|
44
|
-
bottomLeft: '',
|
|
45
|
-
bottomRight: '',
|
|
46
|
-
horizontal: '─',
|
|
47
|
-
vertical: '',
|
|
48
|
-
},
|
|
49
|
-
ascii: {
|
|
50
|
-
topLeft: '+',
|
|
51
|
-
topRight: '+',
|
|
52
|
-
bottomLeft: '+',
|
|
53
|
-
bottomRight: '+',
|
|
54
|
-
horizontal: '-',
|
|
55
|
-
vertical: '|',
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
// Default style (can be overridden via environment variable)
|
|
60
|
-
const DEFAULT_STYLE = process.env.START_OUTPUT_STYLE || 'rounded';
|
|
14
|
+
// Metadata spine character
|
|
15
|
+
const SPINE = '│';
|
|
61
16
|
|
|
62
|
-
//
|
|
63
|
-
const
|
|
17
|
+
// Result markers
|
|
18
|
+
const SUCCESS_MARKER = '✓';
|
|
19
|
+
const FAILURE_MARKER = '✗';
|
|
64
20
|
|
|
65
21
|
/**
|
|
66
|
-
*
|
|
67
|
-
* @param {string}
|
|
68
|
-
* @
|
|
22
|
+
* Create a metadata line with spine prefix
|
|
23
|
+
* @param {string} label - Label (e.g., 'session', 'start', 'exit')
|
|
24
|
+
* @param {string} value - Value for the label
|
|
25
|
+
* @returns {string} Formatted line with spine prefix
|
|
69
26
|
*/
|
|
70
|
-
function
|
|
71
|
-
|
|
27
|
+
function createSpineLine(label, value) {
|
|
28
|
+
// Pad label to 10 characters for alignment
|
|
29
|
+
const paddedLabel = label.padEnd(10);
|
|
30
|
+
return `${SPINE} ${paddedLabel}${value}`;
|
|
72
31
|
}
|
|
73
32
|
|
|
74
33
|
/**
|
|
75
|
-
* Create
|
|
76
|
-
* @
|
|
77
|
-
* @param {object} style - Box style
|
|
78
|
-
* @returns {string} Horizontal line
|
|
34
|
+
* Create an empty spine line (just the spine character)
|
|
35
|
+
* @returns {string} Empty spine line
|
|
79
36
|
*/
|
|
80
|
-
function
|
|
81
|
-
return
|
|
37
|
+
function createEmptySpineLine() {
|
|
38
|
+
return SPINE;
|
|
82
39
|
}
|
|
83
40
|
|
|
84
41
|
/**
|
|
85
|
-
*
|
|
86
|
-
* @param {string}
|
|
87
|
-
* @
|
|
88
|
-
* @param {boolean} [allowOverflow=false] - If true, don't truncate long text
|
|
89
|
-
* @returns {string} Padded text
|
|
42
|
+
* Create a command line with $ prefix
|
|
43
|
+
* @param {string} command - The command being executed
|
|
44
|
+
* @returns {string} Formatted command line
|
|
90
45
|
*/
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
// If overflow is allowed, return text as-is (for copyable content like paths)
|
|
94
|
-
if (allowOverflow) {
|
|
95
|
-
return text;
|
|
96
|
-
}
|
|
97
|
-
return text.substring(0, width);
|
|
98
|
-
}
|
|
99
|
-
return text + ' '.repeat(width - text.length);
|
|
46
|
+
function createCommandLine(command) {
|
|
47
|
+
return `$ ${command}`;
|
|
100
48
|
}
|
|
101
49
|
|
|
102
50
|
/**
|
|
103
|
-
*
|
|
104
|
-
* @param {
|
|
105
|
-
* @
|
|
106
|
-
* @param {object} style - Box style
|
|
107
|
-
* @param {boolean} [allowOverflow=false] - If true, allow text to overflow (for copyable content)
|
|
108
|
-
* @returns {string} Bordered line
|
|
51
|
+
* Get the result marker based on exit code
|
|
52
|
+
* @param {number} exitCode - Exit code (0 = success)
|
|
53
|
+
* @returns {string} Result marker (✓ or ✗)
|
|
109
54
|
*/
|
|
110
|
-
function
|
|
111
|
-
|
|
112
|
-
const innerWidth = width - 4; // 2 for borders, 2 for padding
|
|
113
|
-
const paddedText = padText(text, innerWidth, allowOverflow);
|
|
114
|
-
// If text overflows, extend the right border position
|
|
115
|
-
if (allowOverflow && text.length > innerWidth) {
|
|
116
|
-
return `${style.vertical} ${paddedText} ${style.vertical}`;
|
|
117
|
-
}
|
|
118
|
-
return `${style.vertical} ${paddedText} ${style.vertical}`;
|
|
119
|
-
}
|
|
120
|
-
return text;
|
|
55
|
+
function getResultMarker(exitCode) {
|
|
56
|
+
return exitCode === 0 ? SUCCESS_MARKER : FAILURE_MARKER;
|
|
121
57
|
}
|
|
122
58
|
|
|
123
59
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* @param {
|
|
127
|
-
* @returns {
|
|
60
|
+
* Parse isolation metadata from extraLines
|
|
61
|
+
* Extracts key-value pairs from lines like "[Isolation] Environment: docker, Mode: attached"
|
|
62
|
+
* @param {string[]} extraLines - Extra lines containing isolation info
|
|
63
|
+
* @returns {object} Parsed isolation metadata
|
|
128
64
|
*/
|
|
129
|
-
function
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
65
|
+
function parseIsolationMetadata(extraLines) {
|
|
66
|
+
const metadata = {
|
|
67
|
+
isolation: null,
|
|
68
|
+
mode: null,
|
|
69
|
+
image: null,
|
|
70
|
+
container: null,
|
|
71
|
+
screen: null,
|
|
72
|
+
session: null,
|
|
73
|
+
endpoint: null,
|
|
74
|
+
user: null,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
for (const line of extraLines) {
|
|
78
|
+
// Parse [Isolation] Environment: docker, Mode: attached
|
|
79
|
+
const envModeMatch = line.match(
|
|
80
|
+
/\[Isolation\] Environment: (\w+), Mode: (\w+)/
|
|
81
|
+
);
|
|
82
|
+
if (envModeMatch) {
|
|
83
|
+
metadata.isolation = envModeMatch[1];
|
|
84
|
+
metadata.mode = envModeMatch[2];
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Parse [Isolation] Session: name
|
|
89
|
+
const sessionMatch = line.match(/\[Isolation\] Session: (.+)/);
|
|
90
|
+
if (sessionMatch) {
|
|
91
|
+
metadata.session = sessionMatch[1];
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Parse [Isolation] Image: name
|
|
96
|
+
const imageMatch = line.match(/\[Isolation\] Image: (.+)/);
|
|
97
|
+
if (imageMatch) {
|
|
98
|
+
metadata.image = imageMatch[1];
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Parse [Isolation] Endpoint: user@host
|
|
103
|
+
const endpointMatch = line.match(/\[Isolation\] Endpoint: (.+)/);
|
|
104
|
+
if (endpointMatch) {
|
|
105
|
+
metadata.endpoint = endpointMatch[1];
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Parse [Isolation] User: name (isolated)
|
|
110
|
+
const userMatch = line.match(/\[Isolation\] User: (\w+)/);
|
|
111
|
+
if (userMatch) {
|
|
112
|
+
metadata.user = userMatch[1];
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
133
115
|
}
|
|
134
|
-
|
|
116
|
+
|
|
117
|
+
return metadata;
|
|
135
118
|
}
|
|
136
119
|
|
|
137
120
|
/**
|
|
138
|
-
*
|
|
139
|
-
* @param {
|
|
140
|
-
* @param {
|
|
141
|
-
* @returns {string}
|
|
121
|
+
* Generate isolation metadata lines for spine format
|
|
122
|
+
* @param {object} metadata - Parsed isolation metadata
|
|
123
|
+
* @param {string} [containerOrScreenName] - Container or screen session name
|
|
124
|
+
* @returns {string[]} Array of spine-formatted isolation lines
|
|
142
125
|
*/
|
|
143
|
-
function
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
126
|
+
function generateIsolationLines(metadata, containerOrScreenName = null) {
|
|
127
|
+
const lines = [];
|
|
128
|
+
|
|
129
|
+
if (metadata.isolation) {
|
|
130
|
+
lines.push(createSpineLine('isolation', metadata.isolation));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (metadata.mode) {
|
|
134
|
+
lines.push(createSpineLine('mode', metadata.mode));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (metadata.image) {
|
|
138
|
+
lines.push(createSpineLine('image', metadata.image));
|
|
147
139
|
}
|
|
148
|
-
|
|
140
|
+
|
|
141
|
+
// Use provided container/screen name or fall back to metadata.session
|
|
142
|
+
if (metadata.isolation === 'docker') {
|
|
143
|
+
const containerName = containerOrScreenName || metadata.session;
|
|
144
|
+
if (containerName) {
|
|
145
|
+
lines.push(createSpineLine('container', containerName));
|
|
146
|
+
}
|
|
147
|
+
} else if (metadata.isolation === 'screen') {
|
|
148
|
+
const screenName = containerOrScreenName || metadata.session;
|
|
149
|
+
if (screenName) {
|
|
150
|
+
lines.push(createSpineLine('screen', screenName));
|
|
151
|
+
}
|
|
152
|
+
} else if (metadata.isolation === 'tmux') {
|
|
153
|
+
const tmuxName = containerOrScreenName || metadata.session;
|
|
154
|
+
if (tmuxName) {
|
|
155
|
+
lines.push(createSpineLine('tmux', tmuxName));
|
|
156
|
+
}
|
|
157
|
+
} else if (metadata.isolation === 'ssh') {
|
|
158
|
+
if (metadata.endpoint) {
|
|
159
|
+
lines.push(createSpineLine('endpoint', metadata.endpoint));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (metadata.user) {
|
|
164
|
+
lines.push(createSpineLine('user', metadata.user));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return lines;
|
|
149
168
|
}
|
|
150
169
|
|
|
151
170
|
/**
|
|
152
|
-
* Create a start block for command execution
|
|
171
|
+
* Create a start block for command execution using status spine format
|
|
153
172
|
* @param {object} options - Options for the block
|
|
154
173
|
* @param {string} options.sessionId - Session UUID
|
|
155
174
|
* @param {string} options.timestamp - Timestamp string
|
|
156
175
|
* @param {string} options.command - Command being executed
|
|
157
|
-
* @param {string[]} [options.extraLines] - Additional lines
|
|
158
|
-
* @param {string} [options.style] -
|
|
159
|
-
* @param {number} [options.width] -
|
|
160
|
-
* @returns {string} Formatted start block
|
|
176
|
+
* @param {string[]} [options.extraLines] - Additional lines with isolation info
|
|
177
|
+
* @param {string} [options.style] - Ignored (kept for backward compatibility)
|
|
178
|
+
* @param {number} [options.width] - Ignored (kept for backward compatibility)
|
|
179
|
+
* @returns {string} Formatted start block in spine format
|
|
161
180
|
*/
|
|
162
181
|
function createStartBlock(options) {
|
|
163
|
-
const {
|
|
164
|
-
sessionId,
|
|
165
|
-
timestamp,
|
|
166
|
-
command,
|
|
167
|
-
extraLines = [],
|
|
168
|
-
style: styleName = DEFAULT_STYLE,
|
|
169
|
-
width = DEFAULT_WIDTH,
|
|
170
|
-
} = options;
|
|
182
|
+
const { sessionId, timestamp, command, extraLines = [] } = options;
|
|
171
183
|
|
|
172
|
-
const style = getBoxStyle(styleName);
|
|
173
184
|
const lines = [];
|
|
174
185
|
|
|
175
|
-
|
|
176
|
-
lines.push(
|
|
177
|
-
lines.push(
|
|
178
|
-
createBorderedLine(`Starting at ${timestamp}: ${command}`, width, style)
|
|
179
|
-
);
|
|
186
|
+
// Header: session and start time
|
|
187
|
+
lines.push(createSpineLine('session', sessionId));
|
|
188
|
+
lines.push(createSpineLine('start', timestamp));
|
|
180
189
|
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
190
|
+
// Parse and add isolation metadata if present
|
|
191
|
+
const metadata = parseIsolationMetadata(extraLines);
|
|
192
|
+
|
|
193
|
+
if (metadata.isolation) {
|
|
194
|
+
lines.push(createEmptySpineLine());
|
|
195
|
+
lines.push(...generateIsolationLines(metadata));
|
|
184
196
|
}
|
|
185
197
|
|
|
186
|
-
|
|
198
|
+
// Empty spine line before command
|
|
199
|
+
lines.push(createEmptySpineLine());
|
|
200
|
+
|
|
201
|
+
// Command line
|
|
202
|
+
lines.push(createCommandLine(command));
|
|
187
203
|
|
|
188
204
|
return lines.join('\n');
|
|
189
205
|
}
|
|
@@ -191,34 +207,46 @@ function createStartBlock(options) {
|
|
|
191
207
|
/**
|
|
192
208
|
* Format duration in seconds with appropriate precision
|
|
193
209
|
* @param {number} durationMs - Duration in milliseconds
|
|
194
|
-
* @returns {string} Formatted duration string
|
|
210
|
+
* @returns {string} Formatted duration string (e.g., "0.273s")
|
|
195
211
|
*/
|
|
196
212
|
function formatDuration(durationMs) {
|
|
197
213
|
const seconds = durationMs / 1000;
|
|
198
214
|
if (seconds < 0.001) {
|
|
199
|
-
return '0.
|
|
215
|
+
return '0.001s';
|
|
200
216
|
} else if (seconds < 10) {
|
|
201
217
|
// For durations under 10 seconds, show 3 decimal places
|
|
202
|
-
return seconds.toFixed(3)
|
|
218
|
+
return `${seconds.toFixed(3)}s`;
|
|
203
219
|
} else if (seconds < 100) {
|
|
204
|
-
return seconds.toFixed(2)
|
|
220
|
+
return `${seconds.toFixed(2)}s`;
|
|
205
221
|
} else {
|
|
206
|
-
return seconds.toFixed(1)
|
|
222
|
+
return `${seconds.toFixed(1)}s`;
|
|
207
223
|
}
|
|
208
224
|
}
|
|
209
225
|
|
|
210
226
|
/**
|
|
211
|
-
* Create a finish block for command execution
|
|
227
|
+
* Create a finish block for command execution using status spine format
|
|
228
|
+
*
|
|
229
|
+
* Bottom block ordering rules:
|
|
230
|
+
* 1. Result marker (✓ or ✗)
|
|
231
|
+
* 2. finish timestamp
|
|
232
|
+
* 3. duration
|
|
233
|
+
* 4. exit code
|
|
234
|
+
* 5. (repeated isolation metadata, if any)
|
|
235
|
+
* 6. empty spine line
|
|
236
|
+
* 7. log path (always second-to-last)
|
|
237
|
+
* 8. session ID (always last)
|
|
238
|
+
*
|
|
212
239
|
* @param {object} options - Options for the block
|
|
213
240
|
* @param {string} options.sessionId - Session UUID
|
|
214
241
|
* @param {string} options.timestamp - Timestamp string
|
|
215
242
|
* @param {number} options.exitCode - Exit code
|
|
216
243
|
* @param {string} options.logPath - Path to log file
|
|
217
244
|
* @param {number} [options.durationMs] - Duration in milliseconds
|
|
218
|
-
* @param {string} [options.resultMessage] - Result message (
|
|
219
|
-
* @param {string} [options.
|
|
220
|
-
* @param {
|
|
221
|
-
* @
|
|
245
|
+
* @param {string} [options.resultMessage] - Result message (ignored in new format)
|
|
246
|
+
* @param {string[]} [options.extraLines] - Isolation info for repetition in footer
|
|
247
|
+
* @param {string} [options.style] - Ignored (kept for backward compatibility)
|
|
248
|
+
* @param {number} [options.width] - Ignored (kept for backward compatibility)
|
|
249
|
+
* @returns {string} Formatted finish block in spine format
|
|
222
250
|
*/
|
|
223
251
|
function createFinishBlock(options) {
|
|
224
252
|
const {
|
|
@@ -227,36 +255,36 @@ function createFinishBlock(options) {
|
|
|
227
255
|
exitCode,
|
|
228
256
|
logPath,
|
|
229
257
|
durationMs,
|
|
230
|
-
|
|
231
|
-
style: styleName = DEFAULT_STYLE,
|
|
232
|
-
width = DEFAULT_WIDTH,
|
|
258
|
+
extraLines = [],
|
|
233
259
|
} = options;
|
|
234
260
|
|
|
235
|
-
const style = getBoxStyle(styleName);
|
|
236
261
|
const lines = [];
|
|
237
262
|
|
|
238
|
-
//
|
|
239
|
-
|
|
263
|
+
// Result marker appears first in footer (after program output)
|
|
264
|
+
lines.push(getResultMarker(exitCode));
|
|
265
|
+
|
|
266
|
+
// Finish metadata
|
|
267
|
+
lines.push(createSpineLine('finish', timestamp));
|
|
268
|
+
|
|
240
269
|
if (durationMs !== undefined && durationMs !== null) {
|
|
241
|
-
|
|
270
|
+
lines.push(createSpineLine('duration', formatDuration(durationMs)));
|
|
242
271
|
}
|
|
243
272
|
|
|
244
|
-
lines.push(
|
|
273
|
+
lines.push(createSpineLine('exit', String(exitCode)));
|
|
245
274
|
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
if (
|
|
249
|
-
lines.push(
|
|
275
|
+
// Repeat isolation metadata if present
|
|
276
|
+
const metadata = parseIsolationMetadata(extraLines);
|
|
277
|
+
if (metadata.isolation) {
|
|
278
|
+
lines.push(createEmptySpineLine());
|
|
279
|
+
lines.push(...generateIsolationLines(metadata));
|
|
250
280
|
}
|
|
251
281
|
|
|
252
|
-
|
|
253
|
-
lines.push(
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
lines.push(
|
|
257
|
-
|
|
258
|
-
);
|
|
259
|
-
lines.push(createBottomBorder(width, style));
|
|
282
|
+
// Empty spine line before final two entries
|
|
283
|
+
lines.push(createEmptySpineLine());
|
|
284
|
+
|
|
285
|
+
// Log and session are ALWAYS last (in that order)
|
|
286
|
+
lines.push(createSpineLine('log', logPath));
|
|
287
|
+
lines.push(createSpineLine('session', sessionId));
|
|
260
288
|
|
|
261
289
|
return lines.join('\n');
|
|
262
290
|
}
|
|
@@ -371,17 +399,23 @@ function formatAsNestedLinksNotation(obj, indent = 2, depth = 0) {
|
|
|
371
399
|
}
|
|
372
400
|
|
|
373
401
|
module.exports = {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
402
|
+
// Status spine format API
|
|
403
|
+
SPINE,
|
|
404
|
+
SUCCESS_MARKER,
|
|
405
|
+
FAILURE_MARKER,
|
|
406
|
+
createSpineLine,
|
|
407
|
+
createEmptySpineLine,
|
|
408
|
+
createCommandLine,
|
|
409
|
+
getResultMarker,
|
|
410
|
+
parseIsolationMetadata,
|
|
411
|
+
generateIsolationLines,
|
|
412
|
+
|
|
413
|
+
// Main block creation functions
|
|
382
414
|
createStartBlock,
|
|
383
415
|
createFinishBlock,
|
|
384
416
|
formatDuration,
|
|
417
|
+
|
|
418
|
+
// Links notation utilities
|
|
385
419
|
escapeForLinksNotation,
|
|
386
420
|
formatAsNestedLinksNotation,
|
|
387
421
|
};
|
package/test/args-parser.test.js
CHANGED
|
@@ -199,10 +199,26 @@ describe('parseArgs', () => {
|
|
|
199
199
|
assert.strictEqual(result.wrapperOptions.image, 'alpine:latest');
|
|
200
200
|
});
|
|
201
201
|
|
|
202
|
-
it('should
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
202
|
+
it('should use default OS-matched image for docker without --image', () => {
|
|
203
|
+
const result = parseArgs(['--isolated', 'docker', '--', 'npm', 'test']);
|
|
204
|
+
// Should have a default image set (OS-matched)
|
|
205
|
+
assert.ok(
|
|
206
|
+
result.wrapperOptions.image,
|
|
207
|
+
'Expected default image to be set'
|
|
208
|
+
);
|
|
209
|
+
// Should be one of the known default images
|
|
210
|
+
const knownDefaults = [
|
|
211
|
+
'alpine:latest',
|
|
212
|
+
'ubuntu:latest',
|
|
213
|
+
'debian:latest',
|
|
214
|
+
'archlinux:latest',
|
|
215
|
+
'fedora:latest',
|
|
216
|
+
'centos:latest',
|
|
217
|
+
];
|
|
218
|
+
assert.ok(
|
|
219
|
+
knownDefaults.includes(result.wrapperOptions.image),
|
|
220
|
+
`Expected image to be one of ${knownDefaults.join(', ')}, got ${result.wrapperOptions.image}`
|
|
221
|
+
);
|
|
206
222
|
});
|
|
207
223
|
|
|
208
224
|
it('should throw error for image with non-docker backend', () => {
|