bitwrench 2.0.25 → 2.0.31

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 (75) hide show
  1. package/README.md +10 -4
  2. package/dist/bitwrench-bccl.cjs.js +1 -1
  3. package/dist/bitwrench-bccl.cjs.min.js +1 -1
  4. package/dist/bitwrench-bccl.cjs.min.js.gz +0 -0
  5. package/dist/bitwrench-bccl.esm.js +1 -1
  6. package/dist/bitwrench-bccl.esm.min.js +1 -1
  7. package/dist/bitwrench-bccl.esm.min.js.gz +0 -0
  8. package/dist/bitwrench-bccl.umd.js +1 -1
  9. package/dist/bitwrench-bccl.umd.min.js +1 -1
  10. package/dist/bitwrench-bccl.umd.min.js.gz +0 -0
  11. package/dist/bitwrench-code-edit.cjs.js +1 -1
  12. package/dist/bitwrench-code-edit.cjs.min.js +1 -1
  13. package/dist/bitwrench-code-edit.es5.js +1 -1
  14. package/dist/bitwrench-code-edit.es5.min.js +1 -1
  15. package/dist/bitwrench-code-edit.esm.js +1 -1
  16. package/dist/bitwrench-code-edit.esm.min.js +1 -1
  17. package/dist/bitwrench-code-edit.umd.js +1 -1
  18. package/dist/bitwrench-code-edit.umd.min.js +1 -1
  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 +623 -155
  23. package/dist/bitwrench-lean.cjs.min.js +7 -7
  24. package/dist/bitwrench-lean.cjs.min.js.gz +0 -0
  25. package/dist/bitwrench-lean.es5.js +650 -157
  26. package/dist/bitwrench-lean.es5.min.js +5 -5
  27. package/dist/bitwrench-lean.es5.min.js.gz +0 -0
  28. package/dist/bitwrench-lean.esm.js +623 -155
  29. package/dist/bitwrench-lean.esm.min.js +6 -6
  30. package/dist/bitwrench-lean.esm.min.js.gz +0 -0
  31. package/dist/bitwrench-lean.umd.js +623 -155
  32. package/dist/bitwrench-lean.umd.min.js +7 -7
  33. package/dist/bitwrench-lean.umd.min.js.gz +0 -0
  34. package/dist/bitwrench-util-css.cjs.js +1 -1
  35. package/dist/bitwrench-util-css.cjs.min.js +1 -1
  36. package/dist/bitwrench-util-css.es5.js +1 -1
  37. package/dist/bitwrench-util-css.es5.min.js +1 -1
  38. package/dist/bitwrench-util-css.esm.js +1 -1
  39. package/dist/bitwrench-util-css.esm.min.js +1 -1
  40. package/dist/bitwrench-util-css.umd.js +1 -1
  41. package/dist/bitwrench-util-css.umd.min.js +1 -1
  42. package/dist/bitwrench-util-css.umd.min.js.gz +0 -0
  43. package/dist/bitwrench.cjs.js +621 -153
  44. package/dist/bitwrench.cjs.min.js +6 -6
  45. package/dist/bitwrench.cjs.min.js.gz +0 -0
  46. package/dist/bitwrench.css +1 -1
  47. package/dist/bitwrench.d.ts +18 -11
  48. package/dist/bitwrench.es5.js +647 -154
  49. package/dist/bitwrench.es5.min.js +6 -6
  50. package/dist/bitwrench.es5.min.js.gz +0 -0
  51. package/dist/bitwrench.esm.js +621 -153
  52. package/dist/bitwrench.esm.min.js +5 -5
  53. package/dist/bitwrench.esm.min.js.gz +0 -0
  54. package/dist/bitwrench.umd.js +621 -153
  55. package/dist/bitwrench.umd.min.js +6 -6
  56. package/dist/bitwrench.umd.min.js.gz +0 -0
  57. package/dist/builds.json +92 -92
  58. package/dist/bwserve.cjs.js +140 -7
  59. package/dist/bwserve.esm.js +141 -8
  60. package/dist/sri.json +45 -45
  61. package/docs/bitwrench-for-wasm.md +851 -0
  62. package/docs/bitwrench_api.md +133 -23
  63. package/docs/llm-bitwrench-guide.md +6 -5
  64. package/docs/state-management.md +27 -3
  65. package/docs/thinking-in-bitwrench.md +3 -2
  66. package/package.json +11 -9
  67. package/readme.html +17 -8
  68. package/src/bitwrench.d.ts +18 -11
  69. package/src/bitwrench.js +617 -148
  70. package/src/bwserve/bwclient.js +3 -3
  71. package/src/bwserve/client.js +26 -0
  72. package/src/bwserve/index.js +110 -3
  73. package/src/cli/attach.js +7 -5
  74. package/src/cli/serve.js +53 -10
  75. package/src/version.js +3 -3
