phyll 0.4.3 → 0.4.5

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/README.md CHANGED
@@ -10,12 +10,12 @@ The AI work runs in your agent, on your own plan. This package adds the browser,
10
10
 
11
11
  ```bash
12
12
  npx phyll signup you@example.com
13
- npx phyll setup codex # or: npx phyll setup claude
13
+ npx phyll setup codex # or claude, cursor, windsurf, gemini
14
14
  ```
15
15
 
16
16
  Then ask your agent: "review my app at http://localhost:3000".
17
17
 
18
- `setup` registers the connector in Codex's `config.toml` or with `claude mcp add`, and installs the Chromium build it uses. You need Node 20 or newer.
18
+ `setup` registers the connector in Codex's `config.toml`, with `claude mcp add`, or in the `mcpServers` of Cursor's `~/.cursor/mcp.json`, Windsurf's `~/.codeium/windsurf/mcp_config.json` or Gemini CLI's `~/.gemini/settings.json`, keeping everything else in the file. It also installs the Chromium build the connector uses. For any other agent that takes MCP servers, `npx phyll setup other` prints the entry to paste. You need Node 20 or newer.
19
19
 
20
20
  Your reports, keys and plan are also on the site: `npx phyll account` opens your account there, already signed in. If you lose the key, sign in at [agentphyll.com/login](https://agentphyll.com/login) with your email and create a new one.
21
21
 
@@ -25,12 +25,12 @@ Your reports, keys and plan are also on the site: `npx phyll account` opens your
25
25
  | --- | --- |
26
26
  | `signup <email>` | Create a free account, with 5 full reviews. The key is saved in `~/.phyll` |
27
27
  | `login <key>` | Use an account you already have on this computer |
28
- | `setup codex` or `setup claude` | Connect Phyll to your agent and install the browser |
28
+ | `setup <agent>` | Connect Phyll to `codex`, `claude`, `cursor`, `windsurf` or `gemini` and install the browser; `setup other` prints the settings for any other agent |
29
29
  | `status` | Your plan and the reviews left |
30
30
  | `account` | Open your account on agentphyll.com, already signed in: reports, usage, keys and plan |
31
31
  | `pro` | Subscribe to Phyll Pro, R$ 9 a month, with unlimited reviews |
32
32
  | `billing` | Change the card or cancel Phyll Pro |
33
- | `scan [folder]` | Scan the source for AI tells, with no account and no AI |
33
+ | `scan [folder]` | Scan the source for AI tells, with no account and no AI. `--format json` for scripts, `--format badge` for a README badge |
34
34
  | `mcp` | Run the connector for your agent; `setup` registers it for you |
35
35
  | `logout` | Forget the key on this computer |
36
36
 
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "phyll",
3
- "version": "0.4.3",
4
- "description": "UX review for apps built with AI, inside the agent you already use. Connects Codex or Claude Code to the Phyll engine; the AI work stays on your own plan.",
3
+ "version": "0.4.5",
4
+ "description": "UX review for apps built with AI, inside the agent you already use. Connects Codex, Claude Code, Cursor, Windsurf or Gemini CLI to the Phyll engine; the AI work stays on your own plan.",
5
+ "mcpName": "io.github.carlosphyll/phyll",
5
6
  "license": "MIT",
6
7
  "type": "module",
7
8
  "homepage": "https://github.com/carlosphyll/phyll",
@@ -14,8 +15,12 @@
14
15
  "ux",
15
16
  "ux-review",
16
17
  "mcp",
18
+ "mcp-server",
17
19
  "codex",
18
20
  "claude-code",
21
+ "cursor",
22
+ "windsurf",
23
+ "gemini-cli",
19
24
  "ai",
20
25
  "vibe-coding"
21
26
  ],
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.4.3",
2
+ "version": "0.4.5",
3
3
  "tells": [
4
4
  {
5
5
  "id": "P01",
@@ -1636,6 +1636,17 @@
1636
1636
  "<input className=\"focus:outline-none focus:ring-2\" />"
1637
1637
  ]
1638
1638
  }
