@citadel-labs/beads-ui 2.4.0 → 2.5.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 +6 -3
- package/bin/beads-board.js +173 -183
- package/package.json +2 -2
- package/server/__tests__/api.test.js +241 -2
- package/server/__tests__/pidfile.test.js +99 -0
- package/server/__tests__/terminal.test.js +339 -0
- package/server/dist/assets/index-Dm1YZe0A.css +1 -0
- package/server/dist/assets/index-G6bcoKqz.js +232 -0
- package/server/dist/index.html +2 -2
- package/server/handlers.js +167 -57
- package/server/index.js +5 -26
- package/server/pidfile.js +71 -0
- package/server/terminal-sessions.js +149 -0
- package/server/terminal.js +132 -31
- package/terminal-session-01-initial.png +0 -0
- package/terminal-session-02-before-refresh.png +0 -0
- package/terminal-session-03-after-refresh.png +0 -0
- package/.claude/worktrees/agent-a7b97047/LICENSE +0 -21
- package/.claude/worktrees/agent-a7b97047/README.md +0 -63
- package/.claude/worktrees/agent-a7b97047/bin/beads-board.js +0 -183
- package/.claude/worktrees/agent-a7b97047/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- package/.claude/worktrees/agent-a7b97047/package-lock.json +0 -1752
- package/.claude/worktrees/agent-a7b97047/package.json +0 -43
- package/.claude/worktrees/agent-a7b97047/server/__tests__/api.test.js +0 -206
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/angular-html-DA-rfuFy.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/angular-ts-BrjP3tb8.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/c-BIGW1oBm.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/cpp-BRuaLJcg.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/csharp-COcwbKMJ.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/css-CLj8gQPS.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/dockerfile-BcOcwvcX.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/dotenv-Da5cRb03.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/github-dark-DHJKELXO.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/go-C27-OAKa.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/graphql-ChdNCCLP.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/html-derivative-C6UeqQa8.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/html-pp8916En.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/http-l_GQhCeT.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/index-BBBYE21N.css +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/index-cZFE6wf9.js +0 -231
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/ini-BEwlwnbL.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/java-CylS5w8V.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/javascript-wDzz0qaB.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/json-Cp-IABpG.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/jsonc-Des-eS-w.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/jsonl-DcaNXYhu.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/jsx-g9-lgVsj.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/kotlin-BdnUsdx6.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/kusto-wEQ09or8.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/latex-DdMFrP5M.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/markdown-Cvjx9yec.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/mdc-Dz5ISc6g.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/mdx-Cmh6b_Ma.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/mermaid-mWjccvbQ.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/php-R6g_5hLQ.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/powershell-Dpen1YoG.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/python-B6aJPvgy.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/ruby-AcS3PBV-.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/rust-B1yitclQ.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/sass-Cj5Yp3dK.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/scss-D5BDwBP9.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/shellscript-DfDnw5Jg.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/sql-BLtJtn59.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/svelte-DR4MIrkg.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/swift-D82vCrfD.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/toml-vGWfd6FD.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/tsx-COt5Ahok.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/typescript-BPQ3VLAy.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/vue-CJgBXYWu.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/xml-sdJ4AIDG.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/yaml-Buea-lGh.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/assets/zig-VOosw3JB.js +0 -1
- package/.claude/worktrees/agent-a7b97047/server/dist/favicon.svg +0 -103
- package/.claude/worktrees/agent-a7b97047/server/dist/index.html +0 -14
- package/.claude/worktrees/agent-a7b97047/server/dist/vite.svg +0 -1
- package/.claude/worktrees/agent-a7b97047/server/handlers.js +0 -357
- package/.claude/worktrees/agent-a7b97047/server/index.js +0 -88
- package/.claude/worktrees/agent-a7b97047/server/terminal.js +0 -103
- package/.claude/worktrees/agent-a7b97047/vitest.config.mjs +0 -8
- package/.claude/worktrees/agent-a952bde8/LICENSE +0 -21
- package/.claude/worktrees/agent-a952bde8/README.md +0 -63
- package/.claude/worktrees/agent-a952bde8/bin/beads-board.js +0 -183
- package/.claude/worktrees/agent-a952bde8/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- package/.claude/worktrees/agent-a952bde8/package-lock.json +0 -1752
- package/.claude/worktrees/agent-a952bde8/package.json +0 -43
- package/.claude/worktrees/agent-a952bde8/server/__tests__/api.test.js +0 -122
- package/.claude/worktrees/agent-a952bde8/server/dist/assets/index-BsWRmNbj.js +0 -79
- package/.claude/worktrees/agent-a952bde8/server/dist/assets/index-C7JKZkTD.css +0 -1
- package/.claude/worktrees/agent-a952bde8/server/dist/favicon.svg +0 -103
- package/.claude/worktrees/agent-a952bde8/server/dist/index.html +0 -14
- package/.claude/worktrees/agent-a952bde8/server/dist/vite.svg +0 -1
- package/.claude/worktrees/agent-a952bde8/server/handlers.js +0 -269
- package/.claude/worktrees/agent-a952bde8/server/index.js +0 -88
- package/.claude/worktrees/agent-a952bde8/server/terminal.js +0 -103
- package/.claude/worktrees/agent-a952bde8/vitest.config.mjs +0 -8
- package/server/dist/assets/index-DOFQi_E1.js +0 -231
- package/server/dist/assets/index-DeppoR8O.css +0 -1
package/README.md
CHANGED
|
@@ -5,12 +5,15 @@ A minimal kanban dashboard and git log viewer for [Beads](https://github.com/ste
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- **Kanban board** — Issues organized by status: Ready, In Progress, Blocked, Done
|
|
8
|
-
- **Git log** — Scrollable commit history with branch selector
|
|
9
|
-
- **
|
|
8
|
+
- **Git log** — Scrollable commit history with branch selector and diff viewer
|
|
9
|
+
- **File explorer** — Browse project files with syntax-highlighted file viewer (40+ languages via Shiki)
|
|
10
|
+
- **Dependency graph** — Interactive DAG visualization with hover highlighting, zoom, and pan
|
|
11
|
+
- **Bead ID linking** — Bead IDs in commit messages are highlighted as clickable badges
|
|
12
|
+
- **Search and filtering** — Filter issues by priority, type, assignee, or free-text search
|
|
10
13
|
- **Dark/light theme** — Toggle between themes, dark by default
|
|
11
14
|
- **Auto-refresh** — Polls for updates every 5 seconds
|
|
12
15
|
- **Integrated terminal** — Built-in terminal panel powered by node-pty and xterm.js
|
|
13
|
-
- **
|
|
16
|
+
- **Settings** — Configurable terminal font-family via settings modal
|
|
14
17
|
|
|
15
18
|
## Quick Start
|
|
16
19
|
|
package/bin/beads-board.js
CHANGED
|
@@ -1,183 +1,173 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const path = require('node:path');
|
|
4
|
-
const fs = require('node:fs');
|
|
5
|
-
const { spawn } = require('node:child_process');
|
|
6
|
-
|
|
7
|
-
const SERVER_SCRIPT = path.join(__dirname, '..', 'server', 'index.js');
|
|
8
|
-
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
// Argument parsing
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
|
|
13
|
-
const rawArgs = process.argv.slice(2);
|
|
14
|
-
|
|
15
|
-
// Extract subcommand (start, stop, status) — default to "start"
|
|
16
|
-
const SUBCOMMANDS = ['start', 'stop', 'status'];
|
|
17
|
-
let subcommand = 'start';
|
|
18
|
-
const args = [];
|
|
19
|
-
for (const arg of rawArgs) {
|
|
20
|
-
if (SUBCOMMANDS.includes(arg) && args.length === 0 && subcommand === 'start') {
|
|
21
|
-
subcommand = arg;
|
|
22
|
-
} else {
|
|
23
|
-
args.push(arg);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// --help
|
|
28
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
29
|
-
console.log(`bdui — Kanban dashboard and git log viewer for Beads
|
|
30
|
-
|
|
31
|
-
Usage:
|
|
32
|
-
bdui [project-dir] [options] Start the dashboard (default)
|
|
33
|
-
bdui start [project-dir] [options] Start the dashboard
|
|
34
|
-
bdui stop [project-dir] Stop a running dashboard
|
|
35
|
-
bdui status [project-dir] Show running dashboard info
|
|
36
|
-
|
|
37
|
-
Options:
|
|
38
|
-
--port <port> Port to listen on (default: 8377)
|
|
39
|
-
--foreground Run in foreground (don't daemonize)
|
|
40
|
-
--help, -h Show this help message
|
|
41
|
-
--version, -v Show version number
|
|
42
|
-
|
|
43
|
-
Examples:
|
|
44
|
-
bdui # Start dashboard for current directory
|
|
45
|
-
bdui /path/to/project # Specify project directory
|
|
46
|
-
bdui --port 9000 # Custom port
|
|
47
|
-
bdui stop # Stop the running dashboard
|
|
48
|
-
bdui status # Check if dashboard is running`);
|
|
49
|
-
process.exit(0);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// --version
|
|
53
|
-
if (args.includes('--version') || args.includes('-v')) {
|
|
54
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
55
|
-
console.log(pkg.version);
|
|
56
|
-
process.exit(0);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// --foreground
|
|
60
|
-
const foreground = args.includes('--foreground');
|
|
61
|
-
const filteredArgs = args.filter(a => a !== '--foreground');
|
|
62
|
-
|
|
63
|
-
// --port
|
|
64
|
-
let customPort = null;
|
|
65
|
-
const portIdx = filteredArgs.indexOf('--port');
|
|
66
|
-
if (portIdx !== -1) {
|
|
67
|
-
customPort = filteredArgs[portIdx + 1];
|
|
68
|
-
if (!customPort || isNaN(parseInt(customPort, 10))) {
|
|
69
|
-
console.error('Error: --port requires a numeric value');
|
|
70
|
-
process.exit(1);
|
|
71
|
-
}
|
|
72
|
-
filteredArgs.splice(portIdx, 2);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Remaining arg is the project directory
|
|
76
|
-
const projectDir = filteredArgs[0] ? path.resolve(filteredArgs[0]) : process.cwd();
|
|
77
|
-
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
// Validation
|
|
80
|
-
// ---------------------------------------------------------------------------
|
|
81
|
-
|
|
82
|
-
if (!fs.existsSync(projectDir)) {
|
|
83
|
-
console.error(`Error: directory not found: ${projectDir}`);
|
|
84
|
-
process.exit(1);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (!fs.existsSync(path.join(projectDir, '.beads'))) {
|
|
88
|
-
console.error(`Error: no .beads/ directory found in ${projectDir}`);
|
|
89
|
-
console.error('This project does not appear to use Beads issue tracking.');
|
|
90
|
-
console.error('See https://github.com/steveyegge/beads to get started.');
|
|
91
|
-
process.exit(1);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// ---------------------------------------------------------------------------
|
|
95
|
-
// Pidfile helpers
|
|
96
|
-
// ---------------------------------------------------------------------------
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
function getRunningInstance() {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
process.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
console.log(`beads-board running at http://localhost:${instance.port}`);
|
|
175
|
-
process.exit(0);
|
|
176
|
-
}
|
|
177
|
-
if (Date.now() - start > 3000) {
|
|
178
|
-
clearInterval(poll);
|
|
179
|
-
console.error('Error: server failed to start within 3 seconds');
|
|
180
|
-
process.exit(1);
|
|
181
|
-
}
|
|
182
|
-
}, 100);
|
|
183
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const { spawn } = require('node:child_process');
|
|
6
|
+
|
|
7
|
+
const SERVER_SCRIPT = path.join(__dirname, '..', 'server', 'index.js');
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Argument parsing
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
const rawArgs = process.argv.slice(2);
|
|
14
|
+
|
|
15
|
+
// Extract subcommand (start, stop, status) — default to "start"
|
|
16
|
+
const SUBCOMMANDS = ['start', 'stop', 'status'];
|
|
17
|
+
let subcommand = 'start';
|
|
18
|
+
const args = [];
|
|
19
|
+
for (const arg of rawArgs) {
|
|
20
|
+
if (SUBCOMMANDS.includes(arg) && args.length === 0 && subcommand === 'start') {
|
|
21
|
+
subcommand = arg;
|
|
22
|
+
} else {
|
|
23
|
+
args.push(arg);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// --help
|
|
28
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
29
|
+
console.log(`bdui — Kanban dashboard and git log viewer for Beads
|
|
30
|
+
|
|
31
|
+
Usage:
|
|
32
|
+
bdui [project-dir] [options] Start the dashboard (default)
|
|
33
|
+
bdui start [project-dir] [options] Start the dashboard
|
|
34
|
+
bdui stop [project-dir] Stop a running dashboard
|
|
35
|
+
bdui status [project-dir] Show running dashboard info
|
|
36
|
+
|
|
37
|
+
Options:
|
|
38
|
+
--port <port> Port to listen on (default: 8377)
|
|
39
|
+
--foreground Run in foreground (don't daemonize)
|
|
40
|
+
--help, -h Show this help message
|
|
41
|
+
--version, -v Show version number
|
|
42
|
+
|
|
43
|
+
Examples:
|
|
44
|
+
bdui # Start dashboard for current directory
|
|
45
|
+
bdui /path/to/project # Specify project directory
|
|
46
|
+
bdui --port 9000 # Custom port
|
|
47
|
+
bdui stop # Stop the running dashboard
|
|
48
|
+
bdui status # Check if dashboard is running`);
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// --version
|
|
53
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
54
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
55
|
+
console.log(pkg.version);
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --foreground
|
|
60
|
+
const foreground = args.includes('--foreground');
|
|
61
|
+
const filteredArgs = args.filter(a => a !== '--foreground');
|
|
62
|
+
|
|
63
|
+
// --port
|
|
64
|
+
let customPort = null;
|
|
65
|
+
const portIdx = filteredArgs.indexOf('--port');
|
|
66
|
+
if (portIdx !== -1) {
|
|
67
|
+
customPort = filteredArgs[portIdx + 1];
|
|
68
|
+
if (!customPort || isNaN(parseInt(customPort, 10))) {
|
|
69
|
+
console.error('Error: --port requires a numeric value');
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
filteredArgs.splice(portIdx, 2);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Remaining arg is the project directory
|
|
76
|
+
const projectDir = filteredArgs[0] ? path.resolve(filteredArgs[0]) : process.cwd();
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Validation
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
if (!fs.existsSync(projectDir)) {
|
|
83
|
+
console.error(`Error: directory not found: ${projectDir}`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!fs.existsSync(path.join(projectDir, '.beads'))) {
|
|
88
|
+
console.error(`Error: no .beads/ directory found in ${projectDir}`);
|
|
89
|
+
console.error('This project does not appear to use Beads issue tracking.');
|
|
90
|
+
console.error('See https://github.com/steveyegge/beads to get started.');
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Pidfile helpers (shared module)
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
const { createPidfileManager } = require('../server/pidfile.js');
|
|
99
|
+
const pidfile = createPidfileManager(projectDir);
|
|
100
|
+
function getRunningInstance() { return pidfile.getRunningInstance(); }
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Subcommands
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
if (subcommand === 'status') {
|
|
107
|
+
const instance = getRunningInstance();
|
|
108
|
+
if (instance) {
|
|
109
|
+
console.log(`beads-board running at http://localhost:${instance.port} (pid ${instance.pid})`);
|
|
110
|
+
} else {
|
|
111
|
+
console.log('beads-board is not running');
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (subcommand === 'stop') {
|
|
118
|
+
const instance = getRunningInstance();
|
|
119
|
+
if (!instance) {
|
|
120
|
+
console.log('beads-board is not running');
|
|
121
|
+
process.exit(0);
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
process.kill(instance.pid, 'SIGTERM');
|
|
125
|
+
console.log(`beads-board stopped (was http://localhost:${instance.port}, pid ${instance.pid})`);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.error(`Failed to stop beads-board (pid ${instance.pid}): ${err.message}`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
process.exit(0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// subcommand === 'start'
|
|
134
|
+
const existing = getRunningInstance();
|
|
135
|
+
if (existing) {
|
|
136
|
+
console.log(`beads-board already running at http://localhost:${existing.port} (pid ${existing.pid})`);
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (foreground) {
|
|
141
|
+
// Run server in foreground (debugging)
|
|
142
|
+
if (customPort) process.env.PORT = customPort;
|
|
143
|
+
process.argv = [process.argv[0], __filename, projectDir];
|
|
144
|
+
require(SERVER_SCRIPT);
|
|
145
|
+
} else {
|
|
146
|
+
// Spawn detached server process
|
|
147
|
+
const env = { ...process.env };
|
|
148
|
+
if (customPort) env.PORT = customPort;
|
|
149
|
+
|
|
150
|
+
const child = spawn(process.execPath, [SERVER_SCRIPT, projectDir], {
|
|
151
|
+
detached: true,
|
|
152
|
+
stdio: 'ignore',
|
|
153
|
+
env,
|
|
154
|
+
windowsHide: true,
|
|
155
|
+
});
|
|
156
|
+
child.unref();
|
|
157
|
+
|
|
158
|
+
// Wait for pidfile to confirm startup (poll up to 3s)
|
|
159
|
+
const start = Date.now();
|
|
160
|
+
const poll = setInterval(() => {
|
|
161
|
+
const instance = getRunningInstance();
|
|
162
|
+
if (instance) {
|
|
163
|
+
clearInterval(poll);
|
|
164
|
+
console.log(`beads-board running at http://localhost:${instance.port}`);
|
|
165
|
+
process.exit(0);
|
|
166
|
+
}
|
|
167
|
+
if (Date.now() - start > 3000) {
|
|
168
|
+
clearInterval(poll);
|
|
169
|
+
console.error('Error: server failed to start within 3 seconds');
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
}, 100);
|
|
173
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@citadel-labs/beads-ui",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "Kanban dashboard and git log viewer for Beads",
|
|
5
5
|
"bin": {
|
|
6
6
|
"bdui": "bin/beads-board.js"
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"author": "Stuart Rimel <stuart.rimel@gmail.com>",
|
|
34
34
|
"license": "MIT",
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"node-pty": "
|
|
36
|
+
"node-pty": "1.2.0-beta.12",
|
|
37
37
|
"ws": "^8.19.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
@@ -55,6 +55,18 @@ async function get(urlPath) {
|
|
|
55
55
|
return { status: r.status, data, text };
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
async function patch(urlPath, body) {
|
|
59
|
+
const r = await fetch(`${baseUrl}${urlPath}`, {
|
|
60
|
+
method: 'PATCH',
|
|
61
|
+
headers: { 'Content-Type': 'application/json' },
|
|
62
|
+
body: JSON.stringify(body),
|
|
63
|
+
});
|
|
64
|
+
const text = await r.text();
|
|
65
|
+
let data = null;
|
|
66
|
+
try { data = JSON.parse(text); } catch {}
|
|
67
|
+
return { status: r.status, data, text };
|
|
68
|
+
}
|
|
69
|
+
|
|
58
70
|
describe('API endpoints', () => {
|
|
59
71
|
it('GET /api/issues returns JSON array', async () => {
|
|
60
72
|
mockExecFile((cmd, args) => {
|
|
@@ -251,11 +263,238 @@ describe('GET /api/files', () => {
|
|
|
251
263
|
expect(res.data).toHaveProperty('language', 'javascript');
|
|
252
264
|
});
|
|
253
265
|
|
|
254
|
-
it('excludes
|
|
266
|
+
it('excludes only .git but shows dotfiles, node_modules, and coverage', async () => {
|
|
255
267
|
const res = await get('/api/files');
|
|
256
268
|
expect(res.status).toBe(200);
|
|
257
269
|
const names = res.data.map(e => e.name);
|
|
258
270
|
expect(names).not.toContain('.git');
|
|
259
|
-
|
|
271
|
+
// Dotfiles, node_modules, coverage should all be visible
|
|
272
|
+
expect(names).toContain('.gitignore');
|
|
273
|
+
expect(names).toContain('node_modules');
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe('Branch-aware endpoints', () => {
|
|
278
|
+
describe('GET /api/git-status', () => {
|
|
279
|
+
it('without branch param calls git status --porcelain', async () => {
|
|
280
|
+
mockExecFile((cmd, args) => {
|
|
281
|
+
if (cmd === 'git') {
|
|
282
|
+
expect(args[0]).toBe('status');
|
|
283
|
+
expect(args).toContain('--porcelain');
|
|
284
|
+
return 'M README.md\n';
|
|
285
|
+
}
|
|
286
|
+
return '';
|
|
287
|
+
});
|
|
288
|
+
const res = await get('/api/git-status');
|
|
289
|
+
expect(res.status).toBe(200);
|
|
290
|
+
expect(res.data[0]).toHaveProperty('path', 'README.md');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('with branch param calls git diff --name-status <branch>', async () => {
|
|
294
|
+
mockExecFile((cmd, args) => {
|
|
295
|
+
if (cmd === 'git') {
|
|
296
|
+
expect(args[0]).toBe('diff');
|
|
297
|
+
expect(args).toContain('--name-status');
|
|
298
|
+
expect(args).toContain('feature-branch');
|
|
299
|
+
return 'M\tREADME.md\nA\tnew-file.js\n';
|
|
300
|
+
}
|
|
301
|
+
return '';
|
|
302
|
+
});
|
|
303
|
+
const res = await get('/api/git-status?branch=feature-branch');
|
|
304
|
+
expect(res.status).toBe(200);
|
|
305
|
+
expect(Array.isArray(res.data)).toBe(true);
|
|
306
|
+
expect(res.data[0]).toHaveProperty('status', 'M');
|
|
307
|
+
expect(res.data[0]).toHaveProperty('path', 'README.md');
|
|
308
|
+
expect(res.data[1]).toHaveProperty('status', 'A');
|
|
309
|
+
expect(res.data[1]).toHaveProperty('path', 'new-file.js');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('with branch param rejects invalid branch names', async () => {
|
|
313
|
+
const res = await get('/api/git-status?branch=bad;branch');
|
|
314
|
+
expect(res.status).toBe(400);
|
|
315
|
+
expect(res.data).toHaveProperty('error', 'Invalid branch name');
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe('GET /api/git-diff', () => {
|
|
320
|
+
it('without branch param calls git diff HEAD -- <file>', async () => {
|
|
321
|
+
mockExecFile((cmd, args) => {
|
|
322
|
+
if (cmd === 'git' && args.includes('diff')) {
|
|
323
|
+
expect(args).toContain('HEAD');
|
|
324
|
+
expect(args).toContain('README.md');
|
|
325
|
+
return '--- a/README.md\n+++ b/README.md\n@@ -1 +1 @@\n-old\n+new';
|
|
326
|
+
}
|
|
327
|
+
return '';
|
|
328
|
+
});
|
|
329
|
+
const res = await get('/api/git-diff?file=README.md');
|
|
330
|
+
expect(res.status).toBe(200);
|
|
331
|
+
expect(res.data).toHaveProperty('file', 'README.md');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('with branch param calls git diff <branch> -- <file>', async () => {
|
|
335
|
+
mockExecFile((cmd, args) => {
|
|
336
|
+
if (cmd === 'git' && args.includes('diff')) {
|
|
337
|
+
expect(args).toContain('feature-branch');
|
|
338
|
+
expect(args).toContain('--');
|
|
339
|
+
expect(args).toContain('server.js');
|
|
340
|
+
return '--- a/server.js\n+++ b/server.js\n@@ -1 +1 @@\n-old\n+new';
|
|
341
|
+
}
|
|
342
|
+
return '';
|
|
343
|
+
});
|
|
344
|
+
const res = await get('/api/git-diff?file=server.js&branch=feature-branch');
|
|
345
|
+
expect(res.status).toBe(200);
|
|
346
|
+
expect(res.data).toHaveProperty('file', 'server.js');
|
|
347
|
+
expect(res.data).toHaveProperty('diff');
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('with branch param rejects invalid branch names', async () => {
|
|
351
|
+
const res = await get('/api/git-diff?file=README.md&branch=bad;branch');
|
|
352
|
+
expect(res.status).toBe(400);
|
|
353
|
+
expect(res.data).toHaveProperty('error', 'Invalid branch name');
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
describe('GET /api/files with branch param', () => {
|
|
358
|
+
it('with branch param calls git ls-tree (single call)', async () => {
|
|
359
|
+
let lsTreeCalls = 0;
|
|
360
|
+
mockExecFile((cmd, args) => {
|
|
361
|
+
if (cmd === 'git') {
|
|
362
|
+
expect(args[0]).toBe('ls-tree');
|
|
363
|
+
expect(args).not.toContain('--name-only');
|
|
364
|
+
expect(args).toContain('feature-branch');
|
|
365
|
+
lsTreeCalls++;
|
|
366
|
+
return '100644 blob abcdef1234567890\tREADME.md\n100644 blob abcdef1234567891\tpackage.json\n040000 tree abcdef1234567892\tsrc\n';
|
|
367
|
+
}
|
|
368
|
+
return '';
|
|
369
|
+
});
|
|
370
|
+
const res = await get('/api/files?branch=feature-branch');
|
|
371
|
+
expect(res.status).toBe(200);
|
|
372
|
+
expect(lsTreeCalls).toBe(1);
|
|
373
|
+
expect(Array.isArray(res.data)).toBe(true);
|
|
374
|
+
expect(res.data.length).toBe(3);
|
|
375
|
+
const srcEntry = res.data.find(e => e.name === 'src');
|
|
376
|
+
expect(srcEntry).toHaveProperty('type', 'directory');
|
|
377
|
+
const readmeEntry = res.data.find(e => e.name === 'README.md');
|
|
378
|
+
expect(readmeEntry).toHaveProperty('type', 'file');
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('with branch param filters ignored directories', async () => {
|
|
382
|
+
mockExecFile((cmd, args) => {
|
|
383
|
+
if (cmd === 'git') {
|
|
384
|
+
return '040000 tree aaa\tnode_modules\n040000 tree bbb\t.git\n040000 tree ccc\tsrc\n100644 blob ddd\tREADME.md\n100644 blob eee\t.env\n';
|
|
385
|
+
}
|
|
386
|
+
return '';
|
|
387
|
+
});
|
|
388
|
+
const res = await get('/api/files?branch=feature-branch');
|
|
389
|
+
expect(res.status).toBe(200);
|
|
390
|
+
const names = res.data.map(e => e.name);
|
|
391
|
+
expect(names).toContain('src');
|
|
392
|
+
expect(names).toContain('README.md');
|
|
393
|
+
expect(names).toContain('node_modules');
|
|
394
|
+
expect(names).not.toContain('.git');
|
|
395
|
+
expect(names).toContain('.env');
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('with branch param and path lists subdirectory via git ls-tree', async () => {
|
|
399
|
+
mockExecFile((cmd, args) => {
|
|
400
|
+
if (cmd === 'git') {
|
|
401
|
+
expect(args[0]).toBe('ls-tree');
|
|
402
|
+
expect(args).toContain('feature-branch:server');
|
|
403
|
+
return '100644 blob abcdef1234567890\tindex.js\n100644 blob abcdef1234567891\thandlers.js\n';
|
|
404
|
+
}
|
|
405
|
+
return '';
|
|
406
|
+
});
|
|
407
|
+
const res = await get('/api/files?path=server&branch=feature-branch');
|
|
408
|
+
expect(res.status).toBe(200);
|
|
409
|
+
expect(Array.isArray(res.data)).toBe(true);
|
|
410
|
+
expect(res.data.length).toBe(2);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('with branch param rejects invalid branch names', async () => {
|
|
414
|
+
const res = await get('/api/files?branch=bad;branch');
|
|
415
|
+
expect(res.status).toBe(400);
|
|
416
|
+
expect(res.data).toHaveProperty('error', 'Invalid branch name');
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('with branch param shows everything except .git', async () => {
|
|
420
|
+
mockExecFile((cmd, args) => {
|
|
421
|
+
if (cmd === 'git') {
|
|
422
|
+
return '040000 tree aaa\tnode_modules\n040000 tree bbb\t.git\n040000 tree ccc\tsrc\n100644 blob ddd\tREADME.md\n100644 blob eee\t.env\n100644 blob fff\t.eslintrc\n040000 tree abc\tcoverage\n';
|
|
423
|
+
}
|
|
424
|
+
return '';
|
|
425
|
+
});
|
|
426
|
+
const res = await get('/api/files?branch=feature-branch');
|
|
427
|
+
expect(res.status).toBe(200);
|
|
428
|
+
const names = res.data.map(e => e.name);
|
|
429
|
+
expect(names).toContain('src');
|
|
430
|
+
expect(names).toContain('README.md');
|
|
431
|
+
expect(names).toContain('.env');
|
|
432
|
+
expect(names).toContain('.eslintrc');
|
|
433
|
+
expect(names).toContain('node_modules');
|
|
434
|
+
expect(names).toContain('coverage');
|
|
435
|
+
expect(names).not.toContain('.git');
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
describe('PATCH /api/issue/:id', () => {
|
|
440
|
+
it('updates description via bd update', async () => {
|
|
441
|
+
mockExecFile((cmd, args) => {
|
|
442
|
+
if (cmd === 'bd') {
|
|
443
|
+
expect(args[0]).toBe('update');
|
|
444
|
+
expect(args[1]).toBe('beads-board-abc');
|
|
445
|
+
expect(args).toContain('--description');
|
|
446
|
+
expect(args).toContain('New description text');
|
|
447
|
+
return '';
|
|
448
|
+
}
|
|
449
|
+
return '';
|
|
450
|
+
});
|
|
451
|
+
const res = await patch('/api/issue/beads-board-abc', { description: 'New description text' });
|
|
452
|
+
expect(res.status).toBe(200);
|
|
453
|
+
expect(res.data).toHaveProperty('ok', true);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('returns 400 for invalid issue ID', async () => {
|
|
457
|
+
const res = await patch('/api/issue/invalid;id', { description: 'test' });
|
|
458
|
+
expect(res.status).toBe(400);
|
|
459
|
+
expect(res.data).toHaveProperty('error', 'Invalid issue ID');
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('returns 400 when description field is missing', async () => {
|
|
463
|
+
const res = await patch('/api/issue/beads-board-abc', {});
|
|
464
|
+
expect(res.status).toBe(400);
|
|
465
|
+
expect(res.data).toHaveProperty('error');
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('returns 500 when bd update fails', async () => {
|
|
469
|
+
mockExecFile((cmd, args) => {
|
|
470
|
+
if (cmd === 'bd') throw new Error('bd update failed: issue not found');
|
|
471
|
+
return '';
|
|
472
|
+
});
|
|
473
|
+
const res = await patch('/api/issue/beads-board-abc', { description: 'test' });
|
|
474
|
+
expect(res.status).toBe(500);
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
describe('GET /api/file-content', () => {
|
|
479
|
+
it('with branch param calls git show <branch>:<path>', async () => {
|
|
480
|
+
mockExecFile((cmd, args) => {
|
|
481
|
+
if (cmd === 'git') {
|
|
482
|
+
expect(args[0]).toBe('show');
|
|
483
|
+
expect(args[1]).toBe('feature-branch:src/index.ts');
|
|
484
|
+
return 'console.log("hello")';
|
|
485
|
+
}
|
|
486
|
+
return '';
|
|
487
|
+
});
|
|
488
|
+
const res = await get('/api/file-content?path=src/index.ts&branch=feature-branch');
|
|
489
|
+
expect(res.status).toBe(200);
|
|
490
|
+
expect(res.data).toHaveProperty('content', 'console.log("hello")');
|
|
491
|
+
expect(res.data).toHaveProperty('language', 'typescript');
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('with branch param rejects invalid branch names', async () => {
|
|
495
|
+
const res = await get('/api/file-content?path=README.md&branch=bad;branch');
|
|
496
|
+
expect(res.status).toBe(400);
|
|
497
|
+
expect(res.data).toHaveProperty('error', 'Invalid branch name');
|
|
498
|
+
});
|
|
260
499
|
});
|
|
261
500
|
});
|