@fyresmith/hive-server 2.2.0 → 2.3.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/README.md +8 -0
- package/cli/main.js +204 -2
- package/cli/tunnel.js +133 -3
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -125,10 +125,18 @@ 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
|
|
133
|
+
hive update
|
|
130
134
|
```
|
|
131
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`).
|
|
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.
|
|
139
|
+
|
|
132
140
|
## Migration Notes
|
|
133
141
|
|
|
134
142
|
On first `hive setup`, if legacy `server/.env` exists and no `~/.hive/config.json` exists, setup will offer to import legacy env values.
|
package/cli/main.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync } from 'fs';
|
|
2
|
-
import { access } from 'fs/promises';
|
|
2
|
+
import { access, readFile } from 'fs/promises';
|
|
3
3
|
import { constants as fsConstants } from 'fs';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import { join } from 'path';
|
|
4
6
|
import process from 'process';
|
|
5
7
|
import { Command, CommanderError } from 'commander';
|
|
6
8
|
import prompts from 'prompts';
|
|
@@ -26,11 +28,17 @@ import {
|
|
|
26
28
|
writeEnvFile,
|
|
27
29
|
} from './env-file.js';
|
|
28
30
|
import { isPortAvailable, pathExists, validateDomain } from './checks.js';
|
|
29
|
-
import { run } from './exec.js';
|
|
31
|
+
import { run, runInherit } from './exec.js';
|
|
30
32
|
import {
|
|
33
|
+
detectPlatform,
|
|
31
34
|
cloudflaredServiceStatus,
|
|
35
|
+
isCloudflaredServiceInstalled,
|
|
32
36
|
installCloudflaredService,
|
|
33
37
|
runTunnelForeground,
|
|
38
|
+
restartCloudflaredServiceIfInstalled,
|
|
39
|
+
startCloudflaredServiceIfInstalled,
|
|
40
|
+
stopCloudflaredServiceIfInstalled,
|
|
41
|
+
streamCloudflaredServiceLogs,
|
|
34
42
|
setupTunnel,
|
|
35
43
|
tunnelStatus,
|
|
36
44
|
getCloudflaredPath,
|
|
@@ -85,6 +93,176 @@ function resolveServiceConfig(config) {
|
|
|
85
93
|
};
|
|
86
94
|
}
|
|
87
95
|
|
|
96
|
+
async function loadPackageMeta() {
|
|
97
|
+
const raw = await readFile(new URL('../package.json', import.meta.url), 'utf-8');
|
|
98
|
+
const parsed = JSON.parse(raw);
|
|
99
|
+
const name = String(parsed?.name ?? '').trim();
|
|
100
|
+
const version = String(parsed?.version ?? '').trim() || 'unknown';
|
|
101
|
+
if (!name) {
|
|
102
|
+
throw new CliError('Could not resolve package name from package.json', EXIT.FAIL);
|
|
103
|
+
}
|
|
104
|
+
return { name, version };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isHiveServiceInstalled({ servicePlatform, serviceName }) {
|
|
108
|
+
if (servicePlatform === 'launchd') {
|
|
109
|
+
return existsSync(join(homedir(), 'Library', 'LaunchAgents', `${serviceName}.plist`));
|
|
110
|
+
}
|
|
111
|
+
return existsSync(`/etc/systemd/system/${serviceName}.service`);
|
|
112
|
+
}
|
|
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
|
+
|
|
236
|
+
async function runUpdateFlow(options = {}) {
|
|
237
|
+
section('Hive Update');
|
|
238
|
+
|
|
239
|
+
const { config } = await resolveContext({});
|
|
240
|
+
const pkg = await loadPackageMeta();
|
|
241
|
+
const packageName = requiredOrFallback(options.package, pkg.name);
|
|
242
|
+
const hiveService = resolveServiceConfig(config);
|
|
243
|
+
|
|
244
|
+
info(`Current CLI version: ${pkg.version}`);
|
|
245
|
+
info(`Updating ${packageName} from npm (latest)`);
|
|
246
|
+
await runInherit('npm', ['install', '-g', `${packageName}@latest`]);
|
|
247
|
+
success(`Installed latest ${packageName}`);
|
|
248
|
+
|
|
249
|
+
if (isHiveServiceInstalled(hiveService)) {
|
|
250
|
+
info(`Restarting Hive service: ${hiveService.serviceName}`);
|
|
251
|
+
await restartHiveService(hiveService);
|
|
252
|
+
success('Hive service restarted');
|
|
253
|
+
} else {
|
|
254
|
+
info(`Hive service not installed: ${hiveService.serviceName}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
info('Restarting cloudflared service if installed');
|
|
258
|
+
const tunnelRestart = await restartCloudflaredServiceIfInstalled();
|
|
259
|
+
if (tunnelRestart.installed) {
|
|
260
|
+
success('cloudflared service restarted');
|
|
261
|
+
} else {
|
|
262
|
+
info('cloudflared service not installed');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
88
266
|
async function loadValidatedEnv(envFile, { requireFile = true } = {}) {
|
|
89
267
|
if (requireFile && !existsSync(envFile)) {
|
|
90
268
|
throw new CliError(`Env file not found: ${envFile}. Run: hive env init`, EXIT.FAIL);
|
|
@@ -645,6 +823,30 @@ function registerRootCommands(program) {
|
|
|
645
823
|
.option('--yes', 'non-interactive mode', false)
|
|
646
824
|
.action(runSetupWizard);
|
|
647
825
|
|
|
826
|
+
program
|
|
827
|
+
.command('update')
|
|
828
|
+
.description('Update Hive from npm and restart installed services')
|
|
829
|
+
.option('--package <name>', 'npm package override')
|
|
830
|
+
.action(runUpdateFlow);
|
|
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
|
+
|
|
648
850
|
program
|
|
649
851
|
.command('doctor')
|
|
650
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';
|
|
@@ -160,6 +163,134 @@ export async function installCloudflaredService() {
|
|
|
160
163
|
await runInherit('sudo', ['cloudflared', 'service', 'install']);
|
|
161
164
|
}
|
|
162
165
|
|
|
166
|
+
function isMissingCloudflaredService(output) {
|
|
167
|
+
const text = String(output ?? '').toLowerCase();
|
|
168
|
+
return (
|
|
169
|
+
text.includes('could not find service')
|
|
170
|
+
|| text.includes('service does not exist')
|
|
171
|
+
|| text.includes('unit cloudflared.service not found')
|
|
172
|
+
|| text.includes('could not be found')
|
|
173
|
+
|| text.includes('not loaded')
|
|
174
|
+
|| text.includes('no such file or directory')
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function getCloudflaredErrorOutput(err) {
|
|
179
|
+
return [
|
|
180
|
+
err?.stdout,
|
|
181
|
+
err?.stderr,
|
|
182
|
+
err?.shortMessage,
|
|
183
|
+
err?.message,
|
|
184
|
+
]
|
|
185
|
+
.filter(Boolean)
|
|
186
|
+
.join('\n');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function startCloudflaredServiceIfInstalled() {
|
|
190
|
+
const platform = detectPlatform();
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
if (platform === 'darwin') {
|
|
194
|
+
await runInherit('sudo', ['launchctl', 'kickstart', '-k', CLOUDFLARED_DARWIN_TARGET]);
|
|
195
|
+
} else {
|
|
196
|
+
await runInherit('sudo', ['systemctl', 'start', 'cloudflared']);
|
|
197
|
+
}
|
|
198
|
+
return { installed: true, started: true };
|
|
199
|
+
} catch (err) {
|
|
200
|
+
const output = getCloudflaredErrorOutput(err);
|
|
201
|
+
if (isMissingCloudflaredService(output)) {
|
|
202
|
+
if (platform === 'darwin' && existsSync(CLOUDFLARED_DARWIN_PLIST)) {
|
|
203
|
+
await runInherit('sudo', ['launchctl', 'bootstrap', 'system', CLOUDFLARED_DARWIN_PLIST]);
|
|
204
|
+
await runInherit('sudo', ['launchctl', 'enable', CLOUDFLARED_DARWIN_TARGET]);
|
|
205
|
+
await runInherit('sudo', ['launchctl', 'kickstart', '-k', CLOUDFLARED_DARWIN_TARGET]);
|
|
206
|
+
return { installed: true, started: true };
|
|
207
|
+
}
|
|
208
|
+
return { installed: false, started: false };
|
|
209
|
+
}
|
|
210
|
+
throw err;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function stopCloudflaredServiceIfInstalled() {
|
|
215
|
+
const platform = detectPlatform();
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
if (platform === 'darwin') {
|
|
219
|
+
await runInherit('sudo', ['launchctl', 'bootout', CLOUDFLARED_DARWIN_TARGET]);
|
|
220
|
+
} else {
|
|
221
|
+
await runInherit('sudo', ['systemctl', 'stop', 'cloudflared']);
|
|
222
|
+
}
|
|
223
|
+
return { installed: true, stopped: true };
|
|
224
|
+
} catch (err) {
|
|
225
|
+
const output = getCloudflaredErrorOutput(err);
|
|
226
|
+
if (isMissingCloudflaredService(output)) {
|
|
227
|
+
return { installed: false, stopped: false };
|
|
228
|
+
}
|
|
229
|
+
throw err;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function restartCloudflaredServiceIfInstalled() {
|
|
234
|
+
const platform = detectPlatform();
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
if (platform === 'darwin') {
|
|
238
|
+
await runInherit('sudo', ['launchctl', 'kickstart', '-k', CLOUDFLARED_DARWIN_TARGET]);
|
|
239
|
+
} else {
|
|
240
|
+
await runInherit('sudo', ['systemctl', 'restart', 'cloudflared']);
|
|
241
|
+
}
|
|
242
|
+
return { installed: true, restarted: true };
|
|
243
|
+
} catch (err) {
|
|
244
|
+
const output = getCloudflaredErrorOutput(err);
|
|
245
|
+
if (isMissingCloudflaredService(output)) {
|
|
246
|
+
return { installed: false, restarted: false };
|
|
247
|
+
}
|
|
248
|
+
throw err;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export async function streamCloudflaredServiceLogs({ follow = true, lines = 80 } = {}) {
|
|
253
|
+
const platform = detectPlatform();
|
|
254
|
+
if (platform === 'darwin') {
|
|
255
|
+
if (follow) {
|
|
256
|
+
await runInherit('log', [
|
|
257
|
+
'stream',
|
|
258
|
+
'--style',
|
|
259
|
+
'compact',
|
|
260
|
+
'--predicate',
|
|
261
|
+
'process == "cloudflared"',
|
|
262
|
+
]);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
await runInherit('log', [
|
|
266
|
+
'show',
|
|
267
|
+
'--style',
|
|
268
|
+
'compact',
|
|
269
|
+
'--last',
|
|
270
|
+
'1h',
|
|
271
|
+
'--predicate',
|
|
272
|
+
'process == "cloudflared"',
|
|
273
|
+
]);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const args = ['journalctl', '-u', 'cloudflared', '--no-pager', '-n', String(lines)];
|
|
278
|
+
if (follow) args.push('-f');
|
|
279
|
+
await runInherit('sudo', args);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function isCloudflaredServiceInstalled() {
|
|
283
|
+
const platform = detectPlatform();
|
|
284
|
+
if (platform === 'darwin') {
|
|
285
|
+
return existsSync(CLOUDFLARED_DARWIN_PLIST);
|
|
286
|
+
}
|
|
287
|
+
return (
|
|
288
|
+
existsSync('/etc/systemd/system/cloudflared.service')
|
|
289
|
+
|| existsSync('/usr/lib/systemd/system/cloudflared.service')
|
|
290
|
+
|| existsSync('/lib/systemd/system/cloudflared.service')
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
163
294
|
export async function cloudflaredServiceStatus() {
|
|
164
295
|
const platform = detectPlatform();
|
|
165
296
|
if (platform === 'darwin') {
|
|
@@ -168,8 +299,7 @@ export async function cloudflaredServiceStatus() {
|
|
|
168
299
|
return true;
|
|
169
300
|
}
|
|
170
301
|
|
|
171
|
-
const
|
|
172
|
-
const systemPrint = await run('launchctl', ['print', `system/${systemLabel}`])
|
|
302
|
+
const systemPrint = await run('launchctl', ['print', CLOUDFLARED_DARWIN_TARGET])
|
|
173
303
|
.catch((err) => ({ stdout: err?.stdout ?? '', stderr: err?.stderr ?? '' }));
|
|
174
304
|
const combined = `${systemPrint.stdout}\n${systemPrint.stderr}`.toLowerCase();
|
|
175
305
|
if (
|
|
@@ -180,7 +310,7 @@ export async function cloudflaredServiceStatus() {
|
|
|
180
310
|
return true;
|
|
181
311
|
}
|
|
182
312
|
|
|
183
|
-
return existsSync(
|
|
313
|
+
return existsSync(CLOUDFLARED_DARWIN_PLIST);
|
|
184
314
|
}
|
|
185
315
|
const { stdout } = await run('systemctl', ['is-active', 'cloudflared']);
|
|
186
316
|
return stdout.trim() === 'active';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fyresmith/hive-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Collaborative Obsidian vault server",
|
|
6
6
|
"main": "index.js",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"test": "npm run verify"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"@fyresmith/hive-server": "^2.2.0",
|
|
34
35
|
"chalk": "^5.6.2",
|
|
35
36
|
"chokidar": "^3.6.0",
|
|
36
37
|
"commander": "^13.1.0",
|