@fyresmith/hive-server 2.3.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 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';
@@ -172,25 +175,73 @@ function isMissingCloudflaredService(output) {
172
175
  );
173
176
  }
174
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
+
175
233
  export async function restartCloudflaredServiceIfInstalled() {
176
234
  const platform = detectPlatform();
177
235
 
178
236
  try {
179
237
  if (platform === 'darwin') {
180
- await runInherit('sudo', ['launchctl', 'kickstart', '-k', 'system/com.cloudflare.cloudflared']);
238
+ await runInherit('sudo', ['launchctl', 'kickstart', '-k', CLOUDFLARED_DARWIN_TARGET]);
181
239
  } else {
182
240
  await runInherit('sudo', ['systemctl', 'restart', 'cloudflared']);
183
241
  }
184
242
  return { installed: true, restarted: true };
185
243
  } catch (err) {
186
- const output = [
187
- err?.stdout,
188
- err?.stderr,
189
- err?.shortMessage,
190
- err?.message,
191
- ]
192
- .filter(Boolean)
193
- .join('\n');
244
+ const output = getCloudflaredErrorOutput(err);
194
245
  if (isMissingCloudflaredService(output)) {
195
246
  return { installed: false, restarted: false };
196
247
  }
@@ -198,6 +249,48 @@ export async function restartCloudflaredServiceIfInstalled() {
198
249
  }
199
250
  }
200
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
+
201
294
  export async function cloudflaredServiceStatus() {
202
295
  const platform = detectPlatform();
203
296
  if (platform === 'darwin') {
@@ -206,8 +299,7 @@ export async function cloudflaredServiceStatus() {
206
299
  return true;
207
300
  }
208
301
 
209
- const systemLabel = 'com.cloudflare.cloudflared';
210
- const systemPrint = await run('launchctl', ['print', `system/${systemLabel}`])
302
+ const systemPrint = await run('launchctl', ['print', CLOUDFLARED_DARWIN_TARGET])
211
303
  .catch((err) => ({ stdout: err?.stdout ?? '', stderr: err?.stderr ?? '' }));
212
304
  const combined = `${systemPrint.stdout}\n${systemPrint.stderr}`.toLowerCase();
213
305
  if (
@@ -218,7 +310,7 @@ export async function cloudflaredServiceStatus() {
218
310
  return true;
219
311
  }
220
312
 
221
- return existsSync('/Library/LaunchDaemons/com.cloudflare.cloudflared.plist');
313
+ return existsSync(CLOUDFLARED_DARWIN_PLIST);
222
314
  }
223
315
  const { stdout } = await run('systemctl', ['is-active', 'cloudflared']);
224
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.0",
3
+ "version": "2.3.1",
4
4
  "type": "module",
5
5
  "description": "Collaborative Obsidian vault server",
6
6
  "main": "index.js",