start-command 0.3.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +67 -0
- package/README.md +8 -0
- package/docs/case-studies/issue-15/README.md +162 -0
- package/eslint.config.mjs +9 -0
- package/experiments/debug-regex.js +1 -1
- package/experiments/test-screen-attached.js +126 -0
- package/experiments/test-screen-modes.js +128 -0
- package/experiments/test-screen-output.sh +27 -0
- package/experiments/test-substitution.js +1 -1
- package/package.json +1 -1
- package/scripts/changeset-version.mjs +1 -1
- package/scripts/check-file-size.mjs +1 -1
- package/scripts/create-github-release.mjs +1 -1
- package/scripts/create-manual-changeset.mjs +1 -1
- package/scripts/format-github-release.mjs +1 -1
- package/scripts/format-release-notes.mjs +1 -1
- package/scripts/instant-version-bump.mjs +1 -1
- package/scripts/publish-to-npm.mjs +1 -1
- package/scripts/setup-npm.mjs +1 -1
- package/scripts/validate-changeset.mjs +1 -1
- package/scripts/version-and-commit.mjs +1 -1
- package/src/bin/cli.js +58 -19
- package/src/lib/isolation.js +288 -23
- package/test/args-parser.test.js +1 -1
- package/test/isolation.test.js +67 -2
- package/test/substitution.test.js +1 -1
- package/.changeset/isolation-support.md +0 -30
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,72 @@
|
|
|
1
1
|
# start-command
|
|
2
2
|
|
|
3
|
+
## 0.5.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- bdf77c7: Fix screen isolation environment not capturing command output in attached mode
|
|
8
|
+
|
|
9
|
+
When running commands with `--isolated screen` in attached mode, the command output was not being displayed (only "screen is terminating" was shown). This was because GNU Screen requires a TTY to run in attached mode, which is not available when spawning from Node.js without a terminal.
|
|
10
|
+
|
|
11
|
+
The fix implements a fallback mechanism that:
|
|
12
|
+
- Checks if a TTY is available before spawning screen
|
|
13
|
+
- If no TTY is available, uses detached mode with log capture to run the command and display its output
|
|
14
|
+
- Polls for session completion and reads the captured log file
|
|
15
|
+
- Displays the output to the user just as if it was running in attached mode
|
|
16
|
+
|
|
17
|
+
This ensures that `$ --isolated screen -- echo "hello"` now correctly displays "hello" even when running from environments without a TTY (like CI/CD pipelines, scripts, or when piping output).
|
|
18
|
+
|
|
19
|
+
## 0.5.1
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- Test patch release
|
|
24
|
+
|
|
25
|
+
## 0.5.0
|
|
26
|
+
|
|
27
|
+
### Minor Changes
|
|
28
|
+
|
|
29
|
+
- 95d8760: Unify output experience for isolation mode
|
|
30
|
+
- Change terminology from "Backend" to "Environment" in isolation output
|
|
31
|
+
- Add unified logging with timestamps for isolation modes (screen, tmux, docker, zellij)
|
|
32
|
+
- Save log files for all execution modes with consistent format
|
|
33
|
+
- Display start/end timestamps, exit code, and log file path uniformly across all modes
|
|
34
|
+
|
|
35
|
+
## 0.4.1
|
|
36
|
+
|
|
37
|
+
### Patch Changes
|
|
38
|
+
|
|
39
|
+
- 73635f9: Make it bun first - update shebangs and installation docs
|
|
40
|
+
|
|
41
|
+
## 0.4.0
|
|
42
|
+
|
|
43
|
+
### Minor Changes
|
|
44
|
+
|
|
45
|
+
- e8bec3c: Add process isolation support with --isolated option
|
|
46
|
+
|
|
47
|
+
This release adds the ability to run commands in isolated environments:
|
|
48
|
+
|
|
49
|
+
**New Features:**
|
|
50
|
+
- `--isolated` / `-i` option to run commands in screen, tmux, zellij, or docker
|
|
51
|
+
- `--attached` / `-a` and `--detached` / `-d` modes for foreground/background execution
|
|
52
|
+
- `--session` / `-s` option for custom session names
|
|
53
|
+
- `--image` option for Docker container image specification
|
|
54
|
+
- Two command syntax patterns: `$ [options] -- [command]` or `$ [options] command`
|
|
55
|
+
|
|
56
|
+
**Supported Backends:**
|
|
57
|
+
- GNU Screen - classic terminal multiplexer
|
|
58
|
+
- tmux - modern terminal multiplexer
|
|
59
|
+
- zellij - modern terminal workspace
|
|
60
|
+
- Docker - container isolation
|
|
61
|
+
|
|
62
|
+
**Examples:**
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
$ --isolated tmux -- npm start
|
|
66
|
+
$ -i screen -d npm start
|
|
67
|
+
$ --isolated docker --image node:20 -- npm install
|
|
68
|
+
```
|
|
69
|
+
|
|
3
70
|
## 0.3.1
|
|
4
71
|
|
|
5
72
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -4,6 +4,14 @@ Gamification of coding - execute any command with automatic logging and ability
|
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
|
+
We recommend using [Bun](https://bun.sh) for the best performance:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun install -g start-command
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or using npm:
|
|
14
|
+
|
|
7
15
|
```bash
|
|
8
16
|
npm install -g start-command
|
|
9
17
|
```
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Case Study: Issue #15 - Screen Isolation Not Working As Expected
|
|
2
|
+
|
|
3
|
+
## Issue Summary
|
|
4
|
+
|
|
5
|
+
**Issue URL:** https://github.com/link-foundation/start/issues/15
|
|
6
|
+
**Date Reported:** 2025-12-22
|
|
7
|
+
**Reporter:** @konard
|
|
8
|
+
**Status:** Investigating
|
|
9
|
+
|
|
10
|
+
### Problem Statement
|
|
11
|
+
|
|
12
|
+
The screen isolation environment does not display command output when running in attached mode. When using `$ --isolated screen -- echo "hello"`, the expected output "hello" is not shown - instead, only `[screen is terminating]` appears.
|
|
13
|
+
|
|
14
|
+
### Environment
|
|
15
|
+
|
|
16
|
+
- **Platform:** macOS (reported), Linux (reproduced)
|
|
17
|
+
- **Package:** start-command@0.5.1
|
|
18
|
+
- **Screen version:** Tested with GNU Screen
|
|
19
|
+
|
|
20
|
+
## Timeline of Events
|
|
21
|
+
|
|
22
|
+
1. User installs start-command: `bun install -g start-command`
|
|
23
|
+
2. Direct command execution works: `$ echo "hello"` shows "hello"
|
|
24
|
+
3. Docker isolation works: `$ --isolated docker --image alpine -- echo "hello"` shows "hello"
|
|
25
|
+
4. Screen isolation fails: `$ --isolated screen -- echo "hello"` shows only `[screen is terminating]`
|
|
26
|
+
|
|
27
|
+
## Reproduction
|
|
28
|
+
|
|
29
|
+
### Observed Behavior
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
$ echo "hello"
|
|
33
|
+
[2025-12-22 13:45:05.245] Starting: echo hello
|
|
34
|
+
hello
|
|
35
|
+
[2025-12-22 13:45:05.254] Finished
|
|
36
|
+
Exit code: 0
|
|
37
|
+
|
|
38
|
+
$ --isolated docker --image alpine -- echo "hello"
|
|
39
|
+
[2025-12-22 13:45:07.847] Starting: echo hello
|
|
40
|
+
[Isolation] Environment: docker, Mode: attached
|
|
41
|
+
[Isolation] Image: alpine
|
|
42
|
+
hello
|
|
43
|
+
Docker container "docker-..." exited with code 0
|
|
44
|
+
[2025-12-22 13:45:08.066] Finished
|
|
45
|
+
Exit code: 0
|
|
46
|
+
|
|
47
|
+
$ --isolated screen -- echo "hello"
|
|
48
|
+
[2025-12-22 13:45:11.134] Starting: echo hello
|
|
49
|
+
[Isolation] Environment: screen, Mode: attached
|
|
50
|
+
[screen is terminating]
|
|
51
|
+
Screen session "screen-..." exited with code 0
|
|
52
|
+
[2025-12-22 13:45:11.199] Finished
|
|
53
|
+
Exit code: 0
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Notice:** No "hello" output in the screen isolation case, though exit code is 0.
|
|
57
|
+
|
|
58
|
+
## Root Cause Analysis
|
|
59
|
+
|
|
60
|
+
### Investigation
|
|
61
|
+
|
|
62
|
+
1. **TTY Requirement**: The GNU Screen command requires a connected terminal (TTY/PTY) to run in attached mode.
|
|
63
|
+
|
|
64
|
+
2. **Node.js spawn behavior**: When spawning processes with `child_process.spawn()`, even with `stdio: 'inherit'`, Node.js does not always provide a proper pseudo-terminal (PTY) that screen requires.
|
|
65
|
+
|
|
66
|
+
3. **Error in non-TTY environments**: Running `screen -S session shell -c command` without a TTY results in:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
Must be connected to a terminal.
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
4. **Detached mode works**: Running `screen -dmS session shell -c command` works because it doesn't require an attached terminal.
|
|
73
|
+
|
|
74
|
+
### Experimental Evidence
|
|
75
|
+
|
|
76
|
+
Testing revealed:
|
|
77
|
+
|
|
78
|
+
- `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
|
|
80
|
+
- Using `script -q -c "screen ..." /dev/null` can provide a PTY but includes terminal escape codes
|
|
81
|
+
|
|
82
|
+
### Comparison with Docker
|
|
83
|
+
|
|
84
|
+
Docker isolation works because:
|
|
85
|
+
|
|
86
|
+
1. Docker run with `-it` flags handles terminal attachment
|
|
87
|
+
2. Docker spawns an isolated container that manages its own pseudo-terminal
|
|
88
|
+
3. The command output flows through Docker's I/O handling
|
|
89
|
+
|
|
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
|
|
135
|
+
|
|
136
|
+
The fix modifies `src/lib/isolation.js` to:
|
|
137
|
+
|
|
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
|
|
142
|
+
|
|
143
|
+
## Testing Strategy
|
|
144
|
+
|
|
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
|
|
148
|
+
|
|
149
|
+
## References
|
|
150
|
+
|
|
151
|
+
- [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
|
+
- [script command man page](https://man7.org/linux/man-pages/man1/script.1.html)
|
|
155
|
+
|
|
156
|
+
## Appendix: Test Logs
|
|
157
|
+
|
|
158
|
+
See accompanying log files:
|
|
159
|
+
|
|
160
|
+
- `test-output-1.log` - Initial reproduction
|
|
161
|
+
- `screen-modes-test.log` - Screen modes investigation
|
|
162
|
+
- `screen-attached-approaches.log` - Solution approaches testing
|
package/eslint.config.mjs
CHANGED
|
@@ -105,9 +105,18 @@ export default [
|
|
|
105
105
|
'**/*.test.js',
|
|
106
106
|
'experiments/**/*.js',
|
|
107
107
|
],
|
|
108
|
+
languageOptions: {
|
|
109
|
+
globals: {
|
|
110
|
+
setTimeout: 'readonly',
|
|
111
|
+
setInterval: 'readonly',
|
|
112
|
+
clearTimeout: 'readonly',
|
|
113
|
+
clearInterval: 'readonly',
|
|
114
|
+
},
|
|
115
|
+
},
|
|
108
116
|
rules: {
|
|
109
117
|
'require-await': 'off', // Async functions without await are common in tests
|
|
110
118
|
'no-unused-vars': 'warn', // Relax for experiments
|
|
119
|
+
'no-empty': 'off', // Empty catch blocks are common in experiments
|
|
111
120
|
},
|
|
112
121
|
},
|
|
113
122
|
{
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Experiment to test different approaches for running screen in attached mode
|
|
4
|
+
* from Node.js without a TTY
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { spawn, spawnSync, execSync } = require('child_process');
|
|
8
|
+
|
|
9
|
+
async function testApproaches() {
|
|
10
|
+
console.log('=== Testing Attached Mode Approaches ===\n');
|
|
11
|
+
|
|
12
|
+
// Approach 1: Using script command as wrapper
|
|
13
|
+
console.log('Approach 1: script -q -c "screen ..." /dev/null');
|
|
14
|
+
try {
|
|
15
|
+
const sessionName = `approach1-${Date.now()}`;
|
|
16
|
+
const result = spawnSync(
|
|
17
|
+
'script',
|
|
18
|
+
[
|
|
19
|
+
'-q',
|
|
20
|
+
'-c',
|
|
21
|
+
`screen -S ${sessionName} /bin/sh -c "echo hello; exit 0"`,
|
|
22
|
+
'/dev/null',
|
|
23
|
+
],
|
|
24
|
+
{
|
|
25
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
26
|
+
timeout: 5000,
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
console.log(
|
|
30
|
+
` stdout: "${result.stdout.toString().trim().replace(/\n/g, '\\n')}"`
|
|
31
|
+
);
|
|
32
|
+
console.log(
|
|
33
|
+
` stderr: "${result.stderr.toString().trim().replace(/\n/g, '\\n')}"`
|
|
34
|
+
);
|
|
35
|
+
console.log(` exit: ${result.status}`);
|
|
36
|
+
// Cleanup
|
|
37
|
+
try {
|
|
38
|
+
execSync(`screen -S ${sessionName} -X quit 2>/dev/null`);
|
|
39
|
+
} catch {}
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.log(` Error: ${e.message}`);
|
|
42
|
+
}
|
|
43
|
+
console.log('');
|
|
44
|
+
|
|
45
|
+
// Approach 2: Using detached mode + wait for completion + capture output via log
|
|
46
|
+
console.log('Approach 2: detached + log capture');
|
|
47
|
+
try {
|
|
48
|
+
const sessionName = `approach2-${Date.now()}`;
|
|
49
|
+
const logFile = `/tmp/screen-${sessionName}.log`;
|
|
50
|
+
|
|
51
|
+
// Start with logging
|
|
52
|
+
execSync(
|
|
53
|
+
`screen -dmS ${sessionName} -L -Logfile ${logFile} /bin/sh -c "echo 'hello from approach2'"`,
|
|
54
|
+
{
|
|
55
|
+
stdio: 'inherit',
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Wait for completion
|
|
60
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
61
|
+
|
|
62
|
+
// Read log
|
|
63
|
+
const output = execSync(`cat ${logFile}`, { encoding: 'utf8' });
|
|
64
|
+
console.log(` Output: "${output.trim()}"`);
|
|
65
|
+
console.log(` Status: SUCCESS`);
|
|
66
|
+
|
|
67
|
+
// Cleanup
|
|
68
|
+
try {
|
|
69
|
+
execSync(`rm ${logFile} 2>/dev/null`);
|
|
70
|
+
} catch {}
|
|
71
|
+
try {
|
|
72
|
+
execSync(`screen -S ${sessionName} -X quit 2>/dev/null`);
|
|
73
|
+
} catch {}
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.log(` Error: ${e.message}`);
|
|
76
|
+
}
|
|
77
|
+
console.log('');
|
|
78
|
+
|
|
79
|
+
// Approach 3: Using stdio: 'inherit' with proper terminal allocation via script
|
|
80
|
+
console.log('Approach 3: Run through script, inherit all stdio');
|
|
81
|
+
try {
|
|
82
|
+
const sessionName = `approach3-${Date.now()}`;
|
|
83
|
+
const result = spawnSync(
|
|
84
|
+
'script',
|
|
85
|
+
[
|
|
86
|
+
'-q',
|
|
87
|
+
'-e',
|
|
88
|
+
'-c',
|
|
89
|
+
`screen -S ${sessionName} /bin/sh -c "echo hello_approach3"`,
|
|
90
|
+
'/dev/null',
|
|
91
|
+
],
|
|
92
|
+
{
|
|
93
|
+
stdio: 'inherit',
|
|
94
|
+
timeout: 5000,
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
console.log(` exit: ${result.status}`);
|
|
98
|
+
// Cleanup
|
|
99
|
+
try {
|
|
100
|
+
execSync(`screen -S ${sessionName} -X quit 2>/dev/null`);
|
|
101
|
+
} catch {}
|
|
102
|
+
} catch (e) {
|
|
103
|
+
console.log(` Error: ${e.message}`);
|
|
104
|
+
}
|
|
105
|
+
console.log('');
|
|
106
|
+
|
|
107
|
+
// Approach 4: Direct run without screen for attached mode (fallback)
|
|
108
|
+
console.log(
|
|
109
|
+
'Approach 4: Just spawn the shell command directly (fallback, no screen)'
|
|
110
|
+
);
|
|
111
|
+
try {
|
|
112
|
+
const result = spawnSync('/bin/sh', ['-c', 'echo hello_direct'], {
|
|
113
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
114
|
+
timeout: 5000,
|
|
115
|
+
});
|
|
116
|
+
console.log(` stdout: "${result.stdout.toString().trim()}"`);
|
|
117
|
+
console.log(` exit: ${result.status}`);
|
|
118
|
+
} catch (e) {
|
|
119
|
+
console.log(` Error: ${e.message}`);
|
|
120
|
+
}
|
|
121
|
+
console.log('');
|
|
122
|
+
|
|
123
|
+
console.log('=== Approaches Tested ===');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
testApproaches();
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Experiment to test different screen invocation modes
|
|
4
|
+
* This helps understand how screen behaves in different contexts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { spawn, execSync, spawnSync } = require('child_process');
|
|
8
|
+
|
|
9
|
+
async function testScreenModes() {
|
|
10
|
+
console.log('=== Testing Screen Modes ===\n');
|
|
11
|
+
|
|
12
|
+
// Test 1: Check if running in a terminal
|
|
13
|
+
console.log('1. Terminal check:');
|
|
14
|
+
console.log(` process.stdin.isTTY: ${process.stdin.isTTY}`);
|
|
15
|
+
console.log(` process.stdout.isTTY: ${process.stdout.isTTY}`);
|
|
16
|
+
console.log(` TERM: ${process.env.TERM || 'not set'}`);
|
|
17
|
+
console.log('');
|
|
18
|
+
|
|
19
|
+
// Test 2: Detached mode should work
|
|
20
|
+
console.log('2. Testing detached mode (screen -dmS):');
|
|
21
|
+
try {
|
|
22
|
+
const sessionName = `test-${Date.now()}`;
|
|
23
|
+
execSync(
|
|
24
|
+
`screen -dmS ${sessionName} /bin/sh -c "echo hello > /tmp/screen-test-${sessionName}.txt"`,
|
|
25
|
+
{
|
|
26
|
+
stdio: 'inherit',
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
// Wait a bit for the command to complete
|
|
30
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
31
|
+
|
|
32
|
+
const output = execSync(
|
|
33
|
+
`cat /tmp/screen-test-${sessionName}.txt 2>/dev/null || echo "file not found"`,
|
|
34
|
+
{ encoding: 'utf8' }
|
|
35
|
+
);
|
|
36
|
+
console.log(` Output: ${output.trim()}`);
|
|
37
|
+
|
|
38
|
+
// Cleanup
|
|
39
|
+
try {
|
|
40
|
+
execSync(`screen -S ${sessionName} -X quit 2>/dev/null`);
|
|
41
|
+
} catch {}
|
|
42
|
+
try {
|
|
43
|
+
execSync(`rm /tmp/screen-test-${sessionName}.txt 2>/dev/null`);
|
|
44
|
+
} catch {}
|
|
45
|
+
|
|
46
|
+
console.log(' Status: SUCCESS');
|
|
47
|
+
} catch (e) {
|
|
48
|
+
console.log(` Status: FAILED - ${e.message}`);
|
|
49
|
+
}
|
|
50
|
+
console.log('');
|
|
51
|
+
|
|
52
|
+
// Test 3: Attached mode with spawn (current implementation)
|
|
53
|
+
console.log('3. Testing attached mode with spawn (current implementation):');
|
|
54
|
+
try {
|
|
55
|
+
const sessionName = `test-${Date.now()}`;
|
|
56
|
+
const result = spawnSync(
|
|
57
|
+
'screen',
|
|
58
|
+
['-S', sessionName, '/bin/sh', '-c', 'echo hello'],
|
|
59
|
+
{
|
|
60
|
+
stdio: 'inherit',
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
console.log(` Exit code: ${result.status}`);
|
|
64
|
+
console.log(` Status: ${result.status === 0 ? 'SUCCESS' : 'FAILED'}`);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
console.log(` Status: FAILED - ${e.message}`);
|
|
67
|
+
}
|
|
68
|
+
console.log('');
|
|
69
|
+
|
|
70
|
+
// Test 4: Try to use script command to allocate PTY
|
|
71
|
+
console.log('4. Testing with script command to allocate PTY:');
|
|
72
|
+
try {
|
|
73
|
+
const result = spawnSync(
|
|
74
|
+
'script',
|
|
75
|
+
['-q', '-c', 'echo hello', '/dev/null'],
|
|
76
|
+
{
|
|
77
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
console.log(` Output: ${result.stdout.toString().trim()}`);
|
|
81
|
+
console.log(` Exit code: ${result.status}`);
|
|
82
|
+
console.log(` Status: SUCCESS`);
|
|
83
|
+
} catch (e) {
|
|
84
|
+
console.log(` Status: FAILED - ${e.message}`);
|
|
85
|
+
}
|
|
86
|
+
console.log('');
|
|
87
|
+
|
|
88
|
+
// Test 5: Detached mode with log capture
|
|
89
|
+
console.log('5. Testing detached mode with log capture:');
|
|
90
|
+
try {
|
|
91
|
+
const sessionName = `test-${Date.now()}`;
|
|
92
|
+
const logFile = `/tmp/screen-log-${sessionName}.txt`;
|
|
93
|
+
|
|
94
|
+
// Create detached session with logging
|
|
95
|
+
execSync(
|
|
96
|
+
`screen -dmS ${sessionName} -L -Logfile ${logFile} /bin/sh -c "echo 'hello from screen'; sleep 0.2"`,
|
|
97
|
+
{
|
|
98
|
+
stdio: 'inherit',
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Wait for command to complete
|
|
103
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
104
|
+
|
|
105
|
+
const output = execSync(
|
|
106
|
+
`cat ${logFile} 2>/dev/null || echo "log not found"`,
|
|
107
|
+
{ encoding: 'utf8' }
|
|
108
|
+
);
|
|
109
|
+
console.log(` Log content: ${output.trim().replace(/\n/g, '\\n')}`);
|
|
110
|
+
|
|
111
|
+
// Cleanup
|
|
112
|
+
try {
|
|
113
|
+
execSync(`screen -S ${sessionName} -X quit 2>/dev/null`);
|
|
114
|
+
} catch {}
|
|
115
|
+
try {
|
|
116
|
+
execSync(`rm ${logFile} 2>/dev/null`);
|
|
117
|
+
} catch {}
|
|
118
|
+
|
|
119
|
+
console.log(' Status: SUCCESS');
|
|
120
|
+
} catch (e) {
|
|
121
|
+
console.log(` Status: FAILED - ${e.message}`);
|
|
122
|
+
}
|
|
123
|
+
console.log('');
|
|
124
|
+
|
|
125
|
+
console.log('=== Tests Complete ===');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
testScreenModes();
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Experiment to understand screen output behavior
|
|
3
|
+
|
|
4
|
+
echo "=== Test 1: Direct screen with command ==="
|
|
5
|
+
screen -S test1 /bin/sh -c 'echo "hello from test1"'
|
|
6
|
+
|
|
7
|
+
echo ""
|
|
8
|
+
echo "=== Test 2: Screen with -L logging ==="
|
|
9
|
+
cd /tmp
|
|
10
|
+
screen -L -Logfile screen-test.log -S test2 /bin/sh -c 'echo "hello from test2"'
|
|
11
|
+
echo "Log contents:"
|
|
12
|
+
cat /tmp/screen-test.log 2>/dev/null || echo "No log file created"
|
|
13
|
+
|
|
14
|
+
echo ""
|
|
15
|
+
echo "=== Test 3: Screen detached then capture ==="
|
|
16
|
+
screen -dmS test3 /bin/sh -c 'echo "hello from test3" > /tmp/screen-test3-out.txt'
|
|
17
|
+
sleep 0.5
|
|
18
|
+
echo "Output from test3:"
|
|
19
|
+
cat /tmp/screen-test3-out.txt 2>/dev/null || echo "No output file"
|
|
20
|
+
|
|
21
|
+
echo ""
|
|
22
|
+
echo "=== Test 4: Screen with wrap using script command ==="
|
|
23
|
+
script -q /dev/null -c 'screen -S test4 /bin/sh -c "echo hello from test4"' 2>/dev/null || echo "Script method failed"
|
|
24
|
+
|
|
25
|
+
echo ""
|
|
26
|
+
echo "Cleanup"
|
|
27
|
+
rm -f /tmp/screen-test.log /tmp/screen-test3-out.txt
|
package/package.json
CHANGED
package/scripts/setup-npm.mjs
CHANGED
package/src/bin/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
const { spawn, execSync } = require('child_process');
|
|
4
4
|
const process = require('process');
|
|
@@ -13,7 +13,14 @@ const {
|
|
|
13
13
|
hasIsolation,
|
|
14
14
|
getEffectiveMode,
|
|
15
15
|
} = require('../lib/args-parser');
|
|
16
|
-
const {
|
|
16
|
+
const {
|
|
17
|
+
runIsolated,
|
|
18
|
+
getTimestamp,
|
|
19
|
+
createLogHeader,
|
|
20
|
+
createLogFooter,
|
|
21
|
+
writeLogFile,
|
|
22
|
+
createLogPath,
|
|
23
|
+
} = require('../lib/isolation');
|
|
17
24
|
|
|
18
25
|
// Configuration from environment variables
|
|
19
26
|
const config = {
|
|
@@ -55,7 +62,7 @@ function printUsage() {
|
|
|
55
62
|
console.log('');
|
|
56
63
|
console.log('Options:');
|
|
57
64
|
console.log(
|
|
58
|
-
' --isolated, -i <
|
|
65
|
+
' --isolated, -i <environment> Run in isolated environment (screen, tmux, docker, zellij)'
|
|
59
66
|
);
|
|
60
67
|
console.log(' --attached, -a Run in attached mode (foreground)');
|
|
61
68
|
console.log(' --detached, -d Run in detached mode (background)');
|
|
@@ -143,36 +150,70 @@ if (!config.disableSubstitutions) {
|
|
|
143
150
|
* @param {string} cmd - Command to execute
|
|
144
151
|
*/
|
|
145
152
|
async function runWithIsolation(options, cmd) {
|
|
146
|
-
const
|
|
153
|
+
const environment = options.isolated;
|
|
147
154
|
const mode = getEffectiveMode(options);
|
|
155
|
+
const startTime = getTimestamp();
|
|
156
|
+
|
|
157
|
+
// Create log file path
|
|
158
|
+
const logFilePath = createLogPath(environment);
|
|
159
|
+
|
|
160
|
+
// Get session name (will be generated by runIsolated if not provided)
|
|
161
|
+
const sessionName =
|
|
162
|
+
options.session ||
|
|
163
|
+
`${environment}-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
|
164
|
+
|
|
165
|
+
// Print start message (unified format)
|
|
166
|
+
console.log(`[${startTime}] Starting: ${cmd}`);
|
|
167
|
+
console.log('');
|
|
148
168
|
|
|
149
169
|
// Log isolation info
|
|
150
|
-
console.log(`[Isolation]
|
|
170
|
+
console.log(`[Isolation] Environment: ${environment}, Mode: ${mode}`);
|
|
151
171
|
if (options.session) {
|
|
152
172
|
console.log(`[Isolation] Session: ${options.session}`);
|
|
153
173
|
}
|
|
154
174
|
if (options.image) {
|
|
155
175
|
console.log(`[Isolation] Image: ${options.image}`);
|
|
156
176
|
}
|
|
157
|
-
console.log(`[Isolation] Command: ${cmd}`);
|
|
158
177
|
console.log('');
|
|
159
178
|
|
|
179
|
+
// Create log content
|
|
180
|
+
let logContent = createLogHeader({
|
|
181
|
+
command: cmd,
|
|
182
|
+
environment,
|
|
183
|
+
mode,
|
|
184
|
+
sessionName,
|
|
185
|
+
image: options.image,
|
|
186
|
+
startTime,
|
|
187
|
+
});
|
|
188
|
+
|
|
160
189
|
// Run in isolation
|
|
161
|
-
const result = await runIsolated(
|
|
190
|
+
const result = await runIsolated(environment, cmd, {
|
|
162
191
|
session: options.session,
|
|
163
192
|
image: options.image,
|
|
164
193
|
detached: mode === 'detached',
|
|
165
194
|
});
|
|
166
195
|
|
|
167
|
-
//
|
|
196
|
+
// Get exit code
|
|
197
|
+
const exitCode =
|
|
198
|
+
result.exitCode !== undefined ? result.exitCode : result.success ? 0 : 1;
|
|
199
|
+
const endTime = getTimestamp();
|
|
200
|
+
|
|
201
|
+
// Add result to log content
|
|
202
|
+
logContent += `${result.message}\n`;
|
|
203
|
+
logContent += createLogFooter(endTime, exitCode);
|
|
204
|
+
|
|
205
|
+
// Write log file
|
|
206
|
+
writeLogFile(logFilePath, logContent);
|
|
207
|
+
|
|
208
|
+
// Print result and footer (unified format)
|
|
168
209
|
console.log('');
|
|
169
210
|
console.log(result.message);
|
|
211
|
+
console.log('');
|
|
212
|
+
console.log(`[${endTime}] Finished`);
|
|
213
|
+
console.log(`Exit code: ${exitCode}`);
|
|
214
|
+
console.log(`Log saved: ${logFilePath}`);
|
|
170
215
|
|
|
171
|
-
|
|
172
|
-
process.exit(result.exitCode || 0);
|
|
173
|
-
} else {
|
|
174
|
-
process.exit(1);
|
|
175
|
-
}
|
|
216
|
+
process.exit(exitCode);
|
|
176
217
|
}
|
|
177
218
|
|
|
178
219
|
/**
|
|
@@ -301,12 +342,10 @@ function runDirect(cmd) {
|
|
|
301
342
|
});
|
|
302
343
|
}
|
|
303
344
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
// Generate unique log filename
|
|
345
|
+
/**
|
|
346
|
+
* Generate unique log filename for direct execution
|
|
347
|
+
* @returns {string} Log filename
|
|
348
|
+
*/
|
|
310
349
|
function generateLogFilename() {
|
|
311
350
|
const timestamp = Date.now();
|
|
312
351
|
const random = Math.random().toString(36).substring(2, 8);
|
package/src/lib/isolation.js
CHANGED
|
@@ -9,8 +9,13 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const { execSync, spawn } = require('child_process');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const path = require('path');
|
|
12
15
|
const { generateSessionName } = require('./args-parser');
|
|
13
16
|
|
|
17
|
+
const setTimeout = globalThis.setTimeout;
|
|
18
|
+
|
|
14
19
|
// Debug mode from environment
|
|
15
20
|
const DEBUG =
|
|
16
21
|
process.env.START_DEBUG === '1' || process.env.START_DEBUG === 'true';
|
|
@@ -42,6 +47,146 @@ function getShell() {
|
|
|
42
47
|
return { shell, shellArg };
|
|
43
48
|
}
|
|
44
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Check if the current process has a TTY attached
|
|
52
|
+
* @returns {boolean} True if TTY is available
|
|
53
|
+
*/
|
|
54
|
+
function hasTTY() {
|
|
55
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Run command in GNU Screen using detached mode with log capture
|
|
60
|
+
* This is a workaround for environments without TTY
|
|
61
|
+
* @param {string} command - Command to execute
|
|
62
|
+
* @param {string} sessionName - Session name
|
|
63
|
+
* @param {object} shellInfo - Shell info from getShell()
|
|
64
|
+
* @returns {Promise<{success: boolean, sessionName: string, message: string, output: string}>}
|
|
65
|
+
*/
|
|
66
|
+
function runScreenWithLogCapture(command, sessionName, shellInfo) {
|
|
67
|
+
const { shell, shellArg } = shellInfo;
|
|
68
|
+
const logFile = path.join(os.tmpdir(), `screen-output-${sessionName}.log`);
|
|
69
|
+
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
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
|
+
];
|
|
84
|
+
|
|
85
|
+
if (DEBUG) {
|
|
86
|
+
console.log(
|
|
87
|
+
`[DEBUG] Running screen with log capture: screen ${screenArgs.join(' ')}`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
execSync(`screen ${screenArgs.map((a) => `"${a}"`).join(' ')}`, {
|
|
92
|
+
stdio: 'inherit',
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Poll for session completion
|
|
96
|
+
const checkInterval = 100; // ms
|
|
97
|
+
const maxWait = 300000; // 5 minutes max
|
|
98
|
+
let waited = 0;
|
|
99
|
+
|
|
100
|
+
const checkCompletion = () => {
|
|
101
|
+
try {
|
|
102
|
+
// Check if session still exists
|
|
103
|
+
const sessions = execSync('screen -ls', {
|
|
104
|
+
encoding: 'utf8',
|
|
105
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (!sessions.includes(sessionName)) {
|
|
109
|
+
// Session ended, read output
|
|
110
|
+
let output = '';
|
|
111
|
+
try {
|
|
112
|
+
output = fs.readFileSync(logFile, 'utf8');
|
|
113
|
+
// Display the output
|
|
114
|
+
if (output.trim()) {
|
|
115
|
+
process.stdout.write(output);
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
// Log file might not exist if command was very quick
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Clean up log file
|
|
122
|
+
try {
|
|
123
|
+
fs.unlinkSync(logFile);
|
|
124
|
+
} catch {
|
|
125
|
+
// Ignore cleanup errors
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
resolve({
|
|
129
|
+
success: true,
|
|
130
|
+
sessionName,
|
|
131
|
+
message: `Screen session "${sessionName}" exited with code 0`,
|
|
132
|
+
exitCode: 0,
|
|
133
|
+
output,
|
|
134
|
+
});
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
waited += checkInterval;
|
|
139
|
+
if (waited >= maxWait) {
|
|
140
|
+
resolve({
|
|
141
|
+
success: false,
|
|
142
|
+
sessionName,
|
|
143
|
+
message: `Screen session "${sessionName}" timed out after ${maxWait / 1000} seconds`,
|
|
144
|
+
exitCode: 1,
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
setTimeout(checkCompletion, checkInterval);
|
|
150
|
+
} catch {
|
|
151
|
+
// screen -ls failed, session probably ended
|
|
152
|
+
let output = '';
|
|
153
|
+
try {
|
|
154
|
+
output = fs.readFileSync(logFile, 'utf8');
|
|
155
|
+
if (output.trim()) {
|
|
156
|
+
process.stdout.write(output);
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
// Ignore
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
fs.unlinkSync(logFile);
|
|
164
|
+
} catch {
|
|
165
|
+
// Ignore
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
resolve({
|
|
169
|
+
success: true,
|
|
170
|
+
sessionName,
|
|
171
|
+
message: `Screen session "${sessionName}" exited with code 0`,
|
|
172
|
+
exitCode: 0,
|
|
173
|
+
output,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Start checking after a brief delay
|
|
179
|
+
setTimeout(checkCompletion, checkInterval);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
resolve({
|
|
182
|
+
success: false,
|
|
183
|
+
sessionName,
|
|
184
|
+
message: `Failed to run in screen: ${err.message}`,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
45
190
|
/**
|
|
46
191
|
* Run command in GNU Screen
|
|
47
192
|
* @param {string} command - Command to execute
|
|
@@ -59,7 +204,8 @@ function runInScreen(command, options = {}) {
|
|
|
59
204
|
}
|
|
60
205
|
|
|
61
206
|
const sessionName = options.session || generateSessionName('screen');
|
|
62
|
-
const
|
|
207
|
+
const shellInfo = getShell();
|
|
208
|
+
const { shell, shellArg } = shellInfo;
|
|
63
209
|
|
|
64
210
|
try {
|
|
65
211
|
if (options.detached) {
|
|
@@ -80,35 +226,50 @@ function runInScreen(command, options = {}) {
|
|
|
80
226
|
message: `Command started in detached screen session: ${sessionName}\nReattach with: screen -r ${sessionName}`,
|
|
81
227
|
});
|
|
82
228
|
} else {
|
|
83
|
-
// Attached mode:
|
|
84
|
-
const screenArgs = ['-S', sessionName, shell, shellArg, command];
|
|
229
|
+
// Attached mode: need TTY for screen to work properly
|
|
85
230
|
|
|
86
|
-
if
|
|
87
|
-
|
|
88
|
-
|
|
231
|
+
// Check if we have a TTY
|
|
232
|
+
if (hasTTY()) {
|
|
233
|
+
// We have a TTY, use direct screen invocation
|
|
234
|
+
const screenArgs = ['-S', sessionName, shell, shellArg, command];
|
|
89
235
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
});
|
|
236
|
+
if (DEBUG) {
|
|
237
|
+
console.log(`[DEBUG] Running: screen ${screenArgs.join(' ')}`);
|
|
238
|
+
}
|
|
94
239
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
sessionName,
|
|
99
|
-
message: `Screen session "${sessionName}" exited with code ${code}`,
|
|
100
|
-
exitCode: code,
|
|
240
|
+
return new Promise((resolve) => {
|
|
241
|
+
const child = spawn('screen', screenArgs, {
|
|
242
|
+
stdio: 'inherit',
|
|
101
243
|
});
|
|
102
|
-
});
|
|
103
244
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
245
|
+
child.on('exit', (code) => {
|
|
246
|
+
resolve({
|
|
247
|
+
success: code === 0,
|
|
248
|
+
sessionName,
|
|
249
|
+
message: `Screen session "${sessionName}" exited with code ${code}`,
|
|
250
|
+
exitCode: code,
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
child.on('error', (err) => {
|
|
255
|
+
resolve({
|
|
256
|
+
success: false,
|
|
257
|
+
sessionName,
|
|
258
|
+
message: `Failed to start screen: ${err.message}`,
|
|
259
|
+
});
|
|
109
260
|
});
|
|
110
261
|
});
|
|
111
|
-
}
|
|
262
|
+
} else {
|
|
263
|
+
// No TTY available - use detached mode with log capture
|
|
264
|
+
// This allows screen to run without a terminal while still capturing output
|
|
265
|
+
if (DEBUG) {
|
|
266
|
+
console.log(
|
|
267
|
+
`[DEBUG] No TTY available, using detached mode with log capture`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return runScreenWithLogCapture(command, sessionName, shellInfo);
|
|
272
|
+
}
|
|
112
273
|
}
|
|
113
274
|
} catch (err) {
|
|
114
275
|
return Promise.resolve({
|
|
@@ -409,11 +570,115 @@ function runIsolated(backend, command, options = {}) {
|
|
|
409
570
|
}
|
|
410
571
|
}
|
|
411
572
|
|
|
573
|
+
/**
|
|
574
|
+
* Generate timestamp for logging
|
|
575
|
+
* @returns {string} ISO timestamp without 'T' and 'Z'
|
|
576
|
+
*/
|
|
577
|
+
function getTimestamp() {
|
|
578
|
+
return new Date().toISOString().replace('T', ' ').replace('Z', '');
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Generate unique log filename
|
|
583
|
+
* @param {string} environment - The isolation environment name
|
|
584
|
+
* @returns {string} Log filename
|
|
585
|
+
*/
|
|
586
|
+
function generateLogFilename(environment) {
|
|
587
|
+
const timestamp = Date.now();
|
|
588
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
589
|
+
return `start-command-${environment}-${timestamp}-${random}.log`;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Create log content header
|
|
594
|
+
* @param {object} params - Log parameters
|
|
595
|
+
* @param {string} params.command - The command being executed
|
|
596
|
+
* @param {string} params.environment - The isolation environment
|
|
597
|
+
* @param {string} params.mode - attached or detached
|
|
598
|
+
* @param {string} params.sessionName - Session/container name
|
|
599
|
+
* @param {string} [params.image] - Docker image (for docker environment)
|
|
600
|
+
* @param {string} params.startTime - Start timestamp
|
|
601
|
+
* @returns {string} Log header content
|
|
602
|
+
*/
|
|
603
|
+
function createLogHeader(params) {
|
|
604
|
+
let content = `=== Start Command Log ===\n`;
|
|
605
|
+
content += `Timestamp: ${params.startTime}\n`;
|
|
606
|
+
content += `Command: ${params.command}\n`;
|
|
607
|
+
content += `Environment: ${params.environment}\n`;
|
|
608
|
+
content += `Mode: ${params.mode}\n`;
|
|
609
|
+
content += `Session: ${params.sessionName}\n`;
|
|
610
|
+
if (params.image) {
|
|
611
|
+
content += `Image: ${params.image}\n`;
|
|
612
|
+
}
|
|
613
|
+
content += `Platform: ${process.platform}\n`;
|
|
614
|
+
content += `Node Version: ${process.version}\n`;
|
|
615
|
+
content += `Working Directory: ${process.cwd()}\n`;
|
|
616
|
+
content += `${'='.repeat(50)}\n\n`;
|
|
617
|
+
return content;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Create log content footer
|
|
622
|
+
* @param {string} endTime - End timestamp
|
|
623
|
+
* @param {number} exitCode - Exit code
|
|
624
|
+
* @returns {string} Log footer content
|
|
625
|
+
*/
|
|
626
|
+
function createLogFooter(endTime, exitCode) {
|
|
627
|
+
let content = `\n${'='.repeat(50)}\n`;
|
|
628
|
+
content += `Finished: ${endTime}\n`;
|
|
629
|
+
content += `Exit Code: ${exitCode}\n`;
|
|
630
|
+
return content;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Write log file
|
|
635
|
+
* @param {string} logPath - Path to log file
|
|
636
|
+
* @param {string} content - Log content
|
|
637
|
+
* @returns {boolean} Success status
|
|
638
|
+
*/
|
|
639
|
+
function writeLogFile(logPath, content) {
|
|
640
|
+
try {
|
|
641
|
+
fs.writeFileSync(logPath, content, 'utf8');
|
|
642
|
+
return true;
|
|
643
|
+
} catch (err) {
|
|
644
|
+
console.error(`\nWarning: Could not save log file: ${err.message}`);
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Get log directory from environment or use system temp
|
|
651
|
+
* @returns {string} Log directory path
|
|
652
|
+
*/
|
|
653
|
+
function getLogDir() {
|
|
654
|
+
return process.env.START_LOG_DIR || os.tmpdir();
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Create log file path
|
|
659
|
+
* @param {string} environment - The isolation environment
|
|
660
|
+
* @returns {string} Full path to log file
|
|
661
|
+
*/
|
|
662
|
+
function createLogPath(environment) {
|
|
663
|
+
const logDir = getLogDir();
|
|
664
|
+
const logFilename = generateLogFilename(environment);
|
|
665
|
+
return path.join(logDir, logFilename);
|
|
666
|
+
}
|
|
667
|
+
|
|
412
668
|
module.exports = {
|
|
413
669
|
isCommandAvailable,
|
|
670
|
+
hasTTY,
|
|
414
671
|
runInScreen,
|
|
415
672
|
runInTmux,
|
|
416
673
|
runInZellij,
|
|
417
674
|
runInDocker,
|
|
418
675
|
runIsolated,
|
|
676
|
+
// Export logging utilities for unified experience
|
|
677
|
+
getTimestamp,
|
|
678
|
+
generateLogFilename,
|
|
679
|
+
createLogHeader,
|
|
680
|
+
createLogFooter,
|
|
681
|
+
writeLogFile,
|
|
682
|
+
getLogDir,
|
|
683
|
+
createLogPath,
|
|
419
684
|
};
|
package/test/args-parser.test.js
CHANGED
package/test/isolation.test.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
3
|
* Unit tests for the isolation module
|
|
4
4
|
* Tests command availability checking and session name generation
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
const { describe, it } = require('node:test');
|
|
9
9
|
const assert = require('assert');
|
|
10
|
-
const { isCommandAvailable } = require('../src/lib/isolation');
|
|
10
|
+
const { isCommandAvailable, hasTTY } = require('../src/lib/isolation');
|
|
11
11
|
|
|
12
12
|
describe('Isolation Module', () => {
|
|
13
13
|
describe('isCommandAvailable', () => {
|
|
@@ -34,6 +34,21 @@ describe('Isolation Module', () => {
|
|
|
34
34
|
});
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
+
describe('hasTTY', () => {
|
|
38
|
+
it('should return a boolean', () => {
|
|
39
|
+
const result = hasTTY();
|
|
40
|
+
assert.strictEqual(typeof result, 'boolean');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should return false when running in test environment (no TTY)', () => {
|
|
44
|
+
// When running tests, we typically don't have a TTY
|
|
45
|
+
const result = hasTTY();
|
|
46
|
+
// This should be false in CI/test environments
|
|
47
|
+
console.log(` hasTTY: ${result}`);
|
|
48
|
+
assert.ok(typeof result === 'boolean');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
37
52
|
describe('isolation backend checks', () => {
|
|
38
53
|
// These tests check if specific backends are available
|
|
39
54
|
// They don't fail if not installed, just report status
|
|
@@ -197,6 +212,56 @@ describe('Isolation Runner with Available Backends', () => {
|
|
|
197
212
|
// Session may have already exited
|
|
198
213
|
}
|
|
199
214
|
});
|
|
215
|
+
|
|
216
|
+
it('should run command in attached mode and capture output (issue #15)', async () => {
|
|
217
|
+
if (!isCommandAvailable('screen')) {
|
|
218
|
+
console.log(' Skipping: screen not installed');
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Test attached mode - this should work without TTY using log capture fallback
|
|
223
|
+
const result = await runInScreen('echo hello', {
|
|
224
|
+
session: `test-attached-${Date.now()}`,
|
|
225
|
+
detached: false,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
assert.strictEqual(result.success, true);
|
|
229
|
+
assert.ok(result.sessionName);
|
|
230
|
+
assert.ok(result.message.includes('exited with code 0'));
|
|
231
|
+
// The output property should exist when using log capture
|
|
232
|
+
if (result.output !== undefined) {
|
|
233
|
+
console.log(` Captured output: "${result.output.trim()}"`);
|
|
234
|
+
assert.ok(
|
|
235
|
+
result.output.includes('hello'),
|
|
236
|
+
'Output should contain the expected message'
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should handle multi-line output in attached mode', async () => {
|
|
242
|
+
if (!isCommandAvailable('screen')) {
|
|
243
|
+
console.log(' Skipping: screen not installed');
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const result = await runInScreen(
|
|
248
|
+
"echo 'line1'; echo 'line2'; echo 'line3'",
|
|
249
|
+
{
|
|
250
|
+
session: `test-multiline-${Date.now()}`,
|
|
251
|
+
detached: false,
|
|
252
|
+
}
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
assert.strictEqual(result.success, true);
|
|
256
|
+
if (result.output !== undefined) {
|
|
257
|
+
console.log(
|
|
258
|
+
` Captured multi-line output: "${result.output.trim().replace(/\n/g, '\\n')}"`
|
|
259
|
+
);
|
|
260
|
+
assert.ok(result.output.includes('line1'));
|
|
261
|
+
assert.ok(result.output.includes('line2'));
|
|
262
|
+
assert.ok(result.output.includes('line3'));
|
|
263
|
+
}
|
|
264
|
+
});
|
|
200
265
|
});
|
|
201
266
|
|
|
202
267
|
describe('runInTmux (if available)', () => {
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
'start-command': minor
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
Add process isolation support with --isolated option
|
|
6
|
-
|
|
7
|
-
This release adds the ability to run commands in isolated environments:
|
|
8
|
-
|
|
9
|
-
**New Features:**
|
|
10
|
-
|
|
11
|
-
- `--isolated` / `-i` option to run commands in screen, tmux, zellij, or docker
|
|
12
|
-
- `--attached` / `-a` and `--detached` / `-d` modes for foreground/background execution
|
|
13
|
-
- `--session` / `-s` option for custom session names
|
|
14
|
-
- `--image` option for Docker container image specification
|
|
15
|
-
- Two command syntax patterns: `$ [options] -- [command]` or `$ [options] command`
|
|
16
|
-
|
|
17
|
-
**Supported Backends:**
|
|
18
|
-
|
|
19
|
-
- GNU Screen - classic terminal multiplexer
|
|
20
|
-
- tmux - modern terminal multiplexer
|
|
21
|
-
- zellij - modern terminal workspace
|
|
22
|
-
- Docker - container isolation
|
|
23
|
-
|
|
24
|
-
**Examples:**
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
$ --isolated tmux -- npm start
|
|
28
|
-
$ -i screen -d npm start
|
|
29
|
-
$ --isolated docker --image node:20 -- npm install
|
|
30
|
-
```
|