start-command 0.7.2 → 0.7.4
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 +8 -0
- package/docs/case-studies/issue-25/README.md +225 -0
- package/docs/case-studies/issue-25/issue-data.json +21 -0
- package/experiments/test-screen-tee-debug.js +237 -0
- package/experiments/test-screen-tee-fallback.js +230 -0
- package/package.json +1 -1
- package/src/lib/isolation.js +18 -3
- package/test/isolation.test.js +50 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# start-command
|
|
2
2
|
|
|
3
|
+
## 0.7.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d058c43: fix: Screen isolation output not captured for quoted commands
|
|
8
|
+
|
|
9
|
+
This fixes issue #25 where commands with quoted strings (e.g., echo "hello") would not show their output when using screen isolation. The fix uses spawnSync with array arguments instead of execSync with a constructed string to avoid shell quoting issues.
|
|
10
|
+
|
|
3
11
|
## 0.7.2
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# Case Study: Issue #25 - Screen Isolation Output Missing
|
|
2
|
+
|
|
3
|
+
## Issue Summary
|
|
4
|
+
|
|
5
|
+
**Issue URL:** https://github.com/link-foundation/start/issues/25
|
|
6
|
+
**Date Reported:** 2025-12-23
|
|
7
|
+
**Reporter:** @konard
|
|
8
|
+
**Status:** Resolved
|
|
9
|
+
|
|
10
|
+
### Problem Statement
|
|
11
|
+
|
|
12
|
+
When running commands with screen isolation in attached mode (without `-d`/`--detached`), the command output is not displayed. Specifically:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
$ --isolated screen --verbose -- echo "hello"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Shows `[screen is terminating]` but no "hello" output, even though the command executes successfully with exit code 0.
|
|
19
|
+
|
|
20
|
+
### Environment
|
|
21
|
+
|
|
22
|
+
- **Platform:** macOS 15.7.2
|
|
23
|
+
- **Package:** start-command@0.7.2
|
|
24
|
+
- **Screen version:** macOS bundled 4.00.03 (FAU) 23-Oct-06
|
|
25
|
+
- **Bun Version:** 1.2.20
|
|
26
|
+
- **Architecture:** arm64
|
|
27
|
+
|
|
28
|
+
## Timeline of Events
|
|
29
|
+
|
|
30
|
+
1. User installs start-command: `bun install -g start-command`
|
|
31
|
+
2. Direct command execution works: `$ echo "hello"` shows "hello"
|
|
32
|
+
3. Docker isolation works: `$ --isolated docker --image alpine -- echo "hello"` shows "hello"
|
|
33
|
+
4. **Screen isolation fails**: `$ --isolated screen -- echo "hello"` shows only `[screen is terminating]`
|
|
34
|
+
|
|
35
|
+
## Observed Behavior
|
|
36
|
+
|
|
37
|
+
### Expected
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
$ --isolated screen --verbose -- echo "hello"
|
|
41
|
+
[2025-12-23 20:56:28.265] Starting: echo hello
|
|
42
|
+
|
|
43
|
+
[Isolation] Environment: screen, Mode: attached
|
|
44
|
+
|
|
45
|
+
hello
|
|
46
|
+
|
|
47
|
+
Screen session "screen-1766523388276-4oecji" exited with code 0
|
|
48
|
+
|
|
49
|
+
[2025-12-23 20:56:28.362] Finished
|
|
50
|
+
Exit code: 0
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Actual (Before Fix)
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
$ --isolated screen --verbose -- echo "hello"
|
|
57
|
+
[2025-12-23 20:56:28.265] Starting: echo hello
|
|
58
|
+
|
|
59
|
+
[Isolation] Environment: screen, Mode: attached
|
|
60
|
+
|
|
61
|
+
[screen is terminating]
|
|
62
|
+
|
|
63
|
+
Screen session "screen-1766523388276-4oecji" exited with code 0
|
|
64
|
+
|
|
65
|
+
[2025-12-23 20:56:28.362] Finished
|
|
66
|
+
Exit code: 0
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Notice:** No "hello" output in the screen isolation case, though exit code is 0.
|
|
70
|
+
|
|
71
|
+
## Root Cause Analysis
|
|
72
|
+
|
|
73
|
+
### PRIMARY ROOT CAUSE: Shell Quoting Issues with execSync
|
|
74
|
+
|
|
75
|
+
The issue was in the `runScreenWithLogCapture` function in `src/lib/isolation.js`.
|
|
76
|
+
|
|
77
|
+
**The Problematic Code:**
|
|
78
|
+
|
|
79
|
+
```javascript
|
|
80
|
+
execSync(`screen ${screenArgs.map((a) => `"${a}"`).join(' ')}`, {
|
|
81
|
+
stdio: 'inherit',
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
This code constructs a shell command string by wrapping each argument in double quotes. However, when the command being executed already contains double quotes (like `echo "hello"`), the nested quoting breaks the shell parsing.
|
|
86
|
+
|
|
87
|
+
**Example of Broken Command:**
|
|
88
|
+
|
|
89
|
+
For the command `echo "hello"`:
|
|
90
|
+
|
|
91
|
+
1. `effectiveCommand` becomes: `(echo "hello") 2>&1 | tee "/tmp/...log"`
|
|
92
|
+
2. `screenArgs` is: `['-dmS', 'session-name', '/bin/sh', '-c', '(echo "hello") 2>&1 | tee "/tmp/...log"']`
|
|
93
|
+
3. After wrapping with `"${a}"`:
|
|
94
|
+
```
|
|
95
|
+
screen "-dmS" "session-name" "/bin/sh" "-c" "(echo "hello") 2>&1 | tee "/tmp/...log""
|
|
96
|
+
```
|
|
97
|
+
4. **Problem**: The nested double quotes cause shell parsing errors - the shell sees `hello` as a separate token!
|
|
98
|
+
|
|
99
|
+
**Why Simple Commands Worked:**
|
|
100
|
+
|
|
101
|
+
Commands without quotes (like `echo hello` without the quotes) worked because there was no quoting conflict.
|
|
102
|
+
|
|
103
|
+
### Experimental Evidence
|
|
104
|
+
|
|
105
|
+
We created `experiments/test-screen-tee-debug.js` to test different approaches:
|
|
106
|
+
|
|
107
|
+
| Test | Command | Result |
|
|
108
|
+
| ------ | ----------------------------------------------- | -------------------------------- |
|
|
109
|
+
| Test 3 | `echo "hello"` (simple) | SUCCESS |
|
|
110
|
+
| Test 4 | `echo "hello from attached mode"` (with spaces) | **FAILED** - No log file created |
|
|
111
|
+
| Test 5 | Same with escaped quotes | SUCCESS |
|
|
112
|
+
| Test 6 | Using `spawnSync` with array | **SUCCESS** |
|
|
113
|
+
|
|
114
|
+
The experiments clearly showed that:
|
|
115
|
+
|
|
116
|
+
1. Commands with spaces in quoted strings fail with `execSync` + string construction
|
|
117
|
+
2. Using `spawnSync` with an array of arguments works correctly
|
|
118
|
+
|
|
119
|
+
### Why spawnSync Works
|
|
120
|
+
|
|
121
|
+
Node.js/Bun's `spawnSync` with array arguments:
|
|
122
|
+
|
|
123
|
+
- Passes arguments directly to the process without shell interpretation
|
|
124
|
+
- Each array element becomes a separate argv entry
|
|
125
|
+
- No shell quoting issues - the quotes in the command are preserved as-is
|
|
126
|
+
|
|
127
|
+
## The Solution
|
|
128
|
+
|
|
129
|
+
### Code Changes in `src/lib/isolation.js`
|
|
130
|
+
|
|
131
|
+
1. **Added `spawnSync` import:**
|
|
132
|
+
|
|
133
|
+
```javascript
|
|
134
|
+
const { execSync, spawn, spawnSync } = require('child_process');
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
2. **Replaced `execSync` with `spawnSync` in two locations:**
|
|
138
|
+
|
|
139
|
+
**Location 1: `runScreenWithLogCapture` function (attached mode with log capture)**
|
|
140
|
+
|
|
141
|
+
```javascript
|
|
142
|
+
// Before (broken):
|
|
143
|
+
execSync(`screen ${screenArgs.map((a) => `"${a}"`).join(' ')}`, {
|
|
144
|
+
stdio: 'inherit',
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// After (fixed):
|
|
148
|
+
const result = spawnSync('screen', screenArgs, {
|
|
149
|
+
stdio: 'inherit',
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (result.error) {
|
|
153
|
+
throw result.error;
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Location 2: `runInScreen` function (detached mode)**
|
|
158
|
+
|
|
159
|
+
```javascript
|
|
160
|
+
// Same pattern - replaced execSync with spawnSync
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Testing Strategy
|
|
164
|
+
|
|
165
|
+
### New Regression Tests Added
|
|
166
|
+
|
|
167
|
+
Two new tests were added to `test/isolation.test.js`:
|
|
168
|
+
|
|
169
|
+
1. **Test: should capture output from commands with quoted strings (issue #25)**
|
|
170
|
+
- Tests: `echo "hello"`
|
|
171
|
+
- Verifies the exact scenario from issue #25
|
|
172
|
+
|
|
173
|
+
2. **Test: should capture output from commands with complex quoted strings**
|
|
174
|
+
- Tests: `echo "hello from attached mode"`
|
|
175
|
+
- Verifies commands with spaces inside quotes work
|
|
176
|
+
|
|
177
|
+
### Test Results
|
|
178
|
+
|
|
179
|
+
All 25 isolation tests pass:
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
bun test test/isolation.test.js
|
|
183
|
+
|
|
184
|
+
Captured quoted output: "hello"
|
|
185
|
+
Captured complex quote output: "hello from attached mode"
|
|
186
|
+
|
|
187
|
+
25 pass
|
|
188
|
+
0 fail
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Key Learnings
|
|
192
|
+
|
|
193
|
+
1. **String construction for shell commands is fragile**: When building shell command strings, nested quoting can cause silent failures.
|
|
194
|
+
|
|
195
|
+
2. **Prefer array-based process spawning**: `spawnSync`/`spawn` with arrays are more robust than `execSync` with constructed strings.
|
|
196
|
+
|
|
197
|
+
3. **Test with varied input**: Simple commands may work while complex ones fail - test with real-world examples including quotes and spaces.
|
|
198
|
+
|
|
199
|
+
4. **Debug systematically**: Creating experiments (`test-screen-tee-debug.js`) helped isolate the exact failure mode.
|
|
200
|
+
|
|
201
|
+
## Connection to Previous Issues
|
|
202
|
+
|
|
203
|
+
This issue is related to Issue #15 (Screen Isolation Not Working As Expected) which addressed a different root cause:
|
|
204
|
+
|
|
205
|
+
- Issue #15: macOS Screen version incompatibility (lacking `-Logfile` option)
|
|
206
|
+
- Issue #25: Shell quoting issues in the tee fallback approach (used for older screen versions)
|
|
207
|
+
|
|
208
|
+
Both issues together ensure screen isolation works on:
|
|
209
|
+
|
|
210
|
+
- Modern screen (>= 4.5.1) with native `-Logfile` support
|
|
211
|
+
- Older screen (< 4.5.1, like macOS bundled 4.0.3) with tee fallback
|
|
212
|
+
|
|
213
|
+
## Files Modified
|
|
214
|
+
|
|
215
|
+
1. `src/lib/isolation.js` - Core fix: use `spawnSync` instead of `execSync`
|
|
216
|
+
2. `test/isolation.test.js` - Added 2 regression tests for issue #25
|
|
217
|
+
3. `experiments/test-screen-tee-fallback.js` - Experiment script (new)
|
|
218
|
+
4. `experiments/test-screen-tee-debug.js` - Debug experiment script (new)
|
|
219
|
+
|
|
220
|
+
## References
|
|
221
|
+
|
|
222
|
+
- [Node.js child_process.spawnSync](https://nodejs.org/api/child_process.html#child_processspawnsynccommand-args-options)
|
|
223
|
+
- [GNU Screen Manual](https://www.gnu.org/software/screen/manual/screen.html)
|
|
224
|
+
- [Issue #15 Case Study](../issue-15/README.md)
|
|
225
|
+
- [Issue #22 Case Study](../issue-22/analysis.md)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"author": {
|
|
3
|
+
"id": "MDQ6VXNlcjE0MzE5MDQ=",
|
|
4
|
+
"is_bot": false,
|
|
5
|
+
"login": "konard",
|
|
6
|
+
"name": "Konstantin Diachenko"
|
|
7
|
+
},
|
|
8
|
+
"body": "```\nkonard@MacBook-Pro-Konstantin ~ % bun install -g start-command \nbun add v1.2.20 (6ad208bc)\n\ninstalled start-command@0.7.2 with binaries:\n - $\n\n1 package installed [2.34s]\nkonard@MacBook-Pro-Konstantin ~ % $ --version \nstart-command version: 0.7.2\n\nOS: darwin\nOS Version: 15.7.2\nBun Version: 1.2.20\nArchitecture: arm64\n\nIsolation tools:\n screen: Screen version 4.00.03 (FAU) 23-Oct-06\n tmux: not installed\n docker: Docker version 28.5.1, build e180ab8\nkonard@MacBook-Pro-Konstantin ~ % $ --version -- \nstart-command version: 0.7.2\n\nOS: darwin\nOS Version: 15.7.2\nBun Version: 1.2.20\nArchitecture: arm64\n\nIsolation tools:\n screen: Screen version 4.00.03 (FAU) 23-Oct-06\n tmux: not installed\n docker: Docker version 28.5.1, build e180ab8\nkonard@MacBook-Pro-Konstantin ~ % $ --isolated screen --verbose -- echo \"hello\"\n[2025-12-23 20:56:28.265] Starting: echo hello\n\n[Isolation] Environment: screen, Mode: attached\n\n[screen is terminating]\n\nScreen session \"screen-1766523388276-4oecji\" exited with code 0\n\n[2025-12-23 20:56:28.362] Finished\nExit code: 0\nLog saved: /var/folders/cl/831lqjgd58v5mb_m74cfdfcw0000gn/T/start-command-screen-1766523388265-j5puqf.log\nkonard@MacBook-Pro-Konstantin ~ % $ echo \"hello\" \n[2025-12-23 20:56:37.680] Starting: echo hello\n\nhello\n\n[2025-12-23 20:56:37.688] Finished\nExit code: 0\nLog saved: /var/folders/cl/831lqjgd58v5mb_m74cfdfcw0000gn/T/start-command-1766523397680-fb62fx.log\nkonard@MacBook-Pro-Konstantin ~ % $ --isolated docker --image alpine -- echo \"hello\"\n[2025-12-23 20:56:45.619] Starting: echo hello\n\n[Isolation] Environment: docker, Mode: attached\n[Isolation] Image: alpine\n\nhello\n\nDocker container \"docker-1766523405627-qgv7h1\" exited with code 0\n\n[2025-12-23 20:56:47.091] Finished\nExit code: 0\nLog saved: /var/folders/cl/831lqjgd58v5mb_m74cfdfcw0000gn/T/start-command-docker-1766523405619-8izdfr.log\nkonard@MacBook-Pro-Konstantin ~ % \n```\n\nMake sure we have test on macOS that does specifically reproduces this exact error. After that it must be fixed. And that test will guarantee we will never get regression again.\n\nPlease download all logs and data related about the issue to this repository, make sure we compile that data to `./docs/case-studies/issue-{id}` folder, and use it to do deep case study analysis (also make sure to search online for additional facts and data), in which we will reconstruct timeline/sequence of events, find root causes of the problem, and propose possible solutions.",
|
|
9
|
+
"createdAt": "2025-12-23T20:57:23Z",
|
|
10
|
+
"labels": [
|
|
11
|
+
{
|
|
12
|
+
"id": "LA_kwDOP85RQM8AAAACMMqWww",
|
|
13
|
+
"name": "bug",
|
|
14
|
+
"description": "Something isn't working",
|
|
15
|
+
"color": "d73a4a"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"number": 25,
|
|
19
|
+
"state": "OPEN",
|
|
20
|
+
"title": "We don't get `Hello` output from `$ --isolated screen --verbose -- echo \"hello\"` command"
|
|
21
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Debug experiment to find the root cause of issue #25
|
|
4
|
+
* We're testing different ways of running screen commands with tee
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { execSync, spawn } = require('child_process');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
|
|
12
|
+
async function sleep(ms) {
|
|
13
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function testDebug() {
|
|
17
|
+
console.log('=== Debugging Screen Tee Fallback ===\n');
|
|
18
|
+
|
|
19
|
+
// Test 1: Simple command without quotes issues
|
|
20
|
+
console.log('Test 1: Simple command (no spaces in the command)');
|
|
21
|
+
const sessionName1 = `debug1-${Date.now()}`;
|
|
22
|
+
const logFile1 = path.join(os.tmpdir(), `debug1-${sessionName1}.log`);
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// Direct screen command with tee
|
|
26
|
+
const cmd = `screen -dmS "${sessionName1}" /bin/sh -c "(echo hello) 2>&1 | tee \\"${logFile1}\\""`;
|
|
27
|
+
console.log(` Command: ${cmd}`);
|
|
28
|
+
|
|
29
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
30
|
+
await sleep(500);
|
|
31
|
+
|
|
32
|
+
if (fs.existsSync(logFile1)) {
|
|
33
|
+
console.log(
|
|
34
|
+
` Log content: "${fs.readFileSync(logFile1, 'utf8').trim()}"`
|
|
35
|
+
);
|
|
36
|
+
console.log(` Result: SUCCESS ✓`);
|
|
37
|
+
fs.unlinkSync(logFile1);
|
|
38
|
+
} else {
|
|
39
|
+
console.log(` Result: FAILED - No log file ✗`);
|
|
40
|
+
}
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.log(` Error: ${e.message}`);
|
|
43
|
+
}
|
|
44
|
+
console.log('');
|
|
45
|
+
|
|
46
|
+
// Test 2: Command with nested quotes
|
|
47
|
+
console.log('Test 2: Command with "hello" (has quotes)');
|
|
48
|
+
const sessionName2 = `debug2-${Date.now()}`;
|
|
49
|
+
const logFile2 = path.join(os.tmpdir(), `debug2-${sessionName2}.log`);
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
// Note: The original command has quotes: echo "hello"
|
|
53
|
+
// When we wrap it with tee, the quoting becomes complex
|
|
54
|
+
const cmd = `screen -dmS "${sessionName2}" /bin/sh -c "(echo \\"hello\\") 2>&1 | tee \\"${logFile2}\\""`;
|
|
55
|
+
console.log(` Command: ${cmd}`);
|
|
56
|
+
|
|
57
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
58
|
+
await sleep(500);
|
|
59
|
+
|
|
60
|
+
if (fs.existsSync(logFile2)) {
|
|
61
|
+
console.log(
|
|
62
|
+
` Log content: "${fs.readFileSync(logFile2, 'utf8').trim()}"`
|
|
63
|
+
);
|
|
64
|
+
console.log(` Result: SUCCESS ✓`);
|
|
65
|
+
fs.unlinkSync(logFile2);
|
|
66
|
+
} else {
|
|
67
|
+
console.log(` Result: FAILED - No log file ✗`);
|
|
68
|
+
}
|
|
69
|
+
} catch (e) {
|
|
70
|
+
console.log(` Error: ${e.message}`);
|
|
71
|
+
}
|
|
72
|
+
console.log('');
|
|
73
|
+
|
|
74
|
+
// Test 3: Using array-based command (like the current implementation)
|
|
75
|
+
console.log('Test 3: Using array-based args (current implementation style)');
|
|
76
|
+
const sessionName3 = `debug3-${Date.now()}`;
|
|
77
|
+
const logFile3 = path.join(os.tmpdir(), `debug3-${sessionName3}.log`);
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// This is what the current code does
|
|
81
|
+
const command = 'echo "hello"';
|
|
82
|
+
const effectiveCommand = `(${command}) 2>&1 | tee "${logFile3}"`;
|
|
83
|
+
const screenArgs = [
|
|
84
|
+
'-dmS',
|
|
85
|
+
sessionName3,
|
|
86
|
+
'/bin/sh',
|
|
87
|
+
'-c',
|
|
88
|
+
effectiveCommand,
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
// Construct the command string as the code does
|
|
92
|
+
const cmdStr = `screen ${screenArgs.map((a) => `"${a}"`).join(' ')}`;
|
|
93
|
+
console.log(` Constructed command: ${cmdStr}`);
|
|
94
|
+
|
|
95
|
+
execSync(cmdStr, { stdio: 'inherit' });
|
|
96
|
+
await sleep(500);
|
|
97
|
+
|
|
98
|
+
if (fs.existsSync(logFile3)) {
|
|
99
|
+
console.log(
|
|
100
|
+
` Log content: "${fs.readFileSync(logFile3, 'utf8').trim()}"`
|
|
101
|
+
);
|
|
102
|
+
console.log(` Result: SUCCESS ✓`);
|
|
103
|
+
fs.unlinkSync(logFile3);
|
|
104
|
+
} else {
|
|
105
|
+
console.log(` Result: FAILED - No log file ✗`);
|
|
106
|
+
}
|
|
107
|
+
} catch (e) {
|
|
108
|
+
console.log(` Error: ${e.message}`);
|
|
109
|
+
}
|
|
110
|
+
console.log('');
|
|
111
|
+
|
|
112
|
+
// Test 4: Check what happens with the nested quotes
|
|
113
|
+
console.log('Test 4: Checking quote escaping issue');
|
|
114
|
+
const sessionName4 = `debug4-${Date.now()}`;
|
|
115
|
+
const logFile4 = path.join(os.tmpdir(), `debug4-${sessionName4}.log`);
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const command = 'echo "hello from attached mode"';
|
|
119
|
+
const effectiveCommand = `(${command}) 2>&1 | tee "${logFile4}"`;
|
|
120
|
+
console.log(` effectiveCommand: ${effectiveCommand}`);
|
|
121
|
+
|
|
122
|
+
// When we quote each arg with `"${a}"`, the command becomes double-quoted
|
|
123
|
+
// which can cause issues with nested quotes
|
|
124
|
+
const screenArgs = [
|
|
125
|
+
'-dmS',
|
|
126
|
+
sessionName4,
|
|
127
|
+
'/bin/sh',
|
|
128
|
+
'-c',
|
|
129
|
+
effectiveCommand,
|
|
130
|
+
];
|
|
131
|
+
const cmdStr = `screen ${screenArgs.map((a) => `"${a}"`).join(' ')}`;
|
|
132
|
+
console.log(` Full command: ${cmdStr}`);
|
|
133
|
+
|
|
134
|
+
// The problem: effectiveCommand has double quotes inside,
|
|
135
|
+
// and we're wrapping it with MORE double quotes
|
|
136
|
+
// This results in: screen "-dmS" "debug4-xxx" "/bin/sh" "-c" "(echo "hello from attached mode") 2>&1 | tee "...""
|
|
137
|
+
// The nested double quotes break the shell parsing!
|
|
138
|
+
|
|
139
|
+
execSync(cmdStr, { stdio: 'inherit' });
|
|
140
|
+
await sleep(500);
|
|
141
|
+
|
|
142
|
+
if (fs.existsSync(logFile4)) {
|
|
143
|
+
console.log(
|
|
144
|
+
` Log content: "${fs.readFileSync(logFile4, 'utf8').trim()}"`
|
|
145
|
+
);
|
|
146
|
+
console.log(` Result: SUCCESS ✓`);
|
|
147
|
+
fs.unlinkSync(logFile4);
|
|
148
|
+
} else {
|
|
149
|
+
console.log(` Result: FAILED - No log file ✗`);
|
|
150
|
+
}
|
|
151
|
+
} catch (e) {
|
|
152
|
+
console.log(` Error: ${e.message}`);
|
|
153
|
+
}
|
|
154
|
+
console.log('');
|
|
155
|
+
|
|
156
|
+
// Test 5: Proper escaping
|
|
157
|
+
console.log('Test 5: With proper quote escaping');
|
|
158
|
+
const sessionName5 = `debug5-${Date.now()}`;
|
|
159
|
+
const logFile5 = path.join(os.tmpdir(), `debug5-${sessionName5}.log`);
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const command = 'echo "hello from attached mode"';
|
|
163
|
+
// Escape the inner quotes
|
|
164
|
+
const escapedCommand = command.replace(/"/g, '\\"');
|
|
165
|
+
const effectiveCommand = `(${escapedCommand}) 2>&1 | tee "${logFile5}"`;
|
|
166
|
+
console.log(` effectiveCommand: ${effectiveCommand}`);
|
|
167
|
+
|
|
168
|
+
const screenArgs = [
|
|
169
|
+
'-dmS',
|
|
170
|
+
sessionName5,
|
|
171
|
+
'/bin/sh',
|
|
172
|
+
'-c',
|
|
173
|
+
effectiveCommand,
|
|
174
|
+
];
|
|
175
|
+
const cmdStr = `screen ${screenArgs.map((a) => `"${a}"`).join(' ')}`;
|
|
176
|
+
console.log(` Full command: ${cmdStr}`);
|
|
177
|
+
|
|
178
|
+
execSync(cmdStr, { stdio: 'inherit' });
|
|
179
|
+
await sleep(500);
|
|
180
|
+
|
|
181
|
+
if (fs.existsSync(logFile5)) {
|
|
182
|
+
console.log(
|
|
183
|
+
` Log content: "${fs.readFileSync(logFile5, 'utf8').trim()}"`
|
|
184
|
+
);
|
|
185
|
+
console.log(` Result: SUCCESS ✓`);
|
|
186
|
+
fs.unlinkSync(logFile5);
|
|
187
|
+
} else {
|
|
188
|
+
console.log(` Result: FAILED - No log file ✗`);
|
|
189
|
+
}
|
|
190
|
+
} catch (e) {
|
|
191
|
+
console.log(` Error: ${e.message}`);
|
|
192
|
+
}
|
|
193
|
+
console.log('');
|
|
194
|
+
|
|
195
|
+
// Test 6: Use spawnSync instead of execSync with constructed string
|
|
196
|
+
console.log('Test 6: Using spawnSync with array (better approach)');
|
|
197
|
+
const sessionName6 = `debug6-${Date.now()}`;
|
|
198
|
+
const logFile6 = path.join(os.tmpdir(), `debug6-${sessionName6}.log`);
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const command = 'echo "hello from attached mode"';
|
|
202
|
+
const effectiveCommand = `(${command}) 2>&1 | tee "${logFile6}"`;
|
|
203
|
+
console.log(` effectiveCommand: ${effectiveCommand}`);
|
|
204
|
+
|
|
205
|
+
const { spawnSync } = require('child_process');
|
|
206
|
+
const screenArgs = [
|
|
207
|
+
'-dmS',
|
|
208
|
+
sessionName6,
|
|
209
|
+
'/bin/sh',
|
|
210
|
+
'-c',
|
|
211
|
+
effectiveCommand,
|
|
212
|
+
];
|
|
213
|
+
console.log(` spawnSync args: screen ${screenArgs.join(' ')}`);
|
|
214
|
+
|
|
215
|
+
const result = spawnSync('screen', screenArgs, { stdio: 'inherit' });
|
|
216
|
+
console.log(` spawnSync exit code: ${result.status}`);
|
|
217
|
+
|
|
218
|
+
await sleep(500);
|
|
219
|
+
|
|
220
|
+
if (fs.existsSync(logFile6)) {
|
|
221
|
+
console.log(
|
|
222
|
+
` Log content: "${fs.readFileSync(logFile6, 'utf8').trim()}"`
|
|
223
|
+
);
|
|
224
|
+
console.log(` Result: SUCCESS ✓`);
|
|
225
|
+
fs.unlinkSync(logFile6);
|
|
226
|
+
} else {
|
|
227
|
+
console.log(` Result: FAILED - No log file ✗`);
|
|
228
|
+
}
|
|
229
|
+
} catch (e) {
|
|
230
|
+
console.log(` Error: ${e.message}`);
|
|
231
|
+
}
|
|
232
|
+
console.log('');
|
|
233
|
+
|
|
234
|
+
console.log('=== Debug Tests Complete ===');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
testDebug();
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Experiment to test screen's tee fallback functionality
|
|
4
|
+
* This simulates the behavior on macOS with older screen (< 4.5.1)
|
|
5
|
+
* which doesn't support -Logfile option
|
|
6
|
+
*
|
|
7
|
+
* Issue #25: We don't get `Hello` output from `$ --isolated screen --verbose -- echo "hello"` command
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { execSync, spawnSync, spawn } = require('child_process');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
|
|
15
|
+
async function sleep(ms) {
|
|
16
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function testTeeFallback() {
|
|
20
|
+
console.log('=== Testing Screen Tee Fallback (macOS 4.0.3 simulation) ===\n');
|
|
21
|
+
|
|
22
|
+
// Test environment info
|
|
23
|
+
console.log('Environment:');
|
|
24
|
+
console.log(` Platform: ${process.platform}`);
|
|
25
|
+
console.log(` Node: ${process.version}`);
|
|
26
|
+
try {
|
|
27
|
+
const screenVersion = execSync('screen --version', {
|
|
28
|
+
encoding: 'utf8',
|
|
29
|
+
}).trim();
|
|
30
|
+
console.log(` Screen: ${screenVersion}`);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.log(` Screen: Not available - ${e.message}`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
console.log(
|
|
36
|
+
` TTY: stdin=${process.stdin.isTTY}, stdout=${process.stdout.isTTY}`
|
|
37
|
+
);
|
|
38
|
+
console.log('');
|
|
39
|
+
|
|
40
|
+
// Test 1: The tee fallback approach (current implementation for macOS)
|
|
41
|
+
console.log('Test 1: Tee fallback approach (current implementation)');
|
|
42
|
+
const sessionName1 = `tee-test-${Date.now()}`;
|
|
43
|
+
const logFile1 = path.join(os.tmpdir(), `screen-tee-${sessionName1}.log`);
|
|
44
|
+
const command = 'echo "hello"';
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
// This is the current implementation for older screen versions
|
|
48
|
+
const effectiveCommand = `(${command}) 2>&1 | tee "${logFile1}"`;
|
|
49
|
+
const shell = '/bin/sh';
|
|
50
|
+
const shellArg = '-c';
|
|
51
|
+
const screenArgs = [
|
|
52
|
+
'-dmS',
|
|
53
|
+
sessionName1,
|
|
54
|
+
shell,
|
|
55
|
+
shellArg,
|
|
56
|
+
effectiveCommand,
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
console.log(` Command: screen ${screenArgs.join(' ')}`);
|
|
60
|
+
console.log(` Effective command inside screen: ${effectiveCommand}`);
|
|
61
|
+
|
|
62
|
+
execSync(`screen ${screenArgs.map((a) => `"${a}"`).join(' ')}`, {
|
|
63
|
+
stdio: 'inherit',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Wait for completion and poll for session
|
|
67
|
+
let waited = 0;
|
|
68
|
+
const maxWait = 5000;
|
|
69
|
+
const interval = 100;
|
|
70
|
+
|
|
71
|
+
while (waited < maxWait) {
|
|
72
|
+
await sleep(interval);
|
|
73
|
+
waited += interval;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const sessions = execSync('screen -ls', {
|
|
77
|
+
encoding: 'utf8',
|
|
78
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
79
|
+
});
|
|
80
|
+
if (!sessions.includes(sessionName1)) {
|
|
81
|
+
console.log(` Session ended after ${waited}ms`);
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// screen -ls returns non-zero if no sessions
|
|
86
|
+
console.log(` Session ended after ${waited}ms (no sessions)`);
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check log file
|
|
92
|
+
if (fs.existsSync(logFile1)) {
|
|
93
|
+
const content = fs.readFileSync(logFile1, 'utf8');
|
|
94
|
+
console.log(` Log file exists: YES`);
|
|
95
|
+
console.log(` Log file size: ${content.length} bytes`);
|
|
96
|
+
console.log(` Log content: "${content.trim()}"`);
|
|
97
|
+
console.log(
|
|
98
|
+
` Contains expected output: ${content.includes('hello') ? 'YES ✓' : 'NO ✗'}`
|
|
99
|
+
);
|
|
100
|
+
fs.unlinkSync(logFile1);
|
|
101
|
+
} else {
|
|
102
|
+
console.log(` Log file exists: NO ✗`);
|
|
103
|
+
console.log(` Expected path: ${logFile1}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Cleanup
|
|
107
|
+
try {
|
|
108
|
+
execSync(`screen -S ${sessionName1} -X quit 2>/dev/null`);
|
|
109
|
+
} catch {}
|
|
110
|
+
} catch (e) {
|
|
111
|
+
console.log(` Error: ${e.message}`);
|
|
112
|
+
}
|
|
113
|
+
console.log('');
|
|
114
|
+
|
|
115
|
+
// Test 2: Test the attached mode WITHOUT TTY (hasTTY() returns false)
|
|
116
|
+
console.log(
|
|
117
|
+
'Test 2: Simulating attached mode without TTY (current code path)'
|
|
118
|
+
);
|
|
119
|
+
const sessionName2 = `tee-notty-${Date.now()}`;
|
|
120
|
+
const logFile2 = path.join(os.tmpdir(), `screen-tee-${sessionName2}.log`);
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
// Simulate the runScreenWithLogCapture function behavior for older screen
|
|
124
|
+
const command2 = 'echo "hello from attached mode"';
|
|
125
|
+
const effectiveCommand2 = `(${command2}) 2>&1 | tee "${logFile2}"`;
|
|
126
|
+
const shell = '/bin/sh';
|
|
127
|
+
const shellArg = '-c';
|
|
128
|
+
const screenArgs = [
|
|
129
|
+
'-dmS',
|
|
130
|
+
sessionName2,
|
|
131
|
+
shell,
|
|
132
|
+
shellArg,
|
|
133
|
+
effectiveCommand2,
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
console.log(` Screen args: ${screenArgs.join(' ')}`);
|
|
137
|
+
|
|
138
|
+
execSync(`screen ${screenArgs.map((a) => `"${a}"`).join(' ')}`, {
|
|
139
|
+
stdio: 'inherit',
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Poll for session completion (as done in current implementation)
|
|
143
|
+
const checkInterval = 100;
|
|
144
|
+
const maxWait = 5000;
|
|
145
|
+
let waited = 0;
|
|
146
|
+
|
|
147
|
+
const checkCompletion = async () => {
|
|
148
|
+
while (waited < maxWait) {
|
|
149
|
+
await sleep(checkInterval);
|
|
150
|
+
waited += checkInterval;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const sessions = execSync('screen -ls', {
|
|
154
|
+
encoding: 'utf8',
|
|
155
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
156
|
+
});
|
|
157
|
+
if (!sessions.includes(sessionName2)) {
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
await checkCompletion();
|
|
167
|
+
console.log(` Session ended after ${waited}ms`);
|
|
168
|
+
|
|
169
|
+
// Read output
|
|
170
|
+
if (fs.existsSync(logFile2)) {
|
|
171
|
+
const content = fs.readFileSync(logFile2, 'utf8');
|
|
172
|
+
console.log(` Log file exists: YES`);
|
|
173
|
+
console.log(` Log content: "${content.trim()}"`);
|
|
174
|
+
console.log(
|
|
175
|
+
` Contains expected: ${content.includes('hello from attached mode') ? 'YES ✓' : 'NO ✗'}`
|
|
176
|
+
);
|
|
177
|
+
fs.unlinkSync(logFile2);
|
|
178
|
+
} else {
|
|
179
|
+
console.log(` Log file exists: NO ✗`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
execSync(`screen -S ${sessionName2} -X quit 2>/dev/null`);
|
|
184
|
+
} catch {}
|
|
185
|
+
} catch (e) {
|
|
186
|
+
console.log(` Error: ${e.message}`);
|
|
187
|
+
}
|
|
188
|
+
console.log('');
|
|
189
|
+
|
|
190
|
+
// Test 3: What happens if we have a TTY?
|
|
191
|
+
console.log('Test 3: Test with attached mode WITH TTY (spawn with inherit)');
|
|
192
|
+
const sessionName3 = `tty-test-${Date.now()}`;
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const command3 = 'echo "hello TTY"';
|
|
196
|
+
const screenArgs = ['-S', sessionName3, '/bin/sh', '-c', command3];
|
|
197
|
+
|
|
198
|
+
console.log(` Screen args for attached mode: ${screenArgs.join(' ')}`);
|
|
199
|
+
console.log(
|
|
200
|
+
` hasTTY: stdin=${process.stdin.isTTY}, stdout=${process.stdout.isTTY}`
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// This mimics what the current code does when hasTTY() returns true
|
|
204
|
+
return new Promise((resolve) => {
|
|
205
|
+
const child = spawn('screen', screenArgs, {
|
|
206
|
+
stdio: 'inherit',
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
child.on('exit', (code) => {
|
|
210
|
+
console.log(` Exit code: ${code}`);
|
|
211
|
+
console.log(
|
|
212
|
+
` Note: In attached mode with TTY, output goes directly to terminal`
|
|
213
|
+
);
|
|
214
|
+
resolve();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
child.on('error', (err) => {
|
|
218
|
+
console.log(` Error: ${err.message}`);
|
|
219
|
+
resolve();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
} catch (e) {
|
|
223
|
+
console.log(` Error: ${e.message}`);
|
|
224
|
+
}
|
|
225
|
+
console.log('');
|
|
226
|
+
|
|
227
|
+
console.log('=== Tests Complete ===');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
testTeeFallback();
|
package/package.json
CHANGED
package/src/lib/isolation.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - docker: Docker containers
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
const { execSync, spawn } = require('child_process');
|
|
10
|
+
const { execSync, spawn, spawnSync } = require('child_process');
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const os = require('os');
|
|
13
13
|
const path = require('path');
|
|
@@ -189,10 +189,18 @@ function runScreenWithLogCapture(command, sessionName, shellInfo) {
|
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
|
|
192
|
+
// Use spawnSync with array arguments to avoid shell quoting issues
|
|
193
|
+
// This is critical for commands containing quotes (e.g., echo "hello")
|
|
194
|
+
// Using execSync with a constructed string would break on nested quotes
|
|
195
|
+
// See issue #25 for details
|
|
196
|
+
const result = spawnSync('screen', screenArgs, {
|
|
193
197
|
stdio: 'inherit',
|
|
194
198
|
});
|
|
195
199
|
|
|
200
|
+
if (result.error) {
|
|
201
|
+
throw result.error;
|
|
202
|
+
}
|
|
203
|
+
|
|
196
204
|
// Poll for session completion
|
|
197
205
|
const checkInterval = 100; // ms
|
|
198
206
|
const maxWait = 300000; // 5 minutes max
|
|
@@ -317,10 +325,17 @@ function runInScreen(command, options = {}) {
|
|
|
317
325
|
console.log(`[DEBUG] Running: screen ${screenArgs.join(' ')}`);
|
|
318
326
|
}
|
|
319
327
|
|
|
320
|
-
|
|
328
|
+
// Use spawnSync with array arguments to avoid shell quoting issues
|
|
329
|
+
// This is critical for commands containing quotes (e.g., echo "hello")
|
|
330
|
+
// See issue #25 for details
|
|
331
|
+
const result = spawnSync('screen', screenArgs, {
|
|
321
332
|
stdio: 'inherit',
|
|
322
333
|
});
|
|
323
334
|
|
|
335
|
+
if (result.error) {
|
|
336
|
+
throw result.error;
|
|
337
|
+
}
|
|
338
|
+
|
|
324
339
|
return Promise.resolve({
|
|
325
340
|
success: true,
|
|
326
341
|
sessionName,
|
package/test/isolation.test.js
CHANGED
|
@@ -330,6 +330,56 @@ describe('Isolation Runner with Available Backends', () => {
|
|
|
330
330
|
assert.ok(result.output.includes('line3'));
|
|
331
331
|
}
|
|
332
332
|
});
|
|
333
|
+
|
|
334
|
+
it('should capture output from commands with quoted strings (issue #25)', async () => {
|
|
335
|
+
if (!isCommandAvailable('screen')) {
|
|
336
|
+
console.log(' Skipping: screen not installed');
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// This is the exact scenario from issue #25:
|
|
341
|
+
// $ --isolated screen --verbose -- echo "hello"
|
|
342
|
+
// Previously failed because of shell quoting issues with execSync
|
|
343
|
+
const result = await runInScreen('echo "hello"', {
|
|
344
|
+
session: `test-quoted-${Date.now()}`,
|
|
345
|
+
detached: false,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
assert.strictEqual(result.success, true);
|
|
349
|
+
assert.ok(result.sessionName);
|
|
350
|
+
assert.ok(result.message.includes('exited with code 0'));
|
|
351
|
+
if (result.output !== undefined) {
|
|
352
|
+
console.log(` Captured quoted output: "${result.output.trim()}"`);
|
|
353
|
+
assert.ok(
|
|
354
|
+
result.output.includes('hello'),
|
|
355
|
+
'Output should contain "hello" (issue #25 regression test)'
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should capture output from commands with complex quoted strings', async () => {
|
|
361
|
+
if (!isCommandAvailable('screen')) {
|
|
362
|
+
console.log(' Skipping: screen not installed');
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Test more complex quoting scenarios
|
|
367
|
+
const result = await runInScreen('echo "hello from attached mode"', {
|
|
368
|
+
session: `test-complex-quote-${Date.now()}`,
|
|
369
|
+
detached: false,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
assert.strictEqual(result.success, true);
|
|
373
|
+
if (result.output !== undefined) {
|
|
374
|
+
console.log(
|
|
375
|
+
` Captured complex quote output: "${result.output.trim()}"`
|
|
376
|
+
);
|
|
377
|
+
assert.ok(
|
|
378
|
+
result.output.includes('hello from attached mode'),
|
|
379
|
+
'Output should contain the full message with spaces'
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
333
383
|
});
|
|
334
384
|
|
|
335
385
|
describe('runInTmux (if available)', () => {
|