@polderlabs/bizar 2.6.1 → 3.0.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.
Files changed (43) hide show
  1. package/cli/bin.mjs +158 -130
  2. package/cli/plan.test.mjs +2331 -0
  3. package/cli/service.mjs +309 -0
  4. package/package.json +19 -27
  5. package/cli/dashboard/api.mjs +0 -473
  6. package/cli/dashboard/browser.mjs +0 -40
  7. package/cli/dashboard/server.mjs +0 -366
  8. package/cli/dashboard/state.mjs +0 -438
  9. package/cli/dashboard/tasks-store.mjs +0 -203
  10. package/cli/dashboard/watcher.mjs +0 -81
  11. package/cli/dashboard.mjs +0 -97
  12. package/dist/assets/index-BVvY22Gt.css +0 -1
  13. package/dist/assets/index-CO3c8O32.js +0 -285
  14. package/dist/assets/index-CO3c8O32.js.map +0 -1
  15. package/dist/index.html +0 -18
  16. package/src/App.tsx +0 -233
  17. package/src/components/Button.tsx +0 -55
  18. package/src/components/Card.tsx +0 -40
  19. package/src/components/EmptyState.tsx +0 -30
  20. package/src/components/Modal.tsx +0 -137
  21. package/src/components/Spinner.tsx +0 -19
  22. package/src/components/StatusBadge.tsx +0 -25
  23. package/src/components/Tag.tsx +0 -28
  24. package/src/components/Toast.tsx +0 -142
  25. package/src/components/Topbar.tsx +0 -88
  26. package/src/index.html +0 -17
  27. package/src/lib/api.ts +0 -71
  28. package/src/lib/markdown.tsx +0 -59
  29. package/src/lib/types.ts +0 -200
  30. package/src/lib/utils.ts +0 -79
  31. package/src/lib/ws.ts +0 -132
  32. package/src/main.tsx +0 -12
  33. package/src/styles/main.css +0 -2324
  34. package/src/views/Agents.tsx +0 -199
  35. package/src/views/Chat.tsx +0 -255
  36. package/src/views/Config.tsx +0 -250
  37. package/src/views/Overview.tsx +0 -267
  38. package/src/views/Plans.tsx +0 -667
  39. package/src/views/Projects.tsx +0 -155
  40. package/src/views/Settings.tsx +0 -253
  41. package/src/views/Tasks.tsx +0 -567
  42. package/tsconfig.json +0 -23
  43. package/vite.config.ts +0 -24