@@ -86,8 +86,8 @@ var BWCLIENT_SOURCE = '(function(bw) {\n'
86
86
  + ' // ── Register built-in functions ──\n'
87
87
  + ' _client._registerBuiltins = function() {\n'
88
88
  + ' var builtins = {\n'
89
- + ' scrollTo: "function(sel){var el=bw._el(sel);if(el)el.scrollTop=el.scrollHeight;}",\n'
90
- + ' focus: "function(sel){var el=bw._el(sel);if(el&&typeof el.focus===\\"function\\")el.focus();}",\n'
89
+ + ' scrollTo: "function(sel){var el=bw.el(sel);if(el)el.scrollTop=el.scrollHeight;}",\n'
90
+ + ' focus: "function(sel){var el=bw.el(sel);if(el&&typeof el.focus===\\"function\\")el.focus();}",\n'
91
91
  + ' download: "function(fn,c,m){if(typeof document===\\"undefined\\")return;var b=new Blob([c],{type:m||\\"text/plain\\"});var a=document.createElement(\\"a\\");a.href=URL.createObjectURL(b);a.download=fn;a.click();URL.revokeObjectURL(a.href);}",\n'
92
92
  + ' clipboard: "function(t){if(typeof navigator!==\\"undefined\\"&&navigator.clipboard)navigator.clipboard.writeText(t);}",\n'
93
93
  + ' redirect: "function(u){if(typeof window!==\\"undefined\\")window.location.href=u;}",\n'
@@ -95,7 +95,7 @@ var BWCLIENT_SOURCE = '(function(bw) {\n'
95
95
  + ' _bw_query: "function(opts){if(!bw._bwClient)return;try{var r=new Function(opts.code)();if(r&&typeof r.then===\\"function\\"){r.then(function(v){bw._bwClient.respond(\\"query\\",opts.requestId,v);}).catch(function(e){bw._bwClient.respond(\\"query\\",opts.requestId,null,e.message);});}else{bw._bwClient.respond(\\"query\\",opts.requestId,r);}}catch(e){bw._bwClient.respond(\\"query\\",opts.requestId,null,e.message);}}",\n'
96
96
  + ' _bw_mount: "function(opts){if(!bw._bwClient)return;try{var taco;var f=opts.factory;var n=f.replace(/-([a-z])/g,function(_,c){return c.toUpperCase();});if(bw.BCCL&&bw.BCCL[n]){taco=bw.make(n,opts.props||{});}else if(bw._allowExec){taco=new Function(\\"props\\",f)(opts.props||{});}else{throw new Error(\\"Unknown component and allowExec disabled\\");}bw.DOM(opts.target,taco);bw._bwClient.respond(\\"mount\\",opts.requestId,{mounted:true});}catch(e){bw._bwClient.respond(\\"mount\\",opts.requestId,null,e.message);}}",\n'
