@fyresmith/hive-server 2.3.0 → 2.3.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/README.md +5 -0
- package/cli/main.js +145 -0
- package/cli/tunnel.js +121 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -125,11 +125,16 @@ hive run
|
|
|
125
125
|
Diagnostics:
|
|
126
126
|
|
|
127
127
|
```bash
|
|
128
|
+
hive up
|
|
129
|
+
hive down
|
|
130
|
+
hive logs
|
|
128
131
|
hive doctor
|
|
129
132
|
hive status
|
|
130
133
|
hive update
|
|
131
134
|
```
|
|
132
135
|
|
|
136
|
+
`hive up` / `hive down` start or stop installed Hive and cloudflared services together.
|
|
137
|
+
`hive logs` streams service logs (`--component hive|tunnel|both`).
|
|
133
138
|
`hive update` installs the latest npm release for the current package and then restarts the Hive OS service and cloudflared service when they are installed.
|
|
134
139
|
|
|
135
140
|
## Migration Notes
|
package/cli/main.js
CHANGED
|
@@ -30,10 +30,15 @@ import {
|
|
|
30
30
|
import { isPortAvailable, pathExists, validateDomain } from './checks.js';
|
|
31
31
|
import { run, runInherit } from './exec.js';
|
|
32
32
|
import {
|
|
33
|
+
detectPlatform,
|
|
33
34
|
cloudflaredServiceStatus,
|
|
35
|
+
isCloudflaredServiceInstalled,
|
|
34
36
|
installCloudflaredService,
|
|
35
37
|
runTunnelForeground,
|
|
36
38
|
restartCloudflaredServiceIfInstalled,
|
|
39
|
+
startCloudflaredServiceIfInstalled,
|
|
40
|
+
stopCloudflaredServiceIfInstalled,
|
|
41
|
+
streamCloudflaredServiceLogs,
|
|
37
42
|
setupTunnel,
|
|
38
43
|
tunnelStatus,
|
|
39
44
|
getCloudflaredPath,
|
|
@@ -106,6 +111,128 @@ function isHiveServiceInstalled({ servicePlatform, serviceName }) {
|
|
|
106
111
|
return existsSync(`/etc/systemd/system/${serviceName}.service`);
|
|
107
112
|
}
|
|
108
113
|
|
|
114
|
+
function normalizeLogsComponent(value) {
|
|
115
|
+
const component = String(value ?? 'hive').trim().toLowerCase();
|
|
116
|
+
if (component === 'hive' || component === 'tunnel' || component === 'both') {
|
|
117
|
+
return component;
|
|
118
|
+
}
|
|
119
|
+
throw new CliError(`Invalid logs component: ${value}. Use hive, tunnel, or both.`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function runUpFlow() {
|
|
123
|
+
section('Hive Up');
|
|
124
|
+
|
|
125
|
+
const { config } = await resolveContext({});
|
|
126
|
+
const hiveService = resolveServiceConfig(config);
|
|
127
|
+
|
|
128
|
+
let startedAny = false;
|
|
129
|
+
|
|
130
|
+
if (isHiveServiceInstalled(hiveService)) {
|
|
131
|
+
info(`Starting Hive service: ${hiveService.serviceName}`);
|
|
132
|
+
await startHiveService(hiveService);
|
|
133
|
+
startedAny = true;
|
|
134
|
+
success('Hive service started');
|
|
135
|
+
} else {
|
|
136
|
+
warn('Hive service is not installed. Use `hive service install` (or `hive setup`).');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
info('Starting cloudflared service if installed');
|
|
140
|
+
const tunnelStart = await startCloudflaredServiceIfInstalled();
|
|
141
|
+
if (tunnelStart.installed) {
|
|
142
|
+
startedAny = true;
|
|
143
|
+
success('cloudflared service started');
|
|
144
|
+
} else {
|
|
145
|
+
warn('cloudflared service is not installed. Use `hive tunnel service-install`.');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!startedAny) {
|
|
149
|
+
throw new CliError('No installed services were started.', EXIT.FAIL);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function runDownFlow() {
|
|
154
|
+
section('Hive Down');
|
|
155
|
+
|
|
156
|
+
const { config } = await resolveContext({});
|
|
157
|
+
const hiveService = resolveServiceConfig(config);
|
|
158
|
+
|
|
159
|
+
let stoppedAny = false;
|
|
160
|
+
|
|
161
|
+
if (isHiveServiceInstalled(hiveService)) {
|
|
162
|
+
info(`Stopping Hive service: ${hiveService.serviceName}`);
|
|
163
|
+
await stopHiveService(hiveService);
|
|
164
|
+
stoppedAny = true;
|
|
165
|
+
success('Hive service stopped');
|
|
166
|
+
} else {
|
|
167
|
+
warn('Hive service is not installed.');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
info('Stopping cloudflared service if installed');
|
|
171
|
+
const tunnelStop = await stopCloudflaredServiceIfInstalled();
|
|
172
|
+
if (tunnelStop.installed) {
|
|
173
|
+
stoppedAny = true;
|
|
174
|
+
success('cloudflared service stopped');
|
|
175
|
+
} else {
|
|
176
|
+
warn('cloudflared service is not installed.');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!stoppedAny) {
|
|
180
|
+
throw new CliError('No installed services were stopped.', EXIT.FAIL);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function runLogsFlow(options = {}) {
|
|
185
|
+
const component = normalizeLogsComponent(options.component);
|
|
186
|
+
const follow = Boolean(options.follow);
|
|
187
|
+
const lines = parseInteger(options.lines, 'lines');
|
|
188
|
+
const { config } = await resolveContext({});
|
|
189
|
+
const hiveService = resolveServiceConfig(config);
|
|
190
|
+
const hiveInstalled = isHiveServiceInstalled(hiveService);
|
|
191
|
+
const tunnelInstalled = isCloudflaredServiceInstalled();
|
|
192
|
+
|
|
193
|
+
if (component === 'hive') {
|
|
194
|
+
if (!hiveInstalled) {
|
|
195
|
+
throw new CliError(`Hive service is not installed: ${hiveService.serviceName}`);
|
|
196
|
+
}
|
|
197
|
+
await streamHiveServiceLogs({ ...hiveService, follow, lines });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (component === 'tunnel') {
|
|
202
|
+
if (!tunnelInstalled) {
|
|
203
|
+
throw new CliError('cloudflared service is not installed');
|
|
204
|
+
}
|
|
205
|
+
await streamCloudflaredServiceLogs({ follow, lines });
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!hiveInstalled && !tunnelInstalled) {
|
|
210
|
+
throw new CliError('No installed services found for logs');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (detectPlatform() === 'linux') {
|
|
214
|
+
const args = ['journalctl', '--no-pager', '-n', String(lines)];
|
|
215
|
+
if (hiveInstalled) args.push('-u', hiveService.serviceName);
|
|
216
|
+
if (tunnelInstalled) args.push('-u', 'cloudflared');
|
|
217
|
+
if (follow) args.push('-f');
|
|
218
|
+
await runInherit('sudo', args);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (follow) {
|
|
223
|
+
throw new CliError('Combined follow logs are not supported on macOS. Use --component hive or --component tunnel.');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (hiveInstalled) {
|
|
227
|
+
section('Hive Service Logs');
|
|
228
|
+
await streamHiveServiceLogs({ ...hiveService, follow: false, lines });
|
|
229
|
+
}
|
|
230
|
+
if (tunnelInstalled) {
|
|
231
|
+
section('Tunnel Service Logs');
|
|
232
|
+
await streamCloudflaredServiceLogs({ follow: false, lines });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
109
236
|
async function runUpdateFlow(options = {}) {
|
|
110
237
|
section('Hive Update');
|
|
111
238
|
|
|
@@ -702,6 +829,24 @@ function registerRootCommands(program) {
|
|
|
702
829
|
.option('--package <name>', 'npm package override')
|
|
703
830
|
.action(runUpdateFlow);
|
|
704
831
|
|
|
832
|
+
program
|
|
833
|
+
.command('up')
|
|
834
|
+
.description('Start installed Hive + cloudflared services')
|
|
835
|
+
.action(runUpFlow);
|
|
836
|
+
|
|
837
|
+
program
|
|
838
|
+
.command('down')
|
|
839
|
+
.description('Stop installed Hive + cloudflared services')
|
|
840
|
+
.action(runDownFlow);
|
|
841
|
+
|
|
842
|
+
program
|
|
843
|
+
.command('logs')
|
|
844
|
+
.description('Stream logs for Hive and/or cloudflared services')
|
|
845
|
+
.option('-c, --component <name>', 'hive|tunnel|both', 'hive')
|
|
846
|
+
.option('-n, --lines <n>', 'lines to show', '80')
|
|
847
|
+
.option('--no-follow', 'do not follow logs')
|
|
848
|
+
.action(runLogsFlow);
|
|
849
|
+
|
|
705
850
|
program
|
|
706
851
|
.command('doctor')
|
|
707
852
|
.description('Run prerequisite and configuration checks')
|
package/cli/tunnel.js
CHANGED
|
@@ -10,6 +10,9 @@ import { run, runInherit } from './exec.js';
|
|
|
10
10
|
import { info, success, warn } from './output.js';
|
|
11
11
|
|
|
12
12
|
const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
13
|
+
const CLOUDFLARED_DARWIN_LABEL = 'com.cloudflare.cloudflared';
|
|
14
|
+
const CLOUDFLARED_DARWIN_TARGET = `system/${CLOUDFLARED_DARWIN_LABEL}`;
|
|
15
|
+
const CLOUDFLARED_DARWIN_PLIST = '/Library/LaunchDaemons/com.cloudflare.cloudflared.plist';
|
|
13
16
|
|
|
14
17
|
export function detectPlatform() {
|
|
15
18
|
if (process.platform === 'darwin') return 'darwin';
|
|
@@ -157,7 +160,18 @@ export async function ensureDnsRoute({ tunnelName, domain }) {
|
|
|
157
160
|
}
|
|
158
161
|
|
|
159
162
|
export async function installCloudflaredService() {
|
|
160
|
-
|
|
163
|
+
try {
|
|
164
|
+
await runInherit('sudo', ['cloudflared', 'service', 'install']);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
const output = getCloudflaredErrorOutput(err);
|
|
167
|
+
if (!isAlreadyInstalledCloudflaredService(output)) {
|
|
168
|
+
throw err;
|
|
169
|
+
}
|
|
170
|
+
warn('cloudflared service already installed; skipping reinstall');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Ensure the existing/new service is actually running.
|
|
174
|
+
await startCloudflaredServiceIfInstalled();
|
|
161
175
|
}
|
|
162
176
|
|
|
163
177
|
function isMissingCloudflaredService(output) {
|
|
@@ -172,25 +186,78 @@ function isMissingCloudflaredService(output) {
|
|
|
172
186
|
);
|
|
173
187
|
}
|
|
174
188
|
|
|
189
|
+
function isAlreadyInstalledCloudflaredService(output) {
|
|
190
|
+
const text = String(output ?? '').toLowerCase();
|
|
191
|
+
return text.includes('cloudflared service is already installed at');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function getCloudflaredErrorOutput(err) {
|
|
195
|
+
return [
|
|
196
|
+
err?.stdout,
|
|
197
|
+
err?.stderr,
|
|
198
|
+
err?.shortMessage,
|
|
199
|
+
err?.message,
|
|
200
|
+
]
|
|
201
|
+
.filter(Boolean)
|
|
202
|
+
.join('\n');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function startCloudflaredServiceIfInstalled() {
|
|
206
|
+
const platform = detectPlatform();
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
if (platform === 'darwin') {
|
|
210
|
+
await runInherit('sudo', ['launchctl', 'kickstart', '-k', CLOUDFLARED_DARWIN_TARGET]);
|
|
211
|
+
} else {
|
|
212
|
+
await runInherit('sudo', ['systemctl', 'start', 'cloudflared']);
|
|
213
|
+
}
|
|
214
|
+
return { installed: true, started: true };
|
|
215
|
+
} catch (err) {
|
|
216
|
+
const output = getCloudflaredErrorOutput(err);
|
|
217
|
+
if (isMissingCloudflaredService(output)) {
|
|
218
|
+
if (platform === 'darwin' && existsSync(CLOUDFLARED_DARWIN_PLIST)) {
|
|
219
|
+
await runInherit('sudo', ['launchctl', 'bootstrap', 'system', CLOUDFLARED_DARWIN_PLIST]);
|
|
220
|
+
await runInherit('sudo', ['launchctl', 'enable', CLOUDFLARED_DARWIN_TARGET]);
|
|
221
|
+
await runInherit('sudo', ['launchctl', 'kickstart', '-k', CLOUDFLARED_DARWIN_TARGET]);
|
|
222
|
+
return { installed: true, started: true };
|
|
223
|
+
}
|
|
224
|
+
return { installed: false, started: false };
|
|
225
|
+
}
|
|
226
|
+
throw err;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export async function stopCloudflaredServiceIfInstalled() {
|
|
231
|
+
const platform = detectPlatform();
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
if (platform === 'darwin') {
|
|
235
|
+
await runInherit('sudo', ['launchctl', 'bootout', CLOUDFLARED_DARWIN_TARGET]);
|
|
236
|
+
} else {
|
|
237
|
+
await runInherit('sudo', ['systemctl', 'stop', 'cloudflared']);
|
|
238
|
+
}
|
|
239
|
+
return { installed: true, stopped: true };
|
|
240
|
+
} catch (err) {
|
|
241
|
+
const output = getCloudflaredErrorOutput(err);
|
|
242
|
+
if (isMissingCloudflaredService(output)) {
|
|
243
|
+
return { installed: false, stopped: false };
|
|
244
|
+
}
|
|
245
|
+
throw err;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
175
249
|
export async function restartCloudflaredServiceIfInstalled() {
|
|
176
250
|
const platform = detectPlatform();
|
|
177
251
|
|
|
178
252
|
try {
|
|
179
253
|
if (platform === 'darwin') {
|
|
180
|
-
await runInherit('sudo', ['launchctl', 'kickstart', '-k',
|
|
254
|
+
await runInherit('sudo', ['launchctl', 'kickstart', '-k', CLOUDFLARED_DARWIN_TARGET]);
|
|
181
255
|
} else {
|
|
182
256
|
await runInherit('sudo', ['systemctl', 'restart', 'cloudflared']);
|
|
183
257
|
}
|
|
184
258
|
return { installed: true, restarted: true };
|
|
185
259
|
} catch (err) {
|
|
186
|
-
const output =
|
|
187
|
-
err?.stdout,
|
|
188
|
-
err?.stderr,
|
|
189
|
-
err?.shortMessage,
|
|
190
|
-
err?.message,
|
|
191
|
-
]
|
|
192
|
-
.filter(Boolean)
|
|
193
|
-
.join('\n');
|
|
260
|
+
const output = getCloudflaredErrorOutput(err);
|
|
194
261
|
if (isMissingCloudflaredService(output)) {
|
|
195
262
|
return { installed: false, restarted: false };
|
|
196
263
|
}
|
|
@@ -198,6 +265,48 @@ export async function restartCloudflaredServiceIfInstalled() {
|
|
|
198
265
|
}
|
|
199
266
|
}
|
|
200
267
|
|
|
268
|
+
export async function streamCloudflaredServiceLogs({ follow = true, lines = 80 } = {}) {
|
|
269
|
+
const platform = detectPlatform();
|
|
270
|
+
if (platform === 'darwin') {
|
|
271
|
+
if (follow) {
|
|
272
|
+
await runInherit('log', [
|
|
273
|
+
'stream',
|
|
274
|
+
'--style',
|
|
275
|
+
'compact',
|
|
276
|
+
'--predicate',
|
|
277
|
+
'process == "cloudflared"',
|
|
278
|
+
]);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
await runInherit('log', [
|
|
282
|
+
'show',
|
|
283
|
+
'--style',
|
|
284
|
+
'compact',
|
|
285
|
+
'--last',
|
|
286
|
+
'1h',
|
|
287
|
+
'--predicate',
|
|
288
|
+
'process == "cloudflared"',
|
|
289
|
+
]);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const args = ['journalctl', '-u', 'cloudflared', '--no-pager', '-n', String(lines)];
|
|
294
|
+
if (follow) args.push('-f');
|
|
295
|
+
await runInherit('sudo', args);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function isCloudflaredServiceInstalled() {
|
|
299
|
+
const platform = detectPlatform();
|
|
300
|
+
if (platform === 'darwin') {
|
|
301
|
+
return existsSync(CLOUDFLARED_DARWIN_PLIST);
|
|
302
|
+
}
|
|
303
|
+
return (
|
|
304
|
+
existsSync('/etc/systemd/system/cloudflared.service')
|
|
305
|
+
|| existsSync('/usr/lib/systemd/system/cloudflared.service')
|
|
306
|
+
|| existsSync('/lib/systemd/system/cloudflared.service')
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
201
310
|
export async function cloudflaredServiceStatus() {
|
|
202
311
|
const platform = detectPlatform();
|
|
203
312
|
if (platform === 'darwin') {
|
|
@@ -206,8 +315,7 @@ export async function cloudflaredServiceStatus() {
|
|
|
206
315
|
return true;
|
|
207
316
|
}
|
|
208
317
|
|
|
209
|
-
const
|
|
210
|
-
const systemPrint = await run('launchctl', ['print', `system/${systemLabel}`])
|
|
318
|
+
const systemPrint = await run('launchctl', ['print', CLOUDFLARED_DARWIN_TARGET])
|
|
211
319
|
.catch((err) => ({ stdout: err?.stdout ?? '', stderr: err?.stderr ?? '' }));
|
|
212
320
|
const combined = `${systemPrint.stdout}\n${systemPrint.stderr}`.toLowerCase();
|
|
213
321
|
if (
|
|
@@ -218,7 +326,7 @@ export async function cloudflaredServiceStatus() {
|
|
|
218
326
|
return true;
|
|
219
327
|
}
|
|
220
328
|
|
|
221
|
-
return existsSync(
|
|
329
|
+
return existsSync(CLOUDFLARED_DARWIN_PLIST);
|
|
222
330
|
}
|
|
223
331
|
const { stdout } = await run('systemctl', ['is-active', 'cloudflared']);
|
|
224
332
|
return stdout.trim() === 'active';
|