termbeam 1.21.0 → 1.21.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.21.1] - 2026-04-21
4
+
5
+ - fix(windows): service wizard uses getDefaultShell instead of COMSPEC
6
+ - fix(windows): use tasklist fallback when wmic unavailable for shell detection
7
+ - fix(windows): add windowsHide to all child_process calls
8
+ - fix(windows): hide devtunnel console windows and fix DEP0190 deprecation
9
+
3
10
  ## [1.21.0] - 2026-04-20
4
11
 
5
12
  - feat(review): add Review Mode to diff viewer + SessionsHub filter chips (#195)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termbeam",
3
- "version": "1.21.0",
3
+ "version": "1.21.1",
4
4
  "description": "Beam your terminal to any device — mobile-optimized web terminal with multi-session support",
5
5
  "main": "src/server/index.js",
6
6
  "bin": {
package/src/cli/index.js CHANGED
@@ -75,48 +75,66 @@ function getWindowsAncestors(startPid, maxDepth = 4) {
75
75
  const safePid = parseInt(startPid, 10);
76
76
  if (!Number.isFinite(safePid) || safePid <= 0) return names;
77
77
 
78
+ // Strategy 1: wmic (available on older Windows builds)
78
79
  try {
79
80
  const result = execFileSync(
80
81
  'wmic',
81
82
  ['process', 'get', 'Name,ParentProcessId,ProcessId', '/format:csv'],
82
- { stdio: ['pipe', 'pipe', 'ignore'], encoding: 'utf8', timeout: 5000 },
83
+ { stdio: ['pipe', 'pipe', 'ignore'], encoding: 'utf8', timeout: 5000, windowsHide: true },
83
84
  );
84
85
 
85
- // Parse CSV output — first non-empty line is the header
86
86
  const lines = result.split(/\r?\n/).filter((l) => l.trim());
87
- if (lines.length === 0) return names;
88
-
89
- const header = lines[0].split(',').map((h) => h.trim());
90
- const nameIdx = header.indexOf('Name');
91
- const pidIdx = header.indexOf('ProcessId');
92
- const ppidIdx = header.indexOf('ParentProcessId');
93
- if (nameIdx === -1 || pidIdx === -1 || ppidIdx === -1) return names;
94
-
95
- const processes = new Map();
96
- for (let i = 1; i < lines.length; i++) {
97
- const cols = lines[i].split(',');
98
- if (cols.length <= Math.max(nameIdx, pidIdx, ppidIdx)) continue;
99
- const pid = parseInt(cols[pidIdx], 10);
100
- if (Number.isFinite(pid)) {
101
- processes.set(pid, {
102
- name: cols[nameIdx].trim().toLowerCase(),
103
- ppid: parseInt(cols[ppidIdx], 10),
104
- });
87
+ if (lines.length > 0) {
88
+ const header = lines[0].split(',').map((h) => h.trim());
89
+ const nameIdx = header.indexOf('Name');
90
+ const pidIdx = header.indexOf('ProcessId');
91
+ const ppidIdx = header.indexOf('ParentProcessId');
92
+ if (nameIdx !== -1 && pidIdx !== -1 && ppidIdx !== -1) {
93
+ const processes = new Map();
94
+ for (let i = 1; i < lines.length; i++) {
95
+ const cols = lines[i].split(',');
96
+ if (cols.length <= Math.max(nameIdx, pidIdx, ppidIdx)) continue;
97
+ const pid = parseInt(cols[pidIdx], 10);
98
+ if (Number.isFinite(pid)) {
99
+ processes.set(pid, {
100
+ name: cols[nameIdx].trim().toLowerCase(),
101
+ ppid: parseInt(cols[ppidIdx], 10),
102
+ });
103
+ }
104
+ }
105
+
106
+ let currentPid = safePid;
107
+ for (let i = 0; i < maxDepth; i++) {
108
+ const proc = processes.get(currentPid);
109
+ if (!proc) break;
110
+ log.debug(`Process tree: ${proc.name}`);
111
+ names.push(proc.name);
112
+ if (!Number.isFinite(proc.ppid) || proc.ppid === 0 || proc.ppid === currentPid) break;
113
+ currentPid = proc.ppid;
114
+ }
115
+ if (names.length > 0) return names;
105
116
  }
106
117
  }
118
+ } catch (err) {
119
+ log.debug(`wmic not available: ${err.message}`);
120
+ }
107
121
 
108
- // Walk up the tree in memory no more subprocess calls
109
- let currentPid = safePid;
110
- for (let i = 0; i < maxDepth; i++) {
111
- const proc = processes.get(currentPid);
112
- if (!proc) break;
113
- log.debug(`Process tree: ${proc.name}`);
114
- names.push(proc.name);
115
- if (!Number.isFinite(proc.ppid) || proc.ppid === 0 || proc.ppid === currentPid) break;
116
- currentPid = proc.ppid;
122
+ // Strategy 2: tasklist for direct parent name (always available on Windows)
123
+ try {
124
+ const result = execFileSync('tasklist', ['/FI', `PID eq ${safePid}`, '/FO', 'CSV', '/NH'], {
125
+ stdio: ['pipe', 'pipe', 'ignore'],
126
+ encoding: 'utf8',
127
+ timeout: 5000,
128
+ windowsHide: true,
129
+ });
130
+ const match = result.match(/"([^"]+)"/);
131
+ if (match) {
132
+ const name = match[1].toLowerCase();
133
+ log.debug(`Process tree (tasklist): ${name}`);
134
+ names.push(name);
117
135
  }
118
136
  } catch (err) {
119
- log.debug(`Could not query process tree: ${err.message}`);
137
+ log.debug(`tasklist failed: ${err.message}`);
120
138
  }
121
139
 
122
140
  return names;
@@ -188,6 +206,19 @@ function getDefaultShell() {
188
206
  log.debug(`Using detected shell: cmd.exe`);
189
207
  return 'cmd.exe';
190
208
  }
209
+
210
+ // Heuristic: PSModulePath env var is set by PowerShell and inherited by children.
211
+ // When the tree walk only found intermediaries (node.exe), this detects the real shell.
212
+ const psModulePath = (process.env.PSModulePath || '').toLowerCase();
213
+ if (psModulePath.includes('\\powershell\\')) {
214
+ log.debug('Detected pwsh.exe via PSModulePath');
215
+ return 'pwsh.exe';
216
+ }
217
+ if (psModulePath.includes('\\windowspowershell\\')) {
218
+ log.debug('Detected powershell.exe via PSModulePath');
219
+ return 'powershell.exe';
220
+ }
221
+
191
222
  const fallback = process.env.COMSPEC || 'cmd.exe';
192
223
  log.debug(`Falling back to: ${fallback}`);
193
224
  return fallback;
@@ -384,4 +415,4 @@ function parseArgs() {
384
415
  return config;
385
416
  }
386
417
 
387
- module.exports = { parseArgs, printHelp, isKnownShell, getWindowsAncestors };
418
+ module.exports = { parseArgs, printHelp, isKnownShell, getWindowsAncestors, getDefaultShell };
@@ -32,6 +32,7 @@ function findPm2() {
32
32
  encoding: 'utf8',
33
33
  stdio: ['pipe', 'pipe', 'ignore'],
34
34
  timeout: 5000,
35
+ windowsHide: true,
35
36
  });
36
37
  return result.trim().split('\n')[0].trim();
37
38
  } catch {
@@ -43,10 +44,13 @@ function installPm2Global() {
43
44
  log.info('Installing PM2 globally');
44
45
  console.log(yellow('\nInstalling PM2 globally...'));
45
46
  try {
46
- execFileSync('npm', ['install', '-g', 'pm2'], {
47
+ const isWin = os.platform() === 'win32';
48
+ const cmd = isWin ? process.env.ComSpec || 'cmd.exe' : 'npm';
49
+ const cmdArgs = isWin ? ['/c', 'npm', 'install', '-g', 'pm2'] : ['install', '-g', 'pm2'];
50
+ execFileSync(cmd, cmdArgs, {
47
51
  stdio: 'inherit',
48
52
  timeout: 120000,
49
- shell: os.platform() === 'win32',
53
+ windowsHide: true,
50
54
  });
51
55
  console.log(green('✓ PM2 installed successfully.\n'));
52
56
  return true;
@@ -141,13 +145,17 @@ function readEcosystemName() {
141
145
 
142
146
  function pm2Exec(args, opts = {}) {
143
147
  log.debug(`PM2 command: pm2 ${args.join(' ')}`);
148
+ const isWin = os.platform() === 'win32';
149
+ // Windows npm globals are .cmd wrappers — use cmd.exe /c to resolve them
150
+ // without shell:true (which triggers DEP0190 when combined with args).
151
+ const cmd = isWin ? process.env.ComSpec || 'cmd.exe' : 'pm2';
152
+ const cmdArgs = isWin ? ['/c', 'pm2', ...args] : args;
144
153
  try {
145
- return execFileSync('pm2', args, {
154
+ return execFileSync(cmd, cmdArgs, {
146
155
  encoding: 'utf8',
147
156
  stdio: opts.inherit ? 'inherit' : ['pipe', 'pipe', 'pipe'],
148
157
  timeout: 30000,
149
- // Windows npm globals are .cmd wrappers; execFileSync needs shell to resolve them
150
- shell: os.platform() === 'win32',
158
+ windowsHide: true,
151
159
  ...opts,
152
160
  });
153
161
  } catch (err) {
@@ -347,8 +355,9 @@ async function actionInstall() {
347
355
  config.cwd = await ask(rl, 'Working directory:', process.cwd());
348
356
  decisions.push({ label: 'Directory', value: config.cwd });
349
357
 
350
- // Shell — use current shell automatically
351
- config.shell = process.env.SHELL || (os.platform() === 'win32' ? process.env.COMSPEC : '/bin/sh');
358
+ // Shell — detect the actual shell the user is running from
359
+ const { getDefaultShell } = require('./index');
360
+ config.shell = getDefaultShell();
352
361
  decisions.push({ label: 'Shell', value: config.shell });
353
362
 
354
363
  // Log level
@@ -665,9 +674,13 @@ function actionLogs() {
665
674
  process.exit(1);
666
675
  }
667
676
  const { spawn } = require('child_process');
668
- const child = spawn('pm2', ['logs', readEcosystemName(), '--lines', '200'], {
677
+ const isWin = os.platform() === 'win32';
678
+ const cmd = isWin ? process.env.ComSpec || 'cmd.exe' : 'pm2';
679
+ const logsArgs = ['logs', readEcosystemName(), '--lines', '200'];
680
+ const cmdArgs = isWin ? ['/c', 'pm2', ...logsArgs] : logsArgs;
681
+ const child = spawn(cmd, cmdArgs, {
669
682
  stdio: 'inherit',
670
- shell: os.platform() === 'win32',
683
+ windowsHide: true,
671
684
  });
672
685
  child.on('error', (err) => {
673
686
  console.error(red(`✗ Failed to stream logs: ${err.message}`));
@@ -106,6 +106,7 @@ function isLoggedIn() {
106
106
  encoding: 'utf-8',
107
107
  stdio: ['pipe', 'pipe', 'pipe'],
108
108
  timeout: 10_000,
109
+ windowsHide: true,
109
110
  });
110
111
  return out && !out.toLowerCase().includes('not logged in');
111
112
  } catch {
@@ -119,6 +120,7 @@ function getLoginInfo() {
119
120
  encoding: 'utf-8',
120
121
  stdio: ['pipe', 'pipe', 'pipe'],
121
122
  timeout: 10_000,
123
+ windowsHide: true,
122
124
  });
123
125
  return parseLoginInfo(out);
124
126
  } catch {
@@ -148,6 +150,7 @@ function deviceCodeLogin(cmd) {
148
150
  return new Promise((resolve, reject) => {
149
151
  const proc = spawn(cmd, ['user', 'login', '-e', '-d'], {
150
152
  stdio: ['inherit', 'pipe', 'pipe'],
153
+ windowsHide: true,
151
154
  });
152
155
 
153
156
  let gotOutput = false;
@@ -197,7 +200,7 @@ function deviceCodeLogin(cmd) {
197
200
  function findDevtunnel() {
198
201
  // Try devtunnel directly
199
202
  try {
200
- execSync('devtunnel --version', { stdio: 'pipe' });
203
+ execSync('devtunnel --version', { stdio: 'pipe', windowsHide: true });
201
204
  return 'devtunnel';
202
205
  } catch {}
203
206
 
@@ -222,7 +225,7 @@ function findDevtunnel() {
222
225
  );
223
226
  if (fs.existsSync(homeBin)) {
224
227
  try {
225
- execFileSync(homeBin, ['--version'], { stdio: 'pipe' });
228
+ execFileSync(homeBin, ['--version'], { stdio: 'pipe', windowsHide: true });
226
229
  return homeBin;
227
230
  } catch {}
228
231
  }
@@ -253,6 +256,7 @@ function isTunnelValid(id) {
253
256
  execFileSync(devtunnelCmd, ['show', id, '--json'], {
254
257
  encoding: 'utf-8',
255
258
  stdio: ['pipe', 'pipe', 'pipe'],
259
+ windowsHide: true,
256
260
  });
257
261
  return true;
258
262
  } catch {
@@ -273,7 +277,7 @@ function checkTunnelHealth() {
273
277
  execFile(
274
278
  devtunnelCmd,
275
279
  ['show', tunnelId],
276
- { encoding: 'utf-8', signal: abortCtrl.signal },
280
+ { encoding: 'utf-8', signal: abortCtrl.signal, windowsHide: true },
277
281
  (err, stdout) => {
278
282
  clearTimeout(timer);
279
283
 
@@ -401,6 +405,7 @@ function killTunnelProc() {
401
405
  execFileSync('taskkill', ['/pid', String(tunnelProc.pid), '/T', '/F'], {
402
406
  stdio: 'pipe',
403
407
  timeout: 5000,
408
+ windowsHide: true,
404
409
  });
405
410
  } catch {
406
411
  /* best effort */
@@ -566,6 +571,7 @@ function scheduleRestart() {
566
571
  function hostTunnel() {
567
572
  const hostProc = spawn(devtunnelCmd, ['host', tunnelId], {
568
573
  stdio: ['pipe', 'pipe', 'pipe'],
574
+ windowsHide: true,
569
575
  });
570
576
  tunnelProc = hostProc;
571
577
 
@@ -657,7 +663,11 @@ async function startTunnel(port, options = {}) {
657
663
  if (!loggedIn) {
658
664
  log.info('Logging in to DevTunnel with Microsoft Entra (recommended for long sessions)...');
659
665
  try {
660
- execFileSync(devtunnelCmd, ['user', 'login', '-e'], { stdio: 'inherit', timeout: 30000 });
666
+ execFileSync(devtunnelCmd, ['user', 'login', '-e'], {
667
+ stdio: 'inherit',
668
+ timeout: 30000,
669
+ windowsHide: true,
670
+ });
661
671
  } catch {
662
672
  log.info('Browser login failed or unavailable, falling back to device code flow...');
663
673
  log.info('A code will be displayed — open the URL on any device to authenticate.');
@@ -694,6 +704,7 @@ async function startTunnel(port, options = {}) {
694
704
  }
695
705
  const createOut = execFileSync(devtunnelCmd, ['create', '--expiration', '30d', '--json'], {
696
706
  encoding: 'utf-8',
707
+ windowsHide: true,
697
708
  });
698
709
  const tunnelData = JSON.parse(createOut);
699
710
  tunnelId = tunnelData.tunnel.tunnelId;
@@ -706,6 +717,7 @@ async function startTunnel(port, options = {}) {
706
717
  // Ephemeral tunnel — create fresh, will be deleted on shutdown
707
718
  const createOut = execFileSync(devtunnelCmd, ['create', '--expiration', '1d', '--json'], {
708
719
  encoding: 'utf-8',
720
+ windowsHide: true,
709
721
  });
710
722
  const tunnelData = JSON.parse(createOut);
711
723
  tunnelId = tunnelData.tunnel.tunnelId;
@@ -717,7 +729,7 @@ async function startTunnel(port, options = {}) {
717
729
  execFileSync(
718
730
  devtunnelCmd,
719
731
  ['port', 'create', tunnelId, '-p', String(port), '--protocol', 'http'],
720
- { stdio: 'pipe' },
732
+ { stdio: 'pipe', windowsHide: true },
721
733
  );
722
734
  } catch {}
723
735
  // Set tunnel access: public (anonymous) or private (owner-only via Microsoft login)
@@ -726,7 +738,7 @@ async function startTunnel(port, options = {}) {
726
738
  execFileSync(
727
739
  devtunnelCmd,
728
740
  ['access', 'create', tunnelId, '-p', String(port), '--anonymous'],
729
- { stdio: 'pipe' },
741
+ { stdio: 'pipe', windowsHide: true },
730
742
  );
731
743
  } catch {}
732
744
  log.info('Tunnel access: public (anonymous)');
@@ -735,6 +747,7 @@ async function startTunnel(port, options = {}) {
735
747
  try {
736
748
  execFileSync(devtunnelCmd, ['access', 'reset', tunnelId], {
737
749
  stdio: 'pipe',
750
+ windowsHide: true,
738
751
  });
739
752
  } catch {}
740
753
  log.info('Tunnel access: private (owner-only via Microsoft login)');
@@ -773,7 +786,11 @@ function cleanupTunnel() {
773
786
  log.info('Tunnel host stopped (tunnel preserved for reuse)');
774
787
  } else {
775
788
  try {
776
- execFileSync(devtunnelCmd, ['delete', id, '-f'], { stdio: 'pipe', timeout: 10000 });
789
+ execFileSync(devtunnelCmd, ['delete', id, '-f'], {
790
+ stdio: 'pipe',
791
+ timeout: 10000,
792
+ windowsHide: true,
793
+ });
777
794
  log.info('Tunnel cleaned up');
778
795
  } catch {
779
796
  /* best effort — tunnel will expire on its own */
@@ -98,7 +98,7 @@ async function installDevtunnel() {
98
98
  function findInstalledBinary() {
99
99
  // Check PATH first
100
100
  try {
101
- execSync('devtunnel --version', { stdio: 'pipe', timeout: 10000 });
101
+ execSync('devtunnel --version', { stdio: 'pipe', timeout: 10000, windowsHide: true });
102
102
  return 'devtunnel';
103
103
  } catch {}
104
104
 
@@ -110,6 +110,7 @@ function findInstalledBinary() {
110
110
  encoding: 'utf-8',
111
111
  stdio: 'pipe',
112
112
  timeout: 10000,
113
+ windowsHide: true,
113
114
  })
114
115
  .trim()
115
116
  .split(/\r?\n/)[0];
@@ -130,7 +131,7 @@ function findInstalledBinary() {
130
131
  const homeBin = path.join(os.homedir(), 'bin', getBinaryName());
131
132
  if (fs.existsSync(homeBin)) {
132
133
  try {
133
- execFileSync(homeBin, ['--version'], { stdio: 'pipe', timeout: 10000 });
134
+ execFileSync(homeBin, ['--version'], { stdio: 'pipe', timeout: 10000, windowsHide: true });
134
135
  return homeBin;
135
136
  } catch {}
136
137
  }
@@ -62,24 +62,29 @@ function tryDetectAgent(agent) {
62
62
  let remaining = candidates.length;
63
63
 
64
64
  for (const bin of candidates) {
65
- child_process.execFile(bin, args, { timeout: 5000, encoding: 'utf8' }, (err, stdout) => {
66
- remaining--;
67
- if (resolved) return;
68
- if (!err) {
69
- resolved = true;
70
- const version = (stdout || '').trim().split('\n')[0] || 'unknown';
71
- resolve({
72
- id: agent.id,
73
- name: agent.name,
74
- cmd: agent.cmd,
75
- args: agent.args || [],
76
- icon: agent.icon,
77
- version,
78
- });
79
- } else if (remaining === 0) {
80
- resolve(null);
81
- }
82
- });
65
+ child_process.execFile(
66
+ bin,
67
+ args,
68
+ { timeout: 5000, encoding: 'utf8', windowsHide: true },
69
+ (err, stdout) => {
70
+ remaining--;
71
+ if (resolved) return;
72
+ if (!err) {
73
+ resolved = true;
74
+ const version = (stdout || '').trim().split('\n')[0] || 'unknown';
75
+ resolve({
76
+ id: agent.id,
77
+ name: agent.name,
78
+ cmd: agent.cmd,
79
+ args: agent.args || [],
80
+ icon: agent.icon,
81
+ version,
82
+ });
83
+ } else if (remaining === 0) {
84
+ resolve(null);
85
+ }
86
+ },
87
+ );
83
88
  }
84
89
  });
85
90
  }
package/src/utils/git.js CHANGED
@@ -3,7 +3,9 @@ const path = require('path');
3
3
  const log = require('./logger');
4
4
 
5
5
  function git(cmd, cwd) {
6
- return execSync(`git ${cmd}`, { cwd, stdio: 'pipe', timeout: 3000 }).toString().trim();
6
+ return execSync(`git ${cmd}`, { cwd, stdio: 'pipe', timeout: 3000, windowsHide: true })
7
+ .toString()
8
+ .trim();
7
9
  }
8
10
 
9
11
  function getGitInfo(cwd) {
@@ -144,7 +146,7 @@ async function getGitRoot(cwd) {
144
146
  require('child_process').execFile(
145
147
  'git',
146
148
  ['rev-parse', '--show-toplevel'],
147
- { cwd, timeout: GIT_TIMEOUT },
149
+ { cwd, timeout: GIT_TIMEOUT, windowsHide: true },
148
150
  (err, stdout) => {
149
151
  if (err) return reject(err);
150
152
  resolve(stdout.trim());
@@ -167,6 +169,7 @@ async function gitAsync(args, cwd, options = {}) {
167
169
  cwd,
168
170
  timeout: options.timeout || GIT_TIMEOUT,
169
171
  maxBuffer: options.maxBuffer || MAX_DIFF_BUFFER,
172
+ windowsHide: true,
170
173
  },
171
174
  (err, stdout) => {
172
175
  if (err) return reject(err);
@@ -343,6 +346,7 @@ async function getFileDiff(cwd, filePath, options = {}) {
343
346
  cwd: root,
344
347
  timeout: GIT_TIMEOUT,
345
348
  maxBuffer: MAX_DIFF_BUFFER,
349
+ windowsHide: true,
346
350
  },
347
351
  (err, stdout) => {
348
352
  // git diff --no-index exits with 1 when files differ — that's expected
@@ -25,6 +25,7 @@ function detectWindowsShells() {
25
25
  stdio: ['pipe', 'pipe', 'ignore'],
26
26
  encoding: 'utf8',
27
27
  timeout: 3000,
28
+ windowsHide: true,
28
29
  });
29
30
  const fullPath = result.trim().split('\n')[0].trim();
30
31
  if (fullPath) {
@@ -339,15 +339,20 @@ function clearUpdateResult() {
339
339
 
340
340
  function execFilePromise(cmd, args, options = {}) {
341
341
  return new Promise((resolve, reject) => {
342
- execFile(cmd, args, { encoding: 'utf8', ...options }, (err, stdout, stderr) => {
343
- if (err) {
344
- err.stdout = stdout;
345
- err.stderr = stderr;
346
- reject(err);
347
- } else {
348
- resolve({ stdout, stderr });
349
- }
350
- });
342
+ execFile(
343
+ cmd,
344
+ args,
345
+ { encoding: 'utf8', windowsHide: true, ...options },
346
+ (err, stdout, stderr) => {
347
+ if (err) {
348
+ err.stdout = stdout;
349
+ err.stderr = stderr;
350
+ reject(err);
351
+ } else {
352
+ resolve({ stdout, stderr });
353
+ }
354
+ },
355
+ );
351
356
  });
352
357
  }
353
358
 
@@ -23,6 +23,7 @@ function getVersion() {
23
23
  cwd: path.join(__dirname, '..', '..'),
24
24
  encoding: 'utf-8',
25
25
  stdio: ['pipe', 'pipe', 'pipe'],
26
+ windowsHide: true,
26
27
  }).trim();
27
28
 
28
29
  const tagMatch = gitDesc.match(/^v(\d+\.\d+\.\d+)(?:-(\d+)-g([0-9a-f]+))?(-dirty)?$/);