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.
- package/index.js +94 -0
- 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.
|
|
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"
|