@abarcenas/ai-workflow-template 1.45.0 → 1.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/.agents/config/README.md +43 -0
  2. package/.agents/config/domain.md +37 -0
  3. package/.agents/instructions/learned-knowledge.instructions.md +110 -0
  4. package/.agents/instructions/memory-bank.instructions.md +11 -0
  5. package/.agents/skills/domain-modeling/ADR-FORMAT.md +47 -0
  6. package/.agents/skills/domain-modeling/GLOSSARY-FORMAT.md +60 -0
  7. package/.agents/skills/domain-modeling/SKILL.md +74 -0
  8. package/.agents/skills/grill-me/SKILL.md +7 -0
  9. package/.agents/skills/grill-with-docs/SKILL.md +7 -0
  10. package/.agents/skills/grilling/SKILL.md +28 -0
  11. package/.agents/skills/prototype/LOGIC.md +67 -0
  12. package/.agents/skills/prototype/SKILL.md +26 -0
  13. package/.agents/skills/prototype/UI.md +112 -0
  14. package/.github/workflows/npm-publish.yml +10 -0
  15. package/.github/workflows/pr-test.yml +31 -0
  16. package/.opencode/agents/orchestrator/orchestrator.agent.md +68 -3
  17. package/.opencode/agents/scope.agent.md +299 -0
  18. package/AGENTS.md +42 -3
  19. package/README.md +5 -2
  20. package/package.json +1 -1
  21. package/scripts/bump-version.test.js +238 -0
  22. package/scripts/mcp/playwright-mcp-launcher.js +55 -29
  23. package/scripts/mcp/playwright-mcp-launcher.test.js +217 -0
  24. package/scripts/mcp-memory-server.js +93 -76
  25. package/scripts/mcp-memory-server.test.js +398 -0
  26. package/scripts/memory-cli.test.js +297 -0
  27. package/scripts/memory-index.js +119 -31
  28. package/scripts/memory-index.test.js +1173 -0
  29. package/scripts/normalize-memory.js +26 -1
  30. package/scripts/normalize-memory.test.js +753 -0
  31. package/scripts/setup/discover.test.js +124 -2
  32. package/scripts/setup/hooks.test.js +188 -1
  33. package/scripts/setup/husky-init.test.js +311 -0
  34. package/scripts/setup/index.js +35 -5
  35. package/scripts/setup/index.test.js +279 -0
  36. package/scripts/setup/knowledgebase.test.js +100 -0
  37. package/scripts/setup/skills-phase.js +568 -0
  38. package/scripts/setup/skills-phase.test.js +1274 -0
  39. package/scripts/setup/sync-phase.js +6 -1
  40. package/scripts/setup/sync-phase.test.js +180 -2
  41. package/scripts/setup/ui.test.js +579 -0
  42. package/scripts/setup/utils.test.js +586 -0
  43. package/scripts/sync-unit.test.js +1342 -0
  44. package/scripts/sync.js +540 -246
  45. package/scripts/sync.test.js +283 -0
  46. package/scripts/vocab-sync.test.js +88 -0
  47. package/skills-lock.json +90 -0
package/AGENTS.md CHANGED
@@ -12,7 +12,7 @@ This project includes AI-assisted development configuration distributed through
12
12
  ## Core Workflow
13
13
 
14
14
  1. **Understand before acting** — Read relevant instructions from `.agents/instructions/` before working with specific technologies
15
- 2. **Plan before implementing** — Use `.opencode/agents/plan.agent.md` or `.opencode/agents/implementer.agent.md` patterns for structured work
15
+ 2. **Plan before implementing** — Narrow underspecified work with the `scope` agent first, then use the `.opencode/agents/plan.agent.md` (optional deep planning) or `.opencode/agents/implementer.agent.md` patterns for structured work
16
16
  3. **Track progress** — Update `memory-bank/activeContext.md` and `memory-bank/progress.md` after each task (see `.agents/instructions/task-implementation.instructions.md`)
