bitwrench 2.0.31 → 2.1.0

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 (126) hide show
  1. package/README.md +210 -125
  2. package/dist/bitwrench-bccl.cjs.js +349 -188
  3. package/dist/bitwrench-bccl.cjs.min.js +2 -39
  4. package/dist/bitwrench-bccl.cjs.min.js.gz +0 -0
  5. package/dist/bitwrench-bccl.esm.js +349 -188
  6. package/dist/bitwrench-bccl.esm.min.js +2 -39
  7. package/dist/bitwrench-bccl.esm.min.js.gz +0 -0
  8. package/dist/bitwrench-bccl.umd.js +349 -188
  9. package/dist/bitwrench-bccl.umd.min.js +2 -39
  10. package/dist/bitwrench-bccl.umd.min.js.gz +0 -0
  11. package/dist/bitwrench-code-edit.cjs.js +17 -6
  12. package/dist/bitwrench-code-edit.cjs.min.js +2 -20
  13. package/dist/bitwrench-code-edit.es5.js +8 -3
  14. package/dist/bitwrench-code-edit.es5.min.js +2 -19
  15. package/dist/bitwrench-code-edit.esm.js +17 -6
  16. package/dist/bitwrench-code-edit.esm.min.js +2 -19
  17. package/dist/bitwrench-code-edit.umd.js +17 -6
  18. package/dist/bitwrench-code-edit.umd.min.js +2 -19
  19. package/dist/bitwrench-code-edit.umd.min.js.gz +0 -0
  20. package/dist/bitwrench-debug.js +1 -1
  21. package/dist/bitwrench-debug.min.js +1 -1
  22. package/dist/bitwrench-lean.cjs.js +2492 -1628
  23. package/dist/bitwrench-lean.cjs.min.js +2 -80
  24. package/dist/bitwrench-lean.cjs.min.js.gz +0 -0
  25. package/dist/bitwrench-lean.es5.js +2740 -1838
  26. package/dist/bitwrench-lean.es5.min.js +2 -49
  27. package/dist/bitwrench-lean.es5.min.js.gz +0 -0
  28. package/dist/bitwrench-lean.esm.js +2492 -1628
  29. package/dist/bitwrench-lean.esm.min.js +2 -80
  30. package/dist/bitwrench-lean.esm.min.js.gz +0 -0
  31. package/dist/bitwrench-lean.umd.js +2492 -1628
  32. package/dist/bitwrench-lean.umd.min.js +2 -80
  33. package/dist/bitwrench-lean.umd.min.js.gz +0 -0
  34. package/dist/bitwrench-util-color.cjs.js +251 -0
  35. package/dist/bitwrench-util-color.cjs.min.js +3 -0
  36. package/dist/bitwrench-util-color.es5.js +256 -0
  37. package/dist/bitwrench-util-color.es5.min.js +3 -0
  38. package/dist/bitwrench-util-color.esm.js +241 -0
  39. package/dist/bitwrench-util-color.esm.min.js +3 -0
  40. package/dist/bitwrench-util-color.umd.js +257 -0
  41. package/dist/bitwrench-util-color.umd.min.js +3 -0
  42. package/dist/bitwrench-util-css.cjs.js +2 -1
  43. package/dist/bitwrench-util-css.cjs.min.js +2 -21
  44. package/dist/bitwrench-util-css.es5.js +2 -1
  45. package/dist/bitwrench-util-css.es5.min.js +2 -20
  46. package/dist/bitwrench-util-css.esm.js +2 -1
  47. package/dist/bitwrench-util-css.esm.min.js +1 -19
  48. package/dist/bitwrench-util-css.umd.js +2 -1
  49. package/dist/bitwrench-util-css.umd.min.js +2 -20
  50. package/dist/bitwrench-util-css.umd.min.js.gz +0 -0
  51. package/dist/bitwrench.cjs.js +2826 -1801
  52. package/dist/bitwrench.cjs.min.js +2 -99
  53. package/dist/bitwrench.cjs.min.js.gz +0 -0
  54. package/dist/bitwrench.css +403 -479
  55. package/dist/bitwrench.d.ts +70 -73
  56. package/dist/bitwrench.es5.js +3106 -2020
  57. package/dist/bitwrench.es5.min.js +2 -67
  58. package/dist/bitwrench.es5.min.js.gz +0 -0
  59. package/dist/bitwrench.esm.js +2826 -1801
  60. package/dist/bitwrench.esm.min.js +2 -99
  61. package/dist/bitwrench.esm.min.js.gz +0 -0
  62. package/dist/bitwrench.min.css +1 -1
  63. package/dist/bitwrench.umd.js +2826 -1801
  64. package/dist/bitwrench.umd.min.js +2 -99
  65. package/dist/bitwrench.umd.min.js.gz +0 -0
  66. package/dist/builds.json +222 -134
  67. package/dist/bwserve.cjs.js +308 -298
  68. package/dist/bwserve.d.ts +157 -0
  69. package/dist/bwserve.esm.js +309 -299
  70. package/dist/sri.json +54 -46
  71. package/docs/README.md +6 -3
  72. package/docs/app-patterns.md +7 -6
  73. package/docs/bitwrench-for-wasm.md +53 -54
  74. package/docs/bitwrench-mcp.md +2 -2
  75. package/docs/bitwrench-northstar-principles.md +406 -0
  76. package/docs/bitwrench-taco-schema-discussion.md +2 -2
  77. package/docs/bitwrench_api.md +191 -106
  78. package/docs/bitwrench_typescript_usage.md +5 -5
  79. package/docs/bw-attach.md +29 -75
  80. package/docs/bwserve.md +200 -168
  81. package/docs/cli.md +36 -12
  82. package/docs/component-cheatsheet.md +2 -2
  83. package/docs/component-library.md +4 -4
  84. package/docs/component-lifecycle.md +234 -0
  85. package/docs/drift-lint.md +268 -0
  86. package/docs/framework-translation-table.md +4 -4
  87. package/docs/llm-bitwrench-guide.md +60 -50
  88. package/docs/routing.md +11 -13
  89. package/docs/state-management.md +110 -109
  90. package/docs/taco-format.md +13 -14
  91. package/docs/theming.md +13 -3
  92. package/docs/thinking-in-bitwrench.md +858 -983
  93. package/docs/tutorial-bwserve.md +37 -36
  94. package/docs/tutorial-embedded.md +10 -21
  95. package/docs/tutorial-website.md +2 -2
  96. package/package.json +38 -7
  97. package/readme.html +262 -160
  98. package/src/bitwrench-bccl-entry.js +2 -2
  99. package/src/bitwrench-bccl.js +346 -185
  100. package/src/bitwrench-code-edit.js +16 -5
  101. package/src/bitwrench-color-utils.js +117 -181
  102. package/src/bitwrench-file-ops.js +2 -2
  103. package/src/bitwrench-lean.js +4 -3
  104. package/src/bitwrench-router.js +5 -2
  105. package/src/bitwrench-styles.js +420 -504
  106. package/src/bitwrench-util-color.js +240 -0
  107. package/src/bitwrench-util-css.js +1 -0
  108. package/src/bitwrench-utils.js +4 -0
  109. package/src/bitwrench.d.ts +70 -73
  110. package/src/bitwrench.h +5 -0
  111. package/src/bitwrench.js +1939 -933
  112. package/src/bwserve/attach.js +0 -1
  113. package/src/bwserve/bwclient.js +172 -32
  114. package/src/bwserve/bwshell.js +0 -4
  115. package/src/bwserve/client.js +59 -220
  116. package/src/bwserve/index.js +78 -42
  117. package/src/bwserve.d.ts +157 -0
  118. package/src/bwserve.h +5 -0
  119. package/src/cli/attach.js +12 -75
  120. package/src/cli/convert.js +2 -2
  121. package/src/cli/serve.js +37 -35
  122. package/src/generate-css.js +1 -1
  123. package/src/mcp/knowledge.js +4 -4
  124. package/src/mcp/live.js +21 -13
  125. package/src/mcp/tools.js +0 -1
  126. package/src/version.js +3 -7
