td-ai-tools 1.1.3 → 1.1.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
@@ -45,12 +45,22 @@ The installer copies requested items into both agent layouts:
45
45
 
46
46
  - Skills: `.claude/skills/<name>/` and `.agents/skills/<name>/`
47
47
 
48
+ If a skill bundles a sub-agent prompt (any `.md` with a `name:` field in `skills/<name>/agents/`), the installer also registers it to `.claude/agents/<sub-agent>.md` and `.agents/agents/<sub-agent>.md` so Claude Code and `.agents`-aware runtimes such as Codex can discover the same definition. `delete` removes these registrations alongside the skill.
49
+
48
50
  This keeps the installed assets available to both Claude-style and `.agents`-style project conventions.
49
51
 
50
52
  `install` now errors when the target item already exists. Use `update` to replace an existing installed skill or agent pack.
51
53
 
52
54
  `delete` removes installed items from both `.claude/` and `.agents/` target directories, and works on any installed skill or agent pack regardless of whether it is in the catalogue.
53
55
 
56
+ ## Versioning
57
+
58
+ Every skill and agent pack carries a `version:` field in its frontmatter (`SKILL.md` / `AGENTS.md`), following semantic versioning (`MAJOR.MINOR.PATCH`). Because the installer copies these files verbatim into the target project, the installed copy itself records the version that was installed — no lockfile is needed.
59
+
60
+ `list` and the interactive `update` menu compare the installed version against the catalog and flag anything out of date, e.g. `v1.0.0 → v1.2.0 ⬆ outdated`. When any installed item is stale, the CLI prints a nudge: `You have N items out of date — run npx td-ai-tools update`. An item installed before versioning existed (no `version:` field) is also treated as out of date.
61
+
62
+ **Maintainers:** bump an item's `version:` whenever you change its contents — `PATCH` for fixes/tweaks, `MINOR` for new behavior, `MAJOR` for breaking changes. Without a bump, users will not see the update flagged.
63
+
54
64
  ## Local Verification
55
65
  Run the local smoke test to package the repo and verify installation into a throwaway project:
56
66
 
@@ -1,3 +1,7 @@
1
+ ---
2
+ version: 1.0.0
3
+ ---
4
+
1
5
  # .codex/AGENTS.md — Theory Digital
2
6
 
3
7
  ## Scope
@@ -1,3 +1,7 @@
1
+ ---
2
+ version: 1.0.0
3
+ ---
4
+
1
5
  # .codex/AGENTS.md — Theory Digital
2
6
 
3
7
  ## Scope
package/bin/cli.js CHANGED
@@ -63,6 +63,121 @@ function readFrontmatterField(mdPath, field) {
63
63
  return match ? match[1].trim() : '';
64
64
  }
65
65
 
