@abarcenas/ai-workflow-template 1.49.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.
- package/.agents/instructions/knowledgebase.instructions.md +2 -2
- package/.agents/instructions/learned-knowledge.instructions.md +47 -2
- package/.agents/instructions/memory-schema.instructions.md +1 -1
- package/.github/workflows/npm-publish.yml +69 -1
- package/.opencode/plugins/graphify.js +22 -0
- package/LICENSE +15 -0
- package/README.md +56 -22
- package/memory-bank/.vocabulary.json +3 -0
- package/opencode.mcp.example.json +13 -0
- package/package.json +20 -10
- package/scripts/bump-version.js +18 -2
- package/scripts/mcp/playwright-mcp-launcher.js +45 -2
- package/scripts/bump-version.test.js +0 -238
- package/scripts/knowledgebase-cli.test.js +0 -576
- package/scripts/knowledgebase-index.test.js +0 -759
- package/scripts/mcp/playwright-mcp-launcher.test.js +0 -217
- package/scripts/mcp-knowledgebase-server.test.js +0 -745
- package/scripts/mcp-memory-server.test.js +0 -398
- package/scripts/memory-cli.test.js +0 -297
- package/scripts/memory-index.test.js +0 -1173
- package/scripts/normalize-memory.test.js +0 -753
- package/scripts/setup/discover.test.js +0 -530
- package/scripts/setup/hooks.test.js +0 -722
- package/scripts/setup/husky-init.test.js +0 -311
- package/scripts/setup/index.test.js +0 -1021
- package/scripts/setup/knowledgebase.test.js +0 -352
- package/scripts/setup/prepare.test.js +0 -718
- package/scripts/setup/skills-phase.test.js +0 -1274
- package/scripts/setup/sync-phase.test.js +0 -373
- package/scripts/setup/ui.test.js +0 -579
- package/scripts/setup/utils.test.js +0 -586
- package/scripts/sync-unit.test.js +0 -1881
- package/scripts/sync.test.js +0 -472
- package/scripts/validate-memory-schema.test.js +0 -328
- 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
|
-
})
|