linear-grab-bridge 0.15.1 → 0.19.0

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.
@@ -28,7 +28,7 @@ const flag = (name, fallback) => {
28
28
  const PORT = Number(flag('--port', '4577'));
29
29
  const DIR = flag('--dir', process.cwd());
30
30
  const CLAUDE_BIN = flag('--claude', 'claude');
31
- const VERSION = '0.15.1';
31
+ const VERSION = '0.19.0';
32
32
 
33
33
  /** Best-effort command runner (git/gh introspection). Never throws. */
34
34
  function run(cmd, args, cwd = DIR) {
@@ -588,6 +588,7 @@ createServer(async (req, res) => {
588
588
  const body = await readBody(req);
589
589
  const urls = (Array.isArray(body.urls) ? body.urls : []).slice(0, 20);
590
590
  const statuses = {};
591
+ const previews = {};
591
592
  await Promise.all(
592
593
  urls.map(async (u) => {
593
594
  const prUrl = String(u);
@@ -595,21 +596,58 @@ createServer(async (req, res) => {
595
596
  const cached = prStatusCache.get(prUrl);
596
597
  if (cached && Date.now() - cached.at < 60_000) {
597
598
  statuses[prUrl] = cached.state;
599
+ if (cached.preview) previews[prUrl] = cached.preview;
598
600
  return;
599
601
  }
600
- const out = await run('gh', ['pr', 'view', prUrl, '--json', 'state']);
602
+ // comments carry the Vercel bot's preview link; the PR body often
603
+ // has it too (agents embed it per the completion gate).
604
+ const out = await run('gh', ['pr', 'view', prUrl, '--json', 'state,comments,body']);
601
605
  try {
602
- const state = JSON.parse(out ?? '').state;
606
+ const parsed = JSON.parse(out ?? '');
607
+ const state = parsed.state;
608
+ const haystack =
609
+ (parsed.comments ?? []).map((c) => c.body ?? '').join('\n') + '\n' + (parsed.body ?? '');
610
+ const preview = (haystack.match(/https:\/\/[a-z0-9-]+\.vercel\.app[^\s)"\]]*/i) ?? [])[0] ?? null;
603
611
  if (state) {
604
- prStatusCache.set(prUrl, { state, at: Date.now() });
612
+ prStatusCache.set(prUrl, { state, preview, at: Date.now() });
605
613
  statuses[prUrl] = state;
614
+ if (preview) previews[prUrl] = preview;
606
615
  }
607
616
  } catch {
608
617
  /* gh missing / not accessible */
609
618
  }
610
619
  }),
611
620
  );
612
- return json(res, 200, { statuses });
621
+ return json(res, 200, { statuses, previews });
622
+ }
623
+ // Merge a PR's branch into a staging branch (creating it from the default
624
+ // branch if missing) — Vercel then deploys it to the staging domain.
625
+ if (req.method === 'POST' && url.pathname === '/pr/stage') {
626
+ const body = await readBody(req);
627
+ const prUrl = String(body.url ?? '');
628
+ const base = String(body.base || 'staging').replace(/[^\w./-]/g, '');
629
+ const m = prUrl.match(/^https:\/\/github\.com\/([\w.-]+)\/([\w.-]+)\/pull\/\d+$/);
630
+ if (!m) return json(res, 400, { error: 'invalid PR url' });
631
+ const repo = `${m[1]}/${m[2]}`;
632
+ let head = null;
633
+ try {
634
+ head = JSON.parse((await run('gh', ['pr', 'view', prUrl, '--json', 'headRefName'])) ?? '').headRefName;
635
+ } catch {
636
+ /* fallthrough */
637
+ }
638
+ if (!head) return json(res, 500, { error: 'could not resolve the PR head branch (is gh authenticated?)' });
639
+ const baseExists = await run('gh', ['api', `repos/${repo}/branches/${base}`]);
640
+ if (baseExists == null) {
641
+ const def = ((await run('gh', ['api', `repos/${repo}`, '-q', '.default_branch'])) ?? 'main').trim();
642
+ const sha = ((await run('gh', ['api', `repos/${repo}/git/ref/heads/${def}`, '-q', '.object.sha'])) ?? '').trim();
643
+ if (!sha) return json(res, 500, { error: `staging branch missing and could not read ${def}` });
644
+ const created = await run('gh', ['api', '-X', 'POST', `repos/${repo}/git/refs`, '-f', `ref=refs/heads/${base}`, '-f', `sha=${sha}`]);
645
+ if (created == null) return json(res, 500, { error: `could not create branch ${base}` });
646
+ }
647
+ const out = await run('gh', ['api', '-X', 'POST', `repos/${repo}/merges`, '-f', `base=${base}`, '-f', `head=${head}`]);
648
+ if (out == null)
649
+ return json(res, 409, { error: `merge of ${head} into ${base} failed — likely a conflict; resolve manually` });
650
+ return json(res, 200, { ok: true, base, head });
613
651
  }
614
652
  // Fast-merge: after reviewing the demo, one click merges the PR via gh.
615
653
  if (req.method === 'POST' && url.pathname === '/pr/merge') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linear-grab-bridge",
3
- "version": "0.15.1",
3
+ "version": "0.19.0",
4
4
  "description": "Local bridge for Linear Grab — delegate issues from the browser panel to headless Claude Code sessions running in your repo, with live status and an upload relay.",
5
5
  "type": "module",
6
6
  "bin": {