17
17
  4. **Maintain context** — Keep memory bank files updated (see `.agents/instructions/memory-bank.instructions.md`)
18
18
 
@@ -41,7 +41,7 @@ This project includes AI-assisted development configuration distributed through
41
41
 
42
42
  ## Available Skills
43
43
 
44
- Skills in `.agents/skills/` provide structured workflows for common tasks. Load them when the task matches the skill's purpose:
44
+ All 34 skills installed in `.agents/skills/` are listed below. Load one when the task matches its purpose. The five scoping skills (`domain-modeling`, `grill-me`, `grill-with-docs`, `grilling`, `prototype`) are described in detail in [`## Agent skills`](#agent-skills).
45
45
 
46
46
  - `architecture-blueprint-generator` — Generate project architecture blueprints
47
47
  - `conventional-commit` — Create conventional commit messages
@@ -69,6 +69,14 @@ Skills in `.agents/skills/` provide structured workflows for common tasks. Load
69
69
  - `caveman-stats` — Tracking and statistics for caveman usage
70
70
  - `cavecrew` — Multi-agent coordination for caveman skills
71
71
  - `compress` — General compression utilities
72
+ - `domain-modeling` — Build and sharpen the project's domain model (`GLOSSARY.md`, ADRs)
73
+ - `graphify-framework-aware` — Exclude framework boilerplate from graphify knowledge graphs
74
+ - `grill-me` — Human-invoked router for a relentless interview (calls `grilling`)
75
+ - `grill-with-docs` — Human-invoked router for the interview plus glossary/ADR writing
76
+ - `grilling` — Stress-test a plan or idea with rounds of numbered questions
77
+ - `lighthouse-cli` — Run Lighthouse performance, accessibility, and SEO audits
78
+ - `memory-search` — Semantic vector search over the memory bank
79
+ - `prototype` — Build a throwaway prototype to answer a design question
72
80
 
73
81
  ## Available Prompts
74
82
 
@@ -79,13 +87,44 @@ Prompts in `.agents/prompts/` can be invoked directly:
79
87
  - `playwright-generate-test` — Generate Playwright tests
80
88
  - `documentation-writer` — Write documentation (Diataxis framework)
81
89
 
90
+ ## Agent skills
91
+
92
+ Five skills installed from `mattpocock/skills` (pinned at `v1.3.1`) drive scoping and design work. They sit beside the other skills in `.agents/skills/`; this section explains when to reach for them and what they pair with.
93
+
94
+ ### Skills
95
+
96
+ | Skill | Path | Use it when |
97
+ |-------|------|-------------|
98
+ | `grilling` | `.agents/skills/grilling/SKILL.md` | A plan, decision, or idea needs stress-testing, or a request is too vague to plan. Works a design tree in rounds: the current frontier is asked as numbered questions, each with a recommended answer. |
99
+ | `grill-me` | `.agents/skills/grill-me/SKILL.md` | You want the same interview with no repository context and no document output — a stateless, one-shot entry point. |
100
+ | `grill-with-docs` | `.agents/skills/grill-with-docs/SKILL.md` | You want the interview plus the written record — the glossary and ADRs are created as decisions settle. |
101
+ | `domain-modeling` | `.agents/skills/domain-modeling/SKILL.md` | Project terminology or an architectural decision is being introduced or changed and `GLOSSARY.md` or an ADR needs to capture it. |
102
+ | `prototype` | `.agents/skills/prototype/SKILL.md` | A design question can only be settled by running throwaway code — the logic branch for state models and data flow, the UI branch for look and feel. |
103
+
104
+ `grill-me` and `grill-with-docs` are **human-invoked routers**: both set `disable-model-invocation: true`, so an agent cannot load them. Agents invoke `grilling` directly, plus `domain-modeling` when a term or decision actually settles.
105
+
106
+ ### Scope agent
107
+
108
+ `.opencode/agents/scope.agent.md` is the front door for underspecified work. It runs `grilling` in rounds against a single draft file, emits each round's frontier as numbered questions with a recommended answer per question, and stops when the frontier is empty or the remaining unknowns are explicitly deferred.
109
+
110
+ It terminates by writing `docs/spike-<feature>.md` — the spike document `.opencode/agents/researcher.agent.md` already expects as its input. From there the work continues to `researcher` (open questions need evidence), `implementer` (scope settled, planning remains), or `architect` (the shape of the system is the open problem).
111
+
112
+ The agent has no command-execution permission: it never builds, runs, or tests code. When a question can only be settled empirically it emits `prototype needed: <question>` with a branch, and that work is handed off to `coder` (logic) or `designer` (UI).
113
+
114
+ ### Domain docs
115
+
116
+ `.agents/config/domain.md` declares where domain documentation lives: `GLOSSARY.md` at the repository root and ADRs in `docs/adr/`, numbered `0001-slug.md` onwards — the single-context layout.
117
+ The `domain-modeling` skill reads it before exploring the codebase; when the file or the docs it names are absent, the skill proceeds silently and creates them lazily, only once a term or decision has actually been settled.
118
+
82
119
  ## Available Agent Workflows
83
120
 
84
121
  These agent workflows (from `.opencode/agents/`) define specialized modes of operation. Invoke the relevant workflow at the start of a task by stating which mode you want to activate.
85
122
 
86
123
  ### Plan Mode — Strategic Planning & Architecture
87
124
 
88
- Use this when starting a new feature, refactoring, or any non-trivial change. Follow this sequence:
125
+ Underspecified work does not start here. The default front door is the **`scope` agent** (see [`## Agent skills`](#agent-skills)): it narrows the request through grilling rounds and writes `docs/spike-<feature>.md`, which is handed to `researcher` when open questions need evidence, or to `implementer` when only planning remains.
126
+
127
+ `.opencode/agents/plan.agent.md` remains available as an **optional deep-planning mode** for work that is already scoped but needs a step-by-step plan. It is not deleted and not deprecated — it is demoted in the default flow only. When you use it, follow this sequence:
89
128
 
90
129
  1. **Understand the Goal** — Ask clarifying questions about requirements and goals
91
130
  2. **Explore Context** — Examine relevant files, components, and systems that will be affected
package/README.md CHANGED
@@ -52,7 +52,7 @@ npx ai-workflow-setup --help # Show all options
52
52
 
53
53
  ## What This Package Does
54
54
 
55
- - **29 specialized AI skills**, framework instructions, and prompt templates (`.agents/`)
55
+ - **34 specialized AI skills**, framework instructions, and prompt templates (`.agents/`)
56
56
  - **Smart agent frameworks** — Plan Mode, Implementation Plans, Research Spikes
57
57
  - **Multi-agent orchestrator** — dynamic pipelines with 10+ specialized sub-agents
58
58
  - **TDD orchestrator** — strict RED→GREEN→REFACTOR/VERIFY pipeline (90% coverage gate)
@@ -84,7 +84,7 @@ Post-install, your project receives this structure:
84
84
  ```text
85
85
  .agents/
86
86
  ├── instructions/ # Framework, workflow, and security guidance
87
- ├── skills/ # 29 specialized AI skills
87
+ ├── skills/ # 34 specialized AI skills
88
88
  └── prompts/ # Reusable prompt templates
89
89
  .opencode/agents/ # Orchestrator + specialized sub-agents
90
90
  .github/workflows/ # CI/CD automation (excluded from sync)
@@ -99,6 +99,7 @@ AGENTS.md # Master instruction file with agent guidance
99
99
  | Agent | File | Role |
100
100
  | --- | --- | --- |
101
101
  | Orchestrator | `.opencode/agents/orchestrator/orchestrator.agent.md` | Master — analyzes requests, builds pipelines, delegates |
102
+ | Scope | `.opencode/agents/scope.agent.md` | Conditional front door for underspecified requests — grilling rounds, then a spike doc |
102
103
  | Researcher | `.opencode/agents/researcher.agent.md` | Technical investigation, library evaluation |
103
104
  | Architect | `.opencode/agents/architect.agent.md` | System design, component architecture |
104
105
  | Implementer | `.opencode/agents/implementer.agent.md` | Translates designs into implementation plans |
@@ -121,6 +122,7 @@ Invoke the orchestrator with `@orchestrator` followed by your task:
121
122
  ```
122
123
 
123
124
  - **Run flow:** auto-bootstraps architecture on first run → builds a pipeline → executes agents sequentially (confirm/skip each step, or enable auto-confirm) → tracker documents to `docs/tracker-log.md` → persists new knowledge.
125
+ - **Scope step (conditional, runs first):** inserted at the front of the pipeline only when the request is underspecified — `scope` runs grilling rounds against a draft file, writes `docs/spike-<feature>.md`, then hands off to `researcher` or `implementer`. Always interactive (auto-confirm cannot skip it) and has **no command-execution permission** — prototypes are delegated to `coder` (logic) or `designer` (UI).
124
126
  - **First run only:** runs the architecture-blueprint-generator skill to detect tech stack/patterns, writing context to `docs/.architecture-context.md` and `.agents/instructions/learned-knowledge.instructions.md`; later runs skip this.
125
127
 
126
128
  ### MCP Tooling
@@ -222,6 +224,7 @@ All memory-bank files use YAML frontmatter (`id`, `title`, `updated`, `tags`, `e
222
224
 
223
225
  ## Featured Skills
224
226
 
227
+ - **Feature scoping:** `grilling`, `grill-me`, `grill-with-docs`, `domain-modeling`, `prototype` — narrow underspecified requests into a spike; `GLOSSARY.md` and `docs/adr/` locations come from `.agents/config/domain.md`
225
228
  - **Workflow automation:** `architecture-blueprint-generator`, `create-implementation-plan`, `create-readme`, `context-map`, `graphify` (global skill), `graphify-framework-aware`
226
229
  - **Development tools:** `playwright-explore-website`, `playwright-generate-test`, `github-issues`, `gh-cli`, `polygot-test-agent`, `microsoft-docs`
227
230
  - **Token & efficiency:** `caveman`, `caveman-compress`, `compress`, `caveman-commit`, `cavecrew`, `caveman-stats`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abarcenas/ai-workflow-template",
3
- "version": "1.45.0",
3
+ "version": "1.46.0",
4
4
  "description": "A comprehensive template repository for AI-assisted development workflows using GitHub Copilot. This template provides a structured approach to leveraging AI assistance through pre-configured skills, prompts, agents, and coding standards.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -0,0 +1,238 @@
1
+ // ═════════════════════════════════════════════════════════════════════════════
2
+ // bump-version.test.js — Unit tests for the pre-commit version bumper
3
+ // ═════════════════════════════════════════════════════════════════════════════
4
+ //
5
+ // bump-version.js is a top-level script: it reads package.json on import,
6
+ // derives the previous major version from `git show HEAD:package.json`, writes
7
+ // the new version, and stages the file. Tests therefore mock `fs` and
8
+ // `child_process` so the repository's real package.json is never touched, then
9
+ // re-import the module for each scenario with `vi.resetModules()`.
10
+ //
11
+ // Covered scenarios:
12
+ // 1. SKIP_BUMP=1 → logs skip, exits 0, no fs/git calls
13
+ // 2. SKIP_BUMP=yes → same (any non-empty value is truthy)
14
+ // 3. SKIP_BUMP=0 → same (string "0" is truthy in Node)
15
+ // 4. SKIP_BUMP="" → falsy, bump proceeds
16
+ // 5. Minor bump (same major) → 1.4.7 → 1.5.0
17
+ // 6. Minor bump with multi-digit minor → 1.10.3 → 1.11.0
18
+ // 7. No HEAD (git show fails) → falls back to current major, bumps minor
19
+ // 8. Major changed → 3.2.1 → 3.0.0
20
+ // 9. Exact serialisation → 2-space JSON + trailing newline, fields preserved
21
+ // 10. Git commands → `git show HEAD:package.json` then `git add package.json`
22
+ // ═════════════════════════════════════════════════════════════════════════════
23
+
24
+ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'
25
+
26
+ vi.mock('fs', () => ({
27
+ readFileSync: vi.fn(),
28
+ writeFileSync: vi.fn(),
29
+ }))
30
+
31
+ vi.mock('child_process', () => ({
32
+ execSync: vi.fn(),
33
+ }))
34
+
35
+ import { readFileSync, writeFileSync } from 'fs'
36
+ import { execSync } from 'child_process'
37
+
38
+ const EXIT_SENTINEL = new Error('__process_exit__')
39
+
40
+ /**
41
+ * Re-imports bump-version.js with the current mock state.
42
+ *
43
+ * @returns {Promise<void>}
44
+ */
45
+ async function loadModule() {
46
+ vi.resetModules()
47
+ await import('./bump-version.js')
48
+ }
49
+
50
+ /**
51
+ * Wires the fs/git mocks for a normal (non-skip) run.
52
+ *
53
+ * @param {object} opts
54
+ * @param {object} opts.pkg - Contents returned for package.json.
55
+ * @param {object} [opts.prevPkg] - Contents returned for HEAD:package.json.
56
+ * @param {boolean} [opts.noHead] - When true, `git show` throws.
57
+ */
58
+ function mockNormalRun({ pkg, prevPkg, noHead = false }) {
59
+ readFileSync.mockReturnValue(JSON.stringify(pkg))
60
+ execSync.mockImplementation((cmd) => {
61
+ if (cmd === 'git show HEAD:package.json') {
62
+ if (noHead) throw new Error('fatal: invalid object name HEAD')
63
+ return JSON.stringify(prevPkg)
64
+ }
65
+ return ''
66
+ })
67
+ }
68
+
69
+ describe('bump-version.js', () => {
70
+ let logSpy
71
+ let exitSpy
72
+
73
+ beforeEach(() => {
74
+ vi.clearAllMocks()
75
+ delete process.env.SKIP_BUMP
76
+ logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
77
+ exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {})
78
+ })
79
+
80
+ afterEach(() => {
81
+ delete process.env.SKIP_BUMP
82
+ logSpy.mockRestore()
83
+ exitSpy.mockRestore()
84
+ })
85
+
86
+ // ── SKIP_BUMP guard ──────────────────────────────────────────────────────
87
+
88
+ it('SKIP_BUMP=1: logs skip message, exits 0, touches nothing', async () => {
89
+ process.env.SKIP_BUMP = '1'
90
+ exitSpy.mockImplementation(() => {
91
+ throw EXIT_SENTINEL
92
+ })
93
+
94
+ await expect(loadModule()).rejects.toBe(EXIT_SENTINEL)
95
+
96
+ expect(logSpy).toHaveBeenCalledWith('SKIP_BUMP set — version bump skipped')
97
+ expect(exitSpy).toHaveBeenCalledWith(0)
98
+ expect(readFileSync).not.toHaveBeenCalled()
99
+ expect(writeFileSync).not.toHaveBeenCalled()
100
+ expect(execSync).not.toHaveBeenCalled()
101
+ })
102
+
103
+ it('SKIP_BUMP=yes: any non-empty value skips the bump', async () => {
104
+ process.env.SKIP_BUMP = 'yes'
105
+ exitSpy.mockImplementation(() => {
106
+ throw EXIT_SENTINEL
107
+ })
108
+
109
+ await expect(loadModule()).rejects.toBe(EXIT_SENTINEL)
110
+
111
+ expect(exitSpy).toHaveBeenCalledWith(0)
112
+ expect(writeFileSync).not.toHaveBeenCalled()
113
+ })
114
+
115
+ it('SKIP_BUMP=0: the string "0" is truthy, so the bump is skipped', async () => {
116
+ process.env.SKIP_BUMP = '0'
117
+ exitSpy.mockImplementation(() => {
118
+ throw EXIT_SENTINEL
119
+ })
120
+
121
+ await expect(loadModule()).rejects.toBe(EXIT_SENTINEL)
122
+
123
+ expect(exitSpy).toHaveBeenCalledWith(0)
124
+ expect(execSync).not.toHaveBeenCalled()
125
+ })
126
+
127
+ it('SKIP_BUMP="": empty string is falsy, so the bump proceeds', async () => {
128
+ process.env.SKIP_BUMP = ''
129
+ mockNormalRun({
130
+ pkg: { name: 'fixture', version: '1.4.7' },
131
+ prevPkg: { version: '1.4.0' },
132
+ })
133
+
134
+ await loadModule()
135
+
136
+ expect(exitSpy).not.toHaveBeenCalled()
137
+ expect(logSpy).toHaveBeenCalledWith('Version bumped: 1.4.7 → 1.5.0')
138
+ })
139
+
140
+ // ── Version arithmetic ───────────────────────────────────────────────────
141
+
142
+ it('same major: bumps the minor and resets the patch', async () => {
143
+ mockNormalRun({
144
+ pkg: { name: 'fixture', version: '1.4.7' },
145
+ prevPkg: { version: '1.4.0' },
146
+ })
147
+
148
+ await loadModule()
149
+
150
+ const written = JSON.parse(writeFileSync.mock.calls[0][1])
151
+ expect(written.version).toBe('1.5.0')
152
+ })
153
+
154
+ it('multi-digit minor: increments numerically (1.10.3 → 1.11.0)', async () => {
155
+ mockNormalRun({
156
+ pkg: { name: 'fixture', version: '1.10.3' },
157
+ prevPkg: { version: '1.9.9' },
158
+ })
159
+
160
+ await loadModule()
161
+
162
+ expect(logSpy).toHaveBeenCalledWith('Version bumped: 1.10.3 → 1.11.0')
163
+ })
164
+
165
+ it('no HEAD yet: catch path keeps the current major and bumps the minor', async () => {
166
+ mockNormalRun({
167
+ pkg: { name: 'fixture', version: '2.3.1' },
168
+ noHead: true,
169
+ })
170
+
171
+ await loadModule()
172
+
173
+ expect(logSpy).toHaveBeenCalledWith('Version bumped: 2.3.1 → 2.4.0')
174
+ })
175
+
176
+ it('major changed: normalises to N.0.0', async () => {
177
+ mockNormalRun({
178
+ pkg: { name: 'fixture', version: '3.2.1' },
179
+ prevPkg: { version: '2.9.9' },
180
+ })
181
+
182
+ await loadModule()
183
+
184
+ expect(logSpy).toHaveBeenCalledWith('Version bumped: 3.2.1 → 3.0.0')
185
+ })
186
+
187
+ // ── Serialisation and git side-effects ───────────────────────────────────
188
+
189
+ it('writes 2-space JSON with a trailing newline and preserves other fields', async () => {
190
+ const pkg = {
191
+ name: 'fixture',
192
+ version: '1.4.7',
193
+ scripts: { test: 'vitest' },
194
+ keywords: ['a', 'b'],
195
+ }
196
+ mockNormalRun({ pkg, prevPkg: { version: '1.0.0' } })
197
+
198
+ await loadModule()
199
+
200
+ const [path, contents] = writeFileSync.mock.calls[0]
201
+ expect(path).toMatch(/package\.json$/)
202
+ expect(contents.endsWith('\n')).toBe(true)
203
+ expect(contents).toBe(
204
+ JSON.stringify({ ...pkg, version: '1.5.0' }, null, 2) + '\n',
205
+ )
206
+ })
207
+
208
+ it('reads HEAD:package.json then stages package.json', async () => {
209
+ mockNormalRun({
210
+ pkg: { name: 'fixture', version: '0.9.2' },
211
+ prevPkg: { version: '0.9.0' },
212
+ })
213
+
214
+ await loadModule()
215
+
216
+ expect(execSync).toHaveBeenNthCalledWith(
217
+ 1,
218
+ 'git show HEAD:package.json',
219
+ { encoding: 'utf8' },
220
+ )
221
+ expect(execSync).toHaveBeenNthCalledWith(2, 'git add package.json')
222
+ expect(execSync).toHaveBeenCalledTimes(2)
223
+ })
224
+
225
+ it('reads package.json from the package root', async () => {
226
+ mockNormalRun({
227
+ pkg: { name: 'fixture', version: '1.0.0' },
228
+ prevPkg: { version: '1.0.0' },
229
+ })
230
+
231
+ await loadModule()
232
+
233
+ expect(readFileSync).toHaveBeenCalledWith(
234
+ expect.stringMatching(/package\.json$/),
235
+ 'utf8',
236
+ )
237
+ })
238
+ })
@@ -6,41 +6,67 @@
6
6
  * based on environment variables and project settings.
7
7
  */
8
8
 
9
- const { spawn } = require('child_process');
10
- const path = require('path');
9
+ import { spawn } from 'node:child_process'
10
+ import { resolve } from 'node:path'
11
+ import { fileURLToPath } from 'node:url'
11
12
 
12
- // Get environment variables
13
- const headless = process.env.HEADLESS === 'true';
14
- const slowMo = process.env.SLOW_MO ? parseInt(process.env.SLOW_MO) : 0;
13
+ /**
14
+ * Builds the `npx @playwright/mcp@latest` argument list from the environment.
15
+ *
16
+ * @param {NodeJS.ProcessEnv} [env=process.env] - Environment to read.
17
+ * @returns {string[]} Arguments passed to `npx`.
18
+ */
19
+ export function buildArgs(env = process.env) {
20
+ const args = ['@playwright/mcp@latest']
15
21
 
16
- // Build command arguments
17
- const args = ['@playwright/mcp@latest'];
22
+ if (env.HEADLESS === 'true') {
23
+ args.push('--headless')
24
+ }
18
25
 
19
- // Add headless flag if needed
20
- if (headless) {
21
- args.push('--headless');
22
- }
26
+ if (env.VIEWPORT) {
27
+ args.push('--viewport', env.VIEWPORT)
28
+ }
23
29
 
24
- // Add viewport size if specified
25
- if (process.env.VIEWPORT) {
26
- args.push('--viewport', process.env.VIEWPORT);
30
+ return args
27
31
  }
28
32
 
29
- // Launch the MCP server
30
- const child = spawn('npx', args, {
31
- stdio: 'inherit',
32
- shell: true
33
- });
33
+ /**
34
+ * Spawns the Playwright MCP server and forwards signals / exit codes.
35
+ *
36
+ * @param {object} [options]
37
+ * @param {object} [options.env=process.env] - Environment to read.
38
+ * @param {object} [options.proc=process] - Process-like emitter for signals.
39
+ * @param {Function} [options.exit=process.exit] - Exit function.
40
+ * @returns {import('node:child_process').ChildProcess} The spawned child.
41
+ */
42
+ export function launch({
43
+ env = process.env,
44
+ proc = process,
45
+ exit = process.exit,
46
+ } = {}) {
47
+ const child = spawn('npx', buildArgs(env), {
48
+ stdio: 'inherit',
49
+ shell: true,
50
+ })
51
+
52
+ proc.on('SIGINT', () => {
53
+ child.kill('SIGINT')
54
+ })
55
+
56
+ proc.on('SIGTERM', () => {
57
+ child.kill('SIGTERM')
58
+ })
34
59
 
35
- // Handle process signals
36
- process.on('SIGINT', () => {
37
- child.kill('SIGINT');
38
- });
60
+ child.on('exit', (code) => {
61
+ exit(code || 0)
62
+ })
39
63
 
40
- process.on('SIGTERM', () => {
41
- child.kill('SIGTERM');
42
- });
64
+ return child
65
+ }
43
66
 
44
- child.on('exit', (code) => {
45
- process.exit(code || 0);
46
- });
67
+ const isDirectRun =
68
+ process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)
69
+
70
+ if (isDirectRun) {
71
+ launch()
72
+ }