aboutsummaryrefslogtreecommitdiff
path: root/src/setup.ts
blob: 561a343bdd3a124892f79b7fba92b8d90ffc6228 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
/**
 * Setup — verifies Chrome is running with remote debugging and connects
 * the pi agent to it. Exposed as both a slash command (/browser-setup) for
 * users and a tool (browser_setup) for the agent to self-recover.
 */

import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { execSync } from "node:child_process";
import type { BrowserClient } from "./client";
import { ensureDaemon } from "./daemon/spawn";

// ── Public: register the /browser-setup slash command ──────────────────────

export function registerSetupCommand(pi: ExtensionAPI, client: BrowserClient): void {
  pi.registerCommand("browser-setup", {
    description: "Connect pi to your Chrome browser",
    handler: async (_args, ctx) => {
      const result = await performSetup(client);
      if (result.success) {
        ctx.ui.notify(result.data, "info");
      } else {
        ctx.ui.notify(result.error, "error");
      }
    },
  });
}

// ── Shared setup result ────────────────────────────────────────────────────

export type SetupResult = { success: true; data: string } | { success: false; error: string };

// ── Shared setup logic (used by both the command and the tool) ─────────────

export async function performSetup(client: BrowserClient): Promise<SetupResult> {
  // Step 1: Check Chrome is running
  const chromeRunning = checkChromeRunning();
  if (!chromeRunning) {
    return { success: false, error: "No browser instance running. Please open your browser and then run /browser-setup." };
  }

  // Step 2: Start the browser daemon (spawns if not running, silently reuses if alive)
  const daemonReady = await ensureDaemon();
  if (!daemonReady) {
    return { success: false, error: "Could not start the browser daemon. Check /tmp/pi-browser-daemon.sock." };
  }

  // Step 3: Connect to Chrome DevTools
  const startResult = await client.start();
  if (!startResult.success) {
    const msg = startResult.error.message;
    const lower = msg.toLowerCase();

    if (
      lower.includes("devtoolsactiveport") ||
      lower.includes("remote debugging") ||
      lower.includes("econnrefused") ||
      lower.includes("cannot reach chrome devtools")
    ) {
      return {
        success: false,
        error:
          "Chrome remote debugging needs to be enabled.\n\n" +
          'Open chrome://inspect/#remote-debugging in your browser, tick the\n' +
          '"Discover network targets" / Allow checkbox, then retry.\n\n' +
          "Or set BU_CDP_WS to a remote browser WebSocket URL.",
      };
    }

    return { success: false, error: `Connection failed: ${msg}` };
  }

  // Step 4: Verify with test navigation
  const tabResult = await client.newTab("https://github.com");
  if (!tabResult.success) {
    return { success: false, error: `Browser connected but test navigation failed: ${tabResult.error.message}` };
  }

  const info = await client.pageInfo();
  if (info.success && "dialog" in info.data) {
    await client.session().call("Page.handleJavaScriptDialog", { accept: true });
  }

  const pageUrl = info.success && !("dialog" in info.data) ? info.data.url : "github.com";
  return { success: true, data: `Browser connected ✓\nNavigated to: ${pageUrl}` };
}

// ── Chrome process detection ───────────────────────────────────────────────

function checkChromeRunning(): boolean {
  try {
    if (process.platform === "darwin") {
      // ps -o comm= returns the full executable path on modern macOS.
      // We check for "Google Chrome" anywhere in the path (case-insensitive),
      // excluding helper/renderer subprocesses.
      const out = execSync("ps -A -o comm=", { timeout: 5000 }).toString().toLowerCase();
      const lines = out.split("\n").map((l) => l.trim()).filter(Boolean);
      return lines.some((l) => {
        if (l.includes("helper") || l.includes("renderer") || l.includes("crashpad")) return false;
        return l.includes("google chrome") || l.includes("chromium") || l.includes("microsoft edge");
      });
    } else if (process.platform === "linux") {
      const out = execSync("ps -A -o comm=,args=", { timeout: 5000 }).toString().toLowerCase();
      const lines = out.split("\n").map((l) => l.trim()).filter(Boolean);
      const browserComms = ["chrome", "chromium", "chromium-browser", "msedge", "microsoft-edge", "google-chrome"];
      return lines.some((line) => {
        const parts = line.split(/\s+/);
        const comm = parts[0] ?? "";
        if (!comm || !browserComms.includes(comm)) return false;
        const isSubprocess = line.includes("--type=") && !line.includes("--type=browser");
        return !isSubprocess;
      });
    } else if (process.platform === "win32") {
      const out = execSync("tasklist", { timeout: 5000 }).toString().toLowerCase();
      return ["chrome.exe", "msedge.exe"].some((n) => out.includes(n));
    }
  } catch {
    // best-effort
  }
  return false;
}