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 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)
@@ -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.0",
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": "NODE_ENV=production npm run build"
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, env: string, app_dir: string, root_dir: string }} config - Server configuration.
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
- // In development, support on-demand bundling for a smooth DX.
30
- // In production, we expect `app/main.bundle.js` to be pre-built and served statically.
31
- if (config.env !== 'production') {
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
@@ -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, env } = getConfig();
260
- console.log(`beads ui listening on ${url} (${env})`);
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, env: string, app_dir: string, root_dir: string, url: string }}
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
 
@@ -1,3 +0,0 @@
1
- // Server-side access to protocol utilities and constants.
2
- // Import from the shared client definition to ensure a single source of truth.
3
- export * from '../app/protocol.js';