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 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
- // Stop daemon before updating files
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
- for (const dir of ['bin', 'supervisor', 'worker', 'shared', 'scripts']) {
1283
- const src = path.join(extracted, dir);
1284
- if (fs.existsSync(src)) {
1285
- fs.cpSync(src, path.join(DATA_DIR, dir), { recursive: true, force: true });
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
- // Copy workspace template only if it doesn't exist yet
1290
- if (!fs.existsSync(path.join(DATA_DIR, 'workspace'))) {
1291
- const wsSrc = path.join(extracted, 'workspace');
1292
- if (fs.existsSync(wsSrc)) {
1293
- fs.cpSync(wsSrc, path.join(DATA_DIR, 'workspace'), { recursive: true });
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
- // Update code files (never touches config.json, memory.db, etc.)
1298
- for (const file of ['package.json', 'vite.config.ts', 'vite.fluxy.config.ts', 'tsconfig.json', 'postcss.config.js', 'components.json']) {
1299
- const src = path.join(extracted, file);
1300
- if (fs.existsSync(src)) {
1301
- fs.copyFileSync(src, path.join(DATA_DIR, file));
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
- // Update pre-built UI from tarball
1306
- const distSrc = path.join(extracted, 'dist-fluxy');
1307
- const distDst = path.join(DATA_DIR, 'dist-fluxy');
1308
- if (fs.existsSync(distSrc)) {
1309
- if (fs.existsSync(distDst)) fs.rmSync(distDst, { recursive: true });
1310
- fs.cpSync(distSrc, distDst, { recursive: true });
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
- // Install dependencies
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
+ }