beads-ui 0.4.0 → 0.4.1
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 +7 -0
- package/app/protocol.js +206 -0
- package/package.json +2 -1
- package/server/ws.js +1 -1
- package/server/protocol.js +0 -3
package/CHANGES.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changes
|
|
2
2
|
|
|
3
|
+
## 0.4.1
|
|
4
|
+
|
|
5
|
+
- [`03d3477`](https://github.com/mantoni/beads-ui/commit/03d34774cd35bf03d142d2869633327cbe4902bd)
|
|
6
|
+
Fix missing protocol.js in bundle
|
|
7
|
+
|
|
8
|
+
_Released by [Maximilian Antoni](https://github.com/mantoni) on 2025-10-29._
|
|
9
|
+
|
|
3
10
|
## 0.4.0
|
|
4
11
|
|
|
5
12
|
- [`20a787c`](https://github.com/mantoni/beads-ui/commit/20a787c248225b4959b18b703894daf483f380b6)
|
package/app/protocol.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protocol definitions for beads-ui WebSocket communication.
|
|
3
|
+
*
|
|
4
|
+
* Conventions
|
|
5
|
+
* - All messages are JSON objects.
|
|
6
|
+
* - Client → Server uses RequestEnvelope.
|
|
7
|
+
* - Server → Client uses ReplyEnvelope.
|
|
8
|
+
* - Every request is correlated by `id` in replies.
|
|
9
|
+
* - Server can also send unsolicited events (e.g., subscription `snapshot`).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** @typedef {'list-issues'|'update-status'|'edit-text'|'update-priority'|'create-issue'|'list-ready'|'dep-add'|'dep-remove'|'epic-status'|'update-assignee'|'label-add'|'label-remove'|'subscribe-list'|'unsubscribe-list'|'snapshot'|'upsert'|'delete'} MessageType */
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} RequestEnvelope
|
|
16
|
+
* @property {string} id - Unique id to correlate request/response.
|
|
17
|
+
* @property {MessageType} type - Message type.
|
|
18
|
+
* @property {unknown} [payload] - Message payload.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} ErrorObject
|
|
23
|
+
* @property {string} code - Stable error code.
|
|
24
|
+
* @property {string} message - Human-readable message.
|
|
25
|
+
* @property {unknown} [details] - Optional extra info for debugging.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {Object} ReplyEnvelope
|
|
30
|
+
* @property {string} id - Correlates to the originating request.
|
|
31
|
+
* @property {boolean} ok - True when request succeeded; false on error.
|
|
32
|
+
* @property {MessageType} type - Echoes request type (or event type).
|
|
33
|
+
* @property {unknown} [payload] - Response payload.
|
|
34
|
+
* @property {ErrorObject} [error] - Present when ok=false.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/** @type {MessageType[]} */
|
|
38
|
+
export const MESSAGE_TYPES = /** @type {const} */ ([
|
|
39
|
+
'list-issues',
|
|
40
|
+
'update-status',
|
|
41
|
+
'edit-text',
|
|
42
|
+
'update-priority',
|
|
43
|
+
'create-issue',
|
|
44
|
+
'list-ready',
|
|
45
|
+
'dep-add',
|
|
46
|
+
'dep-remove',
|
|
47
|
+
'epic-status',
|
|
48
|
+
'update-assignee',
|
|
49
|
+
'label-add',
|
|
50
|
+
'label-remove',
|
|
51
|
+
'subscribe-list',
|
|
52
|
+
'unsubscribe-list',
|
|
53
|
+
// vNext per-subscription full-issue push events
|
|
54
|
+
'snapshot',
|
|
55
|
+
'upsert',
|
|
56
|
+
'delete'
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Generate a lexically sortable request id.
|
|
61
|
+
*
|
|
62
|
+
* @returns {string}
|
|
63
|
+
*/
|
|
64
|
+
export function nextId() {
|
|
65
|
+
const now = Date.now().toString(36);
|
|
66
|
+
const rand = Math.random().toString(36).slice(2, 8);
|
|
67
|
+
return `${now}-${rand}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Create a request envelope.
|
|
72
|
+
*
|
|
73
|
+
* @param {MessageType} type - Message type.
|
|
74
|
+
* @param {unknown} [payload] - Message payload.
|
|
75
|
+
* @param {string} [id] - Optional id; generated if omitted.
|
|
76
|
+
* @returns {RequestEnvelope}
|
|
77
|
+
*/
|
|
78
|
+
export function makeRequest(type, payload, id = nextId()) {
|
|
79
|
+
return { id, type, payload };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Create a successful reply envelope for a given request.
|
|
84
|
+
*
|
|
85
|
+
* @param {RequestEnvelope} req - Original request.
|
|
86
|
+
* @param {unknown} [payload] - Reply payload.
|
|
87
|
+
* @returns {ReplyEnvelope}
|
|
88
|
+
*/
|
|
89
|
+
export function makeOk(req, payload) {
|
|
90
|
+
return { id: req.id, ok: true, type: req.type, payload };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create an error reply envelope for a given request.
|
|
95
|
+
*
|
|
96
|
+
* @param {RequestEnvelope} req - Original request.
|
|
97
|
+
* @param {string} code
|
|
98
|
+
* @param {string} message
|
|
99
|
+
* @param {unknown} [details]
|
|
100
|
+
* @returns {ReplyEnvelope}
|
|
101
|
+
*/
|
|
102
|
+
export function makeError(req, code, message, details) {
|
|
103
|
+
return {
|
|
104
|
+
id: req.id,
|
|
105
|
+
ok: false,
|
|
106
|
+
type: req.type,
|
|
107
|
+
error: { code, message, details }
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check if a value is a plain object.
|
|
113
|
+
*
|
|
114
|
+
* @param {unknown} value
|
|
115
|
+
* @returns {value is Record<string, unknown>}
|
|
116
|
+
*/
|
|
117
|
+
function isRecord(value) {
|
|
118
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Type guard for MessageType values.
|
|
123
|
+
*
|
|
124
|
+
* @param {unknown} value
|
|
125
|
+
* @returns {value is MessageType}
|
|
126
|
+
*/
|
|
127
|
+
export function isMessageType(value) {
|
|
128
|
+
return (
|
|
129
|
+
typeof value === 'string' &&
|
|
130
|
+
MESSAGE_TYPES.includes(/** @type {MessageType} */ (value))
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Type guard for RequestEnvelope.
|
|
136
|
+
*
|
|
137
|
+
* @param {unknown} value
|
|
138
|
+
* @returns {value is RequestEnvelope}
|
|
139
|
+
*/
|
|
140
|
+
export function isRequest(value) {
|
|
141
|
+
if (!isRecord(value)) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
return (
|
|
145
|
+
typeof value.id === 'string' &&
|
|
146
|
+
typeof value.type === 'string' &&
|
|
147
|
+
(value.payload === undefined || 'payload' in value)
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Type guard for ReplyEnvelope.
|
|
153
|
+
*
|
|
154
|
+
* @param {unknown} value
|
|
155
|
+
* @returns {value is ReplyEnvelope}
|
|
156
|
+
*/
|
|
157
|
+
export function isReply(value) {
|
|
158
|
+
if (!isRecord(value)) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
if (
|
|
162
|
+
typeof value.id !== 'string' ||
|
|
163
|
+
typeof value.ok !== 'boolean' ||
|
|
164
|
+
!isMessageType(value.type)
|
|
165
|
+
) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
if (value.ok === false) {
|
|
169
|
+
const err = value.error;
|
|
170
|
+
if (
|
|
171
|
+
!isRecord(err) ||
|
|
172
|
+
typeof err.code !== 'string' ||
|
|
173
|
+
typeof err.message !== 'string'
|
|
174
|
+
) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Normalize and validate an incoming JSON value as a RequestEnvelope.
|
|
183
|
+
* Throws a user-friendly error if invalid.
|
|
184
|
+
*
|
|
185
|
+
* @param {unknown} json
|
|
186
|
+
* @returns {RequestEnvelope}
|
|
187
|
+
*/
|
|
188
|
+
export function decodeRequest(json) {
|
|
189
|
+
if (!isRequest(json)) {
|
|
190
|
+
throw new Error('Invalid request envelope');
|
|
191
|
+
}
|
|
192
|
+
return json;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Normalize and validate an incoming JSON value as a ReplyEnvelope.
|
|
197
|
+
*
|
|
198
|
+
* @param {unknown} json
|
|
199
|
+
* @returns {ReplyEnvelope}
|
|
200
|
+
*/
|
|
201
|
+
export function decodeReply(json) {
|
|
202
|
+
if (!isReply(json)) {
|
|
203
|
+
throw new Error('Invalid reply envelope');
|
|
204
|
+
}
|
|
205
|
+
return json;
|
|
206
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beads-ui",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Local UI for Beads — Collaborate on issues with your coding agent.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
"app/styles.css",
|
|
64
64
|
"app/main.bundle.js",
|
|
65
65
|
"app/main.bundle.js.map",
|
|
66
|
+
"app/protocol.js",
|
|
66
67
|
"bin",
|
|
67
68
|
"server",
|
|
68
69
|
"CHANGES.md",
|
package/server/ws.js
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* @import { MessageType } from '../app/protocol.js'
|
|
5
5
|
*/
|
|
6
6
|
import { WebSocketServer } from 'ws';
|
|
7
|
+
import { isRequest, makeError, makeOk } from '../app/protocol.js';
|
|
7
8
|
import { runBd, runBdJson } from './bd.js';
|
|
8
9
|
import { fetchListForSubscription } from './list-adapters.js';
|
|
9
10
|
import { debug } from './logging.js';
|
|
10
|
-
import { isRequest, makeError, makeOk } from './protocol.js';
|
|
11
11
|
import { keyOf, registry } from './subscriptions.js';
|
|
12
12
|
import { validateSubscribeListPayload } from './validators.js';
|
|
13
13
|
|
package/server/protocol.js
DELETED