97
97
  + ' _bw_screenshot: "function(opts){if(!bw._bwClient)return;var sel=opts.selector||\\"body\\";var el=document.querySelector(sel);if(!el){bw._bwClient.respond(\\"screenshot\\",opts.requestId,null,\\"Element not found: \\"+sel);return;}function _ls(url){return new Promise(function(res,rej){var s=document.createElement(\\"script\\");s.src=url;s.onload=function(){res(window.html2canvas);};s.onerror=function(){rej(new Error(\\"Failed to load html2canvas\\"));};document.head.appendChild(s);});}var p=window.html2canvas?Promise.resolve(window.html2canvas):_ls(opts.captureUrl||\\"/bw/lib/vendor/html2canvas.min.js\\");p.then(function(h2c){return h2c(el,{scale:opts.scale||1,useCORS:true});}).then(function(canvas){var out=canvas;var mw=opts.maxWidth;var mh=opts.maxHeight;if((mw&&canvas.width>mw)||(mh&&canvas.height>mh)){var sw=mw?mw/canvas.width:1;var sh=mh?mh/canvas.height:1;var sc=Math.min(sw,sh);out=document.createElement(\\"canvas\\");out.width=Math.round(canvas.width*sc);out.height=Math.round(canvas.height*sc);out.getContext(\\"2d\\").drawImage(canvas,0,0,out.width,out.height);}var fmt=opts.format===\\"jpeg\\"?\\"image/jpeg\\":\\"image/png\\";var q=opts.format===\\"jpeg\\"?(opts.quality||0.85):undefined;var dataUrl=out.toDataURL(fmt,q);bw._bwClient.respond(\\"screenshot\\",opts.requestId,{data:dataUrl,width:out.width,height:out.height,format:opts.format||\\"png\\"});}).catch(function(err){bw._bwClient.respond(\\"screenshot\\",opts.requestId,null,err.message||String(err));});}",\n'
98
- + ' _bw_tree: "function(opts){if(!bw._bwClient)return;var sel=opts.selector||\\"body\\";var depth=opts.depth||3;function walk(el,d){if(!el||d>depth)return null;var info={tag:el.tagName?el.tagName.toLowerCase():\\"#text\\"};if(el.id)info.id=el.id;if(el.className&&typeof el.className===\\"string\\")info.cls=el.className.split(\\" \\").slice(0,5).join(\\" \\");if(el.children&&el.children.length>0&&d<depth){info.children=[];for(var i=0;i<Math.min(el.children.length,20);i++){var c=walk(el.children[i],d+1);if(c)info.children.push(c);}}return info;}var root=document.querySelector(sel);bw._bwClient.respond(\\"query\\",opts.requestId,walk(root,0));}",\n'
98
+ + ' _bw_tree: "function(opts){if(!bw._bwClient)return;var sel=opts.selector||\\"body\\";var depth=opts.depth||3;var root=document.querySelector(sel);if(typeof bw.inspect===\\"function\\"&&bw.inspect.length===2){bw._bwClient.respond(\\"query\\",opts.requestId,bw.inspect(root,depth));return;}function walk(el,d){if(!el||d>depth)return null;var info={tag:el.tagName?el.tagName.toLowerCase():\\"#text\\"};if(el.id)info.id=el.id;if(el.className&&typeof el.className===\\"string\\")info.cls=el.className.split(\\" \\").slice(0,5).join(\\" \\");if(el.children&&el.children.length>0&&d<depth){info.children=[];for(var i=0;i<Math.min(el.children.length,20);i++){var c=walk(el.children[i],d+1);if(c)info.children.push(c);}}return info;}bw._bwClient.respond(\\"query\\",opts.requestId,walk(root,0));}",\n'
99
99
  + ' _bw_listen: "function(opts){if(!bw._bwClient)return;if(!bw._bwClient._listeners)bw._bwClient._listeners={};var key=opts.selector+\\":::\\"+opts.event;if(bw._bwClient._listeners[key])return;var fn=function(e){var el=e.target.closest?e.target.closest(opts.selector):null;if(!el)return;bw._bwClient.respond(\\"event\\",null,{event:opts.event,selector:opts.selector,tagName:el.tagName,id:el.id||null,text:(el.textContent||\\"\\").slice(0,100)});};document.addEventListener(opts.event,fn,true);bw._bwClient._listeners[key]={fn:fn,event:opts.event};}",\n'
