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.
@@ -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.1",
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
- "node": ">=20.0.0"
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
- args.length === 1 || (args.length === 2 && args[1] === '--');
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
- // Check screen
111
- const screenVersion = getToolVersion('screen', '--version');
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
- // Redirect stderr to stdout (2>&1) to capture version info from stderr
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
- }).trim();
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
  /**
@@ -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
- execSync(`screen ${screenArgs.map((a) => `"${a}"`).join(' ')}`, {
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
- execSync(`screen ${screenArgs.map((a) => `"${a}"`).join(' ')}`, {
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,