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.
- package/.github/workflows/release.yml +8 -0
- package/CHANGELOG.md +17 -0
- package/README.md +7 -8
- package/REQUIREMENTS.md +0 -1
- package/docs/case-studies/issue-15/README.md +104 -58
- package/experiments/isolation-design.md +3 -14
- package/experiments/test-screen-logfile.js +286 -0
- package/package.json +1 -1
- package/src/bin/cli.js +2 -2
- package/src/lib/args-parser.js +4 -4
- package/src/lib/isolation.js +131 -102
- package/test/args-parser.test.js +0 -4
- package/test/isolation.test.js +91 -23
|
@@ -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
|
|
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:**
|
|
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
|
-
###
|
|
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
|
|
91
|
-
|
|
92
|
-
###
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
**
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
178
|
+
For older versions, wrap command with tee:
|
|
137
179
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
146
|
-
2. **
|
|
147
|
-
3. **
|
|
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
|
-
|
|
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
|
|
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
|
|
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
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
|
|
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,
|
|
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');
|
package/src/lib/args-parser.js
CHANGED
|
@@ -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
|
|
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'
|
|
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
|
|
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
|
|
119
|
+
`Option ${arg} requires a backend argument (screen, tmux, docker)`
|
|
120
120
|
);
|
|
121
121
|
}
|
|
122
122
|
}
|
package/src/lib/isolation.js
CHANGED
|
@@ -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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
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
|
};
|
package/test/args-parser.test.js
CHANGED
package/test/isolation.test.js
CHANGED
|
@@ -7,7 +7,13 @@
|
|
|
7
7
|
|
|
8
8
|
const { describe, it } = require('node:test');
|
|
9
9
|
const assert = require('assert');
|
|
10
|
-
const {
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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', () => {
|