1639
+ },
1640
+ {
1641
+ "id": "S07",
1642
+ "name": "No results that look like an empty list",
1643
+ "dimension": "states",
1644
+ "kind": "function",
1645
+ "severity": "major",
1646
+ "weight": 2,
1647
+ "cap": 1,
1648
+ "detection": "dynamic",
1649
+ "summary": "A search or filter that matches nothing shows the same screen as a list with nothing in it, or gives no way to clear it, so people think their data is gone or get stuck."
1639
1650
  }
1640
1651
  ]
1641
1652
  }
@@ -1,3 +1,3 @@
1
1
  // Single source for the tool name and version used in scan output and reports.
2
2
  export const NAME = "phyll";
3
- export const VERSION = "0.4.3";
3
+ export const VERSION = "0.4.5";
@@ -90,7 +90,7 @@ export function formatText(result) {
90
90
  const row = (t) => {
91
91
  const where = t.locations[0] ? `${t.locations[0].file}:${t.locations[0].line}` : "";
92
92
  const name = t.name.length > width ? t.name.slice(0, width - 1) + "." : t.name.padEnd(width);
93
- return ` ${t.id} ${name} ${String(t.hits).padStart(3)} hits ${where}`;
93
+ return ` ${t.id} ${name} ${String(t.hits).padStart(3)} ${t.hits === 1 ? "hit " : "hits"} ${where}`;
94
94
  };
95
95
 
96
96
  if (functionTells.length === 0) lines.push("No tells that get in the way of use were found in the source.");
package/src/cli.mjs CHANGED
@@ -1,22 +1,29 @@
1
1
  // phyll: the commands a person types. The review itself happens inside their agent, through
2
2
  // the MCP server that `phyll mcp` runs and `phyll setup` registers.
3
+ import { spawn } from "node:child_process";
4
+ import { homedir, hostname } from "node:os";
5
+ import { join } from "node:path";
3
6
  import { parseArgs } from "node:util";
4
7
  import { clearCredentials, credentialsPath, loadCredentials, saveCredentials } from "./credentials.mjs";
5
8
  import { engineClient } from "./engine.mjs";
6
9
  import { importSkill, VERSION } from "./paths.mjs";
7
- import { ensureBrowser, mcpCommand, registerClaude, writeCodexConfig } from "./setup.mjs";
10
+ import { ensureBrowser, JSON_AGENTS, mcpCommand, mcpJson, registerClaude, writeCodexConfig, writeJsonConfig } from "./setup.mjs";
11
+
12
+ const AGENTS = ["codex", "claude", ...Object.keys(JSON_AGENTS)];
8
13
 
9
14
  export const HELP = `Phyll ${VERSION}: UX review for apps built with AI, inside the agent you already use.
10
15
 
11
16
  Get started:
12
17
  npx phyll signup you@example.com Create a free account; the key is saved on this computer
13
- npx phyll setup codex Connect Phyll to Codex (or: npx phyll setup claude)
18
+ npx phyll setup codex Connect Phyll to Codex (or claude, cursor, windsurf, gemini)
14
19
  Then ask your agent: review my app at http://localhost:3000
15
20
 
16
21
  Commands:
17
22
  signup <email> Create a free account, with free full reviews
18
- login <key> Use an account you already have on this computer
19
- setup <agent> Connect Phyll to codex or claude, and install the browser it uses
23
+ login Use your account on this computer: allow it in the browser
24
+ login <key> Or use it with a key
25
+ setup <agent> Connect Phyll to codex, claude, cursor, windsurf or gemini, and install the
26
+ browser it uses. setup other prints the settings for any agent with MCP
20
27
  status Your plan and the reviews left
21
28
  account Open your account on the site, signed in: reports, keys and plan
22
29
  pro Subscribe to Phyll Pro
@@ -25,17 +32,48 @@ Commands:
25
32
  mcp Run the connector for your agent (setup registers it for you)
26
33
  logout Forget the key on this computer
27
34
 
28
- Options: --server <address> for signup and login, --lang <code> for signup, --format json for scan.
35
+ Options: --server <address> for signup and login, --lang <code> for signup, --format json or badge for scan.
29
36
 
30
37
  The AI work runs in your agent, on your own plan. Phyll's engine sends the method and keeps the reports.
38
+ Every command, with examples: https://agentphyll.com/commands
31
39
  `;
32
40
 
41
+ // Opens a page in the person's browser, best effort, since the address is printed too. Only the
42
+ // server's own connect page opens, so an odd answer never reaches the shell.
43
+ function openBrowser(url, server) {
44
+ if (!url.startsWith(`${server}/connect/`) || !/^[\w:/.-]+$/.test(url)) return;
45
+ const [command, args] =
46
+ process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
47
+ try {
48
+ spawn(command, args, { stdio: "ignore", detached: true }).on("error", () => {}).unref();
49
+ } catch {
50
+ // No browser here; the address above is enough.
51
+ }
52
+ }
53
+
33
54
  const guessLanguage = () => ((Intl.DateTimeFormat().resolvedOptions().locale ?? "").toLowerCase().startsWith("pt") ? "pt-BR" : "en");
34
55
 
35
56
  function options(args, spec) {
36
57
  return parseArgs({ args, allowPositionals: true, strict: true, options: spec });
37
58
  }
38
59
 
60
+ // After a scan: the full review, which sees what the source cannot show, and the badge once the
61
+ // index is low enough to show off.
62
+ export function scanNext(index) {
63
+ const lines = [
64
+ "",
65
+ "Next: a full review opens the app as a first-time user, checks these hints along with what",
66
+ "only the running app shows, and can fix what it finds without changing your design.",
67
+ " npx phyll setup codex (or claude, cursor, windsurf, gemini)",
68
+ " then ask your agent: review my app at http://localhost:3000",
69
+ ];
70
+ if (index !== null && index !== undefined && index <= 25) lines.push("Show the index in your README: npx phyll scan --format badge");
71
+ return `${lines.join("\n")}\n`;
72
+ }
73
+
74
+ // A README badge with the static index. The engine draws it, and it links to Phyll.
75
+ export const badgeMarkdown = (index, server) => `[![Phyll AI tell index: ${index}/100](${server}/badge/index/${index}.svg)](${server})`;
76
+
39
77
  export async function main(argv, io = {}) {
40
78
  const out = io.stdout ?? process.stdout;
41
79
  const err = io.stderr ?? process.stderr;
@@ -48,6 +86,28 @@ export async function main(argv, io = {}) {
48
86
  return code;
49
87
  };
50
88
  const client = (server, key) => engineClient({ server, key, version: VERSION, fetchImpl });
89
+ const sleep = io.sleep ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
90
+
91
+ // Without a key, the terminal asks the site for a code, the person allows this computer in the
92
+ // browser, signed in, and the terminal's next check brings a key of its own.
93
+ async function loginInBrowser(server) {
94
+ const started = await client(server).deviceStart((io.hostname ?? hostname)());
95
+ if (!started.ok) return fail(started.json.message ?? `Phyll answered ${started.status}.`);
96
+ const { deviceCode, userCode, url, interval = 2, expiresIn = 600 } = started.json;
97
+ write(`To use your account on this computer, allow it in the browser:\n${url}\nCheck that the page shows the code ${userCode}. Waiting...\n`);
98
+ await (io.openBrowser ?? openBrowser)(url, server);
99
+ const deadline = Date.now() + Math.min(Number(expiresIn) || 600, 900) * 1000;
100
+ while (Date.now() < deadline) {
101
+ await sleep(Math.max(1, Number(interval) || 2) * 1000);
102
+ const answer = await client(server).deviceCheck(deviceCode);
103
+ if (answer.status === 202) continue;
104
+ if (!answer.ok) return fail(answer.json.message ?? `Phyll answered ${answer.status}.`);
105
+ saveCredentials({ server, key: answer.json.key }, env);
106
+ write(`Signed in as ${answer.json.email}, on the ${answer.json.plan === "pro" ? "Phyll Pro" : "free"} plan. The key is saved in ${credentialsPath(env)}.\n`);
107
+ return 0;
108
+ }
109
+ return fail("the code expired before it was allowed. Run npx phyll login again.");
110
+ }
51
111
  const [command, ...rest] = argv;
52
112
 
53
113
  try {
@@ -72,15 +132,16 @@ export async function main(argv, io = {}) {
72
132
  const answer = await client(server, null).signup(positionals[0], values.lang ?? guessLanguage());
73
133
  if (!answer.ok) return fail(answer.json.message ?? `Phyll answered ${answer.status}.`);
74
134
  const path = saveCredentials({ server, key: answer.json.key }, env);
75
- write(`${answer.json.message}\nYour key, shown only now: ${answer.json.key}\nIt is saved in ${path}.\n\nNext, connect Phyll to your agent:\n npx phyll setup codex\n npx phyll setup claude\n`);
135
+ write(`${answer.json.message}\nYour key, shown only now: ${answer.json.key}\nIt is saved in ${path}.\n\nNext, connect Phyll to your agent:\n npx phyll setup codex (or claude, cursor, windsurf, gemini)\n`);
76
136
  return 0;
77
137
  }
78
138
 
79
139
  case "login": {
80
140
  const { values, positionals } = options(rest, { server: { type: "string" } });
81
- if (positionals.length !== 1) return fail("give your key, such as npx phyll login phyll_...", 2);
141
+ if (positionals.length > 1) return fail("give one key, such as npx phyll login phyll_..., or none to sign in with the browser", 2);
82
142
  const server = String(values.server ?? loadCredentials(env).server ?? "").replace(/\/+$/, "");
83
143
  if (!server) return fail("give the address of the Phyll server with --server", 2);
144
+ if (!positionals.length) return await loginInBrowser(server);
84
145
  const answer = await client(server, positionals[0]).me();
85
146
  if (!answer.ok) return fail(answer.json.message ?? `Phyll answered ${answer.status}.`);
86
147
  saveCredentials({ server, key: positionals[0] }, env);
@@ -95,7 +156,7 @@ export async function main(argv, io = {}) {
95
156
 
96
157
  case "status": {
97
158
  const { server, key } = loadCredentials(env);
98
- if (!key) return fail("no account on this computer yet. Run npx phyll signup you@example.com");
159
+ if (!key) return fail("no account on this computer yet. Run npx phyll login, or npx phyll signup you@example.com");
99
160
  const answer = await client(server, key).me();
100
161
  if (!answer.ok) return fail(answer.json.message ?? `Phyll answered ${answer.status}.`);
101
162
  const me = answer.json;
@@ -110,7 +171,7 @@ export async function main(argv, io = {}) {
110
171
 
111
172
  case "account": {
112
173
  const { server, key } = loadCredentials(env);
113
- if (!key) return fail(`no account on this computer yet. Run npx phyll signup you@example.com, or sign in at ${server}/login`);
174
+ if (!key) return fail("no account on this computer yet. Run npx phyll login, or npx phyll signup you@example.com");
114
175
  const answer = await client(server, key).loginLink();
115
176
  if (!answer.ok) return fail(answer.json.message ?? `Phyll answered ${answer.status}.`);
116
177
  write(`Open your account, already signed in. The link works once, for 15 minutes:\n${answer.json.url}\n`);
@@ -120,7 +181,7 @@ export async function main(argv, io = {}) {
120
181
  case "pro":
121
182
  case "billing": {
122
183
  const { server, key } = loadCredentials(env);
123
- if (!key) return fail("no account on this computer yet. Run npx phyll signup you@example.com");
184
+ if (!key) return fail("no account on this computer yet. Run npx phyll login, or npx phyll signup you@example.com");
124
185
  const answer = command === "pro" ? await client(server, key).checkout() : await client(server, key).portal();
125
186
  if (!answer.ok) return fail(answer.json.message ?? `Phyll answered ${answer.status}.`);
126
187
  write(`${command === "pro" ? "Subscribe to Phyll Pro here" : "Manage Phyll Pro here"}:\n${answer.json.url}\n`);
@@ -129,18 +190,30 @@ export async function main(argv, io = {}) {
129
190
 
130
191
  case "setup": {
131
192
  const agent = rest[0];
132
- if (!["codex", "claude"].includes(agent)) return fail("say which agent to connect: npx phyll setup codex, or npx phyll setup claude", 2);
193
+ if (![...AGENTS, "other"].includes(agent)) {
194
+ return fail("say which agent to connect: npx phyll setup codex, claude, cursor, windsurf or gemini. For any other agent with MCP: npx phyll setup other", 2);
195
+ }
133
196
  const command = io.mcpCommand ?? mcpCommand();
134
197
  if (agent === "codex") {
135
198
  const file = writeCodexConfig(command, env);
136
199
  write(`Phyll is connected to Codex in ${file}. Restart Codex so it loads the connector.\n`);
137
- } else {
200
+ } else if (agent === "claude") {
138
201
  const result = registerClaude(command, io.run);
139
202
  write(
140
203
  result.ok
141
204
  ? "Phyll is connected to Claude Code. Start a new session so it loads the connector.\n"
142
205
  : `Claude Code was not found on this computer. Run this where it is installed:\n ${result.manual}\n`,
143
206
  );
207
+ } else if (agent === "other") {
208
+ write(`Add Phyll to your agent's MCP servers, then restart the agent:\n${mcpJson(command)}\n`);
209
+ } else {
210
+ const { name, path } = JSON_AGENTS[agent];
211
+ const file = join(io.home ?? homedir(), ...path);
212
+ write(
213
+ writeJsonConfig(file, command)
214
+ ? `Phyll is connected to ${name} in ${file}. Restart ${name} so it loads the connector.\n`
215
+ : `${file} is not plain JSON, so Phyll left it as it was. Add this to its mcpServers, then restart ${name}:\n${mcpJson(command)}\n`,
216
+ );
144
217
  }
145
218
  if (!(await (io.ensureBrowser ?? ensureBrowser)({ write }))) return fail("the browser could not be installed. Run: npx playwright install chromium");
146
219
  write(
@@ -153,9 +226,14 @@ export async function main(argv, io = {}) {
153
226
 
154
227
  case "scan": {
155
228
  const { values, positionals } = options(rest, { format: { type: "string" } });
229
+ const format = values.format ?? "text";
230
+ if (!["text", "json", "badge"].includes(format)) return fail("the format is text, json or badge", 2);
156
231
  const { scan, formatText } = await importSkill("scripts/scan.mjs");
157
232
  const result = scan(positionals[0] ?? cwd);
158
- write(values.format === "json" ? `${JSON.stringify(result, null, 2)}\n` : formatText(result));
233
+ if (format === "json") write(`${JSON.stringify(result, null, 2)}\n`);
234
+ else if (format === "text") write(`${formatText(result)}${scanNext(result.staticIndex)}`);
235
+ else if (result.staticIndex === null) return fail("there is no index to show: the scan found no files it reads");
236
+ else write(`${badgeMarkdown(result.staticIndex, loadCredentials(env).server)}\n`);
159
237
  return 0;
160
238
  }
161
239
 
package/src/engine.mjs CHANGED
@@ -37,5 +37,9 @@ export function engineClient({ server, key = null, version = "0", fetchImpl = gl
37
37
  checkout: () => call("POST", "/v1/billing/checkout"),
38
38
  portal: () => call("POST", "/v1/billing/portal"),
39
39
  loginLink: () => call("POST", "/v1/login-link"),
40
+ // Signing in from the browser: the terminal asks for a code, then checks until the person
41
+ // allows it on the site.
42
+ deviceStart: (name) => call("POST", "/v1/device", { name }),
43
+ deviceCheck: (deviceCode) => call("POST", "/v1/device/token", { deviceCode }),
40
44
  };
41
45
  }
package/src/review.mjs CHANGED
@@ -100,7 +100,7 @@ export class Connector {
100
100
  await session.start();
101
101
  } catch (error) {
102
102
  const missing = /Executable doesn't exist|playwright install/i.test(error?.message ?? "");
103
- throw new Error(missing ? "the browser is not installed. Ask the person to run npx phyll setup codex (or claude) once." : `the browser did not start: ${error.message}`);
103
+ throw new Error(missing ? "the browser is not installed. Ask the person to run npx phyll setup once, with the name of their agent, such as npx phyll setup codex." : `the browser did not start: ${error.message}`);
104
104
  }
105
105
  review.browser = session;
106
106
  }
package/src/setup.mjs CHANGED
@@ -1,10 +1,11 @@
1
- // Connects Phyll to the person's agent: an MCP server entry in Codex's config.toml, or a
2
- // `claude mcp add` for Claude Code, and the Chromium build Playwright needs.
1
+ // Connects Phyll to the person's agent: an MCP server entry in Codex's config.toml, a
2
+ // `claude mcp add` for Claude Code, or an entry in the JSON file Cursor, Windsurf or Gemini CLI
3
+ // read, and the Chromium build Playwright needs.
3
4
  import { spawnSync } from "node:child_process";
4
5
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
6
  import { createRequire } from "node:module";
6
7
  import { homedir } from "node:os";
7
- import { join } from "node:path";
8
+ import { dirname, join } from "node:path";
8
9
  import { BIN, PUBLISHED } from "./paths.mjs";
9
10
 
10
11
  // How the agent starts the connector: through npx when installed from npm, or straight from
@@ -43,6 +44,38 @@ export function writeCodexConfig(command, env = process.env) {
43
44
  return file;
44
45
  }
45
46
 
47
+ // Agents that keep their MCP servers under "mcpServers" in a JSON file in the home folder.
48
+ export const JSON_AGENTS = {
49
+ cursor: { name: "Cursor", path: [".cursor", "mcp.json"] },
50
+ windsurf: { name: "Windsurf", path: [".codeium", "windsurf", "mcp_config.json"] },
51
+ gemini: { name: "Gemini CLI", path: [".gemini", "settings.json"] },
52
+ };
53
+
54
+ const serverEntry = (command) => ({ command: command[0], args: command.slice(1) });
55
+
56
+ // The entry to paste into the MCP settings of any other agent.
57
+ export const mcpJson = (command) => JSON.stringify({ mcpServers: { phyll: serverEntry(command) } }, null, 2);
58
+
59
+ // Adds phyll to mcpServers and keeps the rest of the file as it was. A file that is not plain
60
+ // JSON, such as one with comments, is left alone and the caller shows the entry to paste.
61
+ export function writeJsonConfig(file, command) {
62
+ let config = {};
63
+ if (existsSync(file)) {
64
+ const text = readFileSync(file, "utf8").replace(String.fromCharCode(0xfeff), "").trim();
65
+ try {
66
+ config = text ? JSON.parse(text) : {};
67
+ } catch {
68
+ return false;
69
+ }
70
+ if (!config || typeof config !== "object" || Array.isArray(config)) return false;
71
+ }
72
+ const servers = config.mcpServers && typeof config.mcpServers === "object" && !Array.isArray(config.mcpServers) ? config.mcpServers : {};
73
+ config.mcpServers = { ...servers, phyll: serverEntry(command) };
74
+ mkdirSync(dirname(file), { recursive: true });
75
+ writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
76
+ return true;
77
+ }
78
+
46
79
  const quote = (arg) => (/[\s"]/.test(arg) ? `"${arg.replaceAll('"', '\\"')}"` : arg);
47
80
 
48
81
  export function registerClaude(command, run = spawnSync) {
@@ -54,16 +87,23 @@ export function registerClaude(command, run = spawnSync) {
54
87
  return { ok: true };
55
88
  }
56
89
 
90
+ // Playwright's own command line. The package does not export "./cli", so it is found through
91
+ // the package.json, which it does export, and the file its "bin" names.
92
+ export function playwrightCli(require = createRequire(import.meta.url)) {
93
+ const manifest = require.resolve("playwright/package.json");
94
+ const { bin } = JSON.parse(readFileSync(manifest, "utf8"));
95
+ return join(dirname(manifest), typeof bin === "string" ? bin : bin.playwright);
96
+ }
97
+
57
98
  // Opens Chromium once; when Playwright says it is missing, installs it.
58
- export async function ensureBrowser({ write = () => {}, run = spawnSync } = {}) {
59
- const { chromium } = await import("playwright");
99
+ export async function ensureBrowser({ write = () => {}, run = spawnSync, chromium = null } = {}) {
100
+ const browser = chromium ?? (await import("playwright")).chromium;
60
101
  try {
61
- await (await chromium.launch()).close();
102
+ await (await browser.launch()).close();
62
103
  return true;
63
104
  } catch (error) {
64
105
  if (!/Executable doesn't exist|playwright install/i.test(error?.message ?? "")) throw error;
65
106
  }
66
107
  write("Installing the browser Phyll uses, Chromium (about 150 MB)...\n");
67
- const cli = createRequire(import.meta.url).resolve("playwright/cli");
68
- return run(process.execPath, [cli, "install", "chromium"], { stdio: "inherit" }).status === 0;
108
+ return run(process.execPath, [playwrightCli(), "install", "chromium"], { stdio: "inherit" }).status === 0;
69
109
  }