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.
- package/bin/cli.js +263 -5
- 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}
|
|
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}
|
|
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
|
-
|
|
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
|
}
|