100
100
  + ' _bw_unlisten: "function(opts){if(!bw._bwClient||!bw._bwClient._listeners)return;var key=opts.selector+\\":::\\"+opts.event;var entry=bw._bwClient._listeners[key];if(!entry)return;document.removeEventListener(entry.event,entry.fn,true);delete bw._bwClient._listeners[key];}"\n'
101
101
  + ' };\n'
@@ -263,6 +263,32 @@ export class BwServeClient {
263
263
  return pend.promise;
264
264
  }
265
265
 
266
+ // ── Inspect ──
267
+
268
+ /**
269
+ * Inspect the DOM tree of the connected client.
270
+ *
271
+ * Calls the `_bw_tree` builtin on the client which delegates to
272
+ * `bw.inspect()` when available, returning a plain-object tree with
273
+ * bitwrench metadata (tag, uuid, type, handles, state, children).
274
+ *
275
+ * @param {string} [selector='body'] - CSS selector of root element
276
+ * @param {Object} [options]
277
+ * @param {number} [options.depth=3] - Max recursion depth
278
+ * @param {number} [options.timeout=10000] - Timeout in ms
279
+ * @returns {Promise<Object|null>} Tree object, or null if element not found
280
+ */
281
+ inspect(selector, options) {
282
+ var opts = options || {};
283
+ var pend = this._pend(opts.timeout || 10000);
284
+ this.call('_bw_tree', {
285
+ selector: selector || 'body',
286
+ depth: opts.depth || 3,
287
+ requestId: pend.requestId
288
+ });
289
+ return pend.promise;
290
+ }
291
+
266
292
  // ── Screenshot ──
267
293
 
268
294
  /**
@@ -25,7 +25,7 @@ import { VERSION } from '../version.js';
25
25
  import { fileURLToPath } from 'url';
26
26
  import { dirname, resolve, join, extname } from 'path';
27
27
  import { createServer } from 'http';
28
- import { readFileSync, existsSync, statSync } from 'fs';
28
+ import { readFileSync, readdirSync, existsSync, statSync } from 'fs';
29
29
 
30
30
  var __dirname = dirname(fileURLToPath(import.meta.url));
31
31
 
@@ -54,7 +54,21 @@ var MIME_TYPES = {
54
54
  '.woff': 'font/woff',
55
55
  '.woff2': 'font/woff2',
56
56
  '.ttf': 'font/ttf',
57
- '.map': 'application/json'
57
+ '.map': 'application/json',
58
+ '.txt': 'text/plain; charset=utf-8',
59
+ '.xml': 'application/xml; charset=utf-8',
60
+ '.pdf': 'application/pdf',
61
+ '.zip': 'application/zip',
62
+ '.gz': 'application/gzip',
63
+ '.mp3': 'audio/mpeg',
64
+ '.mp4': 'video/mp4',
65
+ '.webm': 'video/webm',
66
+ '.webp': 'image/webp',
67
+ '.avif': 'image/avif',
68
+ '.wasm': 'application/wasm',
69
+ '.csv': 'text/csv; charset=utf-8',
70
+ '.md': 'text/markdown; charset=utf-8',
71
+ '.mjs': 'application/javascript; charset=utf-8'
58
72
  };
59
73
 
60
74
  /**
@@ -67,6 +81,8 @@ var MIME_TYPES = {
67
81
  * @param {boolean} [opts.injectBitwrench=true] - Auto-inject bitwrench client JS
68
82
  * @param {string|Object} [opts.theme] - Theme preset name or config object
69
83
  * @param {boolean} [opts.allowScreenshot=false] - Enable client.screenshot() capability
84
+ * @param {boolean} [opts.dirList=true] - Enable directory listings when no index.html
85
+ * @param {string} [opts.host='0.0.0.0'] - Host/address to bind to
70
86
  * @returns {BwServeApp} Application instance
71
87
  */
