fluxy-bot 0.5.32 → 0.5.34

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/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
 
@@ -350,6 +422,25 @@ async function init() {
350
422
  stepper.finish();
351
423
  finalMessage(tunnelUrl, relayUrl);
352
424
 
425
+ // Offer daemon install on Linux
426
+ if (os.platform() === 'linux' && process.stdin.isTTY) {
427
+ try {
428
+ const answer = await askQuestion(` ${c.bold}Install as a system daemon (auto-start on boot)?${c.reset} ${c.dim}[Y/n]${c.reset} `);
429
+ if (!answer || answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
430
+ console.log(`\n ${c.dim}Stopping foreground server...${c.reset}`);
431
+ child.kill('SIGTERM');
432
+ await new Promise((r) => setTimeout(r, 2000));
433
+ const nodePath = process.execPath;
434
+ const realHome = os.homedir();
435
+ const result = spawnSync(process.execPath, [process.argv[1], 'daemon', 'install'], {
436
+ stdio: 'inherit',
437
+ env: { ...process.env, FLUXY_NODE_PATH: nodePath, FLUXY_REAL_HOME: realHome },
438
+ });
439
+ process.exit(result.status ?? 0);
440
+ }
441
+ } catch {}
442
+ }
443
+
353
444
  child.stdout.on('data', (d) => {
354
445
  process.stdout.write(` ${c.dim}${d.toString().trim()}${c.reset}\n`);
355
446
  });
@@ -532,7 +623,148 @@ async function update() {
532
623
  stepper.finish();
533
624
 
534
625
  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`);
626
+
627
+ // Auto-restart daemon if installed
628
+ if (os.platform() === 'linux' && isServiceInstalled()) {
629
+ try {
630
+ execSync(`systemctl restart ${SERVICE_NAME}`, { stdio: 'ignore' });
631
+ console.log(` ${c.blue}✔${c.reset} Daemon restarted with new version.\n`);
632
+ } catch {
633
+ console.log(` ${c.yellow}⚠${c.reset} Could not restart daemon. Run ${c.pink}fluxy daemon restart${c.reset} manually.\n`);
634
+ }
635
+ } else {
636
+ console.log(` ${c.dim}Run ${c.reset}${c.pink}fluxy start${c.reset}${c.dim} to launch.${c.reset}\n`);
637
+ }
638
+ }
639
+
640
+ // ── Daemon ──
641
+
642
+ async function daemon(sub) {
643
+ // Platform guard
644
+ if (os.platform() !== 'linux') {
645
+ const hint = os.platform() === 'darwin'
646
+ ? 'Use tmux or nohup to keep Fluxy running in the background.'
647
+ : 'Use Task Scheduler to keep Fluxy running in the background.';
648
+ console.log(`\n ${c.yellow}⚠${c.reset} Daemon mode is only supported on Linux with systemd.`);
649
+ console.log(` ${c.dim}${hint}${c.reset}\n`);
650
+ process.exit(1);
651
+ }
652
+
653
+ // Check systemd is available
654
+ try {
655
+ execSync('systemctl --version', { stdio: 'ignore' });
656
+ } catch {
657
+ console.log(`\n ${c.red}✗${c.reset} systemd not found. Daemon mode requires systemd.\n`);
658
+ process.exit(1);
659
+ }
660
+
661
+ const action = sub || 'install';
662
+
663
+ switch (action) {
664
+ case 'install': {
665
+ if (!fs.existsSync(path.join(getRealHome(), '.fluxy', 'supervisor', 'index.ts'))) {
666
+ console.log(`\n ${c.red}✗${c.reset} Run ${c.pink}fluxy init${c.reset} first.\n`);
667
+ process.exit(1);
668
+ }
669
+
670
+ // Re-exec with sudo if needed
671
+ if (needsSudo()) {
672
+ const nodePath = process.env.FLUXY_NODE_PATH || process.execPath;
673
+ const realHome = getRealHome();
674
+ const args = process.argv.slice(1);
675
+ const result = spawnSync('sudo', [
676
+ `FLUXY_NODE_PATH=${nodePath}`,
677
+ `FLUXY_REAL_HOME=${realHome}`,
678
+ nodePath, ...args,
679
+ ], { stdio: 'inherit' });
680
+ process.exit(result.status ?? 1);
681
+ }
682
+
683
+ const user = getRealUser();
684
+ const home = getRealHome();
685
+ const nodePath = process.env.FLUXY_NODE_PATH || process.execPath;
686
+ const dataDir = path.join(home, '.fluxy');
687
+
688
+ const unit = generateUnitFile({ user, home, nodePath, dataDir });
689
+ fs.writeFileSync(SERVICE_PATH, unit);
690
+
691
+ execSync('systemctl daemon-reload', { stdio: 'ignore' });
692
+ execSync(`systemctl enable ${SERVICE_NAME}`, { stdio: 'ignore' });
693
+ execSync(`systemctl start ${SERVICE_NAME}`, { stdio: 'ignore' });
694
+
695
+ // Verify it started
696
+ await new Promise((r) => setTimeout(r, 2000));
697
+ if (isServiceActive()) {
698
+ console.log(`\n ${c.blue}✔${c.reset} Fluxy daemon installed and running.`);
699
+ console.log(` ${c.dim}It will auto-start on boot.${c.reset}`);
700
+ console.log(`\n ${c.dim}View logs:${c.reset} ${c.pink}fluxy daemon logs${c.reset}`);
701
+ console.log(` ${c.dim}Stop:${c.reset} ${c.pink}fluxy daemon stop${c.reset}`);
702
+ console.log(` ${c.dim}Uninstall:${c.reset} ${c.pink}fluxy daemon uninstall${c.reset}\n`);
703
+ } else {
704
+ console.log(`\n ${c.yellow}⚠${c.reset} Service installed but may not be running.`);
705
+ console.log(` ${c.dim}Check with: ${c.reset}${c.pink}fluxy daemon status${c.reset}\n`);
706
+ }
707
+ break;
708
+ }
709
+
710
+ case 'stop': {
711
+ if (needsSudo()) {
712
+ const result = spawnSync('sudo', [process.execPath, ...process.argv.slice(1)], { stdio: 'inherit' });
713
+ process.exit(result.status ?? 1);
714
+ }
715
+ execSync(`systemctl stop ${SERVICE_NAME}`, { stdio: 'inherit' });
716
+ console.log(`\n ${c.blue}✔${c.reset} Fluxy daemon stopped.\n`);
717
+ break;
718
+ }
719
+
720
+ case 'start': {
721
+ if (needsSudo()) {
722
+ const result = spawnSync('sudo', [process.execPath, ...process.argv.slice(1)], { stdio: 'inherit' });
723
+ process.exit(result.status ?? 1);
724
+ }
725
+ execSync(`systemctl start ${SERVICE_NAME}`, { stdio: 'inherit' });
726
+ console.log(`\n ${c.blue}✔${c.reset} Fluxy daemon started.\n`);
727
+ break;
728
+ }
729
+
730
+ case 'restart': {
731
+ if (needsSudo()) {
732
+ const result = spawnSync('sudo', [process.execPath, ...process.argv.slice(1)], { stdio: 'inherit' });
733
+ process.exit(result.status ?? 1);
734
+ }
735
+ execSync(`systemctl restart ${SERVICE_NAME}`, { stdio: 'inherit' });
736
+ console.log(`\n ${c.blue}✔${c.reset} Fluxy daemon restarted.\n`);
737
+ break;
738
+ }
739
+
740
+ case 'status': {
741
+ spawnSync('systemctl', ['status', SERVICE_NAME], { stdio: 'inherit' });
742
+ break;
743
+ }
744
+
745
+ case 'logs': {
746
+ spawnSync('journalctl', ['-u', SERVICE_NAME, '-f', '-n', '50'], { stdio: 'inherit' });
747
+ break;
748
+ }
749
+
750
+ case 'uninstall': {
751
+ if (needsSudo()) {
752
+ const result = spawnSync('sudo', [process.execPath, ...process.argv.slice(1)], { stdio: 'inherit' });
753
+ process.exit(result.status ?? 1);
754
+ }
755
+ try { execSync(`systemctl stop ${SERVICE_NAME}`, { stdio: 'ignore' }); } catch {}
756
+ try { execSync(`systemctl disable ${SERVICE_NAME}`, { stdio: 'ignore' }); } catch {}
757
+ if (fs.existsSync(SERVICE_PATH)) fs.unlinkSync(SERVICE_PATH);
758
+ execSync('systemctl daemon-reload', { stdio: 'ignore' });
759
+ console.log(`\n ${c.blue}✔${c.reset} Fluxy daemon uninstalled.\n`);
760
+ break;
761
+ }
762
+
763
+ default:
764
+ console.log(`\n ${c.red}✗${c.reset} Unknown daemon command: ${action}`);
765
+ console.log(` ${c.dim}Available: install, start, stop, restart, status, logs, uninstall${c.reset}\n`);
766
+ process.exit(1);
767
+ }
536
768
  }
537
769
 
538
770
  // ── Route ──
@@ -542,6 +774,7 @@ switch (command) {
542
774
  case 'start': start(); break;
543
775
  case 'status': status(); break;
544
776
  case 'update': update(); break;
777
+ case 'daemon': daemon(subcommand); break;
545
778
  default:
546
779
  fs.existsSync(CONFIG_PATH) ? start() : init();
547
780
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fluxy-bot",
3
- "version": "0.5.32",
3
+ "version": "0.5.34",
4
4
  "description": "Self-hosted, self-evolving AI agent with its own dashboard.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -205,6 +205,7 @@ export async function startFluxyAgentQuery(
205
205
  env: {
206
206
  ...process.env as Record<string, string>,
207
207
  CLAUDE_CODE_OAUTH_TOKEN: oauthToken,
208
+ CLAUDE_CODE_BUBBLEWRAP: '1',
208
209
  },
209
210
  },
210
211
  });