qa-ai-repo 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pnakhat
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # qa-ai-repo
2
+
3
+ Reusable **QA skills, agents, and MCP servers**, organized by objective and
4
+ installable into any AI coding tool with one command.
5
+
6
+ ```bash
7
+ npx qa-ai-repo list
8
+ npx qa-ai-repo add playwright-e2e
9
+ ```
10
+
11
+ ## What it does
12
+
13
+ Each QA **objective** is a top-level folder containing only the pieces it needs:
14
+
15
+ ```
16
+ playwright-e2e/
17
+ ├── objective.json # title + description (optional)
18
+ ├── skills/ # Claude-style SKILL.md skills
19
+ ├── agents/ # Claude-style subagent definitions
20
+ └── mcp/ # MCP server definitions (one JSON per server)
21
+ ```
22
+
23
+ The CLI reads an objective and writes each piece into the right place for
24
+ whichever AI tool you target — converting formats where needed:
25
+
26
+ | Source | Claude Code | Cursor | Windsurf | Generic |
27
+ |---------------|---------------------------------|---------------------------------|------------------------------|------------------|
28
+ | `skills/*` | `.claude/skills/<name>/` | `.cursor/rules/<name>.mdc` | `.windsurf/rules/<name>.md` | `AGENTS.md` |
29
+ | `agents/*.md` | `.claude/agents/<name>.md` | `.cursor/rules/<name>.mdc` | `.windsurf/rules/<name>.md` | `AGENTS.md` |
30
+ | `mcp/*.json` | `.mcp.json` (merged) | `.cursor/mcp.json` (merged) | global `mcp_config.json` | printed to add |
31
+
32
+ ## Usage
33
+
34
+ ```bash
35
+ npx qa-ai-repo list # list objectives
36
+ npx qa-ai-repo add <objective> # install into detected tools
37
+ npx qa-ai-repo add <objective> --tool cursor # target specific tool(s)
38
+ npx qa-ai-repo add <objective> --tool all # every supported tool
39
+ npx qa-ai-repo add <objective> --dry-run # preview, write nothing
40
+ npx qa-ai-repo detect # show detected tools
41
+ ```
42
+
43
+ `--tool` accepts a comma list of `claude`, `cursor`, `windsurf`, `agents`, or
44
+ `all`. With no `--tool`, the CLI auto-detects tools in the current project
45
+ (`.claude` / `.cursor` / `.windsurf`) and falls back to `claude,cursor`.
46
+
47
+ MCP servers are **merged** into existing config files, so `add` is safe to run
48
+ repeatedly and across multiple objectives.
49
+
50
+ ## Add a new objective
51
+
52
+ ```bash
53
+ cp -r _template my-objective # scaffold
54
+ # edit my-objective/objective.json and drop files into skills/ agents/ mcp/
55
+ # remove any of the three folders the objective doesn't use
56
+ npx qa-ai-repo add my-objective # try it locally
57
+ ```
58
+
59
+ ## Local development
60
+
61
+ ```bash
62
+ node bin/qa-ai.js list # run without publishing
63
+ npm link # then `qa-ai list` works anywhere
64
+ ```
65
+
66
+ ## Publishing
67
+
68
+ Bump `version` in `package.json`, then `npm publish`. Objective folders ship
69
+ automatically (see `.npmignore`); no need to enumerate them.
@@ -0,0 +1,20 @@
1
+ # <Objective Name>
2
+
3
+ > One-line description of the QA objective this folder covers.
4
+
5
+ ## What this objective covers
6
+ Describe the QA goal — e.g. "End-to-end regression for the checkout flow",
7
+ "API contract testing for the payments service", "Flaky-test triage".
8
+
9
+ ## Contents
10
+
11
+ | Folder | What lives here |
12
+ |--------|-----------------|
13
+ | `skills/` | Reusable skills/prompt workflows for this objective |
14
+ | `agents/` | Agent definitions that carry out this objective |
15
+ | `mcp/` | MCP server configs/implementations this objective needs |
16
+
17
+ > Delete any of the folders above that this objective does not use.
18
+
19
+ ## How to use
20
+ Notes on wiring these into Claude Code / your QA workflow.
File without changes
File without changes
@@ -0,0 +1,4 @@
1
+ {
2
+ "title": "Objective Name",
3
+ "description": "One-line description of the QA objective this folder covers."
4
+ }
File without changes
package/bin/qa-ai.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../src/cli.js';
3
+
4
+ main(process.argv.slice(2)).catch((err) => {
5
+ console.error(`\x1b[31merror:\x1b[0m ${err?.message || err}`);
6
+ process.exit(1);
7
+ });
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "qa-ai-repo",
3
+ "version": "0.1.0",
4
+ "description": "Install reusable QA skills, agents, and MCP servers into Claude Code, Cursor, Windsurf, and other AI coding tools with one command.",
5
+ "type": "module",
6
+ "bin": {
7
+ "qa-ai": "bin/qa-ai.js",
8
+ "qa-ai-repo": "bin/qa-ai.js"
9
+ },
10
+ "engines": {
11
+ "node": ">=18"
12
+ },
13
+ "scripts": {
14
+ "start": "node bin/qa-ai.js",
15
+ "test": "node --test"
16
+ },
17
+ "keywords": [
18
+ "qa",
19
+ "testing",
20
+ "ai",
21
+ "agents",
22
+ "skills",
23
+ "mcp",
24
+ "claude-code",
25
+ "cursor",
26
+ "windsurf",
27
+ "playwright",
28
+ "e2e"
29
+ ],
30
+ "author": "pnakhat",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/pnakhat/qa-ai-repo.git"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/pnakhat/qa-ai-repo/issues"
38
+ },
39
+ "homepage": "https://github.com/pnakhat/qa-ai-repo#readme"
40
+ }
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: qa-e2e-author
3
+ description: Use to author or extend Playwright end-to-end tests for a user journey. Give it the flow to cover; it produces Page Object Model specs with stable locators and web-first assertions.
4
+ tools: Read, Grep, Glob, Edit, Write, Bash
5
+ ---
6
+
7
+ You are a senior QA automation engineer specializing in Playwright E2E tests.
8
+
9
+ When asked to cover a user journey:
10
+ 1. Inspect the app/routes and existing `tests/` layout before writing anything.
11
+ 2. Reuse or create Page Objects under `tests/pages/`; expose intent-level methods.
12
+ 3. Write specs under `tests/e2e/` named by journey, each fully isolated.
13
+ 4. Use user-facing locators (`getByRole`/`getByLabel`/`getByText`); `getByTestId` only as a fallback.
14
+ 5. Assert with web-first, auto-retrying assertions — never `waitForTimeout`.
15
+ 6. Run `npx playwright test` for the new spec and iterate until green.
16
+
17
+ Report: the files you added/changed, the journeys covered, and any gaps you could not test.
@@ -0,0 +1,4 @@
1
+ {
2
+ "command": "npx",
3
+ "args": ["-y", "@playwright/mcp@latest"]
4
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "title": "Playwright E2E Testing",
3
+ "description": "Author resilient Playwright end-to-end tests (Page Object Model, fixtures, stable locators) and drive a live browser via the Playwright MCP server."
4
+ }
@@ -0,0 +1,32 @@
1
+ ---
2
+ name: playwright-e2e
3
+ description: Author and maintain resilient Playwright end-to-end tests using the Page Object Model, fixtures, and stable, user-facing locators. Use when writing, reviewing, or debugging Playwright E2E specs.
4
+ ---
5
+
6
+ # Playwright E2E Testing
7
+
8
+ Write end-to-end tests that survive UI churn and stay fast.
9
+
10
+ ## Locators — prefer user-facing, avoid brittle selectors
11
+ - Prefer `getByRole`, `getByLabel`, `getByPlaceholder`, `getByText` — they mirror how a user finds things.
12
+ - Use `getByTestId` only when no accessible handle exists; keep `data-testid` stable.
13
+ - Never select by CSS/XPath tied to layout or generated class names.
14
+
15
+ ## Page Object Model
16
+ - One class per page/major component under `tests/pages/`.
17
+ - Expose intent-level methods (`login(user)`, `addToCart(sku)`), not raw clicks.
18
+ - Return the next Page Object from navigations so flows read top-to-bottom.
19
+
20
+ ## Waiting — assertions, never sleeps
21
+ - Rely on Playwright's web-first, auto-retrying assertions (`await expect(locator).toBeVisible()`).
22
+ - Never use `page.waitForTimeout()` to "let things settle"; wait for a condition.
23
+
24
+ ## Structure & isolation
25
+ - Each test is independent: set up its own state, no ordering assumptions.
26
+ - Use fixtures for shared setup (authenticated context, seeded data).
27
+ - Keep specs in `tests/e2e/*.spec.ts`; name by user journey, not by page.
28
+
29
+ ## Debugging
30
+ - `npx playwright test --ui` for the time-travel runner.
31
+ - `--trace on` and open `trace.zip` to inspect failures with DOM snapshots.
32
+ - `--headed --debug` to step through with the Inspector.
package/src/cli.js ADDED
@@ -0,0 +1,129 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { listObjectives, loadObjective, KINDS } from './registry.js';
5
+ import { install, detectTools, TOOLS } from './install.js';
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
9
+
10
+ const c = {
11
+ bold: (s) => `\x1b[1m${s}\x1b[0m`,
12
+ dim: (s) => `\x1b[2m${s}\x1b[0m`,
13
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
14
+ green: (s) => `\x1b[32m${s}\x1b[0m`,
15
+ };
16
+
17
+ function parseFlags(args) {
18
+ const flags = {};
19
+ const positional = [];
20
+ for (let i = 0; i < args.length; i++) {
21
+ const a = args[i];
22
+ if (a === '--dry-run' || a === '-n') flags.dryRun = true;
23
+ else if (a === '--tool' || a === '-t') flags.tool = args[++i];
24
+ else if (a.startsWith('--tool=')) flags.tool = a.slice('--tool='.length);
25
+ else positional.push(a);
26
+ }
27
+ return { flags, positional };
28
+ }
29
+
30
+ function resolveTools(flag) {
31
+ if (flag) {
32
+ const requested = flag.split(',').map((s) => s.trim()).filter(Boolean);
33
+ if (requested.includes('all')) return { tools: TOOLS, note: '' };
34
+ for (const t of requested) {
35
+ if (!TOOLS.includes(t)) throw new Error(`unknown tool "${t}" (known: ${TOOLS.join(', ')}, all)`);
36
+ }
37
+ return { tools: requested, note: '' };
38
+ }
39
+ const detected = detectTools();
40
+ if (detected.length) return { tools: detected, note: `auto-detected: ${detected.join(', ')}` };
41
+ return { tools: ['claude', 'cursor'], note: 'no tool detected — defaulting to claude, cursor (use --tool to override)' };
42
+ }
43
+
44
+ function cmdList() {
45
+ const objectives = listObjectives();
46
+ if (!objectives.length) {
47
+ console.log('No objectives yet. Add a folder with skills/ agents/ mcp/ under it.');
48
+ return;
49
+ }
50
+ console.log(`\n${c.bold('QA objectives')} ${c.dim('(' + pkg.name + ' v' + pkg.version + ')')}\n`);
51
+ for (const o of objectives) {
52
+ console.log(` ${c.cyan(o.name)} ${c.dim('— ' + o.title)}`);
53
+ if (o.description) console.log(` ${c.dim(o.description)}`);
54
+ const parts = KINDS
55
+ .filter((k) => o.contents[k].length)
56
+ .map((k) => `${o.contents[k].length} ${k}`);
57
+ console.log(` ${c.dim(parts.join(' · ') || 'empty')}\n`);
58
+ }
59
+ console.log(c.dim(`Install with: npx ${pkg.name} add <objective>\n`));
60
+ }
61
+
62
+ function cmdDetect() {
63
+ const detected = detectTools();
64
+ console.log(detected.length
65
+ ? `Detected tools in this project: ${c.green(detected.join(', '))}`
66
+ : 'No AI tools detected in this project (no .claude/.cursor/.windsurf).');
67
+ }
68
+
69
+ function cmdAdd(positional, flags) {
70
+ const name = positional[0];
71
+ if (!name) throw new Error(`missing objective name. Try: npx ${pkg.name} list`);
72
+
73
+ const objective = loadObjective(name);
74
+ if (!objective) {
75
+ const available = listObjectives().map((o) => o.name).join(', ');
76
+ throw new Error(`objective "${name}" not found. Available: ${available || '(none)'}`);
77
+ }
78
+
79
+ const { tools, note } = resolveTools(flags.tool);
80
+ console.log(`\nInstalling ${c.cyan(objective.name)} into: ${c.bold(tools.join(', '))}`);
81
+ if (note) console.log(c.dim(note));
82
+ if (flags.dryRun) console.log(c.dim('(dry run — no files written)'));
83
+
84
+ install(objective, tools, { dryRun: flags.dryRun });
85
+
86
+ console.log(`\n${flags.dryRun ? c.dim('Dry run complete.') : c.green('Done.')}`);
87
+ }
88
+
89
+ function help() {
90
+ console.log(`
91
+ ${c.bold(pkg.name)} ${c.dim('v' + pkg.version)}
92
+ ${pkg.description}
93
+
94
+ ${c.bold('Usage')}
95
+ npx ${pkg.name} <command> [options]
96
+
97
+ ${c.bold('Commands')}
98
+ list List available QA objectives
99
+ add <objective> Install an objective's skills/agents/mcp
100
+ detect Show which AI tools are detected here
101
+ help Show this help
102
+
103
+ ${c.bold('Options')}
104
+ -t, --tool <list> Comma-separated: ${TOOLS.join(', ')}, all
105
+ (default: auto-detected, else claude,cursor)
106
+ -n, --dry-run Show what would be written without writing
107
+
108
+ ${c.bold('Examples')}
109
+ npx ${pkg.name} list
110
+ npx ${pkg.name} add playwright-e2e
111
+ npx ${pkg.name} add playwright-e2e --tool cursor,claude
112
+ npx ${pkg.name} add playwright-e2e --tool all --dry-run
113
+ `);
114
+ }
115
+
116
+ export async function main(argv) {
117
+ const { flags, positional } = parseFlags(argv);
118
+ const cmd = positional.shift();
119
+
120
+ if (flags.version || cmd === '--version') return void console.log(pkg.version);
121
+ switch (cmd) {
122
+ case 'list': case 'ls': return cmdList();
123
+ case 'add': case 'install': return cmdAdd(positional, flags);
124
+ case 'detect': return cmdDetect();
125
+ case undefined: case 'help': case '--help': case '-h': return help();
126
+ default:
127
+ throw new Error(`unknown command "${cmd}". Run: npx ${pkg.name} help`);
128
+ }
129
+ }
package/src/install.js ADDED
@@ -0,0 +1,214 @@
1
+ // Installs an objective's skills/agents/mcp into a target AI tool by writing
2
+ // the files each tool expects. All writes are relative to process.cwd() (the
3
+ // user's project), except Windsurf's MCP config which is global.
4
+
5
+ import {
6
+ existsSync, readFileSync, writeFileSync, mkdirSync, cpSync,
7
+ } from 'node:fs';
8
+ import { join, dirname, basename } from 'node:path';
9
+ import { homedir } from 'node:os';
10
+ import { KINDS } from './registry.js';
11
+
12
+ const c = {
13
+ dim: (s) => `\x1b[2m${s}\x1b[0m`,
14
+ green: (s) => `\x1b[32m${s}\x1b[0m`,
15
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
16
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
17
+ };
18
+
19
+ // ---- small helpers -------------------------------------------------------
20
+
21
+ function parseFrontmatter(md) {
22
+ const m = md.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
23
+ if (!m) return { data: {}, body: md };
24
+ const data = {};
25
+ for (const line of m[1].split(/\r?\n/)) {
26
+ const kv = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
27
+ if (kv) data[kv[1]] = kv[2].trim().replace(/^["']|["']$/g, '');
28
+ }
29
+ return { data, body: m[2] };
30
+ }
31
+
32
+ function toMdc(data, body) {
33
+ const desc = (data.description || '').replace(/\n/g, ' ');
34
+ return `---\ndescription: ${desc}\nglobs:\nalwaysApply: false\n---\n\n${body.trim()}\n`;
35
+ }
36
+
37
+ // ---- write primitives ----------------------------------------------------
38
+
39
+ function ensureWrite(ctx, absPath, content) {
40
+ const rel = relForLog(absPath);
41
+ if (ctx.dryRun) {
42
+ ctx.log(` ${c.yellow('would write')} ${rel}`);
43
+ return;
44
+ }
45
+ mkdirSync(dirname(absPath), { recursive: true });
46
+ writeFileSync(absPath, content);
47
+ ctx.log(` ${c.green('wrote')} ${rel}`);
48
+ }
49
+
50
+ function copyDir(ctx, src, dest) {
51
+ const rel = relForLog(dest);
52
+ if (ctx.dryRun) {
53
+ ctx.log(` ${c.yellow('would copy')} ${rel}${'/'}`);
54
+ return;
55
+ }
56
+ mkdirSync(dirname(dest), { recursive: true });
57
+ cpSync(src, dest, { recursive: true });
58
+ ctx.log(` ${c.green('copied')} ${rel}/`);
59
+ }
60
+
61
+ function mergeMcp(ctx, targetPath, serverName, serverDef) {
62
+ let config = { mcpServers: {} };
63
+ if (existsSync(targetPath)) {
64
+ try {
65
+ config = JSON.parse(readFileSync(targetPath, 'utf8'));
66
+ } catch {
67
+ throw new Error(`${targetPath} exists but is not valid JSON; fix or remove it first`);
68
+ }
69
+ }
70
+ config.mcpServers = config.mcpServers || {};
71
+ const existed = Boolean(config.mcpServers[serverName]);
72
+ config.mcpServers[serverName] = serverDef;
73
+
74
+ const rel = relForLog(targetPath);
75
+ const verb = existed ? 'updated mcp' : 'added mcp';
76
+ if (ctx.dryRun) {
77
+ ctx.log(` ${c.yellow('would ' + verb)} "${serverName}" -> ${rel}`);
78
+ return;
79
+ }
80
+ mkdirSync(dirname(targetPath), { recursive: true });
81
+ writeFileSync(targetPath, JSON.stringify(config, null, 2) + '\n');
82
+ ctx.log(` ${c.green(verb)} "${serverName}" -> ${rel}`);
83
+ }
84
+
85
+ function relForLog(abs) {
86
+ const cwd = process.cwd();
87
+ return abs.startsWith(cwd) ? abs.slice(cwd.length + 1) : abs;
88
+ }
89
+
90
+ // ---- tool adapters -------------------------------------------------------
91
+ // Each adapter maps the three kinds into the files a given tool consumes.
92
+
93
+ function readSkill(objective, skillName) {
94
+ const skillMd = join(objective.dir, 'skills', skillName, 'SKILL.md');
95
+ const raw = existsSync(skillMd) ? readFileSync(skillMd, 'utf8') : '';
96
+ const { data, body } = parseFrontmatter(raw);
97
+ return { skillMd, raw, data, body };
98
+ }
99
+
100
+ const adapters = {
101
+ claude: {
102
+ label: 'Claude Code',
103
+ skill(ctx, objective, skillName) {
104
+ copyDir(ctx, join(objective.dir, 'skills', skillName), join(ctx.cwd, '.claude', 'skills', skillName));
105
+ },
106
+ agent(ctx, objective, agentFile) {
107
+ const src = join(objective.dir, 'agents', agentFile);
108
+ ensureWrite(ctx, join(ctx.cwd, '.claude', 'agents', agentFile), readFileSync(src, 'utf8'));
109
+ },
110
+ mcp(ctx, objective, mcpFile) {
111
+ const name = basename(mcpFile, '.json');
112
+ const def = JSON.parse(readFileSync(join(objective.dir, 'mcp', mcpFile), 'utf8'));
113
+ mergeMcp(ctx, join(ctx.cwd, '.mcp.json'), name, def);
114
+ },
115
+ },
116
+
117
+ cursor: {
118
+ label: 'Cursor',
119
+ skill(ctx, objective, skillName) {
120
+ const { data, body } = readSkill(objective, skillName);
121
+ ensureWrite(ctx, join(ctx.cwd, '.cursor', 'rules', `${skillName}.mdc`), toMdc(data, body));
122
+ },
123
+ agent(ctx, objective, agentFile) {
124
+ const name = basename(agentFile, '.md');
125
+ const { data, body } = parseFrontmatter(readFileSync(join(objective.dir, 'agents', agentFile), 'utf8'));
126
+ ensureWrite(ctx, join(ctx.cwd, '.cursor', 'rules', `${name}.mdc`), toMdc(data, body));
127
+ },
128
+ mcp(ctx, objective, mcpFile) {
129
+ const name = basename(mcpFile, '.json');
130
+ const def = JSON.parse(readFileSync(join(objective.dir, 'mcp', mcpFile), 'utf8'));
131
+ mergeMcp(ctx, join(ctx.cwd, '.cursor', 'mcp.json'), name, def);
132
+ },
133
+ },
134
+
135
+ windsurf: {
136
+ label: 'Windsurf',
137
+ skill(ctx, objective, skillName) {
138
+ const { data, body } = readSkill(objective, skillName);
139
+ const md = `# ${data.name || skillName}\n\n${data.description || ''}\n\n${body.trim()}\n`;
140
+ ensureWrite(ctx, join(ctx.cwd, '.windsurf', 'rules', `${skillName}.md`), md);
141
+ },
142
+ agent(ctx, objective, agentFile) {
143
+ const name = basename(agentFile, '.md');
144
+ const { data, body } = parseFrontmatter(readFileSync(join(objective.dir, 'agents', agentFile), 'utf8'));
145
+ const md = `# ${data.name || name}\n\n${data.description || ''}\n\n${body.trim()}\n`;
146
+ ensureWrite(ctx, join(ctx.cwd, '.windsurf', 'rules', `${name}.md`), md);
147
+ },
148
+ mcp(ctx, objective, mcpFile) {
149
+ const name = basename(mcpFile, '.json');
150
+ const def = JSON.parse(readFileSync(join(objective.dir, 'mcp', mcpFile), 'utf8'));
151
+ // Windsurf reads a single global MCP config file.
152
+ mergeMcp(ctx, join(homedir(), '.codeium', 'windsurf', 'mcp_config.json'), name, def);
153
+ },
154
+ },
155
+
156
+ agents: {
157
+ label: 'AGENTS.md (generic)',
158
+ skill(ctx, objective, skillName) {
159
+ const { data, body } = readSkill(objective, skillName);
160
+ appendAgentsMd(ctx, data.name || skillName, data.description, body);
161
+ },
162
+ agent(ctx, objective, agentFile) {
163
+ const name = basename(agentFile, '.md');
164
+ const { data, body } = parseFrontmatter(readFileSync(join(objective.dir, 'agents', agentFile), 'utf8'));
165
+ appendAgentsMd(ctx, data.name || name, data.description, body);
166
+ },
167
+ mcp(ctx, objective, mcpFile) {
168
+ const name = basename(mcpFile, '.json');
169
+ const def = readFileSync(join(objective.dir, 'mcp', mcpFile), 'utf8').trim();
170
+ ctx.log(` ${c.yellow('mcp (manual)')} add "${name}" to your tool's MCP config:`);
171
+ ctx.log(c.dim(def.split('\n').map((l) => ' ' + l).join('\n')));
172
+ },
173
+ },
174
+ };
175
+
176
+ function appendAgentsMd(ctx, name, description, body) {
177
+ const target = join(ctx.cwd, 'AGENTS.md');
178
+ const heading = `## ${name}`;
179
+ const section = `${heading}\n\n${description ? description + '\n\n' : ''}${body.trim()}\n`;
180
+ let existing = existsSync(target) ? readFileSync(target, 'utf8') : '# AGENTS.md\n\n';
181
+ if (existing.includes(heading)) {
182
+ ctx.log(` ${c.dim('skip')} AGENTS.md already has "${name}"`);
183
+ return;
184
+ }
185
+ ensureWrite(ctx, target, existing.replace(/\s*$/, '\n\n') + section);
186
+ }
187
+
188
+ export const TOOLS = Object.keys(adapters);
189
+
190
+ // ---- tool detection ------------------------------------------------------
191
+
192
+ export function detectTools(cwd = process.cwd()) {
193
+ const found = [];
194
+ if (existsSync(join(cwd, '.claude')) || existsSync(join(cwd, '.mcp.json')) || existsSync(join(cwd, 'CLAUDE.md'))) found.push('claude');
195
+ if (existsSync(join(cwd, '.cursor')) || existsSync(join(cwd, '.cursorrules'))) found.push('cursor');
196
+ if (existsSync(join(cwd, '.windsurf'))) found.push('windsurf');
197
+ return found;
198
+ }
199
+
200
+ // ---- orchestration -------------------------------------------------------
201
+
202
+ export function install(objective, tools, { dryRun = false, cwd = process.cwd(), log = console.log } = {}) {
203
+ const ctx = { dryRun, cwd, log };
204
+ for (const tool of tools) {
205
+ const adapter = adapters[tool];
206
+ if (!adapter) throw new Error(`unknown tool "${tool}" (known: ${TOOLS.join(', ')})`);
207
+ log(`\n${c.cyan('▸ ' + adapter.label)}`);
208
+ for (const kind of KINDS) {
209
+ for (const item of objective.contents[kind]) {
210
+ adapter[kind === 'skills' ? 'skill' : kind === 'agents' ? 'agent' : 'mcp'](ctx, objective, item);
211
+ }
212
+ }
213
+ }
214
+ }
@@ -0,0 +1,68 @@
1
+ // Discovers "objectives" — top-level folders in this repo that contain any of
2
+ // skills/ agents/ mcp/. The package ships its objective folders at the repo
3
+ // root (siblings of bin/ and src/), so we scan REPO_ROOT and skip reserved and
4
+ // underscore-prefixed names (like _template).
5
+
6
+ import { readdirSync, existsSync, readFileSync } from 'node:fs';
7
+ import { join, dirname } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ export const REPO_ROOT = join(__dirname, '..');
12
+
13
+ const RESERVED = new Set([
14
+ 'bin', 'src', 'node_modules', 'scripts', 'test', 'tests', 'coverage',
15
+ ]);
16
+
17
+ export const KINDS = ['skills', 'agents', 'mcp'];
18
+
19
+ function isReserved(name) {
20
+ return RESERVED.has(name) || name.startsWith('.') || name.startsWith('_');
21
+ }
22
+
23
+ export function loadObjective(name) {
24
+ const dir = join(REPO_ROOT, name);
25
+ if (!existsSync(dir)) return null;
26
+
27
+ const contents = {};
28
+ let hasAny = false;
29
+ for (const kind of KINDS) {
30
+ const kdir = join(dir, kind);
31
+ if (existsSync(kdir)) {
32
+ contents[kind] = readdirSync(kdir, { withFileTypes: true })
33
+ .filter((e) => !e.name.startsWith('.'))
34
+ .map((e) => e.name);
35
+ if (contents[kind].length) hasAny = true;
36
+ } else {
37
+ contents[kind] = [];
38
+ }
39
+ }
40
+
41
+ const metaPath = join(dir, 'objective.json');
42
+ let meta = {};
43
+ if (existsSync(metaPath)) {
44
+ try {
45
+ meta = JSON.parse(readFileSync(metaPath, 'utf8'));
46
+ } catch {
47
+ /* ignore malformed metadata */
48
+ }
49
+ }
50
+
51
+ if (!hasAny && !existsSync(metaPath)) return null;
52
+
53
+ return {
54
+ name,
55
+ dir,
56
+ title: meta.title || name,
57
+ description: meta.description || '',
58
+ contents,
59
+ };
60
+ }
61
+
62
+ export function listObjectives() {
63
+ return readdirSync(REPO_ROOT, { withFileTypes: true })
64
+ .filter((d) => d.isDirectory() && !isReserved(d.name))
65
+ .map((d) => loadObjective(d.name))
66
+ .filter(Boolean)
67
+ .sort((a, b) => a.name.localeCompare(b.name));
68
+ }