opencastle 0.10.0 → 0.10.2
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/README.md +11 -77
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +13 -7
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +7 -2
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/init.test.d.ts +17 -0
- package/dist/cli/init.test.d.ts.map +1 -0
- package/dist/cli/init.test.js +881 -0
- package/dist/cli/init.test.js.map +1 -0
- package/dist/cli/mcp.d.ts +9 -0
- package/dist/cli/mcp.d.ts.map +1 -1
- package/dist/cli/mcp.js +56 -0
- package/dist/cli/mcp.js.map +1 -1
- package/dist/cli/stack-config-update.test.d.ts +2 -0
- package/dist/cli/stack-config-update.test.d.ts.map +1 -0
- package/dist/cli/stack-config-update.test.js +185 -0
- package/dist/cli/stack-config-update.test.js.map +1 -0
- package/dist/cli/stack-config.d.ts +27 -0
- package/dist/cli/stack-config.d.ts.map +1 -1
- package/dist/cli/stack-config.js +80 -27
- package/dist/cli/stack-config.js.map +1 -1
- package/dist/cli/types.d.ts +1 -1
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +184 -17
- package/dist/cli/update.js.map +1 -1
- package/dist/orchestrator/plugins/astro/config.d.ts +3 -0
- package/dist/orchestrator/plugins/astro/config.d.ts.map +1 -0
- package/dist/orchestrator/plugins/astro/config.js +27 -0
- package/dist/orchestrator/plugins/astro/config.js.map +1 -0
- package/dist/orchestrator/plugins/chrome-devtools/config.js +2 -2
- package/dist/orchestrator/plugins/chrome-devtools/config.js.map +1 -1
- package/dist/orchestrator/plugins/contentful/config.js +1 -1
- package/dist/orchestrator/plugins/contentful/config.js.map +1 -1
- package/dist/orchestrator/plugins/convex/config.js +1 -1
- package/dist/orchestrator/plugins/convex/config.js.map +1 -1
- package/dist/orchestrator/plugins/cypress/config.d.ts +3 -0
- package/dist/orchestrator/plugins/cypress/config.d.ts.map +1 -0
- package/dist/orchestrator/plugins/cypress/config.js +15 -0
- package/dist/orchestrator/plugins/cypress/config.js.map +1 -0
- package/dist/orchestrator/plugins/figma/config.d.ts +3 -0
- package/dist/orchestrator/plugins/figma/config.d.ts.map +1 -0
- package/dist/orchestrator/plugins/figma/config.js +31 -0
- package/dist/orchestrator/plugins/figma/config.js.map +1 -0
- package/dist/orchestrator/plugins/index.d.ts.map +1 -1
- package/dist/orchestrator/plugins/index.js +20 -0
- package/dist/orchestrator/plugins/index.js.map +1 -1
- package/dist/orchestrator/plugins/jira/config.d.ts.map +1 -1
- package/dist/orchestrator/plugins/jira/config.js +2 -3
- package/dist/orchestrator/plugins/jira/config.js.map +1 -1
- package/dist/orchestrator/plugins/linear/config.js +2 -2
- package/dist/orchestrator/plugins/linear/config.js.map +1 -1
- package/dist/orchestrator/plugins/netlify/config.d.ts +3 -0
- package/dist/orchestrator/plugins/netlify/config.d.ts.map +1 -0
- package/dist/orchestrator/plugins/netlify/config.js +30 -0
- package/dist/orchestrator/plugins/netlify/config.js.map +1 -0
- package/dist/orchestrator/plugins/nextjs/config.d.ts +3 -0
- package/dist/orchestrator/plugins/nextjs/config.d.ts.map +1 -0
- package/dist/orchestrator/plugins/nextjs/config.js +35 -0
- package/dist/orchestrator/plugins/nextjs/config.js.map +1 -0
- package/dist/orchestrator/plugins/nx/config.d.ts.map +1 -1
- package/dist/orchestrator/plugins/nx/config.js +2 -3
- package/dist/orchestrator/plugins/nx/config.js.map +1 -1
- package/dist/orchestrator/plugins/playwright/config.d.ts +3 -0
- package/dist/orchestrator/plugins/playwright/config.d.ts.map +1 -0
- package/dist/orchestrator/plugins/playwright/config.js +25 -0
- package/dist/orchestrator/plugins/playwright/config.js.map +1 -0
- package/dist/orchestrator/plugins/prisma/config.d.ts +3 -0
- package/dist/orchestrator/plugins/prisma/config.d.ts.map +1 -0
- package/dist/orchestrator/plugins/prisma/config.js +25 -0
- package/dist/orchestrator/plugins/prisma/config.js.map +1 -0
- package/dist/orchestrator/plugins/resend/config.d.ts +3 -0
- package/dist/orchestrator/plugins/resend/config.d.ts.map +1 -0
- package/dist/orchestrator/plugins/resend/config.js +46 -0
- package/dist/orchestrator/plugins/resend/config.js.map +1 -0
- package/dist/orchestrator/plugins/sanity/config.d.ts.map +1 -1
- package/dist/orchestrator/plugins/sanity/config.js +1 -2
- package/dist/orchestrator/plugins/sanity/config.js.map +1 -1
- package/dist/orchestrator/plugins/slack/config.js +1 -1
- package/dist/orchestrator/plugins/slack/config.js.map +1 -1
- package/dist/orchestrator/plugins/strapi/config.js +1 -1
- package/dist/orchestrator/plugins/strapi/config.js.map +1 -1
- package/dist/orchestrator/plugins/supabase/config.d.ts.map +1 -1
- package/dist/orchestrator/plugins/supabase/config.js +1 -2
- package/dist/orchestrator/plugins/supabase/config.js.map +1 -1
- package/dist/orchestrator/plugins/teams/config.d.ts.map +1 -1
- package/dist/orchestrator/plugins/teams/config.js +1 -2
- package/dist/orchestrator/plugins/teams/config.js.map +1 -1
- package/dist/orchestrator/plugins/turborepo/config.d.ts +3 -0
- package/dist/orchestrator/plugins/turborepo/config.d.ts.map +1 -0
- package/dist/orchestrator/plugins/turborepo/config.js +15 -0
- package/dist/orchestrator/plugins/turborepo/config.js.map +1 -0
- package/dist/orchestrator/plugins/types.d.ts +7 -7
- package/dist/orchestrator/plugins/types.d.ts.map +1 -1
- package/dist/orchestrator/plugins/vercel/config.d.ts.map +1 -1
- package/dist/orchestrator/plugins/vercel/config.js +2 -3
- package/dist/orchestrator/plugins/vercel/config.js.map +1 -1
- package/dist/orchestrator/plugins/vitest/config.d.ts +3 -0
- package/dist/orchestrator/plugins/vitest/config.d.ts.map +1 -0
- package/dist/orchestrator/plugins/vitest/config.js +15 -0
- package/dist/orchestrator/plugins/vitest/config.js.map +1 -0
- package/package.json +1 -1
- package/src/cli/doctor.ts +14 -7
- package/src/cli/init.test.ts +1141 -0
- package/src/cli/init.ts +8 -2
- package/src/cli/mcp.ts +77 -1
- package/src/cli/stack-config-update.test.ts +210 -0
- package/src/cli/stack-config.ts +110 -37
- package/src/cli/types.ts +1 -1
- package/src/cli/update.ts +230 -23
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/orchestrator/agents/api-designer.agent.md +1 -11
- package/src/orchestrator/agents/architect.agent.md +1 -9
- package/src/orchestrator/agents/content-engineer.agent.md +1 -5
- package/src/orchestrator/agents/copywriter.agent.md +1 -9
- package/src/orchestrator/agents/data-expert.agent.md +2 -6
- package/src/orchestrator/agents/database-engineer.agent.md +1 -6
- package/src/orchestrator/agents/developer.agent.md +2 -12
- package/src/orchestrator/agents/devops-expert.agent.md +1 -5
- package/src/orchestrator/agents/documentation-writer.agent.md +1 -4
- package/src/orchestrator/agents/performance-expert.agent.md +1 -5
- package/src/orchestrator/agents/release-manager.agent.md +1 -11
- package/src/orchestrator/agents/researcher.agent.md +1 -4
- package/src/orchestrator/agents/security-expert.agent.md +2 -7
- package/src/orchestrator/agents/seo-specialist.agent.md +1 -10
- package/src/orchestrator/agents/testing-expert.agent.md +2 -11
- package/src/orchestrator/agents/ui-ux-expert.agent.md +3 -10
- package/src/orchestrator/customizations/README.md +2 -1
- package/src/orchestrator/customizations/agents/skill-matrix.json +106 -0
- package/src/orchestrator/customizations/agents/skill-matrix.md +58 -121
- package/src/orchestrator/instructions/general.instructions.md +1 -1
- package/src/orchestrator/plugins/astro/SKILL.md +288 -0
- package/src/orchestrator/plugins/astro/config.ts +28 -0
- package/src/orchestrator/plugins/chrome-devtools/config.ts +2 -2
- package/src/orchestrator/plugins/contentful/config.ts +1 -1
- package/src/orchestrator/plugins/convex/config.ts +1 -1
- package/src/orchestrator/plugins/cypress/SKILL.md +145 -0
- package/src/orchestrator/plugins/cypress/config.ts +16 -0
- package/src/orchestrator/plugins/figma/SKILL.md +85 -0
- package/src/orchestrator/plugins/figma/config.ts +32 -0
- package/src/orchestrator/plugins/index.ts +20 -0
- package/src/orchestrator/plugins/jira/config.ts +2 -3
- package/src/orchestrator/plugins/linear/config.ts +2 -2
- package/src/orchestrator/plugins/netlify/SKILL.md +134 -0
- package/src/orchestrator/plugins/netlify/config.ts +31 -0
- package/src/orchestrator/plugins/nextjs/SKILL.md +376 -0
- package/src/orchestrator/plugins/nextjs/config.ts +36 -0
- package/src/orchestrator/plugins/nx/config.ts +2 -3
- package/src/orchestrator/plugins/playwright/SKILL.md +191 -0
- package/src/orchestrator/plugins/playwright/config.ts +26 -0
- package/src/orchestrator/plugins/prisma/SKILL.md +137 -0
- package/src/orchestrator/plugins/prisma/config.ts +26 -0
- package/src/orchestrator/plugins/resend/SKILL.md +187 -0
- package/src/orchestrator/plugins/resend/config.ts +47 -0
- package/src/orchestrator/plugins/sanity/config.ts +1 -2
- package/src/orchestrator/plugins/slack/config.ts +1 -1
- package/src/orchestrator/plugins/strapi/config.ts +1 -1
- package/src/orchestrator/plugins/supabase/config.ts +1 -2
- package/src/orchestrator/plugins/teams/config.ts +1 -2
- package/src/orchestrator/plugins/turborepo/SKILL.md +121 -0
- package/src/orchestrator/plugins/turborepo/config.ts +16 -0
- package/src/orchestrator/plugins/types.ts +7 -7
- package/src/orchestrator/plugins/vercel/SKILL.md +99 -0
- package/src/orchestrator/plugins/vercel/config.ts +2 -3
- package/src/orchestrator/plugins/vitest/SKILL.md +166 -0
- package/src/orchestrator/plugins/vitest/config.ts +16 -0
- package/src/orchestrator/prompts/bootstrap-customizations.prompt.md +6 -4
- package/src/orchestrator/prompts/create-skill.prompt.md +6 -7
- package/src/orchestrator/skills/agent-hooks/SKILL.md +2 -2
- package/src/orchestrator/skills/memory-merger/SKILL.md +1 -1
- package/src/orchestrator/skills/nextjs-patterns/SKILL.md +0 -200
package/src/cli/init.ts
CHANGED
|
@@ -88,6 +88,7 @@ export default async function init({ pkgRoot, args }: CliContext): Promise<void>
|
|
|
88
88
|
...(repoInfo.databases ?? []),
|
|
89
89
|
...(repoInfo.deployment ?? []),
|
|
90
90
|
...(repoInfo.monorepo ? [repoInfo.monorepo] : []),
|
|
91
|
+
...((repoInfo.frameworks ?? []).map(f => f === 'next' ? 'nextjs' : f)),
|
|
91
92
|
])
|
|
92
93
|
|
|
93
94
|
console.log(` ${c.bold('── Tech Tools ────────────────────────────────')}`)
|
|
@@ -199,6 +200,12 @@ export default async function init({ pkgRoot, args }: CliContext): Promise<void>
|
|
|
199
200
|
manifest.repoInfo = combinedRepoInfo
|
|
200
201
|
await writeManifest(projectRoot, manifest)
|
|
201
202
|
|
|
203
|
+
// ── Ensure .env is gitignored when MCP env vars are needed ────
|
|
204
|
+
const envVars = getRequiredMcpEnvVars(stack, combinedRepoInfo)
|
|
205
|
+
if (envVars.length > 0 && !allManagedPaths.framework.includes('.env')) {
|
|
206
|
+
allManagedPaths.framework.push('.env')
|
|
207
|
+
}
|
|
208
|
+
|
|
202
209
|
// ── Update .gitignore ───────────────────────────────────────────
|
|
203
210
|
const gitignoreResult = await updateGitignore(projectRoot, allManagedPaths)
|
|
204
211
|
|
|
@@ -214,7 +221,6 @@ export default async function init({ pkgRoot, args }: CliContext): Promise<void>
|
|
|
214
221
|
}
|
|
215
222
|
|
|
216
223
|
// ── Env var notice + .env file generation ────────────────────
|
|
217
|
-
const envVars = getRequiredMcpEnvVars(stack, combinedRepoInfo)
|
|
218
224
|
if (envVars.length > 0) {
|
|
219
225
|
console.log(`\n ${c.yellow('⚠')} Required environment variables for MCP servers:\n`)
|
|
220
226
|
for (const { envVar, hint } of envVars) {
|
|
@@ -248,7 +254,7 @@ export default async function init({ pkgRoot, args }: CliContext): Promise<void>
|
|
|
248
254
|
// ── OAuth setup guides ────────────────────────────────────────
|
|
249
255
|
if (teamTools.includes('slack')) {
|
|
250
256
|
console.log(` ${c.cyan('📖')} Slack MCP requires a Slack App with a bot token.`)
|
|
251
|
-
console.log(` Setup guide: ${c.cyan('https://www.opencastle.dev/
|
|
257
|
+
console.log(` Setup guide: ${c.cyan('https://www.opencastle.dev/docs/plugins#slack')}\n`)
|
|
252
258
|
}
|
|
253
259
|
|
|
254
260
|
console.log(`\n ${c.bold('Next steps:')}`)
|
package/src/cli/mcp.ts
CHANGED
|
@@ -111,7 +111,7 @@ export async function scaffoldMcpConfig(
|
|
|
111
111
|
|
|
112
112
|
for (const plugin of Object.values(PLUGINS)) {
|
|
113
113
|
if (plugin.mcpServerKey && included.has(plugin.mcpServerKey)) {
|
|
114
|
-
servers[plugin.mcpServerKey] = plugin.mcpConfig as VsCodeServer;
|
|
114
|
+
servers[plugin.mcpServerKey] = plugin.mcpConfig! as VsCodeServer;
|
|
115
115
|
if (plugin.mcpInputs) {
|
|
116
116
|
inputs.push(...plugin.mcpInputs);
|
|
117
117
|
}
|
|
@@ -181,3 +181,79 @@ export async function scaffoldMcpConfig(
|
|
|
181
181
|
|
|
182
182
|
return { path: destPath, action: 'created' };
|
|
183
183
|
}
|
|
184
|
+
|
|
185
|
+
// ── MCP config rebuild for reconfigure ────────────────────────
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Returns the relative path to the MCP config file for a given IDE.
|
|
189
|
+
*/
|
|
190
|
+
function getMcpConfigRelPath(ide: IdeChoice): string {
|
|
191
|
+
switch (ide) {
|
|
192
|
+
case 'vscode':
|
|
193
|
+
return '.vscode/mcp.json';
|
|
194
|
+
case 'cursor':
|
|
195
|
+
return '.cursor/mcp.json';
|
|
196
|
+
case 'claude-code':
|
|
197
|
+
return '.claude/mcp.json';
|
|
198
|
+
case 'opencode':
|
|
199
|
+
return 'opencode.json';
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Rebuild the MCP config for a specific IDE after a stack reconfigure.
|
|
205
|
+
*
|
|
206
|
+
* 1. Reads the existing MCP config
|
|
207
|
+
* 2. Removes all plugin-managed server entries
|
|
208
|
+
* 3. Preserves manually-added server entries
|
|
209
|
+
* 4. Re-scaffolds with the new stack selection
|
|
210
|
+
*/
|
|
211
|
+
export async function rebuildMcpConfig(
|
|
212
|
+
projectRoot: string,
|
|
213
|
+
ide: IdeChoice,
|
|
214
|
+
stack: StackConfig,
|
|
215
|
+
repoInfo?: RepoInfo
|
|
216
|
+
): Promise<void> {
|
|
217
|
+
const destRelPath = getMcpConfigRelPath(ide);
|
|
218
|
+
const destPath = resolve(projectRoot, destRelPath);
|
|
219
|
+
|
|
220
|
+
if (!existsSync(destPath)) {
|
|
221
|
+
// No existing config — scaffold fresh
|
|
222
|
+
await scaffoldMcpConfig(projectRoot, destRelPath, stack, repoInfo, ide);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Read existing config and strip all plugin-managed servers
|
|
227
|
+
const existing = JSON.parse(await readFile(destPath, 'utf8')) as Record<string, unknown>;
|
|
228
|
+
const containerKey =
|
|
229
|
+
ide === 'opencode' ? 'mcp' : ide === 'vscode' ? 'servers' : 'mcpServers';
|
|
230
|
+
|
|
231
|
+
const existingServers = (existing[containerKey] ?? {}) as Record<string, unknown>;
|
|
232
|
+
|
|
233
|
+
// Get all known plugin server keys
|
|
234
|
+
const allPluginServerKeys = new Set(
|
|
235
|
+
Object.values(PLUGINS)
|
|
236
|
+
.filter((p) => p.mcpServerKey)
|
|
237
|
+
.map((p) => p.mcpServerKey!)
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// Remove all plugin-managed servers (they'll be re-added by scaffoldMcpConfig)
|
|
241
|
+
for (const key of Object.keys(existingServers)) {
|
|
242
|
+
if (allPluginServerKeys.has(key)) {
|
|
243
|
+
delete existingServers[key];
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Remove plugin-managed inputs (VS Code only)
|
|
248
|
+
if (ide === 'vscode') {
|
|
249
|
+
delete existing.inputs;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
existing[containerKey] = existingServers;
|
|
253
|
+
|
|
254
|
+
// Write the cleaned config (preserving manually-added servers)
|
|
255
|
+
await writeFile(destPath, JSON.stringify(existing, null, 2) + '\n');
|
|
256
|
+
|
|
257
|
+
// Re-scaffold: merges new plugin servers into the cleaned config
|
|
258
|
+
await scaffoldMcpConfig(projectRoot, destRelPath, stack, repoInfo, ide);
|
|
259
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { updateSkillMatrixContent } from './stack-config.js';
|
|
3
|
+
import type { StackConfig } from './types.js';
|
|
4
|
+
import type { SkillMatrixData } from './stack-config.js';
|
|
5
|
+
|
|
6
|
+
function makeTemplate(): SkillMatrixData {
|
|
7
|
+
return {
|
|
8
|
+
bindings: {
|
|
9
|
+
framework: {
|
|
10
|
+
entries: [],
|
|
11
|
+
description: 'SSR/SSG, routing, layouts, Server/Client Components',
|
|
12
|
+
},
|
|
13
|
+
database: { entries: [], description: 'Schema, migrations, auth flow, roles' },
|
|
14
|
+
cms: { entries: [], description: 'Document types, queries, schema management' },
|
|
15
|
+
deployment: {
|
|
16
|
+
entries: [],
|
|
17
|
+
description: 'Hosting, cron jobs, env vars, caching, headers',
|
|
18
|
+
},
|
|
19
|
+
'codebase-tool': {
|
|
20
|
+
entries: [],
|
|
21
|
+
description: 'Task running, building, linting, testing, code generation',
|
|
22
|
+
},
|
|
23
|
+
testing: {
|
|
24
|
+
entries: [],
|
|
25
|
+
description: 'Unit testing frameworks, coverage, test planning',
|
|
26
|
+
},
|
|
27
|
+
'e2e-testing': {
|
|
28
|
+
entries: [],
|
|
29
|
+
description: 'Browser automation, E2E testing, viewport testing, visual validation',
|
|
30
|
+
},
|
|
31
|
+
'task-management': {
|
|
32
|
+
entries: [],
|
|
33
|
+
description: 'Issue tracking, naming, priorities, workflow states',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
agents: {},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function templateJson(): string {
|
|
41
|
+
return JSON.stringify(makeTemplate(), null, 2) + '\n';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parse(result: string): SkillMatrixData {
|
|
45
|
+
return JSON.parse(result);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('updateSkillMatrixContent', () => {
|
|
49
|
+
it('fills database slot when a database tool is selected', () => {
|
|
50
|
+
const stack: StackConfig = { ides: ['vscode'], techTools: ['supabase'], teamTools: [] };
|
|
51
|
+
const data = parse(updateSkillMatrixContent(templateJson(), stack));
|
|
52
|
+
expect(data.bindings.database.entries).toEqual([
|
|
53
|
+
{ name: 'Supabase', skill: 'supabase-database' },
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('fills cms slot when a CMS tool is selected', () => {
|
|
58
|
+
const stack: StackConfig = { ides: ['vscode'], techTools: ['sanity'], teamTools: [] };
|
|
59
|
+
const data = parse(updateSkillMatrixContent(templateJson(), stack));
|
|
60
|
+
expect(data.bindings.cms.entries).toEqual([{ name: 'Sanity', skill: 'sanity-cms' }]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('fills framework slot when a framework tool is selected', () => {
|
|
64
|
+
const stack: StackConfig = { ides: ['vscode'], techTools: ['nextjs'], teamTools: [] };
|
|
65
|
+
const data = parse(updateSkillMatrixContent(templateJson(), stack));
|
|
66
|
+
expect(data.bindings.framework.entries).toEqual([
|
|
67
|
+
{ name: 'Next.js', skill: 'nextjs-framework' },
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('fills deployment slot when a deployment tool is selected', () => {
|
|
72
|
+
const stack: StackConfig = { ides: ['vscode'], techTools: ['vercel'], teamTools: [] };
|
|
73
|
+
const data = parse(updateSkillMatrixContent(templateJson(), stack));
|
|
74
|
+
expect(data.bindings.deployment.entries).toEqual([
|
|
75
|
+
{ name: 'Vercel', skill: 'vercel-deployment' },
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('fills codebase-tool slot when a monorepo tool is selected', () => {
|
|
80
|
+
const stack: StackConfig = { ides: ['vscode'], techTools: ['nx'], teamTools: [] };
|
|
81
|
+
const data = parse(updateSkillMatrixContent(templateJson(), stack));
|
|
82
|
+
expect(data.bindings['codebase-tool'].entries).toEqual([
|
|
83
|
+
{ name: 'NX', skill: 'nx-workspace' },
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('fills task-management slot when a tracker tool is selected', () => {
|
|
88
|
+
const stack: StackConfig = { ides: ['vscode'], techTools: [], teamTools: ['linear'] };
|
|
89
|
+
const data = parse(updateSkillMatrixContent(templateJson(), stack));
|
|
90
|
+
expect(data.bindings['task-management'].entries).toEqual([
|
|
91
|
+
{ name: 'Linear', skill: 'linear-task-management' },
|
|
92
|
+
]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('clears database slot when no database tool is selected', () => {
|
|
96
|
+
const template = makeTemplate();
|
|
97
|
+
template.bindings.database.entries = [
|
|
98
|
+
{ name: 'Supabase', skill: 'supabase-database' },
|
|
99
|
+
];
|
|
100
|
+
const stack: StackConfig = { ides: ['vscode'], techTools: [], teamTools: [] };
|
|
101
|
+
const data = parse(
|
|
102
|
+
updateSkillMatrixContent(JSON.stringify(template, null, 2) + '\n', stack)
|
|
103
|
+
);
|
|
104
|
+
expect(data.bindings.database.entries).toEqual([]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('switches from one database to another', () => {
|
|
108
|
+
const template = makeTemplate();
|
|
109
|
+
template.bindings.database.entries = [
|
|
110
|
+
{ name: 'Supabase', skill: 'supabase-database' },
|
|
111
|
+
];
|
|
112
|
+
const stack: StackConfig = { ides: ['vscode'], techTools: ['convex'], teamTools: [] };
|
|
113
|
+
const data = parse(
|
|
114
|
+
updateSkillMatrixContent(JSON.stringify(template, null, 2) + '\n', stack)
|
|
115
|
+
);
|
|
116
|
+
expect(data.bindings.database.entries).toEqual([
|
|
117
|
+
{ name: 'Convex', skill: 'convex-database' },
|
|
118
|
+
]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('fills multiple slots at once', () => {
|
|
122
|
+
const stack: StackConfig = {
|
|
123
|
+
ides: ['vscode'],
|
|
124
|
+
techTools: ['supabase', 'sanity', 'vercel', 'nextjs'],
|
|
125
|
+
teamTools: ['linear'],
|
|
126
|
+
};
|
|
127
|
+
const data = parse(updateSkillMatrixContent(templateJson(), stack));
|
|
128
|
+
expect(data.bindings.database.entries).toEqual([
|
|
129
|
+
{ name: 'Supabase', skill: 'supabase-database' },
|
|
130
|
+
]);
|
|
131
|
+
expect(data.bindings.cms.entries).toEqual([{ name: 'Sanity', skill: 'sanity-cms' }]);
|
|
132
|
+
expect(data.bindings.deployment.entries).toEqual([
|
|
133
|
+
{ name: 'Vercel', skill: 'vercel-deployment' },
|
|
134
|
+
]);
|
|
135
|
+
expect(data.bindings.framework.entries).toEqual([
|
|
136
|
+
{ name: 'Next.js', skill: 'nextjs-framework' },
|
|
137
|
+
]);
|
|
138
|
+
expect(data.bindings['task-management'].entries).toEqual([
|
|
139
|
+
{ name: 'Linear', skill: 'linear-task-management' },
|
|
140
|
+
]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('does not modify unrelated slots', () => {
|
|
144
|
+
const stack: StackConfig = { ides: ['vscode'], techTools: ['supabase'], teamTools: [] };
|
|
145
|
+
const data = parse(updateSkillMatrixContent(templateJson(), stack));
|
|
146
|
+
expect(data.bindings.framework.entries).toEqual([]);
|
|
147
|
+
expect(data.bindings.cms.entries).toEqual([]);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('supports multiple plugins in the same slot', () => {
|
|
151
|
+
const stack: StackConfig = {
|
|
152
|
+
ides: ['vscode'],
|
|
153
|
+
techTools: ['supabase', 'convex'],
|
|
154
|
+
teamTools: [],
|
|
155
|
+
};
|
|
156
|
+
const data = parse(updateSkillMatrixContent(templateJson(), stack));
|
|
157
|
+
expect(data.bindings.database.entries).toEqual([
|
|
158
|
+
{ name: 'Supabase', skill: 'supabase-database' },
|
|
159
|
+
{ name: 'Convex', skill: 'convex-database' },
|
|
160
|
+
]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('supports multiple frameworks in the same slot', () => {
|
|
164
|
+
const stack: StackConfig = {
|
|
165
|
+
ides: ['vscode'],
|
|
166
|
+
techTools: ['nextjs', 'astro'],
|
|
167
|
+
teamTools: [],
|
|
168
|
+
};
|
|
169
|
+
const data = parse(updateSkillMatrixContent(templateJson(), stack));
|
|
170
|
+
expect(data.bindings.framework.entries).toEqual([
|
|
171
|
+
{ name: 'Next.js', skill: 'nextjs-framework' },
|
|
172
|
+
{ name: 'Astro', skill: 'astro-framework' },
|
|
173
|
+
]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('supports multiple CMS tools in the same slot', () => {
|
|
177
|
+
const stack: StackConfig = {
|
|
178
|
+
ides: ['vscode'],
|
|
179
|
+
techTools: ['sanity', 'contentful'],
|
|
180
|
+
teamTools: [],
|
|
181
|
+
};
|
|
182
|
+
const data = parse(updateSkillMatrixContent(templateJson(), stack));
|
|
183
|
+
expect(data.bindings.cms.entries).toEqual([
|
|
184
|
+
{ name: 'Sanity', skill: 'sanity-cms' },
|
|
185
|
+
{ name: 'Contentful', skill: 'contentful-cms' },
|
|
186
|
+
]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('preserves agents section', () => {
|
|
190
|
+
const template = makeTemplate();
|
|
191
|
+
template.agents = {
|
|
192
|
+
Developer: { slots: ['framework'], directSkills: ['validation-gates'] },
|
|
193
|
+
};
|
|
194
|
+
const stack: StackConfig = { ides: ['vscode'], techTools: ['supabase'], teamTools: [] };
|
|
195
|
+
const data = parse(
|
|
196
|
+
updateSkillMatrixContent(JSON.stringify(template, null, 2) + '\n', stack)
|
|
197
|
+
);
|
|
198
|
+
expect(data.agents.Developer).toEqual({
|
|
199
|
+
slots: ['framework'],
|
|
200
|
+
directSkills: ['validation-gates'],
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('outputs valid JSON with trailing newline', () => {
|
|
205
|
+
const stack: StackConfig = { ides: ['vscode'], techTools: ['supabase'], teamTools: [] };
|
|
206
|
+
const result = updateSkillMatrixContent(templateJson(), stack);
|
|
207
|
+
expect(result.endsWith('\n')).toBe(true);
|
|
208
|
+
expect(() => JSON.parse(result)).not.toThrow();
|
|
209
|
+
});
|
|
210
|
+
});
|
package/src/cli/stack-config.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
1
4
|
import type { TechTool, TeamTool, StackConfig, CopyDirOptions, RepoInfo } from './types.js';
|
|
2
5
|
import {
|
|
3
6
|
PLUGINS,
|
|
@@ -15,7 +18,7 @@ import type { PluginConfig } from '../orchestrator/plugins/types.js';
|
|
|
15
18
|
interface ToolInfo {
|
|
16
19
|
tech: string;
|
|
17
20
|
skill: string | null;
|
|
18
|
-
mcpServer
|
|
21
|
+
mcpServer?: string;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
/** All tech-tool metadata — derived from plugin configs. */
|
|
@@ -135,6 +138,23 @@ export function getRequiredMcpEnvVars(stack: StackConfig, repoInfo?: RepoInfo):
|
|
|
135
138
|
|
|
136
139
|
// ── Customization file transforms ─────────────────────────────
|
|
137
140
|
|
|
141
|
+
// ── Skill matrix JSON types ────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
export interface SkillMatrixEntry {
|
|
144
|
+
name: string;
|
|
145
|
+
skill: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface SkillMatrixSlot {
|
|
149
|
+
entries: SkillMatrixEntry[];
|
|
150
|
+
description: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface SkillMatrixData {
|
|
154
|
+
bindings: Record<string, SkillMatrixSlot>;
|
|
155
|
+
agents: Record<string, { slots: string[]; directSkills: string[] }>;
|
|
156
|
+
}
|
|
157
|
+
|
|
138
158
|
/**
|
|
139
159
|
* Return a transform callback that pre-populates customization files
|
|
140
160
|
* based on the user's stack selection.
|
|
@@ -145,47 +165,13 @@ export function getCustomizationsTransform(
|
|
|
145
165
|
stack: StackConfig
|
|
146
166
|
): NonNullable<CopyDirOptions['transform']> {
|
|
147
167
|
return (content: string, srcPath: string) => {
|
|
148
|
-
if (srcPath.endsWith('skill-matrix.
|
|
149
|
-
return
|
|
168
|
+
if (srcPath.endsWith('skill-matrix.json')) {
|
|
169
|
+
return updateSkillMatrixContent(content, stack);
|
|
150
170
|
}
|
|
151
171
|
return content;
|
|
152
172
|
};
|
|
153
173
|
}
|
|
154
174
|
|
|
155
|
-
/**
|
|
156
|
-
* Fill in the `database` and `cms` rows in the skill matrix
|
|
157
|
-
* based on the user's stack selection.
|
|
158
|
-
*/
|
|
159
|
-
function transformSkillMatrix(content: string, stack: StackConfig): string {
|
|
160
|
-
let result = content;
|
|
161
|
-
|
|
162
|
-
// Find first selected DB tool
|
|
163
|
-
const db = stack.techTools.find((t) => (DB_TOOLS as readonly string[]).includes(t));
|
|
164
|
-
if (db) {
|
|
165
|
-
const info = TECH_TOOL_INFO[db as TechTool];
|
|
166
|
-
if (info?.skill) {
|
|
167
|
-
result = result.replace(
|
|
168
|
-
/(\| `database`\s*\|)\s*\|(\s*\|)/,
|
|
169
|
-
`$1 ${info.tech} | \`${info.skill}\` $2`
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Find first selected CMS tool
|
|
175
|
-
const cms = stack.techTools.find((t) => (CMS_TOOLS as readonly string[]).includes(t));
|
|
176
|
-
if (cms) {
|
|
177
|
-
const info = TECH_TOOL_INFO[cms as TechTool];
|
|
178
|
-
if (info?.skill) {
|
|
179
|
-
result = result.replace(
|
|
180
|
-
/(\| `cms`\s*\|)\s*\|(\s*\|)/,
|
|
181
|
-
`$1 ${info.tech} | \`${info.skill}\` $2`
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return result;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
175
|
// ── Agent tool injection ──────────────────────────────────────
|
|
190
176
|
|
|
191
177
|
/**
|
|
@@ -253,3 +239,90 @@ export function getAgentTransform(
|
|
|
253
239
|
return `---\n${newFrontmatter}\n---\n${body}`;
|
|
254
240
|
};
|
|
255
241
|
}
|
|
242
|
+
|
|
243
|
+
// ── Skill matrix update ─────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
/** Mapping from plugin subCategory to skill matrix slot name. */
|
|
246
|
+
const SUBCATEGORY_TO_SLOT: Record<string, string> = {
|
|
247
|
+
database: 'database',
|
|
248
|
+
cms: 'cms',
|
|
249
|
+
deployment: 'deployment',
|
|
250
|
+
framework: 'framework',
|
|
251
|
+
'codebase-tool': 'codebase-tool',
|
|
252
|
+
'task-management': 'task-management',
|
|
253
|
+
testing: 'testing',
|
|
254
|
+
'e2e-testing': 'e2e-testing',
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Get the filesystem path to the skill matrix file for a given IDE.
|
|
259
|
+
*/
|
|
260
|
+
function getSkillMatrixPath(projectRoot: string, ide: string): string {
|
|
261
|
+
const relativePath = 'customizations/agents/skill-matrix.json';
|
|
262
|
+
switch (ide) {
|
|
263
|
+
case 'vscode':
|
|
264
|
+
return resolve(projectRoot, '.github', relativePath);
|
|
265
|
+
case 'cursor':
|
|
266
|
+
return resolve(projectRoot, '.cursor', 'rules', relativePath);
|
|
267
|
+
case 'claude-code':
|
|
268
|
+
return resolve(projectRoot, '.claude', relativePath);
|
|
269
|
+
case 'opencode':
|
|
270
|
+
return resolve(projectRoot, '.opencode', relativePath);
|
|
271
|
+
default:
|
|
272
|
+
return '';
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Update the skill matrix file in-place for a specific IDE.
|
|
278
|
+
* Updates slot entries based on the user's current stack selections.
|
|
279
|
+
* Returns true if the file was updated, false if unchanged or missing.
|
|
280
|
+
*/
|
|
281
|
+
export async function updateSkillMatrixFile(
|
|
282
|
+
projectRoot: string,
|
|
283
|
+
ide: string,
|
|
284
|
+
stack: StackConfig
|
|
285
|
+
): Promise<boolean> {
|
|
286
|
+
const matrixPath = getSkillMatrixPath(projectRoot, ide);
|
|
287
|
+
if (!matrixPath || !existsSync(matrixPath)) return false;
|
|
288
|
+
|
|
289
|
+
const content = await readFile(matrixPath, 'utf8');
|
|
290
|
+
const updated = updateSkillMatrixContent(content, stack);
|
|
291
|
+
if (updated !== content) {
|
|
292
|
+
await writeFile(matrixPath, updated);
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Update skill matrix JSON content based on stack selections.
|
|
300
|
+
* Pure function — sets slot entries for all plugin-mapped subcategories.
|
|
301
|
+
* Supports multiple plugins per slot (e.g. multiple databases).
|
|
302
|
+
*/
|
|
303
|
+
export function updateSkillMatrixContent(content: string, stack: StackConfig): string {
|
|
304
|
+
const data: SkillMatrixData = JSON.parse(content);
|
|
305
|
+
const allTools = [...stack.techTools, ...stack.teamTools] as string[];
|
|
306
|
+
|
|
307
|
+
for (const [subCategory, slotName] of Object.entries(SUBCATEGORY_TO_SLOT)) {
|
|
308
|
+
// Find ALL selected tools matching this subcategory (not just the first)
|
|
309
|
+
const matchingTools = allTools.filter((toolId) => {
|
|
310
|
+
const plugin = PLUGINS[toolId];
|
|
311
|
+
return plugin?.subCategory === subCategory;
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const entries: SkillMatrixEntry[] = matchingTools
|
|
315
|
+
.map((toolId) => {
|
|
316
|
+
const plugin = PLUGINS[toolId];
|
|
317
|
+
if (!plugin?.skillName) return null;
|
|
318
|
+
return { name: plugin.name, skill: plugin.skillName };
|
|
319
|
+
})
|
|
320
|
+
.filter((e): e is SkillMatrixEntry => e !== null);
|
|
321
|
+
|
|
322
|
+
if (data.bindings[slotName]) {
|
|
323
|
+
data.bindings[slotName].entries = entries;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return JSON.stringify(data, null, 2) + '\n';
|
|
328
|
+
}
|
package/src/cli/types.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { ChildProcess } from 'node:child_process';
|
|
|
3
3
|
// ── Stack selection types ──────────────────────────────────────
|
|
4
4
|
|
|
5
5
|
export type IdeChoice = 'vscode' | 'cursor' | 'claude-code' | 'opencode';
|
|
6
|
-
export type TechTool = 'sanity' | 'contentful' | 'strapi' | 'supabase' | 'convex' | 'vercel' | 'nx' | 'chrome-devtools';
|
|
6
|
+
export type TechTool = 'sanity' | 'contentful' | 'strapi' | 'supabase' | 'convex' | 'vercel' | 'nx' | 'chrome-devtools' | 'nextjs' | 'astro';
|
|
7
7
|
export type TeamTool = 'linear' | 'jira' | 'slack' | 'teams';
|
|
8
8
|
|
|
9
9
|
export interface StackConfig {
|