esque-bridge 0.6.1 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +117 -9
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -584,14 +584,103 @@ function openPreviewTunnel(port) {
584
584
  });
585
585
  }
586
586
 
587
+ // Strip ANSI color codes bundlers embed in their error output.
588
+ function stripAnsi(s) {
589
+ return String(s).replace(/\[[0-9;]*m/g, '');
590
+ }
591
+
592
+ // Reduce a bundler error (Metro's JSON payload, or raw stderr text) to a
593
+ // short, human-readable summary: the first few meaningful lines, no stack
594
+ // trace, no code frame, no ANSI, project paths shortened. This is what the
595
+ // phone shows in place of a white screen, so keep it tight.
596
+ function summarizeBuildError(status, body) {
597
+ let msg = '';
598
+ try {
599
+ const j = JSON.parse(body);
600
+ msg = j.message || (j.errors && j.errors[0] && j.errors[0].description) || '';
601
+ } catch {
602
+ msg = body;
603
+ }
604
+ msg = stripAnsi(msg).trim();
605
+ const head = [];
606
+ for (const l of msg.split('\n').map((x) => x.trimEnd()).filter(Boolean)) {
607
+ if (/^\s*at\s/.test(l)) break; // stack trace
608
+ if (/^\s*>?\s*\d+\s*\|/.test(l)) break; // code frame ("> 1 |")
609
+ head.push(l);
610
+ if (head.length >= 5) break;
611
+ }
612
+ let out = head.join('\n').split(`${WORKDIR}/`).join('').trim();
613
+ if (out.length > 600) out = out.slice(0, 600) + '…';
614
+ return out || `Build failed (HTTP ${status}).`;
615
+ }
616
+
617
+ // Fallback for compile errors an HTTP probe can't see (e.g. Vite/Next inject
618
+ // an in-page overlay but still log the failure to stderr): scan the captured
619
+ // dev-server output for a known failure signature.
620
+ function scanOutputForError(out) {
621
+ if (!out) return null;
622
+ const lines = stripAnsi(out).split('\n');
623
+ const sig =
624
+ /(unable to resolve|cannot find module|module not found|failed to compile|could not resolve|\bERROR in\b|SyntaxError|Transform failed|\[ERROR\])/i;
625
+ for (let i = 0; i < lines.length; i++) {
626
+ if (sig.test(lines[i])) {
627
+ let s = lines.slice(i, i + 4).map((l) => l.trim()).filter(Boolean).join('\n');
628
+ if (s.length > 600) s = s.slice(0, 600) + '…';
629
+ return s.split(`${WORKDIR}/`).join('');
630
+ }
631
+ }
632
+ return null;
633
+ }
634
+
635
+ // Fetch a path on the local dev server with a hard timeout.
636
+ async function fetchLocal(port, path, ms) {
637
+ const ctrl = new AbortController();
638
+ const timer = setTimeout(() => ctrl.abort(), ms);
639
+ try {
640
+ const r = await fetch(`http://127.0.0.1:${port}${path}`, { signal: ctrl.signal });
641
+ return { status: r.status, body: await r.text() };
642
+ } catch {
643
+ return null;
644
+ } finally {
645
+ clearTimeout(timer);
646
+ }
647
+ }
648
+
649
+ // Probe a freshly-started dev server for a build failure that would otherwise
650
+ // render as a blank screen on the phone. Metro builds the web bundle lazily,
651
+ // so we trigger it ourselves and catch fast failures (missing deps, syntax
652
+ // errors return in <2s) with a short timeout — we deliberately DON'T wait out
653
+ // a slow-but-healthy compile. Returns a short error string, or null if it
654
+ // looks fine / is still compiling.
655
+ async function probeBuildError(port) {
656
+ const root = await fetchLocal(port, '/', 5000);
657
+ if (!root) return null; // couldn't reach it — don't cry wolf
658
+ if (root.status >= 500) return summarizeBuildError(root.status, root.body);
659
+
660
+ // Expo/Metro: the HTML points at a *.bundle script. Fetching it forces the
661
+ // compile and surfaces the react-native-web class of error as a 500.
662
+ const m = root.body.match(/<script[^>]+src="([^"]*\.bundle[^"]*)"/i);
663
+ if (m) {
664
+ const b = await fetchLocal(port, m[1].replace(/&amp;/g, '&'), 5000);
665
+ if (b && b.status >= 500) return summarizeBuildError(b.status, b.body);
666
+ }
667
+
668
+ return scanOutputForError(preview && preview.output);
669
+ }
670
+
587
671
  async function startPreview(cmd, port) {
588
672
  killPreview();
589
673
  console.log(`[preview] starting: ${cmd} (port ${port})`);
590
674
  const proc = spawn('sh', ['-c', cmd], { cwd: WORKDIR, env: process.env, stdio: ['ignore', 'pipe', 'pipe'] });
591
- proc.stdout.on('data', (d) => process.stdout.write(`[preview] ${d}`));
592
- proc.stderr.on('data', (d) => process.stdout.write(`[preview] ${d}`));
675
+ preview = { port, proc, kind: null, url: null, tunnel: null, tunnelProc: null, output: '', buildError: null };
676
+ // Keep a rolling tail of output so the error scanner has something to read.
677
+ const capture = (d) => {
678
+ process.stdout.write(`[preview] ${d}`);
679
+ if (preview) preview.output = (preview.output + d).slice(-16000);
680
+ };
681
+ proc.stdout.on('data', capture);
682
+ proc.stderr.on('data', capture);
593
683
  proc.on('exit', (code) => console.log(`[preview] dev server exited (${code})`));
594
- preview = { port, proc, kind: null, url: null, tunnel: null, tunnelProc: null };
595
684
 
596
685
  const up = await waitForPort(port, 60000);
597
686
  if (!up) { console.log('[preview] dev server never opened the port'); return null; }
@@ -603,7 +692,14 @@ async function startPreview(cmd, port) {
603
692
  preview.tunnel = t.tunnel || null;
604
693
  preview.tunnelProc = t.tunnelProc || null;
605
694
  console.log(`[preview] live at ${t.url} (${t.kind})`);
606
- return t.url;
695
+
696
+ // Before handing the URL to the phone, make sure the page will actually
697
+ // render — a dev server can be "up" while its bundle fails to compile.
698
+ const buildError = await probeBuildError(port);
699
+ if (preview) preview.buildError = buildError;
700
+ if (buildError) console.error(`[preview] ⚠ web build is failing:\n${buildError}`);
701
+
702
+ return { url: t.url, buildError };
607
703
  }
608
704
 
609
705
  // Tolerate leading markdown punctuation / code-fence backticks the agent
@@ -619,11 +715,23 @@ async function applyPreview(text) {
619
715
  if (!m) return text;
620
716
  const cmd = m[1].trim();
621
717
  const port = Number(m[2]);
622
- let url = null;
623
- try { url = await startPreview(cmd, port); } catch (e) { console.error('[preview] error:', e.message); }
624
- const replacement = url
625
- ? `🔗 Live preview: ${url}`
626
- : `(Couldn't auto-start the preview — run \`${cmd}\` in ${WORKDIR} yourself.)`;
718
+ let result = null;
719
+ try { result = await startPreview(cmd, port); } catch (e) { console.error('[preview] error:', e.message); }
720
+ let replacement;
721
+ if (!result) {
722
+ replacement = `(Couldn't auto-start the preview — run \`${cmd}\` in ${WORKDIR} yourself.)`;
723
+ } else if (result.buildError) {
724
+ // Surface the failure as plain text WITHOUT the URL — the app turns any
725
+ // URL in a message into a "Preview Build" pill, and we don't want a pill
726
+ // that just opens a blank page. The error tells the user (and the agent,
727
+ // who sees this reply) exactly what to fix.
728
+ replacement =
729
+ `⚠️ Preview build failed — the web bundle didn't compile:\n\n` +
730
+ `${result.buildError}\n\n` +
731
+ `Fix that and I'll preview again. (Dev server is still running locally on port ${port}.)`;
732
+ } else {
733
+ replacement = `🔗 Live preview: ${result.url}`;
734
+ }
627
735
  return text.replace(PREVIEW_RE, replacement);
628
736
  }
629
737
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esque-bridge",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "Desktop-side receiver for the Esque Agent mobile app. Pairs your phone with a local coding-agent CLI (Claude Code, Codex, Aider, or any custom command) via a tunnel + QR code, so prompts run through your subscription instead of per-token API billing.",
5
5
  "bin": {
6
6
  "esque-bridge": "index.js"