bitwrench 2.0.32 → 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 +289 -282
  68. package/dist/bwserve.d.ts +157 -0
  69. package/dist/bwserve.esm.js +290 -283
  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 +59 -26
  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' }));
@@ -278,18 +287,26 @@ class BwServeApp {
278
287
  // so that bwserve works as a drop-in static server (like python -m
279
288
  // http.server or npx serve) with opt-in bwserve superpowers.
280
289
  if (method === 'GET' && this.staticDir) {
281
- var filePath = join(this.staticDir, path);
282
- if (existsSync(filePath) && statSync(filePath).isFile()) {
283
- 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);
284
301
  var mime = MIME_TYPES[ext] || 'application/octet-stream';
285
- var content = readFileSync(filePath);
302
+ var content = readFileSync(resolvedPath);
286
303
  res.writeHead(200, { 'Content-Type': mime });
287
304
  res.end(content);
288
305
  return;
289
306
  }
290
307
  // Directory index resolution: /foo/ => /foo/index.html
291
308
  if (path.endsWith('/')) {
292
- var indexPath = join(this.staticDir, path, 'index.html');
309
+ var indexPath = join(resolvedPath, 'index.html');
293
310
  if (existsSync(indexPath) && statSync(indexPath).isFile()) {
294
311
  var indexContent = readFileSync(indexPath);
295
312
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
@@ -297,16 +314,15 @@ class BwServeApp {
297
314
  return;
298
315
  }
299
316
  // Directory listing when no index.html
300
- var dirPath = join(this.staticDir, path);
301
- if (this.dirList && existsSync(dirPath) && statSync(dirPath).isDirectory()) {
302
- var listing = this._generateDirListing(path, dirPath);
317
+ if (this.dirList && existsSync(resolvedPath) && statSync(resolvedPath).isDirectory()) {
318
+ var listing = this._generateDirListing(path, resolvedPath);
303
319
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
304
320
  res.end(listing);
305
321
  return;
306
322
  }
307
323
  }
308
324
  // Bare directory without trailing slash: /foo => 301 to /foo/
309
- if (!path.endsWith('/') && existsSync(filePath) && statSync(filePath).isDirectory()) {
325
+ if (!path.endsWith('/') && existsSync(resolvedPath) && statSync(resolvedPath).isDirectory()) {
310
326
  var qs = url.split('?')[1];
311
327
  var location = path + '/' + (qs ? '?' + qs : '');
312
328
  res.writeHead(301, { 'Location': location });
@@ -323,8 +339,7 @@ class BwServeApp {
323
339
  clientId: clientId2,
324
340
  title: this.title,
325
341
  theme: this.theme,
326
- injectBitwrench: this.injectBitwrench,
327
- allowExec: this.allowExec
342
+ injectBitwrench: this.injectBitwrench
328
343
  });
329
344
  // Store the page path for this client so SSE knows which handler to call
330
345
  this._clients.set(clientId2, { pagePath: path, client: null });
@@ -383,9 +398,13 @@ class BwServeApp {
383
398
  var pagePath = pending ? pending.pagePath : '/';
384
399
  self._clients.set(clientId, { pagePath: pagePath, client: client });
385
400
 
401
+ // Send the handshake as the very first SSE event
402
+ client._send({ type: 'hello' });
403
+
386
404
  // Keep-alive: send SSE comment periodically
387
405
  var keepAlive = setInterval(function() {
388
406
  if (!client._closed) {
407
+ /* c8 ignore next -- keepalive write failure only on socket close */
389
408
  try { res.write(':keepalive\n\n'); } catch (e) { /* ignore */ }
390
409
  }
391
410
  }, self.keepAliveInterval);
@@ -397,11 +416,12 @@ class BwServeApp {
397
416
  self._clients.delete(clientId);
398
417
  });
399
418
 
400
- // Call the page handler
419
+ // Call the page handler (runs on every connection, including reconnects)
401
420
  var handler = self._pages.get(pagePath);
402
421
  if (handler) {
403
422
  try {
404
423
  handler(client);
424
+ /* c8 ignore next 3 -- page handler error catch; requires throwing handler in SSE context */
405
425
  } catch (e) {
406
426
  console.error('[bwserve] Page handler error:', e);
407
427
  }
@@ -414,8 +434,8 @@ class BwServeApp {
414
434
  *
415
435
  * Routes:
416
436
  * action — fire-and-forget action dispatch (no requestId)
417
- * queryresolve pending query promise
418
- * mountresolve pending mount promise
437
+ * eventevent dispatch from client listeners
438
+ * topictopic dispatch from client pub/sub
419
439
  * screenshot — resolve pending screenshot promise
420
440
  *
421
441
  * @private
@@ -433,18 +453,26 @@ class BwServeApp {
433
453
  req.on('end', function() {
434
454
  try {
435
455
  var data = JSON.parse(body);
436
- 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') {
437
462
  // Action/event dispatch (no requestId/pending pattern)
438
463
  var action = route === 'event'
439
464
  ? '_bw_event'
440
465
  : (data.result ? data.result.action : data.action);
466
+ /* c8 ignore next 3 -- data.data fallback; covered in isolation */
441
467
  var payload = route === 'event'
442
468
  ? (data.result || data)
443
469
  : (data.result ? data.result.data : data.data || data);
444
470
  record.client._dispatch(action, payload);
445
471
  } else {
446
- // All other routes: resolve pending promise
447
- 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
+ }
448
476
  }
449
477
  res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
450
478
  res.end(JSON.stringify({ ok: true }));
@@ -462,13 +490,18 @@ class BwServeApp {
462
490
  */
463
491
  _serveAttachScript(req, res) {
464
492
  try {
465
- 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 });
466
498
  res.writeHead(200, {
467
499
  'Content-Type': 'application/javascript; charset=utf-8',
468
500
  'Access-Control-Allow-Origin': '*',
469
501
  'Cache-Control': 'no-cache'
470
502
  });
471
503
  res.end(js);
504
+ /* c8 ignore next 4 -- generateAttachScript is a pure template; cannot throw */
472
505
  } catch (err) {
473
506
  res.writeHead(500, { 'Content-Type': 'text/plain' });
474
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