@fyresmith/hive-server 4.0.0 → 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.
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ This is your new *vault*.
2
+
3
+ Make a note of something, [[create a link]], or try [the Importer](https://help.obsidian.md/Plugins/Importer)!
4
+
5
+ When you're ready, delete this note and make the vault your own.
@@ -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 { inferDomainFromRedirect, loadEnvFile, normalizeEnv, promptForEnv, redactEnv, validateEnvValues } from '../env-file.js';
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 = inferDomainFromRedirect(values.DISCORD_REDIRECT_URI) || config.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);
@@ -19,14 +19,14 @@ async function resolveManagedInputs(options) {
19
19
  }
20
20
  return {
21
21
  vaultPath: env.VAULT_PATH,
22
- ownerDiscordId: env.OWNER_DISCORD_ID,
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 pair/init flow first.', EXIT.FAIL);
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, ownerDiscordId, envFile } = await resolveManagedInputs(options);
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, ownerDiscordId);
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.ownerDiscordId}`);
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, ownerDiscordId } = await resolveManagedInputs(options);
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
- ownerDiscordId,
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.ownerDiscordId ? ' (owner)' : '';
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 <discordId>')
136
+ .command('remove <userId>')
132
137
  .description('Remove a paired member')
133
138
  .option('--env-file <path>', 'env file path')
134
- .action(async (discordId, options) => {
139
+ .action(async (userId, options) => {
135
140
  const { vaultPath } = await resolveManagedInputs(options);
136
- const result = await removeMember({ vaultPath, discordId });
141
+ const result = await removeMember({ vaultPath, userId });
137
142
  if (!result.removed) {
138
- throw new CliError(`Member not found: ${discordId}`, EXIT.FAIL);
143
+ throw new CliError(`Member not found: ${userId}`, EXIT.FAIL);
139
144
  }
140
- success(`Removed member: ${discordId}`);
145
+ success(`Removed member: ${userId}`);
141
146
  });
142
147
  }
@@ -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)')
@@ -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,34 +1,22 @@
1
1
  import { homedir } from 'os';
2
- import { dirname, join } from 'path';
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
- 'DISCORD_GUILD_ID',
22
- 'OWNER_DISCORD_ID',
23
13
  'JWT_SECRET',
24
14
  'VAULT_PATH',
25
15
  'PORT',
26
- 'YJS_PORT',
27
16
  ];
28
17
 
29
18
  export const DEFAULT_ENV_VALUES = {
30
19
  PORT: '3000',
31
- YJS_PORT: '3001',
32
20
  };
33
21
 
34
22
  export const DEFAULT_CONFIG = {
@@ -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, writeEnvFile } from '../env-file.js';
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
- }