opencastle 0.9.2 → 0.10.1
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 +12 -69
- 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 +2 -1
- 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/run/adapters/copilot.d.ts +10 -2
- package/dist/cli/run/adapters/copilot.d.ts.map +1 -1
- package/dist/cli/run/adapters/copilot.js +83 -56
- package/dist/cli/run/adapters/copilot.js.map +1 -1
- package/dist/cli/run.js +2 -2
- package/dist/cli/run.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 +33 -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 +2 -1
- package/src/cli/doctor.ts +14 -7
- package/src/cli/init.test.ts +1141 -0
- package/src/cli/init.ts +2 -1
- package/src/cli/mcp.ts +77 -1
- package/src/cli/run/adapters/copilot.ts +86 -58
- package/src/cli/run.ts +2 -2
- 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 +34 -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/prompts/generate-task-spec.prompt.md +1 -1
- 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 ────────────────────────────────')}`)
|
|
@@ -248,7 +249,7 @@ export default async function init({ pkgRoot, args }: CliContext): Promise<void>
|
|
|
248
249
|
// ── OAuth setup guides ────────────────────────────────────────
|
|
249
250
|
if (teamTools.includes('slack')) {
|
|
250
251
|
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/
|
|
252
|
+
console.log(` Setup guide: ${c.cyan('https://www.opencastle.dev/docs/plugins#slack')}\n`)
|
|
252
253
|
}
|
|
253
254
|
|
|
254
255
|
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
|
+
}
|
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
|
+
import type { CopilotClient as CopilotClientType, CopilotSession, PermissionHandler } from '@github/copilot-sdk'
|
|
2
3
|
import type { Task, ExecuteOptions, ExecuteResult } from '../../types.js'
|
|
3
4
|
|
|
4
5
|
/** Adapter name */
|
|
5
6
|
export const name = 'copilot'
|
|
6
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Lazy-initialized shared client instance.
|
|
10
|
+
* The client manages a single Copilot CLI server process; all task sessions
|
|
11
|
+
* multiplex over it via JSON-RPC.
|
|
12
|
+
*/
|
|
13
|
+
let clientPromise: Promise<CopilotClientType> | null = null
|
|
14
|
+
|
|
15
|
+
/** Cached permission handler from the SDK module. */
|
|
16
|
+
let cachedApproveAll: PermissionHandler | null = null
|
|
17
|
+
|
|
18
|
+
/** Active sessions keyed by task id — used by `kill()` for timeout enforcement. */
|
|
19
|
+
const activeSessions = new Map<string, CopilotSession>()
|
|
20
|
+
|
|
7
21
|
/**
|
|
8
22
|
* Check if the `copilot` CLI is available on the system PATH.
|
|
23
|
+
* The SDK communicates with the CLI in server mode, so it must be installed.
|
|
9
24
|
*/
|
|
10
25
|
export async function isAvailable(): Promise<boolean> {
|
|
11
26
|
return new Promise((resolve) => {
|
|
@@ -16,7 +31,33 @@ export async function isAvailable(): Promise<boolean> {
|
|
|
16
31
|
}
|
|
17
32
|
|
|
18
33
|
/**
|
|
19
|
-
*
|
|
34
|
+
* Get or create the shared CopilotClient.
|
|
35
|
+
* The client is started once and reused across all task executions.
|
|
36
|
+
*/
|
|
37
|
+
async function getClient(): Promise<CopilotClientType> {
|
|
38
|
+
if (!clientPromise) {
|
|
39
|
+
clientPromise = (async () => {
|
|
40
|
+
const { CopilotClient, approveAll } = await import('@github/copilot-sdk')
|
|
41
|
+
cachedApproveAll = approveAll
|
|
42
|
+
const client = new CopilotClient({
|
|
43
|
+
autoStart: false,
|
|
44
|
+
logLevel: 'error',
|
|
45
|
+
})
|
|
46
|
+
await client.start()
|
|
47
|
+
return client
|
|
48
|
+
})()
|
|
49
|
+
}
|
|
50
|
+
return clientPromise
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Execute a task using the Copilot SDK.
|
|
55
|
+
*
|
|
56
|
+
* Each task gets its own session with:
|
|
57
|
+
* - All tool permissions auto-approved (equivalent to `--allow-all-tools`)
|
|
58
|
+
* - No `ask_user` tool (autonomous — equivalent to `--no-ask-user`)
|
|
59
|
+
* - System message injected with the agent role
|
|
60
|
+
* - Streaming enabled in verbose mode for live output
|
|
20
61
|
*/
|
|
21
62
|
export async function execute(task: Task, options: ExecuteOptions = {}): Promise<ExecuteResult> {
|
|
22
63
|
let prompt = `You are a ${task.agent}. ${task.prompt}`
|
|
@@ -25,73 +66,60 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
|
|
|
25
66
|
prompt += `\n\nOnly modify files under: ${task.files.join(', ')}`
|
|
26
67
|
}
|
|
27
68
|
|
|
28
|
-
const
|
|
29
|
-
'-p',
|
|
30
|
-
prompt,
|
|
31
|
-
'--autopilot',
|
|
32
|
-
'--allow-all-tools',
|
|
33
|
-
'--no-ask-user',
|
|
34
|
-
'-s',
|
|
35
|
-
'--max-autopilot-continues',
|
|
36
|
-
'50',
|
|
37
|
-
]
|
|
38
|
-
|
|
39
|
-
return new Promise((resolve) => {
|
|
40
|
-
const proc = spawn('copilot', args, {
|
|
41
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
42
|
-
env: { ...process.env },
|
|
43
|
-
cwd: process.cwd(),
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
let stdout = ''
|
|
47
|
-
let stderr = ''
|
|
69
|
+
const client = await getClient()
|
|
48
70
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
71
|
+
const session = await client.createSession({
|
|
72
|
+
onPermissionRequest: cachedApproveAll!,
|
|
73
|
+
systemMessage: {
|
|
74
|
+
content: [
|
|
75
|
+
`You are a ${task.agent}.`,
|
|
76
|
+
'Work autonomously without asking questions.',
|
|
77
|
+
'Follow all instructions precisely.',
|
|
78
|
+
].join(' '),
|
|
79
|
+
},
|
|
80
|
+
infiniteSessions: { enabled: false },
|
|
81
|
+
...(options.verbose ? { streaming: true } : {}),
|
|
82
|
+
})
|
|
55
83
|
|
|
56
|
-
|
|
57
|
-
stderr += chunk.toString()
|
|
58
|
-
if (options.verbose) {
|
|
59
|
-
process.stderr.write(chunk)
|
|
60
|
-
}
|
|
61
|
-
})
|
|
84
|
+
activeSessions.set(task.id, session)
|
|
62
85
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
output: output.slice(0, 10000), // Cap output size
|
|
68
|
-
exitCode: code ?? -1,
|
|
69
|
-
})
|
|
86
|
+
// Stream deltas to stdout in verbose mode
|
|
87
|
+
if (options.verbose) {
|
|
88
|
+
session.on('assistant.message_delta', (event: { data: { deltaContent: string } }) => {
|
|
89
|
+
process.stdout.write(event.data.deltaContent)
|
|
70
90
|
})
|
|
91
|
+
}
|
|
71
92
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
output: `Failed to spawn copilot: ${err.message}`,
|
|
76
|
-
exitCode: -1,
|
|
77
|
-
})
|
|
78
|
-
})
|
|
93
|
+
try {
|
|
94
|
+
const response = await session.sendAndWait({ prompt })
|
|
95
|
+
const output = response?.data?.content ?? ''
|
|
79
96
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
97
|
+
return {
|
|
98
|
+
success: true,
|
|
99
|
+
output: output.slice(0, 10_000),
|
|
100
|
+
exitCode: 0,
|
|
101
|
+
}
|
|
102
|
+
} catch (err: unknown) {
|
|
103
|
+
return {
|
|
104
|
+
success: false,
|
|
105
|
+
output: `Copilot SDK error: ${(err as Error).message}`,
|
|
106
|
+
exitCode: 1,
|
|
107
|
+
}
|
|
108
|
+
} finally {
|
|
109
|
+
activeSessions.delete(task.id)
|
|
110
|
+
await session.destroy().catch(() => {})
|
|
111
|
+
}
|
|
83
112
|
}
|
|
84
113
|
|
|
85
114
|
/**
|
|
86
|
-
*
|
|
115
|
+
* Abort and destroy the session associated with a task.
|
|
116
|
+
* Called by the executor when a task exceeds its timeout.
|
|
87
117
|
*/
|
|
88
118
|
export function kill(task: Task): void {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
}, 5000)
|
|
119
|
+
const session = activeSessions.get(task.id)
|
|
120
|
+
if (session) {
|
|
121
|
+
session.abort().catch(() => {})
|
|
122
|
+
session.destroy().catch(() => {})
|
|
123
|
+
activeSessions.delete(task.id)
|
|
96
124
|
}
|
|
97
125
|
}
|
package/src/cli/run.ts
CHANGED
|
@@ -119,8 +119,8 @@ export default async function run({ args }: CliContext): Promise<void> {
|
|
|
119
119
|
' Install: npm install -g @anthropic-ai/claude-code\n' +
|
|
120
120
|
' Docs: https://docs.anthropic.com/en/docs/claude-code',
|
|
121
121
|
copilot:
|
|
122
|
-
'
|
|
123
|
-
'
|
|
122
|
+
' Requires the Copilot CLI installed and authenticated:\n' +
|
|
123
|
+
' https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli\n' +
|
|
124
124
|
' Docs: https://docs.github.com/en/copilot',
|
|
125
125
|
cursor:
|
|
126
126
|
' The Cursor agent CLI ships with the Cursor editor.\n' +
|
|
@@ -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
|
+
}
|