@@ -9,7 +9,7 @@
9
9
  * import bwserve from 'bitwrench/bwserve';
10
10
  * const app = bwserve.create({ port: 7902 });
11
11
  * app.page('/', (client) => {
12
- * client.render('#app', bw.makeCard({ title: 'Hello' }));
12
+ * client.mount('#app', bw.makeCard({ title: 'Hello' }));
13
13
  * });
14
14
  * app.listen();
15
15
  *
@@ -23,7 +23,7 @@ import { VERSION } from '../version.js';
23
23
 
24
24
  // Resolve dist/ paths relative to the package root
25
25
  import { fileURLToPath } from 'url';
26
- import { dirname, resolve, join, extname } from 'path';
26
+ import { dirname, resolve, join, extname, sep } from 'path';
27
27
  import { createServer } from 'http';
28
28
  import { readFileSync, readdirSync, existsSync, statSync } from 'fs';
29
29
 
@@ -32,9 +32,11 @@ var __dirname = dirname(fileURLToPath(import.meta.url));
32
32
  // Resolve dist/ — try source layout (src/bwserve/), then npm install layout,
33
33
  // then dist/ itself (when running from dist/bwserve.esm.js)
34
34
  var DIST_DIR = resolve(__dirname, '..', '..', 'dist');
35
+ /* c8 ignore next 3 -- DIST_DIR fallback at module load; only triggers in npm install layout */
35
36
  if (!existsSync(DIST_DIR)) {
36
37
  DIST_DIR = resolve(__dirname, '..', 'dist');
37
38
  }
