start-command 0.7.1 → 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 +18 -0
- package/README.md +11 -17
- package/REQUIREMENTS.md +0 -1
- package/docs/case-studies/issue-22/analysis.md +215 -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 +2 -2
- package/src/bin/cli.js +73 -18
- package/src/lib/isolation.js +18 -3
- package/test/isolation.test.js +50 -0
- package/test/version.test.js +46 -0
|
@@ -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "start-command",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "Gamification of coding, execute any command with ability to auto-report issues on GitHub",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"author": "",
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"engines": {
|
|
33
|
-
"
|
|
33
|
+
"bun": ">=1.0.0"
|
|
34
34
|
},
|
|
35
35
|
"repository": {
|
|
36
36
|
"type": "git",
|
package/src/bin/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
const { spawn, execSync } = require('child_process');
|
|
3
|
+
const { spawn, execSync, spawnSync } = require('child_process');
|
|
4
4
|
const process = require('process');
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const fs = require('fs');
|
|
@@ -51,13 +51,26 @@ const args = process.argv.slice(2);
|
|
|
51
51
|
// Handle --version flag
|
|
52
52
|
// Support: $ --version, $ -v, $ --version --
|
|
53
53
|
// The trailing -- should be ignored for version check
|
|
54
|
+
// Also support --verbose flag for debugging: $ --version --verbose
|
|
54
55
|
const hasVersionFlag =
|
|
55
56
|
args.length >= 1 && (args[0] === '--version' || args[0] === '-v');
|
|
57
|
+
|
|
58
|
+
// Check for --verbose flag in version context
|
|
59
|
+
const hasVerboseWithVersion =
|
|
60
|
+
hasVersionFlag &&
|
|
61
|
+
args.some((arg) => arg === '--verbose' || arg === '--debug');
|
|
62
|
+
|
|
63
|
+
// Determine if this is a version-only call
|
|
64
|
+
// Allow: --version, -v, --version --, --version --verbose, etc.
|
|
65
|
+
const versionRelatedArgs = ['--version', '-v', '--', '--verbose', '--debug'];
|
|
56
66
|
const isVersionOnly =
|
|
57
|
-
|
|
67
|
+
hasVersionFlag &&
|
|
68
|
+
args.every(
|
|
69
|
+
(arg) => versionRelatedArgs.includes(arg) || arg === args[0] // Allow the version flag itself
|
|
70
|
+
);
|
|
58
71
|
|
|
59
72
|
if (hasVersionFlag && isVersionOnly) {
|
|
60
|
-
printVersion();
|
|
73
|
+
printVersion(hasVerboseWithVersion || config.verbose);
|
|
61
74
|
process.exit(0);
|
|
62
75
|
}
|
|
63
76
|
|
|
@@ -68,8 +81,9 @@ if (args.length === 0) {
|
|
|
68
81
|
|
|
69
82
|
/**
|
|
70
83
|
* Print version information
|
|
84
|
+
* @param {boolean} verbose - Whether to show verbose debugging info
|
|
71
85
|
*/
|
|
72
|
-
function printVersion() {
|
|
86
|
+
function printVersion(verbose = false) {
|
|
73
87
|
// Get package version
|
|
74
88
|
const packageJson = require('../../package.json');
|
|
75
89
|
const startCommandVersion = packageJson.version;
|
|
@@ -93,9 +107,17 @@ function printVersion() {
|
|
|
93
107
|
encoding: 'utf8',
|
|
94
108
|
timeout: 5000,
|
|
95
109
|
}).trim();
|
|
110
|
+
if (verbose) {
|
|
111
|
+
console.log(`[verbose] macOS version from sw_vers: ${osVersion}`);
|
|
112
|
+
}
|
|
96
113
|
} catch {
|
|
97
114
|
// Fallback to kernel version if sw_vers fails
|
|
98
115
|
osVersion = os.release();
|
|
116
|
+
if (verbose) {
|
|
117
|
+
console.log(
|
|
118
|
+
`[verbose] sw_vers failed, using kernel version: ${osVersion}`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
99
121
|
}
|
|
100
122
|
}
|
|
101
123
|
|
|
@@ -107,8 +129,12 @@ function printVersion() {
|
|
|
107
129
|
// Check for installed isolation tools
|
|
108
130
|
console.log('Isolation tools:');
|
|
109
131
|
|
|
110
|
-
|
|
111
|
-
|
|
132
|
+
if (verbose) {
|
|
133
|
+
console.log('[verbose] Checking isolation tools...');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check screen (use -v flag for compatibility with older versions)
|
|
137
|
+
const screenVersion = getToolVersion('screen', '-v', verbose);
|
|
112
138
|
if (screenVersion) {
|
|
113
139
|
console.log(` screen: ${screenVersion}`);
|
|
114
140
|
} else {
|
|
@@ -116,7 +142,7 @@ function printVersion() {
|
|
|
116
142
|
}
|
|
117
143
|
|
|
118
144
|
// Check tmux
|
|
119
|
-
const tmuxVersion = getToolVersion('tmux', '-V');
|
|
145
|
+
const tmuxVersion = getToolVersion('tmux', '-V', verbose);
|
|
120
146
|
if (tmuxVersion) {
|
|
121
147
|
console.log(` tmux: ${tmuxVersion}`);
|
|
122
148
|
} else {
|
|
@@ -124,7 +150,7 @@ function printVersion() {
|
|
|
124
150
|
}
|
|
125
151
|
|
|
126
152
|
// Check docker
|
|
127
|
-
const dockerVersion = getToolVersion('docker', '--version');
|
|
153
|
+
const dockerVersion = getToolVersion('docker', '--version', verbose);
|
|
128
154
|
if (dockerVersion) {
|
|
129
155
|
console.log(` docker: ${dockerVersion}`);
|
|
130
156
|
} else {
|
|
@@ -136,24 +162,53 @@ function printVersion() {
|
|
|
136
162
|
* Get version of an installed tool
|
|
137
163
|
* @param {string} toolName - Name of the tool
|
|
138
164
|
* @param {string} versionFlag - Flag to get version (e.g., '--version', '-V')
|
|
165
|
+
* @param {boolean} verbose - Whether to log verbose information
|
|
139
166
|
* @returns {string|null} Version string or null if not installed
|
|
140
167
|
*/
|
|
141
|
-
function getToolVersion(toolName, versionFlag) {
|
|
168
|
+
function getToolVersion(toolName, versionFlag, verbose = false) {
|
|
169
|
+
const isWindows = process.platform === 'win32';
|
|
170
|
+
const whichCmd = isWindows ? 'where' : 'which';
|
|
171
|
+
|
|
172
|
+
// First, check if the tool exists in PATH
|
|
142
173
|
try {
|
|
143
|
-
|
|
144
|
-
// Some tools like screen output version to stderr instead of stdout
|
|
145
|
-
const result = execSync(`${toolName} ${versionFlag} 2>&1`, {
|
|
174
|
+
execSync(`${whichCmd} ${toolName}`, {
|
|
146
175
|
encoding: 'utf8',
|
|
147
176
|
timeout: 5000,
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
// Extract version number from output
|
|
151
|
-
// Most tools output version in various formats, so we'll return the first line
|
|
152
|
-
const firstLine = result.split('\n')[0];
|
|
153
|
-
return firstLine;
|
|
177
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
178
|
+
});
|
|
154
179
|
} catch {
|
|
180
|
+
// Tool not found in PATH
|
|
181
|
+
if (verbose) {
|
|
182
|
+
console.log(`[verbose] ${toolName}: not found in PATH`);
|
|
183
|
+
}
|
|
155
184
|
return null;
|
|
156
185
|
}
|
|
186
|
+
|
|
187
|
+
// Tool exists, try to get version using spawnSync
|
|
188
|
+
// This captures output regardless of exit code (some tools like older screen
|
|
189
|
+
// versions return non-zero exit code even when showing version successfully)
|
|
190
|
+
const result = spawnSync(toolName, [versionFlag], {
|
|
191
|
+
encoding: 'utf8',
|
|
192
|
+
timeout: 5000,
|
|
193
|
+
shell: false,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Combine stdout and stderr (some tools output version to stderr)
|
|
197
|
+
const output = ((result.stdout || '') + (result.stderr || '')).trim();
|
|
198
|
+
|
|
199
|
+
if (verbose) {
|
|
200
|
+
console.log(
|
|
201
|
+
`[verbose] ${toolName} ${versionFlag}: exit=${result.status}, output="${output.substring(0, 100)}"`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!output) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Return the first line of output
|
|
210
|
+
const firstLine = output.split('\n')[0];
|
|
211
|
+
return firstLine || null;
|
|
157
212
|
}
|
|
158
213
|
|
|
159
214
|
/**
|
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,
|