buddy-workbench 0.1.53 → 0.1.55

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/bin/devbuddy.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { execFileSync, execSync, spawn } from 'node:child_process';
3
+ import { execFileSync, spawn } from 'node:child_process';
4
4
  import { existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
5
5
  import { homedir } from 'node:os';
6
6
  import { dirname, join } from 'node:path';
@@ -46,10 +46,24 @@ function packageDetails() {
46
46
  return JSON.parse(readFileSync(packageJsonFile, 'utf8'));
47
47
  }
48
48
 
49
+ function version() {
50
+ console.log(packageDetails().version);
51
+ }
52
+
49
53
  function npmCommand() {
50
54
  return process.platform === 'win32' ? 'npm.cmd' : 'npm';
51
55
  }
52
56
 
57
+ function npmExecOptions(options = {}) {
58
+ return {
59
+ ...options,
60
+ // npm is a .cmd shim on Windows and must be launched through cmd.exe.
61
+ // Keep that helper shell hidden so self-update does not flash a window.
62
+ shell: process.platform === 'win32',
63
+ windowsHide: process.platform === 'win32'
64
+ };
65
+ }
66
+
53
67
  function parseUpdateVersion(args) {
54
68
  if (args.length > 1) throw new Error('Usage: devbuddy self-update [version]');
55
69
  if (!args[0]) return 'latest';
@@ -61,13 +75,14 @@ function parseUpdateVersion(args) {
61
75
 
62
76
  function latestPublishedVersion(packageName) {
63
77
  try {
64
- return execFileSync(npmCommand(), ['view', packageName, 'version'], {
78
+ return execFileSync(npmCommand(), ['view', packageName, 'version'], npmExecOptions({
65
79
  cwd: root,
66
80
  encoding: 'utf8',
67
81
  stdio: ['ignore', 'pipe', 'pipe']
68
- }).trim();
69
- } catch {
70
- throw new Error(`Unable to query the latest ${packageName} version from npm.`);
82
+ })).trim();
83
+ } catch (error) {
84
+ const details = String(error?.stderr || '').trim();
85
+ throw new Error(`Unable to query the latest ${packageName} version from npm.${details ? ` ${details}` : ''}`);
71
86
  }
72
87
  }
73
88
 
@@ -84,12 +99,13 @@ function selfUpdate(args) {
84
99
 
85
100
  console.log(`Updating ${details.name} from ${currentVersion} to ${version}…`);
86
101
  try {
87
- execFileSync(npmCommand(), ['install', '--global', `${details.name}@${version}`], {
102
+ execFileSync(npmCommand(), ['install', '--global', `${details.name}@${version}`], npmExecOptions({
88
103
  cwd: root,
89
104
  stdio: 'inherit'
90
- });
91
- } catch {
92
- throw new Error(`Unable to install ${details.name}@${version}. Check npm permissions and network access.`);
105
+ }));
106
+ } catch (error) {
107
+ const detailsText = String(error?.stderr || '').trim();
108
+ throw new Error(`Unable to install ${details.name}@${version}. ${detailsText || 'Check npm permissions and network access.'}`);
93
109
  }
94
110
  console.log(`Updated ${details.name} to ${targetVersion}.`);
95
111
  }
@@ -129,7 +145,10 @@ function sendSignal(pid, signal) {
129
145
  return true;
130
146
  } catch (err2) {
131
147
  try {
132
- execSync(`kill -${signal === 'SIGTERM' ? '15' : '9'} ${pid}`);
148
+ execFileSync('kill', [`-${signal === 'SIGTERM' ? '15' : '9'}`, String(pid)], {
149
+ stdio: 'ignore',
150
+ windowsHide: process.platform === 'win32'
151
+ });
133
152
  return true;
134
153
  } catch {
135
154
  return false;
@@ -240,6 +259,7 @@ Commands:
240
259
  help Display this help message
241
260
 
242
261
  Options:
262
+ -v, --version Display the installed DevBuddy version
243
263
  --port <number> Port to listen on (default: 3100)
244
264
 
245
265
  Examples:
@@ -252,6 +272,11 @@ async function main() {
252
272
  const args = process.argv.slice(3);
253
273
 
254
274
  switch (command.toLowerCase()) {
275
+ case '-v':
276
+ case '--version':
277
+ if (args.length) throw new Error('Usage: devbuddy --version');
278
+ version();
279
+ break;
255
280
  case 'start':
256
281
  await start(parsePort(args));
257
282
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buddy-workbench",
3
- "version": "0.1.53",
3
+ "version": "0.1.55",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,9 +12,9 @@ async function portPids(port) {
12
12
  export async function freePort(port) {
13
13
  try {
14
14
  if (process.platform === 'win32') {
15
- const { stdout } = await execFileAsync('netstat', ['-ano', '-p', 'tcp']);
15
+ const { stdout } = await execFileAsync('netstat.exe', ['-ano', '-p', 'tcp'], { windowsHide: true });
16
16
  const pids = [...stdout.matchAll(new RegExp(`:${port}\\s+.*?LISTENING\\s+(\\d+)`, 'g'))].map((match) => match[1]);
17
- await Promise.all([...new Set(pids)].map((pid) => execFileAsync('taskkill', ['/F', '/PID', pid])));
17
+ await Promise.all([...new Set(pids)].map((pid) => execFileAsync('taskkill.exe', ['/F', '/PID', pid], { windowsHide: true })));
18
18
  } else {
19
19
  const pids = await portPids(port);
20
20
  await Promise.all(pids.map((pid) => execFileAsync('kill', ['-TERM', pid])));
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import os from 'node:os';
4
4
  import crypto from 'node:crypto';
5
- import { execSync } from 'node:child_process';
5
+ import { execFileSync } from 'node:child_process';
6
6
 
7
7
  /**
8
8
  * Resolves standard bookmark file paths for Chrome and Edge based on OS.
@@ -37,11 +37,15 @@ export function checkBrowserRunning(browserName) {
37
37
  try {
38
38
  if (platform === 'win32') {
39
39
  const exeName = browserName === 'chrome' ? 'chrome.exe' : 'msedge.exe';
40
- const output = execSync(`tasklist /FI "IMAGENAME eq ${exeName}"`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
40
+ const output = execFileSync('tasklist.exe', ['/FI', `IMAGENAME eq ${exeName}`], {
41
+ encoding: 'utf8',
42
+ stdio: ['ignore', 'pipe', 'ignore'],
43
+ windowsHide: true
44
+ });
41
45
  return output.toLowerCase().includes(exeName.toLowerCase());
42
46
  } else {
43
47
  const processPattern = browserName === 'chrome' ? 'Google Chrome' : 'Microsoft Edge';
44
- const output = execSync(`pgrep -f "${processPattern}"`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
48
+ const output = execFileSync('pgrep', ['-f', processPattern], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
45
49
  return output.trim().length > 0;
46
50
  }
47
51
  } catch {
@@ -24,7 +24,7 @@ export function openInSystemBrowser(url) {
24
24
  safari: 'msedge'
25
25
  };
26
26
  const cmd = winCmdMap[browser] || 'chrome';
27
- execFile('cmd', ['/c', 'start', '', cmd, url]);
27
+ execFile('cmd.exe', ['/c', 'start', '', cmd, url], { windowsHide: true });
28
28
  } else {
29
29
  execFile('xdg-open', [url]);
30
30
  }
@@ -82,11 +82,11 @@ export function openInSystemEditor(path, location = '') {
82
82
  }
83
83
  } else if (process.platform === 'win32') {
84
84
  if (editor === 'vscode') {
85
- execFile('cmd', ['/c', 'code', '-n', path]);
85
+ execFile('cmd.exe', ['/c', 'code', '-n', path], { windowsHide: true });
86
86
  } else if (editor === 'idea') {
87
- execFile('cmd', ['/c', 'idea', path]);
87
+ execFile('cmd.exe', ['/c', 'idea', path], { windowsHide: true });
88
88
  } else {
89
- execFile('cmd', ['/c', 'start', '', path]);
89
+ execFile('cmd.exe', ['/c', 'start', '', path], { windowsHide: true });
90
90
  }
91
91
  } else {
92
92
  if (editor === 'vscode') {
@@ -102,7 +102,7 @@ export function openInSystemFolder(targetPath) {
102
102
  if (process.platform === 'darwin') {
103
103
  execFile('open', [targetPath]);
104
104
  } else if (process.platform === 'win32') {
105
- execFile('cmd', ['/c', 'start', '', targetPath]);
105
+ execFile('cmd.exe', ['/c', 'start', '', targetPath], { windowsHide: true });
106
106
  } else {
107
107
  execFile('xdg-open', [targetPath]);
108
108
  }
@@ -126,7 +126,7 @@ export async function openInSystemTerminal(targetPath) {
126
126
  }
127
127
 
128
128
  if (process.platform === 'win32') {
129
- await execFileAsync('powershell.exe', ['-NoProfile', '-Command', 'Start-Process', 'cmd.exe', '-WorkingDirectory', targetPath]);
129
+ await execFileAsync('powershell.exe', ['-NoProfile', '-WindowStyle', 'Hidden', '-Command', 'Start-Process', 'cmd.exe', '-WorkingDirectory', targetPath], { windowsHide: true });
130
130
  return;
131
131
  }
132
132
 
@@ -143,7 +143,7 @@ async function getClipboardText() {
143
143
  const commands = process.platform === 'darwin'
144
144
  ? [['pbpaste', []]]
145
145
  : process.platform === 'win32'
146
- ? [['powershell.exe', ['-NoProfile', '-Command', 'Get-Clipboard -Format Text -Raw']]]
146
+ ? [['powershell.exe', ['-NoProfile', '-WindowStyle', 'Hidden', '-Command', 'Get-Clipboard -Format Text -Raw']]]
147
147
  : [
148
148
  ['wl-paste', ['--no-newline', '--type', 'text']],
149
149
  ['xclip', ['-selection', 'clipboard', '-o', '-t', 'UTF8_STRING']],
@@ -32,7 +32,8 @@ function getEnrichedEnv() {
32
32
  } catch {}
33
33
  }
34
34
  const currentPath = process.env.PATH || '';
35
- const mergedPath = [...new Set([...extraPaths, ...currentPath.split(':')])].filter(Boolean).join(':');
35
+ const pathSeparator = process.platform === 'win32' ? ';' : ':';
36
+ const mergedPath = [...new Set([...extraPaths, ...currentPath.split(pathSeparator)])].filter(Boolean).join(pathSeparator);
36
37
  return { ...process.env, PATH: mergedPath };
37
38
  }
38
39
 
@@ -56,8 +57,17 @@ function parseJsonOutput(text) {
56
57
  }
57
58
 
58
59
  async function run(command, args, timeout = 5000) {
60
+ const executable = process.platform === 'win32'
61
+ ? ({ npm: 'npm.cmd', node: 'node.exe', git: 'git.exe' }[command] || command)
62
+ : command;
59
63
  try {
60
- const { stdout } = await execFileAsync(command, args, { timeout, maxBuffer: 8 * 1024 * 1024, env: getEnrichedEnv() });
64
+ const { stdout } = await execFileAsync(executable, args, {
65
+ timeout,
66
+ maxBuffer: 8 * 1024 * 1024,
67
+ env: getEnrichedEnv(),
68
+ shell: process.platform === 'win32' && executable.endsWith('.cmd'),
69
+ windowsHide: process.platform === 'win32'
70
+ });
61
71
  return stdout.trim();
62
72
  } catch (error) {
63
73
  if (['node', 'npm'].includes(command)) {
@@ -71,9 +81,12 @@ async function run(command, args, timeout = 5000) {
71
81
  }
72
82
 
73
83
  async function runNvm(script, timeout = 5000) {
84
+ if (process.platform === 'win32') {
85
+ throw new Error('Unix NVM shell is not supported on Windows.');
86
+ }
74
87
  for (const shell of ['zsh', 'bash']) {
75
88
  try {
76
- const { stdout } = await execFileAsync(shell, ['-ilc', `${NVM_LOAD}${script}`], { timeout, maxBuffer: 4 * 1024 * 1024, env: getEnrichedEnv() });
89
+ const { stdout } = await execFileAsync(shell, ['-ilc', `${NVM_LOAD}${script}`], { timeout, maxBuffer: 4 * 1024 * 1024, env: getEnrichedEnv(), windowsHide: false });
77
90
  return stdout.trim();
78
91
  } catch {}
79
92
  }
@@ -267,4 +280,3 @@ export async function installNvmVersion(version) {
267
280
  await runNvm(`nvm install ${shellQuote(target)}`, 10 * 60 * 1000);
268
281
  return readNvmConfiguration();
269
282
  }
270
-
@@ -25,7 +25,7 @@ export async function selectDirectory() {
25
25
  Write-Output $dialog.SelectedPath
26
26
  }
27
27
  `;
28
- const { stdout } = await execFileAsync('powershell', ['-NoProfile', '-Command', psScript]);
28
+ const { stdout } = await execFileAsync('powershell.exe', ['-NoProfile', '-WindowStyle', 'Hidden', '-Command', psScript], { windowsHide: true });
29
29
  const dirPath = stdout.trim();
30
30
  return dirPath ? { path: dirPath, canceled: false } : { path: null, canceled: true };
31
31
  } catch {
@@ -64,7 +64,7 @@ export async function selectFile() {
64
64
  Write-Output $dialog.FileName
65
65
  }
66
66
  `;
67
- const { stdout } = await execFileAsync('powershell', ['-NoProfile', '-Command', psScript]);
67
+ const { stdout } = await execFileAsync('powershell.exe', ['-NoProfile', '-WindowStyle', 'Hidden', '-Command', psScript], { windowsHide: true });
68
68
  const filePath = stdout.trim();
69
69
  return filePath ? { path: filePath, canceled: false } : { path: null, canceled: true };
70
70
  } catch {
@@ -114,7 +114,7 @@ export function clearScriptLogs(launcherId, scriptId) { logs.set(keyFor(launcher
114
114
  async function listeningProcesses() {
115
115
  if (process.platform === 'win32') {
116
116
  try {
117
- const { stdout } = await execFileAsync('netstat', ['-ano', '-p', 'tcp']);
117
+ const { stdout } = await execFileAsync('netstat.exe', ['-ano', '-p', 'tcp'], { windowsHide: true });
118
118
  const rows = [];
119
119
  const matches = stdout.matchAll(/TCP\s+(?:\[::\]|[\d.]+):(\d+)\s+.*?LISTENING\s+(\d+)/gi);
120
120
  for (const m of matches) {
@@ -151,6 +151,9 @@ async function listeningProcesses() {
151
151
  }
152
152
 
153
153
  async function processGroupFor(pid) {
154
+ // Windows has no Unix process-group ID. Do not attempt to invoke `ps` here;
155
+ // on some installations that resolves through a visible shell window.
156
+ if (process.platform === 'win32') return null;
154
157
  try {
155
158
  const { stdout } = await execFileAsync('ps', ['-o', 'pgid=', '-p', String(pid)]);
156
159
  return Number(stdout.trim());
@@ -253,7 +256,7 @@ export function runScript(launcher, script) {
253
256
  if (running.has(key)) throw new Error('This script is already running.');
254
257
  if (!existsSync(launcher.folder)) throw new Error('The configured project folder does not exist.');
255
258
  logs.set(key, { output: `$ ${script.command}\n`, error: '', errorCount: 0 });
256
- const child = spawn(process.execPath, [supervisorPath], { cwd: launcher.folder, detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, ...(launcher.executor === 'pnpm' || /^\s*pnpm\b/.test(script.command) ? { CI: 'true' } : {}), BUDDY_PARENT_PID: String(process.pid), BUDDY_SCRIPT_COMMAND: script.command } });
259
+ const child = spawn(process.execPath, [supervisorPath], { cwd: launcher.folder, detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe'], windowsHide: process.platform === 'win32', env: { ...process.env, ...(launcher.executor === 'pnpm' || /^\s*pnpm\b/.test(script.command) ? { CI: 'true' } : {}), BUDDY_PARENT_PID: String(process.pid), BUDDY_SCRIPT_COMMAND: script.command } });
257
260
  child.stdout.on('data', (chunk) => appendLog(key, 'output', chunk.toString()));
258
261
  child.stderr.on('data', (chunk) => appendLog(key, 'error', chunk.toString()));
259
262
  running.set(key, child);
@@ -4,7 +4,12 @@ import { promisify } from 'node:util';
4
4
  const execFileAsync = promisify(execFile);
5
5
  const parentPid = Number(process.env.BUDDY_PARENT_PID);
6
6
  const command = process.env.BUDDY_SCRIPT_COMMAND;
7
- const child = spawn(command, { cwd: process.cwd(), shell: true, stdio: 'inherit' });
7
+ const child = spawn(command, {
8
+ cwd: process.cwd(),
9
+ shell: true,
10
+ stdio: 'inherit',
11
+ windowsHide: process.platform === 'win32'
12
+ });
8
13
 
9
14
  function stopGroup() {
10
15
  if (process.platform === 'win32') { child.kill('SIGTERM'); return; }