fluxy-bot 0.9.0 → 0.9.1
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/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 +132 -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 +2 -2
- package/tsconfig.json +1 -1
- package/vite.config.ts +9 -2
|
@@ -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,132 @@
|
|
|
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
|
+
if (daemonWasRunning && adapter.isActive) {
|
|
46
|
+
s.message('Stopping daemon...');
|
|
47
|
+
try {
|
|
48
|
+
adapter.handleDaemonAction('stop', {
|
|
49
|
+
user: os.userInfo().username,
|
|
50
|
+
home: os.homedir(),
|
|
51
|
+
nodePath: process.execPath,
|
|
52
|
+
dataDir: DATA_DIR,
|
|
53
|
+
});
|
|
54
|
+
} catch {}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const tmpDir = path.join(os.tmpdir(), `fluxy-update-${Date.now()}`);
|
|
58
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
59
|
+
const tarballFilePath = path.join(tmpDir, 'fluxy.tgz');
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const res = await fetch(latest.dist.tarball);
|
|
63
|
+
if (!res.ok) throw new Error('Download failed');
|
|
64
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
65
|
+
fs.writeFileSync(tarballFilePath, buf);
|
|
66
|
+
execSync(`tar xzf "${tarballFilePath}" -C "${tmpDir}"`, { stdio: 'ignore' });
|
|
67
|
+
} catch (e: any) {
|
|
68
|
+
s.stop(pc.red('Download failed: ' + e.message));
|
|
69
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
s.message('Updating files...');
|
|
74
|
+
const extracted = path.join(tmpDir, 'package');
|
|
75
|
+
|
|
76
|
+
for (const dir of ['bin', 'supervisor', 'worker', 'shared', 'scripts']) {
|
|
77
|
+
const src = path.join(extracted, dir);
|
|
78
|
+
if (fs.existsSync(src)) {
|
|
79
|
+
fs.cpSync(src, path.join(DATA_DIR, dir), { recursive: true, force: true });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const wsSrc = path.join(extracted, 'workspace');
|
|
84
|
+
if (!fs.existsSync(path.join(DATA_DIR, 'workspace')) && fs.existsSync(wsSrc)) {
|
|
85
|
+
fs.cpSync(wsSrc, path.join(DATA_DIR, 'workspace'), { recursive: true });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const file of ['package.json', 'vite.config.ts', 'vite.fluxy.config.ts', 'tsconfig.json', 'postcss.config.js', 'components.json']) {
|
|
89
|
+
const src = path.join(extracted, file);
|
|
90
|
+
if (fs.existsSync(src)) {
|
|
91
|
+
fs.cpSync(src, path.join(DATA_DIR, file), { force: true });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const distSrc = path.join(extracted, 'dist-fluxy');
|
|
96
|
+
const distDst = path.join(DATA_DIR, 'dist-fluxy');
|
|
97
|
+
if (fs.existsSync(distSrc)) {
|
|
98
|
+
fs.rmSync(distDst, { recursive: true, force: true });
|
|
99
|
+
fs.cpSync(distSrc, distDst, { recursive: true });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
s.message('Installing dependencies...');
|
|
103
|
+
try {
|
|
104
|
+
execSync('npm install --omit=dev', { cwd: DATA_DIR, stdio: 'ignore' });
|
|
105
|
+
} catch {}
|
|
106
|
+
|
|
107
|
+
if (!fs.existsSync(path.join(distDst, 'onboard.html'))) {
|
|
108
|
+
s.message('Building interface...');
|
|
109
|
+
try {
|
|
110
|
+
execSync('npm run build:fluxy', { cwd: DATA_DIR, stdio: 'ignore' });
|
|
111
|
+
} catch {}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fs.writeFileSync(path.join(DATA_DIR, '.version'), latest.version);
|
|
115
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
116
|
+
|
|
117
|
+
if (daemonWasRunning) {
|
|
118
|
+
s.message('Restarting daemon...');
|
|
119
|
+
try {
|
|
120
|
+
adapter.handleDaemonAction('start', {
|
|
121
|
+
user: os.userInfo().username,
|
|
122
|
+
home: os.homedir(),
|
|
123
|
+
nodePath: process.execPath,
|
|
124
|
+
dataDir: DATA_DIR,
|
|
125
|
+
});
|
|
126
|
+
} catch {}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
s.stop(pc.green(`Successfully updated to v${latest.version}`));
|
|
130
|
+
outro('Done!');
|
|
131
|
+
});
|
|
132
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
|
|
3
|
+
import { CloudflaredManager } from './cloudflared.js';
|
|
4
|
+
import { isUnixLike } from './os-detector.js';
|
|
5
|
+
import type { DaemonAction, DaemonConfig } from './types.js';
|
|
6
|
+
|
|
7
|
+
export abstract class BaseAdapter {
|
|
8
|
+
abstract get hasDaemonSupport(): boolean;
|
|
9
|
+
abstract get isInstalled(): boolean;
|
|
10
|
+
abstract get isActive(): boolean;
|
|
11
|
+
|
|
12
|
+
get isRoot(): boolean {
|
|
13
|
+
return isUnixLike() && process.getuid?.() === 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
protected requiresPrivilegeEscalation(_action: DaemonAction): boolean {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
protected escalatePrivileges(): void {
|
|
21
|
+
console.error(pc.red('Privilege escalation is required but not implemented for this platform.'));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public async handleDaemonAction(action: DaemonAction, config: DaemonConfig): Promise<void> {
|
|
26
|
+
if (!this.hasDaemonSupport) {
|
|
27
|
+
console.log(pc.yellow('\nDaemon mode is not supported on this platform.\n'));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (this.requiresPrivilegeEscalation(action)) {
|
|
32
|
+
return this.escalatePrivileges();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
switch (action) {
|
|
36
|
+
case 'install':
|
|
37
|
+
console.log(pc.cyan('Ensuring Cloudflared is installed...'));
|
|
38
|
+
CloudflaredManager.install();
|
|
39
|
+
if (this.isInstalled) {
|
|
40
|
+
console.log(pc.yellow('Daemon is already installed. Use "fluxy daemon restart" if needed.'));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
console.log(pc.cyan('Installing service...'));
|
|
44
|
+
this.installService(config);
|
|
45
|
+
console.log(pc.green('✓ Daemon installed and started.'));
|
|
46
|
+
break;
|
|
47
|
+
|
|
48
|
+
case 'start':
|
|
49
|
+
if (!this.isInstalled) { console.error(pc.red('Daemon is not installed. Run `fluxy daemon install` first.')); process.exit(1); }
|
|
50
|
+
if (this.isActive) { console.log(pc.yellow('Daemon is already running.')); return; }
|
|
51
|
+
console.log(pc.cyan('Starting daemon...'));
|
|
52
|
+
this.startService();
|
|
53
|
+
console.log(pc.green('✓ Daemon started.'));
|
|
54
|
+
break;
|
|
55
|
+
|
|
56
|
+
case 'stop':
|
|
57
|
+
if (!this.isInstalled) { console.error(pc.red('Daemon is not installed.')); process.exit(1); }
|
|
58
|
+
console.log(pc.cyan('Stopping daemon...'));
|
|
59
|
+
this.stopService();
|
|
60
|
+
console.log(pc.green('✓ Daemon stopped.'));
|
|
61
|
+
break;
|
|
62
|
+
|
|
63
|
+
case 'restart':
|
|
64
|
+
if (!this.isInstalled) { console.error(pc.red('Daemon is not installed.')); process.exit(1); }
|
|
65
|
+
console.log(pc.cyan('Restarting daemon...'));
|
|
66
|
+
this.stopService();
|
|
67
|
+
this.startService();
|
|
68
|
+
console.log(pc.green('✓ Daemon restarted.'));
|
|
69
|
+
break;
|
|
70
|
+
|
|
71
|
+
case 'status':
|
|
72
|
+
this.checkStatus();
|
|
73
|
+
break;
|
|
74
|
+
|
|
75
|
+
case 'logs':
|
|
76
|
+
if (!this.isInstalled) { console.error(pc.red('Daemon is not installed.')); process.exit(1); }
|
|
77
|
+
this.showLogs();
|
|
78
|
+
break;
|
|
79
|
+
|
|
80
|
+
case 'uninstall':
|
|
81
|
+
if (!this.isInstalled) { console.log(pc.yellow('Daemon is not installed.')); return; }
|
|
82
|
+
console.log(pc.cyan('Uninstalling daemon...'));
|
|
83
|
+
this.uninstallService();
|
|
84
|
+
console.log(pc.green('✓ Daemon uninstalled.'));
|
|
85
|
+
break;
|
|
86
|
+
|
|
87
|
+
default:
|
|
88
|
+
console.error(pc.red(`Unknown daemon action: ${action}`));
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
protected abstract installService(config: DaemonConfig): void;
|
|
94
|
+
protected abstract startService(): void;
|
|
95
|
+
protected abstract stopService(): void;
|
|
96
|
+
protected abstract uninstallService(): void;
|
|
97
|
+
protected abstract showLogs(): void;
|
|
98
|
+
protected abstract checkStatus(): void;
|
|
99
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
3
|
+
|
|
4
|
+
import { getNormalizedArchitecture, getPlatform, isWindows } from './os-detector.js';
|
|
5
|
+
import { BIN_DIR, CF_PATH } from './config.js';
|
|
6
|
+
|
|
7
|
+
export class CloudflaredManager {
|
|
8
|
+
static getDownloadUrl(): string {
|
|
9
|
+
const arch = getNormalizedArchitecture();
|
|
10
|
+
const platform = getPlatform();
|
|
11
|
+
const baseUrl = 'https://github.com/cloudflare/cloudflared/releases/latest/download';
|
|
12
|
+
|
|
13
|
+
if (platform === 'darwin') return `${baseUrl}/cloudflared-darwin-${arch}.tgz`;
|
|
14
|
+
if (platform === 'win32') return `${baseUrl}/cloudflared-windows-${arch}.exe`;
|
|
15
|
+
return `${baseUrl}/cloudflared-linux-${arch}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static hasBinary(): boolean {
|
|
19
|
+
try {
|
|
20
|
+
execSync('cloudflared --version', { stdio: 'ignore' });
|
|
21
|
+
return true;
|
|
22
|
+
} catch {}
|
|
23
|
+
|
|
24
|
+
if (fs.existsSync(CF_PATH)) {
|
|
25
|
+
const stats = fs.statSync(CF_PATH);
|
|
26
|
+
if (stats.size > 10 * 1024 * 1024) return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static install(): void {
|
|
33
|
+
if (this.hasBinary()) return;
|
|
34
|
+
|
|
35
|
+
const url = this.getDownloadUrl();
|
|
36
|
+
fs.mkdirSync(BIN_DIR, { recursive: true });
|
|
37
|
+
|
|
38
|
+
if (isWindows()) {
|
|
39
|
+
execSync(`curl.exe -fsSL -o "${CF_PATH}" "${url}"`, { stdio: 'inherit' });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (url.endsWith('.tgz')) {
|
|
44
|
+
execSync(`curl -fsSL "${url}" | tar xz -C "${BIN_DIR}"`, { stdio: 'inherit' });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
execSync(`curl -fsSL -o "${CF_PATH}" "${url}"`, { stdio: 'inherit' });
|
|
49
|
+
fs.chmodSync(CF_PATH, 0o755);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static spawn(args: string[], options: any = {}) {
|
|
53
|
+
try {
|
|
54
|
+
execSync('cloudflared --version', { stdio: 'ignore' });
|
|
55
|
+
return spawnSync('cloudflared', args, options);
|
|
56
|
+
} catch {
|
|
57
|
+
if (!this.hasBinary()) this.install();
|
|
58
|
+
return spawnSync(CF_PATH, args, options);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static exec(command: string, options: any = {}) {
|
|
63
|
+
try {
|
|
64
|
+
execSync('cloudflared --version', { stdio: 'ignore' });
|
|
65
|
+
return execSync(`cloudflared ${command}`, options);
|
|
66
|
+
} catch {
|
|
67
|
+
if (!this.hasBinary()) this.install();
|
|
68
|
+
return execSync(`"${CF_PATH}" ${command}`, options);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import url from 'node:url';
|
|
5
|
+
|
|
6
|
+
import type { BotConfig } from '../../shared/config.js';
|
|
7
|
+
|
|
8
|
+
export const PLATFORM = os.platform();
|
|
9
|
+
|
|
10
|
+
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
11
|
+
export const REPO_ROOT = path.resolve(__dirname, '../..');
|
|
12
|
+
|
|
13
|
+
export const DATA_DIR = path.join(
|
|
14
|
+
process.env.FLUXY_REAL_HOME || os.homedir(),
|
|
15
|
+
'.fluxy',
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
export const IS_DEV = fs.existsSync(path.join(REPO_ROOT, '.git'));
|
|
19
|
+
export const ROOT = IS_DEV ? REPO_ROOT : DATA_DIR;
|
|
20
|
+
export const CONFIG_PATH = path.join(DATA_DIR, 'config.json');
|
|
21
|
+
export const BIN_DIR = path.join(DATA_DIR, 'bin');
|
|
22
|
+
export const CF_PATH = path.join(
|
|
23
|
+
BIN_DIR,
|
|
24
|
+
'cloudflared' + (PLATFORM === 'win32' ? '.exe' : ''),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
export const SERVICE_NAME = 'fluxy';
|
|
28
|
+
|
|
29
|
+
export const pkg = JSON.parse(
|
|
30
|
+
fs.readFileSync(path.join(ROOT, 'package.json'), 'utf-8'),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
export function loadConfig(): BotConfig {
|
|
34
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createConfig() {
|
|
38
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
39
|
+
if (fs.existsSync(CONFIG_PATH)) return;
|
|
40
|
+
|
|
41
|
+
const config: Partial<BotConfig> = {
|
|
42
|
+
port: 3000,
|
|
43
|
+
username: '',
|
|
44
|
+
ai: { provider: '', model: '', apiKey: '' },
|
|
45
|
+
tunnel: { mode: 'quick' },
|
|
46
|
+
relay: { token: '', tier: '', url: '' },
|
|
47
|
+
};
|
|
48
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function safeLoadConfig(fallback: Partial<BotConfig> = {}): BotConfig {
|
|
52
|
+
if (!fs.existsSync(CONFIG_PATH)) return fallback as BotConfig;
|
|
53
|
+
try {
|
|
54
|
+
return loadConfig();
|
|
55
|
+
} catch {
|
|
56
|
+
return fallback as BotConfig;
|
|
57
|
+
}
|
|
58
|
+
}
|