beads-ui 0.1.0 → 0.1.2

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 (57) hide show
  1. package/CHANGES.md +8 -0
  2. package/README.md +7 -3
  3. package/package.json +12 -2
  4. package/.beads/issues.jsonl +0 -107
  5. package/.editorconfig +0 -10
  6. package/.eslintrc.json +0 -36
  7. package/.github/workflows/ci.yml +0 -38
  8. package/.prettierignore +0 -5
  9. package/AGENTS.md +0 -85
  10. package/app/data/providers.test.js +0 -126
  11. package/app/main.board-switch.test.js +0 -94
  12. package/app/main.deep-link.test.js +0 -64
  13. package/app/main.live-updates.test.js +0 -229
  14. package/app/main.test.js +0 -17
  15. package/app/main.theme.test.js +0 -41
  16. package/app/main.view-sync.test.js +0 -54
  17. package/app/protocol.test.js +0 -57
  18. package/app/router.test.js +0 -34
  19. package/app/state.test.js +0 -21
  20. package/app/utils/markdown.test.js +0 -103
  21. package/app/utils/type-badge.test.js +0 -30
  22. package/app/views/board.test.js +0 -184
  23. package/app/views/detail.acceptance-notes.test.js +0 -67
  24. package/app/views/detail.assignee.test.js +0 -161
  25. package/app/views/detail.deps.test.js +0 -97
  26. package/app/views/detail.edits.test.js +0 -146
  27. package/app/views/detail.labels.test.js +0 -73
  28. package/app/views/detail.priority.test.js +0 -86
  29. package/app/views/detail.test.js +0 -188
  30. package/app/views/detail.ui47.test.js +0 -78
  31. package/app/views/epics.test.js +0 -283
  32. package/app/views/list.inline-edits.test.js +0 -84
  33. package/app/views/list.test.js +0 -479
  34. package/app/views/nav.test.js +0 -43
  35. package/app/ws.test.js +0 -168
  36. package/eslint.config.js +0 -59
  37. package/media/bdui-board.png +0 -0
  38. package/media/bdui-epics.png +0 -0
  39. package/media/bdui-issues.png +0 -0
  40. package/prettier.config.js +0 -13
  41. package/server/app.test.js +0 -29
  42. package/server/bd.test.js +0 -93
  43. package/server/cli/cli.test.js +0 -109
  44. package/server/cli/commands.integration.test.js +0 -155
  45. package/server/cli/commands.unit.test.js +0 -94
  46. package/server/cli/open.test.js +0 -26
  47. package/server/db.test.js +0 -70
  48. package/server/protocol.test.js +0 -87
  49. package/server/watcher.test.js +0 -100
  50. package/server/ws.handlers.test.js +0 -174
  51. package/server/ws.labels.test.js +0 -95
  52. package/server/ws.mutations.test.js +0 -261
  53. package/server/ws.subscriptions.test.js +0 -116
  54. package/server/ws.test.js +0 -52
  55. package/test/setup-vitest.js +0 -12
  56. package/tsconfig.json +0 -23
  57. package/vitest.config.mjs +0 -14
