td-ai-tools 1.2.2 → 1.2.3
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 +2 -0
- package/lib/installer.js +27 -17
- package/lib/project-hooks.js +171 -0
- package/package.json +2 -2
- package/skills/README.md +1 -3
- package/skills/shopify-lint/SKILL.md +4 -1
- package/skills/shopify-lint/hooks/claude-stop.sh +49 -0
- package/skills/shopify-lint/hooks/codex-stop.sh +49 -0
- package/skills/shopify-lint/hooks/manifest.json +34 -0
- package/skills/shopify-lint/scripts/__pycache__/shopify_lint.cpython-312.pyc +0 -0
- package/skills/shopify-lint/scripts/setup.sh +2 -0
- package/skills/shopify-lint/tests/__pycache__/test_shopify_lint.cpython-312.pyc +0 -0
- package/skills/car-ticket-generator/SKILL.md +0 -131
- package/skills/car-ticket-generator/agents/openai.yaml +0 -4
- package/skills/everhour-basecamp-estimates/.env.example +0 -2
- package/skills/everhour-basecamp-estimates/SKILL.md +0 -75
- package/skills/everhour-basecamp-estimates/agents/openai.yaml +0 -4
- package/skills/everhour-basecamp-estimates/scripts/update_estimates.py +0 -654
- package/skills/everhour-basecamp-estimates/tests/test_update_estimates.py +0 -286
- package/skills/playwright-cli/PRIMER.md +0 -122
- package/skills/visual-regression/scripts/__pycache__/visual_regression.cpython-312.pyc +0 -0
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
import importlib.util
|
|
2
|
-
import json
|
|
3
|
-
import sys
|
|
4
|
-
import unittest
|
|
5
|
-
from decimal import Decimal
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from unittest import mock
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
SCRIPT_PATH = (
|
|
11
|
-
Path(__file__).resolve().parents[1] / "scripts" / "update_estimates.py"
|
|
12
|
-
)
|
|
13
|
-
SPEC = importlib.util.spec_from_file_location("update_estimates", SCRIPT_PATH)
|
|
14
|
-
MODULE = importlib.util.module_from_spec(SPEC)
|
|
15
|
-
assert SPEC.loader is not None
|
|
16
|
-
sys.modules[SPEC.name] = MODULE
|
|
17
|
-
SPEC.loader.exec_module(MODULE)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class ParseHoursInputTest(unittest.TestCase):
|
|
21
|
-
def test_scalar_hours_are_reused_for_each_todo(self) -> None:
|
|
22
|
-
estimates = MODULE.parse_hours_input("2.5", 3)
|
|
23
|
-
|
|
24
|
-
self.assertEqual(
|
|
25
|
-
estimates,
|
|
26
|
-
[
|
|
27
|
-
(Decimal("2.5"), 9000),
|
|
28
|
-
(Decimal("2.5"), 9000),
|
|
29
|
-
(Decimal("2.5"), 9000),
|
|
30
|
-
],
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
def test_json_array_hours_map_one_to_one(self) -> None:
|
|
34
|
-
estimates = MODULE.parse_hours_input("[1,2.5,\"4\"]", 3)
|
|
35
|
-
|
|
36
|
-
self.assertEqual(
|
|
37
|
-
estimates,
|
|
38
|
-
[
|
|
39
|
-
(Decimal("1"), 3600),
|
|
40
|
-
(Decimal("2.5"), 9000),
|
|
41
|
-
(Decimal("4"), 14400),
|
|
42
|
-
],
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
def test_json_array_length_must_match_active_todos(self) -> None:
|
|
46
|
-
with self.assertRaises(MODULE.ScriptError) as caught:
|
|
47
|
-
MODULE.parse_hours_input("[1,2.5]", 3)
|
|
48
|
-
|
|
49
|
-
self.assertIn("expected 3, got 2", str(caught.exception))
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
class BuildPlanTest(unittest.TestCase):
|
|
53
|
-
def test_rejects_duplicate_normalized_basecamp_titles(self) -> None:
|
|
54
|
-
todos = [
|
|
55
|
-
MODULE.TodoItem(id="1", title="Ship estimate"),
|
|
56
|
-
MODULE.TodoItem(id="2", title="Ship estimate [1h]"),
|
|
57
|
-
]
|
|
58
|
-
estimates = [
|
|
59
|
-
(Decimal("1"), 3600),
|
|
60
|
-
(Decimal("2"), 7200),
|
|
61
|
-
]
|
|
62
|
-
everhour = mock.Mock()
|
|
63
|
-
everhour.match_task.return_value = {"id": "abc123", "name": "Ship estimate"}
|
|
64
|
-
|
|
65
|
-
with self.assertRaises(MODULE.ScriptError) as caught:
|
|
66
|
-
MODULE.build_plan(todos, estimates, everhour)
|
|
67
|
-
|
|
68
|
-
self.assertIn("duplicates todo 1", str(caught.exception))
|
|
69
|
-
everhour.match_task.assert_called_once_with("Ship estimate", source_id="1")
|
|
70
|
-
|
|
71
|
-
def test_rejects_duplicate_everhour_task_matches(self) -> None:
|
|
72
|
-
todos = [
|
|
73
|
-
MODULE.TodoItem(id="1", title="First task"),
|
|
74
|
-
MODULE.TodoItem(id="2", title="Second task"),
|
|
75
|
-
]
|
|
76
|
-
estimates = [
|
|
77
|
-
(Decimal("1"), 3600),
|
|
78
|
-
(Decimal("2"), 7200),
|
|
79
|
-
]
|
|
80
|
-
everhour = mock.Mock()
|
|
81
|
-
everhour.match_task.side_effect = [
|
|
82
|
-
{"id": "abc123", "name": "Shared task"},
|
|
83
|
-
{"id": "abc123", "name": "Shared task"},
|
|
84
|
-
]
|
|
85
|
-
|
|
86
|
-
with self.assertRaises(MODULE.ScriptError) as caught:
|
|
87
|
-
MODULE.build_plan(todos, estimates, everhour)
|
|
88
|
-
|
|
89
|
-
self.assertIn("todo 1 and todo 2", str(caught.exception))
|
|
90
|
-
self.assertEqual(everhour.match_task.call_count, 2)
|
|
91
|
-
|
|
92
|
-
def test_passes_basecamp_todo_id_to_everhour_matcher(self) -> None:
|
|
93
|
-
todos = [MODULE.TodoItem(id="1", title="Ship estimate")]
|
|
94
|
-
estimates = [(Decimal("1"), 3600)]
|
|
95
|
-
everhour = mock.Mock()
|
|
96
|
-
everhour.match_task.return_value = {
|
|
97
|
-
"id": "abc123",
|
|
98
|
-
"name": "Ship estimate",
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
plans = MODULE.build_plan(todos, estimates, everhour)
|
|
102
|
-
|
|
103
|
-
everhour.match_task.assert_called_once_with("Ship estimate", source_id="1")
|
|
104
|
-
self.assertEqual(plans[0].everhour_task_id, "abc123")
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
class EverhourClientMatchTaskTest(unittest.TestCase):
|
|
108
|
-
def test_duplicate_open_matches_are_resolved_by_basecamp_todo_id(self) -> None:
|
|
109
|
-
client = MODULE.EverhourClient(api_key="key", project_id="project")
|
|
110
|
-
client.list_open_tasks = mock.Mock(
|
|
111
|
-
return_value={
|
|
112
|
-
"ship estimate": [
|
|
113
|
-
{"id": "b3:1", "name": "Ship estimate"},
|
|
114
|
-
{"id": "b3:2", "name": "Ship estimate"},
|
|
115
|
-
],
|
|
116
|
-
}
|
|
117
|
-
)
|
|
118
|
-
client.search_tasks = mock.Mock(return_value=[])
|
|
119
|
-
|
|
120
|
-
task = client.match_task("Ship estimate", source_id="2")
|
|
121
|
-
|
|
122
|
-
self.assertEqual(task["id"], "b3:2")
|
|
123
|
-
client.search_tasks.assert_not_called()
|
|
124
|
-
|
|
125
|
-
def test_duplicate_open_matches_still_fail_without_todo_id_match(self) -> None:
|
|
126
|
-
client = MODULE.EverhourClient(api_key="key", project_id="project")
|
|
127
|
-
client.list_open_tasks = mock.Mock(
|
|
128
|
-
return_value={
|
|
129
|
-
"ship estimate": [
|
|
130
|
-
{"id": "b3:1", "name": "Ship estimate"},
|
|
131
|
-
{"id": "b3:2", "name": "Ship estimate"},
|
|
132
|
-
],
|
|
133
|
-
}
|
|
134
|
-
)
|
|
135
|
-
client.search_tasks = mock.Mock(
|
|
136
|
-
return_value=[
|
|
137
|
-
{"id": "b3:1", "name": "Ship estimate"},
|
|
138
|
-
{"id": "b3:2", "name": "Ship estimate"},
|
|
139
|
-
]
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
with self.assertRaises(MODULE.ScriptError) as caught:
|
|
143
|
-
client.match_task("Ship estimate", source_id="3")
|
|
144
|
-
|
|
145
|
-
self.assertIn("Basecamp todo 3", str(caught.exception))
|
|
146
|
-
|
|
147
|
-
def test_duplicate_search_matches_are_resolved_by_basecamp_todo_id(self) -> None:
|
|
148
|
-
client = MODULE.EverhourClient(api_key="key", project_id="project")
|
|
149
|
-
client.list_open_tasks = mock.Mock(return_value={})
|
|
150
|
-
client.search_tasks = mock.Mock(
|
|
151
|
-
return_value=[
|
|
152
|
-
{"id": "b3:1", "name": "Ship estimate"},
|
|
153
|
-
{"id": "b3:2", "name": "Ship estimate"},
|
|
154
|
-
]
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
task = client.match_task("Ship estimate", source_id="2")
|
|
158
|
-
|
|
159
|
-
self.assertEqual(task["id"], "b3:2")
|
|
160
|
-
client.search_tasks.assert_called_once_with("Ship estimate")
|
|
161
|
-
|
|
162
|
-
def test_basecamp_todo_id_can_match_task_url(self) -> None:
|
|
163
|
-
task = {
|
|
164
|
-
"id": "opaque",
|
|
165
|
-
"name": "Ship estimate",
|
|
166
|
-
"url": "https://3.basecamp.com/1/buckets/2/todos/123",
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
self.assertTrue(MODULE.task_matches_source_id(task, "123"))
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
class UpdateBasecampTitleTest(unittest.TestCase):
|
|
173
|
-
def test_fetches_current_todo_then_puts_with_only_changed_content(self) -> None:
|
|
174
|
-
todo = MODULE.TodoItem(
|
|
175
|
-
id="123",
|
|
176
|
-
title="Ship estimate",
|
|
177
|
-
api_url="https://3.basecampapi.com/1/buckets/2/todos/123.json",
|
|
178
|
-
)
|
|
179
|
-
current_payload = {
|
|
180
|
-
"id": 123,
|
|
181
|
-
"content": "Ship estimate",
|
|
182
|
-
"description": "<div>Original notes with image</div>",
|
|
183
|
-
"assignees": [{"id": 7}, {"id": 8}],
|
|
184
|
-
"completion_subscribers": [{"id": 9}],
|
|
185
|
-
"due_on": "2026-05-01",
|
|
186
|
-
"starts_on": None,
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
with mock.patch.object(MODULE, "run_basecamp_json") as run_basecamp_json:
|
|
190
|
-
run_basecamp_json.side_effect = [current_payload, {}]
|
|
191
|
-
MODULE.update_basecamp_title("basecamp", todo, "Ship estimate [2h]")
|
|
192
|
-
|
|
193
|
-
self.assertEqual(run_basecamp_json.call_count, 2)
|
|
194
|
-
get_call, put_call = run_basecamp_json.call_args_list
|
|
195
|
-
|
|
196
|
-
self.assertEqual(
|
|
197
|
-
get_call.args,
|
|
198
|
-
(
|
|
199
|
-
"basecamp",
|
|
200
|
-
"api",
|
|
201
|
-
"get",
|
|
202
|
-
"https://3.basecampapi.com/1/buckets/2/todos/123.json",
|
|
203
|
-
),
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
self.assertEqual(put_call.args[:5], (
|
|
207
|
-
"basecamp",
|
|
208
|
-
"api",
|
|
209
|
-
"put",
|
|
210
|
-
"https://3.basecampapi.com/1/buckets/2/todos/123.json",
|
|
211
|
-
"-d",
|
|
212
|
-
))
|
|
213
|
-
body = json.loads(put_call.args[5])
|
|
214
|
-
self.assertEqual(body["content"], "Ship estimate [2h]")
|
|
215
|
-
self.assertEqual(body["description"], "<div>Original notes with image</div>")
|
|
216
|
-
self.assertEqual(body["assignee_ids"], [7, 8])
|
|
217
|
-
self.assertEqual(body["completion_subscriber_ids"], [9])
|
|
218
|
-
self.assertEqual(body["due_on"], "2026-05-01")
|
|
219
|
-
self.assertNotIn("starts_on", body)
|
|
220
|
-
|
|
221
|
-
def test_omits_fields_absent_from_current_todo(self) -> None:
|
|
222
|
-
todo = MODULE.TodoItem(
|
|
223
|
-
id="123",
|
|
224
|
-
title="Ship estimate",
|
|
225
|
-
api_url="https://3.basecampapi.com/1/buckets/2/todos/123.json",
|
|
226
|
-
)
|
|
227
|
-
current_payload = {
|
|
228
|
-
"id": 123,
|
|
229
|
-
"content": "Ship estimate",
|
|
230
|
-
"description": "",
|
|
231
|
-
"assignees": [],
|
|
232
|
-
"completion_subscribers": [],
|
|
233
|
-
"due_on": None,
|
|
234
|
-
"starts_on": None,
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
with mock.patch.object(MODULE, "run_basecamp_json") as run_basecamp_json:
|
|
238
|
-
run_basecamp_json.side_effect = [current_payload, {}]
|
|
239
|
-
MODULE.update_basecamp_title("basecamp", todo, "Ship estimate [2h]")
|
|
240
|
-
|
|
241
|
-
put_call = run_basecamp_json.call_args_list[1]
|
|
242
|
-
body = json.loads(put_call.args[5])
|
|
243
|
-
self.assertEqual(body, {"content": "Ship estimate [2h]"})
|
|
244
|
-
|
|
245
|
-
def test_rejects_missing_basecamp_api_url(self) -> None:
|
|
246
|
-
todo = MODULE.TodoItem(id="123", title="Ship estimate")
|
|
247
|
-
|
|
248
|
-
with self.assertRaises(MODULE.ScriptError) as caught:
|
|
249
|
-
MODULE.update_basecamp_title("basecamp", todo, "Ship estimate [2h]")
|
|
250
|
-
|
|
251
|
-
self.assertIn("did not include an API URL", str(caught.exception))
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
class BuildBasecampUpdatePayloadTest(unittest.TestCase):
|
|
255
|
-
def test_extracts_assignee_and_subscriber_ids_and_preserves_dates(self) -> None:
|
|
256
|
-
current = {
|
|
257
|
-
"content": "Old",
|
|
258
|
-
"description": "<p>note</p>",
|
|
259
|
-
"assignees": [{"id": 1}, {"id": 2}],
|
|
260
|
-
"completion_subscribers": [{"id": 3}],
|
|
261
|
-
"due_on": "2026-05-01",
|
|
262
|
-
"starts_on": "2026-04-01",
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
payload = MODULE.build_basecamp_update_payload(current, "New title")
|
|
266
|
-
|
|
267
|
-
self.assertEqual(
|
|
268
|
-
payload,
|
|
269
|
-
{
|
|
270
|
-
"content": "New title",
|
|
271
|
-
"description": "<p>note</p>",
|
|
272
|
-
"assignee_ids": [1, 2],
|
|
273
|
-
"completion_subscriber_ids": [3],
|
|
274
|
-
"due_on": "2026-05-01",
|
|
275
|
-
"starts_on": "2026-04-01",
|
|
276
|
-
},
|
|
277
|
-
)
|
|
278
|
-
|
|
279
|
-
def test_skips_empty_or_missing_fields(self) -> None:
|
|
280
|
-
payload = MODULE.build_basecamp_update_payload({"content": "Old"}, "New title")
|
|
281
|
-
|
|
282
|
-
self.assertEqual(payload, {"content": "New title"})
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if __name__ == "__main__":
|
|
286
|
-
unittest.main()
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
# playwright-cli — first-run primer
|
|
2
|
-
|
|
3
|
-
Read this once per machine/clone before using the `playwright-cli` skill. It
|
|
4
|
-
covers the one gotcha that breaks the first run, how to configure each agent
|
|
5
|
-
runtime, and how to verify everything works. Delete this file and any reference
|
|
6
|
-
to it after following its instructions.
|
|
7
|
-
|
|
8
|
-
## TL;DR
|
|
9
|
-
|
|
10
|
-
```bash
|
|
11
|
-
# 1. binary present?
|
|
12
|
-
command -v playwright-cli || npm i -g @playwright/cli
|
|
13
|
-
|
|
14
|
-
# 2. browser present? (Playwright-managed chromium, not branded Chrome)
|
|
15
|
-
ls "${PLAYWRIGHT_BROWSERS_PATH:-$HOME/.cache/ms-playwright}"/chromium-* >/dev/null 2>&1 \
|
|
16
|
-
|| npx playwright install chromium
|
|
17
|
-
|
|
18
|
-
# 3. write per-agent config (choose one)
|
|
19
|
-
.agents/skills/playwright-cli/setup.sh # shared .agents + Claude settings
|
|
20
|
-
.agents/skills/playwright-cli/setup.sh --all # shared .agents + Claude + Codex
|
|
21
|
-
|
|
22
|
-
# 4. smoke test
|
|
23
|
-
PLAYWRIGHT_MCP_BROWSER=chromium playwright-cli open
|
|
24
|
-
playwright-cli goto https://example.com
|
|
25
|
-
playwright-cli eval "document.title" # -> "Example Domain"
|
|
26
|
-
playwright-cli close
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## The one gotcha
|
|
30
|
-
|
|
31
|
-
`playwright-cli open` defaults to **branded Chrome** at
|
|
32
|
-
`/opt/google/chrome/chrome`. On most dev boxes (including WSL) that binary does
|
|
33
|
-
not exist, so the bare command fails with:
|
|
34
|
-
|
|
35
|
-
```
|
|
36
|
-
Error: Chromium distribution 'chrome' is not found at /opt/google/chrome/chrome
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
What *is* installed is the **Playwright-managed chromium** under
|
|
40
|
-
`~/.cache/ms-playwright`. Point the default browser at it in one of two ways:
|
|
41
|
-
|
|
42
|
-
- per-command flag: `playwright-cli open --browser=chromium`
|
|
43
|
-
- environment variable: `PLAYWRIGHT_MCP_BROWSER=chromium` (this is what
|
|
44
|
-
`setup.sh` pins for you — `BROWSER=chromium` does **not** work, only
|
|
45
|
-
`PLAYWRIGHT_MCP_BROWSER`).
|
|
46
|
-
|
|
47
|
-
If chromium isn't installed yet: `npx playwright install chromium`. (This repo
|
|
48
|
-
already uses `@playwright/test`, so it's normally present.)
|
|
49
|
-
|
|
50
|
-
## Per-agent configuration
|
|
51
|
-
|
|
52
|
-
Run `setup.sh` from the repo root. The script is installed into both
|
|
53
|
-
`.agents/skills/playwright-cli/setup.sh` and
|
|
54
|
-
`.claude/skills/playwright-cli/setup.sh`; prefer the `.agents` path when working
|
|
55
|
-
provider-agnostically. With no flags it writes `.claude` + `.agents`; pass
|
|
56
|
-
`--codex` or `--all` to also write the Codex sandbox config.
|
|
57
|
-
|
|
58
|
-
| Target | File written | What it does |
|
|
59
|
-
|---|---|---|
|
|
60
|
-
| `--claude` | `.claude/settings.json` | Sets `env.PLAYWRIGHT_MCP_BROWSER=chromium` and adds `Bash(playwright-cli:*)` to `permissions.allow`. Merges into any existing file (invalid JSON is backed up to `.bak`). Applies on the **next** Claude Code session. |
|
|
61
|
-
| `--agents` | `.agents/playwright-cli.env` | Shareable env file. Load with `set -a; . .agents/playwright-cli.env; set +a`. |
|
|
62
|
-
| `--codex` | `.codex/config.toml` | Codex sandbox: `network_access`, `writable_roots` (browser cache + artifact dirs), and `PLAYWRIGHT_MCP_BROWSER=chromium`. Generated from `config.toml.template`. |
|
|
63
|
-
|
|
64
|
-
All three output dirs (`.claude/`, `.agents/`, `.codex/`) are gitignored, so
|
|
65
|
-
each clone generates its own — nothing machine-specific is committed.
|
|
66
|
-
|
|
67
|
-
### Why config differs by agent
|
|
68
|
-
|
|
69
|
-
- **Claude Code** reads `.claude/settings.json` (`env` + `permissions`). It does
|
|
70
|
-
not read `.codex/config.toml`. The env var is all it needs; the permission is
|
|
71
|
-
belt-and-suspenders since `SKILL.md` already declares `allowed-tools`.
|
|
72
|
-
- **Codex** parses `config.toml` as static TOML and does **not** expand `~`,
|
|
73
|
-
`$HOME`, or `${VAR}` in paths — hence the generator that substitutes absolute
|
|
74
|
-
paths at setup time.
|
|
75
|
-
- **`.agents/`** is the agent-agnostic home; any tool/shell can source the env
|
|
76
|
-
file.
|
|
77
|
-
|
|
78
|
-
## Sub-agent registration
|
|
79
|
-
|
|
80
|
-
Installing this skill registers `playwright-investigator.md` in both agent
|
|
81
|
-
layouts:
|
|
82
|
-
|
|
83
|
-
- `.claude/agents/playwright-investigator.md`
|
|
84
|
-
- `.agents/agents/playwright-investigator.md`
|
|
85
|
-
|
|
86
|
-
Claude Code discovers the `.claude` copy as a custom agent. Codex and other
|
|
87
|
-
`.agents`-aware runtimes should use the `.agents` copy as the sub-agent
|
|
88
|
-
definition when a multi-agent/sub-agent tool is available. The prompt is written
|
|
89
|
-
as a shared contract: the agent needs shell command execution, file reads, and
|
|
90
|
-
access to this `playwright-cli` skill; it returns only the final investigation
|
|
91
|
-
report.
|
|
92
|
-
|
|
93
|
-
## What the old setup got wrong
|
|
94
|
-
|
|
95
|
-
The previous `setup-codex.sh` / template were Codex-only **and** misconfigured
|
|
96
|
-
for this machine:
|
|
97
|
-
|
|
98
|
-
- redirected `PLAYWRIGHT_BROWSERS_PATH` to an empty project dir while the real
|
|
99
|
-
browsers live in `~/.cache/ms-playwright`;
|
|
100
|
-
- pinned `PLAYWRIGHT_MCP_EXECUTABLE_PATH` to `/usr/bin/chromium`, which doesn't
|
|
101
|
-
exist (there is no system chromium binary — it's Playwright-managed);
|
|
102
|
-
- overrode `HOME`, moving Playwright away from its own browser cache;
|
|
103
|
-
- never addressed the actual first-run failure (the `chrome` default).
|
|
104
|
-
|
|
105
|
-
The current setup drops all of that: it keeps the managed browsers where they
|
|
106
|
-
are and only pins the default-browser name.
|
|
107
|
-
|
|
108
|
-
## Artifacts & cleanup
|
|
109
|
-
|
|
110
|
-
`playwright-cli` writes snapshots and console/network logs to `.playwright-cli/`
|
|
111
|
-
in the repo root (gitignored). Always `playwright-cli close` when done; use
|
|
112
|
-
`playwright-cli list` / `close-all` / `kill-all` to manage stray browsers.
|
|
113
|
-
|
|
114
|
-
## Troubleshooting
|
|
115
|
-
|
|
116
|
-
| Symptom | Fix |
|
|
117
|
-
|---|---|
|
|
118
|
-
| `'chrome' is not found at /opt/google/chrome/chrome` | Use `--browser=chromium` or set `PLAYWRIGHT_MCP_BROWSER=chromium` (run `setup.sh`). |
|
|
119
|
-
| `Executable doesn't exist … run "playwright install"` | `npx playwright install chromium`. |
|
|
120
|
-
| `command not found: playwright-cli` | `npm i -g @playwright/cli`, or use `npx playwright-cli …`. |
|
|
121
|
-
| Env var ignored in Claude Code | `.claude/settings.json` is read at session start — restart the session, or pass `--browser=chromium` inline this session. |
|
|
122
|
-
| Codex sandbox can't launch browser | Re-run `setup.sh --codex` so `writable_roots` includes the current `~/.cache/ms-playwright`. |
|