termbeam 1.17.2 → 1.17.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli/service.js +139 -54
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termbeam",
3
- "version": "1.17.2",
3
+ "version": "1.17.3",
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": {
@@ -46,6 +46,7 @@ function installPm2Global() {
46
46
  execFileSync('npm', ['install', '-g', 'pm2'], {
47
47
  stdio: 'inherit',
48
48
  timeout: 120000,
49
+ shell: os.platform() === 'win32',
49
50
  });
50
51
  console.log(green('✓ PM2 installed successfully.\n'));
51
52
  return true;
@@ -122,6 +123,20 @@ function writeEcosystem(content) {
122
123
  fs.writeFileSync(ECOSYSTEM_FILE, content, 'utf8');
123
124
  }
124
125
 
126
+ function readEcosystemName() {
127
+ try {
128
+ const content = fs.readFileSync(ECOSYSTEM_FILE, 'utf8');
129
+ const json = content.replace(/^module\.exports\s*=\s*/, '').replace(/;\s*$/, '');
130
+ const eco = JSON.parse(json);
131
+ if (eco.apps && eco.apps[0] && eco.apps[0].name) {
132
+ return eco.apps[0].name;
133
+ }
134
+ } catch {
135
+ // ecosystem file missing or malformed
136
+ }
137
+ return DEFAULT_SERVICE_NAME;
138
+ }
139
+
125
140
  // ── PM2 Commands ─────────────────────────────────────────────────────────────
126
141
 
127
142
  function pm2Exec(args, opts = {}) {
@@ -131,6 +146,8 @@ function pm2Exec(args, opts = {}) {
131
146
  encoding: 'utf8',
132
147
  stdio: opts.inherit ? 'inherit' : ['pipe', 'pipe', 'pipe'],
133
148
  timeout: 30000,
149
+ // Windows npm globals are .cmd wrappers; execFileSync needs shell to resolve them
150
+ shell: os.platform() === 'win32',
134
151
  ...opts,
135
152
  });
136
153
  } catch (err) {
@@ -225,6 +242,7 @@ async function actionInstall() {
225
242
 
226
243
  // Service name
227
244
  showProgress(0);
245
+ console.log(dim(' The PM2 process name for this service.\n'));
228
246
  config.name = await ask(rl, 'Service name:', DEFAULT_SERVICE_NAME);
229
247
  decisions.push({ label: 'Service name', value: config.name });
230
248
 
@@ -424,62 +442,108 @@ async function actionInstall() {
424
442
  // Run pm2 startup if chosen during wizard
425
443
  if (config.startup) {
426
444
  console.log('');
427
- // pm2 startup outputs a sudo command to copy/paste — but it always exits 1
428
- // (since the startup hook isn't installed yet). Extract stdout from the error.
429
- let startupOutput = '';
430
- try {
431
- startupOutput = execFileSync('pm2', ['startup'], {
432
- encoding: 'utf8',
433
- stdio: ['pipe', 'pipe', 'pipe'],
434
- timeout: 15000,
435
- });
436
- } catch (err) {
437
- // pm2 startup exits 1 by design — the sudo command is in stdout
438
- startupOutput = (err.stdout || '') + (err.stderr || '');
439
- }
440
- const sudoMatch = startupOutput.match(/^(sudo .+)$/m);
441
- if (sudoMatch) {
442
- console.log(dim('Setting up boot persistence (may ask for your password)...\n'));
443
- const { spawnSync } = require('child_process');
444
- // pm2 outputs: sudo env PATH=$PATH:/extra /path/to/pm2 startup <init> -u <user> --hp <home>
445
- // We can't use sh -c because $PATH may contain spaces (e.g. "Visual Studio Code.app").
446
- // Instead, parse the structured command and pass PATH via env to avoid shell expansion.
447
- const envMatch = sudoMatch[1].match(
448
- /^sudo\s+env\s+PATH=\$PATH:([\S]+)\s+(\S+)\s+startup\s+(.+)$/,
445
+ if (os.platform() === 'win32') {
446
+ // Windows: pm2 startup doesn't support Windows init systems.
447
+ // Instead, create a script in the Windows Startup folder that runs pm2 resurrect.
448
+ const startupDir = path.join(
449
+ process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
450
+ 'Microsoft',
451
+ 'Windows',
452
+ 'Start Menu',
453
+ 'Programs',
454
+ 'Startup',
449
455
  );
450
- let result;
451
- if (envMatch) {
452
- const extraPath = envMatch[1]; // e.g. /opt/homebrew/.../bin
453
- const pm2Bin = envMatch[2]; // e.g. /opt/homebrew/.../pm2
454
- const restArgs = envMatch[3].split(/\s+/); // e.g. ['launchd', '-u', 'user', '--hp', '/home']
455
- const fullPath = (process.env.PATH || '') + ':' + extraPath;
456
- result = spawnSync('sudo', ['env', `PATH=${fullPath}`, pm2Bin, 'startup', ...restArgs], {
457
- stdio: 'inherit',
458
- });
459
- } else {
460
- // Fallback: try running via sh with quoted PATH
461
- const resolved = sudoMatch[1].replace(/\$PATH/g, `'${process.env.PATH || ''}'`);
462
- result = spawnSync('sh', ['-c', resolved], { stdio: 'inherit' });
463
- }
464
- const code = result.status;
465
- if (code === 0) {
456
+ const startupScript = path.join(startupDir, 'termbeam-pm2.cmd');
457
+ try {
458
+ fs.writeFileSync(startupScript, '@echo off\r\npm2 resurrect\r\n', 'utf8');
466
459
  pm2Exec(['save'], { inherit: true });
467
460
  console.log(green('✓ TermBeam will start automatically on boot.'));
468
- } else {
469
- console.error(red('\n✗ Failed to set up boot persistence.'));
461
+ console.log(dim(` Startup script: ${startupScript}`));
462
+ } catch (err) {
463
+ console.error(red('✗ Failed to create startup script.'));
470
464
  console.log(yellow(" TermBeam is running, but won't auto-start after a reboot."));
471
- console.log(yellow(' To fix this, run the following command manually:\n'));
472
- console.log(` ${cyan(sudoMatch[1])}`);
465
+ console.log(yellow(' To fix this manually, create a file at:\n'));
466
+ console.log(` ${cyan(startupScript)}`);
467
+ console.log(yellow('\n With contents:'));
468
+ console.log(` ${cyan('@echo off & pm2 resurrect')}`);
473
469
  console.log(yellow('\n Then run:'));
474
470
  console.log(` ${cyan('pm2 save')}\n`);
475
471
  }
476
472
  } else {
477
- console.error(red('✗ Could not determine boot persistence command.'));
478
- console.log(yellow(" TermBeam is running, but won't auto-start after a reboot."));
479
- console.log(yellow(' To fix this, run:\n'));
480
- console.log(` ${cyan('pm2 startup')}`);
481
- console.log(dim(' …then run the sudo command it outputs, followed by:'));
482
- console.log(` ${cyan('pm2 save')}\n`);
473
+ // Unix/macOS/WSL: try pm2 startup to set up boot persistence
474
+ let startupOutput = '';
475
+ let succeeded = false;
476
+ let initSystemError = false;
477
+ try {
478
+ startupOutput = execFileSync('pm2', ['startup'], {
479
+ encoding: 'utf8',
480
+ stdio: ['pipe', 'pipe', 'pipe'],
481
+ timeout: 15000,
482
+ });
483
+ // Exit 0 means pm2 configured startup directly (e.g. running as root)
484
+ succeeded = true;
485
+ } catch (err) {
486
+ // pm2 startup exits 1 by design — the sudo command is in stdout
487
+ startupOutput = (err.stdout || '') + (err.stderr || '');
488
+ if (startupOutput.includes('Init system not found')) {
489
+ initSystemError = true;
490
+ }
491
+ }
492
+
493
+ if (succeeded) {
494
+ // pm2 startup succeeded (typically running as root) — just save
495
+ pm2Exec(['save'], { inherit: true });
496
+ console.log(green('✓ TermBeam will start automatically on boot.'));
497
+ } else if (initSystemError) {
498
+ // WSL without systemd, or other environments without an init system
499
+ console.log(yellow('⚠ No init system detected (common in WSL without systemd).'));
500
+ console.log(yellow(" TermBeam is running, but won't auto-start after a reboot."));
501
+ console.log(dim(' To enable boot persistence, either:'));
502
+ console.log(dim(' • Enable systemd in WSL: add [boot] systemd=true to /etc/wsl.conf'));
503
+ console.log(dim(' • Or add "pm2 resurrect" to your shell profile (~/.bashrc)\n'));
504
+ } else {
505
+ const sudoMatch = startupOutput.match(/^(sudo .+)$/m);
506
+ if (sudoMatch) {
507
+ console.log(dim('Setting up boot persistence (may ask for your password)...\n'));
508
+ const { spawnSync } = require('child_process');
509
+ const envMatch = sudoMatch[1].match(
510
+ /^sudo\s+env\s+PATH=\$PATH:([\S]+)\s+(\S+)\s+startup\s+(.+)$/,
511
+ );
512
+ let result;
513
+ if (envMatch) {
514
+ const extraPath = envMatch[1];
515
+ const pm2Bin = envMatch[2];
516
+ const restArgs = envMatch[3].split(/\s+/);
517
+ const fullPath = (process.env.PATH || '') + ':' + extraPath;
518
+ result = spawnSync(
519
+ 'sudo',
520
+ ['env', `PATH=${fullPath}`, pm2Bin, 'startup', ...restArgs],
521
+ { stdio: 'inherit' },
522
+ );
523
+ } else {
524
+ const resolved = sudoMatch[1].replace(/\$PATH/g, `'${process.env.PATH || ''}'`);
525
+ result = spawnSync('sh', ['-c', resolved], { stdio: 'inherit' });
526
+ }
527
+ if (result.status === 0) {
528
+ pm2Exec(['save'], { inherit: true });
529
+ console.log(green('✓ TermBeam will start automatically on boot.'));
530
+ } else {
531
+ console.error(red('\n✗ Failed to set up boot persistence.'));
532
+ console.log(yellow(" TermBeam is running, but won't auto-start after a reboot."));
533
+ console.log(yellow(' To fix this, run the following command manually:\n'));
534
+ console.log(` ${cyan(sudoMatch[1])}`);
535
+ console.log(yellow('\n Then run:'));
536
+ console.log(` ${cyan('pm2 save')}\n`);
537
+ }
538
+ } else {
539
+ console.error(red('✗ Could not determine boot persistence command.'));
540
+ console.log(yellow(" TermBeam is running, but won't auto-start after a reboot."));
541
+ console.log(yellow(' To fix this, run:\n'));
542
+ console.log(` ${cyan('pm2 startup')}`);
543
+ console.log(dim(' …then run the sudo command it outputs, followed by:'));
544
+ console.log(` ${cyan('pm2 save')}\n`);
545
+ }
546
+ }
483
547
  }
484
548
  }
485
549
 
@@ -528,20 +592,22 @@ async function actionUninstall() {
528
592
  process.exit(1);
529
593
  }
530
594
 
531
- // Find running termbeam services
595
+ // Determine service name: prefer ecosystem config, then PM2 process list, then default
596
+ const ecoName = readEcosystemName();
532
597
  const list = pm2Exec(['jlist'], { silent: true });
533
598
  let services = [];
534
599
  if (list) {
535
600
  try {
536
601
  services = JSON.parse(list).filter(
537
- (p) => p.name === DEFAULT_SERVICE_NAME || p.name.startsWith('termbeam'),
602
+ (p) =>
603
+ p.name === ecoName || p.name === DEFAULT_SERVICE_NAME || p.name.startsWith('termbeam'),
538
604
  );
539
605
  } catch {
540
606
  // ignore parse errors
541
607
  }
542
608
  }
543
609
 
544
- const name = services.length > 0 ? services[0].name : DEFAULT_SERVICE_NAME;
610
+ const name = services.length > 0 ? services[0].name : ecoName;
545
611
 
546
612
  const rl = createRL();
547
613
  const sure = await confirm(rl, `Remove TermBeam service "${name}" from PM2?`, true);
@@ -562,6 +628,23 @@ async function actionUninstall() {
562
628
  console.log(dim(`Removed ${ECOSYSTEM_FILE}`));
563
629
  }
564
630
 
631
+ // Clean up Windows startup script if present
632
+ if (os.platform() === 'win32') {
633
+ const startupScript = path.join(
634
+ process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
635
+ 'Microsoft',
636
+ 'Windows',
637
+ 'Start Menu',
638
+ 'Programs',
639
+ 'Startup',
640
+ 'termbeam-pm2.cmd',
641
+ );
642
+ if (fs.existsSync(startupScript)) {
643
+ fs.unlinkSync(startupScript);
644
+ console.log(dim(`Removed ${startupScript}`));
645
+ }
646
+ }
647
+
565
648
  log.info('Service stopped');
566
649
  console.log(green(`\n✓ TermBeam service "${name}" removed.\n`));
567
650
  }
@@ -572,7 +655,7 @@ function actionStatus() {
572
655
  console.error(red('✗ PM2 is not installed. Run: npm install -g pm2'));
573
656
  process.exit(1);
574
657
  }
575
- pm2Exec(['describe', DEFAULT_SERVICE_NAME], { inherit: true });
658
+ pm2Exec(['describe', readEcosystemName()], { inherit: true });
576
659
  }
577
660
 
578
661
  function actionLogs() {
@@ -582,8 +665,9 @@ function actionLogs() {
582
665
  process.exit(1);
583
666
  }
584
667
  const { spawn } = require('child_process');
585
- const child = spawn('pm2', ['logs', DEFAULT_SERVICE_NAME, '--lines', '200'], {
668
+ const child = spawn('pm2', ['logs', readEcosystemName(), '--lines', '200'], {
586
669
  stdio: 'inherit',
670
+ shell: os.platform() === 'win32',
587
671
  });
588
672
  child.on('error', (err) => {
589
673
  console.error(red(`✗ Failed to stream logs: ${err.message}`));
@@ -596,7 +680,7 @@ function actionRestart() {
596
680
  console.error(red('✗ PM2 is not installed. Run: npm install -g pm2'));
597
681
  process.exit(1);
598
682
  }
599
- pm2Exec(['restart', DEFAULT_SERVICE_NAME], { inherit: true });
683
+ pm2Exec(['restart', readEcosystemName()], { inherit: true });
600
684
  log.info('Service restarted');
601
685
  console.log(green('\n✓ TermBeam service restarted.\n'));
602
686
  }
@@ -651,6 +735,7 @@ module.exports = {
651
735
  buildArgs,
652
736
  generateEcosystem,
653
737
  writeEcosystem,
738
+ readEcosystemName,
654
739
  pm2Exec,
655
740
  actionStatus,
656
741
  actionRestart,