@@ -1,366 +0,0 @@
1
- /**
2
- * cli/dashboard/server.mjs
3
- *
4
- * Wires the API router, the file watcher, the WebSocket layer, and the
5
- * static frontend into a single Express + http.Server pair.
6
- *
7
- * Lifecycle:
8
- * createServer() → returns handles but does NOT listen
9
- * server.listen(...) → caller (cli/dashboard.mjs or tests) starts it
10
- * close() → stops watcher, closes wss, closes http server
11
- *
12
- * v2.6.0: serves the Vite-built React SPA from `dist/` instead of the
13
- * vanilla-JS `dashboard/` directory.
14
- */
15
- import express from 'express';
16
- import { WebSocketServer } from 'ws';
17
- import { createServer as createHttpServer } from 'node:http';
18
- import { fileURLToPath } from 'node:url';
19
- import { dirname, join } from 'node:path';
20
- import { existsSync, readdirSync } from 'node:fs';
21
- import { createApiRouter } from './api.mjs';
22
- import { createState } from './state.mjs';
23
- import { createWatcher } from './watcher.mjs';
24
-
25
- const __dirname = dirname(fileURLToPath(import.meta.url));
26
- // cli/dashboard/server.mjs → repo root is two levels up
27
- const DIST_DIR = join(__dirname, '..', '..', 'dist');
28
-
29
- /**
30
- * @param {object} opts
31
- * @param {number} opts.port - requested port (caller chooses after findFreePort)
32
- * @param {string} opts.projectRoot
33
- * @param {string} opts.opencodeConfigDir
34
- * @param {string} opts.bizarRoot
35
- */
36
- export function createServer({
37
- port,
38
- projectRoot,
39
- opencodeConfigDir,
40
- bizarRoot,
41
- }) {
42
- const app = express();
43
- app.use(express.json({ limit: '1mb' }));
44
-
45
- // JSON parse errors must come back as JSON, not Express's HTML page.
46
- // body-parser emits the error on `app`, so we attach here rather than
47
- // on the router.
48
- app.use(
49
- (
50
- err,
51
- _req,
52
- res,
53
- _next, // eslint-disable-line no-unused-vars
54
- ) => {
55
- const status = err?.status || err?.statusCode || 400;
56
- res.status(status).json({
57
- error: err?.type || 'bad_request',
58
- message: err?.message || String(err),
59
- });
60
- },
61
- );
62
-
63
- // State + watcher are created once for the lifetime of the server
64
- const state = createState({ projectRoot, opencodeConfigDir, bizarRoot });
65
-
66
- const watchPaths = [
67
- state.paths.opencodeJson,
68
- state.paths.agentsDir,
69
- state.paths.commandsDir,
70
- state.paths.bizarDir,
71
- state.paths.plansDir,
72
- state.paths.globalPlansDir,
73
- ].filter((p) => existsSync(p) || safeCanCreate(p));
74
-
75
- const watcher = createWatcher({
76
- paths: watchPaths,
77
- onChange: (event, p) => {
78
- // Broadcast to every live WS client
79
- wss.clients.forEach((client) => {
80
- if (client.readyState === 1) {
81
- try {
82
- client.send(
83
- JSON.stringify({
84
- type: 'change',
85
- event,
86
- path: p,
87
- ts: Date.now(),
88
- }),
89
- );
90
- } catch {
91
- /* dropped — client likely disconnected */
92
- }
93
- }
94
- });
95
- },
96
- });
97
-
98
- // HTTP + WS server pair so they can share a port
99
- const server = createHttpServer(app);
100
- const wss = new WebSocketServer({ server, path: '/ws' });
101
-
102
- /** Broadcast a message to all connected WS clients. */
103
- function broadcast(msg) {
104
- const payload = JSON.stringify(msg);
105
- wss.clients.forEach((client) => {
106
- if (client.readyState === 1) {
107
- try {
108
- client.send(payload);
109
- } catch {
110
- /* dropped */
111
- }
112
- }
113
- });
114
- }
115
-
116
- // Routes
117
- app.use(
118
- '/api',
119
- createApiRouter({
120
- state,
121
- watcher,
122
- projectRoot,
123
- opencodeConfigDir,
124
- bizarRoot,
125
- broadcast,
126
- }),
127
- );
128
-
129
- // ── Static frontend (React SPA in dist/) ─────────────────────────
130
- // If dist/ hasn't been built yet, serve a helpful message instead of 404.
131
- const distBuilt =
132
- existsSync(DIST_DIR) &&
133
- existsSync(join(DIST_DIR, 'index.html'));
134
-
135
- if (distBuilt) {
136
- // Serve hashed assets under /assets/ explicitly so they cache aggressively
137
- const assetsDir = join(DIST_DIR, 'assets');
138
- if (existsSync(assetsDir)) {
139
- app.use(
140
- '/assets',
141
- express.static(assetsDir, {
142
- maxAge: '1y',
143
- immutable: true,
144
- index: false,
145
- }),
146
- );
147
- }
148
- app.use(
149
- express.static(DIST_DIR, {
150
- extensions: ['html'],
151
- // index.html should NOT be cached aggressively (it references hashed bundles)
152
- setHeaders: (res, filePath) => {
153
- if (filePath.endsWith('index.html')) {
154
- res.setHeader('Cache-Control', 'no-cache');
155
- }
156
- },
157
- }),
158
- );
159
- // SPA fallback — any unmatched non-API route gets the index.
160
- app.get('*', (req, res, next) => {
161
- if (req.path.startsWith('/api') || req.path === '/ws') return next();
162
- res.sendFile(join(DIST_DIR, 'index.html'));
163
- });
164
- } else {
165
- // No build yet — tell the user what to do.
166
- app.get('/', (_req, res) => {
167
- res
168
- .status(503)
169
- .type('html')
170
- .send(renderNotBuiltPage());
171
- });
172
- app.get('*', (_req, res) => {
173
- if (_req.path.startsWith('/api') || _req.path === '/ws') {
174
- res.status(404).json({
175
- error: 'not_found',
176
- message: `no route for ${_req.method} ${_req.originalUrl}`,
177
- });
178
- return;
179
- }
180
- res.status(503).type('html').send(renderNotBuiltPage());
181
- });
182
- // eslint-disable-next-line no-console
183
- console.warn(
184
- `[dashboard] dist/ not found at ${DIST_DIR}. ` +
185
- `Run \`npm run build\` from the repo root to build the React SPA.`,
186
- );
187
- }
188
-
189
- // WebSocket handshake — send initial snapshot, accept simple commands
190
- wss.on('connection', (ws) => {
191
- try {
192
- ws.send(
193
- JSON.stringify({
194
- type: 'snapshot',
195
- ts: Date.now(),
196
- data: {
197
- overview: state.getOverview(),
198
- agents: state.getAgents(),
199
- plans: state.getPlans(),
200
- projects: state.getProjects(),
201
- config: state.getConfig(),
202
- settings: state.getSettings(),
203
- tasks: state.getTasks(),
204
- },
205
- }),
206
- );
207
- } catch {
208
- /* ignore */
209
- }
210
-
211
- ws.on('message', (raw) => {
212
- let msg;
213
- try {
214
- msg = JSON.parse(raw.toString());
215
- } catch {
216
- return; // ignore malformed
217
- }
218
- switch (msg?.type) {
219
- case 'ping':
220
- try {
221
- ws.send(JSON.stringify({ type: 'pong', ts: Date.now() }));
222
- } catch {
223
- /* ignore */
224
- }
225
- break;
226
- case 'refresh':
227
- try {
228
- ws.send(
229
- JSON.stringify({
230
- type: 'snapshot',
231
- ts: Date.now(),
232
- data: {
233
- overview: state.getOverview(),
234
- agents: state.getAgents(),
235
- plans: state.getPlans(),
236
- projects: state.getProjects(),
237
- config: state.getConfig(),
238
- settings: state.getSettings(),
239
- tasks: state.getTasks(),
240
- },
241
- }),
242
- );
243
- } catch {
244
- /* ignore */
245
- }
246
- break;
247
- case 'dismiss-notification':
248
- // No-op for now — UI keeps a local dismiss list
249
- break;
250
- default:
251
- // Unknown type — silently ignore
252
- break;
253
- }
254
- });
255
- });
256
-
257
- watcher.start();
258
-
259
- function close() {
260
- try {
261
- watcher.stop();
262
- } catch {
263
- /* ignore */
264
- }
265
- try {
266
- wss.clients.forEach((c) => c.terminate());
267
- wss.close();
268
- } catch {
269
- /* ignore */
270
- }
271
- try {
272
- server.close();
273
- } catch {
274
- /* ignore */
275
- }
276
- }
277
-
278
- return {
279
- app,
280
- server,
281
- wss,
282
- state,
283
- watcher,
284
- port,
285
- close,
286
- };
287
- }
288
-
289
- function safeCanCreate(p) {
290
- // chokidar accepts both existing and not-yet-existing paths, but it logs
291
- // noisily on the latter. We treat anything inside an existing parent as
292
- // watchable.
293
- try {
294
- return existsSync(dirname(p));
295
- } catch {
296
- return false;
297
- }
298
- }
299
-
300
- function renderNotBuiltPage() {
301
- return `<!DOCTYPE html>
302
- <html lang="en">
303
- <head>
304
- <meta charset="UTF-8" />
305
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
306
- <title>Bizar Dashboard — not built</title>
307
- <style>
308
- :root { color-scheme: dark; }
309
- body {
310
- margin: 0;
311
- min-height: 100vh;
312
- display: flex;
313
- align-items: center;
314
- justify-content: center;
315
- background: #0b0e14;
316
- color: #c9d1d9;
317
- font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
318
- padding: 24px;
319
- }
320
- .card {
321
- max-width: 560px;
322
- background: #12161f;
323
- border: 1px solid #f87171;
324
- border-radius: 12px;
325
- padding: 32px;
326
- box-shadow: 0 12px 32px rgba(0,0,0,0.5);
327
- }
328
- h1 { margin: 0 0 12px; color: #f87171; font-size: 22px; }
329
- p { margin: 0 0 12px; line-height: 1.6; }
330
- code {
331
- font-family: 'JetBrains Mono', monospace;
332
- background: #1a1f2b;
333
- border: 1px solid #232a39;
334
- padding: 2px 6px;
335
- border-radius: 4px;
336
- font-size: 13px;
337
- }
338
- pre {
339
- background: #1a1f2b;
340
- border: 1px solid #232a39;
341
- padding: 12px 16px;
342
- border-radius: 8px;
343
- font-family: 'JetBrains Mono', monospace;
344
- font-size: 13px;
345
- overflow-x: auto;
346
- }
347
- </style>
348
- </head>
349
- <body>
350
- <div class="card">
351
- <h1>Dashboard not built</h1>
352
- <p>The React SPA has not been built yet. The Bizar dashboard server
353
- is running, but the frontend bundle is missing.</p>
354
- <p>Build the React + Vite frontend from the repo root:</p>
355
- <pre><code>npm run build</code></pre>
356
- <p>Then restart this server (<code>bizar dashboard start</code>).
357
- For local development with hot reload, run <code>npm run dev</code>
358
- from a second terminal and open the URL Vite prints.</p>
359
- <p>The REST API and WebSocket are still live at
360
- <code>/api/*</code> and <code>/ws</code>.</p>
361
- </div>
362
- </body>
363
- </html>`;
364
- }
365
-
366
- export { DIST_DIR, renderNotBuiltPage };