72
88
  export function create(opts) {
@@ -87,6 +103,8 @@ class BwServeApp {
87
103
  this.theme = opts.theme || null;
88
104
  this.allowExec = opts.allowExec || false;
89
105
  this.allowScreenshot = opts.allowScreenshot || false;
106
+ this.dirList = opts.dirList !== false;
107
+ this.host = opts.host || '0.0.0.0';
90
108
  this.keepAliveInterval = opts.keepAliveInterval || 15000;
91
109
  this._pages = new Map();
92
110
  this._clients = new Map();
@@ -120,7 +138,7 @@ class BwServeApp {
120
138
  self._handleRequest(req, rawRes);
121
139
  });
122
140
 
123
- self._server.listen(self.port, function() {
141
+ self._server.listen(self.port, self.host, function() {
124
142
  if (callback) callback();
125
143
  res();
126
144
  });
@@ -284,6 +302,32 @@ class BwServeApp {
284
302
  res.end(content);
285
303
  return;
286
304
  }
305
+ // Directory index resolution: /foo/ => /foo/index.html
306
+ if (path.endsWith('/')) {
307
+ var indexPath = join(this.staticDir, path, 'index.html');
308
+ if (existsSync(indexPath) && statSync(indexPath).isFile()) {
309
+ var indexContent = readFileSync(indexPath);
310
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
311
+ res.end(indexContent);
312
+ return;
313
+ }
314
+ // 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);
318
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
319
+ res.end(listing);
320
+ return;
321
+ }
322
+ }
323
+ // Bare directory without trailing slash: /foo => 301 to /foo/
324
+ if (!path.endsWith('/') && existsSync(filePath) && statSync(filePath).isDirectory()) {
325
+ var qs = url.split('?')[1];
326
+ var location = path + '/' + (qs ? '?' + qs : '');
327
+ res.writeHead(301, { 'Location': location });
328
+ res.end();
329
+ return;
330
+ }
287
331
  }
288
332
 
289
333
  // 404
@@ -453,6 +497,69 @@ class BwServeApp {
453
497
  });
454
498
  res.end(content);
455
499
  }
500
+
501
+ /**
502
+ * Generate an HTML directory listing page.
503
+ * @private
504
+ * @param {string} urlPath - URL path (with trailing slash)
505
+ * @param {string} dirPath - Filesystem path to the directory
506
+ * @returns {string} HTML page
507
+ */
508
+ _generateDirListing(urlPath, dirPath) {
509
+ var entries = readdirSync(dirPath);
510
+ var dirs = [];
511
+ var files = [];
512
+
513
+ for (var i = 0; i < entries.length; i++) {
514
+ var name = entries[i];
515
+ var fullPath = join(dirPath, name);
516
+ try {
517
+ var st = statSync(fullPath);
518
+ if (st.isDirectory()) {
519
+ dirs.push({ name: name + '/', size: '-' });
520
+ } else {
521
+ files.push({ name: name, size: _formatSize(st.size) });
522
+ }
523
+ } catch (e) {
524
+ // Skip entries we cannot stat
525
+ }
526
+ }
527
+
528
+ // Sort alphabetically
529
+ dirs.sort(function(a, b) { return a.name.localeCompare(b.name); });
530
+ files.sort(function(a, b) { return a.name.localeCompare(b.name); });
531
+
532
+ var all = dirs.concat(files);
533
+
534
+ var rows = '';
535
+ // Parent directory link (unless at root)
536
+ if (urlPath !== '/') {
537
+ rows += '<tr><td><a href="../">..</a></td><td>-</td></tr>\n';
538
+ }
539
+ for (var j = 0; j < all.length; j++) {
540
+ var entry = all[j];
541
+ var escaped = entry.name.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
542
+ rows += '<tr><td><a href="' + encodeURIComponent(entry.name.replace(/\/$/, '')) + (entry.size === '-' ? '/' : '') + '">' + escaped + '</a></td><td>' + entry.size + '</td></tr>\n';
543
+ }
544
+
545
+ var escapedPath = urlPath.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
546
+ return '<!DOCTYPE html>\n<html><head><meta charset="utf-8"><title>Index of ' + escapedPath + '</title>' +
547
+ '<style>body{font-family:monospace;margin:2em}table{border-collapse:collapse}td,th{text-align:left;padding:4px 16px}a{text-decoration:none}a:hover{text-decoration:underline}</style>' +
548
+ '</head><body><h1>Index of ' + escapedPath + '</h1><table><tr><th>Name</th><th>Size</th></tr>\n' +
549
+ rows + '</table></body></html>';
550
+ }
551
+ }
552
+
553
+ /**
554
+ * Format a byte size as a human-readable string.
555
+ * @param {number} bytes
556
+ * @returns {string}
557
+ */
558
+ function _formatSize(bytes) {
559
+ if (bytes < 1024) return bytes + ' B';
560
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
561
+ if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
562
+ return (bytes / (1024 * 1024 * 1024)).toFixed(1) + ' GB';
456
563
  }
