@abarcenas/ai-workflow-template 1.45.0 → 1.47.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 (49) 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 +128 -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/.prettierignore +26 -0
  19. package/AGENTS.md +46 -3
  20. package/README.md +29 -2
  21. package/opencode.mcp.example.json +1 -0
  22. package/package.json +3 -1
  23. package/scripts/bump-version.test.js +238 -0
  24. package/scripts/mcp/playwright-mcp-launcher.js +55 -29
  25. package/scripts/mcp/playwright-mcp-launcher.test.js +217 -0
  26. package/scripts/mcp-memory-server.js +93 -76
  27. package/scripts/mcp-memory-server.test.js +398 -0
  28. package/scripts/memory-cli.test.js +297 -0
  29. package/scripts/memory-index.js +119 -31
  30. package/scripts/memory-index.test.js +1173 -0
  31. package/scripts/normalize-memory.js +26 -1
  32. package/scripts/normalize-memory.test.js +753 -0
  33. package/scripts/setup/discover.test.js +124 -2
  34. package/scripts/setup/hooks.test.js +188 -1
  35. package/scripts/setup/husky-init.test.js +311 -0
  36. package/scripts/setup/index.js +35 -5
  37. package/scripts/setup/index.test.js +279 -0
  38. package/scripts/setup/knowledgebase.test.js +100 -0
  39. package/scripts/setup/skills-phase.js +568 -0
  40. package/scripts/setup/skills-phase.test.js +1274 -0
  41. package/scripts/setup/sync-phase.js +6 -1
  42. package/scripts/setup/sync-phase.test.js +180 -2
  43. package/scripts/setup/ui.test.js +579 -0
  44. package/scripts/setup/utils.test.js +586 -0
  45. package/scripts/sync-unit.test.js +1881 -0
  46. package/scripts/sync.js +816 -239
  47. package/scripts/sync.test.js +283 -0
  48. package/scripts/vocab-sync.test.js +88 -0
  49. package/skills-lock.json +90 -0
package/AGENTS.md CHANGED
@@ -9,10 +9,14 @@ This project includes AI-assisted development configuration distributed through
9
9
  - `.opencode/agents/` — Specialized agent definitions for planning, research, and implementation
10
10
  - `.agents/prompts/` — Reusable prompt templates
11
11
 
12
+ ## Project-Specific Rules
13
+
14
+ Put project-specific agent rules in `AGENTS.project.md` at the repository root. That file is project-owned: sync never overwrites or prunes it, and `--force` cannot replace it. New projects load it automatically through the `instructions` entry in `opencode.json`.
15
+
12
16
  ## Core Workflow
13
17
 
14
18
  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
19
+ 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
20
  3. **Track progress** — Update `memory-bank/activeContext.md` and `memory-bank/progress.md` after each task (see `.agents/instructions/task-implementation.instructions.md`)
17
21
  4. **Maintain context** — Keep memory bank files updated (see `.agents/instructions/memory-bank.instructions.md`)
18
22
 
@@ -41,7 +45,7 @@ This project includes AI-assisted development configuration distributed through
41
45
 
42
46
  ## Available Skills
43
47
 
44
- Skills in `.agents/skills/` provide structured workflows for common tasks. Load them when the task matches the skill's purpose:
48
+ 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
49
 
46
50
  - `architecture-blueprint-generator` — Generate project architecture blueprints
47
51
  - `conventional-commit` — Create conventional commit messages
@@ -69,6 +73,14 @@ Skills in `.agents/skills/` provide structured workflows for common tasks. Load
69
73
  - `caveman-stats` — Tracking and statistics for caveman usage
70
74
  - `cavecrew` — Multi-agent coordination for caveman skills
71
75
  - `compress` — General compression utilities
76
+ - `domain-modeling` — Build and sharpen the project's domain model (`GLOSSARY.md`, ADRs)
77
+ - `graphify-framework-aware` — Exclude framework boilerplate from graphify knowledge graphs
78
+ - `grill-me` — Human-invoked router for a relentless interview (calls `grilling`)
79
+ - `grill-with-docs` — Human-invoked router for the interview plus glossary/ADR writing
80
+ - `grilling` — Stress-test a plan or idea with rounds of numbered questions
81
+ - `lighthouse-cli` — Run Lighthouse performance, accessibility, and SEO audits
82
+ - `memory-search` — Semantic vector search over the memory bank
83
+ - `prototype` — Build a throwaway prototype to answer a design question
72
84
 
