esque-bridge 0.6.9 → 0.6.10

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 +73 -0
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -611,6 +611,32 @@ const { execSync } = require('child_process');
611
611
 
612
612
  let preview = null; // { port, proc, kind, url, tunnel, tunnelProc }
613
613
 
614
+ // ── Preview persistence ───────────────────────────────────────────────
615
+ // Tunnels die with the laptop lid / a bridge restart, and the URL embedded in
616
+ // the phone's chat history can never come back (quick-tunnel URLs are random).
617
+ // Remember each workdir's last preview COMMAND so a restarted bridge can
618
+ // revive the dev server + mint a fresh tunnel — the phone then resolves the
619
+ // current URL via GET /preview instead of trusting the dead one.
620
+ const PREVIEWS_FILE = path.join(os.homedir(), '.esque-bridge-previews.json');
621
+ function savePreviewCmd(cmd, port) {
622
+ try {
623
+ let all = {};
624
+ try { all = JSON.parse(fs.readFileSync(PREVIEWS_FILE, 'utf8')); } catch { /* first run */ }
625
+ all[WORKDIR] = { cmd, port };
626
+ fs.writeFileSync(PREVIEWS_FILE, JSON.stringify(all, null, 2));
627
+ } catch (err) {
628
+ console.warn('[preview] could not persist preview command:', err.message);
629
+ }
630
+ }
631
+ function loadPreviewCmd() {
632
+ try {
633
+ const all = JSON.parse(fs.readFileSync(PREVIEWS_FILE, 'utf8'));
634
+ const e = all[WORKDIR];
635
+ if (e && typeof e.cmd === 'string' && Number.isInteger(e.port)) return e;
636
+ } catch { /* none saved */ }
637
+ return null;
638
+ }
639
+
614
640
  // Kill a spawned command's WHOLE process tree. `proc.kill()` alone signals
615
641
  // only the immediate child (`sh`/`npm`) — npm does not reliably forward
616
642
  // signals, so the actual dev server survives, keeps the port, and the next
@@ -902,6 +928,9 @@ async function startPreview(cmd, port) {
902
928
  return { url: null, buildError: deps.error };
903
929
  }
904
930
  console.log(`[preview] starting: ${cmd} (port ${port})`);
931
+ // Remember the command now (not on success): a revival attempt after a
932
+ // mid-start crash should still know what to run.
933
+ savePreviewCmd(cmd, port);
905
934
  // Own process group (POSIX) so killPreview can take down the WHOLE tree —
906
935
  // `npm run dev` does not forward signals to the real dev server. Windows
907
936
  // has no `sh`; route through cmd.exe and kill via taskkill /T.
@@ -1202,6 +1231,45 @@ function resultHandler(req, res) {
1202
1231
  res.json({ status: job.status, text: job.text });
1203
1232
  }
1204
1233
 
1234
+ // Single-flight wrapper for reviving the saved preview: GET /preview and the
1235
+ // boot-time revival can race; both should await the same start, never two.
1236
+ let previewStartPromise = null;
1237
+ function revivePreview() {
1238
+ if (preview && preview.url) return Promise.resolve({ url: preview.url, buildError: preview.buildError });
1239
+ if (!previewStartPromise) {
1240
+ const saved = loadPreviewCmd();
1241
+ if (!saved) return Promise.resolve(null);
1242
+ console.log('[preview] reviving last preview for this folder…');
1243
+ previewStartPromise = startPreview(saved.cmd, saved.port)
1244
+ .catch((e) => { console.error('[preview] revive failed:', e.message); return null; })
1245
+ .finally(() => { previewStartPromise = null; });
1246
+ }
1247
+ return previewStartPromise;
1248
+ }
1249
+
1250
+ // Current-preview resolver. The phone calls this when a stored preview URL
1251
+ // goes dead (laptop slept → tunnel gone): if the dev server isn't running,
1252
+ // we restart the last one for this folder and answer with the FRESH url.
1253
+ // Response: { ok, url?, port?, building?, buildError? } — 404 when this
1254
+ // folder has never had a preview.
1255
+ app.get('/preview', requireAuth, async (_req, res) => {
1256
+ if (preview && preview.url) {
1257
+ return res.json({ ok: true, url: preview.url, port: preview.port, buildError: preview.buildError ?? null });
1258
+ }
1259
+ if (!loadPreviewCmd() && !previewStartPromise) {
1260
+ return res.status(404).json({ ok: false, error: 'no_preview_for_this_folder' });
1261
+ }
1262
+ const result = await revivePreview();
1263
+ if (result && result.url) {
1264
+ return res.json({ ok: true, url: result.url, port: preview?.port ?? null, buildError: result.buildError ?? null });
1265
+ }
1266
+ return res.status(503).json({
1267
+ ok: false,
1268
+ error: 'preview_failed_to_start',
1269
+ buildError: (result && result.buildError) || null,
1270
+ });
1271
+ });
1272
+
1205
1273
  app.post('/execute', requireAuth, executeHandler);
1206
1274
  app.post('/', requireAuth, executeHandler);
1207
1275
  app.get('/result/:id', requireAuth, resultHandler);
@@ -1384,6 +1452,11 @@ async function main() {
1384
1452
  console.log(' Press Ctrl-C to stop.');
1385
1453
  console.log('━'.repeat(68));
1386
1454
 
1455
+ // Revive this folder's last preview in the background, so a "Preview
1456
+ // Build" pill already sitting in the phone's chat resolves again right
1457
+ // after a restart (the app asks GET /preview for the fresh URL).
1458
+ if (loadPreviewCmd()) revivePreview();
1459
+
1387
1460
  let shuttingDown = false;
1388
1461
  const shutdown = (signal) => {
1389
1462
  shuttingDown = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esque-bridge",
3
- "version": "0.6.9",
3
+ "version": "0.6.10",
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"