457
564
 
458
565
  export var version = VERSION;
package/src/cli/attach.js CHANGED
@@ -47,7 +47,7 @@ REPL Commands:
47
47
  <expression> Evaluate JS in the connected browser (e.g., document.title)
48
48
  /help Show command reference
49
49
  /quit, /q Exit
50
- /tree [selector] [depth] Show DOM tree summary (default: body, depth 3)
50
+ /inspect [selector] [depth] Show DOM tree summary (default: body, depth 3)
51
51
  /screenshot [sel] [file] Capture screenshot (requires --allow-screenshot)
52
52
  /mount <sel> <comp> [json] Mount a BCCL component on the client
53
53
  /render <sel> <taco-json> Render a TACO object at selector
@@ -66,7 +66,7 @@ Examples:
66
66
  # In the REPL:
67
67
  bw> document.title
68
68
  bw> bw.$('.bw-card').length
69
- bw> /tree #app 2
69
+ bw> /inspect #app 2
70
70
  bw> /screenshot body page.png
71
71
  bw> /listen .bw-btn click
72
72
  bw> /mount #app card {"title":"Hello","content":"World"}
@@ -76,7 +76,7 @@ Examples:
76
76
  bw> /patch users "Users: 342"
77
77
  bw> /patch orders "Orders: 28"
78
78
  bw> /mount #app card {"title":"Status","content":"All systems go"}
79
- bw> /tree #app 2
79
+ bw> /inspect #app 2
80
80
  bw> /listen .bw-btn click
