@citadel-labs/beads-ui 2.0.2 → 2.0.7

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 (35) hide show
  1. package/README.md +62 -71
  2. package/package.json +15 -1
  3. package/server/dist/assets/index-BzxBsA4M.css +1 -0
  4. package/server/dist/assets/index-CKSk2-yh.js +57 -0
  5. package/server/dist/index.html +14 -14
  6. package/server/index.js +281 -242
  7. package/.claude/worktrees/agent-a6ab297c/LICENSE +0 -21
  8. package/.claude/worktrees/agent-a6ab297c/README.md +0 -71
  9. package/.claude/worktrees/agent-a6ab297c/bin/beads-board.js +0 -183
  10. package/.claude/worktrees/agent-a6ab297c/package.json +0 -16
  11. package/.claude/worktrees/agent-a6ab297c/server/dist/assets/index-B8Dp1QU-.js +0 -53
  12. package/.claude/worktrees/agent-a6ab297c/server/dist/assets/index-Cx3XpFYM.css +0 -1
  13. package/.claude/worktrees/agent-a6ab297c/server/dist/index.html +0 -14
  14. package/.claude/worktrees/agent-a6ab297c/server/dist/vite.svg +0 -1
  15. package/.claude/worktrees/agent-a6ab297c/server/index.js +0 -242
  16. package/.claude/worktrees/agent-a8b08b0a/LICENSE +0 -21
  17. package/.claude/worktrees/agent-a8b08b0a/README.md +0 -71
  18. package/.claude/worktrees/agent-a8b08b0a/bin/beads-board.js +0 -183
  19. package/.claude/worktrees/agent-a8b08b0a/package.json +0 -16
  20. package/.claude/worktrees/agent-a8b08b0a/server/dist/assets/index-CidSj3mC.css +0 -1
  21. package/.claude/worktrees/agent-a8b08b0a/server/dist/assets/index-tKlN_npR.js +0 -53
  22. package/.claude/worktrees/agent-a8b08b0a/server/dist/index.html +0 -14
  23. package/.claude/worktrees/agent-a8b08b0a/server/dist/vite.svg +0 -1
  24. package/.claude/worktrees/agent-a8b08b0a/server/index.js +0 -242
  25. package/screenshot-badge-error.png +0 -0
  26. package/screenshot-card-modal.png +0 -0
  27. package/screenshot-collapsed.png +0 -0
  28. package/screenshot-collapsed2.png +0 -0
  29. package/screenshot-hover-commit.png +0 -0
  30. package/screenshot-modal-from-badge.png +0 -0
  31. package/screenshot-overview.png +0 -0
  32. package/screenshot-polished-modal.png +0 -0
  33. package/screenshot-tooltip-hover.png +0 -0
  34. package/server/dist/assets/index-BaFX4Ey_.css +0 -1
  35. package/server/dist/assets/index-CkFv0lE0.js +0 -57
