@fyresmith/hive-server 2.1.0 → 2.3.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 CHANGED
@@ -127,8 +127,11 @@ Diagnostics:
127
127
  ```bash
128
128
  hive doctor
129
129
  hive status
130
+ hive update
130
131
  ```
131
132
 
133
+ `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
+
132
135
  ## Migration Notes
133
136
 
134
137
  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,12 @@ 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 {
31
33
  cloudflaredServiceStatus,
32
34
  installCloudflaredService,
33
35
  runTunnelForeground,
36
+ restartCloudflaredServiceIfInstalled,
34
37
  setupTunnel,
35
38
  tunnelStatus,
36
39
  getCloudflaredPath,
@@ -85,6 +88,54 @@ function resolveServiceConfig(config) {
85
88
  };
86
89
  }
87
90
 
91
+ async function loadPackageMeta() {
92
+ const raw = await readFile(new URL('../package.json', import.meta.url), 'utf-8');
93
+ const parsed = JSON.parse(raw);
94
+ const name = String(parsed?.name ?? '').trim();
95
+ const version = String(parsed?.version ?? '').trim() || 'unknown';
96
+ if (!name) {
97
+ throw new CliError('Could not resolve package name from package.json', EXIT.FAIL);
98
+ }
99
+ return { name, version };
100
+ }
101
+
102
+ function isHiveServiceInstalled({ servicePlatform, serviceName }) {
103
+ if (servicePlatform === 'launchd') {
104
+ return existsSync(join(homedir(), 'Library', 'LaunchAgents', `${serviceName}.plist`));
105
+ }
106
+ return existsSync(`/etc/systemd/system/${serviceName}.service`);
107
+ }
108
+
109
+ async function runUpdateFlow(options = {}) {
110
+ section('Hive Update');
111
+
112
+ const { config } = await resolveContext({});
113
+ const pkg = await loadPackageMeta();
114
+ const packageName = requiredOrFallback(options.package, pkg.name);
115
+ const hiveService = resolveServiceConfig(config);
116
+
117
+ info(`Current CLI version: ${pkg.version}`);
118
+ info(`Updating ${packageName} from npm (latest)`);
119
+ await runInherit('npm', ['install', '-g', `${packageName}@latest`]);
120
+ success(`Installed latest ${packageName}`);
121
+
122
+ if (isHiveServiceInstalled(hiveService)) {
123
+ info(`Restarting Hive service: ${hiveService.serviceName}`);
124
+ await restartHiveService(hiveService);
125
+ success('Hive service restarted');
126
+ } else {
127
+ info(`Hive service not installed: ${hiveService.serviceName}`);
128
+ }
129
+
130
+ info('Restarting cloudflared service if installed');
131
+ const tunnelRestart = await restartCloudflaredServiceIfInstalled();
132
+ if (tunnelRestart.installed) {
133
+ success('cloudflared service restarted');
134
+ } else {
135
+ info('cloudflared service not installed');
136
+ }
137
+ }
138
+
88
139
  async function loadValidatedEnv(envFile, { requireFile = true } = {}) {
89
140
  if (requireFile && !existsSync(envFile)) {
90
141
  throw new CliError(`Env file not found: ${envFile}. Run: hive env init`, EXIT.FAIL);
@@ -645,6 +696,12 @@ function registerRootCommands(program) {
645
696
  .option('--yes', 'non-interactive mode', false)
646
697
  .action(runSetupWizard);
647
698
 
699
+ program
700
+ .command('update')
701
+ .description('Update Hive from npm and restart installed services')
702
+ .option('--package <name>', 'npm package override')
703
+ .action(runUpdateFlow);
704
+
648
705
  program
649
706
  .command('doctor')
650
707
  .description('Run prerequisite and configuration checks')
@@ -703,6 +760,11 @@ export async function runCli(argv = process.argv) {
703
760
  registerTunnelCommands(program);
704
761
  registerServiceCommands(program);
705
762
 
763
+ if ((argv?.length ?? 0) <= 2) {
764
+ program.outputHelp();
765
+ return EXIT.OK;
766
+ }
767
+
706
768
  program.exitOverride();
707
769
 
708
770
  try {
@@ -710,7 +772,13 @@ export async function runCli(argv = process.argv) {
710
772
  return EXIT.OK;
711
773
  } catch (err) {
712
774
  if (err instanceof CommanderError) {
713
- if (err.code === 'commander.helpDisplayed') return EXIT.OK;
775
+ if (
776
+ err.code === 'commander.helpDisplayed'
777
+ || err.code === 'commander.help'
778
+ || err.message === '(outputHelp)'
779
+ ) {
780
+ return EXIT.OK;
781
+ }
714
782
  throw new CliError(err.message, err.exitCode ?? EXIT.FAIL);
715
783
  }
716
784
  throw err;
@@ -720,7 +788,7 @@ export async function runCli(argv = process.argv) {
720
788
  export async function runCliOrExit(argv = process.argv) {
721
789
  try {
722
790
  const code = await runCli(argv);
723
- process.exit(code);
791
+ process.exitCode = code;
724
792
  } catch (err) {
725
793
  const exitCode = err instanceof CliError ? err.exitCode : EXIT.FAIL;
726
794
  const message = err instanceof Error ? err.message : String(err);
package/cli/tunnel.js CHANGED
@@ -160,11 +160,65 @@ export async function installCloudflaredService() {
160
160
  await runInherit('sudo', ['cloudflared', 'service', 'install']);
161
161
  }
162
162
 
163
+ function isMissingCloudflaredService(output) {
164
+ const text = String(output ?? '').toLowerCase();
165
+ return (
166
+ text.includes('could not find service')
167
+ || text.includes('service does not exist')
168
+ || text.includes('unit cloudflared.service not found')
169
+ || text.includes('could not be found')
170
+ || text.includes('not loaded')
171
+ || text.includes('no such file or directory')
172
+ );
173
+ }
174
+
175
+ export async function restartCloudflaredServiceIfInstalled() {
176
+ const platform = detectPlatform();
177
+
178
+ try {
179
+ if (platform === 'darwin') {
180
+ await runInherit('sudo', ['launchctl', 'kickstart', '-k', 'system/com.cloudflare.cloudflared']);
181
+ } else {
182
+ await runInherit('sudo', ['systemctl', 'restart', 'cloudflared']);
183
+ }
184
+ return { installed: true, restarted: true };
185
+ } catch (err) {
186
+ const output = [
187
+ err?.stdout,
188
+ err?.stderr,
189
+ err?.shortMessage,
190
+ err?.message,
191
+ ]
192
+ .filter(Boolean)
193
+ .join('\n');
194
+ if (isMissingCloudflaredService(output)) {
195
+ return { installed: false, restarted: false };
196
+ }
197
+ throw err;
198
+ }
199
+ }
200
+
163
201
  export async function cloudflaredServiceStatus() {
164
202
  const platform = detectPlatform();
165
203
  if (platform === 'darwin') {
166
- const { stdout } = await run('launchctl', ['list']);
167
- return stdout.includes('cloudflared');
204
+ const userList = await run('launchctl', ['list']).catch(() => ({ stdout: '' }));
205
+ if (userList.stdout.toLowerCase().includes('cloudflared')) {
206
+ return true;
207
+ }
208
+
209
+ const systemLabel = 'com.cloudflare.cloudflared';
210
+ const systemPrint = await run('launchctl', ['print', `system/${systemLabel}`])
211
+ .catch((err) => ({ stdout: err?.stdout ?? '', stderr: err?.stderr ?? '' }));
212
+ const combined = `${systemPrint.stdout}\n${systemPrint.stderr}`.toLowerCase();
213
+ if (
214
+ combined
215
+ && !combined.includes('could not find service')
216
+ && !combined.includes('service does not exist')
217
+ ) {
218
+ return true;
219
+ }
220
+
221
+ return existsSync('/Library/LaunchDaemons/com.cloudflare.cloudflared.plist');
168
222
  }
169
223
  const { stdout } = await run('systemctl', ['is-active', 'cloudflared']);
170
224
  return stdout.trim() === 'active';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fyresmith/hive-server",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
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",