81
81
  `.trim();
82
82
 
@@ -342,7 +342,8 @@ export function handleSlashCommand(line, activeClient, clients, opts, rl) {
342
342
  rl.prompt();
343
343
  break;
344
344
 
345
- case '/tree':
345
+ case '/inspect':
346
+ case '/tree': // alias kept for muscle memory
346
347
  if (!activeClient) {
347
348
  console.log('No client connected.');
348
349
  rl.prompt();
@@ -563,7 +564,8 @@ export function printHelp() {
563
564
  ' /help, /h Show this help',
564
565
  ' /quit, /q Exit',
565
566
  '',
566
- ' /tree [sel] [depth] Show DOM tree (default: body, depth 3)',
567
+ ' /inspect [sel] [depth] Show DOM tree (default: body, depth 3)',
568
+ ' /tree [sel] [depth] Alias for /inspect',
567
569
  ' /screenshot [sel] [file] Capture screenshot to PNG file',
568
570
  ' Requires --allow-screenshot flag',
569
571
  ' /mount <sel> <comp> [json] Mount a BCCL component',
package/src/cli/serve.js CHANGED
@@ -30,10 +30,12 @@ Arguments:
30
30
  Options:
31
31
  -p, --port <number> Browser-facing web port (default: 8080)
32
32
  -l, --listen <number> Input port for protocol messages (default: 9000)
33
+ -b, --bind <address> Host/address to bind to (default: 0.0.0.0)
33
34
  --stdin Read protocol messages from stdin (newline-delimited JSON)
34
35
  -t, --theme <name> Theme preset or hex colors ("#pri,#sec")
35
36
  --title <string> Page title (default: "bwcli serve")
36
37
  --allow-exec Enable exec messages (runs JS in browser, use for dev only)
38
+ --no-dir-list Disable directory listings
37
39
  --open Open browser on start
38
40
  -v, --verbose Verbose output
39
41
  -h, --help Print this help
@@ -292,10 +294,12 @@ export function runServe(argv, ioOpts) {
292
294
  options: {
293
295
  port: { type: 'string', short: 'p' },
294
296
  listen: { type: 'string', short: 'l' },
297
+ bind: { type: 'string', short: 'b' },
295
298
  stdin: { type: 'boolean' },
296
299
  theme: { type: 'string', short: 't' },
297
300
  title: { type: 'string' },
298
301
  'allow-exec': { type: 'boolean' },
302
+ 'no-dir-list': { type: 'boolean' },
299
303
  open: { type: 'boolean' },
300
304
  verbose: { type: 'boolean', short: 'v' },
301
305
  help: { type: 'boolean', short: 'h' }
@@ -317,9 +321,11 @@ export function runServe(argv, ioOpts) {
317
321
  var dir = positionals[0] || '.';
318
322
  var webPort = values.port ? parseInt(values.port, 10) : 8080;
319
323
  var listenPort = values.listen ? parseInt(values.listen, 10) : 9000;
324
+ var bindAddr = values.bind || '0.0.0.0';
320
325
  var useStdin = !!values.stdin;
321
326
  var theme = values.theme || null;
322
327
  var title = values.title || 'bwcli serve';
328
+ var dirList = !values['no-dir-list'];
323
329
  var verbose = !!values.verbose;
324
330
 
325
331
  if (isNaN(webPort) || webPort < 1 || webPort > 65535) {
@@ -339,9 +345,11 @@ export function runServe(argv, ioOpts) {
339
345
  dir: dir,
340
346
  webPort: webPort,
341
347
  listenPort: listenPort,
348
+ bind: bindAddr,
342
349
  useStdin: useStdin,
343
350
  theme: theme,
344
351
  title: title,
352
+ dirList: dirList,
345
353
  verbose: verbose,
346
354
  open: !!values.open,
347
355
  allowExec: !!values['allow-exec']
@@ -360,9 +368,11 @@ export function runServe(argv, ioOpts) {
360
368
  function startServer(bwserve, opts) {
361
369
  var app = bwserve.create({
362
370
  port: opts.webPort,
371
+ host: opts.bind,
363
372
  title: opts.title,
364
373
  static: opts.dir,
365
374
  theme: opts.theme,
375
+ dirList: opts.dirList,
366
376
  allowExec: opts.allowExec
367
377
  });
368
378
 
@@ -381,21 +391,24 @@ function startServer(bwserve, opts) {
381
391
  // Start web server
382
392
  app.listen(function() {
383
393
  console.error('bwcli serve v' + VERSION);
384
- console.error(' Web server: http://localhost:' + opts.webPort);
394
+ console.error(' Web server: http://' + (opts.bind === '0.0.0.0' ? 'localhost' : opts.bind) + ':' + opts.webPort);
395
+ console.error(' Bind: ' + opts.bind);
385
396
  console.error(' Static dir: ' + opts.dir);
386
397
  if (opts.theme) console.error(' Theme: ' + opts.theme);
398
+ if (opts.dirList === false) console.error(' Dir listing: disabled');
387
399
 
388
400
  if (opts.useStdin) {
389
401
  console.error(' Input: stdin (newline-delimited JSON)');
390
402
  startStdinReader(app, opts.verbose);
403
+ console.error('');
404
+ console.error('Ready. Send protocol messages to push UI to browsers.');
391
405
  } else {
392
- console.error(' Input port: http://localhost:' + opts.listenPort);
393
- startInputServer(app, opts.listenPort, opts.verbose);
406
+ startInputServer(app, opts.listenPort, opts.verbose).then(function() {
407
+ console.error('');
408
+ console.error('Ready. Send protocol messages to push UI to browsers.');
409
+ });
394
410
  }
395
411
 
396
- console.error('');
397
- console.error('Ready. Send protocol messages to push UI to browsers.');
398
-
399
412
  if (opts.open) {
400
413
  import('node:child_process').then(function(cp) {
401
414
  var url = 'http://localhost:' + opts.webPort;
@@ -414,7 +427,40 @@ function startServer(bwserve, opts) {
414
427
  * Messages with a `type` field (no `command`) are broadcast to all clients.
415
428
  */
416
429
  function startInputServer(app, listenPort, verbose) {
417
- var inputServer = createServer(function(req, res) {
430
+ var inputServer = _createInputServer(app, verbose);
431
+
432
+ return new Promise(function(resolve) {
433
+ inputServer.on('error', function(err) {
434
+ if (err.code === 'EADDRINUSE') {
435
+ console.error(' Warning: Input port ' + listenPort + ' in use, picking a free port...');
436
+ var retry = _createInputServer(app, verbose);
437
+ retry.on('error', function(err2) {
438
+ console.error(' Warning: Could not bind input server (' + err2.message + '). Continuing without input port.');
439
+ resolve(null);
440
+ });
441
+ retry.listen(0, function() {
442
+ var actualPort = retry.address().port;
443
+ console.error(' Input port: http://localhost:' + actualPort + ' (fallback)');
444
+ resolve(retry);
445
+ });
446
+ } else {
447
+ console.error(' Warning: Input server error (' + err.message + '). Continuing without input port.');
448
+ resolve(null);
449
+ }
450
+ });
451
+ inputServer.listen(listenPort, function() {
452
+ console.error(' Input port: http://localhost:' + listenPort);
453
+ resolve(inputServer);
454
+ });
455
+ });
456
+ }
457
+
458
+ /**
459
+ * Create the input HTTP server (without binding).
460
+ * @private
461
+ */
462
+ function _createInputServer(app, verbose) {
463
+ return createServer(function(req, res) {
418
464
  if (req.method !== 'POST') {
419
465
  res.writeHead(405, { 'Content-Type': 'application/json' });
420
466
  res.end(JSON.stringify({ error: 'Use POST' }));
@@ -458,9 +504,6 @@ function startInputServer(app, listenPort, verbose) {
458
504
  res.end(JSON.stringify({ ok: true, clients: count }));
459
505
  });
460
506
  });
461
-
462
- inputServer.listen(listenPort);
463
- return inputServer;
464
507
  }
465
508
 
466
509
  /**
package/src/version.js CHANGED
@@ -3,14 +3,14 @@
3
3
  * DO NOT EDIT DIRECTLY - Use npm run generate-version
4
4
  */
5
5
 
6
- export const VERSION = '2.0.25';
6
+ export const VERSION = '2.0.31';
7
7
  export const VERSION_INFO = {
8
- version: '2.0.25',
8
+ version: '2.0.31',
9
9
  name: 'bitwrench',
10
10
  description: 'A library for javascript UI functions.',
11
11
  license: 'BSD-2-Clause',
12
12
  homepage: 'https://deftio.github.com/bitwrench/pages',
13
13
  repository: 'git+https://github.com/deftio/bitwrench.git',
14
14
  author: 'manu a. chatterjee <deftio@deftio.com> (https://deftio.com/)',
15
- buildDate: '2026-03-31T03:03:30.752Z'
15
+ buildDate: '2026-04-12T07:56:29.791Z'
16
16
  };