linear-grab-bridge 0.19.0 → 0.21.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.19.0';
31
+ const VERSION = '0.21.0';
32
32
 
33
33
  /** Best-effort command runner (git/gh introspection). Never throws. */
34
34
  function run(cmd, args, cwd = DIR) {
@@ -175,6 +175,7 @@ function summary(t) {
175
175
  subagents: t.subagents ?? 0,
176
176
  permissionMode: t.permissionMode ?? 'acceptEdits',
177
177
  worktree: t.worktree ?? null,
178
+ lastEventAt: t.tail?.length ? t.tail[t.tail.length - 1].at : (t.startedAt ?? null),
178
179
  };
179
180
  }
180
181
 
@@ -620,6 +621,80 @@ createServer(async (req, res) => {
620
621
  );
621
622
  return json(res, 200, { statuses, previews });
622
623
  }
624
+ // Reset the staging branch: delete + recreate from the default branch.
625
+ if (req.method === 'POST' && url.pathname === '/branch/reset') {
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
+ await run('gh', ['api', '-X', 'DELETE', `repos/${repo}/git/refs/heads/${base}`]);
633
+ const def = ((await run('gh', ['api', `repos/${repo}`, '-q', '.default_branch'])) ?? 'main').trim();
634
+ const sha = ((await run('gh', ['api', `repos/${repo}/git/ref/heads/${def}`, '-q', '.object.sha'])) ?? '').trim();
635
+ if (!sha) return json(res, 500, { error: `could not read ${def}` });
636
+ const created = await run('gh', ['api', '-X', 'POST', `repos/${repo}/git/refs`, '-f', `ref=refs/heads/${base}`, '-f', `sha=${sha}`]);
637
+ if (created == null) return json(res, 500, { error: `could not recreate ${base}` });
638
+ return json(res, 200, { ok: true, base, from: def });
639
+ }
640
+ // Vercel build logs for a deployment URL — the panel's terminal card.
641
+ if (req.method === 'POST' && url.pathname === '/deploy/logs') {
642
+ const body = await readBody(req);
643
+ const deployUrl = String(body.deployUrl ?? '');
644
+ if (!/^https:\/\/[\w.-]+\.vercel\.app/.test(deployUrl))
645
+ return json(res, 400, { error: 'invalid deployment url' });
646
+ const out = await new Promise((resolve) => {
647
+ execFile(
648
+ 'vercel',
649
+ ['inspect', deployUrl, '--logs'],
650
+ { timeout: 30_000, maxBuffer: 4_000_000 },
651
+ (err, stdout, stderr) => resolve(stdout || stderr || (err ? String(err.message) : '')),
652
+ );
653
+ });
654
+ return json(res, 200, { logs: String(out).split('\n').slice(-400).join('\n') });
655
+ }
656
+ // Live status of the staging deploy: Vercel mirrors every branch deploy
657
+ // into GitHub Deployments (state + environment_url) — pollable via gh.
658
+ if (req.method === 'POST' && url.pathname === '/branch/status') {
659
+ const body = await readBody(req);
660
+ const prUrl = String(body.url ?? '');
661
+ const base = String(body.base || 'staging').replace(/[^\w./-]/g, '');
662
+ const m = prUrl.match(/^https:\/\/github\.com\/([\w.-]+)\/([\w.-]+)\/pull\/\d+$/);
663
+ if (!m) return json(res, 400, { error: 'invalid PR url' });
664
+ const repo = `${m[1]}/${m[2]}`;
665
+ try {
666
+ const deps = JSON.parse(
667
+ (await run('gh', ['api', `repos/${repo}/deployments?ref=${base}&per_page=1`])) ?? '[]',
668
+ );
669
+ if (!deps.length) {
670
+ // Not every Vercel project populates GitHub Deployments — the commit
671
+ // status ('vercel' context) is the reliable fallback.
672
+ const combined = JSON.parse(
673
+ (await run('gh', ['api', `repos/${repo}/commits/${base}/status`])) ?? '{}',
674
+ );
675
+ const st = (combined.statuses ?? []).find((x) => /vercel/i.test(x.context ?? '')) ?? null;
676
+ const map = { success: 'success', pending: 'in_progress', failure: 'failure', error: 'error' };
677
+ return json(res, 200, {
678
+ state: st ? (map[st.state] ?? st.state) : 'none',
679
+ url: st?.target_url ?? null,
680
+ at: st?.updated_at ?? null,
681
+ sha: (combined.sha ?? '').slice(0, 7),
682
+ });
683
+ }
684
+ const statuses = JSON.parse(
685
+ (await run('gh', ['api', `repos/${repo}/deployments/${deps[0].id}/statuses?per_page=1`])) ?? '[]',
686
+ );
687
+ const st = statuses[0] ?? null;
688
+ return json(res, 200, {
689
+ state: st?.state ?? 'pending', // pending | in_progress | success | failure | error
690
+ url: st?.environment_url ?? null,
691
+ at: st?.updated_at ?? deps[0].created_at,
692
+ sha: (deps[0].sha ?? '').slice(0, 7),
693
+ });
694
+ } catch {
695
+ return json(res, 200, { state: 'unknown' });
696
+ }
697
+ }
623
698
  // Merge a PR's branch into a staging branch (creating it from the default
624
699
  // branch if missing) — Vercel then deploys it to the staging domain.
625
700
  if (req.method === 'POST' && url.pathname === '/pr/stage') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linear-grab-bridge",
3
- "version": "0.19.0",
3
+ "version": "0.21.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": {