beads-ui 0.3.0 → 0.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.
Files changed (61) hide show
  1. package/CHANGES.md +26 -0
  2. package/README.md +15 -6
  3. package/app/main.bundle.js +617 -0
  4. package/app/main.bundle.js.map +7 -0
  5. package/bin/bdui.js +2 -1
  6. package/package.json +27 -16
  7. package/server/app.js +39 -35
  8. package/server/bd.js +6 -2
  9. package/server/cli/commands.js +12 -8
  10. package/server/cli/daemon.js +20 -5
  11. package/server/cli/index.js +19 -31
  12. package/server/cli/open.js +3 -0
  13. package/server/cli/usage.js +4 -2
  14. package/server/config.js +3 -2
  15. package/server/db.js +9 -6
  16. package/server/index.js +10 -4
  17. package/server/list-adapters.js +9 -3
  18. package/server/logging.js +23 -0
  19. package/server/subscriptions.js +12 -0
  20. package/server/validators.js +2 -0
  21. package/server/watcher.js +10 -5
  22. package/server/ws.js +31 -10
  23. package/app/data/list-selectors.js +0 -98
  24. package/app/data/providers.js +0 -76
  25. package/app/data/sort.js +0 -45
  26. package/app/data/subscription-issue-store.js +0 -161
  27. package/app/data/subscription-issue-stores.js +0 -102
  28. package/app/data/subscriptions-store.js +0 -219
  29. package/app/main.js +0 -702
  30. package/app/protocol.js +0 -196
  31. package/app/protocol.md +0 -66
  32. package/app/router.js +0 -114
  33. package/app/state.js +0 -103
  34. package/app/utils/issue-id-renderer.js +0 -71
  35. package/app/utils/issue-id.js +0 -10
  36. package/app/utils/issue-type.js +0 -27
  37. package/app/utils/issue-url.js +0 -9
  38. package/app/utils/markdown.js +0 -22
  39. package/app/utils/priority-badge.js +0 -47
  40. package/app/utils/priority.js +0 -1
  41. package/app/utils/status-badge.js +0 -32
  42. package/app/utils/status.js +0 -23
  43. package/app/utils/toast.js +0 -34
  44. package/app/utils/type-badge.js +0 -33
  45. package/app/views/board.js +0 -535
  46. package/app/views/detail.js +0 -1249
  47. package/app/views/epics.js +0 -280
  48. package/app/views/issue-dialog.js +0 -163
  49. package/app/views/issue-row.js +0 -190
  50. package/app/views/list.js +0 -464
  51. package/app/views/nav.js +0 -67
  52. package/app/views/new-issue-dialog.js +0 -345
  53. package/app/ws.js +0 -279
  54. package/docs/adr/001-push-only-lists.md +0 -134
  55. package/docs/adr/002-per-subscription-stores-and-full-issue-push.md +0 -200
  56. package/docs/architecture.md +0 -194
  57. package/docs/data-exchange-subscription-plan.md +0 -198
  58. package/docs/db-watching.md +0 -30
  59. package/docs/migration-v2.md +0 -54
  60. package/docs/protocol/issues-push-v2.md +0 -179
  61. package/docs/subscription-issue-store.md +0 -112
package/bin/bdui.js CHANGED
@@ -4,6 +4,7 @@
4
4
  * Delegates to `server/cli/index.js` and sets the process exit code.
5
5
  */
6
6
  import { main } from '../server/cli/index.js';
7
+ import { debug } from '../server/logging.js';
7
8
 
8
9
  const argv = process.argv.slice(2);
9
10
 
@@ -13,6 +14,6 @@ try {
13
14
  process.exitCode = code;
14
15
  }
15
16
  } catch (err) {
16
- console.error(String(/** @type {any} */ (err)?.message || err));
17
+ debug('cli')('fatal %o', err);
17
18
  process.exitCode = 1;
18
19
  }