@@ -1,87 +0,0 @@
1
- import { describe, expect, test } from 'vitest';
2
- import {
3
- MESSAGE_TYPES,
4
- PROTOCOL_VERSION,
5
- decodeReply,
6
- decodeRequest,
7
- isMessageType,
8
- isReply,
9
- isRequest,
10
- makeError,
11
- makeOk,
12
- makeRequest
13
- } from './protocol.js';
14
-
15
- describe('server/protocol', () => {
16
- test('isMessageType returns true for known type', () => {
17
- // execution
18
- const res = isMessageType('list-issues');
19
-
20
- // assertion
21
- expect(res).toBe(true);
22
- });
23
-
24
- test('isMessageType returns false for unknown type', () => {
25
- // execution
26
- const res = isMessageType('not-a-type');
27
-
28
- // assertion
29
- expect(res).toBe(false);
30
- });
31
-
32
- test('makeRequest and decodeRequest round-trip', () => {
33
- // setup
34
- const req = makeRequest('show-issue', { id: 'UI-9' }, 'r-9');
35
-
36
- // execution
37
- const decoded = decodeRequest(JSON.parse(JSON.stringify(req)));
38
-
39
- // assertion
40
- expect(isRequest(req)).toBe(true);
41
- expect(decoded.id).toBe('r-9');
42
- expect(decoded.type).toBe('show-issue');
43
- });
44
-
45
- test('makeOk and makeError create valid replies', () => {
46
- // setup
47
- const req = makeRequest('list-issues', undefined, 'r-10');
48
-
49
- // execution
50
- const ok = makeOk(req, [{ id: 'UI-1' }]);
51
- const err = makeError(req, 'boom', 'Something went wrong');
52
-
53
- // assertion
54
- expect(isReply(ok)).toBe(true);
55
- expect(isReply(err)).toBe(true);
56
- expect(ok.ok).toBe(true);
57
- expect(err.ok).toBe(false);
58
- });
59
-
60
- test('decodeReply accepts ok and error envelopes', () => {
61
- // setup
62
- const req = makeRequest('edit-text', { id: 'UI-1', text: 'x' }, 'r-11');
63
- const ok = makeOk(req, { id: 'UI-1' });
64
- const err = makeError(req, 'validation', 'Invalid');
65
-
66
- // execution
67
- const ok2 = decodeReply(JSON.parse(JSON.stringify(ok)));
68
- const err2 = decodeReply(JSON.parse(JSON.stringify(err)));
69
-
70
- // assertion
71
- expect(ok2.ok).toBe(true);
72
- expect(err2.ok).toBe(false);
73
- });
74
-
75
- test('invalid envelopes throw on decode', () => {
76
- // execution + assertion
77
- expect(() => decodeRequest({})).toThrow();
78
- expect(() => decodeReply({ ok: true })).toThrow();
79
- });
80
-
81
- test('exports protocol constants', () => {
82
- // assertion
83
- expect(typeof PROTOCOL_VERSION).toBe('string');
84
- expect(Array.isArray(MESSAGE_TYPES)).toBe(true);
85
- expect(MESSAGE_TYPES.length).toBeGreaterThan(0);
86
- });
87
- });
@@ -1,100 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
2
- import { watchDb } from './watcher.js';
3
-
4
- /** @type {{ dir: string, cb: (event: string, filename?: string) => void, w: { close: () => void } }[]} */
5
- const watchers = [];
6
-
7
- vi.mock('node:fs', () => {
8
- const watch = vi.fn((dir, _opts, cb) => {
9
- // Minimal event emitter interface for FSWatcher
10
- const handlers = /** @type {{ close: Array<() => void> }} */ ({
11
- close: []
12
- });
13
- const w = {
14
- close: () => handlers.close.forEach((fn) => fn())
15
- };
16
- watchers.push({ dir, cb, w });
17
- return /** @type {any} */ (w);
18
- });
19
- return { default: { watch }, watch };
20
- });
21
-
22
- beforeEach(() => {
23
- watchers.length = 0;
24
- vi.useFakeTimers();
25
- vi.spyOn(console, 'warn').mockImplementation(() => {});
26
- });
27
-
28
- afterEach(() => {
29
- vi.useRealTimers();
30
- vi.restoreAllMocks();
31
- });
32
-
33
- describe('watchDb', () => {
34
- test('debounces rapid change events', () => {
35
- const calls = [];
36
- const handle = watchDb('/repo', (p) => calls.push(p), {
37
- debounce_ms: 100,
38
- explicit_db: '/repo/.beads/ui.db'
39
- });
40
- expect(watchers.length).toBe(1);
41
- const { cb } = watchers[0];
42
-
43
- // Fire multiple changes in quick succession
44
- cb('change', 'ui.db');
45
- cb('change', 'ui.db');
46
- cb('rename', 'ui.db');
47
-
48
- // Nothing yet until debounce passes
49
- expect(calls.length).toBe(0);
50
- vi.advanceTimersByTime(99);
51
- expect(calls.length).toBe(0);
52
- vi.advanceTimersByTime(1);
53
- expect(calls.length).toBe(1);
54
-
55
- // Cleanup
56
- handle.close();
57
- });
58
-
59
- test('ignores other filenames', () => {
60
- const calls = [];
61
- const handle = watchDb('/repo', (p) => calls.push(p), {
62
- debounce_ms: 50,
63
- explicit_db: '/repo/.beads/ui.db'
64
- });
65
- const { cb } = watchers[0];
66
- cb('change', 'something-else.db');
67
- vi.advanceTimersByTime(60);
68
- expect(calls.length).toBe(0);
69
- handle.close();
70
- });
71
-
72
- test('rebind attaches to new db path', () => {
73
- const calls = [];
74
- const handle = watchDb('/repo', (p) => calls.push(p), {
75
- debounce_ms: 50,
76
- explicit_db: '/repo/.beads/ui.db'
77
- });
78
- expect(watchers.length).toBe(1);
79
- const first = watchers[0];
80
-
81
- // Rebind to a different DB path
82
- handle.rebind({ explicit_db: '/other/.beads/alt.db' });
83
-
84
- // A new watcher is created
85
- expect(watchers.length).toBe(2);
86
- const second = watchers[1];
87
-
88
- // Old watcher should ignore new file name
89
- first.cb('change', 'ui.db');
90
- vi.advanceTimersByTime(60);
91
- expect(calls.length).toBe(0);
92
-
93
- // New watcher reacts
94
- second.cb('change', 'alt.db');
95
- vi.advanceTimersByTime(60);
96
- expect(calls.length).toBe(1);
97
-
98
- handle.close();
99
- });
100
- });
@@ -1,174 +0,0 @@
1
- import { describe, expect, test, vi } from 'vitest';
2
- import { runBdJson } from './bd.js';
3
- import { handleMessage } from './ws.js';
4
-
5
- vi.mock('./bd.js', () => ({ runBdJson: vi.fn() }));
6
-
7
- function makeStubSocket() {
8
- return {
9
- sent: /** @type {string[]} */ ([]),
10
- readyState: 1,
11
- OPEN: 1,
12
- /** @param {string} msg */
13
- send(msg) {
14
- this.sent.push(String(msg));
15
- }
16
- };
17
- }
18
-
19
- describe('ws handlers: list/show', () => {
20
- test('list-issues forwards payload from bd', async () => {
21
- const mocked = /** @type {import('vitest').Mock} */ (runBdJson);
22
- mocked.mockResolvedValueOnce({ code: 0, stdoutJson: [{ id: 'UI-1' }] });
23
- const ws = makeStubSocket();
24
- const req = {
25
- id: 'r1',
26
- type: 'list-issues',
27
- payload: { filters: { status: 'open' } }
28
- };
29
- await handleMessage(
30
- /** @type {any} */ (ws),
31
- Buffer.from(JSON.stringify(req))
32
- );
33
- const obj = JSON.parse(ws.sent[ws.sent.length - 1]);
34
- expect(obj.ok).toBe(true);
35
- expect(Array.isArray(obj.payload)).toBe(true);
36
- });
37
-
38
- test('list-issues with filters.ready uses bd "ready"', async () => {
39
- const mocked = /** @type {import('vitest').Mock} */ (runBdJson);
40
- mocked.mockResolvedValueOnce({ code: 0, stdoutJson: [{ id: 'UI-2' }] });
41
- const ws = makeStubSocket();
42
- const req = {
43
- id: 'r1a',
44
- type: 'list-issues',
45
- payload: { filters: { ready: true } }
46
- };
47
- await handleMessage(
48
- /** @type {any} */ (ws),
49
- Buffer.from(JSON.stringify(req))
50
- );
51
- // Ensure we called the ready command
52
- const call = mocked.mock.calls[mocked.mock.calls.length - 1];
53
- expect(Array.isArray(call[0])).toBe(true);
54
- expect(call[0][0]).toBe('ready');
55
-
56
- const obj = JSON.parse(ws.sent[ws.sent.length - 1]);
57
- expect(obj.ok).toBe(true);
58
- expect(Array.isArray(obj.payload)).toBe(true);
59
- });
60
-
61
- test('show-issue returns error on missing id', async () => {
62
- const ws = makeStubSocket();
63
- const req = { id: 'r2', type: 'show-issue', payload: {} };
64
- await handleMessage(
65
- /** @type {any} */ (ws),
66
- Buffer.from(JSON.stringify(req))
67
- );
68
- const obj = JSON.parse(ws.sent[ws.sent.length - 1]);
69
- expect(obj.ok).toBe(false);
70
- expect(obj.error.code).toBe('bad_request');
71
- });
72
-
73
- test('show-issue forwards bd JSON on success', async () => {
74
- const mocked = /** @type {import('vitest').Mock} */ (runBdJson);
75
- mocked.mockResolvedValueOnce({
76
- code: 0,
77
- stdoutJson: { id: 'UI-9', title: 'X' }
78
- });
79
- const ws = makeStubSocket();
80
- const req = { id: 'r3', type: 'show-issue', payload: { id: 'UI-9' } };
81
- await handleMessage(
82
- /** @type {any} */ (ws),
83
- Buffer.from(JSON.stringify(req))
84
- );
85
- const obj = JSON.parse(ws.sent[ws.sent.length - 1]);
86
- expect(obj.ok).toBe(true);
87
- expect(obj.payload.id).toBe('UI-9');
88
- });
89
-
90
- test('show-issue unwraps single-element arrays from bd', async () => {
91
- const mocked = /** @type {import('vitest').Mock} */ (runBdJson);
92
- mocked.mockResolvedValueOnce({
93
- code: 0,
94
- stdoutJson: [{ id: 'UI-9', title: 'X' }]
95
- });
96
- const ws = makeStubSocket();
97
- const req = { id: 'r3a', type: 'show-issue', payload: { id: 'UI-9' } };
98
- await handleMessage(
99
- /** @type {any} */ (ws),
100
- Buffer.from(JSON.stringify(req))
101
- );
102
- const obj = JSON.parse(ws.sent[ws.sent.length - 1]);
103
- expect(obj.ok).toBe(true);
104
- expect(obj.payload && obj.payload.id).toBe('UI-9');
105
- });
106
-
107
- test('show-issue returns not_found when bd returns empty array', async () => {
108
- const mocked = /** @type {import('vitest').Mock} */ (runBdJson);
109
- mocked.mockResolvedValueOnce({ code: 0, stdoutJson: [] });
110
- const ws = makeStubSocket();
111
- const req = { id: 'r3b', type: 'show-issue', payload: { id: 'X' } };
112
- await handleMessage(
113
- /** @type {any} */ (ws),
114
- Buffer.from(JSON.stringify(req))
115
- );
116
- const obj = JSON.parse(ws.sent[ws.sent.length - 1]);
117
- expect(obj.ok).toBe(false);
118
- expect(obj.error && obj.error.code).toBe('not_found');
119
- });
120
-
121
- test('epic-status forwards bd epic status', async () => {
122
- const mocked = /** @type {import('vitest').Mock} */ (runBdJson);
123
- mocked.mockResolvedValueOnce({ code: 0, stdoutJson: [] });
124
- const ws = makeStubSocket();
125
- const req = { id: 're', type: /** @type {any} */ ('epic-status') };
126
- await handleMessage(
127
- /** @type {any} */ (ws),
128
- Buffer.from(JSON.stringify(req))
129
- );
130
- const call = mocked.mock.calls[mocked.mock.calls.length - 1];
131
- expect(Array.isArray(call[0])).toBe(true);
132
- expect(call[0][0]).toBe('epic');
133
- expect(call[0][1]).toBe('status');
134
- const obj = JSON.parse(ws.sent[ws.sent.length - 1]);
135
- expect(obj.ok).toBe(true);
136
- expect(Array.isArray(obj.payload)).toBe(true);
137
- });
138
-
139
- test('list-issues supports limit passthrough', async () => {
140
- const mocked = /** @type {import('vitest').Mock} */ (runBdJson);
141
- mocked.mockResolvedValueOnce({ code: 0, stdoutJson: [] });
142
- const ws = makeStubSocket();
143
- const req = {
144
- id: 'rlim',
145
- type: 'list-issues',
146
- payload: { filters: { status: 'closed', limit: 10 } }
147
- };
148
- await handleMessage(
149
- /** @type {any} */ (ws),
150
- Buffer.from(JSON.stringify(req))
151
- );
152
- const call = mocked.mock.calls[mocked.mock.calls.length - 1];
153
- const args = /** @type {string[]} */ (call[0]);
154
- expect(args.includes('--limit')).toBe(true);
155
- expect(args.includes('10')).toBe(true);
156
- const obj = JSON.parse(ws.sent[ws.sent.length - 1]);
157
- expect(obj.ok).toBe(true);
158
- expect(Array.isArray(obj.payload)).toBe(true);
159
- });
160
-
161
- test('bd error propagates as bd_error reply', async () => {
162
- const mocked = /** @type {import('vitest').Mock} */ (runBdJson);
163
- mocked.mockResolvedValueOnce({ code: 1, stderr: 'boom' });
164
- const ws = makeStubSocket();
165
- const req = { id: 'r4', type: 'list-issues', payload: {} };
166
- await handleMessage(
167
- /** @type {any} */ (ws),
168
- Buffer.from(JSON.stringify(req))
169
- );
170
- const obj = JSON.parse(ws.sent[ws.sent.length - 1]);
171
- expect(obj.ok).toBe(false);
172
- expect(obj.error.code).toBe('bd_error');
173
- });
174
- });
@@ -1,95 +0,0 @@
1
- import { describe, expect, test, vi } from 'vitest';
2
- import { runBd, runBdJson } from './bd.js';
3
- import { handleMessage } from './ws.js';
4
-
5
- vi.mock('./bd.js', () => ({
6
- runBd: vi.fn(),
7
- runBdJson: vi.fn()
8
- }));
9
-
10
- function makeStubSocket() {
11
- return {
12
- sent: /** @type {string[]} */ ([]),
13
- readyState: 1,
14
- OPEN: 1,
15
- /** @param {string} msg */
16
- send(msg) {
17
- this.sent.push(String(msg));
18
- }
19
- };
20
- }
21
-
22
- describe('ws labels handlers', () => {
23
- test('label-add validates payload', async () => {
24
- const ws = makeStubSocket();
25
- await handleMessage(
26
- /** @type {any} */ (ws),
27
- Buffer.from(
28
- JSON.stringify({
29
- id: 'x',
30
- type: /** @type {any} */ ('label-add'),
31
- payload: {}
32
- })
33
- )
34
- );
35
- const obj = JSON.parse(ws.sent[0]);
36
- expect(obj.ok).toBe(false);
37
- expect(obj.error.code).toBe('bad_request');
38
- });
39
-
40
- test('label-add runs bd and replies with show', async () => {
41
- const rb = /** @type {import('vitest').Mock} */ (runBd);
42
- const rj = /** @type {import('vitest').Mock} */ (runBdJson);
43
- rb.mockResolvedValueOnce({ code: 0, stdout: '', stderr: '' });
44
- rj.mockResolvedValueOnce({
45
- code: 0,
46
- stdoutJson: { id: 'UI-1', labels: ['frontend'] }
47
- });
48
-
49
- const ws = makeStubSocket();
50
- await handleMessage(
51
- /** @type {any} */ (ws),
52
- Buffer.from(
53
- JSON.stringify({
54
- id: 'a',
55
- type: /** @type {any} */ ('label-add'),
56
- payload: { id: 'UI-1', label: 'frontend' }
57
- })
58
- )
59
- );
60
-
61
- const call = rb.mock.calls[0][0];
62
- expect(call.slice(0, 3)).toEqual(['label', 'add', 'UI-1']);
63
- const obj = JSON.parse(ws.sent[ws.sent.length - 1]);
64
- expect(obj.ok).toBe(true);
65
- expect(obj.payload && obj.payload.id).toBe('UI-1');
66
- });
67
-
68
- test('label-remove runs bd and replies with show', async () => {
69
- const rb = /** @type {import('vitest').Mock} */ (runBd);
70
- const rj = /** @type {import('vitest').Mock} */ (runBdJson);
71
- rb.mockResolvedValueOnce({ code: 0, stdout: '', stderr: '' });
72
- rj.mockResolvedValueOnce({
73
- code: 0,
74
- stdoutJson: { id: 'UI-1', labels: [] }
75
- });
76
-
77
- const ws = makeStubSocket();
78
- await handleMessage(
79
- /** @type {any} */ (ws),
80
- Buffer.from(
81
- JSON.stringify({
82
- id: 'b',
83
- type: /** @type {any} */ ('label-remove'),
84
- payload: { id: 'UI-1', label: 'frontend' }
85
- })
86
- )
87
- );
88
-
89
- const call = rb.mock.calls[rb.mock.calls.length - 1][0];
90
- expect(call.slice(0, 3)).toEqual(['label', 'remove', 'UI-1']);
91
- const obj = JSON.parse(ws.sent[ws.sent.length - 1]);
92
- expect(obj.ok).toBe(true);
93
- expect(obj.payload && obj.payload.id).toBe('UI-1');
94
- });
95
- });