fluxy-bot 0.5.33 → 0.5.36

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/bin/cli.js +263 -5
  2. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { spawn, execSync } from 'child_process';
3
+ import { spawn, execSync, spawnSync } from 'child_process';
4
+ import readline from 'readline';
4
5
  import fs from 'fs';
5
6
  import path from 'path';
6
7
  import os from 'os';
@@ -17,7 +18,78 @@ const BIN_DIR = path.join(DATA_DIR, 'bin');
17
18
  const CF_PATH = path.join(BIN_DIR, 'cloudflared');
18
19
 
19
20
  const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf-8'));
20
- const [, , command] = process.argv;
21
+ const [, , command, subcommand] = process.argv;
22
+
23
+ // ── Daemon constants & helpers ──
24
+
25
+ const SERVICE_NAME = 'fluxy';
26
+ const SERVICE_PATH = `/etc/systemd/system/${SERVICE_NAME}.service`;
27
+
28
+ function needsSudo() {
29
+ return process.getuid() !== 0;
30
+ }
31
+
32
+ function getRealUser() {
33
+ return process.env.SUDO_USER || os.userInfo().username;
34
+ }
35
+
36
+ function getRealHome() {
37
+ if (process.env.FLUXY_REAL_HOME) return process.env.FLUXY_REAL_HOME;
38
+ try {
39
+ return execSync(`getent passwd ${getRealUser()}`, { encoding: 'utf-8' }).split(':')[5];
40
+ } catch {
41
+ return os.homedir();
42
+ }
43
+ }
44
+
45
+ function isServiceInstalled() {
46
+ return fs.existsSync(SERVICE_PATH);
47
+ }
48
+
49
+ function isServiceActive() {
50
+ try {
51
+ execSync(`systemctl is-active ${SERVICE_NAME}`, { stdio: 'ignore' });
52
+ return true;
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+
58
+ function generateUnitFile({ user, home, nodePath, dataDir }) {
59
+ const nodeBinDir = path.dirname(nodePath);
60
+ return `[Unit]
61
+ Description=Fluxy Bot
62
+ After=network-online.target
63
+ Wants=network-online.target
64
+
65
+ [Service]
66
+ Type=simple
67
+ User=${user}
68
+ WorkingDirectory=${dataDir}
69
+ ExecStart=${nodePath} --import tsx/esm ${dataDir}/supervisor/index.ts
70
+ Restart=on-failure
71
+ RestartSec=5
72
+ Environment=HOME=${home}
73
+ Environment=NODE_ENV=production
74
+ Environment=PATH=${nodeBinDir}:/usr/local/bin:/usr/bin:/bin
75
+ StandardOutput=journal
76
+ StandardError=journal
77
+ SyslogIdentifier=fluxy
78
+
79
+ [Install]
80
+ WantedBy=multi-user.target
81
+ `;
82
+ }
83
+
84
+ function askQuestion(query) {
85
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
86
+ return new Promise((resolve) => {
87
+ rl.question(query, (answer) => {
88
+ rl.close();
89
+ resolve(answer);
90
+ });
91
+ });
92
+ }
21
93
 
22
94
  // ── UI helpers ──
23
95
 
@@ -145,6 +217,12 @@ function finalMessage(tunnelUrl, relayUrl) {
145
217
 
146
218
  console.log(`
147
219
  ${c.dim}─────────────────────────────────${c.reset}
220
+
221
+ ${c.bold}${c.white}Commands:${c.reset}
222
+ ${c.dim}Status${c.reset} ${c.pink}fluxy status${c.reset}
223
+ ${c.dim}Update${c.reset} ${c.pink}fluxy update${c.reset}${os.platform() === 'linux' ? `
224
+ ${c.dim}Daemon${c.reset} ${c.pink}fluxy daemon${c.reset}` : ''}
225
+
148
226
  ${c.dim}Press Ctrl+C to stop the bot${c.reset}
149
227
  `);
150
228
  }
@@ -350,6 +428,25 @@ async function init() {
350
428
  stepper.finish();
351
429
  finalMessage(tunnelUrl, relayUrl);
352
430
 
431
+ // Offer daemon install on Linux
432
+ if (os.platform() === 'linux' && process.stdin.isTTY) {
433
+ try {
434
+ const answer = await askQuestion(` ${c.bold}Install as a system daemon (auto-start on boot)?${c.reset} ${c.dim}[Y/n]${c.reset} `);
435
+ if (!answer || answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
436
+ console.log(`\n ${c.dim}Stopping foreground server...${c.reset}`);
437
+ child.kill('SIGTERM');
438
+ await new Promise((r) => setTimeout(r, 2000));
439
+ const nodePath = process.execPath;
440
+ const realHome = os.homedir();
441
+ const result = spawnSync(process.execPath, [process.argv[1], 'daemon', 'install'], {
442
+ stdio: 'inherit',
443
+ env: { ...process.env, FLUXY_NODE_PATH: nodePath, FLUXY_REAL_HOME: realHome },
444
+ });
445
+ process.exit(result.status ?? 0);
446
+ }
447
+ } catch {}
448
+ }
449
+
353
450
  child.stdout.on('data', (d) => {
354
451
  process.stdout.write(` ${c.dim}${d.toString().trim()}${c.reset}\n`);
355
452
  });
@@ -394,6 +491,25 @@ async function start() {
394
491
  stepper.finish();
395
492
  finalMessage(tunnelUrl, relayUrl);
396
493
 
494
+ // Offer daemon install on Linux if not already installed
495
+ if (os.platform() === 'linux' && !isServiceInstalled() && process.stdin.isTTY) {
496
+ try {
497
+ const answer = await askQuestion(` ${c.bold}Install as a system daemon (auto-start on boot)?${c.reset} ${c.dim}[Y/n]${c.reset} `);
498
+ if (!answer || answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
499
+ console.log(`\n ${c.dim}Stopping foreground server...${c.reset}`);
500
+ child.kill('SIGTERM');
501
+ await new Promise((r) => setTimeout(r, 2000));
502
+ const nodePath = process.execPath;
503
+ const realHome = os.homedir();
504
+ const result = spawnSync(process.execPath, [process.argv[1], 'daemon', 'install'], {
505
+ stdio: 'inherit',
506
+ env: { ...process.env, FLUXY_NODE_PATH: nodePath, FLUXY_REAL_HOME: realHome },
507
+ });
508
+ process.exit(result.status ?? 0);
509
+ }
510
+ } catch {}
511
+ }
512
+
397
513
  child.stdout.on('data', (d) => {
398
514
  process.stdout.write(` ${c.dim}${d.toString().trim()}${c.reset}\n`);
399
515
  });
@@ -409,14 +525,14 @@ async function status() {
409
525
  const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
410
526
  const res = await fetch(`http://localhost:${config.port}/api/health`);
411
527
  const data = await res.json();
412
- console.log(`\n ${c.blue}●${c.reset} Bot is running`);
528
+ console.log(`\n ${c.blue}●${c.reset} Fluxy is running`);
413
529
  console.log(` ${c.dim}Uptime: ${data.uptime}s${c.reset}`);
414
530
  if (config.relay?.url) {
415
531
  console.log(` ${c.dim}URL: ${c.reset}${c.pink}${link(config.relay.url)}${c.reset}`);
416
532
  }
417
533
  console.log(` ${c.dim}Config: ${CONFIG_PATH}${c.reset}\n`);
418
534
  } catch {
419
- console.log(`\n ${c.dim}●${c.reset} Bot is not running.\n`);
535
+ console.log(`\n ${c.dim}●${c.reset} Fluxy is not running.\n`);
420
536
  }
421
537
  }
422
538
 
@@ -532,7 +648,148 @@ async function update() {
532
648
  stepper.finish();
533
649
 
534
650
  console.log(`\n ${c.blue}${c.bold}✔ Updated to v${latest.version}${c.reset}\n`);
535
- console.log(` ${c.dim}Run ${c.reset}${c.pink}fluxy start${c.reset}${c.dim} to launch.${c.reset}\n`);
651
+
652
+ // Auto-restart daemon if installed
653
+ if (os.platform() === 'linux' && isServiceInstalled()) {
654
+ try {
655
+ execSync(`systemctl restart ${SERVICE_NAME}`, { stdio: 'ignore' });
656
+ console.log(` ${c.blue}✔${c.reset} Daemon restarted with new version.\n`);
657
+ } catch {
658
+ console.log(` ${c.yellow}⚠${c.reset} Could not restart daemon. Run ${c.pink}fluxy daemon restart${c.reset} manually.\n`);
659
+ }
660
+ } else {
661
+ console.log(` ${c.dim}Run ${c.reset}${c.pink}fluxy start${c.reset}${c.dim} to launch.${c.reset}\n`);
662
+ }
663
+ }
664
+
665
+ // ── Daemon ──
666
+
667
+ async function daemon(sub) {
668
+ // Platform guard
669
+ if (os.platform() !== 'linux') {
670
+ const hint = os.platform() === 'darwin'
671
+ ? 'Use tmux or nohup to keep Fluxy running in the background.'
672
+ : 'Use Task Scheduler to keep Fluxy running in the background.';
673
+ console.log(`\n ${c.yellow}⚠${c.reset} Daemon mode is only supported on Linux with systemd.`);
674
+ console.log(` ${c.dim}${hint}${c.reset}\n`);
675
+ process.exit(1);
676
+ }
677
+
678
+ // Check systemd is available
679
+ try {
680
+ execSync('systemctl --version', { stdio: 'ignore' });
681
+ } catch {
682
+ console.log(`\n ${c.red}✗${c.reset} systemd not found. Daemon mode requires systemd.\n`);
683
+ process.exit(1);
684
+ }
685
+
686
+ const action = sub || 'install';
687
+
688
+ switch (action) {
689
+ case 'install': {
690
+ if (!fs.existsSync(path.join(getRealHome(), '.fluxy', 'supervisor', 'index.ts'))) {
691
+ console.log(`\n ${c.red}✗${c.reset} Run ${c.pink}fluxy init${c.reset} first.\n`);
692
+ process.exit(1);
693
+ }
694
+
695
+ // Re-exec with sudo if needed
696
+ if (needsSudo()) {
697
+ const nodePath = process.env.FLUXY_NODE_PATH || process.execPath;
698
+ const realHome = getRealHome();
699
+ const args = process.argv.slice(1);
700
+ const result = spawnSync('sudo', [
701
+ `FLUXY_NODE_PATH=${nodePath}`,
702
+ `FLUXY_REAL_HOME=${realHome}`,
703
+ nodePath, ...args,
704
+ ], { stdio: 'inherit' });
705
+ process.exit(result.status ?? 1);
706
+ }
707
+
708
+ const user = getRealUser();
709
+ const home = getRealHome();
710
+ const nodePath = process.env.FLUXY_NODE_PATH || process.execPath;
711
+ const dataDir = path.join(home, '.fluxy');
712
+
713
+ const unit = generateUnitFile({ user, home, nodePath, dataDir });
714
+ fs.writeFileSync(SERVICE_PATH, unit);
715
+
716
+ execSync('systemctl daemon-reload', { stdio: 'ignore' });
717
+ execSync(`systemctl enable ${SERVICE_NAME}`, { stdio: 'ignore' });
718
+ execSync(`systemctl start ${SERVICE_NAME}`, { stdio: 'ignore' });
719
+
720
+ // Verify it started
721
+ await new Promise((r) => setTimeout(r, 2000));
722
+ if (isServiceActive()) {
723
+ console.log(`\n ${c.blue}✔${c.reset} Fluxy daemon installed and running.`);
724
+ console.log(` ${c.dim}It will auto-start on boot.${c.reset}`);
725
+ console.log(`\n ${c.dim}View logs:${c.reset} ${c.pink}fluxy daemon logs${c.reset}`);
726
+ console.log(` ${c.dim}Stop:${c.reset} ${c.pink}fluxy daemon stop${c.reset}`);
727
+ console.log(` ${c.dim}Uninstall:${c.reset} ${c.pink}fluxy daemon uninstall${c.reset}\n`);
728
+ } else {
729
+ console.log(`\n ${c.yellow}⚠${c.reset} Service installed but may not be running.`);
730
+ console.log(` ${c.dim}Check with: ${c.reset}${c.pink}fluxy daemon status${c.reset}\n`);
731
+ }
732
+ break;
733
+ }
734
+
735
+ case 'stop': {
736
+ if (needsSudo()) {
737
+ const result = spawnSync('sudo', [process.execPath, ...process.argv.slice(1)], { stdio: 'inherit' });
738
+ process.exit(result.status ?? 1);
739
+ }
740
+ execSync(`systemctl stop ${SERVICE_NAME}`, { stdio: 'inherit' });
741
+ console.log(`\n ${c.blue}✔${c.reset} Fluxy daemon stopped.\n`);
742
+ break;
743
+ }
744
+
745
+ case 'start': {
746
+ if (needsSudo()) {
747
+ const result = spawnSync('sudo', [process.execPath, ...process.argv.slice(1)], { stdio: 'inherit' });
748
+ process.exit(result.status ?? 1);
749
+ }
750
+ execSync(`systemctl start ${SERVICE_NAME}`, { stdio: 'inherit' });
751
+ console.log(`\n ${c.blue}✔${c.reset} Fluxy daemon started.\n`);
752
+ break;
753
+ }
754
+
755
+ case 'restart': {
756
+ if (needsSudo()) {
757
+ const result = spawnSync('sudo', [process.execPath, ...process.argv.slice(1)], { stdio: 'inherit' });
758
+ process.exit(result.status ?? 1);
759
+ }
760
+ execSync(`systemctl restart ${SERVICE_NAME}`, { stdio: 'inherit' });
761
+ console.log(`\n ${c.blue}✔${c.reset} Fluxy daemon restarted.\n`);
762
+ break;
763
+ }
764
+
765
+ case 'status': {
766
+ spawnSync('systemctl', ['status', SERVICE_NAME], { stdio: 'inherit' });
767
+ break;
768
+ }
769
+
770
+ case 'logs': {
771
+ spawnSync('journalctl', ['-u', SERVICE_NAME, '-f', '-n', '50'], { stdio: 'inherit' });
772
+ break;
773
+ }
774
+
775
+ case 'uninstall': {
776
+ if (needsSudo()) {
777
+ const result = spawnSync('sudo', [process.execPath, ...process.argv.slice(1)], { stdio: 'inherit' });
778
+ process.exit(result.status ?? 1);
779
+ }
780
+ try { execSync(`systemctl stop ${SERVICE_NAME}`, { stdio: 'ignore' }); } catch {}
781
+ try { execSync(`systemctl disable ${SERVICE_NAME}`, { stdio: 'ignore' }); } catch {}
782
+ if (fs.existsSync(SERVICE_PATH)) fs.unlinkSync(SERVICE_PATH);
783
+ execSync('systemctl daemon-reload', { stdio: 'ignore' });
784
+ console.log(`\n ${c.blue}✔${c.reset} Fluxy daemon uninstalled.\n`);
785
+ break;
786
+ }
787
+
788
+ default:
789
+ console.log(`\n ${c.red}✗${c.reset} Unknown daemon command: ${action}`);
790
+ console.log(` ${c.dim}Available: install, start, stop, restart, status, logs, uninstall${c.reset}\n`);
791
+ process.exit(1);
792
+ }
536
793
  }
537
794
 
538
795
  // ── Route ──
@@ -542,6 +799,7 @@ switch (command) {
542
799
  case 'start': start(); break;
543
800
  case 'status': status(); break;
544
801
  case 'update': update(); break;
802
+ case 'daemon': daemon(subcommand); break;
545
803
  default:
546
804
  fs.existsSync(CONFIG_PATH) ? start() : init();
547
805
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fluxy-bot",
3
- "version": "0.5.33",
3
+ "version": "0.5.36",
4
4
  "description": "Self-hosted, self-evolving AI agent with its own dashboard.",
5
5
  "type": "module",
6
6
  "license": "MIT",