@fyresmith/hive-server 4.0.1 → 5.0.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 +20 -17
- package/assets/plugin/hive/main.js +16384 -0
- package/assets/plugin/hive/manifest.json +9 -0
- package/assets/plugin/hive/styles.css +1040 -0
- package/assets/template-vault/.obsidian/app.json +1 -0
- package/assets/template-vault/.obsidian/appearance.json +1 -0
- package/assets/template-vault/.obsidian/core-plugins.json +33 -0
- package/assets/template-vault/.obsidian/graph.json +22 -0
- package/assets/template-vault/.obsidian/workspace.json +206 -0
- package/assets/template-vault/Welcome.md +5 -0
- package/cli/commands/env.js +4 -4
- package/cli/commands/managed.js +22 -17
- package/cli/commands/root.js +48 -2
- package/cli/commands/tunnel.js +1 -16
- package/cli/constants.js +1 -10
- package/cli/core/context.js +3 -22
- package/cli/env-file.js +15 -29
- package/cli/flows/doctor.js +1 -6
- package/cli/flows/setup.js +106 -31
- package/index.js +81 -45
- package/lib/accountState.js +189 -0
- package/lib/authTokens.js +75 -0
- package/lib/bundleBuilder.js +169 -0
- package/lib/dashboardAuth.js +80 -0
- package/lib/managedState.js +262 -55
- package/lib/setupOrchestrator.js +76 -0
- package/lib/yjsServer.js +10 -7
- package/package.json +3 -2
- package/routes/auth.js +403 -82
- package/routes/dashboard.js +590 -0
- package/routes/managed.js +0 -174
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"file-explorer": true,
|
|
3
|
+
"global-search": true,
|
|
4
|
+
"switcher": true,
|
|
5
|
+
"graph": true,
|
|
6
|
+
"backlink": true,
|
|
7
|
+
"canvas": true,
|
|
8
|
+
"outgoing-link": true,
|
|
9
|
+
"tag-pane": true,
|
|
10
|
+
"footnotes": false,
|
|
11
|
+
"properties": true,
|
|
12
|
+
"page-preview": true,
|
|
13
|
+
"daily-notes": true,
|
|
14
|
+
"templates": true,
|
|
15
|
+
"note-composer": true,
|
|
16
|
+
"command-palette": true,
|
|
17
|
+
"slash-command": false,
|
|
18
|
+
"editor-status": true,
|
|
19
|
+
"bookmarks": true,
|
|
20
|
+
"markdown-importer": false,
|
|
21
|
+
"zk-prefixer": false,
|
|
22
|
+
"random-note": false,
|
|
23
|
+
"outline": true,
|
|
24
|
+
"word-count": true,
|
|
25
|
+
"slides": false,
|
|
26
|
+
"audio-recorder": false,
|
|
27
|
+
"workspaces": false,
|
|
28
|
+
"file-recovery": true,
|
|
29
|
+
"publish": false,
|
|
30
|
+
"sync": true,
|
|
31
|
+
"bases": true,
|
|
32
|
+
"webviewer": false
|
|
33
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"collapse-filter": true,
|
|
3
|
+
"search": "",
|
|
4
|
+
"showTags": false,
|
|
5
|
+
"showAttachments": false,
|
|
6
|
+
"hideUnresolved": false,
|
|
7
|
+
"showOrphans": true,
|
|
8
|
+
"collapse-color-groups": true,
|
|
9
|
+
"colorGroups": [],
|
|
10
|
+
"collapse-display": true,
|
|
11
|
+
"showArrow": false,
|
|
12
|
+
"textFadeMultiplier": 0,
|
|
13
|
+
"nodeSizeMultiplier": 1,
|
|
14
|
+
"lineSizeMultiplier": 1,
|
|
15
|
+
"collapse-forces": true,
|
|
16
|
+
"centerStrength": 0.518713248970312,
|
|
17
|
+
"repelStrength": 10,
|
|
18
|
+
"linkStrength": 1,
|
|
19
|
+
"linkDistance": 250,
|
|
20
|
+
"scale": 1,
|
|
21
|
+
"close": true
|
|
22
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
{
|
|
2
|
+
"main": {
|
|
3
|
+
"id": "7b9f3c22c91d897c",
|
|
4
|
+
"type": "split",
|
|
5
|
+
"children": [
|
|
6
|
+
{
|
|
7
|
+
"id": "eea5e6133fb6313d",
|
|
8
|
+
"type": "tabs",
|
|
9
|
+
"children": [
|
|
10
|
+
{
|
|
11
|
+
"id": "20957127bee176ea",
|
|
12
|
+
"type": "leaf",
|
|
13
|
+
"state": {
|
|
14
|
+
"type": "markdown",
|
|
15
|
+
"state": {
|
|
16
|
+
"file": "Welcome.md",
|
|
17
|
+
"mode": "source",
|
|
18
|
+
"source": false
|
|
19
|
+
},
|
|
20
|
+
"icon": "lucide-file",
|
|
21
|
+
"title": "Welcome"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "3606ffb170dd4603",
|
|
28
|
+
"type": "tabs",
|
|
29
|
+
"children": [
|
|
30
|
+
{
|
|
31
|
+
"id": "457fdb78bef02573",
|
|
32
|
+
"type": "leaf",
|
|
33
|
+
"state": {
|
|
34
|
+
"type": "graph",
|
|
35
|
+
"state": {},
|
|
36
|
+
"icon": "lucide-git-fork",
|
|
37
|
+
"title": "Graph view"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"direction": "vertical"
|
|
44
|
+
},
|
|
45
|
+
"left": {
|
|
46
|
+
"id": "c93fdc6c295009fd",
|
|
47
|
+
"type": "split",
|
|
48
|
+
"children": [
|
|
49
|
+
{
|
|
50
|
+
"id": "0b2bc1cb1a6c5fdb",
|
|
51
|
+
"type": "tabs",
|
|
52
|
+
"children": [
|
|
53
|
+
{
|
|
54
|
+
"id": "332749683c18294c",
|
|
55
|
+
"type": "leaf",
|
|
56
|
+
"state": {
|
|
57
|
+
"type": "file-explorer",
|
|
58
|
+
"state": {
|
|
59
|
+
"sortOrder": "alphabetical",
|
|
60
|
+
"autoReveal": false
|
|
61
|
+
},
|
|
62
|
+
"icon": "lucide-folder-closed",
|
|
63
|
+
"title": "Files"
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"id": "2824dbf53a5c4597",
|
|
68
|
+
"type": "leaf",
|
|
69
|
+
"state": {
|
|
70
|
+
"type": "search",
|
|
71
|
+
"state": {
|
|
72
|
+
"query": "",
|
|
73
|
+
"matchingCase": false,
|
|
74
|
+
"explainSearch": false,
|
|
75
|
+
"collapseAll": false,
|
|
76
|
+
"extraContext": false,
|
|
77
|
+
"sortOrder": "alphabetical"
|
|
78
|
+
},
|
|
79
|
+
"icon": "lucide-search",
|
|
80
|
+
"title": "Search"
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"id": "9f275c920d60b878",
|
|
85
|
+
"type": "leaf",
|
|
86
|
+
"state": {
|
|
87
|
+
"type": "bookmarks",
|
|
88
|
+
"state": {},
|
|
89
|
+
"icon": "lucide-bookmark",
|
|
90
|
+
"title": "Bookmarks"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
],
|
|
96
|
+
"direction": "horizontal",
|
|
97
|
+
"width": 300
|
|
98
|
+
},
|
|
99
|
+
"right": {
|
|
100
|
+
"id": "720628f43492de54",
|
|
101
|
+
"type": "split",
|
|
102
|
+
"children": [
|
|
103
|
+
{
|
|
104
|
+
"id": "e2e6ee489b84f739",
|
|
105
|
+
"type": "tabs",
|
|
106
|
+
"children": [
|
|
107
|
+
{
|
|
108
|
+
"id": "97a8f2230c937318",
|
|
109
|
+
"type": "leaf",
|
|
110
|
+
"state": {
|
|
111
|
+
"type": "backlink",
|
|
112
|
+
"state": {
|
|
113
|
+
"file": "Welcome.md",
|
|
114
|
+
"collapseAll": false,
|
|
115
|
+
"extraContext": false,
|
|
116
|
+
"sortOrder": "alphabetical",
|
|
117
|
+
"showSearch": false,
|
|
118
|
+
"searchQuery": "",
|
|
119
|
+
"backlinkCollapsed": false,
|
|
120
|
+
"unlinkedCollapsed": true
|
|
121
|
+
},
|
|
122
|
+
"icon": "links-coming-in",
|
|
123
|
+
"title": "Backlinks for Welcome"
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"id": "2a344f218762d53a",
|
|
128
|
+
"type": "leaf",
|
|
129
|
+
"state": {
|
|
130
|
+
"type": "outgoing-link",
|
|
131
|
+
"state": {
|
|
132
|
+
"file": "Welcome.md",
|
|
133
|
+
"linksCollapsed": false,
|
|
134
|
+
"unlinkedCollapsed": true
|
|
135
|
+
},
|
|
136
|
+
"icon": "links-going-out",
|
|
137
|
+
"title": "Outgoing links from Welcome"
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"id": "072e764fb5cc77fa",
|
|
142
|
+
"type": "leaf",
|
|
143
|
+
"state": {
|
|
144
|
+
"type": "tag",
|
|
145
|
+
"state": {
|
|
146
|
+
"sortOrder": "frequency",
|
|
147
|
+
"useHierarchy": true,
|
|
148
|
+
"showSearch": false,
|
|
149
|
+
"searchQuery": ""
|
|
150
|
+
},
|
|
151
|
+
"icon": "lucide-tags",
|
|
152
|
+
"title": "Tags"
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"id": "f304846ef7927a68",
|
|
157
|
+
"type": "leaf",
|
|
158
|
+
"state": {
|
|
159
|
+
"type": "all-properties",
|
|
160
|
+
"state": {
|
|
161
|
+
"sortOrder": "frequency",
|
|
162
|
+
"showSearch": false,
|
|
163
|
+
"searchQuery": ""
|
|
164
|
+
},
|
|
165
|
+
"icon": "lucide-archive",
|
|
166
|
+
"title": "All properties"
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"id": "de791fb3b1b7afdb",
|
|
171
|
+
"type": "leaf",
|
|
172
|
+
"state": {
|
|
173
|
+
"type": "outline",
|
|
174
|
+
"state": {
|
|
175
|
+
"file": "Welcome.md",
|
|
176
|
+
"followCursor": false,
|
|
177
|
+
"showSearch": false,
|
|
178
|
+
"searchQuery": ""
|
|
179
|
+
},
|
|
180
|
+
"icon": "lucide-list",
|
|
181
|
+
"title": "Outline of Welcome"
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
}
|
|
186
|
+
],
|
|
187
|
+
"direction": "horizontal",
|
|
188
|
+
"width": 300,
|
|
189
|
+
"collapsed": true
|
|
190
|
+
},
|
|
191
|
+
"left-ribbon": {
|
|
192
|
+
"hiddenItems": {
|
|
193
|
+
"switcher:Open quick switcher": false,
|
|
194
|
+
"graph:Open graph view": false,
|
|
195
|
+
"canvas:Create new canvas": false,
|
|
196
|
+
"daily-notes:Open today's daily note": false,
|
|
197
|
+
"templates:Insert template": false,
|
|
198
|
+
"command-palette:Open command palette": false,
|
|
199
|
+
"bases:Create new base": false
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
"active": "20957127bee176ea",
|
|
203
|
+
"lastOpenFiles": [
|
|
204
|
+
"Welcome.md"
|
|
205
|
+
]
|
|
206
|
+
}
|
package/cli/commands/env.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { section, success, fail } from '../output.js';
|
|
2
2
|
import { EXIT } from '../constants.js';
|
|
3
3
|
import { CliError } from '../errors.js';
|
|
4
|
-
import {
|
|
4
|
+
import { loadEnvFile, normalizeEnv, promptForEnv, redactEnv, validateEnvValues } from '../env-file.js';
|
|
5
5
|
import { updateHiveConfig } from '../config.js';
|
|
6
6
|
import { assertEnvFileExists, loadValidatedEnv, resolveContext } from '../core/context.js';
|
|
7
7
|
|
|
@@ -18,13 +18,13 @@ export function registerEnvCommands(program) {
|
|
|
18
18
|
const { config, envFile } = await resolveContext(options);
|
|
19
19
|
const existing = await loadEnvFile(envFile);
|
|
20
20
|
const values = await promptForEnv({ envFile, existing, yes: options.yes });
|
|
21
|
-
const issues = validateEnvValues(values);
|
|
21
|
+
const issues = validateEnvValues(values, { requireVaultPath: false });
|
|
22
22
|
if (issues.length > 0) {
|
|
23
23
|
for (const issue of issues) fail(issue);
|
|
24
24
|
throw new CliError('Env file has validation issues', EXIT.FAIL);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
const domain =
|
|
27
|
+
const domain = config.domain;
|
|
28
28
|
await updateHiveConfig({ envFile, domain });
|
|
29
29
|
success(`Env file ready at ${envFile}`);
|
|
30
30
|
});
|
|
@@ -41,7 +41,7 @@ export function registerEnvCommands(program) {
|
|
|
41
41
|
|
|
42
42
|
const existing = await loadEnvFile(envFile);
|
|
43
43
|
const values = await promptForEnv({ envFile, existing, yes: options.yes });
|
|
44
|
-
const issues = validateEnvValues(values);
|
|
44
|
+
const issues = validateEnvValues(values, { requireVaultPath: false });
|
|
45
45
|
if (issues.length > 0) {
|
|
46
46
|
for (const issue of issues) fail(issue);
|
|
47
47
|
throw new CliError('Env file has validation issues', EXIT.FAIL);
|
package/cli/commands/managed.js
CHANGED
|
@@ -19,14 +19,14 @@ async function resolveManagedInputs(options) {
|
|
|
19
19
|
}
|
|
20
20
|
return {
|
|
21
21
|
vaultPath: env.VAULT_PATH,
|
|
22
|
-
|
|
22
|
+
hiveServerUrl: env.HIVE_SERVER_URL || '',
|
|
23
23
|
envFile,
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function assertInitialized(state) {
|
|
28
28
|
if (!state) {
|
|
29
|
-
throw new CliError('Managed vault is not initialized. Run
|
|
29
|
+
throw new CliError('Managed vault is not initialized. Run hive setup or dashboard setup first.', EXIT.FAIL);
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -38,21 +38,19 @@ export function registerManagedCommands(program) {
|
|
|
38
38
|
.description('Show managed vault status')
|
|
39
39
|
.option('--env-file <path>', 'env file path')
|
|
40
40
|
.action(async (options) => {
|
|
41
|
-
const { vaultPath,
|
|
41
|
+
const { vaultPath, envFile } = await resolveManagedInputs(options);
|
|
42
42
|
const state = await loadManagedState(vaultPath);
|
|
43
43
|
section('Managed Status');
|
|
44
44
|
console.log(`Env: ${envFile}`);
|
|
45
45
|
if (!state) {
|
|
46
46
|
console.log('Initialized: no');
|
|
47
|
-
console.log(`Configured owner: ${ownerDiscordId}`);
|
|
48
47
|
return;
|
|
49
48
|
}
|
|
50
|
-
const status = describeManagedStatus(state,
|
|
49
|
+
const status = describeManagedStatus(state, state.ownerId);
|
|
51
50
|
console.log('Initialized: yes');
|
|
51
|
+
console.log(`Vault Name: ${state.vaultName ?? '(not set)'}`);
|
|
52
52
|
console.log(`Vault ID: ${status.vaultId}`);
|
|
53
|
-
console.log(`Owner: ${state.
|
|
54
|
-
console.log(`Configured owner: ${ownerDiscordId}`);
|
|
55
|
-
console.log(`Owner matches env: ${state.ownerDiscordId === ownerDiscordId ? 'yes' : 'no'}`);
|
|
53
|
+
console.log(`Owner: ${state.ownerId}`);
|
|
56
54
|
console.log(`Members: ${status.memberCount}`);
|
|
57
55
|
console.log(`Invites: ${Object.keys(state.invites ?? {}).length}`);
|
|
58
56
|
});
|
|
@@ -64,13 +62,20 @@ export function registerManagedCommands(program) {
|
|
|
64
62
|
.description('Create a single-use invite code')
|
|
65
63
|
.option('--env-file <path>', 'env file path')
|
|
66
64
|
.action(async (options) => {
|
|
67
|
-
const { vaultPath,
|
|
65
|
+
const { vaultPath, hiveServerUrl } = await resolveManagedInputs(options);
|
|
66
|
+
const state = await loadManagedState(vaultPath);
|
|
67
|
+
if (!state) {
|
|
68
|
+
throw new CliError('Managed vault is not initialized. Run hive setup or dashboard setup.', EXIT.FAIL);
|
|
69
|
+
}
|
|
68
70
|
const created = await createInvite({
|
|
69
71
|
vaultPath,
|
|
70
|
-
|
|
71
|
-
createdBy: ownerDiscordId,
|
|
72
|
+
createdBy: state.ownerId,
|
|
72
73
|
});
|
|
73
74
|
success(`Invite created: ${created.code}`);
|
|
75
|
+
if (hiveServerUrl) {
|
|
76
|
+
console.log(`Claim URL: ${hiveServerUrl}/auth/claim?code=${created.code}`);
|
|
77
|
+
console.log('Next: recipient opens claim URL, signs in, then downloads the Hive vault shell.');
|
|
78
|
+
}
|
|
74
79
|
});
|
|
75
80
|
|
|
76
81
|
invite
|
|
@@ -122,21 +127,21 @@ export function registerManagedCommands(program) {
|
|
|
122
127
|
return;
|
|
123
128
|
}
|
|
124
129
|
for (const row of members) {
|
|
125
|
-
const ownerMark = row.id === state.
|
|
130
|
+
const ownerMark = row.id === state.ownerId ? ' (owner)' : '';
|
|
126
131
|
console.log(`${row.id}${ownerMark} @${row.username} added ${row.addedAt}`);
|
|
127
132
|
}
|
|
128
133
|
});
|
|
129
134
|
|
|
130
135
|
member
|
|
131
|
-
.command('remove <
|
|
136
|
+
.command('remove <userId>')
|
|
132
137
|
.description('Remove a paired member')
|
|
133
138
|
.option('--env-file <path>', 'env file path')
|
|
134
|
-
.action(async (
|
|
139
|
+
.action(async (userId, options) => {
|
|
135
140
|
const { vaultPath } = await resolveManagedInputs(options);
|
|
136
|
-
const result = await removeMember({ vaultPath,
|
|
141
|
+
const result = await removeMember({ vaultPath, userId });
|
|
137
142
|
if (!result.removed) {
|
|
138
|
-
throw new CliError(`Member not found: ${
|
|
143
|
+
throw new CliError(`Member not found: ${userId}`, EXIT.FAIL);
|
|
139
144
|
}
|
|
140
|
-
success(`Removed member: ${
|
|
145
|
+
success(`Removed member: ${userId}`);
|
|
141
146
|
});
|
|
142
147
|
}
|
package/cli/commands/root.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { existsSync } from 'fs';
|
|
2
|
+
import { platform } from 'os';
|
|
2
3
|
import { HIVE_CONFIG_FILE, EXIT } from '../constants.js';
|
|
3
4
|
import { CliError } from '../errors.js';
|
|
4
5
|
import { loadHiveConfig } from '../config.js';
|
|
5
|
-
import { section, info } from '../output.js';
|
|
6
|
+
import { section, info, success } from '../output.js';
|
|
6
7
|
import { cloudflaredServiceStatus } from '../tunnel.js';
|
|
7
8
|
import { getHiveServiceStatus } from '../service.js';
|
|
8
|
-
import { resolveContext, resolveServiceConfig } from '../core/context.js';
|
|
9
|
+
import { resolveContext, resolveServiceConfig, loadValidatedEnv } from '../core/context.js';
|
|
10
|
+
import { loadEnvFile, normalizeEnv, writeEnvFile } from '../env-file.js';
|
|
9
11
|
import { runDoctorChecks } from '../flows/doctor.js';
|
|
10
12
|
import { runSetupWizard } from '../flows/setup.js';
|
|
11
13
|
import { runDownFlow, runLogsFlow, runUpFlow, runUpdateFlow } from '../flows/system.js';
|
|
12
14
|
import { startHiveServer } from '../../index.js';
|
|
15
|
+
import { run } from '../exec.js';
|
|
16
|
+
import { randomBytes } from 'crypto';
|
|
13
17
|
|
|
14
18
|
export function registerRootCommands(program) {
|
|
15
19
|
program
|
|
@@ -69,6 +73,48 @@ export function registerRootCommands(program) {
|
|
|
69
73
|
info(`Hive server started using env: ${envFile}`);
|
|
70
74
|
});
|
|
71
75
|
|
|
76
|
+
program
|
|
77
|
+
.command('dashboard')
|
|
78
|
+
.description('Start/open the owner dashboard')
|
|
79
|
+
.option('--env-file <path>', 'env file path')
|
|
80
|
+
.action(async (options) => {
|
|
81
|
+
const { envFile } = await resolveContext(options);
|
|
82
|
+
const { env } = await loadValidatedEnv(envFile, { requireFile: false, requireVaultPath: false });
|
|
83
|
+
|
|
84
|
+
const port = String(env.PORT || '3000').trim();
|
|
85
|
+
const serverUrl = String(env.HIVE_SERVER_URL || '').trim() || `http://localhost:${port}`;
|
|
86
|
+
const dashboardUrl = `${serverUrl}/dashboard`;
|
|
87
|
+
const localUrl = `http://127.0.0.1:${port}`;
|
|
88
|
+
const useLocalRuntime = !String(env.HIVE_SERVER_URL || '').trim();
|
|
89
|
+
|
|
90
|
+
if (useLocalRuntime) {
|
|
91
|
+
if (!String(env.JWT_SECRET || '').trim()) {
|
|
92
|
+
const existing = await loadEnvFile(envFile);
|
|
93
|
+
const next = normalizeEnv(existing);
|
|
94
|
+
next.JWT_SECRET = randomBytes(32).toString('hex');
|
|
95
|
+
await writeEnvFile(envFile, next);
|
|
96
|
+
env.JWT_SECRET = next.JWT_SECRET;
|
|
97
|
+
info(`Generated JWT_SECRET in ${envFile}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const health = await fetch(`${localUrl}/health`).then((res) => res.ok).catch(() => false);
|
|
101
|
+
if (!health) {
|
|
102
|
+
await startHiveServer({ envFile, quiet: true, allowSetupMode: true });
|
|
103
|
+
info(`Hive server started using env: ${envFile}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.log(`Dashboard: ${dashboardUrl}`);
|
|
108
|
+
|
|
109
|
+
const opener = platform() === 'win32' ? 'explorer' : platform() === 'darwin' ? 'open' : 'xdg-open';
|
|
110
|
+
try {
|
|
111
|
+
await run(opener, [dashboardUrl]);
|
|
112
|
+
success('Opened dashboard in browser');
|
|
113
|
+
} catch {
|
|
114
|
+
info('Could not open browser automatically. Visit the URL above.');
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
72
118
|
program
|
|
73
119
|
.command('status')
|
|
74
120
|
.description('Quick status summary (service + tunnel + doctor-lite)')
|
package/cli/commands/tunnel.js
CHANGED
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
EXIT,
|
|
7
7
|
} from '../constants.js';
|
|
8
8
|
import { CliError } from '../errors.js';
|
|
9
|
-
import { inferDomainFromRedirect } from '../env-file.js';
|
|
10
9
|
import { loadHiveConfig, updateHiveConfig } from '../config.js';
|
|
11
10
|
import { validateDomain } from '../checks.js';
|
|
12
11
|
import { run } from '../exec.js';
|
|
@@ -23,7 +22,6 @@ import {
|
|
|
23
22
|
parseInteger,
|
|
24
23
|
requiredOrFallback,
|
|
25
24
|
resolveContext,
|
|
26
|
-
setRedirectUriForDomain,
|
|
27
25
|
} from '../core/context.js';
|
|
28
26
|
|
|
29
27
|
export function registerTunnelCommands(program) {
|
|
@@ -47,10 +45,7 @@ export function registerTunnelCommands(program) {
|
|
|
47
45
|
throw new CliError('Fix env file first (hive env check)', EXIT.FAIL);
|
|
48
46
|
}
|
|
49
47
|
|
|
50
|
-
const domain = requiredOrFallback(
|
|
51
|
-
options.domain,
|
|
52
|
-
inferDomainFromRedirect(env.DISCORD_REDIRECT_URI) || config.domain
|
|
53
|
-
);
|
|
48
|
+
const domain = requiredOrFallback(options.domain, config.domain);
|
|
54
49
|
if (!validateDomain(domain)) {
|
|
55
50
|
throw new CliError(`Invalid domain: ${domain}`);
|
|
56
51
|
}
|
|
@@ -72,13 +67,6 @@ export function registerTunnelCommands(program) {
|
|
|
72
67
|
installService: Boolean(options.installService),
|
|
73
68
|
});
|
|
74
69
|
|
|
75
|
-
const nextEnv = await setRedirectUriForDomain({
|
|
76
|
-
envFile,
|
|
77
|
-
env,
|
|
78
|
-
domain,
|
|
79
|
-
yes: Boolean(options.yes),
|
|
80
|
-
});
|
|
81
|
-
|
|
82
70
|
await updateHiveConfig({
|
|
83
71
|
envFile,
|
|
84
72
|
domain,
|
|
@@ -88,9 +76,6 @@ export function registerTunnelCommands(program) {
|
|
|
88
76
|
cloudflaredConfigFile,
|
|
89
77
|
});
|
|
90
78
|
|
|
91
|
-
if (nextEnv.DISCORD_REDIRECT_URI !== env.DISCORD_REDIRECT_URI) {
|
|
92
|
-
success('Redirect URI synced for tunnel domain');
|
|
93
|
-
}
|
|
94
79
|
success('Tunnel setup complete');
|
|
95
80
|
});
|
|
96
81
|
|
package/cli/constants.js
CHANGED
|
@@ -1,24 +1,15 @@
|
|
|
1
1
|
import { homedir } from 'os';
|
|
2
|
-
import {
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
2
|
+
import { join } from 'path';
|
|
4
3
|
|
|
5
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
-
|
|
7
|
-
export const SERVER_ROOT = join(__dirname, '..');
|
|
8
4
|
export const HIVE_HOME = join(homedir(), '.hive');
|
|
9
5
|
export const HIVE_CONFIG_FILE = join(HIVE_HOME, 'config.json');
|
|
10
6
|
export const DEFAULT_ENV_FILE = join(HIVE_HOME, 'server', '.env');
|
|
11
|
-
export const LEGACY_ENV_FILE = join(SERVER_ROOT, '.env');
|
|
12
7
|
export const DEFAULT_DOMAIN = 'collab.example.com';
|
|
13
8
|
export const DEFAULT_TUNNEL_NAME = 'hive';
|
|
14
9
|
export const DEFAULT_CLOUDFLARED_CONFIG = join(homedir(), '.cloudflared', 'config.yml');
|
|
15
10
|
export const DEFAULT_CLOUDFLARED_CERT = join(homedir(), '.cloudflared', 'cert.pem');
|
|
16
11
|
|
|
17
12
|
export const REQUIRED_ENV_KEYS = [
|
|
18
|
-
'DISCORD_CLIENT_ID',
|
|
19
|
-
'DISCORD_CLIENT_SECRET',
|
|
20
|
-
'DISCORD_REDIRECT_URI',
|
|
21
|
-
'OWNER_DISCORD_ID',
|
|
22
13
|
'JWT_SECRET',
|
|
23
14
|
'VAULT_PATH',
|
|
24
15
|
'PORT',
|
package/cli/core/context.js
CHANGED
|
@@ -6,8 +6,7 @@ import prompts from 'prompts';
|
|
|
6
6
|
import { DEFAULT_ENV_FILE, EXIT } from '../constants.js';
|
|
7
7
|
import { CliError } from '../errors.js';
|
|
8
8
|
import { loadHiveConfig } from '../config.js';
|
|
9
|
-
import { loadEnvFile, normalizeEnv, validateEnvValues
|
|
10
|
-
import { success } from '../output.js';
|
|
9
|
+
import { loadEnvFile, normalizeEnv, validateEnvValues } from '../env-file.js';
|
|
11
10
|
import { getServiceDefaults } from '../service.js';
|
|
12
11
|
|
|
13
12
|
export function parseInteger(value, key) {
|
|
@@ -80,31 +79,13 @@ export function assertEnvFileExists(envFile) {
|
|
|
80
79
|
}
|
|
81
80
|
}
|
|
82
81
|
|
|
83
|
-
export async function loadValidatedEnv(envFile, { requireFile = true } = {}) {
|
|
82
|
+
export async function loadValidatedEnv(envFile, { requireFile = true, requireVaultPath = true } = {}) {
|
|
84
83
|
if (requireFile) {
|
|
85
84
|
assertEnvFileExists(envFile);
|
|
86
85
|
}
|
|
87
86
|
|
|
88
87
|
const raw = await loadEnvFile(envFile);
|
|
89
88
|
const env = normalizeEnv(raw);
|
|
90
|
-
const issues = validateEnvValues(env);
|
|
89
|
+
const issues = validateEnvValues(env, { requireVaultPath });
|
|
91
90
|
return { env, issues };
|
|
92
91
|
}
|
|
93
|
-
|
|
94
|
-
export async function setRedirectUriForDomain({ envFile, env, domain, yes = false }) {
|
|
95
|
-
const expected = `https://${domain}/auth/callback`;
|
|
96
|
-
if (env.DISCORD_REDIRECT_URI === expected) return env;
|
|
97
|
-
|
|
98
|
-
const shouldUpdate = await promptConfirm(
|
|
99
|
-
`Set DISCORD_REDIRECT_URI to ${expected}?`,
|
|
100
|
-
yes,
|
|
101
|
-
true
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
if (!shouldUpdate) return env;
|
|
105
|
-
|
|
106
|
-
const next = { ...env, DISCORD_REDIRECT_URI: expected };
|
|
107
|
-
await writeEnvFile(envFile, next);
|
|
108
|
-
success(`Updated DISCORD_REDIRECT_URI -> ${expected}`);
|
|
109
|
-
return next;
|
|
110
|
-
}
|
package/cli/env-file.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import dotenv from 'dotenv';
|
|
2
2
|
import prompts from 'prompts';
|
|
3
|
+
import { randomBytes } from 'crypto';
|
|
3
4
|
import { access, mkdir, readFile, writeFile } from 'fs/promises';
|
|
4
5
|
import { existsSync } from 'fs';
|
|
5
6
|
import { dirname } from 'path';
|
|
@@ -57,19 +58,12 @@ export async function writeEnvFile(envFile, values) {
|
|
|
57
58
|
await writeFile(envFile, serializeEnv(normalizeEnv(values)), 'utf-8');
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
export function
|
|
61
|
-
if (!redirectUri) return null;
|
|
62
|
-
try {
|
|
63
|
-
const u = new URL(redirectUri);
|
|
64
|
-
return u.host;
|
|
65
|
-
} catch {
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function validateEnvValues(values) {
|
|
61
|
+
export function validateEnvValues(values, { requireVaultPath = true } = {}) {
|
|
71
62
|
const issues = [];
|
|
72
|
-
|
|
63
|
+
const requiredKeys = requireVaultPath
|
|
64
|
+
? REQUIRED_ENV_KEYS
|
|
65
|
+
: REQUIRED_ENV_KEYS.filter((key) => key !== 'VAULT_PATH');
|
|
66
|
+
for (const key of requiredKeys) {
|
|
73
67
|
if (!String(values[key] ?? '').trim()) {
|
|
74
68
|
issues.push(`Missing ${key}`);
|
|
75
69
|
}
|
|
@@ -78,15 +72,6 @@ export function validateEnvValues(values) {
|
|
|
78
72
|
const port = parseInt(values.PORT ?? '', 10);
|
|
79
73
|
if (!Number.isInteger(port) || port <= 0) issues.push('PORT must be a positive integer');
|
|
80
74
|
|
|
81
|
-
try {
|
|
82
|
-
const uri = new URL(values.DISCORD_REDIRECT_URI ?? '');
|
|
83
|
-
if (!/^https?:$/.test(uri.protocol)) {
|
|
84
|
-
issues.push('DISCORD_REDIRECT_URI must use http or https');
|
|
85
|
-
}
|
|
86
|
-
} catch {
|
|
87
|
-
issues.push('DISCORD_REDIRECT_URI must be a valid URL');
|
|
88
|
-
}
|
|
89
|
-
|
|
90
75
|
return issues;
|
|
91
76
|
}
|
|
92
77
|
|
|
@@ -101,7 +86,14 @@ export async function ensureVaultPathReadable(pathValue) {
|
|
|
101
86
|
}
|
|
102
87
|
|
|
103
88
|
export async function promptForEnv({ envFile, existing, yes = false, preset = {} }) {
|
|
104
|
-
const
|
|
89
|
+
const base = { ...existing, ...preset };
|
|
90
|
+
|
|
91
|
+
// Auto-generate secrets if not already set — never prompt for these
|
|
92
|
+
if (!String(base.JWT_SECRET ?? '').trim()) {
|
|
93
|
+
base.JWT_SECRET = randomBytes(32).toString('hex');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const current = normalizeEnv(base);
|
|
105
97
|
|
|
106
98
|
if (yes) {
|
|
107
99
|
await writeEnvFile(envFile, current);
|
|
@@ -109,19 +101,13 @@ export async function promptForEnv({ envFile, existing, yes = false, preset = {}
|
|
|
109
101
|
}
|
|
110
102
|
|
|
111
103
|
const questions = [
|
|
112
|
-
{ name: 'DISCORD_CLIENT_ID', message: 'Discord Client ID' },
|
|
113
|
-
{ name: 'DISCORD_CLIENT_SECRET', message: 'Discord Client Secret', secret: true },
|
|
114
|
-
{ name: 'OWNER_DISCORD_ID', message: 'Managed vault owner Discord ID' },
|
|
115
|
-
{ name: 'JWT_SECRET', message: 'JWT secret', secret: true },
|
|
116
|
-
{ name: 'VAULT_PATH', message: 'Vault absolute path' },
|
|
117
104
|
{ name: 'PORT', message: 'HTTP port' },
|
|
118
|
-
{ name: 'DISCORD_REDIRECT_URI', message: 'Discord redirect URI' },
|
|
119
105
|
];
|
|
120
106
|
|
|
121
107
|
const answers = {};
|
|
122
108
|
for (const q of questions) {
|
|
123
109
|
const response = await prompts({
|
|
124
|
-
type:
|
|
110
|
+
type: 'text',
|
|
125
111
|
name: 'value',
|
|
126
112
|
message: q.message,
|
|
127
113
|
initial: current[q.name] ?? '',
|