bloby-bot 0.66.0 → 0.67.0
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/README.md +11 -5
- package/bin/cli.js +1615 -1471
- package/package.json +4 -5
- package/scripts/install.sh +1 -0
- package/supervisor/channels/manager.ts +2 -0
- package/supervisor/channels/whatsapp-auth.ts +216 -0
- package/supervisor/channels/whatsapp.ts +106 -11
- package/supervisor/index.ts +91 -2
- package/tsconfig.json +1 -1
- package/cli/commands/daemon.ts +0 -31
- package/cli/commands/init.ts +0 -40
- package/cli/commands/start.ts +0 -91
- package/cli/commands/tunnel.ts +0 -175
- package/cli/commands/update.ts +0 -174
- package/cli/core/base-adapter.ts +0 -99
- package/cli/core/cloudflared.ts +0 -71
- package/cli/core/config.ts +0 -58
- package/cli/core/os-detector.ts +0 -31
- package/cli/core/server.ts +0 -87
- package/cli/core/types.ts +0 -15
- package/cli/index.ts +0 -72
- package/cli/platforms/darwin.ts +0 -110
- package/cli/platforms/index.ts +0 -20
- package/cli/platforms/linux.ts +0 -116
- package/cli/platforms/win32.ts +0 -20
- package/cli/utils/ui.ts +0 -38
package/cli/commands/init.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import pc from 'picocolors';
|
|
5
|
-
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
|
|
6
|
-
|
|
7
|
-
import { pkg, DATA_DIR, CONFIG_PATH, createConfig } from '../core/config.js';
|
|
8
|
-
import { runTunnelSetup } from './tunnel.js';
|
|
9
|
-
|
|
10
|
-
import type { BotConfig } from '../../shared/config.js';
|
|
11
|
-
|
|
12
|
-
export function registerInitCommand(program: Command) {
|
|
13
|
-
program
|
|
14
|
-
.command('init')
|
|
15
|
-
.description('Initialize Bloby configuration')
|
|
16
|
-
.action(async () => {
|
|
17
|
-
createConfig();
|
|
18
|
-
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
19
|
-
fs.writeFileSync(path.join(DATA_DIR, '.version'), pkg.version || '1.0.0');
|
|
20
|
-
|
|
21
|
-
console.log(pc.green('✓ Initialized base config.\n'));
|
|
22
|
-
|
|
23
|
-
// Generate USDC wallet (skip if one already exists)
|
|
24
|
-
const cfg: BotConfig = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
25
|
-
if (cfg.wallet?.privateKey) {
|
|
26
|
-
console.log(pc.green('✓ Wallet exists') + pc.dim(` (${cfg.wallet.address})\n`));
|
|
27
|
-
} else {
|
|
28
|
-
const privateKey = generatePrivateKey();
|
|
29
|
-
const account = privateKeyToAccount(privateKey);
|
|
30
|
-
cfg.wallet = { privateKey, address: account.address };
|
|
31
|
-
fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2));
|
|
32
|
-
console.log(pc.green('✓ Wallet created') + pc.dim(` (${account.address})`));
|
|
33
|
-
console.log(pc.dim(' Fund your wallet to enable autonomous purchases.\n'));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
await runTunnelSetup();
|
|
37
|
-
|
|
38
|
-
console.log(pc.dim('Run ' + pc.magenta('bloby start') + ' to launch the server.'));
|
|
39
|
-
});
|
|
40
|
-
}
|
package/cli/commands/start.ts
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
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 Bloby 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(' ● Bloby 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: `, `bloby daemon status`));
|
|
34
|
-
console.log(commandExample(`Logs: `, `bloby daemon logs`));
|
|
35
|
-
console.log(commandExample(`Restart:`, `bloby daemon restart`));
|
|
36
|
-
console.log(commandExample(`Stop: `, `bloby 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('Bloby is ready!')}`);
|
|
67
|
-
console.log(` ${pc.dim('Local:')} ${pc.blue(`http://localhost:${config.port || 7400}`)}`);
|
|
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
|
-
}
|
package/cli/commands/tunnel.ts
DELETED
|
@@ -1,175 +0,0 @@
|
|
|
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: bloby):',
|
|
27
|
-
defaultValue: 'bloby',
|
|
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: bloby 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 = 7400 } = 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(' Bloby 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 Bloby Relay Server and access your bot at open.bloby.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✓ Bloby 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 bloby 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('bloby 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.BLOBY_REAL_HOME || os.homedir(),
|
|
167
|
-
nodePath: process.env.BLOBY_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('bloby start')));
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
}
|
package/cli/commands/update.ts
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
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 Bloby to the latest version')
|
|
16
|
-
.action(async () => {
|
|
17
|
-
intro(pc.inverse(' Bloby 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(
|
|
27
|
-
`https://registry.npmjs.org/bloby-bot/latest?_t=${Date.now()}`,
|
|
28
|
-
{ headers: { 'Accept': 'application/json' } },
|
|
29
|
-
);
|
|
30
|
-
if (!res.ok) throw new Error('Refresh failed');
|
|
31
|
-
latest = await res.json();
|
|
32
|
-
} catch (e: any) {
|
|
33
|
-
s.stop(pc.red('Failed to check for updates. ' + e.message));
|
|
34
|
-
process.exit(1);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (currentVersion === latest.version) {
|
|
38
|
-
s.stop(pc.green(`Already up to date (v${currentVersion})`));
|
|
39
|
-
outro('Nothing to do!');
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
s.message(`Found v${latest.version}. Downloading...`);
|
|
44
|
-
|
|
45
|
-
const adapter = getAdapter();
|
|
46
|
-
const daemonWasRunning = fs.existsSync(DATA_DIR) && adapter.isInstalled;
|
|
47
|
-
|
|
48
|
-
// Download tarball FIRST — before stopping daemon, so failure doesn't leave bot offline
|
|
49
|
-
const tmpDir = path.join(os.tmpdir(), `bloby-update-${Date.now()}`);
|
|
50
|
-
fs.mkdirSync(tmpDir, { recursive: true });
|
|
51
|
-
const tarballFilePath = path.join(tmpDir, 'bloby.tgz');
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
const res = await fetch(latest.dist.tarball);
|
|
55
|
-
if (!res.ok) throw new Error('Download failed');
|
|
56
|
-
const buf = Buffer.from(await res.arrayBuffer());
|
|
57
|
-
fs.writeFileSync(tarballFilePath, buf);
|
|
58
|
-
execSync(`tar xzf "${tarballFilePath}" -C "${tmpDir}"`, { stdio: 'ignore' });
|
|
59
|
-
} catch (e: any) {
|
|
60
|
-
s.stop(pc.red('Download failed: ' + e.message));
|
|
61
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
62
|
-
process.exit(1);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Stop daemon AFTER download succeeds — minimizes downtime
|
|
66
|
-
if (daemonWasRunning && adapter.isActive) {
|
|
67
|
-
s.message('Stopping daemon...');
|
|
68
|
-
try {
|
|
69
|
-
adapter.handleDaemonAction('stop', {
|
|
70
|
-
user: os.userInfo().username,
|
|
71
|
-
home: os.homedir(),
|
|
72
|
-
nodePath: process.execPath,
|
|
73
|
-
dataDir: DATA_DIR,
|
|
74
|
-
});
|
|
75
|
-
} catch {}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
s.message('Updating files...');
|
|
79
|
-
const extracted = path.join(tmpDir, 'package');
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
for (const dir of ['bin', 'supervisor', 'worker', 'shared', 'scripts']) {
|
|
83
|
-
const src = path.join(extracted, dir);
|
|
84
|
-
if (fs.existsSync(src)) {
|
|
85
|
-
fs.cpSync(src, path.join(DATA_DIR, dir), { recursive: true, force: true });
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const wsSrc = path.join(extracted, 'workspace');
|
|
90
|
-
if (!fs.existsSync(path.join(DATA_DIR, 'workspace')) && fs.existsSync(wsSrc)) {
|
|
91
|
-
fs.cpSync(wsSrc, path.join(DATA_DIR, 'workspace'), { recursive: true });
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Always update framework files that ship with each version.
|
|
95
|
-
// These are not user-editable — user code lives in src/components/, src/pages/, backend/, etc.
|
|
96
|
-
const frameworkFiles = [
|
|
97
|
-
'workspace/client/index.html', // splash screen, SW registration, meta tags
|
|
98
|
-
'workspace/client/src/main.tsx', // React entry point, app-ready signal
|
|
99
|
-
];
|
|
100
|
-
for (const rel of frameworkFiles) {
|
|
101
|
-
const src = path.join(extracted, rel);
|
|
102
|
-
const dst = path.join(DATA_DIR, rel);
|
|
103
|
-
if (fs.existsSync(src)) {
|
|
104
|
-
fs.cpSync(src, dst, { force: true });
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Always update public assets that ship with the framework (animation spritesheet, icons).
|
|
109
|
-
// Only copy specific files — never overwrite user-added public assets.
|
|
110
|
-
const frameworkAssets = [
|
|
111
|
-
'spritesheet.webp',
|
|
112
|
-
'headphones_spritesheet.webp',
|
|
113
|
-
];
|
|
114
|
-
const publicSrc = path.join(extracted, 'workspace', 'client', 'public');
|
|
115
|
-
const publicDst = path.join(DATA_DIR, 'workspace', 'client', 'public');
|
|
116
|
-
for (const asset of frameworkAssets) {
|
|
117
|
-
const src = path.join(publicSrc, asset);
|
|
118
|
-
const dst = path.join(publicDst, asset);
|
|
119
|
-
if (fs.existsSync(src)) {
|
|
120
|
-
fs.cpSync(src, dst, { force: true });
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
for (const file of ['package.json', 'vite.config.ts', 'vite.bloby.config.ts', 'tsconfig.json', 'postcss.config.js', 'components.json']) {
|
|
125
|
-
const src = path.join(extracted, file);
|
|
126
|
-
if (fs.existsSync(src)) {
|
|
127
|
-
fs.cpSync(src, path.join(DATA_DIR, file), { force: true });
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const distSrc = path.join(extracted, 'dist-bloby');
|
|
132
|
-
const distDst = path.join(DATA_DIR, 'dist-bloby');
|
|
133
|
-
if (fs.existsSync(distSrc)) {
|
|
134
|
-
fs.rmSync(distDst, { recursive: true, force: true });
|
|
135
|
-
fs.cpSync(distSrc, distDst, { recursive: true });
|
|
136
|
-
}
|
|
137
|
-
} catch (e: any) {
|
|
138
|
-
s.stop(pc.red('File copy failed: ' + e.message));
|
|
139
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
140
|
-
process.exit(1);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
s.message('Installing dependencies...');
|
|
144
|
-
try {
|
|
145
|
-
execSync('npm install --omit=dev', { cwd: DATA_DIR, stdio: 'ignore', timeout: 300_000 });
|
|
146
|
-
} catch {}
|
|
147
|
-
|
|
148
|
-
const distDst = path.join(DATA_DIR, 'dist-bloby');
|
|
149
|
-
if (!fs.existsSync(path.join(distDst, 'onboard.html'))) {
|
|
150
|
-
s.message('Building interface...');
|
|
151
|
-
try {
|
|
152
|
-
execSync('npm run build:bloby', { cwd: DATA_DIR, stdio: 'ignore', timeout: 300_000 });
|
|
153
|
-
} catch {}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
fs.writeFileSync(path.join(DATA_DIR, 'VERSION'), latest.version);
|
|
157
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
158
|
-
|
|
159
|
-
if (daemonWasRunning) {
|
|
160
|
-
s.message('Restarting daemon...');
|
|
161
|
-
try {
|
|
162
|
-
adapter.handleDaemonAction('start', {
|
|
163
|
-
user: os.userInfo().username,
|
|
164
|
-
home: os.homedir(),
|
|
165
|
-
nodePath: process.execPath,
|
|
166
|
-
dataDir: DATA_DIR,
|
|
167
|
-
});
|
|
168
|
-
} catch {}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
s.stop(pc.green(`Successfully updated to v${latest.version}`));
|
|
172
|
-
outro('Done!');
|
|
173
|
-
});
|
|
174
|
-
}
|
package/cli/core/base-adapter.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
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 "bloby 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 `bloby 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
|
-
}
|
package/cli/core/cloudflared.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
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
|
-
}
|