start-command 0.18.0 → 0.19.1
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 +26 -0
- package/package.json +1 -1
- package/src/bin/cli.js +6 -5
- package/src/lib/output-blocks.js +211 -177
- package/test/cli.test.js +94 -0
- package/test/echo-integration.test.js +106 -97
- package/test/output-blocks.test.js +215 -74
- package/test/ssh-integration.test.js +5 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# start-command
|
|
2
2
|
|
|
3
|
+
## 0.19.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- c84c1bb: fix: Always display session/container name in isolation output
|
|
8
|
+
|
|
9
|
+
When using isolation backends (screen, docker, tmux), the output now shows the actual session/container name that users need to reconnect to sessions, especially in detached mode. Previously, only the session UUID was shown, but users need the actual backend name to:
|
|
10
|
+
- Reconnect to detached screen sessions: `screen -r <name>`
|
|
11
|
+
- Attach to tmux sessions: `tmux attach -t <name>`
|
|
12
|
+
- View Docker container logs: `docker logs <name>`
|
|
13
|
+
- Remove containers: `docker rm -f <name>`
|
|
14
|
+
|
|
15
|
+
Fixes #67
|
|
16
|
+
|
|
17
|
+
## 0.19.0
|
|
18
|
+
|
|
19
|
+
### Minor Changes
|
|
20
|
+
|
|
21
|
+
- 112a78e: Replace fixed-width box output with status spine format
|
|
22
|
+
- Width-independent output that doesn't truncate or create jagged boxes
|
|
23
|
+
- All metadata visible and copy-pasteable (log paths, session IDs)
|
|
24
|
+
- Works uniformly in TTY, tmux, SSH, CI, and log files
|
|
25
|
+
- Clear visual distinction: │ for metadata, $ for command, no prefix for output
|
|
26
|
+
- Result markers ✓ and ✗ for success/failure
|
|
27
|
+
- Isolation metadata repeated in footer for context
|
|
28
|
+
|
|
3
29
|
## 0.18.0
|
|
4
30
|
|
|
5
31
|
### Minor Changes
|
package/package.json
CHANGED
package/src/bin/cli.js
CHANGED
|
@@ -470,9 +470,10 @@ async function runWithIsolation(
|
|
|
470
470
|
// Add isolation info to extra lines
|
|
471
471
|
if (environment) {
|
|
472
472
|
extraLines.push(`[Isolation] Environment: ${environment}, Mode: ${mode}`);
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
473
|
+
// Always add the session name so users can reconnect to detached sessions
|
|
474
|
+
// This is important for screen, tmux, docker where the session/container name
|
|
475
|
+
// is different from the session UUID used for tracking (see issue #67)
|
|
476
|
+
extraLines.push(`[Isolation] Session: ${sessionName}`);
|
|
476
477
|
}
|
|
477
478
|
if (effectiveImage) {
|
|
478
479
|
extraLines.push(`[Isolation] Image: ${effectiveImage}`);
|
|
@@ -623,7 +624,7 @@ async function runWithIsolation(
|
|
|
623
624
|
console.log('');
|
|
624
625
|
}
|
|
625
626
|
|
|
626
|
-
// Print finish block with
|
|
627
|
+
// Print finish block with isolation metadata repeated
|
|
627
628
|
// Add empty line before finish block for visual separation
|
|
628
629
|
console.log('');
|
|
629
630
|
const durationMs = Date.now() - startTimeMs;
|
|
@@ -634,7 +635,7 @@ async function runWithIsolation(
|
|
|
634
635
|
exitCode,
|
|
635
636
|
logPath: logFilePath,
|
|
636
637
|
durationMs,
|
|
637
|
-
|
|
638
|
+
extraLines, // Pass extraLines for isolation metadata repetition in footer
|
|
638
639
|
})
|
|
639
640
|
);
|
|
640
641
|
|
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/cli.test.js
CHANGED
|
@@ -122,3 +122,97 @@ describe('CLI basic behavior', () => {
|
|
|
122
122
|
assert.ok(result.stdout.includes('Usage:'), 'Should display usage');
|
|
123
123
|
});
|
|
124
124
|
});
|
|
125
|
+
|
|
126
|
+
describe('CLI isolation output (issue #67)', () => {
|
|
127
|
+
const { isCommandAvailable } = require('../src/lib/isolation');
|
|
128
|
+
|
|
129
|
+
it('should display screen session name when using screen isolation', async () => {
|
|
130
|
+
if (!isCommandAvailable('screen')) {
|
|
131
|
+
console.log(' Skipping: screen not installed');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const result = runCLI(['-i', 'screen', '--', 'echo', 'hello']);
|
|
136
|
+
|
|
137
|
+
// The output should contain the screen session name (in format screen-timestamp-random)
|
|
138
|
+
// Check that the session UUID is displayed
|
|
139
|
+
assert.ok(
|
|
140
|
+
result.stdout.includes('│ session'),
|
|
141
|
+
'Should display session UUID'
|
|
142
|
+
);
|
|
143
|
+
// Check that screen isolation info is displayed
|
|
144
|
+
assert.ok(
|
|
145
|
+
result.stdout.includes('│ isolation screen'),
|
|
146
|
+
'Should display screen isolation'
|
|
147
|
+
);
|
|
148
|
+
// Check that the actual screen session name is displayed (issue #67 fix)
|
|
149
|
+
assert.ok(
|
|
150
|
+
result.stdout.includes('│ screen screen-'),
|
|
151
|
+
'Should display actual screen session name for reconnection (issue #67)'
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should display tmux session name when using tmux isolation', async () => {
|
|
156
|
+
if (!isCommandAvailable('tmux')) {
|
|
157
|
+
console.log(' Skipping: tmux not installed');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const result = runCLI(['-i', 'tmux', '--', 'echo', 'hello']);
|
|
162
|
+
|
|
163
|
+
// The output should contain the tmux session name
|
|
164
|
+
assert.ok(
|
|
165
|
+
result.stdout.includes('│ session'),
|
|
166
|
+
'Should display session UUID'
|
|
167
|
+
);
|
|
168
|
+
assert.ok(
|
|
169
|
+
result.stdout.includes('│ isolation tmux'),
|
|
170
|
+
'Should display tmux isolation'
|
|
171
|
+
);
|
|
172
|
+
// Check that the actual tmux session name is displayed (issue #67 fix)
|
|
173
|
+
assert.ok(
|
|
174
|
+
result.stdout.includes('│ tmux tmux-'),
|
|
175
|
+
'Should display actual tmux session name for reconnection (issue #67)'
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should display docker container name when using docker isolation', async () => {
|
|
180
|
+
const { canRunLinuxDockerImages } = require('../src/lib/isolation');
|
|
181
|
+
|
|
182
|
+
if (!canRunLinuxDockerImages()) {
|
|
183
|
+
console.log(
|
|
184
|
+
' Skipping: docker not available or cannot run Linux images'
|
|
185
|
+
);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const result = runCLI([
|
|
190
|
+
'-i',
|
|
191
|
+
'docker',
|
|
192
|
+
'--image',
|
|
193
|
+
'alpine:latest',
|
|
194
|
+
'--',
|
|
195
|
+
'echo',
|
|
196
|
+
'hello',
|
|
197
|
+
]);
|
|
198
|
+
|
|
199
|
+
// The output should contain the docker container name
|
|
200
|
+
assert.ok(
|
|
201
|
+
result.stdout.includes('│ session'),
|
|
202
|
+
'Should display session UUID'
|
|
203
|
+
);
|
|
204
|
+
assert.ok(
|
|
205
|
+
result.stdout.includes('│ isolation docker'),
|
|
206
|
+
'Should display docker isolation'
|
|
207
|
+
);
|
|
208
|
+
assert.ok(
|
|
209
|
+
result.stdout.includes('│ image alpine:latest'),
|
|
210
|
+
'Should display docker image'
|
|
211
|
+
);
|
|
212
|
+
// Check that the actual container name is displayed (issue #67 fix)
|
|
213
|
+
assert.ok(
|
|
214
|
+
result.stdout.includes('│ container docker-'),
|
|
215
|
+
'Should display actual container name for reconnection (issue #67)'
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
});
|