panex 0.9.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/LICENSE +21 -0
- package/README.md +113 -0
- package/dist/chunk-Z7LPKPEA.js +372 -0
- package/dist/chunk-Z7LPKPEA.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +23 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Anton Veretennikov (king8fisher)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# panex
|
|
2
|
+
|
|
3
|
+
A terminal UI for running multiple processes in parallel. Like Turborepo's TUI, without the monorepo.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
┌──────────────┬──────────────────────────────────────────────┐
|
|
7
|
+
│ PROCESSES │ OUTPUT: api │
|
|
8
|
+
│ │ │
|
|
9
|
+
│ ▶ api ● │ Server listening on http://localhost:3001 │
|
|
10
|
+
│ web ● │ {"level":30,"msg":"request completed"} │
|
|
11
|
+
│ mobile ● │ │
|
|
12
|
+
├──────────────┴──────────────────────────────────────────────┤
|
|
13
|
+
│ [↑↓] select [enter] focus [r] restart [q] quit │
|
|
14
|
+
└─────────────────────────────────────────────────────────────┘
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- **Split-pane TUI** - See all your processes at once
|
|
20
|
+
- **Full PTY support** - QR codes, colors, interactive prompts work
|
|
21
|
+
- **Zero config** - Just pass commands as arguments
|
|
22
|
+
- **Cross-platform** - macOS, Linux, Windows
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# npm
|
|
28
|
+
npm install -g panex
|
|
29
|
+
|
|
30
|
+
# or run directly
|
|
31
|
+
npx panex "npm run api" "npm run web"
|
|
32
|
+
|
|
33
|
+
# bun
|
|
34
|
+
bunx panex "bun run api" "bun run web"
|
|
35
|
+
|
|
36
|
+
# pnpm
|
|
37
|
+
pnpm add -g panex
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
### Quick Start
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Run multiple commands
|
|
46
|
+
panex "npm run api" "npm run web" "npm run mobile"
|
|
47
|
+
|
|
48
|
+
# With custom names
|
|
49
|
+
panex -n api,web,mobile "npm run api" "npm run web" "npm run mobile"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Keyboard Shortcuts
|
|
53
|
+
|
|
54
|
+
| Key | Action |
|
|
55
|
+
|-----|--------|
|
|
56
|
+
| `↑/↓` or `j/k` | Navigate process list |
|
|
57
|
+
| `Enter` | Focus process (interactive mode) |
|
|
58
|
+
| `Esc` | Exit focus mode |
|
|
59
|
+
| `r` | Restart selected process |
|
|
60
|
+
| `x` | Kill selected process |
|
|
61
|
+
| `a` | Restart all processes |
|
|
62
|
+
| `q` | Quit panex |
|
|
63
|
+
| `?` | Show help |
|
|
64
|
+
| `g/G` | Scroll to top/bottom |
|
|
65
|
+
| `PgUp/PgDn` | Scroll output |
|
|
66
|
+
|
|
67
|
+
## Why panex?
|
|
68
|
+
|
|
69
|
+
| Feature | panex | concurrently | mprocs | turbo |
|
|
70
|
+
|---------|-------|--------------|--------|-------|
|
|
71
|
+
| Split-pane TUI | ✅ | ❌ | ✅ | ✅ |
|
|
72
|
+
| PTY support (QR codes) | ✅ | ❌ | ✅ | ✅ |
|
|
73
|
+
| Zero config | ✅ | ✅ | ❌ | ❌ |
|
|
74
|
+
| npm install | ✅ | ✅ | ❌ | ✅ |
|
|
75
|
+
| No monorepo required | ✅ | ✅ | ✅ | ❌ |
|
|
76
|
+
|
|
77
|
+
## Development
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Clone
|
|
81
|
+
git clone https://github.com/king8fisher/panex
|
|
82
|
+
cd panex
|
|
83
|
+
|
|
84
|
+
# Install dependencies
|
|
85
|
+
bun install
|
|
86
|
+
|
|
87
|
+
# Run in dev mode
|
|
88
|
+
bun run dev "echo hello" "sleep 2 && echo world"
|
|
89
|
+
|
|
90
|
+
# Type check
|
|
91
|
+
bun run typecheck
|
|
92
|
+
|
|
93
|
+
# Build for npm
|
|
94
|
+
bun run build
|
|
95
|
+
|
|
96
|
+
# Run tests
|
|
97
|
+
bun test
|
|
98
|
+
|
|
99
|
+
# Test built CLI
|
|
100
|
+
node dist/cli.js "echo test"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Tech Stack
|
|
104
|
+
|
|
105
|
+
- TypeScript + Bun
|
|
106
|
+
- blessed (TUI framework)
|
|
107
|
+
- node-pty (PTY support for interactive processes)
|
|
108
|
+
- commander (CLI parsing)
|
|
109
|
+
- tsup (build tool)
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
MIT
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/process-manager.ts
|
|
4
|
+
import * as pty from "node-pty";
|
|
5
|
+
import { EventEmitter } from "events";
|
|
6
|
+
var ProcessManager = class extends EventEmitter {
|
|
7
|
+
constructor(procs) {
|
|
8
|
+
super();
|
|
9
|
+
this.procs = procs;
|
|
10
|
+
}
|
|
11
|
+
processes = /* @__PURE__ */ new Map();
|
|
12
|
+
maxOutputLines = 1e4;
|
|
13
|
+
async startAll() {
|
|
14
|
+
for (const [name, config] of Object.entries(this.procs)) {
|
|
15
|
+
await this.start(name, config);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async start(name, config) {
|
|
19
|
+
const existing = this.processes.get(name);
|
|
20
|
+
if (existing?.pty) {
|
|
21
|
+
existing.pty.kill();
|
|
22
|
+
}
|
|
23
|
+
const shell = process.platform === "win32" ? "powershell.exe" : "bash";
|
|
24
|
+
const args = config.shell ? ["-c", config.shell] : config.cmd ? ["-c", config.cmd.join(" ")] : [];
|
|
25
|
+
const cwd = config.cwd ?? process.cwd();
|
|
26
|
+
const env = { ...process.env, ...config.env };
|
|
27
|
+
try {
|
|
28
|
+
const ptyProcess = pty.spawn(shell, args, {
|
|
29
|
+
name: "xterm-256color",
|
|
30
|
+
cols: 120,
|
|
31
|
+
rows: 30,
|
|
32
|
+
cwd,
|
|
33
|
+
env
|
|
34
|
+
});
|
|
35
|
+
const managed = {
|
|
36
|
+
name,
|
|
37
|
+
config,
|
|
38
|
+
pty: ptyProcess,
|
|
39
|
+
status: "running",
|
|
40
|
+
output: [],
|
|
41
|
+
exitCode: null
|
|
42
|
+
};
|
|
43
|
+
this.processes.set(name, managed);
|
|
44
|
+
ptyProcess.onData((data) => {
|
|
45
|
+
managed.output.push(data);
|
|
46
|
+
if (managed.output.length > this.maxOutputLines) {
|
|
47
|
+
managed.output = managed.output.slice(-this.maxOutputLines);
|
|
48
|
+
}
|
|
49
|
+
this.emit("output", name, data);
|
|
50
|
+
});
|
|
51
|
+
ptyProcess.onExit(({ exitCode }) => {
|
|
52
|
+
managed.status = exitCode === 0 ? "stopped" : "error";
|
|
53
|
+
managed.exitCode = exitCode;
|
|
54
|
+
managed.pty = null;
|
|
55
|
+
this.emit("exit", name, exitCode);
|
|
56
|
+
if (config.autoRestart && exitCode !== 0) {
|
|
57
|
+
setTimeout(() => {
|
|
58
|
+
this.start(name, config);
|
|
59
|
+
}, 1e3);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
this.emit("started", name);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
const managed = {
|
|
65
|
+
name,
|
|
66
|
+
config,
|
|
67
|
+
pty: null,
|
|
68
|
+
status: "error",
|
|
69
|
+
output: [`Error starting process: ${error}`],
|
|
70
|
+
exitCode: -1
|
|
71
|
+
};
|
|
72
|
+
this.processes.set(name, managed);
|
|
73
|
+
this.emit("error", name, error);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
restart(name) {
|
|
77
|
+
const proc = this.processes.get(name);
|
|
78
|
+
if (proc) {
|
|
79
|
+
if (proc.pty) {
|
|
80
|
+
proc.pty.kill();
|
|
81
|
+
}
|
|
82
|
+
proc.output = [];
|
|
83
|
+
this.start(name, proc.config);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
restartAll() {
|
|
87
|
+
for (const name of this.processes.keys()) {
|
|
88
|
+
this.restart(name);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
kill(name) {
|
|
92
|
+
const proc = this.processes.get(name);
|
|
93
|
+
if (proc?.pty) {
|
|
94
|
+
proc.pty.kill();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
killAll() {
|
|
98
|
+
for (const proc of this.processes.values()) {
|
|
99
|
+
if (proc.pty) {
|
|
100
|
+
proc.pty.kill();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
write(name, data) {
|
|
105
|
+
const proc = this.processes.get(name);
|
|
106
|
+
if (proc?.pty) {
|
|
107
|
+
proc.pty.write(data);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
resize(name, cols, rows) {
|
|
111
|
+
const proc = this.processes.get(name);
|
|
112
|
+
if (proc?.pty) {
|
|
113
|
+
proc.pty.resize(cols, rows);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
getProcess(name) {
|
|
117
|
+
return this.processes.get(name);
|
|
118
|
+
}
|
|
119
|
+
getProcesses() {
|
|
120
|
+
return Array.from(this.processes.values());
|
|
121
|
+
}
|
|
122
|
+
getNames() {
|
|
123
|
+
return Array.from(this.processes.keys());
|
|
124
|
+
}
|
|
125
|
+
getOutput(name) {
|
|
126
|
+
return this.processes.get(name)?.output.join("") ?? "";
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// src/tui.ts
|
|
131
|
+
import blessed from "blessed";
|
|
132
|
+
async function createTUI(config) {
|
|
133
|
+
const processManager = new ProcessManager(config.procs);
|
|
134
|
+
const screen = blessed.screen({
|
|
135
|
+
smartCSR: true,
|
|
136
|
+
title: "panex",
|
|
137
|
+
fullUnicode: true
|
|
138
|
+
});
|
|
139
|
+
const processList = blessed.list({
|
|
140
|
+
parent: screen,
|
|
141
|
+
label: " PROCESSES ",
|
|
142
|
+
top: 0,
|
|
143
|
+
left: 0,
|
|
144
|
+
width: "20%",
|
|
145
|
+
height: "100%-1",
|
|
146
|
+
border: { type: "line" },
|
|
147
|
+
style: {
|
|
148
|
+
border: { fg: "blue" },
|
|
149
|
+
selected: { bg: "blue", fg: "white" },
|
|
150
|
+
item: { fg: "white" }
|
|
151
|
+
},
|
|
152
|
+
keys: true,
|
|
153
|
+
vi: true,
|
|
154
|
+
mouse: config.settings?.mouse ?? true,
|
|
155
|
+
scrollbar: {
|
|
156
|
+
ch: "\u2502",
|
|
157
|
+
style: { bg: "blue" }
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
const outputBox = blessed.box({
|
|
161
|
+
parent: screen,
|
|
162
|
+
label: " OUTPUT ",
|
|
163
|
+
top: 0,
|
|
164
|
+
left: "20%",
|
|
165
|
+
width: "80%",
|
|
166
|
+
height: "100%-1",
|
|
167
|
+
border: { type: "line" },
|
|
168
|
+
style: {
|
|
169
|
+
border: { fg: "green" }
|
|
170
|
+
},
|
|
171
|
+
scrollable: true,
|
|
172
|
+
alwaysScroll: true,
|
|
173
|
+
scrollbar: {
|
|
174
|
+
ch: "\u2502",
|
|
175
|
+
style: { bg: "green" }
|
|
176
|
+
},
|
|
177
|
+
mouse: config.settings?.mouse ?? true,
|
|
178
|
+
keys: true,
|
|
179
|
+
vi: true
|
|
180
|
+
});
|
|
181
|
+
const statusBar = blessed.box({
|
|
182
|
+
parent: screen,
|
|
183
|
+
bottom: 0,
|
|
184
|
+
left: 0,
|
|
185
|
+
width: "100%",
|
|
186
|
+
height: 1,
|
|
187
|
+
style: {
|
|
188
|
+
bg: "blue",
|
|
189
|
+
fg: "white"
|
|
190
|
+
},
|
|
191
|
+
content: " [\u2191\u2193/jk] select [Enter] focus [r] restart [a] restart all [x] kill [q] quit [?] help "
|
|
192
|
+
});
|
|
193
|
+
const helpBox = blessed.box({
|
|
194
|
+
parent: screen,
|
|
195
|
+
top: "center",
|
|
196
|
+
left: "center",
|
|
197
|
+
width: "60%",
|
|
198
|
+
height: "60%",
|
|
199
|
+
label: " Help ",
|
|
200
|
+
border: { type: "line" },
|
|
201
|
+
style: {
|
|
202
|
+
border: { fg: "yellow" },
|
|
203
|
+
bg: "black"
|
|
204
|
+
},
|
|
205
|
+
hidden: true,
|
|
206
|
+
content: `
|
|
207
|
+
Keyboard Shortcuts
|
|
208
|
+
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
209
|
+
|
|
210
|
+
Navigation
|
|
211
|
+
\u2191/\u2193 or j/k Navigate process list
|
|
212
|
+
g/G Scroll to top/bottom of output
|
|
213
|
+
PgUp/PgDn Scroll output
|
|
214
|
+
|
|
215
|
+
Process Control
|
|
216
|
+
Enter Focus process (interactive mode)
|
|
217
|
+
Esc Exit focus mode
|
|
218
|
+
r Restart selected process
|
|
219
|
+
a Restart all processes
|
|
220
|
+
x Kill selected process
|
|
221
|
+
|
|
222
|
+
General
|
|
223
|
+
? Toggle this help
|
|
224
|
+
q Quit panex
|
|
225
|
+
|
|
226
|
+
Press any key to close this help...
|
|
227
|
+
`
|
|
228
|
+
});
|
|
229
|
+
let selectedIndex = 0;
|
|
230
|
+
let focusMode = false;
|
|
231
|
+
const processNames = Object.keys(config.procs);
|
|
232
|
+
function updateProcessList() {
|
|
233
|
+
const items = processNames.map((name, i) => {
|
|
234
|
+
const proc = processManager.getProcess(name);
|
|
235
|
+
const status = proc?.status === "running" ? "\u25CF" : proc?.status === "error" ? "\u2717" : "\u25CB";
|
|
236
|
+
const prefix = i === selectedIndex ? "\u25B6" : " ";
|
|
237
|
+
return `${prefix} ${name} ${status}`;
|
|
238
|
+
});
|
|
239
|
+
processList.setItems(items);
|
|
240
|
+
processList.select(selectedIndex);
|
|
241
|
+
screen.render();
|
|
242
|
+
}
|
|
243
|
+
function updateOutput() {
|
|
244
|
+
const name = processNames[selectedIndex];
|
|
245
|
+
if (name) {
|
|
246
|
+
outputBox.setLabel(` OUTPUT: ${name} `);
|
|
247
|
+
const output = processManager.getOutput(name);
|
|
248
|
+
outputBox.setContent(output);
|
|
249
|
+
outputBox.setScrollPerc(100);
|
|
250
|
+
}
|
|
251
|
+
screen.render();
|
|
252
|
+
}
|
|
253
|
+
processManager.on("output", (name) => {
|
|
254
|
+
if (name === processNames[selectedIndex]) {
|
|
255
|
+
updateOutput();
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
processManager.on("started", () => {
|
|
259
|
+
updateProcessList();
|
|
260
|
+
});
|
|
261
|
+
processManager.on("exit", () => {
|
|
262
|
+
updateProcessList();
|
|
263
|
+
});
|
|
264
|
+
screen.key(["q", "C-c"], () => {
|
|
265
|
+
processManager.killAll();
|
|
266
|
+
process.exit(0);
|
|
267
|
+
});
|
|
268
|
+
screen.key(["?"], () => {
|
|
269
|
+
helpBox.toggle();
|
|
270
|
+
screen.render();
|
|
271
|
+
});
|
|
272
|
+
screen.key(["escape"], () => {
|
|
273
|
+
if (!helpBox.hidden) {
|
|
274
|
+
helpBox.hide();
|
|
275
|
+
screen.render();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (focusMode) {
|
|
279
|
+
focusMode = false;
|
|
280
|
+
statusBar.setContent(" [\u2191\u2193/jk] select [Enter] focus [r] restart [a] restart all [x] kill [q] quit [?] help ");
|
|
281
|
+
screen.render();
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
helpBox.key(["escape", "q", "?", "enter", "space"], () => {
|
|
285
|
+
helpBox.hide();
|
|
286
|
+
screen.render();
|
|
287
|
+
});
|
|
288
|
+
screen.key(["up", "k"], () => {
|
|
289
|
+
if (focusMode || !helpBox.hidden) return;
|
|
290
|
+
selectedIndex = Math.max(0, selectedIndex - 1);
|
|
291
|
+
updateProcessList();
|
|
292
|
+
updateOutput();
|
|
293
|
+
});
|
|
294
|
+
screen.key(["down", "j"], () => {
|
|
295
|
+
if (focusMode || !helpBox.hidden) return;
|
|
296
|
+
selectedIndex = Math.min(processNames.length - 1, selectedIndex + 1);
|
|
297
|
+
updateProcessList();
|
|
298
|
+
updateOutput();
|
|
299
|
+
});
|
|
300
|
+
screen.key(["enter"], () => {
|
|
301
|
+
if (!helpBox.hidden) {
|
|
302
|
+
helpBox.hide();
|
|
303
|
+
screen.render();
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
focusMode = !focusMode;
|
|
307
|
+
const name = processNames[selectedIndex];
|
|
308
|
+
if (focusMode && name) {
|
|
309
|
+
statusBar.setContent(` FOCUS: ${name} - Type to interact, [Esc] to exit focus mode `);
|
|
310
|
+
} else {
|
|
311
|
+
statusBar.setContent(" [\u2191\u2193/jk] select [Enter] focus [r] restart [a] restart all [x] kill [q] quit [?] help ");
|
|
312
|
+
}
|
|
313
|
+
screen.render();
|
|
314
|
+
});
|
|
315
|
+
screen.key(["r"], () => {
|
|
316
|
+
if (focusMode || !helpBox.hidden) return;
|
|
317
|
+
const name = processNames[selectedIndex];
|
|
318
|
+
if (name) {
|
|
319
|
+
processManager.restart(name);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
screen.key(["a"], () => {
|
|
323
|
+
if (focusMode || !helpBox.hidden) return;
|
|
324
|
+
processManager.restartAll();
|
|
325
|
+
});
|
|
326
|
+
screen.key(["x"], () => {
|
|
327
|
+
if (focusMode || !helpBox.hidden) return;
|
|
328
|
+
const name = processNames[selectedIndex];
|
|
329
|
+
if (name) {
|
|
330
|
+
processManager.kill(name);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
screen.key(["g"], () => {
|
|
334
|
+
if (focusMode || !helpBox.hidden) return;
|
|
335
|
+
outputBox.setScrollPerc(0);
|
|
336
|
+
screen.render();
|
|
337
|
+
});
|
|
338
|
+
screen.key(["S-g"], () => {
|
|
339
|
+
if (focusMode || !helpBox.hidden) return;
|
|
340
|
+
outputBox.setScrollPerc(100);
|
|
341
|
+
screen.render();
|
|
342
|
+
});
|
|
343
|
+
screen.on("keypress", (ch, key) => {
|
|
344
|
+
if (focusMode && ch) {
|
|
345
|
+
const name = processNames[selectedIndex];
|
|
346
|
+
if (name) {
|
|
347
|
+
processManager.write(name, ch);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
screen.on("resize", () => {
|
|
352
|
+
const name = processNames[selectedIndex];
|
|
353
|
+
if (name) {
|
|
354
|
+
const cols = Math.floor(screen.width * 0.8) - 2;
|
|
355
|
+
const rows = screen.height - 3;
|
|
356
|
+
processManager.resize(name, cols, rows);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
updateProcessList();
|
|
360
|
+
updateOutput();
|
|
361
|
+
processList.focus();
|
|
362
|
+
await processManager.startAll();
|
|
363
|
+
updateProcessList();
|
|
364
|
+
updateOutput();
|
|
365
|
+
screen.render();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export {
|
|
369
|
+
ProcessManager,
|
|
370
|
+
createTUI
|
|
371
|
+
};
|
|
372
|
+
//# sourceMappingURL=chunk-Z7LPKPEA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/process-manager.ts","../src/tui.ts"],"sourcesContent":["import * as pty from 'node-pty';\nimport { EventEmitter } from 'events';\nimport type { ProcessConfig } from './types';\n\nexport interface ManagedProcess {\n name: string;\n config: ProcessConfig;\n pty: pty.IPty | null;\n status: 'running' | 'stopped' | 'error';\n output: string[];\n exitCode: number | null;\n}\n\nexport class ProcessManager extends EventEmitter {\n private processes: Map<string, ManagedProcess> = new Map();\n private maxOutputLines = 10000;\n\n constructor(private procs: Record<string, ProcessConfig>) {\n super();\n }\n\n async startAll(): Promise<void> {\n for (const [name, config] of Object.entries(this.procs)) {\n await this.start(name, config);\n }\n }\n\n async start(name: string, config: ProcessConfig): Promise<void> {\n const existing = this.processes.get(name);\n if (existing?.pty) {\n existing.pty.kill();\n }\n\n const shell = process.platform === 'win32' ? 'powershell.exe' : 'bash';\n const args = config.shell\n ? ['-c', config.shell]\n : config.cmd\n ? ['-c', config.cmd.join(' ')]\n : [];\n\n const cwd = config.cwd ?? process.cwd();\n const env = { ...process.env, ...config.env };\n\n try {\n const ptyProcess = pty.spawn(shell, args, {\n name: 'xterm-256color',\n cols: 120,\n rows: 30,\n cwd,\n env: env as Record<string, string>,\n });\n\n const managed: ManagedProcess = {\n name,\n config,\n pty: ptyProcess,\n status: 'running',\n output: [],\n exitCode: null,\n };\n\n this.processes.set(name, managed);\n\n ptyProcess.onData((data) => {\n managed.output.push(data);\n // Trim output if too long\n if (managed.output.length > this.maxOutputLines) {\n managed.output = managed.output.slice(-this.maxOutputLines);\n }\n this.emit('output', name, data);\n });\n\n ptyProcess.onExit(({ exitCode }) => {\n managed.status = exitCode === 0 ? 'stopped' : 'error';\n managed.exitCode = exitCode;\n managed.pty = null;\n this.emit('exit', name, exitCode);\n\n // Auto-restart if configured\n if (config.autoRestart && exitCode !== 0) {\n setTimeout(() => {\n this.start(name, config);\n }, 1000);\n }\n });\n\n this.emit('started', name);\n } catch (error) {\n const managed: ManagedProcess = {\n name,\n config,\n pty: null,\n status: 'error',\n output: [`Error starting process: ${error}`],\n exitCode: -1,\n };\n this.processes.set(name, managed);\n this.emit('error', name, error);\n }\n }\n\n restart(name: string): void {\n const proc = this.processes.get(name);\n if (proc) {\n if (proc.pty) {\n proc.pty.kill();\n }\n proc.output = [];\n this.start(name, proc.config);\n }\n }\n\n restartAll(): void {\n for (const name of this.processes.keys()) {\n this.restart(name);\n }\n }\n\n kill(name: string): void {\n const proc = this.processes.get(name);\n if (proc?.pty) {\n proc.pty.kill();\n }\n }\n\n killAll(): void {\n for (const proc of this.processes.values()) {\n if (proc.pty) {\n proc.pty.kill();\n }\n }\n }\n\n write(name: string, data: string): void {\n const proc = this.processes.get(name);\n if (proc?.pty) {\n proc.pty.write(data);\n }\n }\n\n resize(name: string, cols: number, rows: number): void {\n const proc = this.processes.get(name);\n if (proc?.pty) {\n proc.pty.resize(cols, rows);\n }\n }\n\n getProcess(name: string): ManagedProcess | undefined {\n return this.processes.get(name);\n }\n\n getProcesses(): ManagedProcess[] {\n return Array.from(this.processes.values());\n }\n\n getNames(): string[] {\n return Array.from(this.processes.keys());\n }\n\n getOutput(name: string): string {\n return this.processes.get(name)?.output.join('') ?? '';\n }\n}\n","import blessed from 'blessed';\nimport type { PanexConfig } from './types';\nimport { ProcessManager } from './process-manager';\n\nexport async function createTUI(config: PanexConfig): Promise<void> {\n const processManager = new ProcessManager(config.procs);\n\n // Create screen\n const screen = blessed.screen({\n smartCSR: true,\n title: 'panex',\n fullUnicode: true,\n });\n\n // Process list (left panel)\n const processList = blessed.list({\n parent: screen,\n label: ' PROCESSES ',\n top: 0,\n left: 0,\n width: '20%',\n height: '100%-1',\n border: { type: 'line' },\n style: {\n border: { fg: 'blue' },\n selected: { bg: 'blue', fg: 'white' },\n item: { fg: 'white' },\n },\n keys: true,\n vi: true,\n mouse: config.settings?.mouse ?? true,\n scrollbar: {\n ch: '│',\n style: { bg: 'blue' },\n },\n });\n\n // Output panel (right panel)\n const outputBox = blessed.box({\n parent: screen,\n label: ' OUTPUT ',\n top: 0,\n left: '20%',\n width: '80%',\n height: '100%-1',\n border: { type: 'line' },\n style: {\n border: { fg: 'green' },\n },\n scrollable: true,\n alwaysScroll: true,\n scrollbar: {\n ch: '│',\n style: { bg: 'green' },\n },\n mouse: config.settings?.mouse ?? true,\n keys: true,\n vi: true,\n });\n\n // Status bar\n const statusBar = blessed.box({\n parent: screen,\n bottom: 0,\n left: 0,\n width: '100%',\n height: 1,\n style: {\n bg: 'blue',\n fg: 'white',\n },\n content: ' [↑↓/jk] select [Enter] focus [r] restart [a] restart all [x] kill [q] quit [?] help ',\n });\n\n // Help popup\n const helpBox = blessed.box({\n parent: screen,\n top: 'center',\n left: 'center',\n width: '60%',\n height: '60%',\n label: ' Help ',\n border: { type: 'line' },\n style: {\n border: { fg: 'yellow' },\n bg: 'black',\n },\n hidden: true,\n content: `\n Keyboard Shortcuts\n ──────────────────\n\n Navigation\n ↑/↓ or j/k Navigate process list\n g/G Scroll to top/bottom of output\n PgUp/PgDn Scroll output\n\n Process Control\n Enter Focus process (interactive mode)\n Esc Exit focus mode\n r Restart selected process\n a Restart all processes\n x Kill selected process\n\n General\n ? Toggle this help\n q Quit panex\n\n Press any key to close this help...\n `,\n });\n\n // State\n let selectedIndex = 0;\n let focusMode = false;\n const processNames = Object.keys(config.procs);\n\n // Update process list UI\n function updateProcessList() {\n const items = processNames.map((name, i) => {\n const proc = processManager.getProcess(name);\n const status = proc?.status === 'running' ? '●' : proc?.status === 'error' ? '✗' : '○';\n const prefix = i === selectedIndex ? '▶' : ' ';\n return `${prefix} ${name} ${status}`;\n });\n processList.setItems(items);\n processList.select(selectedIndex);\n screen.render();\n }\n\n // Update output panel\n function updateOutput() {\n const name = processNames[selectedIndex];\n if (name) {\n outputBox.setLabel(` OUTPUT: ${name} `);\n const output = processManager.getOutput(name);\n outputBox.setContent(output);\n outputBox.setScrollPerc(100); // Scroll to bottom\n }\n screen.render();\n }\n\n // Event handlers\n processManager.on('output', (name: string) => {\n if (name === processNames[selectedIndex]) {\n updateOutput();\n }\n });\n\n processManager.on('started', () => {\n updateProcessList();\n });\n\n processManager.on('exit', () => {\n updateProcessList();\n });\n\n // Keyboard handling\n screen.key(['q', 'C-c'], () => {\n processManager.killAll();\n process.exit(0);\n });\n\n screen.key(['?'], () => {\n helpBox.toggle();\n screen.render();\n });\n\n screen.key(['escape'], () => {\n if (!helpBox.hidden) {\n helpBox.hide();\n screen.render();\n return;\n }\n if (focusMode) {\n focusMode = false;\n statusBar.setContent(' [↑↓/jk] select [Enter] focus [r] restart [a] restart all [x] kill [q] quit [?] help ');\n screen.render();\n }\n });\n\n helpBox.key(['escape', 'q', '?', 'enter', 'space'], () => {\n helpBox.hide();\n screen.render();\n });\n\n screen.key(['up', 'k'], () => {\n if (focusMode || !helpBox.hidden) return;\n selectedIndex = Math.max(0, selectedIndex - 1);\n updateProcessList();\n updateOutput();\n });\n\n screen.key(['down', 'j'], () => {\n if (focusMode || !helpBox.hidden) return;\n selectedIndex = Math.min(processNames.length - 1, selectedIndex + 1);\n updateProcessList();\n updateOutput();\n });\n\n screen.key(['enter'], () => {\n if (!helpBox.hidden) {\n helpBox.hide();\n screen.render();\n return;\n }\n focusMode = !focusMode;\n const name = processNames[selectedIndex];\n if (focusMode && name) {\n statusBar.setContent(` FOCUS: ${name} - Type to interact, [Esc] to exit focus mode `);\n } else {\n statusBar.setContent(' [↑↓/jk] select [Enter] focus [r] restart [a] restart all [x] kill [q] quit [?] help ');\n }\n screen.render();\n });\n\n screen.key(['r'], () => {\n if (focusMode || !helpBox.hidden) return;\n const name = processNames[selectedIndex];\n if (name) {\n processManager.restart(name);\n }\n });\n\n screen.key(['a'], () => {\n if (focusMode || !helpBox.hidden) return;\n processManager.restartAll();\n });\n\n screen.key(['x'], () => {\n if (focusMode || !helpBox.hidden) return;\n const name = processNames[selectedIndex];\n if (name) {\n processManager.kill(name);\n }\n });\n\n screen.key(['g'], () => {\n if (focusMode || !helpBox.hidden) return;\n outputBox.setScrollPerc(0);\n screen.render();\n });\n\n screen.key(['S-g'], () => {\n if (focusMode || !helpBox.hidden) return;\n outputBox.setScrollPerc(100);\n screen.render();\n });\n\n // Forward input in focus mode\n screen.on('keypress', (ch: string, key: { full: string }) => {\n if (focusMode && ch) {\n const name = processNames[selectedIndex];\n if (name) {\n processManager.write(name, ch);\n }\n }\n });\n\n // Handle resize\n screen.on('resize', () => {\n const name = processNames[selectedIndex];\n if (name) {\n const cols = Math.floor((screen.width as number) * 0.8) - 2;\n const rows = (screen.height as number) - 3;\n processManager.resize(name, cols, rows);\n }\n });\n\n // Initial render\n updateProcessList();\n updateOutput();\n processList.focus();\n\n // Start all processes\n await processManager.startAll();\n updateProcessList();\n updateOutput();\n\n screen.render();\n}"],"mappings":"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","names":[]}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
import {
|
|
4
|
+
createTUI
|
|
5
|
+
} from "./chunk-Z7LPKPEA.js";
|
|
6
|
+
|
|
7
|
+
// src/cli.ts
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
var program = new Command();
|
|
10
|
+
program.name("panex").description("Terminal UI for running multiple processes in parallel").version("0.1.0").argument("<commands...>", "Commands to run in parallel").option("-n, --names <names>", "Comma-separated names for each process").action(async (commands, options) => {
|
|
11
|
+
const names = options.names?.split(",") ?? commands.map((_, i) => `proc${i + 1}`);
|
|
12
|
+
const config = {
|
|
13
|
+
procs: Object.fromEntries(
|
|
14
|
+
commands.map((cmd, i) => [
|
|
15
|
+
names[i] ?? `proc${i + 1}`,
|
|
16
|
+
{ shell: cmd }
|
|
17
|
+
])
|
|
18
|
+
)
|
|
19
|
+
};
|
|
20
|
+
await createTUI(config);
|
|
21
|
+
});
|
|
22
|
+
program.parse();
|
|
23
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { Command } from 'commander';\nimport type { PanexConfig } from './types';\nimport { createTUI } from './tui';\n\nconst program = new Command();\n\nprogram\n .name('panex')\n .description('Terminal UI for running multiple processes in parallel')\n .version('0.1.0')\n .argument('<commands...>', 'Commands to run in parallel')\n .option('-n, --names <names>', 'Comma-separated names for each process')\n .action(async (commands: string[], options: { names?: string }) => {\n const names = options.names?.split(',') ?? commands.map((_, i) => `proc${i + 1}`);\n const config: PanexConfig = {\n procs: Object.fromEntries(\n commands.map((cmd, i) => [\n names[i] ?? `proc${i + 1}`,\n { shell: cmd },\n ])\n ),\n };\n\n await createTUI(config);\n });\n\nprogram.parse();"],"mappings":";;;;;;;AAEA,SAAS,eAAe;AAIxB,IAAM,UAAU,IAAI,QAAQ;AAE5B,QACG,KAAK,OAAO,EACZ,YAAY,wDAAwD,EACpE,QAAQ,OAAO,EACf,SAAS,iBAAiB,6BAA6B,EACvD,OAAO,uBAAuB,wCAAwC,EACtE,OAAO,OAAO,UAAoB,YAAgC;AACjE,QAAM,QAAQ,QAAQ,OAAO,MAAM,GAAG,KAAK,SAAS,IAAI,CAAC,GAAG,MAAM,OAAO,IAAI,CAAC,EAAE;AAChF,QAAM,SAAsB;AAAA,IAC1B,OAAO,OAAO;AAAA,MACZ,SAAS,IAAI,CAAC,KAAK,MAAM;AAAA,QACvB,MAAM,CAAC,KAAK,OAAO,IAAI,CAAC;AAAA,QACxB,EAAE,OAAO,IAAI;AAAA,MACf,CAAC;AAAA,IACH;AAAA,EACF;AAEA,QAAM,UAAU,MAAM;AACxB,CAAC;AAEH,QAAQ,MAAM;","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as pty from 'node-pty';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
|
|
4
|
+
interface ProcessConfig {
|
|
5
|
+
cmd?: string[];
|
|
6
|
+
shell?: string;
|
|
7
|
+
cwd?: string;
|
|
8
|
+
env?: Record<string, string>;
|
|
9
|
+
autoRestart?: boolean;
|
|
10
|
+
}
|
|
11
|
+
interface PanexConfig {
|
|
12
|
+
procs: Record<string, ProcessConfig>;
|
|
13
|
+
settings?: {
|
|
14
|
+
mouse?: boolean;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
declare function createTUI(config: PanexConfig): Promise<void>;
|
|
19
|
+
|
|
20
|
+
interface ManagedProcess {
|
|
21
|
+
name: string;
|
|
22
|
+
config: ProcessConfig;
|
|
23
|
+
pty: pty.IPty | null;
|
|
24
|
+
status: 'running' | 'stopped' | 'error';
|
|
25
|
+
output: string[];
|
|
26
|
+
exitCode: number | null;
|
|
27
|
+
}
|
|
28
|
+
declare class ProcessManager extends EventEmitter {
|
|
29
|
+
private procs;
|
|
30
|
+
private processes;
|
|
31
|
+
private maxOutputLines;
|
|
32
|
+
constructor(procs: Record<string, ProcessConfig>);
|
|
33
|
+
startAll(): Promise<void>;
|
|
34
|
+
start(name: string, config: ProcessConfig): Promise<void>;
|
|
35
|
+
restart(name: string): void;
|
|
36
|
+
restartAll(): void;
|
|
37
|
+
kill(name: string): void;
|
|
38
|
+
killAll(): void;
|
|
39
|
+
write(name: string, data: string): void;
|
|
40
|
+
resize(name: string, cols: number, rows: number): void;
|
|
41
|
+
getProcess(name: string): ManagedProcess | undefined;
|
|
42
|
+
getProcesses(): ManagedProcess[];
|
|
43
|
+
getNames(): string[];
|
|
44
|
+
getOutput(name: string): string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { type ManagedProcess, type PanexConfig, type ProcessConfig, ProcessManager, createTUI };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "panex",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "Terminal UI for running multiple processes in parallel",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"panex": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"module": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"dev": "bun run src/cli.ts",
|
|
17
|
+
"build": "tsup",
|
|
18
|
+
"test": "bun test",
|
|
19
|
+
"prepublishOnly": "bun run build",
|
|
20
|
+
"typecheck": "tsc --noEmit"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/king8fisher/panex.git"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"tui",
|
|
28
|
+
"terminal",
|
|
29
|
+
"process",
|
|
30
|
+
"parallel",
|
|
31
|
+
"devtools",
|
|
32
|
+
"turbo",
|
|
33
|
+
"cli",
|
|
34
|
+
"concurrently"
|
|
35
|
+
],
|
|
36
|
+
"author": "Anton Veretennikov (king8fisher)",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"blessed": "^0.1.81",
|
|
43
|
+
"chalk": "^5.3.0",
|
|
44
|
+
"commander": "^12.1.0",
|
|
45
|
+
"node-pty": "^1.0.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/blessed": "^0.1.25",
|
|
49
|
+
"@types/bun": "^1.3.5",
|
|
50
|
+
"@types/node": "^22.10.0",
|
|
51
|
+
"tsup": "^8.3.5",
|
|
52
|
+
"typescript": "^5.7.2"
|
|
53
|
+
}
|
|
54
|
+
}
|