fluxy-bot 0.5.68 → 0.5.69
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 +264 -34
- package/package.json +1 -1
- package/supervisor/index.ts +23 -12
package/bin/cli.js
CHANGED
|
@@ -22,6 +22,9 @@ const [, , command, subcommand] = process.argv;
|
|
|
22
22
|
|
|
23
23
|
// ── Daemon constants & helpers ──
|
|
24
24
|
|
|
25
|
+
const PLATFORM = os.platform();
|
|
26
|
+
|
|
27
|
+
// --- systemd (Linux) ---
|
|
25
28
|
const SERVICE_NAME = 'fluxy';
|
|
26
29
|
const SERVICE_PATH = `/etc/systemd/system/${SERVICE_NAME}.service`;
|
|
27
30
|
|
|
@@ -82,6 +85,105 @@ WantedBy=multi-user.target
|
|
|
82
85
|
`;
|
|
83
86
|
}
|
|
84
87
|
|
|
88
|
+
// --- launchd (macOS) ---
|
|
89
|
+
const LAUNCHD_LABEL = 'com.fluxy.bot';
|
|
90
|
+
const LAUNCHD_PLIST_PATH = path.join(os.homedir(), 'Library', 'LaunchAgents', `${LAUNCHD_LABEL}.plist`);
|
|
91
|
+
const LAUNCHD_LOG_DIR = path.join(os.homedir(), 'Library', 'Logs', 'fluxy');
|
|
92
|
+
|
|
93
|
+
function isLaunchdInstalled() {
|
|
94
|
+
return fs.existsSync(LAUNCHD_PLIST_PATH);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isLaunchdActive() {
|
|
98
|
+
try {
|
|
99
|
+
const out = execSync(`launchctl list ${LAUNCHD_LABEL} 2>/dev/null`, { encoding: 'utf-8' });
|
|
100
|
+
// launchctl list <label> succeeds if the job is loaded; check PID field
|
|
101
|
+
const pidLine = out.split('\n').find(l => l.includes('PID'));
|
|
102
|
+
if (pidLine) {
|
|
103
|
+
const pid = pidLine.split('=')[1]?.trim();
|
|
104
|
+
return pid && pid !== '0' && pid !== '-';
|
|
105
|
+
}
|
|
106
|
+
// Fallback: if the command succeeded, it's loaded. Check if the process is running.
|
|
107
|
+
const lines = out.trim().split('\n');
|
|
108
|
+
// First line of `launchctl list <label>` output: "PID\tStatus\tLabel" header or direct values
|
|
109
|
+
// The format is: { "PID" = <pid>; "Status" = <code>; ... }
|
|
110
|
+
return !out.includes('"PID" = 0;');
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function generateLaunchdPlist({ nodePath, dataDir }) {
|
|
117
|
+
const nodeBinDir = path.dirname(nodePath);
|
|
118
|
+
fs.mkdirSync(LAUNCHD_LOG_DIR, { recursive: true });
|
|
119
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
120
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
121
|
+
<plist version="1.0">
|
|
122
|
+
<dict>
|
|
123
|
+
<key>Label</key>
|
|
124
|
+
<string>${LAUNCHD_LABEL}</string>
|
|
125
|
+
<key>ProgramArguments</key>
|
|
126
|
+
<array>
|
|
127
|
+
<string>${nodePath}</string>
|
|
128
|
+
<string>--import</string>
|
|
129
|
+
<string>tsx/esm</string>
|
|
130
|
+
<string>${dataDir}/supervisor/index.ts</string>
|
|
131
|
+
</array>
|
|
132
|
+
<key>WorkingDirectory</key>
|
|
133
|
+
<string>${dataDir}</string>
|
|
134
|
+
<key>EnvironmentVariables</key>
|
|
135
|
+
<dict>
|
|
136
|
+
<key>HOME</key>
|
|
137
|
+
<string>${os.homedir()}</string>
|
|
138
|
+
<key>NODE_ENV</key>
|
|
139
|
+
<string>development</string>
|
|
140
|
+
<key>NODE_PATH</key>
|
|
141
|
+
<string>${dataDir}/node_modules</string>
|
|
142
|
+
<key>PATH</key>
|
|
143
|
+
<string>${nodeBinDir}:${dataDir}/node_modules/.bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
144
|
+
</dict>
|
|
145
|
+
<key>RunAtLoad</key>
|
|
146
|
+
<true/>
|
|
147
|
+
<key>KeepAlive</key>
|
|
148
|
+
<dict>
|
|
149
|
+
<key>SuccessfulExit</key>
|
|
150
|
+
<false/>
|
|
151
|
+
</dict>
|
|
152
|
+
<key>ThrottleInterval</key>
|
|
153
|
+
<integer>5</integer>
|
|
154
|
+
<key>StandardOutPath</key>
|
|
155
|
+
<string>${LAUNCHD_LOG_DIR}/stdout.log</string>
|
|
156
|
+
<key>StandardErrorPath</key>
|
|
157
|
+
<string>${LAUNCHD_LOG_DIR}/stderr.log</string>
|
|
158
|
+
<key>ProcessType</key>
|
|
159
|
+
<string>Standard</string>
|
|
160
|
+
</dict>
|
|
161
|
+
</plist>
|
|
162
|
+
`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// --- Platform-agnostic daemon helpers ---
|
|
166
|
+
|
|
167
|
+
function hasDaemonSupport() {
|
|
168
|
+
if (PLATFORM === 'darwin') return true; // launchd is always available on macOS
|
|
169
|
+
if (PLATFORM === 'linux') {
|
|
170
|
+
try { execSync('systemctl --version', { stdio: 'ignore' }); return true; } catch { return false; }
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function isDaemonInstalled() {
|
|
176
|
+
if (PLATFORM === 'darwin') return isLaunchdInstalled();
|
|
177
|
+
if (PLATFORM === 'linux') return isServiceInstalled();
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function isDaemonActive() {
|
|
182
|
+
if (PLATFORM === 'darwin') return isLaunchdActive();
|
|
183
|
+
if (PLATFORM === 'linux') return isServiceActive();
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
85
187
|
function killAndWait(child, timeout = 10_000) {
|
|
86
188
|
return new Promise((resolve) => {
|
|
87
189
|
child.removeAllListeners('exit');
|
|
@@ -663,8 +765,7 @@ async function init() {
|
|
|
663
765
|
}
|
|
664
766
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
665
767
|
|
|
666
|
-
const
|
|
667
|
-
const hasSystemd = isLinux && (() => { try { execSync('systemctl --version', { stdio: 'ignore' }); return true; } catch { return false; } })();
|
|
768
|
+
const canDaemon = hasDaemonSupport();
|
|
668
769
|
|
|
669
770
|
const steps = [
|
|
670
771
|
'Creating config',
|
|
@@ -673,7 +774,7 @@ async function init() {
|
|
|
673
774
|
'Connecting tunnel',
|
|
674
775
|
'Verifying connection',
|
|
675
776
|
'Preparing dashboard',
|
|
676
|
-
...(
|
|
777
|
+
...(canDaemon ? ['Setting up auto-start daemon'] : []),
|
|
677
778
|
];
|
|
678
779
|
|
|
679
780
|
const stepper = new Stepper(steps);
|
|
@@ -720,8 +821,8 @@ async function init() {
|
|
|
720
821
|
await Promise.race([viteWarm, new Promise(r => setTimeout(r, 30_000))]);
|
|
721
822
|
stepper.advance();
|
|
722
823
|
|
|
723
|
-
// Install systemd
|
|
724
|
-
if (
|
|
824
|
+
// Install daemon (systemd on Linux, launchd on macOS)
|
|
825
|
+
if (canDaemon) {
|
|
725
826
|
await killAndWait(child);
|
|
726
827
|
const nodePath = process.execPath;
|
|
727
828
|
const realHome = os.homedir();
|
|
@@ -737,7 +838,7 @@ async function init() {
|
|
|
737
838
|
finalMessage(tunnelUrl, relayUrl);
|
|
738
839
|
}
|
|
739
840
|
if (res.status === 0) {
|
|
740
|
-
console.log(` ${c.blue}✔${c.reset} Daemon installed — Fluxy will auto-start on boot.`);
|
|
841
|
+
console.log(` ${c.blue}✔${c.reset} Daemon installed — Fluxy will auto-start on ${PLATFORM === 'darwin' ? 'login' : 'boot'}.`);
|
|
741
842
|
} else {
|
|
742
843
|
console.log(` ${c.yellow}⚠${c.reset} Daemon install failed. Run ${c.pink}fluxy daemon install${c.reset} manually.`);
|
|
743
844
|
}
|
|
@@ -768,7 +869,7 @@ async function start() {
|
|
|
768
869
|
}
|
|
769
870
|
|
|
770
871
|
// If daemon is already running, don't start a second instance
|
|
771
|
-
if (
|
|
872
|
+
if (isDaemonInstalled() && isDaemonActive()) {
|
|
772
873
|
banner();
|
|
773
874
|
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
774
875
|
console.log(`\n ${c.blue}●${c.reset} Fluxy is already running as a daemon.\n`);
|
|
@@ -782,9 +883,8 @@ async function start() {
|
|
|
782
883
|
return;
|
|
783
884
|
}
|
|
784
885
|
|
|
785
|
-
const
|
|
786
|
-
const
|
|
787
|
-
const needsDaemon = hasSystemd && !isServiceInstalled();
|
|
886
|
+
const canDaemon = hasDaemonSupport();
|
|
887
|
+
const needsDaemon = canDaemon && !isDaemonInstalled();
|
|
788
888
|
|
|
789
889
|
banner();
|
|
790
890
|
|
|
@@ -836,7 +936,7 @@ async function start() {
|
|
|
836
936
|
await Promise.race([viteWarm, new Promise(r => setTimeout(r, 30_000))]);
|
|
837
937
|
stepper.advance();
|
|
838
938
|
|
|
839
|
-
// Install systemd
|
|
939
|
+
// Install daemon (systemd on Linux, launchd on macOS) if not already installed
|
|
840
940
|
if (needsDaemon) {
|
|
841
941
|
await killAndWait(child);
|
|
842
942
|
const nodePath = process.execPath;
|
|
@@ -853,7 +953,7 @@ async function start() {
|
|
|
853
953
|
finalMessage(tunnelUrl, relayUrl);
|
|
854
954
|
}
|
|
855
955
|
if (res.status === 0) {
|
|
856
|
-
console.log(` ${c.blue}✔${c.reset} Daemon installed — Fluxy will auto-start on boot.`);
|
|
956
|
+
console.log(` ${c.blue}✔${c.reset} Daemon installed — Fluxy will auto-start on ${PLATFORM === 'darwin' ? 'login' : 'boot'}.`);
|
|
857
957
|
} else {
|
|
858
958
|
console.log(` ${c.yellow}⚠${c.reset} Daemon install failed. Run ${c.pink}fluxy daemon install${c.reset} manually.`);
|
|
859
959
|
}
|
|
@@ -880,7 +980,7 @@ async function start() {
|
|
|
880
980
|
|
|
881
981
|
async function status() {
|
|
882
982
|
const config = fs.existsSync(CONFIG_PATH) ? JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')) : null;
|
|
883
|
-
const daemonRunning =
|
|
983
|
+
const daemonRunning = isDaemonInstalled() && isDaemonActive();
|
|
884
984
|
|
|
885
985
|
// Try health endpoint
|
|
886
986
|
let healthOk = false;
|
|
@@ -919,7 +1019,7 @@ async function update() {
|
|
|
919
1019
|
// Refuse to run the update as root — file ownership would get poisoned
|
|
920
1020
|
if (os.platform() !== 'win32' && process.getuid?.() === 0) {
|
|
921
1021
|
console.log(`\n ${c.red}✗${c.reset} Do not run ${c.bold}fluxy update${c.reset} with sudo.`);
|
|
922
|
-
console.log(` The update manages sudo internally for
|
|
1022
|
+
console.log(` The update manages sudo internally for daemon commands.\n`);
|
|
923
1023
|
process.exit(1);
|
|
924
1024
|
}
|
|
925
1025
|
|
|
@@ -945,7 +1045,7 @@ async function update() {
|
|
|
945
1045
|
|
|
946
1046
|
console.log(` ${c.dim}v${currentVersion} → v${latest.version}${c.reset}\n`);
|
|
947
1047
|
|
|
948
|
-
const daemonWasRunning =
|
|
1048
|
+
const daemonWasRunning = isDaemonInstalled() && isDaemonActive();
|
|
949
1049
|
|
|
950
1050
|
const steps = [
|
|
951
1051
|
...(daemonWasRunning ? ['Stopping daemon'] : []),
|
|
@@ -962,8 +1062,12 @@ async function update() {
|
|
|
962
1062
|
// Stop daemon before updating files
|
|
963
1063
|
if (daemonWasRunning) {
|
|
964
1064
|
try {
|
|
965
|
-
|
|
966
|
-
|
|
1065
|
+
if (PLATFORM === 'darwin') {
|
|
1066
|
+
execSync(`launchctl unload "${LAUNCHD_PLIST_PATH}"`, { stdio: 'ignore' });
|
|
1067
|
+
} else {
|
|
1068
|
+
const cmd = needsSudo() ? `sudo systemctl stop ${SERVICE_NAME}` : `systemctl stop ${SERVICE_NAME}`;
|
|
1069
|
+
execSync(cmd, { stdio: 'ignore' });
|
|
1070
|
+
}
|
|
967
1071
|
} catch {}
|
|
968
1072
|
stepper.advance();
|
|
969
1073
|
}
|
|
@@ -1053,8 +1157,12 @@ async function update() {
|
|
|
1053
1157
|
// Restart daemon if it was running
|
|
1054
1158
|
if (daemonWasRunning) {
|
|
1055
1159
|
try {
|
|
1056
|
-
|
|
1057
|
-
|
|
1160
|
+
if (PLATFORM === 'darwin') {
|
|
1161
|
+
execSync(`launchctl load "${LAUNCHD_PLIST_PATH}"`, { stdio: 'ignore' });
|
|
1162
|
+
} else {
|
|
1163
|
+
const cmd = needsSudo() ? `sudo systemctl start ${SERVICE_NAME}` : `systemctl start ${SERVICE_NAME}`;
|
|
1164
|
+
execSync(cmd, { stdio: 'ignore' });
|
|
1165
|
+
}
|
|
1058
1166
|
} catch {}
|
|
1059
1167
|
stepper.advance();
|
|
1060
1168
|
}
|
|
@@ -1073,15 +1181,19 @@ async function update() {
|
|
|
1073
1181
|
}
|
|
1074
1182
|
|
|
1075
1183
|
if (daemonWasRunning) {
|
|
1076
|
-
if (
|
|
1184
|
+
if (isDaemonActive()) {
|
|
1077
1185
|
console.log(` ${c.blue}✔${c.reset} Daemon restarted with new version.\n`);
|
|
1078
1186
|
} else {
|
|
1079
1187
|
console.log(` ${c.yellow}⚠${c.reset} Daemon may still be starting. Check ${c.pink}fluxy daemon status${c.reset}\n`);
|
|
1080
1188
|
}
|
|
1081
|
-
} else if (
|
|
1189
|
+
} else if (isDaemonInstalled()) {
|
|
1082
1190
|
try {
|
|
1083
|
-
|
|
1084
|
-
|
|
1191
|
+
if (PLATFORM === 'darwin') {
|
|
1192
|
+
execSync(`launchctl unload "${LAUNCHD_PLIST_PATH}" 2>/dev/null; launchctl load "${LAUNCHD_PLIST_PATH}"`, { stdio: 'ignore' });
|
|
1193
|
+
} else {
|
|
1194
|
+
const cmd = needsSudo() ? `sudo systemctl start ${SERVICE_NAME}` : `systemctl start ${SERVICE_NAME}`;
|
|
1195
|
+
execSync(cmd, { stdio: 'ignore' });
|
|
1196
|
+
}
|
|
1085
1197
|
console.log(` ${c.blue}✔${c.reset} Daemon started with new version.\n`);
|
|
1086
1198
|
} catch {
|
|
1087
1199
|
console.log(` ${c.dim}Run ${c.reset}${c.pink}fluxy daemon start${c.reset}${c.dim} to launch.${c.reset}\n`);
|
|
@@ -1095,15 +1207,131 @@ async function update() {
|
|
|
1095
1207
|
|
|
1096
1208
|
async function daemon(sub) {
|
|
1097
1209
|
// Platform guard
|
|
1098
|
-
if (
|
|
1099
|
-
const hint =
|
|
1100
|
-
? 'Use
|
|
1101
|
-
: '
|
|
1102
|
-
console.log(`\n ${c.yellow}⚠${c.reset} Daemon mode is
|
|
1210
|
+
if (!hasDaemonSupport()) {
|
|
1211
|
+
const hint = PLATFORM === 'win32'
|
|
1212
|
+
? 'Use Task Scheduler to keep Fluxy running in the background.'
|
|
1213
|
+
: 'No supported daemon system found.';
|
|
1214
|
+
console.log(`\n ${c.yellow}⚠${c.reset} Daemon mode is not supported on this platform.`);
|
|
1103
1215
|
console.log(` ${c.dim}${hint}${c.reset}\n`);
|
|
1104
1216
|
process.exit(1);
|
|
1105
1217
|
}
|
|
1106
1218
|
|
|
1219
|
+
const action = sub || 'install';
|
|
1220
|
+
|
|
1221
|
+
// ── macOS (launchd) ──
|
|
1222
|
+
if (PLATFORM === 'darwin') {
|
|
1223
|
+
switch (action) {
|
|
1224
|
+
case 'install': {
|
|
1225
|
+
const dataDir = path.join(os.homedir(), '.fluxy');
|
|
1226
|
+
if (!fs.existsSync(path.join(dataDir, 'supervisor', 'index.ts'))) {
|
|
1227
|
+
console.log(`\n ${c.red}✗${c.reset} Run ${c.pink}fluxy init${c.reset} first.\n`);
|
|
1228
|
+
process.exit(1);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// Unload existing plist if loaded
|
|
1232
|
+
if (isLaunchdInstalled()) {
|
|
1233
|
+
try { execSync(`launchctl unload "${LAUNCHD_PLIST_PATH}" 2>/dev/null`, { stdio: 'ignore' }); } catch {}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
const nodePath = process.env.FLUXY_NODE_PATH || process.execPath;
|
|
1237
|
+
const plist = generateLaunchdPlist({ nodePath, dataDir });
|
|
1238
|
+
|
|
1239
|
+
// Ensure LaunchAgents directory exists
|
|
1240
|
+
fs.mkdirSync(path.dirname(LAUNCHD_PLIST_PATH), { recursive: true });
|
|
1241
|
+
fs.writeFileSync(LAUNCHD_PLIST_PATH, plist);
|
|
1242
|
+
execSync(`launchctl load "${LAUNCHD_PLIST_PATH}"`, { stdio: 'ignore' });
|
|
1243
|
+
|
|
1244
|
+
// Verify it started
|
|
1245
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
1246
|
+
if (isLaunchdActive()) {
|
|
1247
|
+
console.log(`\n ${c.blue}✔${c.reset} Fluxy daemon installed and running.`);
|
|
1248
|
+
console.log(` ${c.dim}It will auto-start on login.${c.reset}`);
|
|
1249
|
+
console.log(`\n ${c.dim}View logs:${c.reset} ${c.pink}fluxy daemon logs${c.reset}`);
|
|
1250
|
+
console.log(` ${c.dim}Stop:${c.reset} ${c.pink}fluxy daemon stop${c.reset}`);
|
|
1251
|
+
console.log(` ${c.dim}Uninstall:${c.reset} ${c.pink}fluxy daemon uninstall${c.reset}\n`);
|
|
1252
|
+
} else {
|
|
1253
|
+
console.log(`\n ${c.yellow}⚠${c.reset} Plist installed but process may not be running.`);
|
|
1254
|
+
console.log(` ${c.dim}Check with: ${c.reset}${c.pink}fluxy daemon status${c.reset}\n`);
|
|
1255
|
+
}
|
|
1256
|
+
break;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
case 'stop': {
|
|
1260
|
+
if (!isLaunchdInstalled()) {
|
|
1261
|
+
console.log(`\n ${c.yellow}⚠${c.reset} Daemon not installed. Run ${c.pink}fluxy daemon install${c.reset} first.\n`);
|
|
1262
|
+
process.exit(1);
|
|
1263
|
+
}
|
|
1264
|
+
execSync(`launchctl unload "${LAUNCHD_PLIST_PATH}"`, { stdio: 'ignore' });
|
|
1265
|
+
console.log(`\n ${c.blue}✔${c.reset} Fluxy daemon stopped.\n`);
|
|
1266
|
+
break;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
case 'start': {
|
|
1270
|
+
if (!isLaunchdInstalled()) {
|
|
1271
|
+
console.log(`\n ${c.yellow}⚠${c.reset} Daemon not installed. Run ${c.pink}fluxy daemon install${c.reset} first.\n`);
|
|
1272
|
+
process.exit(1);
|
|
1273
|
+
}
|
|
1274
|
+
// Reload: unload first in case it's already loaded, then load
|
|
1275
|
+
try { execSync(`launchctl unload "${LAUNCHD_PLIST_PATH}" 2>/dev/null`, { stdio: 'ignore' }); } catch {}
|
|
1276
|
+
execSync(`launchctl load "${LAUNCHD_PLIST_PATH}"`, { stdio: 'ignore' });
|
|
1277
|
+
console.log(`\n ${c.blue}✔${c.reset} Fluxy daemon started.\n`);
|
|
1278
|
+
break;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
case 'restart': {
|
|
1282
|
+
if (!isLaunchdInstalled()) {
|
|
1283
|
+
console.log(`\n ${c.yellow}⚠${c.reset} Daemon not installed. Run ${c.pink}fluxy daemon install${c.reset} first.\n`);
|
|
1284
|
+
process.exit(1);
|
|
1285
|
+
}
|
|
1286
|
+
try { execSync(`launchctl unload "${LAUNCHD_PLIST_PATH}" 2>/dev/null`, { stdio: 'ignore' }); } catch {}
|
|
1287
|
+
execSync(`launchctl load "${LAUNCHD_PLIST_PATH}"`, { stdio: 'ignore' });
|
|
1288
|
+
console.log(`\n ${c.blue}✔${c.reset} Fluxy daemon restarted.\n`);
|
|
1289
|
+
break;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
case 'status': {
|
|
1293
|
+
if (!isLaunchdInstalled()) {
|
|
1294
|
+
console.log(`\n ${c.dim}●${c.reset} Daemon not installed.\n`);
|
|
1295
|
+
break;
|
|
1296
|
+
}
|
|
1297
|
+
const active = isLaunchdActive();
|
|
1298
|
+
if (active) {
|
|
1299
|
+
console.log(`\n ${c.blue}●${c.reset} Fluxy daemon is running.`);
|
|
1300
|
+
} else {
|
|
1301
|
+
console.log(`\n ${c.dim}●${c.reset} Fluxy daemon is stopped.`);
|
|
1302
|
+
}
|
|
1303
|
+
console.log(` ${c.dim}Plist:${c.reset} ${LAUNCHD_PLIST_PATH}`);
|
|
1304
|
+
console.log(` ${c.dim}Logs:${c.reset} ${LAUNCHD_LOG_DIR}/\n`);
|
|
1305
|
+
break;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
case 'logs': {
|
|
1309
|
+
const logFile = path.join(LAUNCHD_LOG_DIR, 'stdout.log');
|
|
1310
|
+
if (!fs.existsSync(logFile)) {
|
|
1311
|
+
console.log(`\n ${c.dim}No logs found at ${logFile}${c.reset}\n`);
|
|
1312
|
+
break;
|
|
1313
|
+
}
|
|
1314
|
+
spawnSync('tail', ['-f', '-n', '50', logFile], { stdio: 'inherit' });
|
|
1315
|
+
break;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
case 'uninstall': {
|
|
1319
|
+
try { execSync(`launchctl unload "${LAUNCHD_PLIST_PATH}" 2>/dev/null`, { stdio: 'ignore' }); } catch {}
|
|
1320
|
+
if (fs.existsSync(LAUNCHD_PLIST_PATH)) fs.unlinkSync(LAUNCHD_PLIST_PATH);
|
|
1321
|
+
console.log(`\n ${c.blue}✔${c.reset} Fluxy daemon uninstalled.\n`);
|
|
1322
|
+
break;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
default:
|
|
1326
|
+
console.log(`\n ${c.red}✗${c.reset} Unknown daemon command: ${action}`);
|
|
1327
|
+
console.log(` ${c.dim}Available: install, start, stop, restart, status, logs, uninstall${c.reset}\n`);
|
|
1328
|
+
process.exit(1);
|
|
1329
|
+
}
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// ── Linux (systemd) ──
|
|
1334
|
+
|
|
1107
1335
|
// Check systemd is available
|
|
1108
1336
|
try {
|
|
1109
1337
|
execSync('systemctl --version', { stdio: 'ignore' });
|
|
@@ -1112,8 +1340,6 @@ async function daemon(sub) {
|
|
|
1112
1340
|
process.exit(1);
|
|
1113
1341
|
}
|
|
1114
1342
|
|
|
1115
|
-
const action = sub || 'install';
|
|
1116
|
-
|
|
1117
1343
|
switch (action) {
|
|
1118
1344
|
case 'install': {
|
|
1119
1345
|
if (!fs.existsSync(path.join(getRealHome(), '.fluxy', 'supervisor', 'index.ts'))) {
|
|
@@ -1250,12 +1476,16 @@ async function tunnel(sub) {
|
|
|
1250
1476
|
console.log(` ${c.blue}✔${c.reset} Fluxy config updated\n`);
|
|
1251
1477
|
|
|
1252
1478
|
// Offer restart if daemon is running
|
|
1253
|
-
if (
|
|
1479
|
+
if (isDaemonInstalled() && isDaemonActive()) {
|
|
1254
1480
|
const restart = await ask(` ${c.bold}Restart daemon now?${c.reset} ${c.dim}(Y/n)${c.reset}: `);
|
|
1255
1481
|
if (!restart || restart.toLowerCase() === 'y') {
|
|
1256
1482
|
try {
|
|
1257
|
-
|
|
1258
|
-
|
|
1483
|
+
if (PLATFORM === 'darwin') {
|
|
1484
|
+
execSync(`launchctl unload "${LAUNCHD_PLIST_PATH}" 2>/dev/null; launchctl load "${LAUNCHD_PLIST_PATH}"`, { stdio: 'ignore' });
|
|
1485
|
+
} else {
|
|
1486
|
+
const cmd = needsSudo() ? `sudo systemctl restart ${SERVICE_NAME}` : `systemctl restart ${SERVICE_NAME}`;
|
|
1487
|
+
execSync(cmd, { stdio: 'ignore' });
|
|
1488
|
+
}
|
|
1259
1489
|
console.log(`\n ${c.blue}✔${c.reset} Daemon restarted.\n`);
|
|
1260
1490
|
} catch {
|
|
1261
1491
|
console.log(`\n ${c.yellow}⚠${c.reset} Restart failed. Try ${c.pink}fluxy daemon restart${c.reset}\n`);
|
|
@@ -1300,7 +1530,7 @@ async function tunnel(sub) {
|
|
|
1300
1530
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
1301
1531
|
console.log(`\n ${c.blue}✔${c.reset} Tunnel mode reset to ${c.bold}quick${c.reset} (random trycloudflare.com URL).\n`);
|
|
1302
1532
|
|
|
1303
|
-
if (
|
|
1533
|
+
if (isDaemonInstalled() && isDaemonActive()) {
|
|
1304
1534
|
console.log(` ${c.dim}Restart the daemon to apply: ${c.reset}${c.pink}fluxy daemon restart${c.reset}\n`);
|
|
1305
1535
|
}
|
|
1306
1536
|
break;
|
package/package.json
CHANGED
package/supervisor/index.ts
CHANGED
|
@@ -673,21 +673,32 @@ export async function startSupervisor() {
|
|
|
673
673
|
let pendingBackendRestart = false; // Set when file watcher fires during agent turn
|
|
674
674
|
let pendingUpdate = false; // Set when .update file is created during agent turn
|
|
675
675
|
|
|
676
|
-
// Run fluxy update in a
|
|
676
|
+
// Run fluxy update in a detached process so it survives daemon stop/restart
|
|
677
677
|
function runDeferredUpdate() {
|
|
678
678
|
const cliPath = path.join(PKG_DIR, 'bin', 'cli.js');
|
|
679
|
-
|
|
680
|
-
log.info('Deferred update triggered — running fluxy update via systemd-run...');
|
|
679
|
+
log.info('Deferred update triggered — running fluxy update...');
|
|
681
680
|
try {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
681
|
+
if (os.platform() === 'linux') {
|
|
682
|
+
// Use systemd-run on Linux so the update survives daemon restart
|
|
683
|
+
const user = os.userInfo().username;
|
|
684
|
+
const child = cpSpawn('sudo', [
|
|
685
|
+
'systemd-run', '--quiet',
|
|
686
|
+
'--unit=fluxy-update',
|
|
687
|
+
'--uid=' + user,
|
|
688
|
+
'--setenv=HOME=' + os.homedir(),
|
|
689
|
+
'--setenv=PATH=' + process.env.PATH,
|
|
690
|
+
process.execPath, cliPath, 'update',
|
|
691
|
+
], { detached: true, stdio: 'ignore' });
|
|
692
|
+
child.unref();
|
|
693
|
+
} else {
|
|
694
|
+
// macOS / other: detached child process survives parent exit
|
|
695
|
+
const child = cpSpawn(process.execPath, [cliPath, 'update'], {
|
|
696
|
+
detached: true,
|
|
697
|
+
stdio: 'ignore',
|
|
698
|
+
env: { ...process.env },
|
|
699
|
+
});
|
|
700
|
+
child.unref();
|
|
701
|
+
}
|
|
691
702
|
} catch (err) {
|
|
692
703
|
log.error(`Deferred update failed: ${err instanceof Error ? err.message : err}`);
|
|
693
704
|
}
|