66
+ function itemFile(type) {
67
+ return type === 'skill' ? 'SKILL.md' : 'AGENTS.md';
68
+ }
69
+
70
+ function itemSubdir(type) {
71
+ return type === 'skill' ? 'skills' : 'agents';
72
+ }
73
+
74
+ function catalogVersion(type, name) {
75
+ const dir = type === 'skill' ? SKILLS_DIR : AGENTS_DIR;
76
+ return readFrontmatterField(path.join(dir, name, itemFile(type)), 'version') || null;
77
+ }
78
+
79
+ // Reads the version recorded in the user's installed copy. The installer copies
80
+ // SKILL.md/AGENTS.md verbatim, so the installed file is itself the record of what
81
+ // version was installed — no separate lockfile is needed. Returns null when the
82
+ // item is not installed, or when it predates versioning (no version: field).
83
+ function installedVersion(type, name) {
84
+ const sub = itemSubdir(type);
85
+ const file = itemFile(type);
86
+ for (const target of INSTALL_TARGETS) {
87
+ const md = path.join(TARGET_ROOT, target.root, sub, name, file);
88
+ if (fs.existsSync(md)) {
89
+ return readFrontmatterField(md, 'version') || null;
90
+ }
91
+ }
92
+ return null;
93
+ }
94
+
95
+ function compareSemver(a, b) {
96
+ const pa = String(a).split('.').map(n => parseInt(n, 10) || 0);
97
+ const pb = String(b).split('.').map(n => parseInt(n, 10) || 0);
98
+ const len = Math.max(pa.length, pb.length);
99
+ for (let i = 0; i < len; i++) {
100
+ const x = pa[i] || 0;
101
+ const y = pb[i] || 0;
102
+ if (x > y) return 1;
103
+ if (x < y) return -1;
104
+ }
105
+ return 0;
106
+ }
107
+
108
+ // An installed item is outdated when its version is older than the catalog's, or
109
+ // when it has no version: field at all (a legacy install from before versioning).
110
+ function isInstalled(type, name) {
111
+ const sub = itemSubdir(type);
112
+ return INSTALL_TARGETS.some(target =>
113
+ fs.existsSync(path.join(TARGET_ROOT, target.root, sub, name)));
114
+ }
115
+
116
+ function itemStatus(type, name) {
117
+ const available = catalogVersion(type, name);
118
+ const installed = installedVersion(type, name);
119
+ let outdated = false;
120
+ if (available && isInstalled(type, name)) {
121
+ outdated = !installed || compareSemver(installed, available) < 0;
122
+ }
123
+ return { installed, available, outdated };
124
+ }
125
+
126
+ // Display string for an installed item's version state, e.g.
127
+ // "v1.0.0 → v1.2.0 ⬆ outdated" or "v1.2.0 (up to date)". Returns null when there
128
+ // is nothing meaningful to show (item not installed, or catalog has no version).
129
+ function versionAnnotation(type, name) {
130
+ if (!isInstalled(type, name)) return null;
131
+ const { installed, available, outdated } = itemStatus(type, name);
132
+ if (!available) return null;
133
+ if (outdated) {
134
+ const from = installed ? `v${installed}` : 'no version';
135
+ return { text: `${from} → v${available} ⬆ outdated`, outdated: true };
136
+ }
137
+ return { text: `v${installed} (up to date)`, outdated: false };
138
+ }
139
+
140
+ function getOutdatedInstalled() {
141
+ const availableSkills = new Set(getAvailable(SKILLS_DIR));
142
+ const availableAgents = new Set(getAvailable(AGENTS_DIR));
143
+ const out = [];
144
+ for (const name of getInstalled('skills')) {
145
+ if (availableSkills.has(name) && itemStatus('skill', name).outdated) {
146
+ out.push({ type: 'skill', name });
147
+ }
148
+ }
149
+ for (const name of getInstalled('agents')) {
150
+ if (availableAgents.has(name) && itemStatus('agent', name).outdated) {
151
+ out.push({ type: 'agent', name });
152
+ }
153
+ }
154
+ return out;
155
+ }
156
+
157
+ function printOutdatedNudge() {
158
+ const outdated = getOutdatedInstalled();
159
+ if (outdated.length === 0) return;
160
+ const n = outdated.length;
161
+ status('warn',
162
+ `You have ${n} item${n === 1 ? '' : 's'} out of date — run \`npx td-ai-tools update\` to update ${n === 1 ? 'it' : 'them'}.`);
163
+ }
164
+
165
+ // Sub-agent prompts bundled inside a skill: any `*.md` in <skill>/agents/ that
166
+ // declares a `name:` frontmatter field. These are registered to every supported
167
+ // target's agents directory so Claude Code and shared `.agents` runtimes can
168
+ // discover the same definition on install.
169
+ function getBundledAgents(skillSrc) {
170
+ const agentsDir = path.join(skillSrc, 'agents');
171
+ if (!fs.existsSync(agentsDir)) return [];
172
+ return fs.readdirSync(agentsDir)
173
+ .filter(f => f.endsWith('.md'))
174
+ .map(f => {
175
+ const srcPath = path.join(agentsDir, f);
176
+ return { file: f, srcPath, name: readFrontmatterField(srcPath, 'name') };
177
+ })
178
+ .filter(a => a.name);
179
+ }
180
+
66
181
  function installSkill(name, { replaceExisting = false } = {}) {
67
182
  const src = path.join(SKILLS_DIR, name);
68
183
  if (!fs.existsSync(src)) {
@@ -81,12 +196,22 @@ function installSkill(name, { replaceExisting = false } = {}) {
81
196
  copyDir(src, dest);
82
197
  const action = replaceExisting ? 'updated' : 'installed';
83
198
  status('success', `skill: ${name} ${action} → ${target.root}/skills/${name}/`);
199
+
200
+ for (const agent of getBundledAgents(src)) {
201
+ const agentDest = path.join(TARGET_ROOT, target.root, 'agents', agent.file);
202
+ fs.mkdirSync(path.dirname(agentDest), { recursive: true });
203
+ fs.copyFileSync(agent.srcPath, agentDest);
204
+ status('success', ` ↳ sub-agent: ${agent.name} registered → ${target.root}/agents/${agent.file}`);
205
+ }
84
206
  }
85
207
  return true;
86
208
  }
87
209
 
88
210
  function deleteSkill(name) {
89
211
  let deletedAny = false;
212
+ // Determine bundled sub-agents from the catalog source (best effort — only
213
+ // available if the skill still exists in the package).
214
+ const bundledAgents = getBundledAgents(path.join(SKILLS_DIR, name));
90
215
  for (const target of INSTALL_TARGETS) {
91
216
  const dest = path.join(TARGET_ROOT, target.root, 'skills', name);
92
217
  if (fs.existsSync(dest)) {
@@ -94,6 +219,14 @@ function deleteSkill(name) {
94
219
  status('success', `skill: ${name} deleted from ${target.root}/skills/${name}/`);
95
220
  deletedAny = true;
96
221
  }
222
+ for (const agent of bundledAgents) {
223
+ const agentDest = path.join(TARGET_ROOT, target.root, 'agents', agent.file);
224
+ if (fs.existsSync(agentDest)) {
225
+ fs.rmSync(agentDest, { force: true });
226
+ status('success', ` ↳ sub-agent: ${agent.name} unregistered from ${target.root}/agents/${agent.file}`);
227
+ deletedAny = true;
228
+ }
229
+ }
97
230
  }
98
231
  if (!deletedAny) {
99
232
  status('error', `Skill "${name}" is not installed.`);
@@ -172,17 +305,25 @@ function printListPlain() {
172
305
 
173
306
  console.log('\nSkills:');
174
307
  for (const name of skills) {
175
- const desc = readFrontmatterField(path.join(SKILLS_DIR, name, 'SKILL.md'), 'description');
176
- const truncated = desc.length > 72 ? desc.slice(0, 72) + '...' : desc;
177
308
  console.log(` ${name}`);
178
- if (truncated) console.log(` ${truncated}`);
309
+ const ann = versionAnnotation('skill', name);
310
+ if (ann) {
311
+ console.log(` ${ann.text}`);
312
+ } else {
313
+ const desc = readFrontmatterField(path.join(SKILLS_DIR, name, 'SKILL.md'), 'description');
314
+ const truncated = desc.length > 72 ? desc.slice(0, 72) + '...' : desc;
315
+ if (truncated) console.log(` ${truncated}`);
316
+ }
179
317
  }
180
318
 
181
319
  console.log('\nAgent Packs:');
182
320
  for (const name of agents) {
183
321
  console.log(` ${name}`);
322
+ const ann = versionAnnotation('agent', name);
323
+ if (ann) console.log(` ${ann.text}`);
184
324
  }
185
325
  console.log('');
326
+ printOutdatedNudge();
186
327
  }
187
328
 
188
329
  function printList() {
@@ -196,22 +337,30 @@ function printList() {
196
337
 
197
338
  p.intro(pc.bgCyan(pc.black(' AgentToolkit ')));
198
339
 
340
+ const lineFor = (type, name, fallbackHint) => {
341
+ const ann = versionAnnotation(type, name);
342
+ if (ann) {
343
+ const colored = ann.outdated ? pc.yellow(ann.text) : pc.dim(ann.text);
344
+ return `${pc.bold(name)}\n ${colored}`;
345
+ }
346
+ return fallbackHint ? `${pc.bold(name)}\n ${pc.dim(fallbackHint)}` : pc.bold(name);
347
+ };
348
+
199
349
  const skillsBody = skills.length
200
- ? skills.map(name => {
201
- const hint = getSkillHint(name);
202
- return hint ? `${pc.bold(name)}\n ${pc.dim(hint)}` : pc.bold(name);
203
- }).join('\n')
350
+ ? skills.map(name => lineFor('skill', name, getSkillHint(name))).join('\n')
204
351
  : pc.dim('(none)');
205
352
  p.note(skillsBody, `Skills (${skills.length})`);
206
353
 
207
354
  const agentsBody = agents.length
208
- ? agents.map(name => {
209
- const hint = getAgentHint(name);
210
- return hint ? `${pc.bold(name)}\n ${pc.dim(hint)}` : pc.bold(name);
211
- }).join('\n')
355
+ ? agents.map(name => lineFor('agent', name, getAgentHint(name))).join('\n')
212
356
  : pc.dim('(none)');
213
357
  p.note(agentsBody, `Agent Packs (${agents.length})`);
214
358
 
359
+ const outdated = getOutdatedInstalled();
360
+ if (outdated.length) {
361
+ p.log.warn(pc.yellow(
362
+ `${outdated.length} installed item${outdated.length === 1 ? '' : 's'} out of date — run \`npx td-ai-tools update\`.`));
363
+ }
215
364
  p.outro(pc.dim(`${skills.length} skills, ${agents.length} agent packs available.`));
216
365
  }
217
366
 
@@ -250,13 +399,23 @@ function buildUpdateMenu() {
250
399
  return [...skills, ...agents];
251
400
  }
252
401
 
253
- function toGroupOptions(menu, { hintForInstalled = false } = {}) {
402
+ function menuHint(type, name, { hintForInstalled, showVersions }) {
403
+ if (showVersions) {
404
+ const ann = versionAnnotation(type, name);
405
+ return ann ? ann.text : '';
406
+ }
407
+ if (hintForInstalled) return '';
408
+ return type === 'skill' ? getSkillHint(name) : getAgentHint(name);
409
+ }
410
+
411
+ function toGroupOptions(menu, { hintForInstalled = false, showVersions = false } = {}) {
412
+ const opts = { hintForInstalled, showVersions };
254
413
  const skillItems = menu.filter(i => i.type === 'skill').map(i => {
255
- const hint = hintForInstalled ? '' : getSkillHint(i.name);
414
+ const hint = menuHint('skill', i.name, opts);
256
415
  return { value: `skill:${i.name}`, label: i.name, hint: hint || undefined };
257
416
  });
258
417
  const agentItems = menu.filter(i => i.type === 'agent').map(i => {
259
- const hint = hintForInstalled ? '' : getAgentHint(i.name);
418
+ const hint = menuHint('agent', i.name, opts);
260
419
  return { value: `agent:${i.name}`, label: i.name, hint: hint || undefined };
261
420
  });
262
421
  const out = {};
@@ -301,10 +460,11 @@ async function interactiveInstall(mode = 'install') {
301
460
 
302
461
  p.intro(pc.bgCyan(pc.black(' AgentToolkit ')));
303
462
  printSelectionControls(mode);
463
+ printOutdatedNudge();
304
464
 
305
465
  const selection = await p.groupMultiselect({
306
466
  message: `Select items to ${mode}`,
307
- options: toGroupOptions(menu),
467
+ options: toGroupOptions(menu, { showVersions: mode === 'update' }),
308
468
  required: false,
309
469
  });
310
470
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "td-ai-tools",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "Install agent skills and packs into your project",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -21,12 +21,16 @@ popd >/dev/null
21
21
  tarball_path="$tmpdir/$tarball"
22
22
 
23
23
  pushd "$project_dir" >/dev/null
24
- npm exec --yes --cache "$cache_dir" --package "$tarball_path" td-ai-tools install pr-solver horizon-component-library
24
+ npm exec --yes --cache "$cache_dir" --package "$tarball_path" td-ai-tools install pr-solver playwright-cli horizon-component-library
25
25
  popd >/dev/null
26
26
 
27
27
  required_paths=(
28
28
  "$project_dir/.claude/skills/pr-solver/SKILL.md"
29
29
  "$project_dir/.agents/skills/pr-solver/SKILL.md"
30
+ "$project_dir/.claude/skills/playwright-cli/SKILL.md"
31
+ "$project_dir/.agents/skills/playwright-cli/SKILL.md"
32
+ "$project_dir/.claude/agents/playwright-investigator.md"
33
+ "$project_dir/.agents/agents/playwright-investigator.md"
30
34
  "$project_dir/.claude/agents/horizon-component-library/AGENTS.md"
31
35
  "$project_dir/.agents/agents/horizon-component-library/AGENTS.md"
32
36
  )
package/skills/README.md CHANGED
@@ -3,6 +3,7 @@
3
3
  ## Available Skills
4
4
  - `cache-reset`: Clear and warm Laravel and Statamic caches after content/template changes.
5
5
  - `horizon-component-migration`: Bundle Horizon components and recursive dependencies into a migration package for another theme.
6
+ - `playwright-cli`: Drive a real browser for web testing, screenshots, and data extraction. Ships the `playwright-investigator` sub-agent for orchestrated URL investigations.
6
7
  - `pr-solver`: Resolve unresolved GitHub PR review threads with GraphQL-driven workflow.
7
8
  - `pull-request`: Generate structured pull request descriptions for Shopify theme work.
8
9
  - `td-js-vanilla-rules`: Theory Digital vanilla JavaScript standards for Shopify themes.
@@ -11,5 +12,11 @@
11
12
  ## Skill Structure Convention
12
13
  - `<skill-name>/SKILL.md`
13
14
  - `<skill-name>/agents/openai.yaml`
15
+ - `<skill-name>/agents/<sub-agent>.md` (optional) — a reusable sub-agent prompt (frontmatter `name`/`description` + system prompt). Any `.md` here with a `name:` field is auto-registered to both `.claude/agents/` and `.agents/agents/` on install, so Claude Code and `.agents`-aware runtimes such as Codex can discover the same definition.
14
16
  - `<skill-name>/scripts/*` (optional)
15
17
  - `<skill-name>/references/*` (optional)
18
+
19
+ ## Versioning
20
+ - Each `SKILL.md` frontmatter must include a `version:` field using semantic versioning (`MAJOR.MINOR.PATCH`).
21
+ - Bump the version whenever the skill's contents change — `PATCH` for fixes/tweaks, `MINOR` for new behavior, `MAJOR` for breaking changes.
22
+ - The CLI compares the installed version against this catalog version to flag outdated installs in `list` and `update`. If you do not bump the version, users will not be told to update.
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: basecamp
3
+ version: 1.0.0
3
4
  description: |
4
5
  Interact with Basecamp via the Basecamp CLI. Full API coverage: projects, todos, cards,
5
6
  messages, files, schedule, check-ins, timeline, recordings, templates, webhooks,
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: cache-reset
3
+ version: 1.0.0
3
4
  description: Clear and warm Laravel and Statamic caches after content or template changes.
4
5
  alwaysApply: false
5
6
  ---
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: car-ticket-generator
3
+ version: 1.0.0
3
4
  description: Generate a ticket for the codex-auto-runner queue
4
5
  alwaysApply: false
5
6
  ---
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: everhour-basecamp-estimates
3
+ version: 1.0.0
3
4
  description: Bulk update Everhour task estimates from a Basecamp todo or todolist URL, then append bracketed hours to the Basecamp todo titles.
4
5
  alwaysApply: false
5
6
  ---
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: forge-cli
3
+ version: 1.0.0
3
4
  description: Manage Laravel Forge servers, sites, and provisioned resources from the terminal with the Laravel Forge CLI, falling back to the Forge HTTP API (Statamic-relevant endpoints documented here) for anything the CLI does not cover. Use when the user wants to inspect Forge state, switch active servers, deploy sites, update environment variables, view logs, run remote commands, use Tinker, manage services like PHP, Nginx, daemons, and databases, or perform Statamic site setup tasks the CLI lacks a verb for (site/SSL/git/worker/scheduler/backup/recipe/composer-auth management).
4
5
  ---
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: horizon-component-migration
3
+ version: 1.0.0
3
4
  description: Bundle Shopify Horizon components into a migration package for a different theme, including recursive dependencies across sections, snippets, blocks, assets, templates, and locale strings. Use when asked to port or extract a Horizon component with all required files and setup notes.
4
5
  ---
5
6
 
@@ -0,0 +1,122 @@
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`. |
@@ -1,11 +1,19 @@
1
1
  ---
2
2
  name: playwright-cli
3
+ version: 1.1.0
3
4
  description: Automates browser interactions for web testing, screenshots, and data extraction. Use when the user needs to navigate websites, interact with web pages, take screenshots, test web applications, or extract information from web pages.
4
5
  allowed-tools: Bash(playwright-cli:*)
5
6
  ---
6
7
 
7
8
  # Browser Automation with playwright-cli
8
9
 
10
+ > **First run in a new clone/machine?** Read [PRIMER.md](PRIMER.md) and run
11
+ > `.agents/skills/playwright-cli/setup.sh` once (or the `.claude/...` copy if
12
+ > you are using Claude Code only). For Codex, run it with `--codex` or `--all`.
13
+ > Key gotcha: `playwright-cli` defaults to branded Chrome (usually absent) —
14
+ > use `--browser=chromium` or set `PLAYWRIGHT_MCP_BROWSER=chromium`. Delete this
15
+ > comment and the setup and primer files after a successful setup.
16
+
9
17
  ## Quick start
10
18
 
11
19
  ```bash
@@ -268,6 +276,17 @@ playwright-cli tracing-stop
268
276
  playwright-cli close
269
277
  ```
270
278
 
279
+ ## Sub-agent
280
+
281
+ This skill ships a provider-agnostic sub-agent prompt, **`playwright-investigator`** ([agents/playwright-investigator.md](agents/playwright-investigator.md)), for orchestrated browser work. Dispatch it with a URL and an objective — diagnose the source of a problem, confirm a change applied correctly, or compare visual output against a target (screenshot or reference URL). It drives the browser through these commands, then returns a concise findings report, keeping all playwright-cli commands and raw output out of the orchestrator's context.
282
+
283
+ Installing this skill auto-registers the sub-agent in both supported layouts:
284
+
285
+ - `.claude/agents/playwright-investigator.md` for Claude Code custom agents.
286
+ - `.agents/agents/playwright-investigator.md` for Codex and other runtimes that read the shared `.agents` convention.
287
+
288
+ Claude Code can dispatch it through the Agent tool with no manual setup. In Codex, use the available multi-agent/sub-agent tool when the runtime exposes one, pointing it at the `.agents/agents/playwright-investigator.md` definition. If a runtime reads `.agents` assets but does not expose native sub-agent dispatch, use the same file as the investigator's system prompt and keep the final report contract unchanged.
289
+
271
290
  ## Specific tasks
272
291
 
273
292
  * **Request mocking** [references/request-mocking.md](references/request-mocking.md)
@@ -276,4 +295,4 @@ playwright-cli close
276
295
  * **Storage state (cookies, localStorage)** [references/storage-state.md](references/storage-state.md)
277
296
  * **Test generation** [references/test-generation.md](references/test-generation.md)
278
297
  * **Tracing** [references/tracing.md](references/tracing.md)
279
- * **Video recording** [references/video-recording.md](references/video-recording.md)
298
+ * **Video recording** [references/video-recording.md](references/video-recording.md)
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: playwright-investigator
3
+ description: Drives a browser via the playwright-cli skill to investigate a website on an orchestrator's behalf. Give it a URL and an objective — diagnose the source of a problem, confirm a change applied correctly, or compare visual output against a target (a screenshot file or a reference URL). It performs the browser work, gathers evidence, and returns a concise findings report. Use it to keep all playwright-cli commands and raw browser/snapshot output out of the orchestrator's context.
4
+ tools: Bash, Read, Glob, Grep, Skill
5
+ ---
6
+
7
+ You are a focused web investigation agent. An orchestrator hands you a URL and an objective; you drive a browser with the `playwright-cli` skill, gather the evidence needed to satisfy your objective, and hand back a clean, self-contained report.
8
+
9
+ Your defining responsibility is **context isolation**. The orchestrator must never see playwright-cli commands, snapshot YAML, console dumps, or network logs. All of that lives and dies inside your context. The orchestrator receives only your final report.
10
+
11
+ ## Runtime Contract
12
+
13
+ This definition is intentionally usable in both Claude Code's `.claude/agents/`
14
+ layout and the shared `.agents/agents/` layout used by Codex-compatible
15
+ runtimes. You need these capabilities, regardless of provider-specific tool
16
+ names:
17
+
18
+ - Run shell commands.
19
+ - Read local files and generated artifacts.
20
+ - Load or follow the `playwright-cli` skill documentation.
21
+
22
+ The `tools:` frontmatter is Claude Code's native declaration; other runtimes
23
+ should map it to the equivalent capabilities above.
24
+
25
+ If your runtime does not expose a native skill-loading tool, read the installed
26
+ `playwright-cli/SKILL.md` file directly and use it as your command reference.
27
+
28
+ ## Operating Principles
29
+
30
+ - **Use the playwright-cli skill.** At the start of a task, invoke the `playwright-cli` skill to load its command reference, then drive the browser through `playwright-cli` commands run via Bash. If the global binary is missing, fall back to `npx playwright-cli`.
31
+ - **Gather evidence** Run whatever sequence of commands the objective requires. You do not report on individual commands; you report on conclusions backed by concrete evidence.
32
+ - **Always clean up.** End every task with `playwright-cli close` (or `close-all` if you opened named sessions), even if the task failed. Leave no orphaned browser.
33
+ - **Report uncertainty honestly.** If the page wouldn't load, an element couldn't be found, auth blocked you, or the evidence is inconclusive, say so plainly. A wrong confident answer is worse than a flagged unknown.
34
+
35
+ ## Inputs You Expect
36
+
37
+ The orchestrator should give you a **URL** and an **objective**. If a critical input is missing (e.g. a comparison target, or credentials for a gated page), state the blocker in your report rather than guessing.
38
+
39
+ ## Report Format
40
+
41
+ Return **only** the report below as your final message. No preamble, no command logs.
42
+
43
+ ```markdown
44
+ ## Web Investigation Report
45
+
46
+ **Objective:** [restate the objective in one line]
47
+ **URL:** [url investigated]
48
+ **Mode:** [Diagnosis | Change confirmation | Visual comparison]
49
+ **Verdict:** [one-line bottom line — e.g. root cause identified / PASS / FAIL / 3 visual differences found / INCONCLUSIVE]
50
+
51
+ ### Findings
52
+ - [Concrete finding backed by evidence. Cite the specific signal: console error text, failing request URL+status, DOM/CSS value, element ref, or screenshot region.]
53
+ - [Additional findings as needed.]
54
+
55
+ ### Evidence
56
+ - [Key evidence items: exact console errors, request statuses, eval results, screenshot file paths captured, computed values. Keep it tight — only what supports the findings.]
57
+
58
+ ### Recommendation / Next Steps
59
+ [For diagnosis: the likely fix or where to look. For confirmation: confirmed live or what's still wrong. For comparison: what to change to match the target. Omit if not applicable.]
60
+
61
+ ### Caveats
62
+ [Anything you couldn't verify, blockers hit, assumptions made, or scope you didn't cover. Write "None." if fully conclusive.]
63
+ ```
64
+
65
+ Keep the report decision-ready: tight, specific, and grounded in evidence the orchestrator can act on without ever needing to see the browser session.
@@ -1,13 +1,14 @@
1
+ # Codex sandbox config for playwright-cli. Generated by setup.sh --codex.
2
+ # setup.sh substitutes the repo root and the Playwright browser-cache path
3
+ # (e.g. ~/.cache/ms-playwright) into the absolute paths below.
4
+
1
5
  sandbox_mode = "workspace-write"
2
6
 
3
7
  [sandbox_workspace_write]
4
8
  network_access = true
5
- exclude_tmpdir_env_var = true
6
- exclude_slash_tmp = true
7
9
  writable_roots = [
8
- "__PROJECT_ROOT__/.cache/codex-playwright",
9
- "__PROJECT_ROOT__/.artifacts/playwright/ms-playwright",
10
- "__PROJECT_ROOT__/.tmp/codex-playwright",
10
+ # The Playwright-managed browsers live here; the sandbox must read/exec them.
11
+ "__BROWSERS_PATH__",
11
12
  "__PROJECT_ROOT__/.playwright-cli",
12
13
  "__PROJECT_ROOT__/test-results",
13
14
  "__PROJECT_ROOT__/playwright-report",
@@ -18,15 +19,8 @@ writable_roots = [
18
19
  inherit = "all"
19
20
 
20
21
  [shell_environment_policy.set]
21
- PLAYWRIGHT_BROWSERS_PATH = "__PROJECT_ROOT__/.artifacts/playwright/ms-playwright"
22
+ # Use the already-installed Playwright chromium, NOT branded chrome
23
+ # (/opt/google/chrome/chrome), which is usually absent.
22
24
  PLAYWRIGHT_MCP_BROWSER = "chromium"
23
- PLAYWRIGHT_MCP_EXECUTABLE_PATH = "__BROWSER_PATH__"
24
- PLAYWRIGHT_DAEMON_SOCKETS_DIR = "__PROJECT_ROOT__/.tmp/codex-playwright/sockets"
25
- BROWSER = "chromium"
26
- HOME = "__PROJECT_ROOT__/.tmp/codex-playwright/home"
27
- XDG_CONFIG_HOME = "__PROJECT_ROOT__/.tmp/codex-playwright/config"
28
- XDG_STATE_HOME = "__PROJECT_ROOT__/.tmp/codex-playwright/state"
29
- TMPDIR = "__PROJECT_ROOT__/.tmp/codex-playwright"
30
- TEMP = "__PROJECT_ROOT__/.tmp/codex-playwright"
31
- TMP = "__PROJECT_ROOT__/.tmp/codex-playwright"
32
- XDG_CACHE_HOME = "__PROJECT_ROOT__/.cache/codex-playwright"
25
+ # Pin the browser cache so the sandbox and the CLI agree on its location.
26
+ PLAYWRIGHT_BROWSERS_PATH = "__BROWSERS_PATH__"
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Bootstraps playwright-cli configuration for one or more agent runtimes.
5
+ #
6
+ # ./setup.sh # default: --claude --agents
7
+ # ./setup.sh --claude # only .claude/settings.json
8
+ # ./setup.sh --agents # only .agents/playwright-cli.env
9
+ # ./setup.sh --codex # only .codex/config.toml (sandbox)
10
+ # ./setup.sh --all # .claude + .agents + .codex
11
+ #
12
+ # What it pins everywhere: PLAYWRIGHT_MCP_BROWSER=chromium.
13
+ # playwright-cli defaults to *branded* Chrome (/opt/google/chrome/chrome),
14
+ # which is usually NOT installed. The Playwright-managed chromium is, so we
15
+ # point the default browser at it. We do NOT override HOME / browsers-path /
16
+ # executable-path: the managed browsers already live in the shared cache and
17
+ # overriding those was the bug in the old Codex-only script.
18
+
19
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20
+
21
+ # --- resolve project root ---------------------------------------------------
22
+ if PROJECT_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null)" && [ -n "$PROJECT_ROOT" ]; then
23
+ :
24
+ else
25
+ # .agents/skills/playwright-cli or .claude/skills/playwright-cli
26
+ # -> repo root (three levels up)
27
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
28
+ fi
29
+
30
+ # --- parse targets ----------------------------------------------------------
31
+ DO_CLAUDE=0; DO_AGENTS=0; DO_CODEX=0
32
+ if [ "$#" -eq 0 ]; then
33
+ DO_CLAUDE=1; DO_AGENTS=1
34
+ else
35
+ for arg in "$@"; do
36
+ case "$arg" in
37
+ --claude) DO_CLAUDE=1 ;;
38
+ --agents) DO_AGENTS=1 ;;
39
+ --codex) DO_CODEX=1 ;;
40
+ --all) DO_CLAUDE=1; DO_AGENTS=1; DO_CODEX=1 ;;
41
+ -h|--help)
42
+ sed -n '3,17p' "$0" | sed 's/^# \{0,1\}//'
43
+ exit 0 ;;
44
+ *) echo "unknown arg: $arg (try --help)" >&2; exit 2 ;;
45
+ esac
46
+ done
47
+ fi
48
+
49
+ # --- browser detection (Playwright-managed, not a system binary) ------------
50
+ BROWSERS_PATH="${PLAYWRIGHT_BROWSERS_PATH:-$HOME/.cache/ms-playwright}"
51
+ DEFAULT_BROWSER="chromium"
52
+ if compgen -G "$BROWSERS_PATH/chromium-*" >/dev/null 2>&1; then
53
+ echo "✓ Playwright chromium found in $BROWSERS_PATH"
54
+ else
55
+ echo "⚠ No Playwright chromium in $BROWSERS_PATH — run: npx playwright install chromium" >&2
56
+ fi
57
+
58
+ echo " PROJECT_ROOT=$PROJECT_ROOT"
59
+ echo " DEFAULT_BROWSER=$DEFAULT_BROWSER"
60
+
61
+ # --- Claude Code: .claude/settings.json (merge, don't clobber) --------------
62
+ if [ "$DO_CLAUDE" -eq 1 ]; then
63
+ OUT="$PROJECT_ROOT/.claude/settings.json"
64
+ mkdir -p "$(dirname "$OUT")"
65
+ node - "$OUT" "$DEFAULT_BROWSER" <<'NODE'
66
+ const fs = require('fs');
67
+ const [, , out, browser] = process.argv;
68
+ let cfg = {};
69
+ if (fs.existsSync(out)) {
70
+ try { cfg = JSON.parse(fs.readFileSync(out, 'utf8')); }
71
+ catch { fs.copyFileSync(out, out + '.bak'); console.error(`! ${out} was invalid JSON; backed up to .bak`); }
72
+ }
73
+ cfg.env = cfg.env || {};
74
+ cfg.env.PLAYWRIGHT_MCP_BROWSER = browser;
75
+ cfg.permissions = cfg.permissions || {};
76
+ const allow = new Set(cfg.permissions.allow || []);
77
+ allow.add('Bash(playwright-cli:*)');
78
+ cfg.permissions.allow = [...allow];
79
+ fs.writeFileSync(out, JSON.stringify(cfg, null, 2) + '\n');
80
+ NODE
81
+ echo "wrote $OUT (env.PLAYWRIGHT_MCP_BROWSER + permissions.allow)"
82
+ fi
83
+
84
+ # --- Shared .agents convention: .agents/playwright-cli.env ------------------
85
+ if [ "$DO_AGENTS" -eq 1 ]; then
86
+ OUT="$PROJECT_ROOT/.agents/playwright-cli.env"
87
+ mkdir -p "$(dirname "$OUT")"
88
+ cat > "$OUT" <<EOF
89
+ # Shared playwright-cli environment (agent-agnostic).
90
+ # Load it before running playwright-cli, e.g.:
91
+ # set -a; . .agents/playwright-cli.env; set +a
92
+ #
93
+ # Forces the Playwright-managed chromium instead of branded Chrome, which is
94
+ # usually not installed.
95
+ PLAYWRIGHT_MCP_BROWSER=$DEFAULT_BROWSER
96
+ EOF
97
+ echo "wrote $OUT"
98
+ fi
99
+
100
+ # --- Codex: .codex/config.toml (sandbox) — opt-in ---------------------------
101
+ if [ "$DO_CODEX" -eq 1 ]; then
102
+ OUT="$PROJECT_ROOT/.codex/config.toml"
103
+ mkdir -p "$(dirname "$OUT")"
104
+ sed \
105
+ -e "s|__PROJECT_ROOT__|$PROJECT_ROOT|g" \
106
+ -e "s|__BROWSERS_PATH__|$BROWSERS_PATH|g" \
107
+ "$SCRIPT_DIR/config.toml.template" > "$OUT"
108
+ echo "wrote $OUT"
109
+ fi
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: pr-solver
3
+ version: 1.0.0
3
4
  description: Resolve GitHub pull request feedback by querying unresolved review conversations with the GitHub GraphQL API and implementing code changes for each unresolved thread. Use when a task includes a GitHub PR URL, asks to address open review comments, or asks to clear unresolved conversations; request a PR link if it is missing.
4
5
  ---
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: pull-request
3
+ version: 1.0.0
3
4
  description: Generates well-structured GitHub pull request descriptions for Shopify theme development teams by analyzing git diffs and gathering context. Use this skill whenever a developer asks to create a PR, open a pull request, write a PR description, or submit code for review — even if they just say "make a PR" or "create a pull request". Also trigger when someone mentions needing a PR title, PR summary, or PR testing steps.
4
5
  ---
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: pull-request-statamic
3
+ version: 1.0.0
3
4
  description: Generates GitHub pull request descriptions for Statamic and Laravel development by analyzing git diffs and gathering context.
4
5
  ---
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: record-changes
3
+ version: 1.0.0
3
4
  description: Update `docs/changes.md` by summarizing the current branch against the primary development branch. Use when a developer asks to record branch changes, document theme customizations, refresh the project change log, or write a branch summary into `docs/changes.md`. Prefer comparing against `main`, but fall back to `origin/main`, `master`, or `origin/master` when the repository does not have a local `main` branch.
4
5
  ---
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: stylesheet-migration
3
+ version: 1.0.0
3
4
  description: Migrate Shopify Liquid `{% stylesheet %}` blocks into theme CSS assets using bundled Python scripts. Use when moving inline section or snippet stylesheet tags into `assets/*.css`, especially in `sections/` and `snippets/`. By default the scripts only process filenames that start with `td-`, creating `section-<source>.css` for files in `sections/` and `component-<source>.css` for files in `snippets/`, unless the caller specifies a different prefix or disables the prefix filter.
4
5
  ---
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: td-js-vanilla-rules
3
+ version: 1.0.0
3
4
  description: Theory Digital vanilla JavaScript standards for Shopify theme work. Use when creating, modifying, refactoring, or debugging JavaScript in Shopify themes, including Web Component lifecycle, selector strategy, Swiper usage, accessibility state sync, and Shopify editor interaction hooks.
4
5
  ---
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: td-review
3
+ version: 1.0.0
3
4
  description: Run parallel code review agents on a PR (including TD theme compliance) and produce a synthesized findings report. Does not post comments or take action — output is for human decision-making.
4
5
  user_invocable: true
5
6
  arguments:
@@ -29,7 +30,7 @@ This skill depends on external agent definitions that must be available at runti
29
30
  - `compound-engineering:research:git-history-analyzer`
30
31
 
31
32
  **Custom agent (this repository):**
32
- - `td-theme-reviewer` — Theory Digital's theme compliance reviewer. Must be registered as a custom agent type in Claude Code settings.
33
+ - `td-theme-reviewer` — Theory Digital's theme compliance reviewer. Bundled with this skill and auto-registered to `.claude/agents/td-theme-reviewer.md` on install, so it is available as a custom agent type with no manual setup.
33
34
 
34
35
  **CLI tools:**
35
36
  - `gh` — GitHub CLI, authenticated with access to the target repository.
@@ -1,81 +0,0 @@
1
- # Codex sandbox config bootstrap
2
-
3
- This directory contains a template + generator that produces `<repo>/.codex/config.toml`
4
- on demand, so the file does not need to be committed with hardcoded paths tied to one
5
- machine or one project clone location.
6
-
7
- ## Files
8
-
9
- - `config.toml.template` — the source of truth. Uses two placeholders:
10
- - `__PROJECT_ROOT__` — replaced with the absolute path to the repo root
11
- - `__BROWSER_PATH__` — replaced with the absolute path to the Chromium/Chrome binary
12
- - `setup-codex.sh` — reads the template, substitutes the placeholders, writes
13
- `<repo>/.codex/config.toml`.
14
-
15
- The generated `.codex/` directory is gitignored, so each clone gets its own copy.
16
-
17
- ## Usage
18
-
19
- Run once after cloning the repo (or after moving the clone to a new path):
20
-
21
- ```bash
22
- .agents/skills/playwright-cli/setup-codex.sh
23
- ```
24
-
25
- The script prints the values it resolved, e.g.:
26
-
27
- ```
28
- wrote /home/you/code/lio/.codex/config.toml
29
- PROJECT_ROOT=/home/you/code/lio
30
- BROWSER_PATH=/usr/bin/chromium
31
- ```
32
-
33
- Re-run it any time the template changes or you move the repo.
34
-
35
- ## How paths are resolved
36
-
37
- **`PROJECT_ROOT`** — derived in this order:
38
-
39
- 1. `git rev-parse --show-toplevel` from the script's directory (works for any clone
40
- location, on any machine, regardless of username)
41
- 2. Falls back to walking three directories up from the script
42
- (`.agents/skills/playwright-cli` → repo root) if the repo isn't a git checkout
43
-
44
- **`BROWSER_PATH`** — derived in this order:
45
-
46
- 1. The `BROWSER_PATH` environment variable, if set
47
- 2. The legacy `CHROME_PATH` environment variable, if set (kept for backwards compat)
48
- 3. First match from `PATH` of: `chromium`, `chromium-browser`, `google-chrome`,
49
- `google-chrome-stable` — Chromium is preferred because Chrome is often missing
50
- on WSL shells
51
- 4. Falls back to `/usr/bin/chromium` with a warning if nothing is found
52
-
53
- Override the browser binary explicitly when needed:
54
-
55
- ```bash
56
- BROWSER_PATH=/usr/bin/chromium .agents/skills/playwright-cli/setup-codex.sh
57
- ```
58
-
59
- ## Why a generator instead of variable expansion
60
-
61
- Codex CLI parses `config.toml` as static TOML and does not expand `~`, `$HOME`, or
62
- `${VAR}` in path values — they would be treated as literal strings. Generating the
63
- file at setup time is the simplest way to keep the template portable while still
64
- producing the absolute paths Codex expects at runtime.
65
-
66
- ## What the generated config does
67
-
68
- The output `.codex/config.toml` configures Codex's sandbox so Playwright can run
69
- inside it:
70
-
71
- - `sandbox_workspace_write.writable_roots` — directories Playwright is allowed to
72
- write to (browser cache, artifacts, test reports, daemon sockets)
73
- - `shell_environment_policy.set` — pins `PLAYWRIGHT_*`, `XDG_*`, and `TMPDIR`-family
74
- env vars to repo-local paths so Playwright stores its browser, cache, and runtime
75
- state under the project rather than the user's home directory
76
-
77
- ## Editing
78
-
79
- Edit `config.toml.template` (not the generated file) and re-run `setup-codex.sh`.
80
- Changes to `.codex/config.toml` directly will be overwritten the next time the
81
- generator runs.
@@ -1,29 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
- TEMPLATE="$SCRIPT_DIR/config.toml.template"
6
-
7
- if PROJECT_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null)" && [ -n "$PROJECT_ROOT" ]; then
8
- :
9
- else
10
- PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
11
- fi
12
- OUTPUT="$PROJECT_ROOT/.codex/config.toml"
13
-
14
- BROWSER_PATH="${BROWSER_PATH:-${CHROME_PATH:-$(command -v chromium || command -v chromium-browser || command -v google-chrome || command -v google-chrome-stable || true)}}"
15
- if [ -z "$BROWSER_PATH" ]; then
16
- echo "warning: no Chromium/Chrome binary found on PATH; set BROWSER_PATH=/path/to/browser and rerun" >&2
17
- BROWSER_PATH="/usr/bin/chromium"
18
- fi
19
-
20
- mkdir -p "$(dirname "$OUTPUT")"
21
-
22
- sed \
23
- -e "s|__PROJECT_ROOT__|$PROJECT_ROOT|g" \
24
- -e "s|__BROWSER_PATH__|$BROWSER_PATH|g" \
25
- "$TEMPLATE" > "$OUTPUT"
26
-
27
- echo "wrote $OUTPUT"
28
- echo " PROJECT_ROOT=$PROJECT_ROOT"
29
- echo " BROWSER_PATH=$BROWSER_PATH"