esque-bridge 0.6.5 → 0.6.7

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 +94 -0
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -687,8 +687,102 @@ async function probeBuildError(port) {
687
687
  return scanOutputForError(preview && preview.output);
688
688
  }
689
689
 
690
+ // Run one `sh -c <cmd>` inside the workdir, streaming a labelled copy of its
691
+ // output and keeping a rolling tail we can scan afterwards. Self-kills after
692
+ // timeoutMs so a wedged install can't hang the preview forever. Resolves
693
+ // { code, out } (code -1 on timeout/spawn error).
694
+ function runInWorkdir(cmd, label, timeoutMs) {
695
+ return new Promise((resolve) => {
696
+ const proc = spawn('sh', ['-c', cmd], { cwd: WORKDIR, env: process.env, stdio: ['ignore', 'pipe', 'pipe'] });
697
+ let out = '';
698
+ const onData = (d) => { out = (out + d).slice(-20000); process.stdout.write(`[${label}] ${d}`); };
699
+ proc.stdout.on('data', onData);
700
+ proc.stderr.on('data', onData);
701
+ const timer = setTimeout(() => { try { proc.kill('SIGKILL'); } catch { /* already gone */ } resolve({ code: -1, out }); }, timeoutMs);
702
+ proc.on('exit', (code) => { clearTimeout(timer); resolve({ code, out }); });
703
+ proc.on('error', (e) => { clearTimeout(timer); resolve({ code: -1, out: `${out}\n${e.message}` }); });
704
+ });
705
+ }
706
+
707
+ // The agent sometimes invents a dependency version that does not exist on the
708
+ // registry (e.g. `react-native-worklets@0.0.0`), and a single bad pin makes the
709
+ // whole `npm install` abort with ETARGET/E404. Pull the offending package name
710
+ // out of npm's error text and delete it from package.json (backing the original
711
+ // up once) so the next attempt can get past it. Returns the dropped name or null.
712
+ function dropUnresolvableDep(npmOutput) {
713
+ const m =
714
+ npmOutput.match(/No matching version found for (@?[\w.\/-]+)@/i) ||
715
+ npmOutput.match(/['"]?(@?[\w.\/-]+)@[^'"\s]+['"]? is not in this registry/i);
716
+ if (!m) return null;
717
+ const name = m[1];
718
+ const pkgPath = path.join(WORKDIR, 'package.json');
719
+ try {
720
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
721
+ let dropped = false;
722
+ for (const sec of ['dependencies', 'devDependencies']) {
723
+ if (pkg[sec] && Object.prototype.hasOwnProperty.call(pkg[sec], name)) { delete pkg[sec][name]; dropped = true; }
724
+ }
725
+ if (!dropped) return null;
726
+ const bak = `${pkgPath}.esque-bak`;
727
+ if (!fs.existsSync(bak)) fs.copyFileSync(pkgPath, bak);
728
+ fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
729
+ return name;
730
+ } catch { return null; }
731
+ }
732
+
733
+ // Make sure the project's dependencies are on disk before a preview tries to
734
+ // boot its dev server. The agent scaffolds package.json but deliberately leaves
735
+ // the slow install to Esque (the blueprint says so), so the first preview would
736
+ // otherwise die with "module 'expo' is not installed" / "next: not found".
737
+ // Idempotent — skipped as soon as node_modules exists.
738
+ async function ensureDeps() {
739
+ let isNode = false, hasModules = false, isExpo = false;
740
+ try {
741
+ const pkgPath = path.join(WORKDIR, 'package.json');
742
+ isNode = fs.existsSync(pkgPath);
743
+ hasModules = fs.existsSync(path.join(WORKDIR, 'node_modules'));
744
+ if (isNode) {
745
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
746
+ isExpo = !!(pkg.dependencies && pkg.dependencies.expo);
747
+ }
748
+ } catch { isNode = false; }
749
+ if (!isNode || hasModules) return true;
750
+
751
+ console.log('[preview] installing dependencies — first preview, this can take a few minutes…');
752
+ // `--legacy-peer-deps`: AI-scaffolded RN projects routinely pin a React-18 app
753
+ // beside test tooling that peer-wants React 19, which a strict install rejects.
754
+ const INSTALL = 'npm install --legacy-peer-deps --no-audit --no-fund';
755
+
756
+ let ok = false;
757
+ // Each failed pass may expose one invented version we can drop, then retry.
758
+ // Bounded so a genuinely unfixable project can't loop forever.
759
+ for (let attempt = 0; attempt < 6; attempt++) {
760
+ const { code, out } = await runInWorkdir(INSTALL, 'npm', 360000);
761
+ if (code === 0) { ok = true; break; }
762
+ const dropped = dropUnresolvableDep(out);
763
+ if (dropped) { console.log(`[preview] "${dropped}" has no installable version — removed it and retrying…`); continue; }
764
+ console.error(`[preview] npm install failed (exit ${code}) — preview may fail`);
765
+ break;
766
+ }
767
+ if (!ok) return false;
768
+ console.log('[preview] dependencies installed');
769
+
770
+ // For Expo apps, realign every SDK-managed package to the version the SDK
771
+ // expects. The agent often mixes incompatible pins (e.g. Expo 56 with
772
+ // react-native 0.76), which compiles to a blank/broken web bundle; this makes
773
+ // the tree coherent. Non-fatal — keep the installed versions if it can't run.
774
+ if (isExpo) {
775
+ const { code } = await runInWorkdir('npx --yes expo install --fix', 'expo', 240000);
776
+ if (code === 0) console.log('[preview] aligned dependency versions to the Expo SDK');
777
+ else console.warn('[preview] could not run expo install --fix — continuing with installed versions');
778
+ }
779
+ return true;
780
+ }
781
+
690
782
  async function startPreview(cmd, port) {
691
783
  killPreview();
784
+ // Make sure node_modules exists before the dev server tries to boot.
785
+ await ensureDeps();
692
786
  console.log(`[preview] starting: ${cmd} (port ${port})`);
693
787
  const proc = spawn('sh', ['-c', cmd], { cwd: WORKDIR, env: process.env, stdio: ['ignore', 'pipe', 'pipe'] });
694
788
  preview = { port, proc, kind: null, url: null, tunnel: null, tunnelProc: null, output: '', buildError: null };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esque-bridge",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
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"