@fyresmith/hive-server 2.3.1 → 2.4.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 +54 -89
- package/cli/commands/env.js +80 -0
- package/cli/commands/root.js +91 -0
- package/cli/commands/service.js +112 -0
- package/cli/commands/tunnel.js +165 -0
- package/cli/core/app.js +57 -0
- package/cli/core/context.js +110 -0
- package/cli/flows/doctor.js +101 -0
- package/cli/flows/setup.js +142 -0
- package/cli/flows/system.js +170 -0
- package/cli/main.js +5 -926
- package/cli/tunnel.js +17 -1
- package/lib/adapterRegistry.js +152 -0
- package/lib/collabProtocol.js +25 -0
- package/lib/collabStore.js +448 -0
- package/lib/discordWebhook.js +81 -0
- package/lib/mentionUtils.js +13 -0
- package/lib/socketHandler.js +891 -38
- package/lib/vaultManager.js +220 -4
- package/lib/yjsServer.js +6 -1
- package/package.json +3 -3
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { readFile } from 'fs/promises';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import prompts from 'prompts';
|
|
6
|
+
import { DEFAULT_ENV_FILE, EXIT } from '../constants.js';
|
|
7
|
+
import { CliError } from '../errors.js';
|
|
8
|
+
import { loadHiveConfig } from '../config.js';
|
|
9
|
+
import { loadEnvFile, normalizeEnv, validateEnvValues, writeEnvFile } from '../env-file.js';
|
|
10
|
+
import { success } from '../output.js';
|
|
11
|
+
import { getServiceDefaults } from '../service.js';
|
|
12
|
+
|
|
13
|
+
export function parseInteger(value, key) {
|
|
14
|
+
const parsed = parseInt(String(value ?? '').trim(), 10);
|
|
15
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
16
|
+
throw new CliError(`${key} must be a positive integer`);
|
|
17
|
+
}
|
|
18
|
+
return parsed;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function requiredOrFallback(value, fallback) {
|
|
22
|
+
const trimmed = String(value ?? '').trim();
|
|
23
|
+
return trimmed || fallback;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function promptConfirm(message, yes = false, initial = true) {
|
|
27
|
+
if (yes) return true;
|
|
28
|
+
const answer = await prompts({
|
|
29
|
+
type: 'confirm',
|
|
30
|
+
name: 'ok',
|
|
31
|
+
message,
|
|
32
|
+
initial,
|
|
33
|
+
});
|
|
34
|
+
return Boolean(answer.ok);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function resolveContext(options = {}) {
|
|
38
|
+
const config = await loadHiveConfig();
|
|
39
|
+
const envFile = options.envFile || config.envFile || DEFAULT_ENV_FILE;
|
|
40
|
+
return { config, envFile };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function resolveServiceConfig(config) {
|
|
44
|
+
const defaults = getServiceDefaults();
|
|
45
|
+
return {
|
|
46
|
+
servicePlatform: config.servicePlatform || defaults.servicePlatform,
|
|
47
|
+
serviceName: config.serviceName || defaults.serviceName,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function loadPackageMeta() {
|
|
52
|
+
const raw = await readFile(new URL('../../package.json', import.meta.url), 'utf-8');
|
|
53
|
+
const parsed = JSON.parse(raw);
|
|
54
|
+
const name = String(parsed?.name ?? '').trim();
|
|
55
|
+
const version = String(parsed?.version ?? '').trim() || 'unknown';
|
|
56
|
+
if (!name) {
|
|
57
|
+
throw new CliError('Could not resolve package name from package.json', EXIT.FAIL);
|
|
58
|
+
}
|
|
59
|
+
return { name, version };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function isHiveServiceInstalled({ servicePlatform, serviceName }) {
|
|
63
|
+
if (servicePlatform === 'launchd') {
|
|
64
|
+
return existsSync(join(homedir(), 'Library', 'LaunchAgents', `${serviceName}.plist`));
|
|
65
|
+
}
|
|
66
|
+
return existsSync(`/etc/systemd/system/${serviceName}.service`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function normalizeLogsComponent(value) {
|
|
70
|
+
const component = String(value ?? 'hive').trim().toLowerCase();
|
|
71
|
+
if (component === 'hive' || component === 'tunnel' || component === 'both') {
|
|
72
|
+
return component;
|
|
73
|
+
}
|
|
74
|
+
throw new CliError(`Invalid logs component: ${value}. Use hive, tunnel, or both.`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function assertEnvFileExists(envFile) {
|
|
78
|
+
if (!existsSync(envFile)) {
|
|
79
|
+
throw new CliError(`Env file not found: ${envFile}. Run: hive env init`, EXIT.FAIL);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function loadValidatedEnv(envFile, { requireFile = true } = {}) {
|
|
84
|
+
if (requireFile) {
|
|
85
|
+
assertEnvFileExists(envFile);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const raw = await loadEnvFile(envFile);
|
|
89
|
+
const env = normalizeEnv(raw);
|
|
90
|
+
const issues = validateEnvValues(env);
|
|
91
|
+
return { env, issues };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function setRedirectUriForDomain({ envFile, env, domain, yes = false }) {
|
|
95
|
+
const expected = `https://${domain}/auth/callback`;
|
|
96
|
+
if (env.DISCORD_REDIRECT_URI === expected) return env;
|
|
97
|
+
|
|
98
|
+
const shouldUpdate = await promptConfirm(
|
|
99
|
+
`Set DISCORD_REDIRECT_URI to ${expected}?`,
|
|
100
|
+
yes,
|
|
101
|
+
true
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
if (!shouldUpdate) return env;
|
|
105
|
+
|
|
106
|
+
const next = { ...env, DISCORD_REDIRECT_URI: expected };
|
|
107
|
+
await writeEnvFile(envFile, next);
|
|
108
|
+
success(`Updated DISCORD_REDIRECT_URI -> ${expected}`);
|
|
109
|
+
return next;
|
|
110
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { existsSync, constants as fsConstants } from 'fs';
|
|
2
|
+
import { access } from 'fs/promises';
|
|
3
|
+
import process from 'process';
|
|
4
|
+
import { EXIT } from '../constants.js';
|
|
5
|
+
import { CliError } from '../errors.js';
|
|
6
|
+
import { isPortAvailable, pathExists } from '../checks.js';
|
|
7
|
+
import { run } from '../exec.js';
|
|
8
|
+
import { getCloudflaredPath } from '../tunnel.js';
|
|
9
|
+
import { fail, info, section, success, warn } from '../output.js';
|
|
10
|
+
import { loadValidatedEnv } from '../core/context.js';
|
|
11
|
+
|
|
12
|
+
export async function runDoctorChecks({ envFile, includeCloudflared = true }) {
|
|
13
|
+
section('Hive Doctor');
|
|
14
|
+
|
|
15
|
+
let prereqFailures = 0;
|
|
16
|
+
let failures = 0;
|
|
17
|
+
|
|
18
|
+
const major = parseInt(process.versions.node.split('.')[0], 10);
|
|
19
|
+
if (major >= 18) {
|
|
20
|
+
success(`Node version OK (${process.versions.node})`);
|
|
21
|
+
} else {
|
|
22
|
+
fail(`Node >= 18 is required (current: ${process.versions.node})`);
|
|
23
|
+
prereqFailures += 1;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (includeCloudflared) {
|
|
27
|
+
const cloudflaredPath = getCloudflaredPath();
|
|
28
|
+
if (!cloudflaredPath) {
|
|
29
|
+
fail('cloudflared is not installed or not on PATH');
|
|
30
|
+
prereqFailures += 1;
|
|
31
|
+
} else {
|
|
32
|
+
const versionOutput = await run('cloudflared', ['--version']).catch(() => ({ stdout: '' }));
|
|
33
|
+
success(`cloudflared found (${cloudflaredPath}) ${versionOutput.stdout.trim()}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!existsSync(envFile)) {
|
|
38
|
+
fail(`Env file missing: ${envFile}`);
|
|
39
|
+
failures += 1;
|
|
40
|
+
} else {
|
|
41
|
+
success(`Env file found: ${envFile}`);
|
|
42
|
+
const { env, issues } = await loadValidatedEnv(envFile, { requireFile: true });
|
|
43
|
+
if (issues.length > 0) {
|
|
44
|
+
for (const issue of issues) fail(issue);
|
|
45
|
+
failures += issues.length;
|
|
46
|
+
} else {
|
|
47
|
+
success('Env values look valid');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (env.VAULT_PATH) {
|
|
51
|
+
const vaultExists = await pathExists(env.VAULT_PATH);
|
|
52
|
+
if (!vaultExists) {
|
|
53
|
+
fail(`VAULT_PATH does not exist: ${env.VAULT_PATH}`);
|
|
54
|
+
failures += 1;
|
|
55
|
+
} else {
|
|
56
|
+
try {
|
|
57
|
+
await access(env.VAULT_PATH, fsConstants.R_OK | fsConstants.W_OK);
|
|
58
|
+
success(`VAULT_PATH is readable/writable: ${env.VAULT_PATH}`);
|
|
59
|
+
} catch {
|
|
60
|
+
fail(`VAULT_PATH is not readable/writable: ${env.VAULT_PATH}`);
|
|
61
|
+
failures += 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const port = parseInt(env.PORT, 10);
|
|
67
|
+
const yjsPort = parseInt(env.YJS_PORT, 10);
|
|
68
|
+
if (Number.isInteger(port) && port > 0) {
|
|
69
|
+
const portFree = await isPortAvailable(port);
|
|
70
|
+
if (portFree) info(`PORT ${port} is available`);
|
|
71
|
+
else warn(`PORT ${port} is in use`);
|
|
72
|
+
}
|
|
73
|
+
if (Number.isInteger(yjsPort) && yjsPort > 0) {
|
|
74
|
+
const yjsFree = await isPortAvailable(yjsPort);
|
|
75
|
+
if (yjsFree) info(`YJS_PORT ${yjsPort} is available`);
|
|
76
|
+
else warn(`YJS_PORT ${yjsPort} is in use`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (Number.isInteger(port) && port > 0) {
|
|
80
|
+
const health = await fetch(`http://127.0.0.1:${port}/health`)
|
|
81
|
+
.then((res) => ({ ok: res.ok, status: res.status }))
|
|
82
|
+
.catch(() => null);
|
|
83
|
+
if (health?.ok) {
|
|
84
|
+
success(`Health endpoint reachable on :${port}`);
|
|
85
|
+
} else if (health) {
|
|
86
|
+
warn(`Health endpoint returned HTTP ${health.status}`);
|
|
87
|
+
} else {
|
|
88
|
+
info(`Health endpoint not reachable on :${port} (server may not be running)`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (prereqFailures > 0) {
|
|
94
|
+
throw new CliError('Doctor found missing prerequisites', EXIT.PREREQ);
|
|
95
|
+
}
|
|
96
|
+
if (failures > 0) {
|
|
97
|
+
throw new CliError('Doctor found configuration issues', EXIT.FAIL);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
success('Doctor checks passed');
|
|
101
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import prompts from 'prompts';
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_CLOUDFLARED_CERT,
|
|
5
|
+
DEFAULT_CLOUDFLARED_CONFIG,
|
|
6
|
+
DEFAULT_TUNNEL_NAME,
|
|
7
|
+
EXIT,
|
|
8
|
+
HIVE_CONFIG_FILE,
|
|
9
|
+
LEGACY_ENV_FILE,
|
|
10
|
+
} from '../constants.js';
|
|
11
|
+
import { CliError } from '../errors.js';
|
|
12
|
+
import { inferDomainFromRedirect, loadEnvFile, normalizeEnv, promptForEnv, validateEnvValues, writeEnvFile } from '../env-file.js';
|
|
13
|
+
import { validateDomain } from '../checks.js';
|
|
14
|
+
import { updateHiveConfig } from '../config.js';
|
|
15
|
+
import { installHiveService } from '../service.js';
|
|
16
|
+
import { setupTunnel } from '../tunnel.js';
|
|
17
|
+
import { fail, info, section, success } from '../output.js';
|
|
18
|
+
import {
|
|
19
|
+
parseInteger,
|
|
20
|
+
promptConfirm,
|
|
21
|
+
requiredOrFallback,
|
|
22
|
+
resolveContext,
|
|
23
|
+
setRedirectUriForDomain,
|
|
24
|
+
} from '../core/context.js';
|
|
25
|
+
import { runDoctorChecks } from './doctor.js';
|
|
26
|
+
|
|
27
|
+
export async function runSetupWizard(options) {
|
|
28
|
+
const yes = Boolean(options.yes);
|
|
29
|
+
|
|
30
|
+
section('Hive Setup');
|
|
31
|
+
|
|
32
|
+
const { config, envFile } = await resolveContext(options);
|
|
33
|
+
let nextConfig = { ...config, envFile };
|
|
34
|
+
let importedLegacy = false;
|
|
35
|
+
|
|
36
|
+
if (!existsSync(HIVE_CONFIG_FILE) && existsSync(LEGACY_ENV_FILE)) {
|
|
37
|
+
const shouldImportLegacy = await promptConfirm(
|
|
38
|
+
`Import existing legacy env from ${LEGACY_ENV_FILE}?`,
|
|
39
|
+
yes,
|
|
40
|
+
true
|
|
41
|
+
);
|
|
42
|
+
if (shouldImportLegacy) {
|
|
43
|
+
const legacyValues = await loadEnvFile(LEGACY_ENV_FILE);
|
|
44
|
+
await writeEnvFile(envFile, legacyValues);
|
|
45
|
+
importedLegacy = true;
|
|
46
|
+
success(`Imported legacy env into ${envFile}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const envExists = existsSync(envFile);
|
|
51
|
+
let envValues;
|
|
52
|
+
if (!envExists || importedLegacy) {
|
|
53
|
+
info(`Initializing env file at ${envFile}`);
|
|
54
|
+
const existing = await loadEnvFile(envFile);
|
|
55
|
+
envValues = await promptForEnv({ envFile, existing, yes });
|
|
56
|
+
} else {
|
|
57
|
+
const edit = await promptConfirm('Env file exists. Edit it now?', yes, false);
|
|
58
|
+
if (edit) {
|
|
59
|
+
const existing = await loadEnvFile(envFile);
|
|
60
|
+
envValues = await promptForEnv({ envFile, existing, yes });
|
|
61
|
+
} else {
|
|
62
|
+
envValues = normalizeEnv(await loadEnvFile(envFile));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const envIssues = validateEnvValues(envValues);
|
|
67
|
+
if (envIssues.length > 0) {
|
|
68
|
+
for (const issue of envIssues) fail(issue);
|
|
69
|
+
throw new CliError('Env configuration is invalid', EXIT.FAIL);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let domain = requiredOrFallback(
|
|
73
|
+
options.domain,
|
|
74
|
+
inferDomainFromRedirect(envValues.DISCORD_REDIRECT_URI) || nextConfig.domain
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (!yes) {
|
|
78
|
+
const response = await prompts({
|
|
79
|
+
type: 'text',
|
|
80
|
+
name: 'domain',
|
|
81
|
+
message: 'Public domain for Hive server',
|
|
82
|
+
initial: domain,
|
|
83
|
+
});
|
|
84
|
+
if (response.domain !== undefined) {
|
|
85
|
+
domain = String(response.domain).trim();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!validateDomain(domain)) {
|
|
90
|
+
throw new CliError(`Invalid domain: ${domain}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
envValues = await setRedirectUriForDomain({ envFile, env: envValues, domain, yes });
|
|
94
|
+
|
|
95
|
+
const shouldSetupTunnel = await promptConfirm('Configure Cloudflare Tunnel now?', yes, true);
|
|
96
|
+
if (shouldSetupTunnel) {
|
|
97
|
+
const port = parseInteger(envValues.PORT, 'PORT');
|
|
98
|
+
const yjsPort = parseInteger(envValues.YJS_PORT, 'YJS_PORT');
|
|
99
|
+
const tunnelName = requiredOrFallback(options.tunnelName, nextConfig.tunnelName || DEFAULT_TUNNEL_NAME);
|
|
100
|
+
const cloudflaredConfigFile = requiredOrFallback(
|
|
101
|
+
options.cloudflaredConfigFile,
|
|
102
|
+
nextConfig.cloudflaredConfigFile || DEFAULT_CLOUDFLARED_CONFIG
|
|
103
|
+
);
|
|
104
|
+
const tunnelService = await promptConfirm('Install cloudflared as a service?', yes, true);
|
|
105
|
+
|
|
106
|
+
const tunnelResult = await setupTunnel({
|
|
107
|
+
tunnelName,
|
|
108
|
+
domain,
|
|
109
|
+
configFile: cloudflaredConfigFile,
|
|
110
|
+
certPath: DEFAULT_CLOUDFLARED_CERT,
|
|
111
|
+
port,
|
|
112
|
+
yjsPort,
|
|
113
|
+
yes,
|
|
114
|
+
installService: tunnelService,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
nextConfig = {
|
|
118
|
+
...nextConfig,
|
|
119
|
+
domain,
|
|
120
|
+
tunnelName,
|
|
121
|
+
tunnelId: tunnelResult.tunnelId,
|
|
122
|
+
tunnelCredentialsFile: tunnelResult.credentialsFile,
|
|
123
|
+
cloudflaredConfigFile: tunnelResult.configFile,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const shouldInstallService = await promptConfirm('Install Hive server as an OS service?', yes, true);
|
|
128
|
+
if (shouldInstallService) {
|
|
129
|
+
const serviceInfo = await installHiveService({ envFile, yes });
|
|
130
|
+
nextConfig = {
|
|
131
|
+
...nextConfig,
|
|
132
|
+
servicePlatform: serviceInfo.servicePlatform,
|
|
133
|
+
serviceName: serviceInfo.serviceName,
|
|
134
|
+
};
|
|
135
|
+
success(`Installed service ${serviceInfo.serviceName}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
await updateHiveConfig(nextConfig);
|
|
139
|
+
success(`Saved config: ${HIVE_CONFIG_FILE}`);
|
|
140
|
+
|
|
141
|
+
await runDoctorChecks({ envFile, includeCloudflared: shouldSetupTunnel });
|
|
142
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { section, info, success, warn } from '../output.js';
|
|
2
|
+
import { runInherit } from '../exec.js';
|
|
3
|
+
import { CliError } from '../errors.js';
|
|
4
|
+
import {
|
|
5
|
+
detectPlatform,
|
|
6
|
+
isCloudflaredServiceInstalled,
|
|
7
|
+
restartCloudflaredServiceIfInstalled,
|
|
8
|
+
startCloudflaredServiceIfInstalled,
|
|
9
|
+
stopCloudflaredServiceIfInstalled,
|
|
10
|
+
streamCloudflaredServiceLogs,
|
|
11
|
+
} from '../tunnel.js';
|
|
12
|
+
import {
|
|
13
|
+
restartHiveService,
|
|
14
|
+
startHiveService,
|
|
15
|
+
stopHiveService,
|
|
16
|
+
streamHiveServiceLogs,
|
|
17
|
+
} from '../service.js';
|
|
18
|
+
import {
|
|
19
|
+
isHiveServiceInstalled,
|
|
20
|
+
loadPackageMeta,
|
|
21
|
+
normalizeLogsComponent,
|
|
22
|
+
parseInteger,
|
|
23
|
+
requiredOrFallback,
|
|
24
|
+
resolveContext,
|
|
25
|
+
resolveServiceConfig,
|
|
26
|
+
} from '../core/context.js';
|
|
27
|
+
|
|
28
|
+
export async function runUpFlow() {
|
|
29
|
+
section('Hive Up');
|
|
30
|
+
|
|
31
|
+
const { config } = await resolveContext({});
|
|
32
|
+
const hiveService = resolveServiceConfig(config);
|
|
33
|
+
|
|
34
|
+
let startedAny = false;
|
|
35
|
+
|
|
36
|
+
if (isHiveServiceInstalled(hiveService)) {
|
|
37
|
+
info(`Starting Hive service: ${hiveService.serviceName}`);
|
|
38
|
+
await startHiveService(hiveService);
|
|
39
|
+
startedAny = true;
|
|
40
|
+
success('Hive service started');
|
|
41
|
+
} else {
|
|
42
|
+
warn('Hive service is not installed. Use `hive service install` (or `hive setup`).');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
info('Starting cloudflared service if installed');
|
|
46
|
+
const tunnelStart = await startCloudflaredServiceIfInstalled();
|
|
47
|
+
if (tunnelStart.installed) {
|
|
48
|
+
startedAny = true;
|
|
49
|
+
success('cloudflared service started');
|
|
50
|
+
} else {
|
|
51
|
+
warn('cloudflared service is not installed. Use `hive tunnel service-install`.');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!startedAny) {
|
|
55
|
+
throw new CliError('No installed services were started.');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function runDownFlow() {
|
|
60
|
+
section('Hive Down');
|
|
61
|
+
|
|
62
|
+
const { config } = await resolveContext({});
|
|
63
|
+
const hiveService = resolveServiceConfig(config);
|
|
64
|
+
|
|
65
|
+
let stoppedAny = false;
|
|
66
|
+
|
|
67
|
+
if (isHiveServiceInstalled(hiveService)) {
|
|
68
|
+
info(`Stopping Hive service: ${hiveService.serviceName}`);
|
|
69
|
+
await stopHiveService(hiveService);
|
|
70
|
+
stoppedAny = true;
|
|
71
|
+
success('Hive service stopped');
|
|
72
|
+
} else {
|
|
73
|
+
warn('Hive service is not installed.');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
info('Stopping cloudflared service if installed');
|
|
77
|
+
const tunnelStop = await stopCloudflaredServiceIfInstalled();
|
|
78
|
+
if (tunnelStop.installed) {
|
|
79
|
+
stoppedAny = true;
|
|
80
|
+
success('cloudflared service stopped');
|
|
81
|
+
} else {
|
|
82
|
+
warn('cloudflared service is not installed.');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!stoppedAny) {
|
|
86
|
+
throw new CliError('No installed services were stopped.');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function runLogsFlow(options = {}) {
|
|
91
|
+
const component = normalizeLogsComponent(options.component);
|
|
92
|
+
const follow = Boolean(options.follow);
|
|
93
|
+
const lines = parseInteger(options.lines, 'lines');
|
|
94
|
+
const { config } = await resolveContext({});
|
|
95
|
+
const hiveService = resolveServiceConfig(config);
|
|
96
|
+
const hiveInstalled = isHiveServiceInstalled(hiveService);
|
|
97
|
+
const tunnelInstalled = isCloudflaredServiceInstalled();
|
|
98
|
+
|
|
99
|
+
if (component === 'hive') {
|
|
100
|
+
if (!hiveInstalled) {
|
|
101
|
+
throw new CliError(`Hive service is not installed: ${hiveService.serviceName}`);
|
|
102
|
+
}
|
|
103
|
+
await streamHiveServiceLogs({ ...hiveService, follow, lines });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (component === 'tunnel') {
|
|
108
|
+
if (!tunnelInstalled) {
|
|
109
|
+
throw new CliError('cloudflared service is not installed');
|
|
110
|
+
}
|
|
111
|
+
await streamCloudflaredServiceLogs({ follow, lines });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!hiveInstalled && !tunnelInstalled) {
|
|
116
|
+
throw new CliError('No installed services found for logs');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (detectPlatform() === 'linux') {
|
|
120
|
+
const args = ['journalctl', '--no-pager', '-n', String(lines)];
|
|
121
|
+
if (hiveInstalled) args.push('-u', hiveService.serviceName);
|
|
122
|
+
if (tunnelInstalled) args.push('-u', 'cloudflared');
|
|
123
|
+
if (follow) args.push('-f');
|
|
124
|
+
await runInherit('sudo', args);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (follow) {
|
|
129
|
+
throw new CliError('Combined follow logs are not supported on macOS. Use --component hive or --component tunnel.');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (hiveInstalled) {
|
|
133
|
+
section('Hive Service Logs');
|
|
134
|
+
await streamHiveServiceLogs({ ...hiveService, follow: false, lines });
|
|
135
|
+
}
|
|
136
|
+
if (tunnelInstalled) {
|
|
137
|
+
section('Tunnel Service Logs');
|
|
138
|
+
await streamCloudflaredServiceLogs({ follow: false, lines });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function runUpdateFlow(options = {}) {
|
|
143
|
+
section('Hive Update');
|
|
144
|
+
|
|
145
|
+
const { config } = await resolveContext({});
|
|
146
|
+
const pkg = await loadPackageMeta();
|
|
147
|
+
const packageName = requiredOrFallback(options.package, pkg.name);
|
|
148
|
+
const hiveService = resolveServiceConfig(config);
|
|
149
|
+
|
|
150
|
+
info(`Current CLI version: ${pkg.version}`);
|
|
151
|
+
info(`Updating ${packageName} from npm (latest)`);
|
|
152
|
+
await runInherit('npm', ['install', '-g', `${packageName}@latest`]);
|
|
153
|
+
success(`Installed latest ${packageName}`);
|
|
154
|
+
|
|
155
|
+
if (isHiveServiceInstalled(hiveService)) {
|
|
156
|
+
info(`Restarting Hive service: ${hiveService.serviceName}`);
|
|
157
|
+
await restartHiveService(hiveService);
|
|
158
|
+
success('Hive service restarted');
|
|
159
|
+
} else {
|
|
160
|
+
info(`Hive service not installed: ${hiveService.serviceName}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
info('Restarting cloudflared service if installed');
|
|
164
|
+
const tunnelRestart = await restartCloudflaredServiceIfInstalled();
|
|
165
|
+
if (tunnelRestart.installed) {
|
|
166
|
+
success('cloudflared service restarted');
|
|
167
|
+
} else {
|
|
168
|
+
info('cloudflared service not installed');
|
|
169
|
+
}
|
|
170
|
+
}
|