fluxy-bot 0.9.0 → 0.9.2
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 +57 -45
- package/cli/commands/daemon.ts +31 -0
- package/cli/commands/init.ts +24 -0
- package/cli/commands/start.ts +91 -0
- package/cli/commands/tunnel.ts +175 -0
- package/cli/commands/update.ts +141 -0
- package/cli/core/base-adapter.ts +99 -0
- package/cli/core/cloudflared.ts +71 -0
- package/cli/core/config.ts +58 -0
- package/cli/core/os-detector.ts +31 -0
- package/cli/core/server.ts +87 -0
- package/cli/core/types.ts +15 -0
- package/cli/index.ts +72 -0
- package/cli/platforms/darwin.ts +110 -0
- package/cli/platforms/index.ts +20 -0
- package/cli/platforms/linux.ts +116 -0
- package/cli/platforms/win32.ts +20 -0
- package/cli/utils/ui.ts +24 -0
- package/package.json +7 -3
- package/shared/paths.ts +1 -1
- package/supervisor/backend.ts +7 -6
- package/supervisor/fluxy-agent.ts +1 -1
- package/supervisor/index.ts +13 -3
- package/tsconfig.json +1 -1
- package/vite.config.ts +9 -2
package/bin/cli.js
CHANGED
|
@@ -1232,8 +1232,8 @@ async function update() {
|
|
|
1232
1232
|
const daemonWasRunning = isDaemonInstalled() && isDaemonActive();
|
|
1233
1233
|
|
|
1234
1234
|
const steps = [
|
|
1235
|
-
...(daemonWasRunning ? ['Stopping daemon'] : []),
|
|
1236
1235
|
'Downloading update',
|
|
1236
|
+
...(daemonWasRunning ? ['Stopping daemon'] : []),
|
|
1237
1237
|
'Updating files',
|
|
1238
1238
|
'Installing dependencies',
|
|
1239
1239
|
'Building interface',
|
|
@@ -1243,20 +1243,7 @@ async function update() {
|
|
|
1243
1243
|
const stepper = new Stepper(steps);
|
|
1244
1244
|
stepper.start();
|
|
1245
1245
|
|
|
1246
|
-
//
|
|
1247
|
-
if (daemonWasRunning) {
|
|
1248
|
-
try {
|
|
1249
|
-
if (PLATFORM === 'darwin') {
|
|
1250
|
-
execSync(`launchctl unload "${LAUNCHD_PLIST_PATH}"`, { stdio: 'ignore' });
|
|
1251
|
-
} else {
|
|
1252
|
-
const cmd = needsSudo() ? `sudo systemctl stop ${SERVICE_NAME}` : `systemctl stop ${SERVICE_NAME}`;
|
|
1253
|
-
execSync(cmd, { stdio: 'ignore' });
|
|
1254
|
-
}
|
|
1255
|
-
} catch {}
|
|
1256
|
-
stepper.advance();
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
// Download tarball
|
|
1246
|
+
// Download tarball FIRST — before stopping daemon, so failure doesn't leave bot offline
|
|
1260
1247
|
const tarballUrl = latest.dist.tarball;
|
|
1261
1248
|
const tmpDir = path.join(os.tmpdir(), `fluxy-update-${Date.now()}`);
|
|
1262
1249
|
fs.mkdirSync(tmpDir, { recursive: true });
|
|
@@ -1264,65 +1251,90 @@ async function update() {
|
|
|
1264
1251
|
|
|
1265
1252
|
try {
|
|
1266
1253
|
const res = await fetch(tarballUrl);
|
|
1267
|
-
if (!res.ok) throw new Error();
|
|
1254
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1268
1255
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
1269
1256
|
fs.writeFileSync(tarball, buf);
|
|
1270
1257
|
execSync(`tar xzf "${tarball}" -C "${tmpDir}"`, { stdio: 'ignore' });
|
|
1271
|
-
} catch {
|
|
1258
|
+
} catch (e) {
|
|
1272
1259
|
stepper.finish();
|
|
1273
|
-
console.log(`\n ${c.red}✗${c.reset} Download failed\n`);
|
|
1260
|
+
console.log(`\n ${c.red}✗${c.reset} Download failed: ${e.message}\n`);
|
|
1274
1261
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1275
1262
|
process.exit(1);
|
|
1276
1263
|
}
|
|
1277
1264
|
stepper.advance();
|
|
1278
1265
|
|
|
1266
|
+
// Stop daemon AFTER download succeeds — minimizes downtime
|
|
1267
|
+
if (daemonWasRunning) {
|
|
1268
|
+
try {
|
|
1269
|
+
if (PLATFORM === 'darwin') {
|
|
1270
|
+
execSync(`launchctl unload "${LAUNCHD_PLIST_PATH}"`, { stdio: 'ignore' });
|
|
1271
|
+
} else {
|
|
1272
|
+
const cmd = needsSudo() ? `sudo systemctl stop ${SERVICE_NAME}` : `systemctl stop ${SERVICE_NAME}`;
|
|
1273
|
+
execSync(cmd, { stdio: 'ignore' });
|
|
1274
|
+
}
|
|
1275
|
+
} catch (e) {
|
|
1276
|
+
console.log(` ${c.yellow}⚠${c.reset} Could not stop daemon: ${e.message}`);
|
|
1277
|
+
}
|
|
1278
|
+
stepper.advance();
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1279
1281
|
const extracted = path.join(tmpDir, 'package');
|
|
1280
1282
|
|
|
1281
1283
|
// Update code directories (preserve workspace/ user data)
|
|
1282
|
-
|
|
1283
|
-
const
|
|
1284
|
-
|
|
1285
|
-
fs.
|
|
1284
|
+
try {
|
|
1285
|
+
for (const dir of ['bin', 'supervisor', 'worker', 'shared', 'scripts']) {
|
|
1286
|
+
const src = path.join(extracted, dir);
|
|
1287
|
+
if (fs.existsSync(src)) {
|
|
1288
|
+
fs.cpSync(src, path.join(DATA_DIR, dir), { recursive: true, force: true });
|
|
1289
|
+
}
|
|
1286
1290
|
}
|
|
1287
|
-
}
|
|
1288
1291
|
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1292
|
+
// Copy workspace template only if it doesn't exist yet
|
|
1293
|
+
if (!fs.existsSync(path.join(DATA_DIR, 'workspace'))) {
|
|
1294
|
+
const wsSrc = path.join(extracted, 'workspace');
|
|
1295
|
+
if (fs.existsSync(wsSrc)) {
|
|
1296
|
+
fs.cpSync(wsSrc, path.join(DATA_DIR, 'workspace'), { recursive: true });
|
|
1297
|
+
}
|
|
1294
1298
|
}
|
|
1295
|
-
}
|
|
1296
1299
|
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1300
|
+
// Update code files (never touches config.json, memory.db, etc.)
|
|
1301
|
+
for (const file of ['package.json', 'vite.config.ts', 'vite.fluxy.config.ts', 'tsconfig.json', 'postcss.config.js', 'components.json']) {
|
|
1302
|
+
const src = path.join(extracted, file);
|
|
1303
|
+
if (fs.existsSync(src)) {
|
|
1304
|
+
fs.copyFileSync(src, path.join(DATA_DIR, file));
|
|
1305
|
+
}
|
|
1302
1306
|
}
|
|
1303
|
-
}
|
|
1304
1307
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1308
|
+
// Update pre-built UI from tarball
|
|
1309
|
+
const distSrc = path.join(extracted, 'dist-fluxy');
|
|
1310
|
+
if (fs.existsSync(distSrc)) {
|
|
1311
|
+
const dst = path.join(DATA_DIR, 'dist-fluxy');
|
|
1312
|
+
if (fs.existsSync(dst)) fs.rmSync(dst, { recursive: true });
|
|
1313
|
+
fs.cpSync(distSrc, dst, { recursive: true });
|
|
1314
|
+
}
|
|
1315
|
+
} catch (e) {
|
|
1316
|
+
console.log(` ${c.red}✗${c.reset} File copy failed: ${e.message}`);
|
|
1317
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1318
|
+
process.exit(1);
|
|
1311
1319
|
}
|
|
1312
1320
|
|
|
1313
1321
|
stepper.advance();
|
|
1314
1322
|
|
|
1315
|
-
|
|
1323
|
+
const distDst = path.join(DATA_DIR, 'dist-fluxy');
|
|
1324
|
+
|
|
1325
|
+
// Install dependencies (5 min timeout to prevent hanging forever)
|
|
1316
1326
|
try {
|
|
1317
|
-
execSync('npm install --omit=dev', { cwd: DATA_DIR, stdio: 'ignore' });
|
|
1318
|
-
} catch {
|
|
1327
|
+
execSync('npm install --omit=dev', { cwd: DATA_DIR, stdio: 'ignore', timeout: 300_000 });
|
|
1328
|
+
} catch (e) {
|
|
1329
|
+
console.log(` ${c.yellow}⚠${c.reset} npm install issue: ${e.message}`);
|
|
1330
|
+
}
|
|
1319
1331
|
stepper.advance();
|
|
1320
1332
|
|
|
1321
1333
|
// Rebuild UI if not in tarball
|
|
1322
1334
|
if (!fs.existsSync(path.join(distDst, 'onboard.html'))) {
|
|
1323
1335
|
try {
|
|
1324
1336
|
if (fs.existsSync(distDst)) fs.rmSync(distDst, { recursive: true });
|
|
1325
|
-
execSync('npm run build:fluxy', { cwd: DATA_DIR, stdio: 'ignore' });
|
|
1337
|
+
execSync('npm run build:fluxy', { cwd: DATA_DIR, stdio: 'ignore', timeout: 300_000 });
|
|
1326
1338
|
} catch {}
|
|
1327
1339
|
}
|
|
1328
1340
|
stepper.advance();
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
|
|
4
|
+
import { DATA_DIR } from '../core/config.js';
|
|
5
|
+
import type { DaemonAction, DaemonConfig } from '../core/types.js';
|
|
6
|
+
import { getAdapter } from '../platforms/index.js';
|
|
7
|
+
|
|
8
|
+
export function registerDaemonCommand(program: Command) {
|
|
9
|
+
const adapter = getAdapter();
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.command('daemon <action>')
|
|
13
|
+
.description('Manage the Fluxy background daemon (install, start, stop, restart, status, logs, uninstall)')
|
|
14
|
+
.action(async (action: string) => {
|
|
15
|
+
const validActions: DaemonAction[] = ['install', 'start', 'stop', 'restart', 'status', 'logs', 'uninstall'];
|
|
16
|
+
|
|
17
|
+
if (!validActions.includes(action as DaemonAction)) {
|
|
18
|
+
console.error(`Invalid daemon action: ${action}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const config: DaemonConfig = {
|
|
23
|
+
user: process.env.SUDO_USER || os.userInfo().username,
|
|
24
|
+
home: process.env.FLUXY_REAL_HOME || os.homedir(),
|
|
25
|
+
nodePath: process.env.FLUXY_NODE_PATH || process.execPath,
|
|
26
|
+
dataDir: DATA_DIR,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
await adapter.handleDaemonAction(action as DaemonAction, config);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
|
|
6
|
+
import { pkg, DATA_DIR, createConfig } from '../core/config.js';
|
|
7
|
+
import { runTunnelSetup } from './tunnel.js';
|
|
8
|
+
|
|
9
|
+
export function registerInitCommand(program: Command) {
|
|
10
|
+
program
|
|
11
|
+
.command('init')
|
|
12
|
+
.description('Initialize Fluxy configuration')
|
|
13
|
+
.action(async () => {
|
|
14
|
+
createConfig();
|
|
15
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
16
|
+
fs.writeFileSync(path.join(DATA_DIR, '.version'), pkg.version || '1.0.0');
|
|
17
|
+
|
|
18
|
+
console.log(pc.green('✓ Initialized base config.\n'));
|
|
19
|
+
|
|
20
|
+
await runTunnelSetup();
|
|
21
|
+
|
|
22
|
+
console.log(pc.dim('Run ' + pc.magenta('fluxy start') + ' to launch the server.'));
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { spinner } from '@clack/prompts';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
|
|
6
|
+
import { loadConfig, createConfig, CONFIG_PATH } from '../core/config.js';
|
|
7
|
+
import { getAdapter } from '../platforms/index.js';
|
|
8
|
+
import { banner, commandExample } from '../utils/ui.js';
|
|
9
|
+
import { bootServer } from '../core/server.js';
|
|
10
|
+
import { CloudflaredManager } from '../core/cloudflared.js';
|
|
11
|
+
|
|
12
|
+
export function registerStartCommand(program: Command) {
|
|
13
|
+
const adapter = getAdapter();
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.command('start', { isDefault: true })
|
|
17
|
+
.description('Start the Fluxy server and tunnel')
|
|
18
|
+
.action(async () => {
|
|
19
|
+
banner();
|
|
20
|
+
|
|
21
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
22
|
+
console.log(pc.yellow(' ● Running first-time setup (creating config)...\n'));
|
|
23
|
+
createConfig();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const config = loadConfig();
|
|
27
|
+
|
|
28
|
+
if (adapter.isInstalled && adapter.isActive) {
|
|
29
|
+
console.log(pc.blue(' ● Fluxy is already running as a daemon.\n'));
|
|
30
|
+
if (config.relay?.url) {
|
|
31
|
+
console.log(commandExample('URL: ', config.relay.url));
|
|
32
|
+
}
|
|
33
|
+
console.log(commandExample(`Status: `, `fluxy daemon status`));
|
|
34
|
+
console.log(commandExample(`Logs: `, `fluxy daemon logs`));
|
|
35
|
+
console.log(commandExample(`Restart:`, `fluxy daemon restart`));
|
|
36
|
+
console.log(commandExample(`Stop: `, `fluxy daemon stop`));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const tunnelMode = config.tunnel?.mode ?? ((config.tunnel as any)?.enabled === false ? 'off' : 'quick');
|
|
41
|
+
const hasTunnel = tunnelMode !== 'off';
|
|
42
|
+
|
|
43
|
+
if (hasTunnel && tunnelMode !== 'named') {
|
|
44
|
+
const s1 = spinner();
|
|
45
|
+
s1.start('Ensuring Cloudflared is installed...');
|
|
46
|
+
CloudflaredManager.install();
|
|
47
|
+
s1.stop(pc.green('Cloudflared ready'));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const s = spinner();
|
|
51
|
+
s.start('Starting server');
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const result = await bootServer({
|
|
55
|
+
onTunnelUp: url => {
|
|
56
|
+
if (!hasTunnel) return;
|
|
57
|
+
s.message(`Connecting tunnel... up at ${pc.blue(url || '')}`);
|
|
58
|
+
},
|
|
59
|
+
onReady: () => {
|
|
60
|
+
s.message('Preparing dashboard...');
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
s.stop(pc.green('Server running'));
|
|
65
|
+
|
|
66
|
+
console.log(`\n${pc.bold('Fluxy is ready!')}`);
|
|
67
|
+
console.log(` ${pc.dim('Local:')} ${pc.blue(`http://localhost:${config.port || 3000}`)}`);
|
|
68
|
+
|
|
69
|
+
if (result.tunnelUrl && hasTunnel) {
|
|
70
|
+
console.log(` ${pc.dim('Tunnel:')} ${pc.blue(result.tunnelUrl)}`);
|
|
71
|
+
}
|
|
72
|
+
if (result.relayUrl) {
|
|
73
|
+
console.log(` ${pc.dim('Relay:')} ${pc.blue(result.relayUrl)}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log(`\n ${pc.dim('Press Ctrl+C to stop')}\n`);
|
|
77
|
+
|
|
78
|
+
Promise.race([
|
|
79
|
+
result.viteWarm,
|
|
80
|
+
new Promise(r => setTimeout(r, 60_000)),
|
|
81
|
+
]).then(() => {});
|
|
82
|
+
|
|
83
|
+
// Block forever
|
|
84
|
+
await new Promise(() => {});
|
|
85
|
+
} catch (err: any) {
|
|
86
|
+
s.stop(pc.red('Failed to start server'));
|
|
87
|
+
console.error(err.message);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { cancel, intro, isCancel, select, text, spinner } from '@clack/prompts';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
|
|
8
|
+
import { loadConfig, safeLoadConfig, CONFIG_PATH, DATA_DIR } from '../core/config.js';
|
|
9
|
+
import { getAdapter } from '../platforms/index.js';
|
|
10
|
+
import { CloudflaredManager } from '../core/cloudflared.js';
|
|
11
|
+
|
|
12
|
+
async function runNamedTunnelSetup() {
|
|
13
|
+
CloudflaredManager.install();
|
|
14
|
+
|
|
15
|
+
console.log(pc.bold(pc.white('\n Step 1: Log in to Cloudflare\n')));
|
|
16
|
+
console.log(pc.dim(' This will open a browser window. Authorize the domain you want to use.\n'));
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
CloudflaredManager.exec('tunnel login', { stdio: 'inherit' });
|
|
20
|
+
} catch {
|
|
21
|
+
console.error(pc.red('cloudflared login failed.'));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const tunnelName = await text({
|
|
26
|
+
message: 'Tunnel name (default: fluxy):',
|
|
27
|
+
defaultValue: 'fluxy',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (isCancel(tunnelName)) { cancel('Setup cancelled.'); process.exit(0); }
|
|
31
|
+
|
|
32
|
+
const s = spinner();
|
|
33
|
+
s.start(`Creating tunnel "${tunnelName}"...`);
|
|
34
|
+
|
|
35
|
+
let createOutput;
|
|
36
|
+
try {
|
|
37
|
+
createOutput = CloudflaredManager.exec(`tunnel create ${tunnelName}`, { encoding: 'utf-8' });
|
|
38
|
+
} catch {
|
|
39
|
+
s.stop(pc.red('Failed to create tunnel. It may already exist.'));
|
|
40
|
+
console.log(pc.dim('Try: fluxy tunnel list (or cloudflared tunnel list)'));
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const uuidMatch = createOutput?.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i);
|
|
45
|
+
if (!uuidMatch) {
|
|
46
|
+
s.stop(pc.red('Could not parse tunnel UUID from output.'));
|
|
47
|
+
console.log(pc.dim(createOutput?.toString() || ''));
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const tunnelUuid = uuidMatch[1];
|
|
52
|
+
s.stop(pc.green(`Tunnel created: ${pc.dim(tunnelUuid)}`));
|
|
53
|
+
|
|
54
|
+
const domain = await text({ message: 'Your domain (e.g. bot.mydomain.com):' });
|
|
55
|
+
if (isCancel(domain) || !domain) { cancel('Domain is required.'); process.exit(1); }
|
|
56
|
+
|
|
57
|
+
const { port = 3000 } = safeLoadConfig();
|
|
58
|
+
const cfHome = path.join(os.homedir(), '.cloudflared');
|
|
59
|
+
const cfConfigPath = path.join(DATA_DIR, 'cloudflared-config.yml');
|
|
60
|
+
|
|
61
|
+
const yamlContent = `tunnel: ${tunnelUuid}\ncredentials-file: ${path.join(cfHome, `${tunnelUuid}.json`)}\ningress:\n - service: http://localhost:${port}\n`;
|
|
62
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
63
|
+
fs.writeFileSync(cfConfigPath, yamlContent);
|
|
64
|
+
|
|
65
|
+
console.log(`\n${pc.dim('─────────────────────────────────')}\n`);
|
|
66
|
+
console.log(` ${pc.bold(pc.white('Tunnel created!'))}`);
|
|
67
|
+
console.log(` ${pc.white('Add a CNAME record pointing to:')}\n`);
|
|
68
|
+
console.log(` ${pc.bold(pc.magenta(`${tunnelUuid}.cfargotunnel.com`))}\n`);
|
|
69
|
+
console.log(` ${pc.dim(`Or run: cloudflared tunnel route dns ${tunnelName} ${domain}`)}\n`);
|
|
70
|
+
|
|
71
|
+
return { tunnelName, domain, cfConfigPath };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function runTunnelSetup() {
|
|
75
|
+
intro(pc.inverse(' Fluxy Tunnel Setup '));
|
|
76
|
+
|
|
77
|
+
const mode = await select({
|
|
78
|
+
message: 'How do you want to connect your bot?',
|
|
79
|
+
options: [
|
|
80
|
+
{
|
|
81
|
+
value: 'quick',
|
|
82
|
+
label: `Quick Tunnel ${pc.green('[Easy and Fast]')}`,
|
|
83
|
+
hint: pc.dim('Random CloudFlare tunnel URL on every start/update\n Optional: Use Fluxy Relay Server and access your bot at my.fluxy.bot/YOURBOT'),
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
value: 'named',
|
|
87
|
+
label: `Named Tunnel ${pc.yellow('[Advanced]')}`,
|
|
88
|
+
hint: pc.dim('Persistent URL with your own domain\n Requires a CloudFlare account + domain'),
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
value: 'off',
|
|
92
|
+
label: `Private Network ${pc.cyan('[Secure]')}`,
|
|
93
|
+
hint: pc.dim('No public URL — access via local network or VPN only\n Your bot stays invisible to the internet'),
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (isCancel(mode)) { cancel('Operation cancelled.'); process.exit(1); }
|
|
99
|
+
|
|
100
|
+
let setupData: any = {};
|
|
101
|
+
if (mode === 'named') setupData = await runNamedTunnelSetup();
|
|
102
|
+
|
|
103
|
+
const config = safeLoadConfig();
|
|
104
|
+
if (mode === 'named') {
|
|
105
|
+
config.tunnel = {
|
|
106
|
+
mode: 'named',
|
|
107
|
+
name: setupData.tunnelName,
|
|
108
|
+
domain: setupData.domain,
|
|
109
|
+
configPath: setupData.cfConfigPath,
|
|
110
|
+
};
|
|
111
|
+
} else {
|
|
112
|
+
config.tunnel = { mode: mode as any };
|
|
113
|
+
delete config.tunnelUrl;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
117
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
118
|
+
|
|
119
|
+
console.log(pc.green('\n✓ Fluxy tunnel configuration updated.\n'));
|
|
120
|
+
return mode;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function registerTunnelCommand(program: Command) {
|
|
124
|
+
const adapter = getAdapter();
|
|
125
|
+
|
|
126
|
+
program
|
|
127
|
+
.command('tunnel [subcommand]')
|
|
128
|
+
.description('Manage the Cloudflared tunnel (setup, login, reset)')
|
|
129
|
+
.action(async (subcommand?: string) => {
|
|
130
|
+
if (subcommand === 'login') {
|
|
131
|
+
CloudflaredManager.install();
|
|
132
|
+
console.log(pc.cyan('Logging into cloudflared...'));
|
|
133
|
+
CloudflaredManager.exec('tunnel login', { stdio: 'inherit' });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (subcommand === 'reset') {
|
|
138
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
139
|
+
console.log(pc.yellow('No config found. Run fluxy init first.'));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const config = loadConfig();
|
|
143
|
+
config.tunnel = { mode: 'quick' };
|
|
144
|
+
delete config.tunnelUrl;
|
|
145
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
146
|
+
console.log(pc.green('Tunnel mode reset to quick.'));
|
|
147
|
+
if (adapter.isInstalled && adapter.isActive) {
|
|
148
|
+
console.log(pc.dim('Restart the daemon to apply: ' + pc.magenta('fluxy daemon restart')));
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
await runTunnelSetup();
|
|
154
|
+
|
|
155
|
+
if (adapter.isInstalled && adapter.isActive) {
|
|
156
|
+
const restart = await select({
|
|
157
|
+
message: 'Restart daemon now to apply changes?',
|
|
158
|
+
options: [
|
|
159
|
+
{ value: true, label: 'Yes' },
|
|
160
|
+
{ value: false, label: 'No' },
|
|
161
|
+
],
|
|
162
|
+
});
|
|
163
|
+
if (restart === true) {
|
|
164
|
+
adapter.handleDaemonAction('restart', {
|
|
165
|
+
user: process.env.SUDO_USER || os.userInfo().username,
|
|
166
|
+
home: process.env.FLUXY_REAL_HOME || os.homedir(),
|
|
167
|
+
nodePath: process.env.FLUXY_NODE_PATH || process.execPath,
|
|
168
|
+
dataDir: DATA_DIR,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
console.log(pc.dim('Start the server to apply changes: ' + pc.magenta('fluxy start')));
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { spinner, intro, outro } from '@clack/prompts';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import { execSync } from 'node:child_process';
|
|
8
|
+
|
|
9
|
+
import { pkg, DATA_DIR } from '../core/config.js';
|
|
10
|
+
import { getAdapter } from '../platforms/index.js';
|
|
11
|
+
|
|
12
|
+
export function registerUpdateCommand(program: Command) {
|
|
13
|
+
program
|
|
14
|
+
.command('update')
|
|
15
|
+
.description('Update Fluxy to the latest version')
|
|
16
|
+
.action(async () => {
|
|
17
|
+
intro(pc.inverse(' Fluxy Update '));
|
|
18
|
+
const currentVersion = pkg.version;
|
|
19
|
+
console.log(pc.dim(`Current version: v${currentVersion}`));
|
|
20
|
+
|
|
21
|
+
const s = spinner();
|
|
22
|
+
s.start('Checking for updates...');
|
|
23
|
+
|
|
24
|
+
let latest;
|
|
25
|
+
try {
|
|
26
|
+
const res = await fetch('https://registry.npmjs.org/fluxy-bot/latest');
|
|
27
|
+
if (!res.ok) throw new Error('Refresh failed');
|
|
28
|
+
latest = await res.json();
|
|
29
|
+
} catch (e: any) {
|
|
30
|
+
s.stop(pc.red('Failed to check for updates. ' + e.message));
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (currentVersion === latest.version) {
|
|
35
|
+
s.stop(pc.green(`Already up to date (v${currentVersion})`));
|
|
36
|
+
outro('Nothing to do!');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
s.message(`Found v${latest.version}. Downloading...`);
|
|
41
|
+
|
|
42
|
+
const adapter = getAdapter();
|
|
43
|
+
const daemonWasRunning = fs.existsSync(DATA_DIR) && adapter.isInstalled;
|
|
44
|
+
|
|
45
|
+
// Download tarball FIRST — before stopping daemon, so failure doesn't leave bot offline
|
|
46
|
+
const tmpDir = path.join(os.tmpdir(), `fluxy-update-${Date.now()}`);
|
|
47
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
48
|
+
const tarballFilePath = path.join(tmpDir, 'fluxy.tgz');
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const res = await fetch(latest.dist.tarball);
|
|
52
|
+
if (!res.ok) throw new Error('Download failed');
|
|
53
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
54
|
+
fs.writeFileSync(tarballFilePath, buf);
|
|
55
|
+
execSync(`tar xzf "${tarballFilePath}" -C "${tmpDir}"`, { stdio: 'ignore' });
|
|
56
|
+
} catch (e: any) {
|
|
57
|
+
s.stop(pc.red('Download failed: ' + e.message));
|
|
58
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Stop daemon AFTER download succeeds — minimizes downtime
|
|
63
|
+
if (daemonWasRunning && adapter.isActive) {
|
|
64
|
+
s.message('Stopping daemon...');
|
|
65
|
+
try {
|
|
66
|
+
adapter.handleDaemonAction('stop', {
|
|
67
|
+
user: os.userInfo().username,
|
|
68
|
+
home: os.homedir(),
|
|
69
|
+
nodePath: process.execPath,
|
|
70
|
+
dataDir: DATA_DIR,
|
|
71
|
+
});
|
|
72
|
+
} catch {}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
s.message('Updating files...');
|
|
76
|
+
const extracted = path.join(tmpDir, 'package');
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
for (const dir of ['bin', 'supervisor', 'worker', 'shared', 'scripts']) {
|
|
80
|
+
const src = path.join(extracted, dir);
|
|
81
|
+
if (fs.existsSync(src)) {
|
|
82
|
+
fs.cpSync(src, path.join(DATA_DIR, dir), { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const wsSrc = path.join(extracted, 'workspace');
|
|
87
|
+
if (!fs.existsSync(path.join(DATA_DIR, 'workspace')) && fs.existsSync(wsSrc)) {
|
|
88
|
+
fs.cpSync(wsSrc, path.join(DATA_DIR, 'workspace'), { recursive: true });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const file of ['package.json', 'vite.config.ts', 'vite.fluxy.config.ts', 'tsconfig.json', 'postcss.config.js', 'components.json']) {
|
|
92
|
+
const src = path.join(extracted, file);
|
|
93
|
+
if (fs.existsSync(src)) {
|
|
94
|
+
fs.cpSync(src, path.join(DATA_DIR, file), { force: true });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const distSrc = path.join(extracted, 'dist-fluxy');
|
|
99
|
+
const distDst = path.join(DATA_DIR, 'dist-fluxy');
|
|
100
|
+
if (fs.existsSync(distSrc)) {
|
|
101
|
+
fs.rmSync(distDst, { recursive: true, force: true });
|
|
102
|
+
fs.cpSync(distSrc, distDst, { recursive: true });
|
|
103
|
+
}
|
|
104
|
+
} catch (e: any) {
|
|
105
|
+
s.stop(pc.red('File copy failed: ' + e.message));
|
|
106
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
s.message('Installing dependencies...');
|
|
111
|
+
try {
|
|
112
|
+
execSync('npm install --omit=dev', { cwd: DATA_DIR, stdio: 'ignore', timeout: 300_000 });
|
|
113
|
+
} catch {}
|
|
114
|
+
|
|
115
|
+
const distDst = path.join(DATA_DIR, 'dist-fluxy');
|
|
116
|
+
if (!fs.existsSync(path.join(distDst, 'onboard.html'))) {
|
|
117
|
+
s.message('Building interface...');
|
|
118
|
+
try {
|
|
119
|
+
execSync('npm run build:fluxy', { cwd: DATA_DIR, stdio: 'ignore', timeout: 300_000 });
|
|
120
|
+
} catch {}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fs.writeFileSync(path.join(DATA_DIR, 'VERSION'), latest.version);
|
|
124
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
125
|
+
|
|
126
|
+
if (daemonWasRunning) {
|
|
127
|
+
s.message('Restarting daemon...');
|
|
128
|
+
try {
|
|
129
|
+
adapter.handleDaemonAction('start', {
|
|
130
|
+
user: os.userInfo().username,
|
|
131
|
+
home: os.homedir(),
|
|
132
|
+
nodePath: process.execPath,
|
|
133
|
+
dataDir: DATA_DIR,
|
|
134
|
+
});
|
|
135
|
+
} catch {}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
s.stop(pc.green(`Successfully updated to v${latest.version}`));
|
|
139
|
+
outro('Done!');
|
|
140
|
+
});
|
|
141
|
+
}
|