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 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
- ## Installation
24
+ ## Requirements
25
25
 
26
- ```bash
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
- # or run directly
31
- npx panex "npm run api" "npm run web"
28
+ ## Installation
32
29
 
33
- # bun
30
+ ```bash
31
+ # Run directly with bunx
34
32
  bunx panex "bun run api" "bun run web"
35
33
 
36
- # pnpm
37
- pnpm add -g panex
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 | 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 |
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 | panex | concurrently | mprocs | turbo |
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
- - node-pty (PTY support for interactive processes)
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 node
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 ptyProcess = pty.spawn(shell, args, {
35
- name: "xterm-256color",
36
- cols: 120,
37
- rows: 30,
42
+ const proc = Bun.spawn([shell, ...args], {
38
43
  cwd,
39
- env
40
- });
41
- const managed = {
42
- name,
43
- config,
44
- pty: ptyProcess,
45
- status: "running",
46
- output: [],
47
- exitCode: null
48
- };
49
- this.processes.set(name, managed);
50
- ptyProcess.onData((data) => {
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
- ptyProcess.onExit(({ exitCode }) => {
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
- const managed = {
71
- name,
72
- config,
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 [a] restart all [x] kill [q] quit [?] help "
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
- a Restart all processes
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
- outputBox.setScrollPerc(100);
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 [a] restart all [x] kill [q] quit [?] help ");
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
- selectedIndex = Math.max(0, selectedIndex - 1);
296
- updateProcessList();
297
- updateOutput();
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 = Math.min(processNames.length - 1, selectedIndex + 1);
302
- updateProcessList();
303
- updateOutput();
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
- focusMode = !focusMode;
312
- const name = processNames[selectedIndex];
313
- if (focusMode && name) {
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
- statusBar.setContent(" [\u2191\u2193/jk] select [Enter] focus [r] restart [a] restart all [x] kill [q] quit [?] help ");
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 names = options.names?.split(",") ?? commands.map((_, i) => `proc${i + 1}`);
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: pty.IPty | null;
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 ptyProcess = pty.spawn(shell, args, {
30
- name: "xterm-256color",
31
- cols: 120,
32
- rows: 30,
37
+ const proc = Bun.spawn([shell, ...args], {
33
38
  cwd,
34
- env
35
- });
36
- const managed = {
37
- name,
38
- config,
39
- pty: ptyProcess,
40
- status: "running",
41
- output: [],
42
- exitCode: null
43
- };
44
- this.processes.set(name, managed);
45
- ptyProcess.onData((data) => {
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
- ptyProcess.onExit(({ exitCode }) => {
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
- const managed = {
66
- name,
67
- config,
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 [a] restart all [x] kill [q] quit [?] help "
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
- a Restart all processes
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
- outputBox.setScrollPerc(100);
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 [a] restart all [x] kill [q] quit [?] help ");
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
- selectedIndex = Math.max(0, selectedIndex - 1);
291
- updateProcessList();
292
- updateOutput();
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 = Math.min(processNames.length - 1, selectedIndex + 1);
297
- updateProcessList();
298
- updateOutput();
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
- focusMode = !focusMode;
307
- const name = processNames[selectedIndex];
308
- if (focusMode && name) {
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
- statusBar.setContent(" [\u2191\u2193/jk] select [Enter] focus [r] restart [a] restart all [x] kill [q] quit [?] help ");
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.1",
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
- "node": ">=18"
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",