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.
- package/CHANGES.md +26 -0
- package/README.md +15 -6
- package/app/main.bundle.js +617 -0
- package/app/main.bundle.js.map +7 -0
- package/bin/bdui.js +2 -1
- package/package.json +27 -16
- package/server/app.js +39 -35
- package/server/bd.js +6 -2
- package/server/cli/commands.js +12 -8
- package/server/cli/daemon.js +20 -5
- package/server/cli/index.js +19 -31
- package/server/cli/open.js +3 -0
- package/server/cli/usage.js +4 -2
- package/server/config.js +3 -2
- package/server/db.js +9 -6
- package/server/index.js +10 -4
- package/server/list-adapters.js +9 -3
- package/server/logging.js +23 -0
- package/server/subscriptions.js +12 -0
- package/server/validators.js +2 -0
- package/server/watcher.js +10 -5
- package/server/ws.js +31 -10
- package/app/data/list-selectors.js +0 -98
- package/app/data/providers.js +0 -76
- package/app/data/sort.js +0 -45
- package/app/data/subscription-issue-store.js +0 -161
- package/app/data/subscription-issue-stores.js +0 -102
- package/app/data/subscriptions-store.js +0 -219
- package/app/main.js +0 -702
- package/app/protocol.js +0 -196
- package/app/protocol.md +0 -66
- package/app/router.js +0 -114
- package/app/state.js +0 -103
- package/app/utils/issue-id-renderer.js +0 -71
- package/app/utils/issue-id.js +0 -10
- package/app/utils/issue-type.js +0 -27
- package/app/utils/issue-url.js +0 -9
- package/app/utils/markdown.js +0 -22
- package/app/utils/priority-badge.js +0 -47
- package/app/utils/priority.js +0 -1
- package/app/utils/status-badge.js +0 -32
- package/app/utils/status.js +0 -23
- package/app/utils/toast.js +0 -34
- package/app/utils/type-badge.js +0 -33
- package/app/views/board.js +0 -535
- package/app/views/detail.js +0 -1249
- package/app/views/epics.js +0 -280
- package/app/views/issue-dialog.js +0 -163
- package/app/views/issue-row.js +0 -190
- package/app/views/list.js +0 -464
- package/app/views/nav.js +0 -67
- package/app/views/new-issue-dialog.js +0 -345
- package/app/ws.js +0 -279
- package/docs/adr/001-push-only-lists.md +0 -134
- package/docs/adr/002-per-subscription-stores-and-full-issue-push.md +0 -200
- package/docs/architecture.md +0 -194
- package/docs/data-exchange-subscription-plan.md +0 -198
- package/docs/db-watching.md +0 -30
- package/docs/migration-v2.md +0 -54
- package/docs/protocol/issues-push-v2.md +0 -179
- 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
|
-
|
|
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.
|
|
4
|
-
"description": "Local
|
|
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": "^
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
|
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,
|
|
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
|
package/server/cli/commands.js
CHANGED
|
@@ -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 {{
|
|
20
|
+
* @param {{ open?: boolean, is_debug?: boolean }} [options]
|
|
20
21
|
*/
|
|
21
22
|
export async function handleStart(options) {
|
|
22
|
-
// Default
|
|
23
|
-
const
|
|
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
|
-
|
|
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 (
|
|
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 `
|
|
87
|
-
*
|
|
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) {
|
package/server/cli/daemon.js
CHANGED
|
@@ -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
|
|
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
|
}
|
package/server/cli/index.js
CHANGED
|
@@ -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 === '--
|
|
21
|
-
flags.push('
|
|
22
|
+
if (token === '--debug' || token === '-d') {
|
|
23
|
+
flags.push('debug');
|
|
22
24
|
continue;
|
|
23
25
|
}
|
|
24
|
-
if (token === '--
|
|
25
|
-
flags.push('
|
|
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
|
-
|
|
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 = {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
package/server/cli/open.js
CHANGED
|
@@ -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>}
|
package/server/cli/usage.js
CHANGED
|
@@ -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
|
|
16
|
-
'
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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
|
|
41
|
+
const home_default = path.join(os.homedir(), '.beads', 'default.db');
|
|
41
42
|
return {
|
|
42
|
-
path:
|
|
43
|
+
path: home_default,
|
|
43
44
|
source: 'home-default',
|
|
44
|
-
exists: fileExists(
|
|
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
|
|
60
|
+
const beads_dir = path.join(dir, '.beads');
|
|
59
61
|
try {
|
|
60
|
-
const entries = fs.readdirSync(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
37
|
+
log('server error %o', err);
|
|
32
38
|
process.exitCode = 1;
|
|
33
39
|
});
|