@@ -1,14 +1,14 @@
1
- <!doctype html>
2
- <html lang="en" class="dark">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>ui</title>
8
- <script type="module" crossorigin src="/assets/index-CkFv0lE0.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-BaFX4Ey_.css">
10
- </head>
11
- <body>
12
- <div id="root"></div>
13
- </body>
14
- </html>
1
+ <!doctype html>
2
+ <html lang="en" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>ui</title>
8
+ <script type="module" crossorigin src="/assets/index-CKSk2-yh.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-BzxBsA4M.css">
10
+ </head>
11
+ <body>
12
+ <div id="root"></div>
13
+ </body>
14
+ </html>
package/server/index.js CHANGED
@@ -1,242 +1,281 @@
1
- const http = require('node:http');
2
- const fs = require('node:fs');
3
- const { execFile } = require('node:child_process');
4
- const path = require('node:path');
5
- const url = require('node:url');
6
-
7
- const DEFAULT_PORT = 8377;
8
- const DIST_DIR = path.join(__dirname, 'dist');
9
-
10
- const MIME_TYPES = {
11
- '.html': 'text/html',
12
- '.js': 'application/javascript',
13
- '.css': 'text/css',
14
- '.json': 'application/json',
15
- '.png': 'image/png',
16
- '.svg': 'image/svg+xml',
17
- '.ico': 'image/x-icon',
18
- };
19
-
20
- // Project directory: passed as CLI arg or cwd
21
- const PROJECT_DIR = process.argv[2] || process.cwd();
22
-
23
- // ---------------------------------------------------------------------------
24
- // CLI helpers
25
- // ---------------------------------------------------------------------------
26
-
27
- function execCmd(cmd, args, cwd) {
28
- return new Promise((resolve, reject) => {
29
- execFile(cmd, args, { cwd, timeout: 10000, windowsHide: true }, (err, stdout, stderr) => {
30
- if (err) {
31
- reject(new Error(`${cmd} ${args.join(' ')} failed: ${stderr || err.message}`));
32
- return;
33
- }
34
- resolve(stdout);
35
- });
36
- });
37
- }
38
-
39
- async function execBd(args) {
40
- const stdout = await execCmd('bd', [...args, '--json'], PROJECT_DIR);
41
- try {
42
- return JSON.parse(stdout);
43
- } catch {
44
- // bd list with no issues prints "No issues found." instead of JSON
45
- return [];
46
- }
47
- }
48
-
49
- async function execGit(args) {
50
- return execCmd('git', args, PROJECT_DIR);
51
- }
52
-
53
- // ---------------------------------------------------------------------------
54
- // Issue normalization — bd CLI outputs `issue_type`, UI expects `type`
55
- // ---------------------------------------------------------------------------
56
-
57
- function normalizeIssue(issue) {
58
- if (issue.issue_type && !issue.type) {
59
- issue.type = issue.issue_type;
60
- }
61
- return issue;
62
- }
63
-
64
- // ---------------------------------------------------------------------------
65
- // JSON response helpers
66
- // ---------------------------------------------------------------------------
67
-
68
- function jsonResponse(res, data, status = 200) {
69
- res.writeHead(status, {
70
- 'Content-Type': 'application/json',
71
- 'Access-Control-Allow-Origin': '*',
72
- });
73
- res.end(JSON.stringify(data));
74
- }
75
-
76
- function errorResponse(res, message, status = 500) {
77
- jsonResponse(res, { error: message }, status);
78
- }
79
-
80
- // ---------------------------------------------------------------------------
81
- // Request handler
82
- // ---------------------------------------------------------------------------
83
-
84
- async function handleRequest(req, res) {
85
- const parsed = url.parse(req.url, true);
86
- const pathname = parsed.pathname;
87
-
88
- if (req.method === 'OPTIONS') {
89
- res.writeHead(204, {
90
- 'Access-Control-Allow-Origin': '*',
91
- 'Access-Control-Allow-Methods': 'GET, OPTIONS',
92
- 'Access-Control-Allow-Headers': 'Content-Type',
93
- });
94
- res.end();
95
- return;
96
- }
97
-
98
- try {
99
- if (pathname === '/api/issues') {
100
- const issues = await execBd(['list', '--flat', '--status=all']);
101
- jsonResponse(res, Array.isArray(issues) ? issues.map(normalizeIssue) : issues);
102
- } else if (pathname === '/api/ready') {
103
- const ready = await execBd(['ready']);
104
- jsonResponse(res, Array.isArray(ready) ? ready.map(normalizeIssue) : ready);
105
- } else if (pathname === '/api/blocked') {
106
- const blocked = await execBd(['blocked']);
107
- jsonResponse(res, Array.isArray(blocked) ? blocked.map(normalizeIssue) : blocked);
108
- } else if (pathname.startsWith('/api/issue/')) {
109
- const id = pathname.split('/api/issue/')[1];
110
- if (!id || !/^[\w-]+$/.test(id)) {
111
- errorResponse(res, 'Invalid issue ID', 400);
112
- return;
113
- }
114
- const issue = await execBd(['show', id]);
115
- jsonResponse(res, issue);
116
- } else if (pathname === '/api/git-log') {
117
- const branch = parsed.query.branch || '';
118
- const limit = Math.min(Math.max(parseInt(parsed.query.limit || '50', 10) || 50, 1), 500);
119
- if (branch && !/^[\w\/.@{}-]+$/.test(branch)) {
120
- errorResponse(res, 'Invalid branch name', 400);
121
- return;
122
- }
123
- const format = '%h%x00%s%x00%b%x00%an%x00%ai%x1e';
124
- const args = ['log', `--format=${format}`, `-n`, `${limit}`];
125
- if (branch) args.splice(1, 0, branch);
126
- const stdout = await execGit(args);
127
- const commits = stdout.split('\x1e').filter(s => s.trim()).map(record => {
128
- const [hash, message, body, author, date] = record.trim().split('\0');
129
- return { hash, message, body: body?.trim() || '', author, date };
130
- });
131
- jsonResponse(res, commits);
132
- } else if (pathname === '/api/project') {
133
- let name = path.basename(PROJECT_DIR);
134
- try {
135
- const origin = (await execGit(['remote', 'get-url', 'origin'])).trim();
136
- const match = origin.match(/\/([^/]+?)(?:\.git)?$/);
137
- if (match) name = match[1];
138
- } catch {}
139
- jsonResponse(res, { name });
140
- } else if (pathname === '/api/branches') {
141
- const stdout = await execGit(['branch', '--format=%(refname:short)']);
142
- const branches = stdout.trim().split('\n').filter(Boolean);
143
- const current = (await execGit(['rev-parse', '--abbrev-ref', 'HEAD'])).trim();
144
- jsonResponse(res, { branches, current });
145
- } else {
146
- // Serve static files from dist/
147
- let filePath = path.join(DIST_DIR, pathname === '/' ? 'index.html' : pathname);
148
- const resolved = path.resolve(filePath);
149
- if (!resolved.startsWith(path.resolve(DIST_DIR))) {
150
- res.writeHead(403, { 'Content-Type': 'text/plain' });
151
- res.end('Forbidden');
152
- return;
153
- }
154
- // SPA fallback: serve index.html for non-file paths
155
- if (!path.extname(filePath)) {
156
- filePath = path.join(DIST_DIR, 'index.html');
157
- }
158
- try {
159
- const data = fs.readFileSync(filePath);
160
- const ext = path.extname(filePath);
161
- res.writeHead(200, { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' });
162
- res.end(data);
163
- } catch {
164
- res.writeHead(404, { 'Content-Type': 'text/plain' });
165
- res.end('Not found');
166
- }
167
- }
168
- } catch (err) {
169
- errorResponse(res, err.message);
170
- }
171
- }
172
-
173
- // ---------------------------------------------------------------------------
174
- // Pidfile management
175
- // ---------------------------------------------------------------------------
176
-
177
- const PIDFILE = path.join(PROJECT_DIR, '.beads-board.pid');
178
-
179
- function writePidfile(port) {
180
- fs.writeFileSync(PIDFILE, JSON.stringify({ pid: process.pid, port }));
181
- }
182
-
183
- function removePidfile() {
184
- try { fs.unlinkSync(PIDFILE); } catch {}
185
- }
186
-
187
- function getRunningInstance() {
188
- try {
189
- const data = JSON.parse(fs.readFileSync(PIDFILE, 'utf8'));
190
- // Check if process is still running
191
- process.kill(data.pid, 0);
192
- return data;
193
- } catch {
194
- removePidfile();
195
- return null;
196
- }
197
- }
198
-
199
- // ---------------------------------------------------------------------------
200
- // Port detection
201
- // ---------------------------------------------------------------------------
202
-
203
- function findAvailablePort(startPort) {
204
- return new Promise((resolve, reject) => {
205
- const s = http.createServer();
206
- s.listen(startPort, () => {
207
- s.close(() => resolve(startPort));
208
- });
209
- s.on('error', () => {
210
- if (startPort < DEFAULT_PORT + 10) {
211
- resolve(findAvailablePort(startPort + 1));
212
- } else {
213
- reject(new Error('No available port found'));
214
- }
215
- });
216
- });
217
- }
218
-
219
- // ---------------------------------------------------------------------------
220
- // Server startup
221
- // ---------------------------------------------------------------------------
222
-
223
- const server = http.createServer(handleRequest);
224
-
225
- async function start() {
226
- const existing = getRunningInstance();
227
- if (existing) {
228
- console.log(`beads-board already running at http://localhost:${existing.port}`);
229
- process.exit(0);
230
- }
231
-
232
- const port = await findAvailablePort(parseInt(process.env.PORT || DEFAULT_PORT, 10));
233
- server.listen(port, () => {
234
- writePidfile(port);
235
- console.log(`beads-board server running at http://localhost:${port}`);
236
- });
237
- }
238
-
239
- process.on('SIGTERM', () => { removePidfile(); process.exit(0); });
240
- process.on('SIGINT', () => { removePidfile(); process.exit(0); });
241
-
242
- start();
1
+ const http = require('node:http');
2
+ const fs = require('node:fs');
3
+ const { execFile } = require('node:child_process');
4
+ const path = require('node:path');
5
+ const url = require('node:url');
6
+
7
+ const DEFAULT_PORT = 8377;
8
+ const DIST_DIR = path.join(__dirname, 'dist');
9
+
10
+ const MIME_TYPES = {
11
+ '.html': 'text/html',
12
+ '.js': 'application/javascript',
13
+ '.css': 'text/css',
14
+ '.json': 'application/json',
15
+ '.png': 'image/png',
16
+ '.svg': 'image/svg+xml',
17
+ '.ico': 'image/x-icon',
18
+ };
19
+
20
+ // Project directory: passed as CLI arg or cwd
21
+ const PROJECT_DIR = process.argv[2] || process.cwd();
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // CLI helpers
25
+ // ---------------------------------------------------------------------------
26
+
27
+ function execCmd(cmd, args, cwd) {
28
+ return new Promise((resolve, reject) => {
29
+ execFile(cmd, args, { cwd, timeout: 10000, windowsHide: true }, (err, stdout, stderr) => {
30
+ if (err) {
31
+ reject(new Error(`${cmd} ${args.join(' ')} failed: ${stderr || err.message}`));
32
+ return;
33
+ }
34
+ resolve(stdout);
35
+ });
36
+ });
37
+ }
38
+
39
+ async function execBd(args) {
40
+ const stdout = await execCmd('bd', [...args, '--json'], PROJECT_DIR);
41
+ try {
42
+ return JSON.parse(stdout);
43
+ } catch {
44
+ // bd list with no issues prints "No issues found." instead of JSON
45
+ return [];
46
+ }
47
+ }
48
+
49
+ async function execGit(args) {
50
+ return execCmd('git', args, PROJECT_DIR);
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Issue normalization — bd CLI outputs `issue_type`, UI expects `type`
55
+ // ---------------------------------------------------------------------------
56
+
57
+ function normalizeIssue(issue) {
58
+ if (issue.issue_type && !issue.type) {
59
+ issue.type = issue.issue_type;
60
+ }
61
+ return issue;
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // JSON response helpers
66
+ // ---------------------------------------------------------------------------
67
+
68
+ function jsonResponse(res, data, status = 200) {
69
+ res.writeHead(status, {
70
+ 'Content-Type': 'application/json',
71
+ 'Access-Control-Allow-Origin': '*',
72
+ });
73
+ res.end(JSON.stringify(data));
74
+ }
75
+
76
+ function errorResponse(res, message, status = 500) {
77
+ jsonResponse(res, { error: message }, status);
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Request handler
82
+ // ---------------------------------------------------------------------------
83
+
84
+ async function handleRequest(req, res) {
85
+ const parsed = url.parse(req.url, true);
86
+ const pathname = parsed.pathname;
87
+
88
+ if (req.method === 'OPTIONS') {
89
+ res.writeHead(204, {
90
+ 'Access-Control-Allow-Origin': '*',
91
+ 'Access-Control-Allow-Methods': 'GET, OPTIONS',
92
+ 'Access-Control-Allow-Headers': 'Content-Type',
93
+ });
94
+ res.end();
95
+ return;
96
+ }
97
+
98
+ try {
99
+ if (pathname === '/api/issues') {
100
+ const issues = await execBd(['list', '--flat', '--status=all']);
101
+ jsonResponse(res, Array.isArray(issues) ? issues.map(normalizeIssue) : issues);
102
+ } else if (pathname === '/api/ready') {
103
+ const ready = await execBd(['ready']);
104
+ jsonResponse(res, Array.isArray(ready) ? ready.map(normalizeIssue) : ready);
105
+ } else if (pathname === '/api/blocked') {
106
+ const blocked = await execBd(['blocked']);
107
+ jsonResponse(res, Array.isArray(blocked) ? blocked.map(normalizeIssue) : blocked);
108
+ } else if (pathname.startsWith('/api/issue/')) {
109
+ const id = pathname.split('/api/issue/')[1];
110
+ if (!id || !/^[\w-]+$/.test(id)) {
111
+ errorResponse(res, 'Invalid issue ID', 400);
112
+ return;
113
+ }
114
+ const issue = await execBd(['show', id]);
115
+ jsonResponse(res, issue);
116
+ } else if (pathname === '/api/git-log') {
117
+ const branch = parsed.query.branch || '';
118
+ const limit = Math.min(Math.max(parseInt(parsed.query.limit || '50', 10) || 50, 1), 500);
119
+ if (branch && !/^[\w\/.@{}-]+$/.test(branch)) {
120
+ errorResponse(res, 'Invalid branch name', 400);
121
+ return;
122
+ }
123
+ const format = '%h%x00%s%x00%b%x00%an%x00%ai%x1e';
124
+ const args = ['log', `--format=${format}`, `-n`, `${limit}`];
125
+ if (branch) args.splice(1, 0, branch);
126
+ const stdout = await execGit(args);
127
+ const commits = stdout.split('\x1e').filter(s => s.trim()).map(record => {
128
+ const [hash, message, body, author, date] = record.trim().split('\0');
129
+ return { hash, message, body: body?.trim() || '', author, date };
130
+ });
131
+ jsonResponse(res, commits);
132
+ } else if (pathname === '/api/project') {
133
+ let name = path.basename(PROJECT_DIR);
134
+ try {
135
+ const origin = (await execGit(['remote', 'get-url', 'origin'])).trim();
136
+ const match = origin.match(/\/([^/]+?)(?:\.git)?$/);
137
+ if (match) name = match[1];
138
+ } catch {}
139
+ jsonResponse(res, { name });
140
+ } else if (pathname === '/api/dependencies') {
141
+ const issues = await execBd(['list', '--flat', '--status=all']);
142
+ const allIssues = Array.isArray(issues) ? issues.map(normalizeIssue) : [];
143
+ const edges = [];
144
+ for (const issue of allIssues) {
145
+ if (issue.dependencies && Array.isArray(issue.dependencies)) {
146
+ for (const dep of issue.dependencies) {
147
+ const depId = typeof dep === 'string' ? dep : dep.id || dep.issue_id;
148
+ if (depId) {
149
+ edges.push({ from: issue.id, to: depId });
150
+ }
151
+ }
152
+ }
153
+ }
154
+ jsonResponse(res, { nodes: allIssues, edges });
155
+ } else if (pathname === '/api/projects') {
156
+ try {
157
+ const parentDir = path.dirname(PROJECT_DIR);
158
+ const entries = fs.readdirSync(parentDir, { withFileTypes: true });
159
+ const projects = [];
160
+ for (const entry of entries) {
161
+ if (entry.isDirectory()) {
162
+ const fullPath = path.join(parentDir, entry.name);
163
+ const beadsDir = path.join(fullPath, '.beads');
164
+ try {
165
+ const stat = fs.statSync(beadsDir);
166
+ if (stat.isDirectory()) {
167
+ projects.push({ name: entry.name, path: fullPath });
168
+ }
169
+ } catch {
170
+ // No .beads directory, skip
171
+ }
172
+ }
173
+ }
174
+ projects.sort((a, b) => a.name.localeCompare(b.name));
175
+ jsonResponse(res, projects);
176
+ } catch {
177
+ jsonResponse(res, []);
178
+ }
179
+ } else if (pathname === '/api/branches') {
180
+ const stdout = await execGit(['branch', '--format=%(refname:short)']);
181
+ const branches = stdout.trim().split('\n').filter(Boolean);
182
+ const current = (await execGit(['rev-parse', '--abbrev-ref', 'HEAD'])).trim();
183
+ jsonResponse(res, { branches, current });
184
+ } else {
185
+ // Serve static files from dist/
186
+ let filePath = path.join(DIST_DIR, pathname === '/' ? 'index.html' : pathname);
187
+ const resolved = path.resolve(filePath);
188
+ if (!resolved.startsWith(path.resolve(DIST_DIR))) {
189
+ res.writeHead(403, { 'Content-Type': 'text/plain' });
190
+ res.end('Forbidden');
191
+ return;
192
+ }
193
+ // SPA fallback: serve index.html for non-file paths
194
+ if (!path.extname(filePath)) {
195
+ filePath = path.join(DIST_DIR, 'index.html');
196
+ }
197
+ try {
198
+ const data = fs.readFileSync(filePath);
199
+ const ext = path.extname(filePath);
200
+ res.writeHead(200, { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' });
201
+ res.end(data);
202
+ } catch {
203
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
204
+ res.end('Not found');
205
+ }
206
+ }
207
+ } catch (err) {
208
+ errorResponse(res, err.message);
209
+ }
210
+ }
211
+
212
+ // ---------------------------------------------------------------------------
213
+ // Pidfile management
214
+ // ---------------------------------------------------------------------------
215
+
216
+ const PIDFILE = path.join(PROJECT_DIR, '.beads-board.pid');
217
+
218
+ function writePidfile(port) {
219
+ fs.writeFileSync(PIDFILE, JSON.stringify({ pid: process.pid, port }));
220
+ }
221
+
222
+ function removePidfile() {
223
+ try { fs.unlinkSync(PIDFILE); } catch {}
224
+ }
225
+
226
+ function getRunningInstance() {
227
+ try {
228
+ const data = JSON.parse(fs.readFileSync(PIDFILE, 'utf8'));
229
+ // Check if process is still running
230
+ process.kill(data.pid, 0);
231
+ return data;
232
+ } catch {
233
+ removePidfile();
234
+ return null;
235
+ }
236
+ }
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // Port detection
240
+ // ---------------------------------------------------------------------------
241
+
242
+ function findAvailablePort(startPort) {
243
+ return new Promise((resolve, reject) => {
244
+ const s = http.createServer();
245
+ s.listen(startPort, () => {
246
+ s.close(() => resolve(startPort));
247
+ });
248
+ s.on('error', () => {
249
+ if (startPort < DEFAULT_PORT + 10) {
250
+ resolve(findAvailablePort(startPort + 1));
251
+ } else {
252
+ reject(new Error('No available port found'));
253
+ }
254
+ });
255
+ });
256
+ }
257
+
258
+ // ---------------------------------------------------------------------------
259
+ // Server startup
260
+ // ---------------------------------------------------------------------------
261
+
262
+ const server = http.createServer(handleRequest);
263
+
264
+ async function start() {
265
+ const existing = getRunningInstance();
266
+ if (existing) {
267
+ console.log(`beads-board already running at http://localhost:${existing.port}`);
268
+ process.exit(0);
269
+ }
270
+
271
+ const port = await findAvailablePort(parseInt(process.env.PORT || DEFAULT_PORT, 10));
272
+ server.listen(port, () => {
273
+ writePidfile(port);
274
+ console.log(`beads-board server running at http://localhost:${port}`);
275
+ });
276
+ }
277
+
278
+ process.on('SIGTERM', () => { removePidfile(); process.exit(0); });
279
+ process.on('SIGINT', () => { removePidfile(); process.exit(0); });
280
+
281
+ start();
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Stuart Rimel
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
@@ -1,71 +0,0 @@
1
- # beads-board
2
-
3
- A minimal kanban dashboard and git log viewer for [Beads](https://github.com/steveyegge/beads). Works standalone as a CLI tool or as a Claude Code plugin.
4
-
5
- ## Features
6
-
7
- - **Kanban board** — Issues organized by status: Ready, In Progress, Blocked, Done
8
- - **Git log** — Scrollable commit history with branch selector
9
- - **Bead ID linking** — Bead IDs in commit messages are highlighted as badges
10
- - **Dark/light theme** — Toggle between themes, dark by default
11
- - **Auto-refresh** — Polls for updates every 5 seconds
12
- - **Zero runtime dependencies** — Server uses only Node.js stdlib
13
-
14
- ## Quick Start
15
-
16
- ### Standalone CLI (works with any editor)
17
-
18
- ```bash
19
- # Install globally
20
- npm install -g @citadel-labs/beads-ui
21
-
22
- # Or run directly with npx (installs temporarily)
23
- npx @citadel-labs/beads-ui
24
- ```
25
-
26
- Then from any project that uses [Beads](https://github.com/steveyegge/beads):
27
-
28
- ```bash
29
- cd /path/to/your/project
30
- bdui # Start dashboard in background, prints URL
31
- bdui /path/to/project # Specify a project directory
32
- bdui --port 9000 # Custom port
33
- bdui status # Check if dashboard is running
34
- bdui stop # Stop the dashboard
35
- ```
36
-
37
- The server starts in the background and returns control to your terminal. Open the printed URL (default **http://localhost:8377**) in your browser. Port auto-increments if taken.
38
-
39
- ### As a Claude Code Plugin
40
-
41
- ```bash
42
- # Install from local directory
43
- claude --plugin-dir /path/to/beads-board
44
-
45
- # Then in any project with .beads/
46
- /beads-board:start # Start the dashboard server
47
- /beads-board:stop # Stop it
48
- ```
49
-
50
- The plugin auto-detects `.beads/` in your project. If your project doesn't use Beads, it will let you know.
51
-
52
- ## How It Works
53
-
54
- The server shells out to `bd` and `git` CLI commands to fetch data, then serves a React dashboard that polls the API every 5 seconds. No direct database access — all data flows through the Beads CLI.
55
-
56
- ```
57
- Browser → GET /api/* → Node.js server → bd/git CLI → JSON response
58
- ← React app ← server/dist/
59
- ```
60
-
61
- See [docs/architecture.md](docs/architecture.md) for details.
62
-
63
- ## Documentation
64
-
65
- - [Architecture](docs/architecture.md) — How the server, UI, and plugin fit together
66
- - [API Reference](docs/api.md) — All API endpoints with examples
67
- - [Contributing](docs/contributing.md) — How to set up a dev environment and make changes
68
-
69
- ## License
70
-
71
- [MIT](LICENSE)