panex 0.9.1 → 0.9.3
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/README.md +27 -29
- package/dist/cli.js +123 -61
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.js +107 -55
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -21,20 +21,18 @@ A terminal UI for running multiple processes in parallel. Like Turborepo's TUI,
|
|
|
21
21
|
- **Zero config** - Just pass commands as arguments
|
|
22
22
|
- **Cross-platform** - macOS, Linux, Windows
|
|
23
23
|
|
|
24
|
-
##
|
|
24
|
+
## Requirements
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
# npm
|
|
28
|
-
npm install -g panex
|
|
26
|
+
**Bun runtime is required.** Panex uses Bun's built-in PTY support for interactive processes.
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
npx panex "npm run api" "npm run web"
|
|
28
|
+
## Installation
|
|
32
29
|
|
|
33
|
-
|
|
30
|
+
```bash
|
|
31
|
+
# Run directly with bunx
|
|
34
32
|
bunx panex "bun run api" "bun run web"
|
|
35
33
|
|
|
36
|
-
#
|
|
37
|
-
|
|
34
|
+
# Or install globally
|
|
35
|
+
bun add -g panex
|
|
38
36
|
```
|
|
39
37
|
|
|
40
38
|
## Usage
|
|
@@ -51,28 +49,28 @@ panex -n api,web,mobile "npm run api" "npm run web" "npm run mobile"
|
|
|
51
49
|
|
|
52
50
|
### Keyboard Shortcuts
|
|
53
51
|
|
|
54
|
-
| Key
|
|
55
|
-
|
|
56
|
-
| `↑/↓` or `j/k` | Navigate process list
|
|
57
|
-
| `Enter`
|
|
58
|
-
| `Esc`
|
|
59
|
-
| `r`
|
|
60
|
-
| `x`
|
|
61
|
-
| `
|
|
62
|
-
| `q`
|
|
63
|
-
| `?`
|
|
64
|
-
| `g/G`
|
|
65
|
-
| `PgUp/PgDn`
|
|
52
|
+
| Key | Action |
|
|
53
|
+
| -------------- | -------------------------------- |
|
|
54
|
+
| `↑/↓` or `j/k` | Navigate process list |
|
|
55
|
+
| `Enter` | Focus process (interactive mode) |
|
|
56
|
+
| `Esc` | Exit focus mode |
|
|
57
|
+
| `r` | Restart selected process |
|
|
58
|
+
| `x` | Kill selected process |
|
|
59
|
+
| `A` | Restart all processes |
|
|
60
|
+
| `q` | Quit panex |
|
|
61
|
+
| `?` | Show help |
|
|
62
|
+
| `g/G` | Scroll to top/bottom |
|
|
63
|
+
| `PgUp/PgDn` | Scroll output |
|
|
66
64
|
|
|
67
65
|
## Why panex?
|
|
68
66
|
|
|
69
|
-
| Feature
|
|
70
|
-
|
|
71
|
-
| Split-pane TUI
|
|
72
|
-
| PTY support (QR codes) | ✅
|
|
73
|
-
| Zero config
|
|
74
|
-
| npm install
|
|
75
|
-
| No monorepo required
|
|
67
|
+
| Feature | panex | concurrently | mprocs | turbo |
|
|
68
|
+
| ---------------------- | ----- | ------------ | ------ | ----- |
|
|
69
|
+
| Split-pane TUI | ✅ | ❌ | ✅ | ✅ |
|
|
70
|
+
| PTY support (QR codes) | ✅ | ❌ | ✅ | ✅ |
|
|
71
|
+
| Zero config | ✅ | ✅ | ❌ | ❌ |
|
|
72
|
+
| npm install | ✅ | ✅ | ❌ | ✅ |
|
|
73
|
+
| No monorepo required | ✅ | ✅ | ✅ | ❌ |
|
|
76
74
|
|
|
77
75
|
## Development
|
|
78
76
|
|
|
@@ -104,7 +102,7 @@ node dist/cli.js "echo test"
|
|
|
104
102
|
|
|
105
103
|
- TypeScript + Bun
|
|
106
104
|
- blessed (TUI framework)
|
|
107
|
-
-
|
|
105
|
+
- Bun.spawn with terminal (PTY support)
|
|
108
106
|
- commander (CLI parsing)
|
|
109
107
|
- tsup (build tool)
|
|
110
108
|
|
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command } from "commander";
|
|
@@ -7,7 +7,6 @@ import { Command } from "commander";
|
|
|
7
7
|
import blessed from "blessed";
|
|
8
8
|
|
|
9
9
|
// src/process-manager.ts
|
|
10
|
-
import * as pty from "node-pty";
|
|
11
10
|
import { EventEmitter } from "events";
|
|
12
11
|
var ProcessManager = class extends EventEmitter {
|
|
13
12
|
constructor(procs) {
|
|
@@ -30,52 +29,51 @@ var ProcessManager = class extends EventEmitter {
|
|
|
30
29
|
const args = config.shell ? ["-c", config.shell] : config.cmd ? ["-c", config.cmd.join(" ")] : [];
|
|
31
30
|
const cwd = config.cwd ?? process.cwd();
|
|
32
31
|
const env = { ...process.env, ...config.env };
|
|
32
|
+
const managed = {
|
|
33
|
+
name,
|
|
34
|
+
config,
|
|
35
|
+
pty: null,
|
|
36
|
+
status: "running",
|
|
37
|
+
output: [],
|
|
38
|
+
exitCode: null
|
|
39
|
+
};
|
|
40
|
+
this.processes.set(name, managed);
|
|
33
41
|
try {
|
|
34
|
-
const
|
|
35
|
-
name: "xterm-256color",
|
|
36
|
-
cols: 120,
|
|
37
|
-
rows: 30,
|
|
42
|
+
const proc = Bun.spawn([shell, ...args], {
|
|
38
43
|
cwd,
|
|
39
|
-
env
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
managed.output.push(data);
|
|
52
|
-
if (managed.output.length > this.maxOutputLines) {
|
|
53
|
-
managed.output = managed.output.slice(-this.maxOutputLines);
|
|
44
|
+
env,
|
|
45
|
+
terminal: {
|
|
46
|
+
cols: 120,
|
|
47
|
+
rows: 30,
|
|
48
|
+
data: (_terminal, data) => {
|
|
49
|
+
const str = new TextDecoder().decode(data);
|
|
50
|
+
managed.output.push(str);
|
|
51
|
+
if (managed.output.length > this.maxOutputLines) {
|
|
52
|
+
managed.output = managed.output.slice(-this.maxOutputLines);
|
|
53
|
+
}
|
|
54
|
+
this.emit("output", name, str);
|
|
55
|
+
}
|
|
54
56
|
}
|
|
55
|
-
this.emit("output", name, data);
|
|
56
57
|
});
|
|
57
|
-
|
|
58
|
+
managed.pty = {
|
|
59
|
+
write: (data) => proc.terminal?.write(data),
|
|
60
|
+
resize: (cols, rows) => proc.terminal?.resize(cols, rows),
|
|
61
|
+
kill: () => proc.kill()
|
|
62
|
+
};
|
|
63
|
+
proc.exited.then((exitCode) => {
|
|
58
64
|
managed.status = exitCode === 0 ? "stopped" : "error";
|
|
59
65
|
managed.exitCode = exitCode;
|
|
60
66
|
managed.pty = null;
|
|
61
67
|
this.emit("exit", name, exitCode);
|
|
62
|
-
if (config.autoRestart && exitCode !== 0) {
|
|
63
|
-
setTimeout(() =>
|
|
64
|
-
this.start(name, config);
|
|
65
|
-
}, 1e3);
|
|
68
|
+
if (managed.config.autoRestart && exitCode !== 0) {
|
|
69
|
+
setTimeout(() => this.start(name, managed.config), 1e3);
|
|
66
70
|
}
|
|
67
71
|
});
|
|
68
72
|
this.emit("started", name);
|
|
69
73
|
} catch (error) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
pty: null,
|
|
74
|
-
status: "error",
|
|
75
|
-
output: [`Error starting process: ${error}`],
|
|
76
|
-
exitCode: -1
|
|
77
|
-
};
|
|
78
|
-
this.processes.set(name, managed);
|
|
74
|
+
managed.status = "error";
|
|
75
|
+
managed.output = [`Error starting process: ${error}`];
|
|
76
|
+
managed.exitCode = -1;
|
|
79
77
|
this.emit("error", name, error);
|
|
80
78
|
}
|
|
81
79
|
}
|
|
@@ -154,8 +152,6 @@ async function createTUI(config) {
|
|
|
154
152
|
selected: { bg: "blue", fg: "white" },
|
|
155
153
|
item: { fg: "white" }
|
|
156
154
|
},
|
|
157
|
-
keys: true,
|
|
158
|
-
vi: true,
|
|
159
155
|
mouse: config.settings?.mouse ?? true,
|
|
160
156
|
scrollbar: {
|
|
161
157
|
ch: "\u2502",
|
|
@@ -193,7 +189,7 @@ async function createTUI(config) {
|
|
|
193
189
|
bg: "blue",
|
|
194
190
|
fg: "white"
|
|
195
191
|
},
|
|
196
|
-
content: " [\u2191\u2193/jk] select [Enter] focus [r] restart [
|
|
192
|
+
content: " [\u2191\u2193/jk] select [Enter] focus [r] restart [A] restart All [x] kill [q] quit [?] help "
|
|
197
193
|
});
|
|
198
194
|
const helpBox = blessed.box({
|
|
199
195
|
parent: screen,
|
|
@@ -221,7 +217,7 @@ async function createTUI(config) {
|
|
|
221
217
|
Enter Focus process (interactive mode)
|
|
222
218
|
Esc Exit focus mode
|
|
223
219
|
r Restart selected process
|
|
224
|
-
|
|
220
|
+
A Restart all processes
|
|
225
221
|
x Kill selected process
|
|
226
222
|
|
|
227
223
|
General
|
|
@@ -234,6 +230,7 @@ async function createTUI(config) {
|
|
|
234
230
|
let selectedIndex = 0;
|
|
235
231
|
let focusMode = false;
|
|
236
232
|
const processNames = Object.keys(config.procs);
|
|
233
|
+
const scrollPositions = /* @__PURE__ */ new Map();
|
|
237
234
|
function updateProcessList() {
|
|
238
235
|
const items = processNames.map((name, i) => {
|
|
239
236
|
const proc = processManager.getProcess(name);
|
|
@@ -245,19 +242,30 @@ async function createTUI(config) {
|
|
|
245
242
|
processList.select(selectedIndex);
|
|
246
243
|
screen.render();
|
|
247
244
|
}
|
|
248
|
-
function updateOutput() {
|
|
245
|
+
function updateOutput(autoScroll = false) {
|
|
249
246
|
const name = processNames[selectedIndex];
|
|
250
247
|
if (name) {
|
|
251
248
|
outputBox.setLabel(` OUTPUT: ${name} `);
|
|
252
249
|
const output = processManager.getOutput(name);
|
|
253
250
|
outputBox.setContent(output);
|
|
254
|
-
|
|
251
|
+
if (autoScroll) {
|
|
252
|
+
outputBox.setScrollPerc(100);
|
|
253
|
+
} else {
|
|
254
|
+
const savedPos = scrollPositions.get(name) ?? 100;
|
|
255
|
+
outputBox.setScrollPerc(savedPos);
|
|
256
|
+
}
|
|
255
257
|
}
|
|
256
258
|
screen.render();
|
|
257
259
|
}
|
|
260
|
+
function saveScrollPosition() {
|
|
261
|
+
const name = processNames[selectedIndex];
|
|
262
|
+
if (name) {
|
|
263
|
+
scrollPositions.set(name, outputBox.getScrollPerc());
|
|
264
|
+
}
|
|
265
|
+
}
|
|
258
266
|
processManager.on("output", (name) => {
|
|
259
267
|
if (name === processNames[selectedIndex]) {
|
|
260
|
-
updateOutput();
|
|
268
|
+
updateOutput(true);
|
|
261
269
|
}
|
|
262
270
|
});
|
|
263
271
|
processManager.on("started", () => {
|
|
@@ -266,6 +274,12 @@ async function createTUI(config) {
|
|
|
266
274
|
processManager.on("exit", () => {
|
|
267
275
|
updateProcessList();
|
|
268
276
|
});
|
|
277
|
+
processManager.on("error", (name) => {
|
|
278
|
+
updateProcessList();
|
|
279
|
+
if (name === processNames[selectedIndex]) {
|
|
280
|
+
updateOutput();
|
|
281
|
+
}
|
|
282
|
+
});
|
|
269
283
|
screen.key(["q", "C-c"], () => {
|
|
270
284
|
processManager.killAll();
|
|
271
285
|
process.exit(0);
|
|
@@ -282,7 +296,7 @@ async function createTUI(config) {
|
|
|
282
296
|
}
|
|
283
297
|
if (focusMode) {
|
|
284
298
|
focusMode = false;
|
|
285
|
-
statusBar.setContent(" [\u2191\u2193/jk] select [Enter] focus [r] restart [
|
|
299
|
+
statusBar.setContent(" [\u2191\u2193/jk] select [Enter] focus [r] restart [A] restart All [x] kill [q] quit [?] help ");
|
|
286
300
|
screen.render();
|
|
287
301
|
}
|
|
288
302
|
});
|
|
@@ -292,15 +306,21 @@ async function createTUI(config) {
|
|
|
292
306
|
});
|
|
293
307
|
screen.key(["up", "k"], () => {
|
|
294
308
|
if (focusMode || !helpBox.hidden) return;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
309
|
+
if (selectedIndex > 0) {
|
|
310
|
+
saveScrollPosition();
|
|
311
|
+
selectedIndex--;
|
|
312
|
+
updateProcessList();
|
|
313
|
+
updateOutput();
|
|
314
|
+
}
|
|
298
315
|
});
|
|
299
316
|
screen.key(["down", "j"], () => {
|
|
300
317
|
if (focusMode || !helpBox.hidden) return;
|
|
301
|
-
selectedIndex
|
|
302
|
-
|
|
303
|
-
|
|
318
|
+
if (selectedIndex < processNames.length - 1) {
|
|
319
|
+
saveScrollPosition();
|
|
320
|
+
selectedIndex++;
|
|
321
|
+
updateProcessList();
|
|
322
|
+
updateOutput();
|
|
323
|
+
}
|
|
304
324
|
});
|
|
305
325
|
screen.key(["enter"], () => {
|
|
306
326
|
if (!helpBox.hidden) {
|
|
@@ -308,14 +328,17 @@ async function createTUI(config) {
|
|
|
308
328
|
screen.render();
|
|
309
329
|
return;
|
|
310
330
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
331
|
+
if (!focusMode) {
|
|
332
|
+
focusMode = true;
|
|
333
|
+
const name = processNames[selectedIndex];
|
|
314
334
|
statusBar.setContent(` FOCUS: ${name} - Type to interact, [Esc] to exit focus mode `);
|
|
335
|
+
screen.render();
|
|
315
336
|
} else {
|
|
316
|
-
|
|
337
|
+
const name = processNames[selectedIndex];
|
|
338
|
+
if (name) {
|
|
339
|
+
processManager.write(name, "\r");
|
|
340
|
+
}
|
|
317
341
|
}
|
|
318
|
-
screen.render();
|
|
319
342
|
});
|
|
320
343
|
screen.key(["r"], () => {
|
|
321
344
|
if (focusMode || !helpBox.hidden) return;
|
|
@@ -324,7 +347,7 @@ async function createTUI(config) {
|
|
|
324
347
|
processManager.restart(name);
|
|
325
348
|
}
|
|
326
349
|
});
|
|
327
|
-
screen.key(["a"], () => {
|
|
350
|
+
screen.key(["S-a"], () => {
|
|
328
351
|
if (focusMode || !helpBox.hidden) return;
|
|
329
352
|
processManager.restartAll();
|
|
330
353
|
});
|
|
@@ -345,8 +368,37 @@ async function createTUI(config) {
|
|
|
345
368
|
outputBox.setScrollPerc(100);
|
|
346
369
|
screen.render();
|
|
347
370
|
});
|
|
371
|
+
outputBox.on("click", () => {
|
|
372
|
+
if (!helpBox.hidden) return;
|
|
373
|
+
if (!focusMode) {
|
|
374
|
+
focusMode = true;
|
|
375
|
+
const name = processNames[selectedIndex];
|
|
376
|
+
statusBar.setContent(` FOCUS: ${name} - Type to interact, [Esc] to exit focus mode `);
|
|
377
|
+
screen.render();
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
processList.on("element click", (_el, data) => {
|
|
381
|
+
if (!helpBox.hidden) return;
|
|
382
|
+
const absTop = processList.atop ?? 0;
|
|
383
|
+
const clickedIndex = data.y - absTop - 1;
|
|
384
|
+
if (clickedIndex < 0 || clickedIndex >= processNames.length) return;
|
|
385
|
+
if (focusMode) {
|
|
386
|
+
focusMode = false;
|
|
387
|
+
statusBar.setContent(" [\u2191\u2193/jk] select [Enter] focus [r] restart [A] restart All [x] kill [q] quit [?] help ");
|
|
388
|
+
}
|
|
389
|
+
if (clickedIndex !== selectedIndex) {
|
|
390
|
+
saveScrollPosition();
|
|
391
|
+
selectedIndex = clickedIndex;
|
|
392
|
+
updateProcessList();
|
|
393
|
+
updateOutput();
|
|
394
|
+
}
|
|
395
|
+
screen.render();
|
|
396
|
+
});
|
|
348
397
|
screen.on("keypress", (ch, key) => {
|
|
349
398
|
if (focusMode && ch) {
|
|
399
|
+
if (key.name === "escape" || key.name === "return" || key.name === "enter") {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
350
402
|
const name = processNames[selectedIndex];
|
|
351
403
|
if (name) {
|
|
352
404
|
processManager.write(name, ch);
|
|
@@ -371,15 +423,25 @@ async function createTUI(config) {
|
|
|
371
423
|
}
|
|
372
424
|
|
|
373
425
|
// src/cli.ts
|
|
426
|
+
if (typeof Bun === "undefined") {
|
|
427
|
+
console.error("Error: panex requires Bun runtime.");
|
|
428
|
+
console.error("Please run with bunx instead of npx:");
|
|
429
|
+
console.error(' bunx panex "cmd1" "cmd2"');
|
|
430
|
+
process.exit(1);
|
|
431
|
+
}
|
|
374
432
|
var program = new Command();
|
|
375
433
|
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) => {
|
|
376
|
-
const
|
|
434
|
+
const rawNames = options.names?.split(",") ?? commands.map((_, i) => `proc${i + 1}`);
|
|
435
|
+
const usedNames = /* @__PURE__ */ new Map();
|
|
436
|
+
const names = rawNames.map((name, i) => {
|
|
437
|
+
const baseName = name || `proc${i + 1}`;
|
|
438
|
+
const count = usedNames.get(baseName) ?? 0;
|
|
439
|
+
usedNames.set(baseName, count + 1);
|
|
440
|
+
return count === 0 ? baseName : `${baseName}-${count + 1}`;
|
|
441
|
+
});
|
|
377
442
|
const config = {
|
|
378
443
|
procs: Object.fromEntries(
|
|
379
|
-
commands.map((cmd, i) => [
|
|
380
|
-
names[i] ?? `proc${i + 1}`,
|
|
381
|
-
{ shell: cmd }
|
|
382
|
-
])
|
|
444
|
+
commands.map((cmd, i) => [names[i], { shell: cmd }])
|
|
383
445
|
)
|
|
384
446
|
};
|
|
385
447
|
await createTUI(config);
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts","../src/tui.ts","../src/process-manager.ts"],"sourcesContent":["import { 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();","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}","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"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/tui.ts","../src/process-manager.ts"],"sourcesContent":["// Check for Bun runtime - must be at top before any Bun APIs are used\nif (typeof Bun === 'undefined') {\n console.error('Error: panex requires Bun runtime.');\n console.error('Please run with bunx instead of npx:');\n console.error('\\tbunx panex \"cmd1\" \"cmd2\"');\n process.exit(1);\n}\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 rawNames = options.names?.split(',') ?? commands.map((_, i) => `proc${i + 1}`);\n\n // Ensure unique names by adding suffix for duplicates\n const usedNames = new Map<string, number>();\n const names = rawNames.map((name, i) => {\n const baseName = name || `proc${i + 1}`;\n const count = usedNames.get(baseName) ?? 0;\n usedNames.set(baseName, count + 1);\n return count === 0 ? baseName : `${baseName}-${count + 1}`;\n });\n\n const config: PanexConfig = {\n procs: Object.fromEntries(\n commands.map((cmd, i) => [names[i], { shell: cmd }])\n ),\n };\n\n await createTUI(config);\n });\n\nprogram.parse();","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 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 const scrollPositions = new Map<string, number>(); // Track scroll % per process\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(autoScroll = false) {\n const name = processNames[selectedIndex];\n if (name) {\n outputBox.setLabel(` OUTPUT: ${name} `);\n const output = processManager.getOutput(name);\n outputBox.setContent(output);\n if (autoScroll) {\n outputBox.setScrollPerc(100); // Scroll to bottom for new output\n } else {\n // Restore saved scroll position or default to bottom\n const savedPos = scrollPositions.get(name) ?? 100;\n outputBox.setScrollPerc(savedPos);\n }\n }\n screen.render();\n }\n\n // Save current scroll position before switching\n function saveScrollPosition() {\n const name = processNames[selectedIndex];\n if (name) {\n scrollPositions.set(name, outputBox.getScrollPerc());\n }\n }\n\n // Event handlers\n processManager.on('output', (name: string) => {\n if (name === processNames[selectedIndex]) {\n updateOutput(true); // Auto-scroll for new output\n }\n });\n\n processManager.on('started', () => {\n updateProcessList();\n });\n\n processManager.on('exit', () => {\n updateProcessList();\n });\n\n processManager.on('error', (name: string) => {\n updateProcessList();\n if (name === processNames[selectedIndex]) {\n updateOutput();\n }\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 if (selectedIndex > 0) {\n saveScrollPosition();\n selectedIndex--;\n updateProcessList();\n updateOutput();\n }\n });\n\n screen.key(['down', 'j'], () => {\n if (focusMode || !helpBox.hidden) return;\n if (selectedIndex < processNames.length - 1) {\n saveScrollPosition();\n selectedIndex++;\n updateProcessList();\n updateOutput();\n }\n });\n\n screen.key(['enter'], () => {\n if (!helpBox.hidden) {\n helpBox.hide();\n screen.render();\n return;\n }\n if (!focusMode) {\n // Enter focus mode (don't forward this Enter)\n focusMode = true;\n const name = processNames[selectedIndex];\n statusBar.setContent(` FOCUS: ${name} - Type to interact, [Esc] to exit focus mode `);\n screen.render();\n } else {\n // Already in focus mode - forward Enter to process\n const name = processNames[selectedIndex];\n if (name) {\n processManager.write(name, '\\r');\n }\n }\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(['S-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 // Mouse click on output box enables focus mode\n outputBox.on('click', () => {\n if (!helpBox.hidden) return;\n if (!focusMode) {\n focusMode = true;\n const name = processNames[selectedIndex];\n statusBar.setContent(` FOCUS: ${name} - Type to interact, [Esc] to exit focus mode `);\n screen.render();\n }\n });\n\n // Mouse click on process list (single click)\n processList.on('element click', (_el: blessed.Widgets.BlessedElement, data: { y: number }) => {\n if (!helpBox.hidden) return;\n\n // Calculate index: y is absolute, subtract list's absolute top and border\n const absTop = (processList.atop as number) ?? 0;\n const clickedIndex = data.y - absTop - 1; // -1 for border\n\n if (clickedIndex < 0 || clickedIndex >= processNames.length) return;\n\n // Exit focus mode on click\n if (focusMode) {\n focusMode = false;\n statusBar.setContent(' [↑↓/jk] select [Enter] focus [r] restart [A] restart All [x] kill [q] quit [?] help ');\n }\n if (clickedIndex !== selectedIndex) {\n saveScrollPosition();\n selectedIndex = clickedIndex;\n updateProcessList();\n updateOutput();\n }\n screen.render();\n });\n\n // Forward input in focus mode\n screen.on('keypress', (ch: string, key: { full: string; name?: string }) => {\n if (focusMode && ch) {\n // Don't forward Escape (used to exit focus) or Enter (handled separately)\n if (key.name === 'escape' || key.name === 'return' || key.name === 'enter') {\n return;\n }\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}","import { EventEmitter } from 'events';\nimport type { ProcessConfig } from './types';\n\ninterface PtyHandle {\n write(data: string): void;\n resize(cols: number, rows: number): void;\n kill(): void;\n}\n\nexport interface ManagedProcess {\n name: string;\n config: ProcessConfig;\n pty: PtyHandle | 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 const managed: ManagedProcess = {\n name,\n config,\n pty: null,\n status: 'running',\n output: [],\n exitCode: null,\n };\n\n this.processes.set(name, managed);\n\n try {\n const proc = Bun.spawn([shell, ...args], {\n cwd,\n env: env as Record<string, string>,\n terminal: {\n cols: 120,\n rows: 30,\n data: (_terminal: unknown, data: Uint8Array) => {\n const str = new TextDecoder().decode(data);\n managed.output.push(str);\n if (managed.output.length > this.maxOutputLines) {\n managed.output = managed.output.slice(-this.maxOutputLines);\n }\n this.emit('output', name, str);\n },\n },\n });\n\n managed.pty = {\n write: (data: string) => proc.terminal?.write(data),\n resize: (cols: number, rows: number) => proc.terminal?.resize(cols, rows),\n kill: () => proc.kill(),\n };\n\n // Handle exit\n proc.exited.then((exitCode) => {\n managed.status = exitCode === 0 ? 'stopped' : 'error';\n managed.exitCode = exitCode;\n managed.pty = null;\n this.emit('exit', name, exitCode);\n\n if (managed.config.autoRestart && exitCode !== 0) {\n setTimeout(() => this.start(name, managed.config), 1000);\n }\n });\n\n this.emit('started', name);\n } catch (error) {\n managed.status = 'error';\n managed.output = [`Error starting process: ${error}`];\n managed.exitCode = -1;\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"],"mappings":"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","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as pty from 'node-pty';
|
|
2
1
|
import { EventEmitter } from 'events';
|
|
3
2
|
|
|
4
3
|
interface ProcessConfig {
|
|
@@ -17,10 +16,15 @@ interface PanexConfig {
|
|
|
17
16
|
|
|
18
17
|
declare function createTUI(config: PanexConfig): Promise<void>;
|
|
19
18
|
|
|
19
|
+
interface PtyHandle {
|
|
20
|
+
write(data: string): void;
|
|
21
|
+
resize(cols: number, rows: number): void;
|
|
22
|
+
kill(): void;
|
|
23
|
+
}
|
|
20
24
|
interface ManagedProcess {
|
|
21
25
|
name: string;
|
|
22
26
|
config: ProcessConfig;
|
|
23
|
-
pty:
|
|
27
|
+
pty: PtyHandle | null;
|
|
24
28
|
status: 'running' | 'stopped' | 'error';
|
|
25
29
|
output: string[];
|
|
26
30
|
exitCode: number | null;
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import blessed from "blessed";
|
|
3
3
|
|
|
4
4
|
// src/process-manager.ts
|
|
5
|
-
import * as pty from "node-pty";
|
|
6
5
|
import { EventEmitter } from "events";
|
|
7
6
|
var ProcessManager = class extends EventEmitter {
|
|
8
7
|
constructor(procs) {
|
|
@@ -25,52 +24,51 @@ var ProcessManager = class extends EventEmitter {
|
|
|
25
24
|
const args = config.shell ? ["-c", config.shell] : config.cmd ? ["-c", config.cmd.join(" ")] : [];
|
|
26
25
|
const cwd = config.cwd ?? process.cwd();
|
|
27
26
|
const env = { ...process.env, ...config.env };
|
|
27
|
+
const managed = {
|
|
28
|
+
name,
|
|
29
|
+
config,
|
|
30
|
+
pty: null,
|
|
31
|
+
status: "running",
|
|
32
|
+
output: [],
|
|
33
|
+
exitCode: null
|
|
34
|
+
};
|
|
35
|
+
this.processes.set(name, managed);
|
|
28
36
|
try {
|
|
29
|
-
const
|
|
30
|
-
name: "xterm-256color",
|
|
31
|
-
cols: 120,
|
|
32
|
-
rows: 30,
|
|
37
|
+
const proc = Bun.spawn([shell, ...args], {
|
|
33
38
|
cwd,
|
|
34
|
-
env
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
managed.output.push(data);
|
|
47
|
-
if (managed.output.length > this.maxOutputLines) {
|
|
48
|
-
managed.output = managed.output.slice(-this.maxOutputLines);
|
|
39
|
+
env,
|
|
40
|
+
terminal: {
|
|
41
|
+
cols: 120,
|
|
42
|
+
rows: 30,
|
|
43
|
+
data: (_terminal, data) => {
|
|
44
|
+
const str = new TextDecoder().decode(data);
|
|
45
|
+
managed.output.push(str);
|
|
46
|
+
if (managed.output.length > this.maxOutputLines) {
|
|
47
|
+
managed.output = managed.output.slice(-this.maxOutputLines);
|
|
48
|
+
}
|
|
49
|
+
this.emit("output", name, str);
|
|
50
|
+
}
|
|
49
51
|
}
|
|
50
|
-
this.emit("output", name, data);
|
|
51
52
|
});
|
|
52
|
-
|
|
53
|
+
managed.pty = {
|
|
54
|
+
write: (data) => proc.terminal?.write(data),
|
|
55
|
+
resize: (cols, rows) => proc.terminal?.resize(cols, rows),
|
|
56
|
+
kill: () => proc.kill()
|
|
57
|
+
};
|
|
58
|
+
proc.exited.then((exitCode) => {
|
|
53
59
|
managed.status = exitCode === 0 ? "stopped" : "error";
|
|
54
60
|
managed.exitCode = exitCode;
|
|
55
61
|
managed.pty = null;
|
|
56
62
|
this.emit("exit", name, exitCode);
|
|
57
|
-
if (config.autoRestart && exitCode !== 0) {
|
|
58
|
-
setTimeout(() =>
|
|
59
|
-
this.start(name, config);
|
|
60
|
-
}, 1e3);
|
|
63
|
+
if (managed.config.autoRestart && exitCode !== 0) {
|
|
64
|
+
setTimeout(() => this.start(name, managed.config), 1e3);
|
|
61
65
|
}
|
|
62
66
|
});
|
|
63
67
|
this.emit("started", name);
|
|
64
68
|
} catch (error) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
pty: null,
|
|
69
|
-
status: "error",
|
|
70
|
-
output: [`Error starting process: ${error}`],
|
|
71
|
-
exitCode: -1
|
|
72
|
-
};
|
|
73
|
-
this.processes.set(name, managed);
|
|
69
|
+
managed.status = "error";
|
|
70
|
+
managed.output = [`Error starting process: ${error}`];
|
|
71
|
+
managed.exitCode = -1;
|
|
74
72
|
this.emit("error", name, error);
|
|
75
73
|
}
|
|
76
74
|
}
|
|
@@ -149,8 +147,6 @@ async function createTUI(config) {
|
|
|
149
147
|
selected: { bg: "blue", fg: "white" },
|
|
150
148
|
item: { fg: "white" }
|
|
151
149
|
},
|
|
152
|
-
keys: true,
|
|
153
|
-
vi: true,
|
|
154
150
|
mouse: config.settings?.mouse ?? true,
|
|
155
151
|
scrollbar: {
|
|
156
152
|
ch: "\u2502",
|
|
@@ -188,7 +184,7 @@ async function createTUI(config) {
|
|
|
188
184
|
bg: "blue",
|
|
189
185
|
fg: "white"
|
|
190
186
|
},
|
|
191
|
-
content: " [\u2191\u2193/jk] select [Enter] focus [r] restart [
|
|
187
|
+
content: " [\u2191\u2193/jk] select [Enter] focus [r] restart [A] restart All [x] kill [q] quit [?] help "
|
|
192
188
|
});
|
|
193
189
|
const helpBox = blessed.box({
|
|
194
190
|
parent: screen,
|
|
@@ -216,7 +212,7 @@ async function createTUI(config) {
|
|
|
216
212
|
Enter Focus process (interactive mode)
|
|
217
213
|
Esc Exit focus mode
|
|
218
214
|
r Restart selected process
|
|
219
|
-
|
|
215
|
+
A Restart all processes
|
|
220
216
|
x Kill selected process
|
|
221
217
|
|
|
222
218
|
General
|
|
@@ -229,6 +225,7 @@ async function createTUI(config) {
|
|
|
229
225
|
let selectedIndex = 0;
|
|
230
226
|
let focusMode = false;
|
|
231
227
|
const processNames = Object.keys(config.procs);
|
|
228
|
+
const scrollPositions = /* @__PURE__ */ new Map();
|
|
232
229
|
function updateProcessList() {
|
|
233
230
|
const items = processNames.map((name, i) => {
|
|
234
231
|
const proc = processManager.getProcess(name);
|
|
@@ -240,19 +237,30 @@ async function createTUI(config) {
|
|
|
240
237
|
processList.select(selectedIndex);
|
|
241
238
|
screen.render();
|
|
242
239
|
}
|
|
243
|
-
function updateOutput() {
|
|
240
|
+
function updateOutput(autoScroll = false) {
|
|
244
241
|
const name = processNames[selectedIndex];
|
|
245
242
|
if (name) {
|
|
246
243
|
outputBox.setLabel(` OUTPUT: ${name} `);
|
|
247
244
|
const output = processManager.getOutput(name);
|
|
248
245
|
outputBox.setContent(output);
|
|
249
|
-
|
|
246
|
+
if (autoScroll) {
|
|
247
|
+
outputBox.setScrollPerc(100);
|
|
248
|
+
} else {
|
|
249
|
+
const savedPos = scrollPositions.get(name) ?? 100;
|
|
250
|
+
outputBox.setScrollPerc(savedPos);
|
|
251
|
+
}
|
|
250
252
|
}
|
|
251
253
|
screen.render();
|
|
252
254
|
}
|
|
255
|
+
function saveScrollPosition() {
|
|
256
|
+
const name = processNames[selectedIndex];
|
|
257
|
+
if (name) {
|
|
258
|
+
scrollPositions.set(name, outputBox.getScrollPerc());
|
|
259
|
+
}
|
|
260
|
+
}
|
|
253
261
|
processManager.on("output", (name) => {
|
|
254
262
|
if (name === processNames[selectedIndex]) {
|
|
255
|
-
updateOutput();
|
|
263
|
+
updateOutput(true);
|
|
256
264
|
}
|
|
257
265
|
});
|
|
258
266
|
processManager.on("started", () => {
|
|
@@ -261,6 +269,12 @@ async function createTUI(config) {
|
|
|
261
269
|
processManager.on("exit", () => {
|
|
262
270
|
updateProcessList();
|
|
263
271
|
});
|
|
272
|
+
processManager.on("error", (name) => {
|
|
273
|
+
updateProcessList();
|
|
274
|
+
if (name === processNames[selectedIndex]) {
|
|
275
|
+
updateOutput();
|
|
276
|
+
}
|
|
277
|
+
});
|
|
264
278
|
screen.key(["q", "C-c"], () => {
|
|
265
279
|
processManager.killAll();
|
|
266
280
|
process.exit(0);
|
|
@@ -277,7 +291,7 @@ async function createTUI(config) {
|
|
|
277
291
|
}
|
|
278
292
|
if (focusMode) {
|
|
279
293
|
focusMode = false;
|
|
280
|
-
statusBar.setContent(" [\u2191\u2193/jk] select [Enter] focus [r] restart [
|
|
294
|
+
statusBar.setContent(" [\u2191\u2193/jk] select [Enter] focus [r] restart [A] restart All [x] kill [q] quit [?] help ");
|
|
281
295
|
screen.render();
|
|
282
296
|
}
|
|
283
297
|
});
|
|
@@ -287,15 +301,21 @@ async function createTUI(config) {
|
|
|
287
301
|
});
|
|
288
302
|
screen.key(["up", "k"], () => {
|
|
289
303
|
if (focusMode || !helpBox.hidden) return;
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
304
|
+
if (selectedIndex > 0) {
|
|
305
|
+
saveScrollPosition();
|
|
306
|
+
selectedIndex--;
|
|
307
|
+
updateProcessList();
|
|
308
|
+
updateOutput();
|
|
309
|
+
}
|
|
293
310
|
});
|
|
294
311
|
screen.key(["down", "j"], () => {
|
|
295
312
|
if (focusMode || !helpBox.hidden) return;
|
|
296
|
-
selectedIndex
|
|
297
|
-
|
|
298
|
-
|
|
313
|
+
if (selectedIndex < processNames.length - 1) {
|
|
314
|
+
saveScrollPosition();
|
|
315
|
+
selectedIndex++;
|
|
316
|
+
updateProcessList();
|
|
317
|
+
updateOutput();
|
|
318
|
+
}
|
|
299
319
|
});
|
|
300
320
|
screen.key(["enter"], () => {
|
|
301
321
|
if (!helpBox.hidden) {
|
|
@@ -303,14 +323,17 @@ async function createTUI(config) {
|
|
|
303
323
|
screen.render();
|
|
304
324
|
return;
|
|
305
325
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
326
|
+
if (!focusMode) {
|
|
327
|
+
focusMode = true;
|
|
328
|
+
const name = processNames[selectedIndex];
|
|
309
329
|
statusBar.setContent(` FOCUS: ${name} - Type to interact, [Esc] to exit focus mode `);
|
|
330
|
+
screen.render();
|
|
310
331
|
} else {
|
|
311
|
-
|
|
332
|
+
const name = processNames[selectedIndex];
|
|
333
|
+
if (name) {
|
|
334
|
+
processManager.write(name, "\r");
|
|
335
|
+
}
|
|
312
336
|
}
|
|
313
|
-
screen.render();
|
|
314
337
|
});
|
|
315
338
|
screen.key(["r"], () => {
|
|
316
339
|
if (focusMode || !helpBox.hidden) return;
|
|
@@ -319,7 +342,7 @@ async function createTUI(config) {
|
|
|
319
342
|
processManager.restart(name);
|
|
320
343
|
}
|
|
321
344
|
});
|
|
322
|
-
screen.key(["a"], () => {
|
|
345
|
+
screen.key(["S-a"], () => {
|
|
323
346
|
if (focusMode || !helpBox.hidden) return;
|
|
324
347
|
processManager.restartAll();
|
|
325
348
|
});
|
|
@@ -340,8 +363,37 @@ async function createTUI(config) {
|
|
|
340
363
|
outputBox.setScrollPerc(100);
|
|
341
364
|
screen.render();
|
|
342
365
|
});
|
|
366
|
+
outputBox.on("click", () => {
|
|
367
|
+
if (!helpBox.hidden) return;
|
|
368
|
+
if (!focusMode) {
|
|
369
|
+
focusMode = true;
|
|
370
|
+
const name = processNames[selectedIndex];
|
|
371
|
+
statusBar.setContent(` FOCUS: ${name} - Type to interact, [Esc] to exit focus mode `);
|
|
372
|
+
screen.render();
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
processList.on("element click", (_el, data) => {
|
|
376
|
+
if (!helpBox.hidden) return;
|
|
377
|
+
const absTop = processList.atop ?? 0;
|
|
378
|
+
const clickedIndex = data.y - absTop - 1;
|
|
379
|
+
if (clickedIndex < 0 || clickedIndex >= processNames.length) return;
|
|
380
|
+
if (focusMode) {
|
|
381
|
+
focusMode = false;
|
|
382
|
+
statusBar.setContent(" [\u2191\u2193/jk] select [Enter] focus [r] restart [A] restart All [x] kill [q] quit [?] help ");
|
|
383
|
+
}
|
|
384
|
+
if (clickedIndex !== selectedIndex) {
|
|
385
|
+
saveScrollPosition();
|
|
386
|
+
selectedIndex = clickedIndex;
|
|
387
|
+
updateProcessList();
|
|
388
|
+
updateOutput();
|
|
389
|
+
}
|
|
390
|
+
screen.render();
|
|
391
|
+
});
|
|
343
392
|
screen.on("keypress", (ch, key) => {
|
|
344
393
|
if (focusMode && ch) {
|
|
394
|
+
if (key.name === "escape" || key.name === "return" || key.name === "enter") {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
345
397
|
const name = processNames[selectedIndex];
|
|
346
398
|
if (name) {
|
|
347
399
|
processManager.write(name, ch);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tui.ts","../src/process-manager.ts"],"sourcesContent":["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}","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"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/tui.ts","../src/process-manager.ts"],"sourcesContent":["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 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 const scrollPositions = new Map<string, number>(); // Track scroll % per process\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(autoScroll = false) {\n const name = processNames[selectedIndex];\n if (name) {\n outputBox.setLabel(` OUTPUT: ${name} `);\n const output = processManager.getOutput(name);\n outputBox.setContent(output);\n if (autoScroll) {\n outputBox.setScrollPerc(100); // Scroll to bottom for new output\n } else {\n // Restore saved scroll position or default to bottom\n const savedPos = scrollPositions.get(name) ?? 100;\n outputBox.setScrollPerc(savedPos);\n }\n }\n screen.render();\n }\n\n // Save current scroll position before switching\n function saveScrollPosition() {\n const name = processNames[selectedIndex];\n if (name) {\n scrollPositions.set(name, outputBox.getScrollPerc());\n }\n }\n\n // Event handlers\n processManager.on('output', (name: string) => {\n if (name === processNames[selectedIndex]) {\n updateOutput(true); // Auto-scroll for new output\n }\n });\n\n processManager.on('started', () => {\n updateProcessList();\n });\n\n processManager.on('exit', () => {\n updateProcessList();\n });\n\n processManager.on('error', (name: string) => {\n updateProcessList();\n if (name === processNames[selectedIndex]) {\n updateOutput();\n }\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 if (selectedIndex > 0) {\n saveScrollPosition();\n selectedIndex--;\n updateProcessList();\n updateOutput();\n }\n });\n\n screen.key(['down', 'j'], () => {\n if (focusMode || !helpBox.hidden) return;\n if (selectedIndex < processNames.length - 1) {\n saveScrollPosition();\n selectedIndex++;\n updateProcessList();\n updateOutput();\n }\n });\n\n screen.key(['enter'], () => {\n if (!helpBox.hidden) {\n helpBox.hide();\n screen.render();\n return;\n }\n if (!focusMode) {\n // Enter focus mode (don't forward this Enter)\n focusMode = true;\n const name = processNames[selectedIndex];\n statusBar.setContent(` FOCUS: ${name} - Type to interact, [Esc] to exit focus mode `);\n screen.render();\n } else {\n // Already in focus mode - forward Enter to process\n const name = processNames[selectedIndex];\n if (name) {\n processManager.write(name, '\\r');\n }\n }\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(['S-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 // Mouse click on output box enables focus mode\n outputBox.on('click', () => {\n if (!helpBox.hidden) return;\n if (!focusMode) {\n focusMode = true;\n const name = processNames[selectedIndex];\n statusBar.setContent(` FOCUS: ${name} - Type to interact, [Esc] to exit focus mode `);\n screen.render();\n }\n });\n\n // Mouse click on process list (single click)\n processList.on('element click', (_el: blessed.Widgets.BlessedElement, data: { y: number }) => {\n if (!helpBox.hidden) return;\n\n // Calculate index: y is absolute, subtract list's absolute top and border\n const absTop = (processList.atop as number) ?? 0;\n const clickedIndex = data.y - absTop - 1; // -1 for border\n\n if (clickedIndex < 0 || clickedIndex >= processNames.length) return;\n\n // Exit focus mode on click\n if (focusMode) {\n focusMode = false;\n statusBar.setContent(' [↑↓/jk] select [Enter] focus [r] restart [A] restart All [x] kill [q] quit [?] help ');\n }\n if (clickedIndex !== selectedIndex) {\n saveScrollPosition();\n selectedIndex = clickedIndex;\n updateProcessList();\n updateOutput();\n }\n screen.render();\n });\n\n // Forward input in focus mode\n screen.on('keypress', (ch: string, key: { full: string; name?: string }) => {\n if (focusMode && ch) {\n // Don't forward Escape (used to exit focus) or Enter (handled separately)\n if (key.name === 'escape' || key.name === 'return' || key.name === 'enter') {\n return;\n }\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}","import { EventEmitter } from 'events';\nimport type { ProcessConfig } from './types';\n\ninterface PtyHandle {\n write(data: string): void;\n resize(cols: number, rows: number): void;\n kill(): void;\n}\n\nexport interface ManagedProcess {\n name: string;\n config: ProcessConfig;\n pty: PtyHandle | 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 const managed: ManagedProcess = {\n name,\n config,\n pty: null,\n status: 'running',\n output: [],\n exitCode: null,\n };\n\n this.processes.set(name, managed);\n\n try {\n const proc = Bun.spawn([shell, ...args], {\n cwd,\n env: env as Record<string, string>,\n terminal: {\n cols: 120,\n rows: 30,\n data: (_terminal: unknown, data: Uint8Array) => {\n const str = new TextDecoder().decode(data);\n managed.output.push(str);\n if (managed.output.length > this.maxOutputLines) {\n managed.output = managed.output.slice(-this.maxOutputLines);\n }\n this.emit('output', name, str);\n },\n },\n });\n\n managed.pty = {\n write: (data: string) => proc.terminal?.write(data),\n resize: (cols: number, rows: number) => proc.terminal?.resize(cols, rows),\n kill: () => proc.kill(),\n };\n\n // Handle exit\n proc.exited.then((exitCode) => {\n managed.status = exitCode === 0 ? 'stopped' : 'error';\n managed.exitCode = exitCode;\n managed.pty = null;\n this.emit('exit', name, exitCode);\n\n if (managed.config.autoRestart && exitCode !== 0) {\n setTimeout(() => this.start(name, managed.config), 1000);\n }\n });\n\n this.emit('started', name);\n } catch (error) {\n managed.status = 'error';\n managed.output = [`Error starting process: ${error}`];\n managed.exitCode = -1;\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"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "panex",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"description": "Terminal UI for running multiple processes in parallel",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -36,13 +36,12 @@
|
|
|
36
36
|
"author": "Anton Veretennikov (king8fisher)",
|
|
37
37
|
"license": "MIT",
|
|
38
38
|
"engines": {
|
|
39
|
-
"
|
|
39
|
+
"bun": ">=1.3.5"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"blessed": "^0.1.81",
|
|
43
43
|
"chalk": "^5.3.0",
|
|
44
|
-
"commander": "^12.1.0"
|
|
45
|
-
"node-pty": "^1.0.0"
|
|
44
|
+
"commander": "^12.1.0"
|
|
46
45
|
},
|
|
47
46
|
"devDependencies": {
|
|
48
47
|
"@types/blessed": "^0.1.25",
|