package/package.json CHANGED
@@ -1,7 +1,13 @@
1
1
  {
2
2
  "name": "beads-ui",
3
- "version": "0.3.0",
4
- "description": "Local‑first UI for Beads — a fast issue tracker for your coding agent.",
3
+ "version": "0.4.0",
4
+ "description": "Local UI for Beads — Collaborate on issues with your coding agent.",
5
+ "keywords": [
6
+ "agent",
7
+ "issue-tracker",
8
+ "local-first",
9
+ "ai-tools"
10
+ ],
5
11
  "homepage": "https://github.com/mantoni/beads-ui",
6
12
  "type": "module",
7
13
  "bin": {
@@ -12,7 +18,8 @@
12
18
  },
13
19
  "scripts": {
14
20
  "all": "npm run lint && npm run typecheck && npm test && npm run format:check",
15
- "start": "node server/index.js",
21
+ "start": "node server/index.js --debug",
22
+ "build": "node scripts/build-frontend.js",
16
23
  "test": "vitest run",
17
24
  "test:watch": "vitest",
18
25
  "typecheck": "tsc -p tsconfig.json --noEmit",
@@ -21,38 +28,42 @@
21
28
  "format:check": "prettier --check .",
22
29
  "preversion": "npm run all",
23
30
  "version": "changes --commits --footer",
24
- "postversion": "git push --follow-tags && npm publish"
31
+ "postversion": "git push --follow-tags && npm publish",
32
+ "prepack": "NODE_ENV=production npm run build"
33
+ },
34
+ "dependencies": {
35
+ "debug": "^4.4.3",
36
+ "dompurify": "^3.3.0",
37
+ "express": "^5.1.0",
38
+ "lit-html": "^3.3.1",
39
+ "marked": "^16.4.1",
40
+ "ws": "^8.18.3"
25
41
  },
26
42
  "devDependencies": {
27
43
  "@eslint/js": "^9.38.0",
28
44
  "@studio/changes": "^3.0.0",
29
45
  "@trivago/prettier-plugin-sort-imports": "^5.2.2",
46
+ "@types/debug": "^4.1.12",
30
47
  "@types/express": "^5.0.3",
31
48
  "@types/node": "^22.7.4",
32
49
  "@types/ws": "^8.18.1",
50
+ "esbuild": "^0.25.11",
33
51
  "eslint": "^9.11.0",
34
52
  "eslint-plugin-import": "^2.29.1",
35
- "eslint-plugin-jsdoc": "^48.10.2",
53
+ "eslint-plugin-jsdoc": "^61.1.9",
36
54
  "eslint-plugin-n": "^17.9.0",
37
- "eslint-plugin-promise": "^6.1.1",
38
55
  "globals": "^16.4.0",
39
56
  "jsdom": "^27.0.1",
40
57
  "prettier": "^3.3.3",
41
58
  "typescript": "^5.6.3",
42
59
  "vitest": "^2.1.3"
43
60
  },
44
- "dependencies": {
45
- "dompurify": "^3.3.0",
46
- "esbuild": "^0.25.11",
47
- "express": "^5.1.0",
48
- "lit-html": "^3.3.1",
49
- "marked": "^16.4.1",
50
- "ws": "^8.18.3"
51
- },
52
61
  "files": [
53
- "app",
62
+ "app/index.html",
63
+ "app/styles.css",
64
+ "app/main.bundle.js",
65
+ "app/main.bundle.js.map",
54
66
  "bin",
55
- "docs",
56
67
  "server",
57
68
  "CHANGES.md",
58
69
  "LICENSE",
package/server/app.js CHANGED
@@ -6,6 +6,7 @@ import path from 'node:path';
6
6
 
7
7
  /**
8
8
  * Create and configure the Express application.
9
+ *
9
10
  * @param {{ host: string, port: number, env: string, app_dir: string, root_dir: string }} config - Server configuration.
10
11
  * @returns {Express} Configured Express app instance.
11
12
  */
@@ -25,43 +26,46 @@ export function createApp(config) {
25
26
  res.status(200).send({ ok: true });
26
27
  });
27
28
 
28
- /**
29
- * On-demand bundle for the browser using esbuild.
30
- * Note: esbuild is loaded lazily so tests don't require it to be installed.
31
- * @param {Request} _req
32
- * @param {Response} res
33
- */
34
- app.get('/main.bundle.js', async (_req, res) => {
35
- try {
36
- const esbuild = await import('esbuild');
37
- const entry = path.join(config.app_dir, 'main.js');
38
- const result = await esbuild.build({
39
- entryPoints: [entry],
40
- bundle: true,
41
- format: 'esm',
42
- platform: 'browser',
43
- target: 'es2020',
44
- sourcemap: config.env === 'production' ? false : 'inline',
45
- minify: config.env === 'production',
46
- write: false
47
- });
48
- const out = result.outputFiles && result.outputFiles[0];
49
- if (!out) {
50
- res.status(500).type('text/plain').send('Bundle failed: no output');
51
- return;
52
- }
53
- res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
54
- if (config.env !== 'production') {
29
+ // In development, support on-demand bundling for a smooth DX.
30
+ // In production, we expect `app/main.bundle.js` to be pre-built and served statically.
31
+ if (config.env !== 'production') {
32
+ /**
33
+ * On-demand bundle for the browser using esbuild.
34
+ * Note: esbuild is loaded lazily so tests don't require it to be installed.
35
+ *
36
+ * @param {Request} _req
37
+ * @param {Response} res
38
+ */
39
+ app.get('/main.bundle.js', async (_req, res) => {
40
+ try {
41
+ const esbuild = await import('esbuild');
42
+ const entry = path.join(config.app_dir, 'main.js');
43
+ const result = await esbuild.build({
44
+ entryPoints: [entry],
45
+ bundle: true,
46
+ format: 'esm',
47
+ platform: 'browser',
48
+ target: 'es2020',
49
+ sourcemap: 'inline',
50
+ minify: false,
51
+ write: false
52
+ });
53
+ const out = result.outputFiles && result.outputFiles[0];
54
+ if (!out) {
55
+ res.status(500).type('text/plain').send('Bundle failed: no output');
56
+ return;
57
+ }
58
+ res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
55
59
  res.setHeader('Cache-Control', 'no-store');
60
+ res.send(out.text);
61
+ } catch (err) {
62
+ res
63
+ .status(500)
64
+ .type('text/plain')
65
+ .send('Bundle error: ' + (err && /** @type {any} */ (err).message));
56
66
  }
57
- res.send(out.text);
58
- } catch (err) {
59
- res
60
- .status(500)
61
- .type('text/plain')
62
- .send('Bundle error: ' + (err && /** @type {any} */ (err).message));
63
- }
64
- });
67
+ });
68
+ }
65
69
 
66
70
  // Static assets from /app
67
71
  app.use(express.static(config.app_dir));
package/server/bd.js CHANGED
@@ -3,6 +3,7 @@ import { resolveDbPath } from './db.js';
3
3
 
4
4
  /**
5
5
  * Resolve the bd executable path.
6
+ *
6
7
  * @returns {string}
7
8
  */
8
9
  export function getBdBin() {
@@ -16,6 +17,7 @@ export function getBdBin() {
16
17
  /**
17
18
  * Run the `bd` CLI with provided arguments.
18
19
  * Shell is not used to avoid injection; args must be pre-split.
20
+ *
19
21
  * @param {string[]} args - Arguments to pass (e.g., ["list", "--json"]).
20
22
  * @param {{ cwd?: string, env?: Record<string, string | undefined>, timeout_ms?: number }} [options]
21
23
  * @returns {Promise<{ code: number, stdout: string, stderr: string }>}
@@ -30,10 +32,10 @@ export function runBd(args, options = {}) {
30
32
 
31
33
  // Ensure a consistent DB by injecting --db if missing, following beads precedence.
32
34
  /** @type {string[]} */
33
- const finalArgs = withDbArg(args, spawn_opts.cwd, spawn_opts.env);
35
+ const final_args = withDbArg(args, spawn_opts.cwd, spawn_opts.env);
34
36
 
35
37
  return new Promise((resolve) => {
36
- const child = spawn(bin, finalArgs, spawn_opts);
38
+ const child = spawn(bin, final_args, spawn_opts);
37
39
 
38
40
  /** @type {string[]} */
39
41
  const out_chunks = [];
@@ -88,6 +90,7 @@ export function runBd(args, options = {}) {
88
90
 
89
91
  /**
90
92
  * Run `bd` and parse JSON from stdout if exit code is 0.
93
+ *
91
94
  * @param {string[]} args - Must include flags that cause JSON to be printed (e.g., `--json`).
92
95
  * @param {{ cwd?: string, env?: Record<string, string | undefined>, timeout_ms?: number }} [options]
93
96
  * @returns {Promise<{ code: number, stdoutJson?: unknown, stderr?: string }>}
@@ -109,6 +112,7 @@ export async function runBdJson(args, options = {}) {
109
112
 
110
113
  /**
111
114
  * Add a resolved "--db <path>" pair to args when none present.
115
+ *
112
116
  * @param {string[]} args
113
117
  * @param {string} cwd
114
118
  * @param {Record<string, string | undefined>} env
@@ -13,17 +13,18 @@ import { openUrl, waitForServer } from './open.js';
13
13
  * Handle `start` command. Idempotent when already running.
14
14
  * - Spawns a detached server process, writes PID file, returns 0.
15
15
  * - If already running (PID file present and process alive), prints URL and returns 0.
16
+ *
16
17
  * @returns {Promise<number>} Exit code (0 on success)
17
18
  */
18
19
  /**
19
- * @param {{ no_open?: boolean }} [options]
20
+ * @param {{ open?: boolean, is_debug?: boolean }} [options]
20
21
  */
21
22
  export async function handleStart(options) {
22
- // Default behavior: do not open a browser unless explicitly requested.
23
- const no_open = options?.no_open !== false;
23
+ // Default: do not open a browser unless explicitly requested via `open: true`.
24
+ const should_open = options?.open === true;
24
25
  const existing_pid = readPidFile();
25
26
  if (existing_pid && isProcessRunning(existing_pid)) {
26
- printServerUrl();
27
+ console.warn('Server is already running.');
27
28
  return 0;
28
29
  }
29
30
  if (existing_pid && !isProcessRunning(existing_pid)) {
@@ -31,11 +32,11 @@ export async function handleStart(options) {
31
32
  removePidFile();
32
33
  }
33
34
 
34
- const started = startDaemon();
35
+ const started = startDaemon({ is_debug: options?.is_debug });
35
36
  if (started && started.pid > 0) {
36
37
  printServerUrl();
37
38
  // Auto-open the browser once for a fresh daemon start
38
- if (!no_open) {
39
+ if (should_open) {
39
40
  const { url } = getConfig();
40
41
  // Wait briefly for the server to accept connections (single retry window)
41
42
  await waitForServer(url, 600);
@@ -52,6 +53,7 @@ export async function handleStart(options) {
52
53
  * Handle `stop` command.
53
54
  * - Sends SIGTERM and waits for exit (with SIGKILL fallback), removes PID file.
54
55
  * - Returns 2 if not running.
56
+ *
55
57
  * @returns {Promise<number>} Exit code
56
58
  */
57
59
  export async function handleStop() {
@@ -78,13 +80,15 @@ export async function handleStop() {
78
80
 
79
81
  /**
80
82
  * Handle `restart` command: stop (ignore not-running) then start.
83
+ *
81
84
  * @returns {Promise<number>} Exit code (0 on success)
82
85
  */
83
86
  /**
84
87
  * Handle `restart` command: stop (ignore not-running) then start.
85
88
  * Accepts the same options as `handleStart` and passes them through,
86
- * so restart only opens a browser when `no_open` is explicitly false.
87
- * @param {{ no_open?: boolean }} [options]
89
+ * so restart only opens a browser when `open` is explicitly true.
90
+ *
91
+ * @param {{ open?: boolean }} [options]
88
92
  * @returns {Promise<number>}
89
93
  */
90
94
  export async function handleRestart(options) {
@@ -13,6 +13,7 @@ import { resolveDbPath } from '../db.js';
13
13
  * Resolve the runtime directory used for PID and log files.
14
14
  * Prefers `BDUI_RUNTIME_DIR`, then `$XDG_RUNTIME_DIR/beads-ui`,
15
15
  * and finally `os.tmpdir()/beads-ui`.
16
+ *
16
17
  * @returns {string}
17
18
  */
18
19
  export function getRuntimeDir() {
@@ -31,6 +32,7 @@ export function getRuntimeDir() {
31
32
 
32
33
  /**
33
34
  * Ensure a directory exists with safe permissions and return its path.
35
+ *
34
36
  * @param {string} dir_path
35
37
  * @returns {string}
36
38
  */
@@ -61,6 +63,7 @@ export function getLogFilePath() {
61
63
 
62
64
  /**
63
65
  * Read PID from the PID file if present.
66
+ *
64
67
  * @returns {number | null}
65
68
  */
66
69
  export function readPidFile() {
@@ -100,6 +103,7 @@ export function removePidFile() {
100
103
 
101
104
  /**
102
105
  * Check whether a process is running.
106
+ *
103
107
  * @param {number} pid
104
108
  * @returns {boolean}
105
109
  */
@@ -122,6 +126,7 @@ export function isProcessRunning(pid) {
122
126
 
123
127
  /**
124
128
  * Compute the absolute path to the server entry file.
129
+ *
125
130
  * @returns {string}
126
131
  */
127
132
  export function getServerEntryPath() {
@@ -134,9 +139,11 @@ export function getServerEntryPath() {
134
139
  /**
135
140
  * Spawn the server as a detached daemon, redirecting stdio to the log file.
136
141
  * Writes the PID file upon success.
142
+ *
143
+ * @param {{ is_debug?: boolean }} [options]
137
144
  * @returns {{ pid: number } | null} Returns child PID on success; null on failure.
138
145
  */
139
- export function startDaemon() {
146
+ export function startDaemon(options = {}) {
140
147
  const server_entry = getServerEntryPath();
141
148
  const log_file = getLogFilePath();
142
149
 
@@ -145,6 +152,9 @@ export function startDaemon() {
145
152
  let log_fd;
146
153
  try {
147
154
  log_fd = fs.openSync(log_file, 'a');
155
+ if (options.is_debug) {
156
+ console.debug('log file ', log_file);
157
+ }
148
158
  } catch {
149
159
  // If log cannot be opened, fallback to ignoring stdio
150
160
  log_fd = -1;
@@ -164,11 +174,15 @@ export function startDaemon() {
164
174
  child.unref();
165
175
  const child_pid = typeof child.pid === 'number' ? child.pid : -1;
166
176
  if (child_pid > 0) {
177
+ if (options.is_debug) {
178
+ console.debug('starting ', child_pid);
179
+ }
167
180
  writePidFile(child_pid);
168
181
  return { pid: child_pid };
169
182
  }
170
183
  return null;
171
184
  } catch (err) {
185
+ console.error('start error', err);
172
186
  // Log startup error to log file for traceability
173
187
  try {
174
188
  const message =
@@ -183,6 +197,7 @@ export function startDaemon() {
183
197
 
184
198
  /**
185
199
  * Send SIGTERM then (optionally) SIGKILL to stop a process and wait for exit.
200
+ *
186
201
  * @param {number} pid
187
202
  * @param {number} timeout_ms
188
203
  * @returns {Promise<boolean>} Resolves true if the process is gone.
@@ -235,12 +250,12 @@ function sleep(ms) {
235
250
  * Print the server URL derived from current config.
236
251
  */
237
252
  export function printServerUrl() {
238
- const { url } = getConfig();
239
- console.log(url);
240
-
241
253
  // Resolve from the caller's working directory by default
242
254
  const resolved_db = resolveDbPath();
243
255
  console.log(
244
- `db: ${resolved_db.path} (${resolved_db.source}${resolved_db.exists ? '' : ', missing'})`
256
+ `beads db ${resolved_db.path} (${resolved_db.source}${resolved_db.exists ? '' : ', missing'})`
245
257
  );
258
+
259
+ const { url, env } = getConfig();
260
+ console.log(`beads ui listening on ${url} (${env})`);
246
261
  }
@@ -1,8 +1,10 @@
1
+ import { enableAllDebug } from '../logging.js';
1
2
  import { handleRestart, handleStart, handleStop } from './commands.js';
2
3
  import { printUsage } from './usage.js';
3
4
 
4
5
  /**
5
6
  * Parse argv into a command token and flags.
7
+ *
6
8
  * @param {string[]} args
7
9
  * @returns {{ command: string | null, flags: string[] }}
8
10
  */
@@ -17,12 +19,12 @@ export function parseArgs(args) {
17
19
  flags.push('help');
18
20
  continue;
19
21
  }
20
- if (token === '--open') {
21
- flags.push('open');
22
+ if (token === '--debug' || token === '-d') {
23
+ flags.push('debug');
22
24
  continue;
23
25
  }
24
- if (token === '--no-open') {
25
- flags.push('no-open');
26
+ if (token === '--open') {
27
+ flags.push('open');
26
28
  continue;
27
29
  }
28
30
  if (
@@ -41,12 +43,18 @@ export function parseArgs(args) {
41
43
  /**
42
44
  * CLI main entry. Returns an exit code and prints usage on `--help` or errors.
43
45
  * No side effects beyond invoking stub handlers.
46
+ *
44
47
  * @param {string[]} args
45
48
  * @returns {Promise<number>}
46
49
  */
47
50
  export async function main(args) {
48
51
  const { command, flags } = parseArgs(args);
49
52
 
53
+ const is_debug = flags.includes('debug');
54
+ if (is_debug) {
55
+ enableAllDebug();
56
+ }
57
+
50
58
  if (flags.includes('help')) {
51
59
  printUsage(process.stdout);
52
60
  return 0;
@@ -58,42 +66,22 @@ export async function main(args) {
58
66
 
59
67
  if (command === 'start') {
60
68
  /**
61
- * Default behavior: do NOT open a browser.
62
- * `--open` explicitly opens, overriding env/config; `--no-open` forces closed.
69
+ * Default behavior: do NOT open a browser. `--open` explicitly opens.
63
70
  */
64
71
  const options = {
65
- no_open: true
72
+ open: flags.includes('open'),
73
+ is_debug: is_debug || Boolean(process.env.DEBUG)
66
74
  };
67
-
68
- const has_open = flags.includes('open');
69
- const has_no_open = flags.includes('no-open');
70
- const env_no_open = String(process.env.BDUI_NO_OPEN || '') === '1';
71
-
72
- if (has_open) {
73
- options.no_open = false;
74
- } else if (has_no_open) {
75
- options.no_open = true;
76
- } else if (env_no_open) {
77
- options.no_open = true;
78
- }
79
75
  return await handleStart(options);
80
76
  }
81
77
  if (command === 'stop') {
82
78
  return await handleStop();
83
79
  }
84
80
  if (command === 'restart') {
85
- const options = { no_open: true };
86
- const has_open = flags.includes('open');
87
- const has_no_open = flags.includes('no-open');
88
- const env_no_open = String(process.env.BDUI_NO_OPEN || '') === '1';
89
-
90
- if (has_open) {
91
- options.no_open = false;
92
- } else if (has_no_open) {
93
- options.no_open = true;
94
- } else if (env_no_open) {
95
- options.no_open = true;
96
- }
81
+ const options = {
82
+ open: flags.includes('open'),
83
+ is_debug: is_debug || Boolean(process.env.DEBUG)
84
+ };
97
85
  return await handleRestart(options);
98
86
  }
99
87
 
@@ -3,6 +3,7 @@ import http from 'node:http';
3
3
 
4
4
  /**
5
5
  * Compute a platform-specific command to open a URL in the default browser.
6
+ *
6
7
  * @param {string} url
7
8
  * @param {string} platform
8
9
  * @returns {{ cmd: string, args: string[] }}
@@ -21,6 +22,7 @@ export function computeOpenCommand(url, platform) {
21
22
 
22
23
  /**
23
24
  * Open the given URL in the default browser. Best-effort; resolves true on spawn success.
25
+ *
24
26
  * @param {string} url
25
27
  * @returns {Promise<boolean>}
26
28
  */
@@ -41,6 +43,7 @@ export async function openUrl(url) {
41
43
  /**
42
44
  * Wait until the server at the URL accepts a connection, with a brief retry.
43
45
  * Does not throw; returns when either a connection was accepted or timeout elapsed.
46
+ *
44
47
  * @param {string} url
45
48
  * @param {number} total_timeout_ms
46
49
  * @returns {Promise<void>}
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Print CLI usage to a stream-like target.
3
+ *
3
4
  * @param {{ write: (chunk: string) => any }} out_stream
4
5
  */
5
6
  export function printUsage(out_stream) {
@@ -12,8 +13,9 @@ export function printUsage(out_stream) {
12
13
  ' restart Restart the UI server',
13
14
  '',
14
15
  'Options:',
15
- ' -h, --help Show this help message',
16
- ' --open Open the browser after start/restart',
16
+ ' -h, --help Show this help message',
17
+ ' -d, --debug Enable debug logging',
18
+ ' --open Open the browser after start/restart',
17
19
  ''
18
20
  ];
19
21
  for (const line of lines) {
package/server/config.js CHANGED
@@ -6,8 +6,9 @@ import { fileURLToPath } from 'node:url';
6
6
  * Notes:
7
7
  * - `app_dir` is resolved relative to the installed package location.
8
8
  * - `root_dir` represents the directory where the process was invoked
9
- * (i.e., the current working directory) so DB resolution follows the
10
- * caller's context rather than the install location.
9
+ * (i.e., the current working directory) so DB resolution follows the
10
+ * caller's context rather than the install location.
11
+ *
11
12
  * @returns {{ host: string, port: number, env: string, app_dir: string, root_dir: string, url: string }}
12
13
  */
13
14
  export function getConfig() {
package/server/db.js CHANGED
@@ -11,6 +11,7 @@ import path from 'node:path';
11
11
  *
12
12
  * Returns a normalized absolute path and a `source` indicator. Existence is
13
13
  * returned via the `exists` boolean.
14
+ *
14
15
  * @param {{ cwd?: string, env?: Record<string, string | undefined>, explicit_db?: string }} [options]
15
16
  * @returns {{ path: string, source: 'flag'|'env'|'nearest'|'home-default', exists: boolean }}
16
17
  */
@@ -37,17 +38,18 @@ export function resolveDbPath(options = {}) {
37
38
  }
38
39
 
39
40
  // 4) ~/.beads/default.db
40
- const homeDefault = path.join(os.homedir(), '.beads', 'default.db');
41
+ const home_default = path.join(os.homedir(), '.beads', 'default.db');
41
42
  return {
42
- path: homeDefault,
43
+ path: home_default,
43
44
  source: 'home-default',
44
- exists: fileExists(homeDefault)
45
+ exists: fileExists(home_default)
45
46
  };
46
47
  }
47
48
 
48
49
  /**
49
50
  * Find nearest .beads/*.db by walking up from start.
50
51
  * First alphabetical .db.
52
+ *
51
53
  * @param {string} start
52
54
  * @returns {string | null}
53
55
  */
@@ -55,15 +57,15 @@ export function findNearestBeadsDb(start) {
55
57
  let dir = path.resolve(start);
56
58
  // Cap iterations to avoid infinite loop in degenerate cases
57
59
  for (let i = 0; i < 100; i++) {
58
- const beadsDir = path.join(dir, '.beads');
60
+ const beads_dir = path.join(dir, '.beads');
59
61
  try {
60
- const entries = fs.readdirSync(beadsDir, { withFileTypes: true });
62
+ const entries = fs.readdirSync(beads_dir, { withFileTypes: true });
61
63
  const dbs = entries
62
64
  .filter((e) => e.isFile() && e.name.endsWith('.db'))
63
65
  .map((e) => e.name)
64
66
  .sort();
65
67
  if (dbs.length > 0) {
66
- return path.join(beadsDir, dbs[0]);
68
+ return path.join(beads_dir, dbs[0]);
67
69
  }
68
70
  } catch {
69
71
  // ignore and walk up
@@ -79,6 +81,7 @@ export function findNearestBeadsDb(start) {
79
81
 
80
82
  /**
81
83
  * Resolve possibly relative `p` against `cwd` to an absolute filesystem path.
84
+ *
82
85
  * @param {string} p
83
86
  * @param {string} cwd
84
87
  */
package/server/index.js CHANGED
@@ -1,12 +1,19 @@
1
1
  import { createServer } from 'node:http';
2
2
  import { createApp } from './app.js';
3
+ import { printServerUrl } from './cli/daemon.js';
3
4
  import { getConfig } from './config.js';
5
+ import { debug, enableAllDebug } from './logging.js';
4
6
  import { watchDb } from './watcher.js';
5
7
  import { attachWsServer } from './ws.js';
6
8
 
9
+ if (process.argv.includes('--debug') || process.argv.includes('-d')) {
10
+ enableAllDebug();
11
+ }
12
+
7
13
  const config = getConfig();
8
14
  const app = createApp(config);
9
15
  const server = createServer(app);
16
+ const log = debug('server');
10
17
  const { scheduleListRefresh } = attachWsServer(server, {
11
18
  path: '/ws',
12
19
  heartbeat_ms: 30000,
@@ -17,17 +24,16 @@ const { scheduleListRefresh } = attachWsServer(server, {
17
24
  // Watch the active beads DB and schedule subscription refresh for active lists
18
25
  watchDb(config.root_dir, () => {
19
26
  // Schedule subscription list refresh run for active subscriptions
27
+ log('db change detected → schedule refresh');
20
28
  scheduleListRefresh();
21
29
  // v2: all updates flow via subscription push envelopes only
22
30
  });
23
31
 
24
32
  server.listen(config.port, config.host, () => {
25
- console.log(
26
- `beads-ui server listening on http://${config.host}:${config.port} (${config.env})`
27
- );
33
+ printServerUrl();
28
34
  });
29
35
 
30
36
  server.on('error', (err) => {
31
- console.error('Server error:', err);
37
+ log('server error %o', err);
32
38
  process.exitCode = 1;
33
39
  });