@abarcenas/ai-workflow-template 1.48.0 → 2.0.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 (37) hide show
  1. package/.agents/instructions/knowledgebase.instructions.md +2 -2
  2. package/.agents/instructions/learned-knowledge.instructions.md +47 -2
  3. package/.agents/instructions/memory-schema.instructions.md +1 -1
  4. package/.github/workflows/npm-publish.yml +69 -1
  5. package/.opencode/agents/implementer.md +3 -1
  6. package/.opencode/agents/unit-tester.md +21 -2
  7. package/.opencode/plugins/graphify.js +22 -0
  8. package/LICENSE +15 -0
  9. package/README.md +56 -22
  10. package/memory-bank/.vocabulary.json +3 -0
  11. package/opencode.mcp.example.json +13 -0
  12. package/package.json +20 -10
  13. package/scripts/bump-version.js +18 -2
  14. package/scripts/mcp/playwright-mcp-launcher.js +45 -2
  15. package/scripts/bump-version.test.js +0 -238
  16. package/scripts/knowledgebase-cli.test.js +0 -576
  17. package/scripts/knowledgebase-index.test.js +0 -759
  18. package/scripts/mcp/playwright-mcp-launcher.test.js +0 -217
  19. package/scripts/mcp-knowledgebase-server.test.js +0 -745
  20. package/scripts/mcp-memory-server.test.js +0 -398
  21. package/scripts/memory-cli.test.js +0 -297
  22. package/scripts/memory-index.test.js +0 -1173
  23. package/scripts/normalize-memory.test.js +0 -753
  24. package/scripts/setup/discover.test.js +0 -530
  25. package/scripts/setup/hooks.test.js +0 -722
  26. package/scripts/setup/husky-init.test.js +0 -311
  27. package/scripts/setup/index.test.js +0 -1021
  28. package/scripts/setup/knowledgebase.test.js +0 -352
  29. package/scripts/setup/prepare.test.js +0 -718
  30. package/scripts/setup/skills-phase.test.js +0 -1274
  31. package/scripts/setup/sync-phase.test.js +0 -373
  32. package/scripts/setup/ui.test.js +0 -579
  33. package/scripts/setup/utils.test.js +0 -586
  34. package/scripts/sync-unit.test.js +0 -1881
  35. package/scripts/sync.test.js +0 -472
  36. package/scripts/validate-memory-schema.test.js +0 -328
  37. package/scripts/vocab-sync.test.js +0 -902
@@ -10,11 +10,32 @@ import { spawn } from 'node:child_process'
10
10
  import { resolve } from 'node:path'
11
11
  import { fileURLToPath } from 'node:url'
12
12
 
13
+ /**
14
+ * Browser values accepted by `@playwright/mcp --browser <browser>`.
15
+ * `chrome` is a Chrome *channel* (your installed Google Chrome), not the
16
+ * bundled Chromium build.
17
+ */
18
+ export const BROWSER_VALUES = ['chrome', 'firefox', 'webkit', 'msedge']
19
+
13
20
  /**
14
21
  * Builds the `npx @playwright/mcp@latest` argument list from the environment.
15
22
  *
23
+ * Supported variables (all optional, order of flags is fixed):
24
+ * - `HEADLESS=true` → `--headless`
25
+ * - `BROWSER=<name>` → `--browser <name>` (chrome | firefox | webkit | msedge)
26
+ * - `ISOLATED=true` → `--isolated` (in-memory profile; no state written to disk)
27
+ * - `USER_DATA_DIR=<path>` → `--user-data-dir <path>` (persistent profile; ignored
28
+ * when `ISOLATED=true`, because the CLI rejects the combination outright:
29
+ * "Browser userDataDir is not supported in isolated mode.")
30
+ * - `VIEWPORT=<WxH>` → `--viewport-size <WxH>`
31
+ *
32
+ * `SLOW_MO` is intentionally not read: @playwright/mcp exposes no `--slow-mo`
33
+ * flag. Slow motion applies to the Playwright test runner only
34
+ * (`playwright.config.ts`), not to the MCP server.
35
+ *
16
36
  * @param {NodeJS.ProcessEnv} [env=process.env] - Environment to read.
17
37
  * @returns {string[]} Arguments passed to `npx`.
38
+ * @throws {Error} When `BROWSER` is set to a value the CLI does not accept.
18
39
  */
19
40
  export function buildArgs(env = process.env) {
20
41
  const args = ['@playwright/mcp@latest']
@@ -23,8 +44,26 @@ export function buildArgs(env = process.env) {
23
44
  args.push('--headless')
24
45
  }
25
46
 
47
+ if (env.BROWSER) {
48
+ if (!BROWSER_VALUES.includes(env.BROWSER)) {
49
+ throw new Error(
50
+ `Invalid BROWSER "${env.BROWSER}". Expected one of: ${BROWSER_VALUES.join(', ')}.`,
51
+ )
52
+ }
53
+ args.push('--browser', env.BROWSER)
54
+ }
55
+
56
+ // Isolation wins over an explicit profile directory: the two are mutually
57
+ // exclusive (the CLI refuses `--isolated` together with `--user-data-dir`),
58
+ // and isolation is the safer of the two.
59
+ if (env.ISOLATED === 'true') {
60
+ args.push('--isolated')
61
+ } else if (env.USER_DATA_DIR) {
62
+ args.push('--user-data-dir', env.USER_DATA_DIR)
63
+ }
64
+
26
65
  if (env.VIEWPORT) {
27
- args.push('--viewport', env.VIEWPORT)
66
+ args.push('--viewport-size', env.VIEWPORT)
28
67
  }
29
68
 
30
69
  return args
@@ -46,7 +85,11 @@ export function launch({
46
85
  } = {}) {
47
86
  const child = spawn('npx', buildArgs(env), {
48
87
  stdio: 'inherit',
49
- shell: true,
88
+ // `shell: true` on every platform would route argv through a shell, where
89
+ // an env value containing spaces (VIEWPORT, USER_DATA_DIR) can split or
90
+ // inject tokens. POSIX resolves the `npx` shim from PATH without a shell;
91
+ // Windows needs the shell for `npx.cmd`.
92
+ shell: process.platform === 'win32',
50
93
  })
51
94
 
52
95
  proc.on('SIGINT', () => {
@@ -1,238 +0,0 @@
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
- })