pnpm-dash 0.1.2 → 0.1.4

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/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ export function parseCLI() {
4
4
  program
5
5
  .name('pnpm-dash')
6
6
  .description('A TUI dashboard for pnpm workspaces - run scripts across packages with a split-pane interface')
7
- .version('0.1.2')
7
+ .version('0.1.3')
8
8
  .argument('<script>', 'Script name to run across workspace packages (e.g., dev, start)')
9
9
  .option('-F, --filter <pattern...>', 'Filter packages by name pattern, supports * for wildcard and ! for exclusions')
10
10
  .parse();
package/dist/runner.js CHANGED
@@ -1,4 +1,4 @@
1
- import { execa } from 'execa';
1
+ import { spawn } from 'node:child_process';
2
2
  import { EventEmitter } from 'node:events';
3
3
  import { RingBuffer } from './ringbuf.js';
4
4
  import { MAX_LOG_LINES } from './constants.js';
@@ -21,7 +21,6 @@ export class Runner extends EventEmitter {
21
21
  let state = this.states.get(pkg.name);
22
22
  if (state) {
23
23
  if (state.subprocess && state.subprocess.exitCode == null) {
24
- console.error(`${pkg.name} subprocess is still running`, state.subprocess.pid);
25
24
  return;
26
25
  }
27
26
  state.status = 'running';
@@ -35,21 +34,19 @@ export class Runner extends EventEmitter {
35
34
  };
36
35
  this.states.set(pkg.name, state);
37
36
  }
38
- const subprocess = execa('pnpm', ['run', this.scriptName], {
37
+ const cmd = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm';
38
+ const subprocess = spawn(cmd, ['run', this.scriptName], {
39
39
  cwd: pkg.path,
40
40
  env: {
41
41
  ...process.env,
42
42
  FORCE_COLOR: '1',
43
43
  },
44
- all: true,
45
- buffer: false,
46
- reject: false,
47
- cleanup: false,
44
+ stdio: ['ignore', 'pipe', 'pipe'],
48
45
  detached: true,
49
46
  });
50
47
  state.subprocess = subprocess;
51
48
  this.emit('start', pkg.name);
52
- subprocess.all.on('data', (data) => {
49
+ const handleData = (data) => {
53
50
  const lines = data.toString().split('\n');
54
51
  for (const line of lines) {
55
52
  if (line) {
@@ -57,15 +54,20 @@ export class Runner extends EventEmitter {
57
54
  this.emit('log', pkg.name, line);
58
55
  }
59
56
  }
60
- });
61
- subprocess.then((result) => {
62
- state.status = result.exitCode === 0 ? 'success' : 'error';
57
+ };
58
+ subprocess.stdout?.on('data', handleData);
59
+ subprocess.stderr?.on('data', handleData);
60
+ subprocess.on('close', (code) => {
61
+ state.status = code === 0 ? 'success' : 'error';
63
62
  state.subprocess = null;
64
- this.emit('exit', pkg.name, result.exitCode ?? null);
65
- }).catch((error) => {
63
+ this.emit('exit', pkg.name, code);
64
+ });
65
+ subprocess.on('error', (error) => {
66
66
  state.status = 'error';
67
- state.logs.push(`Error: ${error.message}`);
68
67
  state.subprocess = null;
68
+ const logLine = `Error: ${error.message}`;
69
+ state.logs.push(logLine);
70
+ this.emit('log', pkg.name, logLine);
69
71
  this.emit('error', pkg.name, error);
70
72
  });
71
73
  }
@@ -103,16 +105,16 @@ export class Runner extends EventEmitter {
103
105
  if (!subprocess?.pid)
104
106
  return;
105
107
  const pid = subprocess.pid;
106
- this.killProcessGroup(pid);
107
- const forceKillTimeout = setTimeout(() => {
108
- this.killProcessGroup(pid, true);
109
- }, 2000);
110
- try {
111
- await subprocess;
112
- }
113
- finally {
114
- clearTimeout(forceKillTimeout);
115
- }
108
+ return new Promise((resolve) => {
109
+ const forceKillTimeout = setTimeout(() => {
110
+ this.killProcessGroup(pid, true);
111
+ }, 2000);
112
+ subprocess.once('close', () => {
113
+ clearTimeout(forceKillTimeout);
114
+ resolve();
115
+ });
116
+ this.killProcessGroup(pid);
117
+ });
116
118
  }
117
119
  async stopAll() {
118
120
  await Promise.all(Array.from(this.states.keys()).map((name) => this.stopPackage(name)));
@@ -2,6 +2,7 @@ import blessed from 'reblessed';
2
2
  import { createSidebar, updateSidebarItems } from './sidebar.js';
3
3
  import { createLogView, updateLogView, appendLog, toggleLogAutoScroll, expandLogView, shrinkLogView } from './logview.js';
4
4
  import { createStatusBar, updateStatusBar } from './statusbar.js';
5
+ const RENDER_INTERVAL = 33;
5
6
  export class Dashboard {
6
7
  screen;
7
8
  sidebar;
@@ -10,6 +11,9 @@ export class Dashboard {
10
11
  runner;
11
12
  state;
12
13
  packageNames = [];
14
+ renderTimer = null;
15
+ needsRender = false;
16
+ pendingLogs = [];
13
17
  constructor(runner, packages) {
14
18
  this.runner = runner;
15
19
  this.packageNames = packages.map((p) => p.name);
@@ -62,20 +66,33 @@ export class Dashboard {
62
66
  setupRunnerEvents() {
63
67
  this.runner.on('start', (packageName) => {
64
68
  this.refreshSidebar();
65
- this.screen.render();
69
+ this.needsRender = true;
66
70
  });
67
71
  this.runner.on('log', (packageName, line) => {
68
- appendLog(this.logView, this.getSelectedPackageName(), packageName, line);
72
+ if (packageName === this.getSelectedPackageName()) {
73
+ this.pendingLogs.push(line);
74
+ this.needsRender = true;
75
+ }
69
76
  });
70
77
  this.runner.on('exit', (packageName, code) => {
71
78
  this.refreshSidebar();
72
- this.screen.render();
79
+ this.needsRender = true;
73
80
  });
74
81
  this.runner.on('error', (packageName, error) => {
75
82
  this.refreshSidebar();
76
- this.screen.render();
83
+ this.needsRender = true;
77
84
  });
78
85
  }
86
+ flushRender() {
87
+ if (this.pendingLogs.length > 0) {
88
+ appendLog(this.logView, this.pendingLogs);
89
+ this.pendingLogs = [];
90
+ }
91
+ if (this.needsRender) {
92
+ this.screen.render();
93
+ this.needsRender = false;
94
+ }
95
+ }
79
96
  getSelectedPackageName() {
80
97
  return this.packageNames[this.state.selectedIndex];
81
98
  }
@@ -92,6 +109,8 @@ export class Dashboard {
92
109
  }
93
110
  this.refreshSidebar();
94
111
  this.refreshLogView();
112
+ this.pendingLogs = [];
113
+ this.needsRender = true;
95
114
  }
96
115
  selectPrev() {
97
116
  if (this.state.selectedIndex > 0) {
@@ -102,12 +121,15 @@ export class Dashboard {
102
121
  }
103
122
  this.refreshSidebar();
104
123
  this.refreshLogView();
124
+ this.pendingLogs = [];
125
+ this.needsRender = true;
105
126
  }
106
127
  clearSelected() {
107
128
  const state = this.getSelectedState();
108
129
  if (state) {
109
130
  state.logs.clear();
110
131
  this.refreshLogView();
132
+ this.needsRender = true;
111
133
  }
112
134
  }
113
135
  stopSelected() {
@@ -128,8 +150,8 @@ export class Dashboard {
128
150
  toggleAutoScroll() {
129
151
  this.state.autoScroll = !this.state.autoScroll;
130
152
  toggleLogAutoScroll(this.logView, this.state.autoScroll);
131
- updateStatusBar(this.statusBar, this.state.autoScroll);
132
- this.screen.render();
153
+ this.refreshStatusBar();
154
+ this.needsRender = true;
133
155
  }
134
156
  toggleSidebar() {
135
157
  this.state.sidebarHidden = !this.state.sidebarHidden;
@@ -141,8 +163,7 @@ export class Dashboard {
141
163
  this.sidebar.show();
142
164
  shrinkLogView(this.logView);
143
165
  }
144
- updateStatusBar(this.statusBar, this.state.autoScroll);
145
- this.screen.render();
166
+ this.needsRender = true;
146
167
  }
147
168
  refreshSidebar() {
148
169
  updateSidebarItems(this.sidebar, this.state.packages, this.state.selectedIndex);
@@ -150,7 +171,14 @@ export class Dashboard {
150
171
  refreshLogView() {
151
172
  updateLogView(this.logView, this.getSelectedState());
152
173
  }
174
+ refreshStatusBar() {
175
+ updateStatusBar(this.statusBar, this.state.autoScroll);
176
+ }
153
177
  async quit() {
178
+ if (this.renderTimer) {
179
+ clearInterval(this.renderTimer);
180
+ this.renderTimer = null;
181
+ }
154
182
  await this.runner.stopAll();
155
183
  this.screen.destroy();
156
184
  process.exit(0);
@@ -158,7 +186,9 @@ export class Dashboard {
158
186
  start() {
159
187
  this.refreshSidebar();
160
188
  this.refreshLogView();
189
+ this.refreshStatusBar();
161
190
  this.logView.focus();
162
191
  this.screen.render();
192
+ this.renderTimer = setInterval(() => this.flushRender(), RENDER_INTERVAL);
163
193
  }
164
194
  }
@@ -39,11 +39,8 @@ export function updateLogView(logView, state) {
39
39
  logView.setContent(state.logs.toArray().join('\n'));
40
40
  logView.setScroll(0);
41
41
  }
42
- export function appendLog(logView, currentPackage, packageName, line) {
43
- if (currentPackage !== packageName) {
44
- return;
45
- }
46
- logView.add(line);
42
+ export function appendLog(logView, lines) {
43
+ logView.add(lines.join("\n"));
47
44
  }
48
45
  export function toggleLogAutoScroll(logView, autoScroll) {
49
46
  logView.scrollOnInput = autoScroll;
@@ -12,7 +12,6 @@ export function createStatusBar(screen) {
12
12
  },
13
13
  tags: true,
14
14
  });
15
- updateStatusBar(statusBar, true);
16
15
  return statusBar;
17
16
  }
18
17
  export function updateStatusBar(statusBar, autoScroll) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pnpm-dash",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "A TUI dashboard for pnpm workspaces - run scripts across packages with a split-pane interface",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -25,11 +25,14 @@
25
25
  ],
26
26
  "author": "artygus",
27
27
  "license": "Unlicense",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/artygus/pnpm-dash.git"
31
+ },
28
32
  "dependencies": {
29
33
  "@pnpm/find-workspace-dir": "^7.0.2",
30
34
  "@pnpm/workspace.find-packages": "^4.0.5",
31
35
  "commander": "^13.1.0",
32
- "execa": "^9.6.1",
33
36
  "reblessed": "^0.2.1"
34
37
  },
35
38
  "devDependencies": {