start-command 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -108,6 +108,14 @@ jobs:
108
108
  with:
109
109
  bun-version: latest
110
110
 
111
+ - name: Install screen (Linux)
112
+ if: runner.os == 'Linux'
113
+ run: sudo apt-get update && sudo apt-get install -y screen
114
+
115
+ - name: Install screen (macOS)
116
+ if: runner.os == 'macOS'
117
+ run: brew install screen
118
+
111
119
  - name: Install dependencies
112
120
  run: bun install
113
121
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # start-command
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 37eb93b: Drop zellij isolation backend support, focusing on screen, tmux, and docker. Remove zellij from VALID_BACKENDS, remove runInZellij function, and update all documentation accordingly.
8
+
9
+ ## 0.5.3
10
+
11
+ ### Patch Changes
12
+
13
+ - 20d0c1c: Fix screen isolation not capturing output on macOS (issue #15)
14
+ - Added version detection for GNU Screen to handle differences between versions
15
+ - Screen >= 4.5.1 uses native `-L -Logfile` for log capture
16
+ - Screen < 4.5.1 (like macOS bundled 4.0.3) uses `tee` command fallback
17
+ - Added tests for version detection and -Logfile support checking
18
+ - Updated case study documentation with root cause analysis
19
+
3
20
  ## 0.5.2
4
21
 
5
22
  ### Patch Changes
package/README.md CHANGED
@@ -141,18 +141,17 @@ $ -i tmux -s my-session -d npm start
141
141
  | -------- | -------------------------------------- | ---------------------------------------------------------- |
142
142
  | `screen` | GNU Screen terminal multiplexer | `apt install screen` / `brew install screen` |
143
143
  | `tmux` | Modern terminal multiplexer | `apt install tmux` / `brew install tmux` |
144
- | `zellij` | Modern terminal workspace | `cargo install zellij` / `brew install zellij` |
145
144
  | `docker` | Container isolation (requires --image) | [Docker Installation](https://docs.docker.com/get-docker/) |
146
145
 
147
146
  #### Isolation Options
148
147
 
149
- | Option | Description |
150
- | ---------------- | ------------------------------------------------ |
151
- | `--isolated, -i` | Isolation backend (screen, tmux, docker, zellij) |
152
- | `--attached, -a` | Run in attached/foreground mode (default) |
153
- | `--detached, -d` | Run in detached/background mode |
154
- | `--session, -s` | Custom session/container name |
155
- | `--image` | Docker image (required for docker isolation) |
148
+ | Option | Description |
149
+ | ---------------- | -------------------------------------------- |
150
+ | `--isolated, -i` | Isolation backend (screen, tmux, docker) |
151
+ | `--attached, -a` | Run in attached/foreground mode (default) |
152
+ | `--detached, -d` | Run in detached/background mode |
153
+ | `--session, -s` | Custom session/container name |
154
+ | `--image` | Docker image (required for docker isolation) |
156
155
 
157
156
  **Note:** Using both `--attached` and `--detached` together will result in an error - you must choose one mode.
158
157
 
package/REQUIREMENTS.md CHANGED
@@ -147,7 +147,6 @@ Support two patterns for passing wrapper options:
147
147
 
148
148
  - `screen`: GNU Screen terminal multiplexer
149
149
  - `tmux`: tmux terminal multiplexer
150
- - `zellij`: Modern terminal workspace
151
150
  - `docker`: Docker containers (requires --image option)
152
151
 
153
152
  #### 6.4 Mode Behavior
@@ -15,7 +15,7 @@ The screen isolation environment does not display command output when running in
15
15
 
16
16
  - **Platform:** macOS (reported), Linux (reproduced)
17
17
  - **Package:** start-command@0.5.1
18
- - **Screen version:** Tested with GNU Screen
18
+ - **Screen version:** macOS bundled 4.0.3, Linux 4.09.01
19
19
 
20
20
  ## Timeline of Events
21
21
 
@@ -57,7 +57,42 @@ Exit code: 0
57
57
 
58
58
  ## Root Cause Analysis
59
59
 
60
- ### Investigation
60
+ ### PRIMARY ROOT CAUSE: macOS Screen Version Incompatibility
61
+
62
+ **macOS ships with GNU Screen version 4.0.3, which does NOT support the `-Logfile` option.**
63
+
64
+ The `-Logfile` option was introduced in **GNU Screen 4.5.1** (released February 2017).
65
+
66
+ | Platform | Screen Version | `-Logfile` Support |
67
+ | --------------- | -------------- | ------------------ |
68
+ | macOS (bundled) | 4.0.3 | **NO** |
69
+ | Linux (CI/Test) | 4.09.01 | YES |
70
+
71
+ The current implementation uses:
72
+
73
+ ```javascript
74
+ const screenArgs = [
75
+ '-dmS',
76
+ sessionName,
77
+ '-L',
78
+ '-Logfile',
79
+ logFile, // <-- NOT SUPPORTED on macOS bundled screen
80
+ shell,
81
+ shellArg,
82
+ command,
83
+ ];
84
+ ```
85
+
86
+ On macOS with screen 4.0.3:
87
+
88
+ 1. The `-Logfile` option is silently ignored or treated as a command argument
89
+ 2. The `-L` flag alone creates a log file named `screenlog.0` in the current directory
90
+ 3. The code tries to read from the wrong file path (`/tmp/screen-output-*.log`)
91
+ 4. Result: No output is captured or displayed
92
+
93
+ ### Secondary Root Cause: TTY Requirement
94
+
95
+ When TTY is available, the code attempts attached mode which fails:
61
96
 
62
97
  1. **TTY Requirement**: The GNU Screen command requires a connected terminal (TTY/PTY) to run in attached mode.
63
98
 
@@ -76,8 +111,21 @@ Exit code: 0
76
111
  Testing revealed:
77
112
 
78
113
  - `process.stdin.isTTY` and `process.stdout.isTTY` are `undefined` when running from Node.js
79
- - Detached mode with logging (`screen -dmS ... -L -Logfile ...`) captures output correctly
114
+ - Detached mode with logging (`screen -dmS ... -L -Logfile ...`) captures output correctly **on Linux only**
80
115
  - Using `script -q -c "screen ..." /dev/null` can provide a PTY but includes terminal escape codes
116
+ - On macOS with screen 4.0.3, the `-Logfile` option is unknown
117
+
118
+ ### Version Check Evidence
119
+
120
+ ```bash
121
+ # Linux (works)
122
+ $ screen --version
123
+ Screen version 4.09.01 (GNU) 20-Aug-23
124
+
125
+ # macOS bundled (broken)
126
+ $ screen --version
127
+ Screen version 4.00.03 (FAU) 23-Oct-06
128
+ ```
81
129
 
82
130
  ### Comparison with Docker
83
131
 
@@ -87,70 +135,67 @@ Docker isolation works because:
87
135
  2. Docker spawns an isolated container that manages its own pseudo-terminal
88
136
  3. The command output flows through Docker's I/O handling
89
137
 
90
- ## Solution Options
91
-
92
- ### Option 1: Use Script Command for PTY Allocation (Recommended)
93
-
94
- Wrap the screen command with `script -q -c "command" /dev/null` to allocate a pseudo-terminal.
95
-
96
- **Pros:**
97
-
98
- - Provides a real PTY that screen requires
99
- - Works across Linux/macOS
100
- - Maintains attached behavior
101
-
102
- **Cons:**
103
-
104
- - Adds terminal escape codes to output
105
- - Requires `script` command to be available
106
-
107
- ### Option 2: Switch to Detached Mode with Log Capture
108
-
109
- Run screen in detached mode (`-dmS`) with logging enabled (`-L -Logfile`), wait for completion, then display the log.
110
-
111
- **Pros:**
112
-
113
- - Clean output without escape codes
114
- - Reliable across platforms
115
- - Captures full command output
116
-
117
- **Cons:**
118
-
119
- - Not truly "attached" - user can't interact with the process
120
- - Requires polling or waiting for completion
121
-
122
- ### Option 3: Hybrid Approach (Chosen Solution)
123
-
124
- For attached mode:
125
-
126
- 1. Check if running in a TTY (`process.stdin.isTTY`)
127
- 2. If TTY available: Use standard screen spawn with `stdio: 'inherit'`
128
- 3. If no TTY: Use `script` command to allocate PTY
129
-
130
- For detached mode:
131
-
132
- - Use existing implementation with `-dmS` flags
133
-
134
- ## Implementation
138
+ ## Solution: Version Detection with Fallback
139
+
140
+ ### Approach
141
+
142
+ 1. **Detect screen version** at runtime
143
+ 2. **Version >= 4.5.1**: Use `-L -Logfile` approach
144
+ 3. **Version < 4.5.1**: Use output redirection (`tee`) approach within the command
145
+
146
+ ### Implementation
147
+
148
+ ```javascript
149
+ function getScreenVersion() {
150
+ try {
151
+ const output = execSync('screen --version', { encoding: 'utf8' });
152
+ const match = output.match(/(\d+)\.(\d+)\.(\d+)/);
153
+ if (match) {
154
+ return {
155
+ major: parseInt(match[1]),
156
+ minor: parseInt(match[2]),
157
+ patch: parseInt(match[3]),
158
+ };
159
+ }
160
+ } catch {
161
+ return null;
162
+ }
163
+ return null;
164
+ }
165
+
166
+ function supportsLogfileOption() {
167
+ const version = getScreenVersion();
168
+ if (!version) return false;
169
+ // -Logfile was added in 4.5.1
170
+ return (
171
+ version.major > 4 ||
172
+ (version.major === 4 && version.minor > 5) ||
173
+ (version.major === 4 && version.minor === 5 && version.patch >= 1)
174
+ );
175
+ }
176
+ ```
135
177
 
136
- The fix modifies `src/lib/isolation.js` to:
178
+ For older versions, wrap command with tee:
137
179
 
138
- 1. Check for TTY availability before spawning screen
139
- 2. Use `script` command as PTY allocator when no TTY is available
140
- 3. Clean terminal escape codes from output when using script wrapper
141
- 4. Maintain compatibility with existing detached mode
180
+ ```javascript
181
+ const wrappedCommand = `(${command}) 2>&1 | tee "${logFile}"`;
182
+ const screenArgs = ['-dmS', sessionName, shell, shellArg, wrappedCommand];
183
+ ```
142
184
 
143
185
  ## Testing Strategy
144
186
 
145
- 1. **Unit tests**: Test TTY detection logic
146
- 2. **Integration tests**: Test screen isolation in detached mode
147
- 3. **Environment tests**: Test behavior with and without TTY
187
+ 1. **Unit tests**: Test version detection logic
188
+ 2. **Unit tests**: Test screen version comparison
189
+ 3. **Integration tests**: Test output capture for both code paths
190
+ 4. **Regression tests**: Verify existing tests still pass
191
+ 5. **CI tests**: Ensure output is verified in assertions (not just exit code)
148
192
 
149
193
  ## References
150
194
 
195
+ - [GNU Screen v.4.5.1 changelog](https://lists.gnu.org/archive/html/info-gnu/2017-02/msg00000.html) - Introduction of `-Logfile` option
196
+ - [GitHub Issue: RHEL7 screen does not know the Logfile option](https://github.com/distributed-system-analysis/pbench/issues/1558)
197
+ - [How to install GNU Screen on OS X using Homebrew](https://gist.github.com/bigeasy/2327150)
151
198
  - [GNU Screen Manual](https://www.gnu.org/software/screen/manual/screen.html)
152
- - [Stack Overflow: Must be connected to terminal](https://stackoverflow.com/questions/tagged/gnu-screen+tty)
153
- - [node-pty for PTY allocation](https://github.com/microsoft/node-pty)
154
199
  - [script command man page](https://man7.org/linux/man-pages/man1/script.1.html)
155
200
 
156
201
  ## Appendix: Test Logs
@@ -160,3 +205,4 @@ See accompanying log files:
160
205
  - `test-output-1.log` - Initial reproduction
161
206
  - `screen-modes-test.log` - Screen modes investigation
162
207
  - `screen-attached-approaches.log` - Solution approaches testing
208
+ - `test-screen-logfile.js` - Version compatibility testing
@@ -10,11 +10,10 @@ This document outlines the design for adding process isolation support to start-
10
10
 
11
11
  1. **screen** - GNU Screen, classic session manager
12
12
  2. **tmux** - Modern terminal multiplexer
13
- 3. **zellij** - Modern, user-friendly multiplexer
14
13
 
15
14
  ### Container Isolation
16
15
 
17
- 4. **docker** - Docker containers
16
+ 3. **docker** - Docker containers
18
17
 
19
18
  ## Command Syntax
20
19
 
@@ -31,7 +30,7 @@ $ [wrapper-options] command [command-options]
31
30
  ### Wrapper Options
32
31
 
33
32
  - `--isolated <backend>` or `-i <backend>`: Run command in isolated environment
34
- - Backends: `screen`, `tmux`, `docker`, `zellij`
33
+ - Backends: `screen`, `tmux`, `docker`
35
34
  - `--attached` or `-a`: Run in attached mode (foreground)
36
35
  - `--detached` or `-d`: Run in detached mode (background)
37
36
  - `--session <name>` or `-s <name>`: Name for the session (optional)
@@ -56,7 +55,7 @@ $ -i tmux -d npm start
56
55
 
57
56
  ### Attached Mode (--attached)
58
57
 
59
- - Default for terminal multiplexers (screen, tmux, zellij)
58
+ - Default for terminal multiplexers (screen, tmux)
60
59
  - Command runs in foreground
61
60
  - User can interact with the terminal
62
61
  - For docker: runs with -it flags
@@ -103,16 +102,6 @@ tmux new-session -s <session> '<command>'
103
102
  tmux new-session -d -s <session> '<command>'
104
103
  ```
105
104
 
106
- #### Zellij
107
-
108
- ```bash
109
- # Attached
110
- zellij run -- <command>
111
-
112
- # Detached (via layout file or action)
113
- zellij -s <session> action new-pane -- <command>
114
- ```
115
-
116
105
  #### Docker
117
106
 
118
107
  ```bash
@@ -0,0 +1,286 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Experiment to test screen's logfile capture functionality
4
+ * to understand the root cause of issue #15
5
+ */
6
+
7
+ const { execSync, spawnSync } = 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 testScreenLogfile() {
17
+ console.log('=== Testing Screen Logfile Capture ===\n');
18
+
19
+ // Test environment info
20
+ console.log('Environment:');
21
+ console.log(` Platform: ${process.platform}`);
22
+ console.log(` Node: ${process.version}`);
23
+ try {
24
+ const screenVersion = execSync('screen --version', {
25
+ encoding: 'utf8',
26
+ }).trim();
27
+ console.log(` Screen: ${screenVersion}`);
28
+ } catch (e) {
29
+ console.log(` Screen: Not available - ${e.message}`);
30
+ return;
31
+ }
32
+ console.log(
33
+ ` TTY: stdin=${process.stdin.isTTY}, stdout=${process.stdout.isTTY}`
34
+ );
35
+ console.log('');
36
+
37
+ // Test 1: Basic logfile capture with -L -Logfile
38
+ console.log('Test 1: Basic -L -Logfile capture');
39
+ const sessionName1 = `logtest-${Date.now()}`;
40
+ const logFile1 = path.join(os.tmpdir(), `screen-log-${sessionName1}.log`);
41
+
42
+ try {
43
+ // Run screen with logging
44
+ const screenArgs = [
45
+ '-dmS',
46
+ sessionName1,
47
+ '-L',
48
+ '-Logfile',
49
+ logFile1,
50
+ '/bin/sh',
51
+ '-c',
52
+ 'echo "TESTOUTPUT123"',
53
+ ];
54
+
55
+ console.log(` Command: screen ${screenArgs.join(' ')}`);
56
+
57
+ execSync(`screen ${screenArgs.map((a) => `"${a}"`).join(' ')}`, {
58
+ stdio: 'inherit',
59
+ });
60
+
61
+ // Wait for completion (screen runs command and exits)
62
+ await sleep(500);
63
+
64
+ // Check if session still exists
65
+ let sessionExists = false;
66
+ try {
67
+ const sessions = execSync('screen -ls', {
68
+ encoding: 'utf8',
69
+ stdio: ['pipe', 'pipe', 'pipe'],
70
+ });
71
+ sessionExists = sessions.includes(sessionName1);
72
+ } catch {
73
+ // screen -ls returns non-zero if no sessions
74
+ }
75
+ console.log(` Session exists after 500ms: ${sessionExists}`);
76
+
77
+ // Check log file
78
+ if (fs.existsSync(logFile1)) {
79
+ const content = fs.readFileSync(logFile1, 'utf8');
80
+ console.log(` Log file exists: YES`);
81
+ console.log(` Log file size: ${content.length} bytes`);
82
+ console.log(
83
+ ` Log content: "${content.trim().replace(/\n/g, '\\n').slice(0, 200)}"`
84
+ );
85
+ console.log(
86
+ ` Contains expected output: ${content.includes('TESTOUTPUT123') ? 'YES ✓' : 'NO ✗'}`
87
+ );
88
+ fs.unlinkSync(logFile1);
89
+ } else {
90
+ console.log(` Log file exists: NO ✗`);
91
+ console.log(` Expected path: ${logFile1}`);
92
+ }
93
+
94
+ // Cleanup
95
+ try {
96
+ execSync(`screen -S ${sessionName1} -X quit 2>/dev/null`);
97
+ } catch {}
98
+ } catch (e) {
99
+ console.log(` Error: ${e.message}`);
100
+ }
101
+ console.log('');
102
+
103
+ // Test 2: Test with sleep to ensure buffer flush
104
+ console.log('Test 2: With sleep for buffer flush');
105
+ const sessionName2 = `logtest2-${Date.now()}`;
106
+ const logFile2 = path.join(os.tmpdir(), `screen-log-${sessionName2}.log`);
107
+
108
+ try {
109
+ const screenArgs = [
110
+ '-dmS',
111
+ sessionName2,
112
+ '-L',
113
+ '-Logfile',
114
+ logFile2,
115
+ '/bin/sh',
116
+ '-c',
117
+ 'echo "FLUSHED_OUTPUT" && sleep 0.5',
118
+ ];
119
+
120
+ console.log(` Command: screen ${screenArgs.join(' ')}`);
121
+
122
+ execSync(`screen ${screenArgs.map((a) => `"${a}"`).join(' ')}`, {
123
+ stdio: 'inherit',
124
+ });
125
+
126
+ // Wait longer for flush (default is 10 seconds)
127
+ await sleep(1500);
128
+
129
+ // Check log file
130
+ if (fs.existsSync(logFile2)) {
131
+ const content = fs.readFileSync(logFile2, 'utf8');
132
+ console.log(` Log file exists: YES`);
133
+ console.log(` Log file size: ${content.length} bytes`);
134
+ console.log(
135
+ ` Contains expected output: ${content.includes('FLUSHED_OUTPUT') ? 'YES ✓' : 'NO ✗'}`
136
+ );
137
+ fs.unlinkSync(logFile2);
138
+ } else {
139
+ console.log(` Log file exists: NO ✗`);
140
+ }
141
+
142
+ // Cleanup
143
+ try {
144
+ execSync(`screen -S ${sessionName2} -X quit 2>/dev/null`);
145
+ } catch {}
146
+ } catch (e) {
147
+ console.log(` Error: ${e.message}`);
148
+ }
149
+ console.log('');
150
+
151
+ // Test 3: Alternative - using hardstatus/output redirection
152
+ console.log('Test 3: Direct command output capture (no screen logging)');
153
+ const sessionName3 = `logtest3-${Date.now()}`;
154
+ const logFile3 = path.join(os.tmpdir(), `screen-log-${sessionName3}.log`);
155
+
156
+ try {
157
+ // Run command through screen but capture output to file within the command
158
+ const command = `echo "DIRECT_CAPTURE" | tee ${logFile3}`;
159
+ const screenArgs = ['-dmS', sessionName3, '/bin/sh', '-c', command];
160
+
161
+ console.log(` Command: screen ${screenArgs.join(' ')}`);
162
+
163
+ execSync(`screen ${screenArgs.map((a) => `"${a}"`).join(' ')}`, {
164
+ stdio: 'inherit',
165
+ });
166
+
167
+ await sleep(500);
168
+
169
+ // Check log file
170
+ if (fs.existsSync(logFile3)) {
171
+ const content = fs.readFileSync(logFile3, 'utf8');
172
+ console.log(` Log file exists: YES`);
173
+ console.log(
174
+ ` Contains expected output: ${content.includes('DIRECT_CAPTURE') ? 'YES ✓' : 'NO ✗'}`
175
+ );
176
+ fs.unlinkSync(logFile3);
177
+ } else {
178
+ console.log(` Log file exists: NO ✗`);
179
+ }
180
+
181
+ // Cleanup
182
+ try {
183
+ execSync(`screen -S ${sessionName3} -X quit 2>/dev/null`);
184
+ } catch {}
185
+ } catch (e) {
186
+ console.log(` Error: ${e.message}`);
187
+ }
188
+ console.log('');
189
+
190
+ // Test 4: Script command approach
191
+ console.log('Test 4: Using script command to capture output');
192
+ const sessionName4 = `logtest4-${Date.now()}`;
193
+ const logFile4 = path.join(os.tmpdir(), `script-log-${sessionName4}.log`);
194
+
195
+ try {
196
+ // Use script to capture output
197
+ const result = spawnSync(
198
+ 'script',
199
+ [
200
+ '-q',
201
+ logFile4,
202
+ '-c',
203
+ `screen -dmS ${sessionName4} /bin/sh -c "echo SCRIPT_CAPTURE"`,
204
+ ],
205
+ {
206
+ stdio: ['inherit', 'pipe', 'pipe'],
207
+ timeout: 5000,
208
+ }
209
+ );
210
+
211
+ await sleep(500);
212
+
213
+ console.log(` Exit code: ${result.status}`);
214
+
215
+ // Check log file
216
+ if (fs.existsSync(logFile4)) {
217
+ const content = fs.readFileSync(logFile4, 'utf8');
218
+ console.log(` Log file exists: YES`);
219
+ console.log(` Log file size: ${content.length} bytes`);
220
+ fs.unlinkSync(logFile4);
221
+ } else {
222
+ console.log(` Log file exists: NO`);
223
+ }
224
+
225
+ // Cleanup
226
+ try {
227
+ execSync(`screen -S ${sessionName4} -X quit 2>/dev/null`);
228
+ } catch {}
229
+ } catch (e) {
230
+ console.log(` Error: ${e.message}`);
231
+ }
232
+ console.log('');
233
+
234
+ // Test 5: Test with -T option (terminal type)
235
+ console.log('Test 5: With explicit terminal type -T xterm');
236
+ const sessionName5 = `logtest5-${Date.now()}`;
237
+ const logFile5 = path.join(os.tmpdir(), `screen-log-${sessionName5}.log`);
238
+
239
+ try {
240
+ const screenArgs = [
241
+ '-T',
242
+ 'xterm',
243
+ '-dmS',
244
+ sessionName5,
245
+ '-L',
246
+ '-Logfile',
247
+ logFile5,
248
+ '/bin/sh',
249
+ '-c',
250
+ 'echo "TERMINAL_OUTPUT" && sleep 0.3',
251
+ ];
252
+
253
+ console.log(` Command: screen ${screenArgs.join(' ')}`);
254
+
255
+ execSync(`screen ${screenArgs.map((a) => `"${a}"`).join(' ')}`, {
256
+ stdio: 'inherit',
257
+ });
258
+
259
+ await sleep(1000);
260
+
261
+ // Check log file
262
+ if (fs.existsSync(logFile5)) {
263
+ const content = fs.readFileSync(logFile5, 'utf8');
264
+ console.log(` Log file exists: YES`);
265
+ console.log(` Log file size: ${content.length} bytes`);
266
+ console.log(
267
+ ` Contains expected output: ${content.includes('TERMINAL_OUTPUT') ? 'YES ✓' : 'NO ✗'}`
268
+ );
269
+ fs.unlinkSync(logFile5);
270
+ } else {
271
+ console.log(` Log file exists: NO ✗`);
272
+ }
273
+
274
+ // Cleanup
275
+ try {
276
+ execSync(`screen -S ${sessionName5} -X quit 2>/dev/null`);
277
+ } catch {}
278
+ } catch (e) {
279
+ console.log(` Error: ${e.message}`);
280
+ }
281
+ console.log('');
282
+
283
+ console.log('=== Tests Complete ===');
284
+ }
285
+
286
+ testScreenLogfile();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "start-command",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
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": {
package/src/bin/cli.js CHANGED
@@ -62,7 +62,7 @@ function printUsage() {
62
62
  console.log('');
63
63
  console.log('Options:');
64
64
  console.log(
65
- ' --isolated, -i <environment> Run in isolated environment (screen, tmux, docker, zellij)'
65
+ ' --isolated, -i <environment> Run in isolated environment (screen, tmux, docker)'
66
66
  );
67
67
  console.log(' --attached, -a Run in attached mode (foreground)');
68
68
  console.log(' --detached, -d Run in detached mode (background)');
@@ -85,7 +85,7 @@ function printUsage() {
85
85
  ' - Auto-reports failures for NPM packages (when gh is available)'
86
86
  );
87
87
  console.log(' - Natural language command aliases (via substitutions.lino)');
88
- console.log(' - Process isolation via screen, tmux, zellij, or docker');
88
+ console.log(' - Process isolation via screen, tmux, or docker');
89
89
  console.log('');
90
90
  console.log('Alias examples:');
91
91
  console.log(' $ install lodash npm package -> npm install lodash');
@@ -6,7 +6,7 @@
6
6
  * 2. $ [wrapper-options] command [command-options]
7
7
  *
8
8
  * Wrapper Options:
9
- * --isolated, -i <backend> Run in isolated environment (screen, tmux, docker, zellij)
9
+ * --isolated, -i <backend> Run in isolated environment (screen, tmux, docker)
10
10
  * --attached, -a Run in attached mode (foreground)
11
11
  * --detached, -d Run in detached mode (background)
12
12
  * --session, -s <name> Session name for isolation
@@ -20,7 +20,7 @@ const DEBUG =
20
20
  /**
21
21
  * Valid isolation backends
22
22
  */
23
- const VALID_BACKENDS = ['screen', 'tmux', 'docker', 'zellij'];
23
+ const VALID_BACKENDS = ['screen', 'tmux', 'docker'];
24
24
 
25
25
  /**
26
26
  * Parse command line arguments into wrapper options and command
@@ -29,7 +29,7 @@ const VALID_BACKENDS = ['screen', 'tmux', 'docker', 'zellij'];
29
29
  */
30
30
  function parseArgs(args) {
31
31
  const wrapperOptions = {
32
- isolated: null, // Isolation backend: screen, tmux, docker, zellij
32
+ isolated: null, // Isolation backend: screen, tmux, docker
33
33
  attached: false, // Run in attached mode
34
34
  detached: false, // Run in detached mode
35
35
  session: null, // Session name
@@ -116,7 +116,7 @@ function parseOption(args, index, options) {
116
116
  return 2;
117
117
  } else {
118
118
  throw new Error(
119
- `Option ${arg} requires a backend argument (screen, tmux, docker, zellij)`
119
+ `Option ${arg} requires a backend argument (screen, tmux, docker)`
120
120
  );
121
121
  }
122
122
  }
@@ -4,7 +4,6 @@
4
4
  * Provides execution of commands in various isolated environments:
5
5
  * - screen: GNU Screen terminal multiplexer
6
6
  * - tmux: tmux terminal multiplexer
7
- * - zellij: Modern terminal workspace
8
7
  * - docker: Docker containers
9
8
  */
10
9
 
@@ -20,6 +19,83 @@ const setTimeout = globalThis.setTimeout;
20
19
  const DEBUG =
21
20
  process.env.START_DEBUG === '1' || process.env.START_DEBUG === 'true';
22
21
 
22
+ // Cache for screen version detection
23
+ let cachedScreenVersion = null;
24
+ let screenVersionChecked = false;
25
+
26
+ /**
27
+ * Get the installed screen version
28
+ * @returns {{major: number, minor: number, patch: number}|null} Version object or null if detection fails
29
+ */
30
+ function getScreenVersion() {
31
+ if (screenVersionChecked) {
32
+ return cachedScreenVersion;
33
+ }
34
+
35
+ screenVersionChecked = true;
36
+
37
+ try {
38
+ const output = execSync('screen --version', {
39
+ encoding: 'utf8',
40
+ stdio: ['pipe', 'pipe', 'pipe'],
41
+ });
42
+ // Match patterns like "4.09.01", "4.00.03", "4.5.1"
43
+ const match = output.match(/(\d+)\.(\d+)\.(\d+)/);
44
+ if (match) {
45
+ cachedScreenVersion = {
46
+ major: parseInt(match[1], 10),
47
+ minor: parseInt(match[2], 10),
48
+ patch: parseInt(match[3], 10),
49
+ };
50
+
51
+ if (DEBUG) {
52
+ console.log(
53
+ `[DEBUG] Detected screen version: ${cachedScreenVersion.major}.${cachedScreenVersion.minor}.${cachedScreenVersion.patch}`
54
+ );
55
+ }
56
+
57
+ return cachedScreenVersion;
58
+ }
59
+ } catch {
60
+ if (DEBUG) {
61
+ console.log('[DEBUG] Could not detect screen version');
62
+ }
63
+ }
64
+
65
+ return null;
66
+ }
67
+
68
+ /**
69
+ * Check if screen supports the -Logfile option
70
+ * The -Logfile option was introduced in GNU Screen 4.5.1
71
+ * @returns {boolean} True if -Logfile is supported
72
+ */
73
+ function supportsLogfileOption() {
74
+ const version = getScreenVersion();
75
+ if (!version) {
76
+ // If we can't detect version, assume older version and use fallback
77
+ return false;
78
+ }
79
+
80
+ // -Logfile was added in 4.5.1
81
+ // Compare: version >= 4.5.1
82
+ if (version.major > 4) {
83
+ return true;
84
+ }
85
+ if (version.major < 4) {
86
+ return false;
87
+ }
88
+ // major === 4
89
+ if (version.minor > 5) {
90
+ return true;
91
+ }
92
+ if (version.minor < 5) {
93
+ return false;
94
+ }
95
+ // minor === 5
96
+ return version.patch >= 1;
97
+ }
98
+
23
99
  /**
24
100
  * Check if a command is available on the system
25
101
  * @param {string} command - Command to check
@@ -58,6 +134,11 @@ function hasTTY() {
58
134
  /**
59
135
  * Run command in GNU Screen using detached mode with log capture
60
136
  * This is a workaround for environments without TTY
137
+ *
138
+ * Supports two methods based on screen version:
139
+ * - screen >= 4.5.1: Uses -L -Logfile option for native log capture
140
+ * - screen < 4.5.1: Uses tee command within the wrapped command for output capture
141
+ *
61
142
  * @param {string} command - Command to execute
62
143
  * @param {string} sessionName - Session name
63
144
  * @param {object} shellInfo - Shell info from getShell()
@@ -67,25 +148,45 @@ function runScreenWithLogCapture(command, sessionName, shellInfo) {
67
148
  const { shell, shellArg } = shellInfo;
68
149
  const logFile = path.join(os.tmpdir(), `screen-output-${sessionName}.log`);
69
150
 
151
+ // Check if screen supports -Logfile option (added in 4.5.1)
152
+ const useNativeLogging = supportsLogfileOption();
153
+
70
154
  return new Promise((resolve) => {
71
155
  try {
72
- // Use detached mode with logging to capture output
73
- // screen -dmS <session> -L -Logfile <logfile> <shell> -c '<command>'
74
- const screenArgs = [
75
- '-dmS',
76
- sessionName,
77
- '-L',
78
- '-Logfile',
79
- logFile,
80
- shell,
81
- shellArg,
82
- command,
83
- ];
156
+ let screenArgs;
157
+ let effectiveCommand = command;
158
+
159
+ if (useNativeLogging) {
160
+ // Modern screen (>= 4.5.1): Use -L -Logfile option for native log capture
161
+ // screen -dmS <session> -L -Logfile <logfile> <shell> -c '<command>'
162
+ screenArgs = [
163
+ '-dmS',
164
+ sessionName,
165
+ '-L',
166
+ '-Logfile',
167
+ logFile,
168
+ shell,
169
+ shellArg,
170
+ command,
171
+ ];
84
172
 
85
- if (DEBUG) {
86
- console.log(
87
- `[DEBUG] Running screen with log capture: screen ${screenArgs.join(' ')}`
88
- );
173
+ if (DEBUG) {
174
+ console.log(
175
+ `[DEBUG] Running screen with native log capture (-Logfile): screen ${screenArgs.join(' ')}`
176
+ );
177
+ }
178
+ } else {
179
+ // Older screen (< 4.5.1, e.g., macOS bundled 4.0.3): Use tee fallback
180
+ // Wrap the command to capture output using tee
181
+ // The parentheses ensure proper grouping of the command and its stderr
182
+ effectiveCommand = `(${command}) 2>&1 | tee "${logFile}"`;
183
+ screenArgs = ['-dmS', sessionName, shell, shellArg, effectiveCommand];
184
+
185
+ if (DEBUG) {
186
+ console.log(
187
+ `[DEBUG] Running screen with tee fallback (older screen version): screen ${screenArgs.join(' ')}`
188
+ );
189
+ }
89
190
  }
90
191
 
91
192
  execSync(`screen ${screenArgs.map((a) => `"${a}"`).join(' ')}`, {
@@ -360,87 +461,6 @@ function runInTmux(command, options = {}) {
360
461
  }
361
462
  }
362
463
 
363
- /**
364
- * Run command in Zellij
365
- * @param {string} command - Command to execute
366
- * @param {object} options - Options (session, detached)
367
- * @returns {Promise<{success: boolean, sessionName: string, message: string}>}
368
- */
369
- function runInZellij(command, options = {}) {
370
- if (!isCommandAvailable('zellij')) {
371
- return Promise.resolve({
372
- success: false,
373
- sessionName: null,
374
- message:
375
- 'zellij is not installed. Install it with: cargo install zellij or brew install zellij (macOS)',
376
- });
377
- }
378
-
379
- const sessionName = options.session || generateSessionName('zellij');
380
- const { shell, shellArg } = getShell();
381
-
382
- try {
383
- if (options.detached) {
384
- // Detached mode for zellij
385
- if (DEBUG) {
386
- console.log(`[DEBUG] Creating detached zellij session: ${sessionName}`);
387
- }
388
-
389
- // Create the session in background
390
- execSync(
391
- `zellij -s "${sessionName}" action new-tab -- ${shell} ${shellArg} "${command}" &`,
392
- { stdio: 'inherit', shell: true }
393
- );
394
-
395
- return Promise.resolve({
396
- success: true,
397
- sessionName,
398
- message: `Command started in detached zellij session: ${sessionName}\nReattach with: zellij attach ${sessionName}`,
399
- });
400
- } else {
401
- // Attached mode: zellij -s <session> -- <shell> -c <command>
402
- if (DEBUG) {
403
- console.log(
404
- `[DEBUG] Running: zellij -s "${sessionName}" -- ${shell} ${shellArg} "${command}"`
405
- );
406
- }
407
-
408
- return new Promise((resolve) => {
409
- const child = spawn(
410
- 'zellij',
411
- ['-s', sessionName, '--', shell, shellArg, command],
412
- {
413
- stdio: 'inherit',
414
- }
415
- );
416
-
417
- child.on('exit', (code) => {
418
- resolve({
419
- success: code === 0,
420
- sessionName,
421
- message: `Zellij session "${sessionName}" exited with code ${code}`,
422
- exitCode: code,
423
- });
424
- });
425
-
426
- child.on('error', (err) => {
427
- resolve({
428
- success: false,
429
- sessionName,
430
- message: `Failed to start zellij: ${err.message}`,
431
- });
432
- });
433
- });
434
- }
435
- } catch (err) {
436
- return Promise.resolve({
437
- success: false,
438
- sessionName,
439
- message: `Failed to run in zellij: ${err.message}`,
440
- });
441
- }
442
- }
443
-
444
464
  /**
445
465
  * Run command in Docker container
446
466
  * @param {string} command - Command to execute
@@ -547,7 +567,7 @@ function runInDocker(command, options = {}) {
547
567
 
548
568
  /**
549
569
  * Run command in the specified isolation backend
550
- * @param {string} backend - Isolation backend (screen, tmux, docker, zellij)
570
+ * @param {string} backend - Isolation backend (screen, tmux, docker)
551
571
  * @param {string} command - Command to execute
552
572
  * @param {object} options - Options
553
573
  * @returns {Promise<{success: boolean, message: string}>}
@@ -558,8 +578,6 @@ function runIsolated(backend, command, options = {}) {
558
578
  return runInScreen(command, options);
559
579
  case 'tmux':
560
580
  return runInTmux(command, options);
561
- case 'zellij':
562
- return runInZellij(command, options);
563
581
  case 'docker':
564
582
  return runInDocker(command, options);
565
583
  default:
@@ -665,12 +683,19 @@ function createLogPath(environment) {
665
683
  return path.join(logDir, logFilename);
666
684
  }
667
685
 
686
+ /**
687
+ * Reset screen version cache (useful for testing)
688
+ */
689
+ function resetScreenVersionCache() {
690
+ cachedScreenVersion = null;
691
+ screenVersionChecked = false;
692
+ }
693
+
668
694
  module.exports = {
669
695
  isCommandAvailable,
670
696
  hasTTY,
671
697
  runInScreen,
672
698
  runInTmux,
673
- runInZellij,
674
699
  runInDocker,
675
700
  runIsolated,
676
701
  // Export logging utilities for unified experience
@@ -681,4 +706,8 @@ module.exports = {
681
706
  writeLogFile,
682
707
  getLogDir,
683
708
  createLogPath,
709
+ // Export screen version utilities for testing and debugging
710
+ getScreenVersion,
711
+ supportsLogfileOption,
712
+ resetScreenVersionCache,
684
713
  };
@@ -382,8 +382,4 @@ describe('VALID_BACKENDS', () => {
382
382
  it('should include docker', () => {
383
383
  assert.ok(VALID_BACKENDS.includes('docker'));
384
384
  });
385
-
386
- it('should include zellij', () => {
387
- assert.ok(VALID_BACKENDS.includes('zellij'));
388
- });
389
385
  });
@@ -7,7 +7,13 @@
7
7
 
8
8
  const { describe, it } = require('node:test');
9
9
  const assert = require('assert');
10
- const { isCommandAvailable, hasTTY } = require('../src/lib/isolation');
10
+ const {
11
+ isCommandAvailable,
12
+ hasTTY,
13
+ getScreenVersion,
14
+ supportsLogfileOption,
15
+ resetScreenVersionCache,
16
+ } = require('../src/lib/isolation');
11
17
 
12
18
  describe('Isolation Module', () => {
13
19
  describe('isCommandAvailable', () => {
@@ -70,11 +76,91 @@ describe('Isolation Module', () => {
70
76
  console.log(` docker available: ${result}`);
71
77
  assert.ok(typeof result === 'boolean');
72
78
  });
79
+ });
73
80
 
74
- it('should check if zellij is available', () => {
75
- const result = isCommandAvailable('zellij');
76
- console.log(` zellij available: ${result}`);
77
- assert.ok(typeof result === 'boolean');
81
+ describe('getScreenVersion', () => {
82
+ it('should return version object or null', () => {
83
+ // Reset cache before testing
84
+ resetScreenVersionCache();
85
+ const version = getScreenVersion();
86
+
87
+ if (isCommandAvailable('screen')) {
88
+ // If screen is installed, we should get a version object
89
+ assert.ok(
90
+ version !== null,
91
+ 'Should return version object when screen is installed'
92
+ );
93
+ assert.ok(typeof version.major === 'number', 'major should be number');
94
+ assert.ok(typeof version.minor === 'number', 'minor should be number');
95
+ assert.ok(typeof version.patch === 'number', 'patch should be number');
96
+ console.log(
97
+ ` Detected screen version: ${version.major}.${version.minor}.${version.patch}`
98
+ );
99
+ } else {
100
+ // If screen is not installed, we should get null
101
+ assert.strictEqual(
102
+ version,
103
+ null,
104
+ 'Should return null when screen is not installed'
105
+ );
106
+ console.log(' screen not installed, version is null');
107
+ }
108
+ });
109
+
110
+ it('should cache the version result', () => {
111
+ // Reset cache first
112
+ resetScreenVersionCache();
113
+
114
+ // Call twice
115
+ const version1 = getScreenVersion();
116
+ const version2 = getScreenVersion();
117
+
118
+ // Results should be identical (same object reference if cached)
119
+ assert.strictEqual(
120
+ version1,
121
+ version2,
122
+ 'Cached version should return same object'
123
+ );
124
+ });
125
+ });
126
+
127
+ describe('supportsLogfileOption', () => {
128
+ it('should return boolean', () => {
129
+ // Reset cache before testing
130
+ resetScreenVersionCache();
131
+ const result = supportsLogfileOption();
132
+ assert.ok(typeof result === 'boolean', 'Should return a boolean');
133
+ console.log(` supportsLogfileOption: ${result}`);
134
+ });
135
+
136
+ it('should return true for screen >= 4.5.1', () => {
137
+ // This tests the logic by checking the current system
138
+ resetScreenVersionCache();
139
+ const version = getScreenVersion();
140
+
141
+ if (version) {
142
+ const expected =
143
+ version.major > 4 ||
144
+ (version.major === 4 && version.minor > 5) ||
145
+ (version.major === 4 && version.minor === 5 && version.patch >= 1);
146
+ const result = supportsLogfileOption();
147
+ assert.strictEqual(
148
+ result,
149
+ expected,
150
+ `Version ${version.major}.${version.minor}.${version.patch} should ${expected ? 'support' : 'not support'} -Logfile`
151
+ );
152
+ console.log(
153
+ ` Version ${version.major}.${version.minor}.${version.patch}: -Logfile supported = ${result}`
154
+ );
155
+ } else {
156
+ // If no version detected, should return false (fallback to safe method)
157
+ const result = supportsLogfileOption();
158
+ assert.strictEqual(
159
+ result,
160
+ false,
161
+ 'Should return false when version cannot be detected'
162
+ );
163
+ }
78
164
  });
79
165
  });
80
166
  });
@@ -86,7 +172,6 @@ describe('Isolation Runner Error Handling', () => {
86
172
  runInScreen,
87
173
  runInTmux,
88
174
  runInDocker,
89
- runInZellij,
90
175
  } = require('../src/lib/isolation');
91
176
 
92
177
  describe('runInScreen', () => {
@@ -156,23 +241,6 @@ describe('Isolation Runner Error Handling', () => {
156
241
  );
157
242
  });
158
243
  });
159
-
160
- describe('runInZellij', () => {
161
- it('should return informative error if zellij is not installed', async () => {
162
- // Skip if zellij is installed
163
- if (isCommandAvailable('zellij')) {
164
- console.log(' Skipping: zellij is installed');
165
- return;
166
- }
167
-
168
- const result = await runInZellij('echo test', { detached: true });
169
- assert.strictEqual(result.success, false);
170
- assert.ok(result.message.includes('zellij is not installed'));
171
- assert.ok(
172
- result.message.includes('cargo') || result.message.includes('brew')
173
- );
174
- });
175
- });
176
244
  });
177
245
 
178
246
  describe('Isolation Runner with Available Backends', () => {