beads-ui 0.4.0 → 0.4.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.
- package/CHANGES.md +14 -0
- package/app/protocol.js +206 -0
- package/package.json +4 -2
- package/server/app.js +7 -5
- package/server/cli/daemon.js +2 -2
- package/server/config.js +1 -2
- package/server/ws.js +1 -1
- package/server/protocol.js +0 -3
package/CHANGES.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changes
|
|
2
2
|
|
|
3
|
+
## 0.4.2
|
|
4
|
+
|
|
5
|
+
- [`66e31ff`](https://github.com/mantoni/beads-ui/commit/66e31ff0e053f3691657ce1175fd9b02155ca699)
|
|
6
|
+
Fix pre-bundled app: Check for bundle instead of NODE_ENV (Maximilian Antoni)
|
|
7
|
+
|
|
8
|
+
_Released by [Maximilian Antoni](https://github.com/mantoni) on 2025-10-29._
|
|
9
|
+
|
|
10
|
+
## 0.4.1
|
|
11
|
+
|
|
12
|
+
- [`03d3477`](https://github.com/mantoni/beads-ui/commit/03d34774cd35bf03d142d2869633327cbe4902bd)
|
|
13
|
+
Fix missing protocol.js in bundle
|
|
14
|
+
|
|
15
|
+
_Released by [Maximilian Antoni](https://github.com/mantoni) on 2025-10-29._
|
|
16
|
+
|
|
3
17
|
## 0.4.0
|
|
4
18
|
|
|
5
19
|
- [`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.2",
|
|
4
4
|
"description": "Local UI for Beads — Collaborate on issues with your coding agent.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"preversion": "npm run all",
|
|
30
30
|
"version": "changes --commits --footer",
|
|
31
31
|
"postversion": "git push --follow-tags && npm publish",
|
|
32
|
-
"prepack": "
|
|
32
|
+
"prepack": "npm run build",
|
|
33
|
+
"postpack": "rm app/main.bundle.js app/main.bundle.js.map"
|
|
33
34
|
},
|
|
34
35
|
"dependencies": {
|
|
35
36
|
"debug": "^4.4.3",
|
|
@@ -63,6 +64,7 @@
|
|
|
63
64
|
"app/styles.css",
|
|
64
65
|
"app/main.bundle.js",
|
|
65
66
|
"app/main.bundle.js.map",
|
|
67
|
+
"app/protocol.js",
|
|
66
68
|
"bin",
|
|
67
69
|
"server",
|
|
68
70
|
"CHANGES.md",
|
package/server/app.js
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
* @import { Express, Request, Response } from 'express'
|
|
3
3
|
*/
|
|
4
4
|
import express from 'express';
|
|
5
|
+
import fs from 'node:fs';
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Create and configure the Express application.
|
|
9
10
|
*
|
|
10
|
-
* @param {{ host: string, port: number,
|
|
11
|
+
* @param {{ host: string, port: number, app_dir: string, root_dir: string }} config - Server configuration.
|
|
11
12
|
* @returns {Express} Configured Express app instance.
|
|
12
13
|
*/
|
|
13
14
|
export function createApp(config) {
|
|
@@ -26,12 +27,13 @@ export function createApp(config) {
|
|
|
26
27
|
res.status(200).send({ ok: true });
|
|
27
28
|
});
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
if (
|
|
31
|
+
!fs.statSync(path.resolve(config.app_dir, 'main.bundle.js'), {
|
|
32
|
+
throwIfNoEntry: false
|
|
33
|
+
})
|
|
34
|
+
) {
|
|
32
35
|
/**
|
|
33
36
|
* On-demand bundle for the browser using esbuild.
|
|
34
|
-
* Note: esbuild is loaded lazily so tests don't require it to be installed.
|
|
35
37
|
*
|
|
36
38
|
* @param {Request} _req
|
|
37
39
|
* @param {Response} res
|
package/server/cli/daemon.js
CHANGED
|
@@ -256,6 +256,6 @@ export function printServerUrl() {
|
|
|
256
256
|
`beads db ${resolved_db.path} (${resolved_db.source}${resolved_db.exists ? '' : ', missing'})`
|
|
257
257
|
);
|
|
258
258
|
|
|
259
|
-
const { url
|
|
260
|
-
console.log(`beads ui listening on ${url}
|
|
259
|
+
const { url } = getConfig();
|
|
260
|
+
console.log(`beads ui listening on ${url}`);
|
|
261
261
|
}
|
package/server/config.js
CHANGED
|
@@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
9
9
|
* (i.e., the current working directory) so DB resolution follows the
|
|
10
10
|
* caller's context rather than the install location.
|
|
11
11
|
*
|
|
12
|
-
* @returns {{ host: string, port: number,
|
|
12
|
+
* @returns {{ host: string, port: number, app_dir: string, root_dir: string, url: string }}
|
|
13
13
|
*/
|
|
14
14
|
export function getConfig() {
|
|
15
15
|
const this_file = fileURLToPath(new URL(import.meta.url));
|
|
@@ -28,7 +28,6 @@ export function getConfig() {
|
|
|
28
28
|
return {
|
|
29
29
|
host: host_value,
|
|
30
30
|
port: port_value,
|
|
31
|
-
env: process.env.NODE_ENV ? String(process.env.NODE_ENV) : 'development',
|
|
32
31
|
app_dir: path.resolve(package_root, 'app'),
|
|
33
32
|
root_dir,
|
|
34
33
|
url: `http://${host_value}:${port_value}`
|
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