@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
package/README.md
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
1
|
# Hive Server
|
|
2
2
|
|
|
3
|
-
Hive server
|
|
3
|
+
Hive server ships with a first-class `hive` operations CLI for install, setup, tunnel management, env management, and service lifecycle.
|
|
4
|
+
|
|
5
|
+
## Collaboration protocol v2 additions
|
|
6
|
+
|
|
7
|
+
Server now supports additive `collab:*` events while retaining backward compatibility for legacy `file-*` and `presence-*` events.
|
|
8
|
+
|
|
9
|
+
New capabilities include:
|
|
10
|
+
|
|
11
|
+
- Protocol negotiation (`collab:hello`).
|
|
12
|
+
- Presence heartbeat/jump/list events.
|
|
13
|
+
- Thread/comment/task persistence and realtime events.
|
|
14
|
+
- Activity stream list + subscription.
|
|
15
|
+
- Notification preferences and notify events.
|
|
16
|
+
- Adapter capability negotiation (`markdown`, `canvas`, `metadata`).
|
|
17
|
+
|
|
18
|
+
## Metadata adapter policy
|
|
19
|
+
|
|
20
|
+
Selective metadata sync uses a whitelist model only.
|
|
21
|
+
|
|
22
|
+
Default safe allowlist (override with `HIVE_METADATA_ALLOWLIST_JSON`):
|
|
23
|
+
|
|
24
|
+
- `.obsidian/appearance.json`
|
|
25
|
+
- `.obsidian/community-plugins.json`
|
|
26
|
+
- `.obsidian/core-plugins.json`
|
|
27
|
+
- `.obsidian/hotkeys.json`
|
|
28
|
+
|
|
29
|
+
## Discord notifications
|
|
30
|
+
|
|
31
|
+
Notification webhook env vars:
|
|
32
|
+
|
|
33
|
+
- `HIVE_DISCORD_WEBHOOK_URL` (default webhook)
|
|
34
|
+
- `HIVE_DISCORD_WEBHOOKS_JSON` (path-prefix mapping)
|
|
35
|
+
|
|
36
|
+
Example `HIVE_DISCORD_WEBHOOKS_JSON`:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"TeamA": "https://discord.com/api/webhooks/...",
|
|
41
|
+
"Projects/Infra": "https://discord.com/api/webhooks/..."
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Presence stale cleanup
|
|
46
|
+
|
|
47
|
+
Presence stale eviction is controlled by:
|
|
48
|
+
|
|
49
|
+
- `HIVE_PRESENCE_STALE_MS` (default `45000`)
|
|
50
|
+
|
|
51
|
+
Only protocol v2 heartbeat participants are stale-evicted.
|
|
4
52
|
|
|
5
53
|
## Install
|
|
6
54
|
|
|
@@ -20,36 +68,12 @@ To build/verify the current local checkout and install it globally:
|
|
|
20
68
|
npm run install-hive
|
|
21
69
|
```
|
|
22
70
|
|
|
23
|
-
##
|
|
24
|
-
|
|
25
|
-
This repo now includes a release pipeline for `hive-server`:
|
|
26
|
-
|
|
27
|
-
- CI: `.github/workflows/hive-server-ci.yml`
|
|
28
|
-
- Release tag workflow: `.github/workflows/hive-server-release-tag.yml`
|
|
29
|
-
- npm publish workflow: `.github/workflows/hive-server-publish.yml`
|
|
71
|
+
## Tests and verification
|
|
30
72
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
- Package: `hive-server`
|
|
36
|
-
- Provider: GitHub Actions
|
|
37
|
-
- Repository: this repository
|
|
38
|
-
- Workflow file: `.github/workflows/hive-server-publish.yml`
|
|
39
|
-
- GitHub Actions publish job should run Node 24+ (already configured in workflow)
|
|
40
|
-
|
|
41
|
-
No `NPM_TOKEN` secret is required when Trusted Publishing is configured.
|
|
42
|
-
|
|
43
|
-
### How releases work
|
|
44
|
-
|
|
45
|
-
1. Run workflow `hive-server-release-tag` from the default branch.
|
|
46
|
-
2. Choose release type (`patch|minor|major|prerelease|custom`).
|
|
47
|
-
3. Workflow bumps `package.json`, commits, and pushes tag `hive-server-vX.Y.Z`.
|
|
48
|
-
4. Tag push triggers `hive-server-publish`, which:
|
|
49
|
-
- verifies the package
|
|
50
|
-
- checks tag version matches `package.json`
|
|
51
|
-
- publishes to npm with provenance
|
|
52
|
-
- creates a GitHub Release with generated notes
|
|
73
|
+
```bash
|
|
74
|
+
npm test
|
|
75
|
+
npm run verify
|
|
76
|
+
```
|
|
53
77
|
|
|
54
78
|
## Fast Path
|
|
55
79
|
|
|
@@ -91,62 +115,3 @@ hive env edit
|
|
|
91
115
|
hive env check
|
|
92
116
|
hive env print
|
|
93
117
|
```
|
|
94
|
-
|
|
95
|
-
## Tunnel Operations
|
|
96
|
-
|
|
97
|
-
```bash
|
|
98
|
-
hive tunnel setup
|
|
99
|
-
hive tunnel status
|
|
100
|
-
hive tunnel run
|
|
101
|
-
hive tunnel service-install
|
|
102
|
-
hive tunnel service-status
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
## Server Service Operations
|
|
106
|
-
|
|
107
|
-
```bash
|
|
108
|
-
hive service install
|
|
109
|
-
hive service start
|
|
110
|
-
hive service stop
|
|
111
|
-
hive service restart
|
|
112
|
-
hive service status
|
|
113
|
-
hive service logs
|
|
114
|
-
hive service uninstall
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
## Runtime and Diagnostics
|
|
118
|
-
|
|
119
|
-
Run directly in foreground:
|
|
120
|
-
|
|
121
|
-
```bash
|
|
122
|
-
hive run
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
Diagnostics:
|
|
126
|
-
|
|
127
|
-
```bash
|
|
128
|
-
hive up
|
|
129
|
-
hive down
|
|
130
|
-
hive logs
|
|
131
|
-
hive doctor
|
|
132
|
-
hive status
|
|
133
|
-
hive update
|
|
134
|
-
```
|
|
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
|
-
|
|
140
|
-
## Migration Notes
|
|
141
|
-
|
|
142
|
-
On first `hive setup`, if legacy `server/.env` exists and no `~/.hive/config.json` exists, setup will offer to import legacy env values.
|
|
143
|
-
|
|
144
|
-
## Legacy Scripts (Deprecated)
|
|
145
|
-
|
|
146
|
-
Legacy operational files are still present for one release cycle:
|
|
147
|
-
|
|
148
|
-
- `/setup-tunnel.sh`
|
|
149
|
-
- `/infra/cloudflare-tunnel.yml`
|
|
150
|
-
- `/infra/collab-server.service`
|
|
151
|
-
|
|
152
|
-
Use `hive` commands instead of editing legacy templates manually.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { section, success, fail } from '../output.js';
|
|
2
|
+
import { EXIT } from '../constants.js';
|
|
3
|
+
import { CliError } from '../errors.js';
|
|
4
|
+
import { inferDomainFromRedirect, loadEnvFile, normalizeEnv, promptForEnv, redactEnv, validateEnvValues } from '../env-file.js';
|
|
5
|
+
import { updateHiveConfig } from '../config.js';
|
|
6
|
+
import { assertEnvFileExists, loadValidatedEnv, resolveContext } from '../core/context.js';
|
|
7
|
+
|
|
8
|
+
export function registerEnvCommands(program) {
|
|
9
|
+
const env = program.command('env').description('Manage Hive .env configuration');
|
|
10
|
+
|
|
11
|
+
env
|
|
12
|
+
.command('init')
|
|
13
|
+
.description('Create or update env file from prompts')
|
|
14
|
+
.option('--env-file <path>', 'env file path')
|
|
15
|
+
.option('--yes', 'accept defaults where possible', false)
|
|
16
|
+
.action(async (options) => {
|
|
17
|
+
section('Env Init');
|
|
18
|
+
const { config, envFile } = await resolveContext(options);
|
|
19
|
+
const existing = await loadEnvFile(envFile);
|
|
20
|
+
const values = await promptForEnv({ envFile, existing, yes: options.yes });
|
|
21
|
+
const issues = validateEnvValues(values);
|
|
22
|
+
if (issues.length > 0) {
|
|
23
|
+
for (const issue of issues) fail(issue);
|
|
24
|
+
throw new CliError('Env file has validation issues', EXIT.FAIL);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const domain = inferDomainFromRedirect(values.DISCORD_REDIRECT_URI) || config.domain;
|
|
28
|
+
await updateHiveConfig({ envFile, domain });
|
|
29
|
+
success(`Env file ready at ${envFile}`);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
env
|
|
33
|
+
.command('edit')
|
|
34
|
+
.description('Edit env values interactively')
|
|
35
|
+
.option('--env-file <path>', 'env file path')
|
|
36
|
+
.option('--yes', 'accept defaults where possible', false)
|
|
37
|
+
.action(async (options) => {
|
|
38
|
+
section('Env Edit');
|
|
39
|
+
const { envFile } = await resolveContext(options);
|
|
40
|
+
assertEnvFileExists(envFile);
|
|
41
|
+
|
|
42
|
+
const existing = await loadEnvFile(envFile);
|
|
43
|
+
const values = await promptForEnv({ envFile, existing, yes: options.yes });
|
|
44
|
+
const issues = validateEnvValues(values);
|
|
45
|
+
if (issues.length > 0) {
|
|
46
|
+
for (const issue of issues) fail(issue);
|
|
47
|
+
throw new CliError('Env file has validation issues', EXIT.FAIL);
|
|
48
|
+
}
|
|
49
|
+
success(`Env file updated: ${envFile}`);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
env
|
|
53
|
+
.command('check')
|
|
54
|
+
.description('Validate env file')
|
|
55
|
+
.option('--env-file <path>', 'env file path')
|
|
56
|
+
.action(async (options) => {
|
|
57
|
+
section('Env Check');
|
|
58
|
+
const { envFile } = await resolveContext(options);
|
|
59
|
+
const { issues } = await loadValidatedEnv(envFile, { requireFile: true });
|
|
60
|
+
if (issues.length > 0) {
|
|
61
|
+
for (const issue of issues) fail(issue);
|
|
62
|
+
throw new CliError('Env validation failed', EXIT.FAIL);
|
|
63
|
+
}
|
|
64
|
+
success('Env validation passed');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
env
|
|
68
|
+
.command('print')
|
|
69
|
+
.description('Print redacted env values')
|
|
70
|
+
.option('--env-file <path>', 'env file path')
|
|
71
|
+
.action(async (options) => {
|
|
72
|
+
const { envFile } = await resolveContext(options);
|
|
73
|
+
const values = normalizeEnv(await loadEnvFile(envFile));
|
|
74
|
+
const redacted = redactEnv(values);
|
|
75
|
+
section(`Env (${envFile})`);
|
|
76
|
+
for (const [key, value] of Object.entries(redacted)) {
|
|
77
|
+
console.log(`${key}=${value}`);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { HIVE_CONFIG_FILE, EXIT } from '../constants.js';
|
|
3
|
+
import { CliError } from '../errors.js';
|
|
4
|
+
import { loadHiveConfig } from '../config.js';
|
|
5
|
+
import { section, info } from '../output.js';
|
|
6
|
+
import { cloudflaredServiceStatus } from '../tunnel.js';
|
|
7
|
+
import { getHiveServiceStatus } from '../service.js';
|
|
8
|
+
import { resolveContext, resolveServiceConfig } from '../core/context.js';
|
|
9
|
+
import { runDoctorChecks } from '../flows/doctor.js';
|
|
10
|
+
import { runSetupWizard } from '../flows/setup.js';
|
|
11
|
+
import { runDownFlow, runLogsFlow, runUpFlow, runUpdateFlow } from '../flows/system.js';
|
|
12
|
+
import { startHiveServer } from '../../index.js';
|
|
13
|
+
|
|
14
|
+
export function registerRootCommands(program) {
|
|
15
|
+
program
|
|
16
|
+
.command('setup')
|
|
17
|
+
.description('Run guided setup for env, tunnel, and service')
|
|
18
|
+
.option('--env-file <path>', 'env file path')
|
|
19
|
+
.option('--domain <domain>', 'public domain')
|
|
20
|
+
.option('--tunnel-name <name>', 'tunnel name')
|
|
21
|
+
.option('--cloudflared-config-file <path>', 'cloudflared config file')
|
|
22
|
+
.option('--yes', 'non-interactive mode', false)
|
|
23
|
+
.action(runSetupWizard);
|
|
24
|
+
|
|
25
|
+
program
|
|
26
|
+
.command('update')
|
|
27
|
+
.description('Update Hive from npm and restart installed services')
|
|
28
|
+
.option('--package <name>', 'npm package override')
|
|
29
|
+
.action(runUpdateFlow);
|
|
30
|
+
|
|
31
|
+
program
|
|
32
|
+
.command('up')
|
|
33
|
+
.description('Start installed Hive + cloudflared services')
|
|
34
|
+
.action(runUpFlow);
|
|
35
|
+
|
|
36
|
+
program
|
|
37
|
+
.command('down')
|
|
38
|
+
.description('Stop installed Hive + cloudflared services')
|
|
39
|
+
.action(runDownFlow);
|
|
40
|
+
|
|
41
|
+
program
|
|
42
|
+
.command('logs')
|
|
43
|
+
.description('Stream logs for Hive and/or cloudflared services')
|
|
44
|
+
.option('-c, --component <name>', 'hive|tunnel|both', 'hive')
|
|
45
|
+
.option('-n, --lines <n>', 'lines to show', '80')
|
|
46
|
+
.option('--no-follow', 'do not follow logs')
|
|
47
|
+
.action(runLogsFlow);
|
|
48
|
+
|
|
49
|
+
program
|
|
50
|
+
.command('doctor')
|
|
51
|
+
.description('Run prerequisite and configuration checks')
|
|
52
|
+
.option('--env-file <path>', 'env file path')
|
|
53
|
+
.action(async (options) => {
|
|
54
|
+
const { envFile } = await resolveContext(options);
|
|
55
|
+
await runDoctorChecks({ envFile, includeCloudflared: true });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
program
|
|
59
|
+
.command('run')
|
|
60
|
+
.description('Start Hive server immediately')
|
|
61
|
+
.option('--env-file <path>', 'env file path')
|
|
62
|
+
.option('--quiet', 'reduce startup logs', false)
|
|
63
|
+
.action(async (options) => {
|
|
64
|
+
const { envFile } = await resolveContext(options);
|
|
65
|
+
if (!existsSync(envFile)) {
|
|
66
|
+
throw new CliError(`Env file not found: ${envFile}. Run: hive env init`, EXIT.FAIL);
|
|
67
|
+
}
|
|
68
|
+
await startHiveServer({ envFile, quiet: Boolean(options.quiet) });
|
|
69
|
+
info(`Hive server started using env: ${envFile}`);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
program
|
|
73
|
+
.command('status')
|
|
74
|
+
.description('Quick status summary (service + tunnel + doctor-lite)')
|
|
75
|
+
.option('--env-file <path>', 'env file path')
|
|
76
|
+
.action(async (options) => {
|
|
77
|
+
const { config, envFile } = await resolveContext(options);
|
|
78
|
+
section('Hive Status');
|
|
79
|
+
console.log(`Config: ${HIVE_CONFIG_FILE}`);
|
|
80
|
+
console.log(`Env: ${envFile} ${existsSync(envFile) ? '' : '(missing)'}`);
|
|
81
|
+
if (config.domain) console.log(`Domain: ${config.domain}`);
|
|
82
|
+
if (config.tunnelName) console.log(`Tunnel: ${config.tunnelName}`);
|
|
83
|
+
|
|
84
|
+
const svc = resolveServiceConfig(config);
|
|
85
|
+
const serviceStatus = await getHiveServiceStatus(svc).catch(() => ({ active: false, detail: 'not installed' }));
|
|
86
|
+
console.log(`Service ${svc.serviceName}: ${serviceStatus.active ? 'active' : 'inactive'}`);
|
|
87
|
+
|
|
88
|
+
const tunnelSvc = await cloudflaredServiceStatus().catch(() => false);
|
|
89
|
+
console.log(`cloudflared service: ${tunnelSvc ? 'active' : 'inactive or unknown'}`);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { loadHiveConfig, updateHiveConfig } from '../config.js';
|
|
2
|
+
import { parseInteger, resolveServiceConfig, resolveContext } from '../core/context.js';
|
|
3
|
+
import {
|
|
4
|
+
getHiveServiceStatus,
|
|
5
|
+
installHiveService,
|
|
6
|
+
restartHiveService,
|
|
7
|
+
startHiveService,
|
|
8
|
+
stopHiveService,
|
|
9
|
+
streamHiveServiceLogs,
|
|
10
|
+
uninstallHiveService,
|
|
11
|
+
} from '../service.js';
|
|
12
|
+
import { section, success } from '../output.js';
|
|
13
|
+
|
|
14
|
+
export function registerServiceCommands(program) {
|
|
15
|
+
const service = program.command('service').description('Manage Hive OS service');
|
|
16
|
+
|
|
17
|
+
service
|
|
18
|
+
.command('install')
|
|
19
|
+
.description('Install Hive as launchd/systemd service')
|
|
20
|
+
.option('--env-file <path>', 'env file path')
|
|
21
|
+
.option('--yes', 'non-interactive mode', false)
|
|
22
|
+
.action(async (options) => {
|
|
23
|
+
const { config, envFile } = await resolveContext(options);
|
|
24
|
+
const infoOut = await installHiveService({
|
|
25
|
+
envFile,
|
|
26
|
+
yes: Boolean(options.yes),
|
|
27
|
+
serviceName: config.serviceName,
|
|
28
|
+
});
|
|
29
|
+
await updateHiveConfig({
|
|
30
|
+
envFile,
|
|
31
|
+
servicePlatform: infoOut.servicePlatform,
|
|
32
|
+
serviceName: infoOut.serviceName,
|
|
33
|
+
});
|
|
34
|
+
success(`Service installed: ${infoOut.serviceName}`);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
service
|
|
38
|
+
.command('start')
|
|
39
|
+
.description('Start Hive service')
|
|
40
|
+
.action(async () => {
|
|
41
|
+
const config = await loadHiveConfig();
|
|
42
|
+
const svc = resolveServiceConfig(config);
|
|
43
|
+
await startHiveService(svc);
|
|
44
|
+
success('Hive service started');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
service
|
|
48
|
+
.command('stop')
|
|
49
|
+
.description('Stop Hive service')
|
|
50
|
+
.action(async () => {
|
|
51
|
+
const config = await loadHiveConfig();
|
|
52
|
+
const svc = resolveServiceConfig(config);
|
|
53
|
+
await stopHiveService(svc);
|
|
54
|
+
success('Hive service stopped');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
service
|
|
58
|
+
.command('restart')
|
|
59
|
+
.description('Restart Hive service')
|
|
60
|
+
.action(async () => {
|
|
61
|
+
const config = await loadHiveConfig();
|
|
62
|
+
const svc = resolveServiceConfig(config);
|
|
63
|
+
await restartHiveService(svc);
|
|
64
|
+
success('Hive service restarted');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
service
|
|
68
|
+
.command('status')
|
|
69
|
+
.description('Show Hive service status')
|
|
70
|
+
.action(async () => {
|
|
71
|
+
const config = await loadHiveConfig();
|
|
72
|
+
const svc = resolveServiceConfig(config);
|
|
73
|
+
const status = await getHiveServiceStatus(svc);
|
|
74
|
+
section('Hive Service Status');
|
|
75
|
+
console.log(`Service: ${svc.serviceName} (${svc.servicePlatform})`);
|
|
76
|
+
console.log(`Active: ${status.active ? 'yes' : 'no'}`);
|
|
77
|
+
if (status.detail) {
|
|
78
|
+
console.log('');
|
|
79
|
+
console.log(status.detail);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
service
|
|
84
|
+
.command('logs')
|
|
85
|
+
.description('Stream service logs')
|
|
86
|
+
.option('-n, --lines <n>', 'lines to show', '80')
|
|
87
|
+
.option('--no-follow', 'do not follow logs')
|
|
88
|
+
.action(async (options) => {
|
|
89
|
+
const config = await loadHiveConfig();
|
|
90
|
+
const svc = resolveServiceConfig(config);
|
|
91
|
+
const lines = parseInteger(options.lines, 'lines');
|
|
92
|
+
await streamHiveServiceLogs({
|
|
93
|
+
...svc,
|
|
94
|
+
follow: Boolean(options.follow),
|
|
95
|
+
lines,
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
service
|
|
100
|
+
.command('uninstall')
|
|
101
|
+
.description('Uninstall Hive service')
|
|
102
|
+
.option('--yes', 'skip confirmation', false)
|
|
103
|
+
.action(async (options) => {
|
|
104
|
+
const config = await loadHiveConfig();
|
|
105
|
+
const svc = resolveServiceConfig(config);
|
|
106
|
+
await uninstallHiveService({
|
|
107
|
+
...svc,
|
|
108
|
+
yes: Boolean(options.yes),
|
|
109
|
+
});
|
|
110
|
+
success(`Service removed: ${svc.serviceName}`);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import process from 'process';
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_CLOUDFLARED_CERT,
|
|
4
|
+
DEFAULT_CLOUDFLARED_CONFIG,
|
|
5
|
+
DEFAULT_TUNNEL_NAME,
|
|
6
|
+
EXIT,
|
|
7
|
+
} from '../constants.js';
|
|
8
|
+
import { CliError } from '../errors.js';
|
|
9
|
+
import { inferDomainFromRedirect } from '../env-file.js';
|
|
10
|
+
import { loadHiveConfig, updateHiveConfig } from '../config.js';
|
|
11
|
+
import { validateDomain } from '../checks.js';
|
|
12
|
+
import { run } from '../exec.js';
|
|
13
|
+
import {
|
|
14
|
+
cloudflaredServiceStatus,
|
|
15
|
+
installCloudflaredService,
|
|
16
|
+
runTunnelForeground,
|
|
17
|
+
setupTunnel,
|
|
18
|
+
tunnelStatus,
|
|
19
|
+
} from '../tunnel.js';
|
|
20
|
+
import { section, success, fail } from '../output.js';
|
|
21
|
+
import {
|
|
22
|
+
loadValidatedEnv,
|
|
23
|
+
parseInteger,
|
|
24
|
+
requiredOrFallback,
|
|
25
|
+
resolveContext,
|
|
26
|
+
setRedirectUriForDomain,
|
|
27
|
+
} from '../core/context.js';
|
|
28
|
+
|
|
29
|
+
export function registerTunnelCommands(program) {
|
|
30
|
+
const tunnel = program.command('tunnel').description('Manage Cloudflare tunnel');
|
|
31
|
+
|
|
32
|
+
tunnel
|
|
33
|
+
.command('setup')
|
|
34
|
+
.description('Run full tunnel lifecycle setup')
|
|
35
|
+
.option('--env-file <path>', 'env file path')
|
|
36
|
+
.option('--domain <domain>', 'public domain')
|
|
37
|
+
.option('--tunnel-name <name>', 'tunnel name')
|
|
38
|
+
.option('--cloudflared-config-file <path>', 'cloudflared config file')
|
|
39
|
+
.option('--install-service', 'install cloudflared service', false)
|
|
40
|
+
.option('--yes', 'non-interactive mode', false)
|
|
41
|
+
.action(async (options) => {
|
|
42
|
+
section('Tunnel Setup');
|
|
43
|
+
const { config, envFile } = await resolveContext(options);
|
|
44
|
+
const { env, issues } = await loadValidatedEnv(envFile, { requireFile: true });
|
|
45
|
+
if (issues.length > 0) {
|
|
46
|
+
for (const issue of issues) fail(issue);
|
|
47
|
+
throw new CliError('Fix env file first (hive env check)', EXIT.FAIL);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const domain = requiredOrFallback(
|
|
51
|
+
options.domain,
|
|
52
|
+
inferDomainFromRedirect(env.DISCORD_REDIRECT_URI) || config.domain
|
|
53
|
+
);
|
|
54
|
+
if (!validateDomain(domain)) {
|
|
55
|
+
throw new CliError(`Invalid domain: ${domain}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const tunnelName = requiredOrFallback(options.tunnelName, config.tunnelName || DEFAULT_TUNNEL_NAME);
|
|
59
|
+
const cloudflaredConfigFile = requiredOrFallback(
|
|
60
|
+
options.cloudflaredConfigFile,
|
|
61
|
+
config.cloudflaredConfigFile || DEFAULT_CLOUDFLARED_CONFIG
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const tunnelResult = await setupTunnel({
|
|
65
|
+
tunnelName,
|
|
66
|
+
domain,
|
|
67
|
+
configFile: cloudflaredConfigFile,
|
|
68
|
+
certPath: DEFAULT_CLOUDFLARED_CERT,
|
|
69
|
+
port: parseInteger(env.PORT, 'PORT'),
|
|
70
|
+
yjsPort: parseInteger(env.YJS_PORT, 'YJS_PORT'),
|
|
71
|
+
yes: Boolean(options.yes),
|
|
72
|
+
installService: Boolean(options.installService),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const nextEnv = await setRedirectUriForDomain({
|
|
76
|
+
envFile,
|
|
77
|
+
env,
|
|
78
|
+
domain,
|
|
79
|
+
yes: Boolean(options.yes),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await updateHiveConfig({
|
|
83
|
+
envFile,
|
|
84
|
+
domain,
|
|
85
|
+
tunnelName,
|
|
86
|
+
tunnelId: tunnelResult.tunnelId,
|
|
87
|
+
tunnelCredentialsFile: tunnelResult.credentialsFile,
|
|
88
|
+
cloudflaredConfigFile,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (nextEnv.DISCORD_REDIRECT_URI !== env.DISCORD_REDIRECT_URI) {
|
|
92
|
+
success('Redirect URI synced for tunnel domain');
|
|
93
|
+
}
|
|
94
|
+
success('Tunnel setup complete');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
tunnel
|
|
98
|
+
.command('status')
|
|
99
|
+
.description('Show tunnel status and config')
|
|
100
|
+
.option('--tunnel-name <name>', 'tunnel name')
|
|
101
|
+
.option('--cloudflared-config-file <path>', 'cloudflared config path')
|
|
102
|
+
.action(async (options) => {
|
|
103
|
+
const config = await loadHiveConfig();
|
|
104
|
+
const tunnelName = requiredOrFallback(options.tunnelName, config.tunnelName || DEFAULT_TUNNEL_NAME);
|
|
105
|
+
const cloudflaredConfigFile = requiredOrFallback(
|
|
106
|
+
options.cloudflaredConfigFile,
|
|
107
|
+
config.cloudflaredConfigFile || DEFAULT_CLOUDFLARED_CONFIG
|
|
108
|
+
);
|
|
109
|
+
const status = await tunnelStatus({ tunnelName, configFile: cloudflaredConfigFile });
|
|
110
|
+
section('Tunnel Status');
|
|
111
|
+
console.log(`Name: ${tunnelName}`);
|
|
112
|
+
console.log(`Tunnel ID: ${status.tunnel?.id || '(not found)'}`);
|
|
113
|
+
console.log(`Config file: ${status.configFile} ${status.configExists ? '' : '(missing)'}`);
|
|
114
|
+
if (config.domain) {
|
|
115
|
+
console.log(`Domain: ${config.domain}`);
|
|
116
|
+
}
|
|
117
|
+
const svc = await cloudflaredServiceStatus().catch(() => false);
|
|
118
|
+
console.log(`cloudflared service: ${svc ? 'active' : 'inactive or unknown'}`);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
tunnel
|
|
122
|
+
.command('run')
|
|
123
|
+
.description('Run tunnel in foreground')
|
|
124
|
+
.option('--tunnel-name <name>', 'tunnel name')
|
|
125
|
+
.action(async (options) => {
|
|
126
|
+
const config = await loadHiveConfig();
|
|
127
|
+
const tunnelName = requiredOrFallback(options.tunnelName, config.tunnelName || DEFAULT_TUNNEL_NAME);
|
|
128
|
+
await runTunnelForeground({ tunnelName });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
tunnel
|
|
132
|
+
.command('service-install')
|
|
133
|
+
.description('Install cloudflared as a system service')
|
|
134
|
+
.action(async () => {
|
|
135
|
+
section('Tunnel Service Install');
|
|
136
|
+
await installCloudflaredService();
|
|
137
|
+
success('cloudflared service installed');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
tunnel
|
|
141
|
+
.command('service-status')
|
|
142
|
+
.description('Show cloudflared service status')
|
|
143
|
+
.action(async () => {
|
|
144
|
+
const active = await cloudflaredServiceStatus();
|
|
145
|
+
section('Tunnel Service Status');
|
|
146
|
+
console.log(active ? 'active' : 'inactive');
|
|
147
|
+
if (process.platform === 'darwin') {
|
|
148
|
+
const listing = await run('launchctl', ['list']).catch(() => ({ stdout: '' }));
|
|
149
|
+
const row = listing.stdout
|
|
150
|
+
.split('\n')
|
|
151
|
+
.find((line) => line.toLowerCase().includes('cloudflared'));
|
|
152
|
+
if (row) {
|
|
153
|
+
console.log('');
|
|
154
|
+
console.log(row.trim());
|
|
155
|
+
}
|
|
156
|
+
} else if (process.platform === 'linux') {
|
|
157
|
+
const status = await run('sudo', ['systemctl', 'status', 'cloudflared', '--no-pager', '--lines', '20'])
|
|
158
|
+
.catch(() => ({ stdout: '' }));
|
|
159
|
+
if (status.stdout) {
|
|
160
|
+
console.log('');
|
|
161
|
+
console.log(status.stdout);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
package/cli/core/app.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import process from 'process';
|
|
2
|
+
import { Command, CommanderError } from 'commander';
|
|
3
|
+
import { EXIT } from '../constants.js';
|
|
4
|
+
import { CliError } from '../errors.js';
|
|
5
|
+
import { registerEnvCommands } from '../commands/env.js';
|
|
6
|
+
import { registerRootCommands } from '../commands/root.js';
|
|
7
|
+
import { registerServiceCommands } from '../commands/service.js';
|
|
8
|
+
import { registerTunnelCommands } from '../commands/tunnel.js';
|
|
9
|
+
|
|
10
|
+
export class HiveCliApp {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.program = this.createProgram();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
createProgram() {
|
|
16
|
+
const program = new Command();
|
|
17
|
+
|
|
18
|
+
program
|
|
19
|
+
.name('hive')
|
|
20
|
+
.description('Hive server operations CLI')
|
|
21
|
+
.showHelpAfterError()
|
|
22
|
+
.version('1.0.0');
|
|
23
|
+
|
|
24
|
+
registerRootCommands(program);
|
|
25
|
+
registerEnvCommands(program);
|
|
26
|
+
registerTunnelCommands(program);
|
|
27
|
+
registerServiceCommands(program);
|
|
28
|
+
|
|
29
|
+
return program;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async run(argv = process.argv) {
|
|
33
|
+
if ((argv?.length ?? 0) <= 2) {
|
|
34
|
+
this.program.outputHelp();
|
|
35
|
+
return EXIT.OK;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.program.exitOverride();
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
await this.program.parseAsync(argv);
|
|
42
|
+
return EXIT.OK;
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (err instanceof CommanderError) {
|
|
45
|
+
if (
|
|
46
|
+
err.code === 'commander.helpDisplayed'
|
|
47
|
+
|| err.code === 'commander.help'
|
|
48
|
+
|| err.message === '(outputHelp)'
|
|
49
|
+
) {
|
|
50
|
+
return EXIT.OK;
|
|
51
|
+
}
|
|
52
|
+
throw new CliError(err.message, err.exitCode ?? EXIT.FAIL);
|
|
53
|
+
}
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|