39
+ /* c8 ignore next 3 -- DIST_DIR fallback at module load; only triggers when no dist/ exists */
38
40
  if (!existsSync(DIST_DIR)) {
39
41
  DIST_DIR = __dirname;
40
42
  }
@@ -96,15 +98,14 @@ export function create(opts) {
96
98
  */
97
99
  class BwServeApp {
98
100
  constructor(opts) {
99
- this.port = opts.port || 7902;
101
+ this.port = opts.port != null ? opts.port : 7902;
100
102
  this.title = opts.title || 'bwserve';
101
103
  this.staticDir = opts.static || null;
102
104
  this.injectBitwrench = opts.injectBitwrench !== false;
103
105
  this.theme = opts.theme || null;
104
- this.allowExec = opts.allowExec || false;
105
106
  this.allowScreenshot = opts.allowScreenshot || false;
106
107
  this.dirList = opts.dirList !== false;
107
- this.host = opts.host || '0.0.0.0';
108
+ this.host = opts.host || '127.0.0.1';
108
109
  this.keepAliveInterval = opts.keepAliveInterval || 15000;
109
110
  this._pages = new Map();
110
111
  this._clients = new Map();
@@ -139,6 +140,11 @@ class BwServeApp {
139
140
  });
140
141
 
141
142
  self._server.listen(self.port, self.host, function() {
143
+ // Update port to the actual bound port (important when port 0 is used)
144
+ var addr = self._server.address();
145
+ if (addr && addr.port) {
146
+ self.port = addr.port;
147
+ }
142
148
  if (callback) callback();
143
149
  res();
144
150
  });
@@ -197,9 +203,9 @@ class BwServeApp {
197
203
  return 0;
198
204
  }
199
205
  var count = 0;
200
- for (var record of this._clients.values()) {
201
- if (record.client && !record.client._closed) {
202
- record.client._send(msg);
206
+ for (var rec of this._clients.values()) {
207
+ if (rec.client && !rec.client._closed) {
208
+ rec.client._send(msg);
203
209
  count++;
204
210
  }
205
211
  }
@@ -218,6 +224,7 @@ class BwServeApp {
218
224
  var path = url.split('?')[0];
219
225
 
220
226
  // /bw/attach.js — self-contained attach script for remote debugging
227
+ /* c8 ignore next 3 -- covered in isolation; flaky in combined suite due to server state */
221
228
  if (path === '/bw/attach.js' && method === 'GET') {
222
229
  return this._serveAttachScript(req, res);
223
230
  }
@@ -244,6 +251,7 @@ class BwServeApp {
244
251
  }
245
252
 
246
253
  // CORS preflight for /bw/return/ (needed for cross-origin attach)
254
+ /* c8 ignore next 9 -- covered in isolation; flaky in combined suite */
247
255
  if (method === 'OPTIONS' && path.startsWith('/bw/return/')) {
248
256
  res.writeHead(204, {
249
257
  'Access-Control-Allow-Origin': '*',
@@ -258,6 +266,7 @@ class BwServeApp {
258
266
  if (method === 'POST' && path.startsWith('/bw/return/')) {
259
267
  var rest = path.slice('/bw/return/'.length);
260
268
  var slash = rest.indexOf('/');
269
+ /* c8 ignore next 4 -- covered in isolation; flaky in combined suite */
261
270
  if (slash === -1) {
262
271
  res.writeHead(400, { 'Content-Type': 'application/json' });
263
272
  res.end(JSON.stringify({ error: 'Invalid return path' }));
@@ -274,37 +283,30 @@ class BwServeApp {
274
283
  return this._serveVendorFile(res, vendorFile);
275
284
  }
276
285
 
277
- // Registered page routesserve shell HTML
278
- if (method === 'GET' && this._pages.has(path)) {
279
- var clientId2 = 'c' + (++this._clientCounter);
280
- var shell = generateShell({
281
- clientId: clientId2,
282
- title: this.title,
283
- theme: this.theme,
284
- injectBitwrench: this.injectBitwrench,
285
- allowExec: this.allowExec
286
- });
287
- // Store the page path for this client so SSE knows which handler to call
288
- this._clients.set(clientId2, { pagePath: path, client: null });
289
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
290
- res.end(shell);
291
- return;
292
- }
293
-
294
- // Static file serving
286
+ // Static file servingtakes priority over registered page handlers
287
+ // so that bwserve works as a drop-in static server (like python -m
288
+ // http.server or npx serve) with opt-in bwserve superpowers.
295
289
  if (method === 'GET' && this.staticDir) {
296
- var filePath = join(this.staticDir, path);
297
- if (existsSync(filePath) && statSync(filePath).isFile()) {
298
- var ext = extname(filePath);
290
+ // Path traversal guard: resolve to absolute and verify containment
291
+ var resolvedBase = resolve(this.staticDir);
292
+ var resolvedPath = resolve(resolvedBase, '.' + path);
293
+ if (resolvedPath !== resolvedBase && !resolvedPath.startsWith(resolvedBase + sep)) {
294
+ res.writeHead(403, { 'Content-Type': 'text/plain' });
295
+ res.end('Forbidden');
296
+ return;
297
+ }
298
+
299
+ if (existsSync(resolvedPath) && statSync(resolvedPath).isFile()) {
300
+ var ext = extname(resolvedPath);
299
301
  var mime = MIME_TYPES[ext] || 'application/octet-stream';
300
- var content = readFileSync(filePath);
302
+ var content = readFileSync(resolvedPath);
301
303
  res.writeHead(200, { 'Content-Type': mime });
302
304
  res.end(content);
303
305
  return;
304
306
  }
305
307
  // Directory index resolution: /foo/ => /foo/index.html
306
308
  if (path.endsWith('/')) {
307
- var indexPath = join(this.staticDir, path, 'index.html');
309
+ var indexPath = join(resolvedPath, 'index.html');
308
310
  if (existsSync(indexPath) && statSync(indexPath).isFile()) {
309
311
  var indexContent = readFileSync(indexPath);
310
312
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
@@ -312,16 +314,15 @@ class BwServeApp {
312
314
  return;
313
315
  }
314
316
  // Directory listing when no index.html
315
- var dirPath = join(this.staticDir, path);
316
- if (this.dirList && existsSync(dirPath) && statSync(dirPath).isDirectory()) {
317
- var listing = this._generateDirListing(path, dirPath);
317
+ if (this.dirList && existsSync(resolvedPath) && statSync(resolvedPath).isDirectory()) {
318
+ var listing = this._generateDirListing(path, resolvedPath);
318
319
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
319
320
  res.end(listing);
320
321
  return;
321
322
  }
322
323
  }
323
324
  // Bare directory without trailing slash: /foo => 301 to /foo/
324
- if (!path.endsWith('/') && existsSync(filePath) && statSync(filePath).isDirectory()) {
325
+ if (!path.endsWith('/') && existsSync(resolvedPath) && statSync(resolvedPath).isDirectory()) {
325
326
  var qs = url.split('?')[1];
326
327
  var location = path + '/' + (qs ? '?' + qs : '');
327
328
  res.writeHead(301, { 'Location': location });
@@ -330,6 +331,23 @@ class BwServeApp {
330
331
  }
331
332
  }
332
333
 
334
+ // Registered page routes — serve bwserve shell HTML (fallback when no
335
+ // static file matched, e.g. pipe/SSE driven pages)
336
+ if (method === 'GET' && this._pages.has(path)) {
337
+ var clientId2 = 'c' + (++this._clientCounter);
338
+ var shell = generateShell({
339
+ clientId: clientId2,
340
+ title: this.title,
341
+ theme: this.theme,
342
+ injectBitwrench: this.injectBitwrench
343
+ });
344
+ // Store the page path for this client so SSE knows which handler to call
345
+ this._clients.set(clientId2, { pagePath: path, client: null });
346
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
347
+ res.end(shell);
348
+ return;
349
+ }
350
+
333
351
  // 404
334
352
  res.writeHead(404, { 'Content-Type': 'text/plain' });
335
353
  res.end('Not Found');
@@ -380,9 +398,13 @@ class BwServeApp {
380
398
  var pagePath = pending ? pending.pagePath : '/';
381
399
  self._clients.set(clientId, { pagePath: pagePath, client: client });
382
400
 
401
+ // Send the handshake as the very first SSE event
402
+ client._send({ type: 'hello' });
403
+
383
404
  // Keep-alive: send SSE comment periodically
384
405
  var keepAlive = setInterval(function() {
385
406
  if (!client._closed) {
407
+ /* c8 ignore next -- keepalive write failure only on socket close */
386
408
  try { res.write(':keepalive\n\n'); } catch (e) { /* ignore */ }
387
409
  }
388
410
  }, self.keepAliveInterval);
@@ -394,11 +416,12 @@ class BwServeApp {
394
416
  self._clients.delete(clientId);
395
417
  });
396
418
 
397
- // Call the page handler
419
+ // Call the page handler (runs on every connection, including reconnects)
398
420
  var handler = self._pages.get(pagePath);
399
421
  if (handler) {
400
422
  try {
401
423
  handler(client);
424
+ /* c8 ignore next 3 -- page handler error catch; requires throwing handler in SSE context */
402
425
  } catch (e) {
403
426
  console.error('[bwserve] Page handler error:', e);
404
427
  }
@@ -411,8 +434,8 @@ class BwServeApp {
411
434
  *
412
435
  * Routes:
413
436
  * action — fire-and-forget action dispatch (no requestId)
414
- * queryresolve pending query promise
415
- * mountresolve pending mount promise
437
+ * eventevent dispatch from client listeners
438
+ * topictopic dispatch from client pub/sub
416
439
  * screenshot — resolve pending screenshot promise
417
440
  *
418
441
  * @private
@@ -430,18 +453,26 @@ class BwServeApp {
430
453
  req.on('end', function() {
431
454
  try {
432
455
  var data = JSON.parse(body);
433
- if (route === 'action' || route === 'event') {
456
+ if (route === 'topic') {
457
+ // Topic dispatch — forward to the listen handler registered on the client
458
+ var topic = data.topic;
459
+ var topicData = data.data;
460
+ record.client._dispatch('_topic:' + topic, topicData);
461
+ } else if (route === 'action' || route === 'event') {
434
462
  // Action/event dispatch (no requestId/pending pattern)
435
463
  var action = route === 'event'
436
464
  ? '_bw_event'
437
465
  : (data.result ? data.result.action : data.action);
466
+ /* c8 ignore next 3 -- data.data fallback; covered in isolation */
438
467
  var payload = route === 'event'
439
468
  ? (data.result || data)
440
469
  : (data.result ? data.result.data : data.data || data);
441
470
  record.client._dispatch(action, payload);
442
471
  } else {
443
- // All other routes: resolve pending promise
444
- record.client._resolvePending(data.requestId, data);
472
+ // All other routes: resolve pending promise if mechanism exists
473
+ if (record.client._resolvePending) {
474
+ record.client._resolvePending(data.requestId, data);
475
+ }
445
476
  }
446
477
  res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
447
478
  res.end(JSON.stringify({ ok: true }));
@@ -459,13 +490,18 @@ class BwServeApp {
459
490
  */
460
491
  _serveAttachScript(req, res) {
461
492
  try {
462
- var js = generateAttachScript({ origin: '' });
493
+ var h = req.headers || {};
494
+ var proto = (h['x-forwarded-proto'] || 'http');
495
+ var host = h['x-forwarded-host'] || h.host || '';
496
+ var origin = host ? (proto + '://' + host) : '';
497
+ var js = generateAttachScript({ origin: origin });
463
498
  res.writeHead(200, {
464
499
  'Content-Type': 'application/javascript; charset=utf-8',
465
500
  'Access-Control-Allow-Origin': '*',
466
501
  'Cache-Control': 'no-cache'
467
502
  });
468
503
  res.end(js);
504
+ /* c8 ignore next 4 -- generateAttachScript is a pure template; cannot throw */
469
505
  } catch (err) {
470
506
  res.writeHead(500, { 'Content-Type': 'text/plain' });
471
507
  res.end('Error generating attach script: ' + err.message);
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Type declarations for bitwrench/bwserve — server-driven UI over SSE.
3
+ */
4
+
5
+ import { IncomingMessage, ServerResponse } from 'http';
6
+
7
+ /** Options for creating a BwServeApp instance. */
8
+ export interface BwServeOptions {
9
+ /** Port to listen on (default: 7902) */
10
+ port?: number;
11
+ /** Page title (default: 'bwserve') */
12
+ title?: string;
13
+ /** Directory path for static file serving */
14
+ static?: string;
15
+ /** Auto-inject bitwrench client JS (default: true) */
16
+ injectBitwrench?: boolean;
17
+ /** Theme preset name or config object */
18
+ theme?: string | Record<string, string>;
19
+ /** Enable client.screenshot() (default: false) */
20
+ allowScreenshot?: boolean;
21
+ /** Enable directory listings (default: true) */
22
+ dirList?: boolean;
23
+ /** Host/address to bind to (default: '127.0.0.1') */
24
+ host?: string;
25
+ /** SSE keep-alive interval in ms (default: 15000) */
26
+ keepAliveInterval?: number;
27
+ }
28
+
29
+ /** Options for generateShell(). */
30
+ export interface ShellOptions {
31
+ /** Unique client ID for this connection */
32
+ clientId: string;
33
+ /** Page title (default: 'bwserve') */
34
+ title?: string;
35
+ /** Theme preset name or config object */
36
+ theme?: string | Record<string, string>;
37
+ /** Whether to inject bitwrench scripts (default: true) */
38
+ injectBitwrench?: boolean;
39
+ }
40
+
41
+ /** A TACO object — Tag, Attributes, Content, Options. */
42
+ export interface Taco {
43
+ t?: string;
44
+ a?: Record<string, any>;
45
+ c?: string | Taco | (string | Taco)[];
46
+ o?: Record<string, any>;
47
+ }
48
+
49
+ /** SSE protocol message. */
50
+ export interface BwServeMessage {
51
+ v?: number;
52
+ type?: string;
53
+ ref?: string;
54
+ taco?: Taco;
55
+ [key: string]: any;
56
+ }
57
+
58
+ /** Batch operation entry. */
59
+ export interface BatchOp {
60
+ type: string;
61
+ ref?: string;
62
+ taco?: Taco;
63
+ [key: string]: any;
64
+ }
65
+
66
+ /** Server-side client connection — represents one browser tab via SSE. */
67
+ export declare class BwServeClient {
68
+ /** bwserve version string */
69
+ static version: string;
70
+
71
+ /** Unique client identifier */
72
+ id: string;
73
+
74
+ constructor(id: string, res: ServerResponse);
75
+
76
+ /** Mount TACO at selector, replacing contents */
77
+ mount(selector: string, taco: Taco): void;
78
+
79
+ /** @deprecated Use mount() instead */
80
+ render(selector: string, taco: Taco): void;
81
+
82
+ /** Patch element properties without full rebuild */
83
+ patch(ref: string, fields: Record<string, any>): void;
84
+
85
+ /** Append TACO as child of target element */
86
+ append(selector: string, taco: Taco): void;
87
+
88
+ /** Remove element from DOM with cleanup */
89
+ remove(selector: string): void;
90
+
91
+ /** Send array of operations as single batch message */
92
+ batch(ops: BatchOp[]): void;
93
+
94
+ /** Dispatch to component's el.bw[action](data) */
95
+ message(ref: string, action: string, data?: any): void;
96
+
97
+ /** Call registered or built-in client function */
98
+ call(name: string, ...args: any[]): void;
99
+
100
+ /** Subscribe to client topic. Returns this for chaining. */
101
+ listen(topic: string, handler: (data: any) => void): BwServeClient;
102
+
103
+ /** Register handler for client action. Returns this for chaining. */
104
+ on(action: string, handler: (data: any, client: BwServeClient) => void): BwServeClient;
105
+
106
+ /** Close SSE connection */
107
+ close(): void;
108
+ }
109
+
110
+ /** Main bwserve application — HTTP + SSE server for server-driven UI. */
111
+ export declare class BwServeApp {
112
+ port: number;
113
+ title: string;
114
+ staticDir: string | null;
115
+ injectBitwrench: boolean;
116
+ theme: string | Record<string, string> | null;
117
+ allowScreenshot: boolean;
118
+ dirList: boolean;
119
+ host: string;
120
+ keepAliveInterval: number;
121
+
122
+ /** Number of active client connections */
123
+ readonly clientCount: number;
124
+
125
+ constructor(opts?: BwServeOptions);
126
+
127
+ /** Register a page handler. Returns this for chaining. */
128
+ page(path: string, handler: (client: BwServeClient) => void): BwServeApp;
129
+
130
+ /** Start HTTP server. Returns Promise that resolves when listening. */
131
+ listen(callback?: () => void): Promise<void>;
132
+
133
+ /** Stop server and close all client connections. */
134
+ close(): Promise<void>;
135
+
136
+ /** Send protocol message to all clients (or specific client if msg.clientId is set). Returns count of recipients. */
137
+ broadcast(msg: BwServeMessage): number;
138
+ }
139
+
140
+ /** Generate a complete HTML shell document for SSE connection. */
141
+ export declare function generateShell(opts?: ShellOptions): string;
142
+
143
+ /** Create a new BwServeApp instance. */
144
+ export declare function create(opts?: BwServeOptions): BwServeApp;
145
+
146
+ /** bwserve version string */
147
+ export declare const version: string;
148
+
149
+ /** Default export */
150
+ declare const bwserve: {
151
+ create: typeof create;
152
+ version: string;
153
+ BwServeApp: typeof BwServeApp;
154
+ BwServeClient: typeof BwServeClient;
155
+ generateShell: typeof generateShell;
156
+ };
157
+ export default bwserve;
package/src/bwserve.h ADDED
@@ -0,0 +1,5 @@
1
+ /*
2
+ * Shim header for the Arduino library layout ("recursive" layout adds only
3
+ * src/ to the include path). The real header lives in embedded_c/bwserve.h.
4
+ */
5
+ #include "../embedded_c/bwserve.h"
package/src/cli/attach.js CHANGED
@@ -29,8 +29,8 @@ Usage:
29
29
  Description:
30
30
  Starts a bwserve instance and waits for a browser to connect via the
31
31
  drop-in attach script. Once connected, you get an interactive REPL
32
- where you can evaluate JS expressions, inspect the DOM, take
33
- screenshots, and listen to events — all from your terminal.
32
+ for inspecting the DOM, mounting components, taking screenshots,
33
+ and listening to events — all from your terminal.
34
34
 
35
35
  To connect a page, either:
36
36
  1. Add <script src="http://localhost:<port>/bw/attach.js"></script>
@@ -44,7 +44,6 @@ Options:
44
44
  -h, --help Print this help
45
45
 
46
46
  REPL Commands:
47
- <expression> Evaluate JS in the connected browser (e.g., document.title)
48
47
  /help Show command reference
49
48
  /quit, /q Exit
50
49
  /inspect [selector] [depth] Show DOM tree summary (default: body, depth 3)
@@ -55,7 +54,6 @@ REPL Commands:
55
54
  /listen <sel> <event> Start listening for DOM events (e.g., /listen button click)
56
55
  /unlisten <sel> <event> Stop listening for a previously added listener
57
56
  /clients List connected clients
58
- /exec <code> Execute JS without capturing return value
59
57
 
60
58
  Examples:
61
59
  bwcli attach Start on default port 7902
@@ -64,8 +62,6 @@ Examples:
64
62
  bwcli attach -v Verbose mode (shows protocol)
65
63
 
66
64
  # In the REPL:
67
- bw> document.title
68
- bw> bw.$('.bw-card').length
69
65
  bw> /inspect #app 2
70
66
  bw> /screenshot body page.png
71
67
  bw> /listen .bw-btn click
@@ -80,21 +76,7 @@ Examples:
80
76
  bw> /listen .bw-btn click
81
77
  `.trim();
82
78
 
83
- /**
84
- * Wrap a JS expression for client.query().
85
- * Statements (var, let, const, if, for, etc.) are sent as-is.
86
- * Expressions are wrapped in return() so the result comes back.
87
- *
88
- * @param {string} code - User input
89
- * @returns {string} Code suitable for new Function()
90
- */
91
- export function wrapExpression(code) {
92
- code = code.trim();
93
- if (/^(var |let |const |if |for |while |function |try |switch |throw |class |\{)/.test(code)) {
94
- return code;
95
- }
96
- return 'return (' + code + ')';
97
- }
79
+ // v2.1: wrapExpression and bare JS eval removed — no code-over-wire.
98
80
 
99
81
  /**
100
82
  * Run the attach subcommand.
@@ -160,7 +142,6 @@ export function startAttach(bwserve, opts) {
160
142
  var app = bwserve.create({
161
143
  port: opts.port,
162
144
  title: 'bwcli attach',
163
- allowExec: true,
164
145
  allowScreenshot: opts.allowScreenshot
165
146
  });
166
147
 
@@ -266,33 +247,9 @@ export function startAttach(bwserve, opts) {
266
247
  return;
267
248
  }
268
249
 
269
- // JS expression requires active client
270
- if (!activeClient) {
271
- console.log('No client connected. Add the attach script to a page first.');
272
- safePrompt();
273
- return;
274
- }
275
-
276
- var code = wrapExpression(line);
277
- if (opts.verbose) {
278
- console.log('[query] ' + code);
279
- }
280
-
281
- activeClient.query(code, { timeout: 10000 }).then(function(result) {
282
- if (result !== undefined && result !== null) {
283
- try {
284
- console.log(typeof result === 'string' ? result : JSON.stringify(result, null, 2));
285
- } catch (e) {
286
- console.log(String(result));
287
- }
288
- } else {
289
- console.log('undefined');
290
- }
291
- safePrompt();
292
- }).catch(function(err) {
293
- console.error('[error] ' + err.message);
294
- safePrompt();
295
- });
250
+ // Non-slash input: show hint (no more bare JS eval)
251
+ console.log('Unknown input. Type /help for available commands.');
252
+ safePrompt();
296
253
  });
297
254
 
298
255
  rl.on('close', function() {
@@ -417,13 +374,13 @@ export function handleSlashCommand(line, activeClient, clients, opts, rl) {
417
374
  break;
418
375
  }
419
376
  }
420
- activeClient.mount(mountSel, mountComp, mountProps, { timeout: 10000 }).then(function() {
421
- console.log('Mounted ' + mountComp + ' at ' + mountSel);
422
- rl.prompt();
423
- }).catch(function(err) {
424
- console.error('[error] ' + err.message);
425
- rl.prompt();
377
+ activeClient.call('_bw_mount', {
378
+ target: mountSel,
379
+ factory: mountComp,
380
+ props: mountProps
426
381
  });
382
+ console.log('Mounted ' + mountComp + ' at ' + mountSel);
383
+ rl.prompt();
427
384
  break;
428
385
 
429
386
  case '/render':
@@ -508,22 +465,6 @@ export function handleSlashCommand(line, activeClient, clients, opts, rl) {
508
465
  rl.prompt();
509
466
  break;
510
467
 
511
- case '/exec':
512
- if (!activeClient) {
513
- console.log('No client connected.');
514
- rl.prompt();
515
- break;
516
- }
517
- if (parts.length < 2) {
518
- console.log('Usage: /exec <code>');
519
- rl.prompt();
520
- break;
521
- }
522
- activeClient.exec(parts.slice(1).join(' '));
523
- console.log('Executed.');
524
- rl.prompt();
525
- break;
526
-
527
468
  default:
528
469
  console.log('Unknown command: ' + cmd + '. Type /help for available commands.');
529
470
  rl.prompt();
@@ -558,9 +499,6 @@ export function printHelp() {
558
499
  '',
559
500
  'bwcli attach — REPL Commands',
560
501
  '',
561
- ' <expression> Evaluate JS in the browser and print result',
562
- ' Examples: document.title, bw.$(".card").length',
563
- '',
564
502
  ' /help, /h Show this help',
565
503
  ' /quit, /q Exit',
566
504
  '',
@@ -577,7 +515,6 @@ export function printHelp() {
577
515
  ' /listen <sel> <event> Listen for DOM events (prints inline)',
578
516
  ' Example: /listen button click',
579
517
  ' /unlisten <sel> <event> Remove a listener',
580
- ' /exec <code> Execute JS without capturing return value',
581
518
  ' /clients List connected clients',
582
519
  '',
583
520
  ' Workflow — build and push a component:',
@@ -7,7 +7,7 @@ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
7
7
  import { resolve, extname, basename, dirname } from 'node:path';
8
8
  import quikdown from '../vendor/quikdown.js';
9
9
  import bw from '../bitwrench.js';
10
- import { getAllStyles, THEME_PRESETS } from '../bitwrench-styles.js';
10
+ import { THEME_PRESETS } from '../bitwrench-styles.js';
11
11
  import { getInjectionHead, getInjectionBodyEnd } from './inject.js';
12
12
  import { makePageLayout } from './layout-default.js';
13
13
 
@@ -110,7 +110,7 @@ export function convertFile(inputPath, flags = {}) {
110
110
  const raw = readFileSync(absInput, 'utf8');
111
111
  const ext = extname(absInput).toLowerCase();
112
112
 
113
- let bodyHTML = '';
113
+ let bodyHTML;
114
114
  let autoTitle = null;
115
115
 
116
116
  // Process based on file extension