73
85
  ## Available Prompts
74
86
 
@@ -79,13 +91,44 @@ Prompts in `.agents/prompts/` can be invoked directly:
79
91
  - `playwright-generate-test` — Generate Playwright tests
80
92
  - `documentation-writer` — Write documentation (Diataxis framework)
81
93
 
94
+ ## Agent skills
95
+
96
+ 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.
97
+
98
+ ### Skills
99
+
100
+ | Skill | Path | Use it when |
101
+ |-------|------|-------------|
102
+ | `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. |
103
+ | `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. |
104
+ | `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. |
105
+ | `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. |
106
+ | `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. |
107
+
108
+ `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.
109
+
110
+ ### Scope agent
111
+
112
+ `.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.
113
+
114
+ 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).
115
+
116
+ 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).
117
+
118
+ ### Domain docs
119
+
120
+ `.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.
121
+ 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.
122
+
82
123
  ## Available Agent Workflows
83
124
 
84
125
  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
126
 
86
127
  ### Plan Mode — Strategic Planning & Architecture
87
128
 
88
- Use this when starting a new feature, refactoring, or any non-trivial change. Follow this sequence:
129
+ 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.
130
+
131
+ `.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
132
 
90
133
  1. **Understand the Goal** — Ask clarifying questions about requirements and goals
91
134
  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)
@@ -75,8 +75,29 @@ npm run sync # Normal sync
75
75
  - New files are copied automatically
76
76
  - Unmodified files are refreshed from the package
77
77
  - Locally edited files are preserved (skipped by default)
78
+ - Project-owned files are never overwritten or pruned after creation, and `--force` cannot replace them
78
79
  - GitHub Actions workflows are excluded from consumer sync (`.github/workflows/`)
79
80
 
81
+ > `--dry-run` previews copies and prunes, but missing scaffold files (memory-bank stubs, `opencode.json`, `AGENTS.project.md`, `.prettierignore`) are still created. This matches the pre-existing scaffold behaviour.
82
+
83
+ ### Project-owned files
84
+
85
+ `AGENTS.project.md` is the supported home for project-specific agent rules. It is created if absent, then treated as project-owned: sync never overwrites or prunes it, `--force` cannot replace it, and deleting it recreates it on the next sync. New projects load it through `"instructions": ["AGENTS.project.md"]` in `opencode.json`; a project created before that entry existed must add it once, because sync never rewrites an existing `opencode.json`.
86
+
87
+ The same ownership rule protects `opencode.json`, `.prettierignore`, and `.agents/instructions/learned-knowledge.instructions.md`. `.agents-sync-ignore` is also protected from overwrite and deletion, but sync does **not** read or honour its contents yet — do not rely on it.
88
+
89
+ `__project__/<file>` entries in `.agents-sync-manifest.json` are reporting metadata written only when sync creates the file. Files that already existed before the upgrade are intentionally absent, because sync has no managed baseline for them.
90
+
91
+ The package ships a `.prettierignore` and scaffolds it when absent. New projects get coverage automatically for `.agents/`, `.opencode/agents/`, `.opencode/commands/`, `AGENTS.md`, `skills-lock.json`, and every synced script; the shipped file also ignores `AGENTS.project.md`, though that file is not hash-managed. Existing projects keep their own `.prettierignore` untouched (it is project-owned), so if it does not cover every managed path sync prints one warning:
92
+
93
+ ```text
94
+ [ai-workflow-template] Existing .prettierignore does not protect managed files; merge the patterns from node_modules/@abarcenas/ai-workflow-template/.prettierignore
95
+ ```
96
+
97
+ Merge those patterns manually — sync never edits the file. Template-side additions to `rootFiles` or `scriptsToSync` also require a matching manual line in an existing project; the shipped file only updates the defaults for new projects. A formatter run over managed files rewrites their hashes, so every later sync skips them and the consumer stops receiving template updates.
98
+
99
+ If sync warns `Skipped locally modified root file: AGENTS.md`, move those local rules into `AGENTS.project.md`, restore the managed file from `node_modules/@abarcenas/ai-workflow-template/AGENTS.md`, then re-run sync.
100
+
80
101
  ## Core Structure
81
102
 
82
103
  Post-install, your project receives this structure:
@@ -84,12 +105,13 @@ Post-install, your project receives this structure:
84
105
  ```text
85
106
  .agents/
86
107
  ├── instructions/ # Framework, workflow, and security guidance
87
- ├── skills/ # 29 specialized AI skills
108
+ ├── skills/ # 34 specialized AI skills
88
109
  └── prompts/ # Reusable prompt templates
89
110
  .opencode/agents/ # Orchestrator + specialized sub-agents
90
111
  .github/workflows/ # CI/CD automation (excluded from sync)
91
112
  docs/ # Guides (e.g. playwright-mcp-configuration.md)
92
113
  AGENTS.md # Master instruction file with agent guidance
114
+ AGENTS.project.md # Project-owned rules (recreated if deleted, never overwritten)
93
115
  ```
94
116
 
95
117
  ## Multi-Agent Orchestrator
@@ -99,6 +121,7 @@ AGENTS.md # Master instruction file with agent guidance
99
121
  | Agent | File | Role |
100
122
  | --- | --- | --- |
101
123
  | Orchestrator | `.opencode/agents/orchestrator/orchestrator.agent.md` | Master — analyzes requests, builds pipelines, delegates |
124
+ | Scope | `.opencode/agents/scope.agent.md` | Conditional front door for underspecified requests — grilling rounds, then a spike doc |
102
125
  | Researcher | `.opencode/agents/researcher.agent.md` | Technical investigation, library evaluation |
103
126
  | Architect | `.opencode/agents/architect.agent.md` | System design, component architecture |
104
127
  | Implementer | `.opencode/agents/implementer.agent.md` | Translates designs into implementation plans |
@@ -121,6 +144,7 @@ Invoke the orchestrator with `@orchestrator` followed by your task:
121
144
  ```
122
145
 
123
146
  - **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.
147
+ - **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
148
  - **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
149
 
126
150
  ### MCP Tooling
@@ -222,6 +246,7 @@ All memory-bank files use YAML frontmatter (`id`, `title`, `updated`, `tags`, `e
222
246
 
223
247
  ## Featured Skills
224
248
 
249
+ - **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
250
  - **Workflow automation:** `architecture-blueprint-generator`, `create-implementation-plan`, `create-readme`, `context-map`, `graphify` (global skill), `graphify-framework-aware`
226
251
  - **Development tools:** `playwright-explore-website`, `playwright-generate-test`, `github-issues`, `gh-cli`, `polygot-test-agent`, `microsoft-docs`
227
252
  - **Token & efficiency:** `caveman`, `caveman-compress`, `compress`, `caveman-commit`, `cavecrew`, `caveman-stats`
@@ -283,6 +308,8 @@ npm run memory:normalize # Normalize frontmatter (runs automatically on instal
283
308
 
284
309
  - Uses `INIT_CWD` to install against the consuming project (not the package dir)
285
310
  - Local changes are always preserved unless `--force` is used
311
+ - Project-owned files (`AGENTS.project.md`, `opencode.json`, `.prettierignore`, learned knowledge) are created if absent and never overwritten or pruned after that, even with `--force`
312
+ - Do not add `AGENTS.project.md` or `opencode.json` to `package.json` `files`: both are scaffolded create-if-absent and must never ship from the package
286
313
  - `AGENTS.md` is the main instruction file agents should read
287
314
  - Package assumes the public npm registry
288
315
  - Playwright MCP integration is documented in `docs/`
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "$schema": "https://opencode.ai/config.json",
3
+ "instructions": ["AGENTS.project.md"],
3
4
  "mcp": {
4
5
  "chrome-devtools": {
5
6
  "type": "local",
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.47.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": {
@@ -38,6 +38,7 @@
38
38
  "url": "https://github.com/abarcenas29/ai-workflow-template/issues"
39
39
  },
40
40
  "homepage": "https://github.com/abarcenas29/ai-workflow-template#readme",
41
+ "//": "Never add AGENTS.project.md or opencode.json to files: both are scaffolded create-if-absent at the consumer root and must not ship from the package.",
41
42
  "files": [
42
43
  "bin/",
43
44
  ".agents/",
@@ -49,6 +50,7 @@
49
50
  ".husky/",
50
51
  "memory-bank/.vocabulary.json",
51
52
  "AGENTS.md",
53
+ ".prettierignore",
52
54
  "opencode.mcp.example.json",
53
55
  "skills-lock.json",
54
56
  ".husky/post-merge",
@@ -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
+ }