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
|
@@ -0,0 +1,881 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the init command — validates that all IDE adapters generate
|
|
3
|
+
* correct files based on stack selections (tech tools, team tools, IDEs).
|
|
4
|
+
*
|
|
5
|
+
* Tests the dynamic parts:
|
|
6
|
+
* - Excluded agents (no CMS → no content-engineer, no DB → no database-engineer)
|
|
7
|
+
* - Excluded skills (only selected plugin skills are installed)
|
|
8
|
+
* - Plugin skills (SKILL.md from plugin dirs)
|
|
9
|
+
* - MCP config generation per IDE format
|
|
10
|
+
* - Agent tool injection from plugin agentToolMap
|
|
11
|
+
* - Skill matrix transform (database/cms rows filled)
|
|
12
|
+
* - Gitignore block generation
|
|
13
|
+
* - Single-file root documents (CLAUDE.md, AGENTS.md)
|
|
14
|
+
* - Cursor .mdc conversion
|
|
15
|
+
*/
|
|
16
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
17
|
+
import { mkdtemp, readFile, readdir, rm } from 'node:fs/promises';
|
|
18
|
+
import { join, resolve } from 'node:path';
|
|
19
|
+
import { tmpdir } from 'node:os';
|
|
20
|
+
import { existsSync } from 'node:fs';
|
|
21
|
+
import { updateGitignore } from './gitignore.js';
|
|
22
|
+
import { getExcludedSkills, getExcludedAgents, getIncludedMcpServers, getRequiredMcpEnvVars, getAgentToolInjections, getCustomizationsTransform, } from './stack-config.js';
|
|
23
|
+
import { ALL_PLUGIN_SKILL_NAMES } from '../orchestrator/plugins/index.js';
|
|
24
|
+
import { IDE_ADAPTERS } from './adapters/index.js';
|
|
25
|
+
// ── Helpers ────────────────────────────────────────────────────
|
|
26
|
+
/** The real package root — tests run against the actual source tree. */
|
|
27
|
+
const PKG_ROOT = resolve(import.meta.dirname, '../..');
|
|
28
|
+
/** Read a JSON file from disk. */
|
|
29
|
+
async function readJson(path) {
|
|
30
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
31
|
+
}
|
|
32
|
+
/** Recursively list all files in a directory (relative paths). */
|
|
33
|
+
async function listFilesRecursive(dir, prefix = '') {
|
|
34
|
+
if (!existsSync(dir))
|
|
35
|
+
return [];
|
|
36
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
37
|
+
const files = [];
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
40
|
+
if (entry.isDirectory()) {
|
|
41
|
+
files.push(...await listFilesRecursive(join(dir, entry.name), rel));
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
files.push(rel);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return files.sort();
|
|
48
|
+
}
|
|
49
|
+
// ── Stack fixtures ─────────────────────────────────────────────
|
|
50
|
+
const STACK_EMPTY = {
|
|
51
|
+
ides: ['vscode'],
|
|
52
|
+
techTools: [],
|
|
53
|
+
teamTools: [],
|
|
54
|
+
};
|
|
55
|
+
const STACK_SANITY_LINEAR = {
|
|
56
|
+
ides: ['vscode'],
|
|
57
|
+
techTools: ['sanity'],
|
|
58
|
+
teamTools: ['linear'],
|
|
59
|
+
};
|
|
60
|
+
const STACK_SUPABASE_SLACK = {
|
|
61
|
+
ides: ['vscode'],
|
|
62
|
+
techTools: ['supabase'],
|
|
63
|
+
teamTools: ['slack'],
|
|
64
|
+
};
|
|
65
|
+
const STACK_FULL = {
|
|
66
|
+
ides: ['vscode', 'cursor', 'claude-code', 'opencode'],
|
|
67
|
+
techTools: ['sanity', 'supabase', 'vercel'],
|
|
68
|
+
teamTools: ['linear', 'slack'],
|
|
69
|
+
};
|
|
70
|
+
const EMPTY_REPO_INFO = {};
|
|
71
|
+
// ═══════════════════════════════════════════════════════════════
|
|
72
|
+
// § 1 Stack Config Logic (pure functions — no filesystem)
|
|
73
|
+
// ═══════════════════════════════════════════════════════════════
|
|
74
|
+
describe('stack-config: getExcludedAgents', () => {
|
|
75
|
+
it('excludes content-engineer when no CMS tool is selected', () => {
|
|
76
|
+
const excluded = getExcludedAgents(STACK_EMPTY);
|
|
77
|
+
expect(excluded.has('content-engineer.agent.md')).toBe(true);
|
|
78
|
+
expect(excluded.has('database-engineer.agent.md')).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
it('includes content-engineer when a CMS tool is selected', () => {
|
|
81
|
+
const excluded = getExcludedAgents(STACK_SANITY_LINEAR);
|
|
82
|
+
expect(excluded.has('content-engineer.agent.md')).toBe(false);
|
|
83
|
+
// No DB selected → database-engineer still excluded
|
|
84
|
+
expect(excluded.has('database-engineer.agent.md')).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
it('includes database-engineer when a DB tool is selected', () => {
|
|
87
|
+
const excluded = getExcludedAgents(STACK_SUPABASE_SLACK);
|
|
88
|
+
expect(excluded.has('database-engineer.agent.md')).toBe(false);
|
|
89
|
+
// No CMS selected → content-engineer still excluded
|
|
90
|
+
expect(excluded.has('content-engineer.agent.md')).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
it('includes both when CMS and DB are selected', () => {
|
|
93
|
+
const excluded = getExcludedAgents(STACK_FULL);
|
|
94
|
+
expect(excluded.has('content-engineer.agent.md')).toBe(false);
|
|
95
|
+
expect(excluded.has('database-engineer.agent.md')).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('stack-config: getExcludedSkills', () => {
|
|
99
|
+
it('excludes all plugin skills when nothing is selected', () => {
|
|
100
|
+
const excluded = getExcludedSkills(STACK_EMPTY);
|
|
101
|
+
// Every plugin-specific skill should be excluded
|
|
102
|
+
for (const skill of ALL_PLUGIN_SKILL_NAMES) {
|
|
103
|
+
expect(excluded.has(skill)).toBe(true);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
it('includes only selected plugin skills', () => {
|
|
107
|
+
const excluded = getExcludedSkills(STACK_SANITY_LINEAR);
|
|
108
|
+
expect(excluded.has('sanity-cms')).toBe(false);
|
|
109
|
+
expect(excluded.has('linear-task-management')).toBe(false);
|
|
110
|
+
// Unselected skills should still be excluded
|
|
111
|
+
expect(excluded.has('supabase-database')).toBe(true);
|
|
112
|
+
expect(excluded.has('slack-notifications')).toBe(true);
|
|
113
|
+
expect(excluded.has('vercel-deployment')).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
describe('stack-config: getIncludedMcpServers', () => {
|
|
117
|
+
it('returns empty set when nothing selected', () => {
|
|
118
|
+
const servers = getIncludedMcpServers(STACK_EMPTY);
|
|
119
|
+
expect(servers.size).toBe(0);
|
|
120
|
+
});
|
|
121
|
+
it('includes servers for selected tech and team tools', () => {
|
|
122
|
+
const servers = getIncludedMcpServers(STACK_SANITY_LINEAR);
|
|
123
|
+
expect(servers.has('Sanity')).toBe(true);
|
|
124
|
+
expect(servers.has('Linear')).toBe(true);
|
|
125
|
+
expect(servers.has('Supabase')).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
it('auto-includes Vercel when detected in deployment', () => {
|
|
128
|
+
const repoInfo = { deployment: ['vercel'] };
|
|
129
|
+
const servers = getIncludedMcpServers(STACK_EMPTY, repoInfo);
|
|
130
|
+
expect(servers.has('Vercel')).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
it('auto-includes NX when monorepo detected and not in stack', () => {
|
|
133
|
+
const repoInfo = { monorepo: 'nx' };
|
|
134
|
+
const servers = getIncludedMcpServers(STACK_EMPTY, repoInfo);
|
|
135
|
+
expect(servers.has('Nx')).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe('stack-config: getRequiredMcpEnvVars', () => {
|
|
139
|
+
it('returns empty when no tools need env vars', () => {
|
|
140
|
+
// Sanity uses OAuth (no env vars), so only Linear needs one
|
|
141
|
+
const vars = getRequiredMcpEnvVars({
|
|
142
|
+
ides: ['vscode'],
|
|
143
|
+
techTools: ['sanity'],
|
|
144
|
+
teamTools: [],
|
|
145
|
+
});
|
|
146
|
+
expect(vars).toHaveLength(0);
|
|
147
|
+
});
|
|
148
|
+
it('returns LINEAR_API_KEY when linear is selected', () => {
|
|
149
|
+
const vars = getRequiredMcpEnvVars(STACK_SANITY_LINEAR);
|
|
150
|
+
expect(vars).toEqual(expect.arrayContaining([
|
|
151
|
+
expect.objectContaining({ envVar: 'LINEAR_API_KEY' }),
|
|
152
|
+
]));
|
|
153
|
+
});
|
|
154
|
+
it('returns SLACK_MCP_XOXB_TOKEN when slack is selected', () => {
|
|
155
|
+
const vars = getRequiredMcpEnvVars(STACK_SUPABASE_SLACK);
|
|
156
|
+
expect(vars).toEqual(expect.arrayContaining([
|
|
157
|
+
expect.objectContaining({ envVar: 'SLACK_MCP_XOXB_TOKEN' }),
|
|
158
|
+
]));
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
describe('stack-config: getAgentToolInjections', () => {
|
|
162
|
+
it('returns empty map when no tools selected', () => {
|
|
163
|
+
const injections = getAgentToolInjections(STACK_EMPTY);
|
|
164
|
+
expect(injections.size).toBe(0);
|
|
165
|
+
});
|
|
166
|
+
it('injects sanity tools into content-engineer when sanity selected', () => {
|
|
167
|
+
const injections = getAgentToolInjections(STACK_SANITY_LINEAR);
|
|
168
|
+
const contentTools = injections.get('content-engineer');
|
|
169
|
+
expect(contentTools).toBeDefined();
|
|
170
|
+
expect(contentTools).toContain('sanity/get_schema');
|
|
171
|
+
expect(contentTools).toContain('sanity/query_documents');
|
|
172
|
+
});
|
|
173
|
+
it('injects linear tools into team-lead when linear selected', () => {
|
|
174
|
+
const injections = getAgentToolInjections(STACK_SANITY_LINEAR);
|
|
175
|
+
const teamLeadTools = injections.get('team-lead');
|
|
176
|
+
expect(teamLeadTools).toBeDefined();
|
|
177
|
+
expect(teamLeadTools).toContain('linear/create_issue');
|
|
178
|
+
expect(teamLeadTools).toContain('linear/list_issues');
|
|
179
|
+
});
|
|
180
|
+
it('injects supabase tools into database-engineer when supabase selected', () => {
|
|
181
|
+
const injections = getAgentToolInjections(STACK_SUPABASE_SLACK);
|
|
182
|
+
const dbTools = injections.get('database-engineer');
|
|
183
|
+
expect(dbTools).toBeDefined();
|
|
184
|
+
expect(dbTools).toContain('supabase/apply_migration');
|
|
185
|
+
expect(dbTools).toContain('supabase/execute_sql');
|
|
186
|
+
});
|
|
187
|
+
it('aggregates tools from multiple plugins per agent', () => {
|
|
188
|
+
const injections = getAgentToolInjections(STACK_FULL);
|
|
189
|
+
const teamLeadTools = injections.get('team-lead');
|
|
190
|
+
// Linear + Slack tools on team-lead
|
|
191
|
+
expect(teamLeadTools).toContain('linear/create_issue');
|
|
192
|
+
expect(teamLeadTools).toContain('slack/*');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
describe('stack-config: getCustomizationsTransform', () => {
|
|
196
|
+
const emptyMatrix = JSON.stringify({
|
|
197
|
+
bindings: {
|
|
198
|
+
database: { entries: [], description: 'Schema' },
|
|
199
|
+
cms: { entries: [], description: 'CMS' },
|
|
200
|
+
},
|
|
201
|
+
agents: {},
|
|
202
|
+
}, null, 2) + '\n';
|
|
203
|
+
it('fills database slot in skill-matrix.json when DB tool is selected', () => {
|
|
204
|
+
const transform = getCustomizationsTransform(STACK_SUPABASE_SLACK);
|
|
205
|
+
const result = transform(emptyMatrix, 'skill-matrix.json');
|
|
206
|
+
expect(result).toContain('Supabase');
|
|
207
|
+
expect(result).toContain('supabase-database');
|
|
208
|
+
});
|
|
209
|
+
it('fills CMS slot in skill-matrix.json when CMS tool is selected', () => {
|
|
210
|
+
const transform = getCustomizationsTransform(STACK_SANITY_LINEAR);
|
|
211
|
+
const result = transform(emptyMatrix, 'skill-matrix.json');
|
|
212
|
+
expect(result).toContain('Sanity');
|
|
213
|
+
expect(result).toContain('sanity-cms');
|
|
214
|
+
});
|
|
215
|
+
it('leaves slots empty when no DB or CMS selected', () => {
|
|
216
|
+
const transform = getCustomizationsTransform(STACK_EMPTY);
|
|
217
|
+
const result = transform(emptyMatrix, 'skill-matrix.json');
|
|
218
|
+
const data = JSON.parse(result);
|
|
219
|
+
expect(data.bindings.database.entries).toEqual([]);
|
|
220
|
+
expect(data.bindings.cms.entries).toEqual([]);
|
|
221
|
+
});
|
|
222
|
+
it('passes through non-skill-matrix files unchanged', () => {
|
|
223
|
+
const transform = getCustomizationsTransform(STACK_FULL);
|
|
224
|
+
const input = '# Some other file\nContent here';
|
|
225
|
+
const result = transform(input, 'something-else.md');
|
|
226
|
+
expect(result).toBe(input);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
// ═══════════════════════════════════════════════════════════════
|
|
230
|
+
// § 2 Gitignore Generation
|
|
231
|
+
// ═══════════════════════════════════════════════════════════════
|
|
232
|
+
describe('gitignore generation', () => {
|
|
233
|
+
let tempDir;
|
|
234
|
+
beforeEach(async () => {
|
|
235
|
+
tempDir = await mkdtemp(join(tmpdir(), 'opencastle-init-test-'));
|
|
236
|
+
});
|
|
237
|
+
afterEach(async () => {
|
|
238
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
239
|
+
});
|
|
240
|
+
it('creates .gitignore with framework paths ignored and customizable un-ignored', async () => {
|
|
241
|
+
const managed = {
|
|
242
|
+
framework: ['.github/copilot-instructions.md', '.github/agents/'],
|
|
243
|
+
customizable: ['.github/customizations/', '.vscode/mcp.json'],
|
|
244
|
+
};
|
|
245
|
+
await updateGitignore(tempDir, managed);
|
|
246
|
+
const content = await readFile(join(tempDir, '.gitignore'), 'utf8');
|
|
247
|
+
// Framework paths should be ignored
|
|
248
|
+
expect(content).toContain('.github/copilot-instructions.md');
|
|
249
|
+
expect(content).toContain('.github/agents/');
|
|
250
|
+
// Customizable paths should be un-ignored
|
|
251
|
+
expect(content).toContain('!.github/customizations/');
|
|
252
|
+
expect(content).toContain('!.vscode/mcp.json');
|
|
253
|
+
// Markers should be present
|
|
254
|
+
expect(content).toContain('# >>> OpenCastle managed (do not edit) >>>');
|
|
255
|
+
expect(content).toContain('# <<< OpenCastle managed <<<');
|
|
256
|
+
});
|
|
257
|
+
it('replaces existing block on re-init', async () => {
|
|
258
|
+
const managed1 = {
|
|
259
|
+
framework: ['.github/agents/'],
|
|
260
|
+
customizable: ['.vscode/mcp.json'],
|
|
261
|
+
};
|
|
262
|
+
await updateGitignore(tempDir, managed1);
|
|
263
|
+
const managed2 = {
|
|
264
|
+
framework: ['.github/agents/', '.github/skills/'],
|
|
265
|
+
customizable: ['.vscode/mcp.json', '.github/customizations/'],
|
|
266
|
+
};
|
|
267
|
+
const result = await updateGitignore(tempDir, managed2);
|
|
268
|
+
expect(result).toBe('updated');
|
|
269
|
+
const content = await readFile(join(tempDir, '.gitignore'), 'utf8');
|
|
270
|
+
expect(content).toContain('.github/skills/');
|
|
271
|
+
expect(content).toContain('!.github/customizations/');
|
|
272
|
+
// Only one managed block
|
|
273
|
+
const startCount = (content.match(/>>> OpenCastle managed/g) ?? []).length;
|
|
274
|
+
expect(startCount).toBe(1);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
// ═══════════════════════════════════════════════════════════════
|
|
278
|
+
// § 3 VS Code Adapter — Full Install Validation
|
|
279
|
+
// ═══════════════════════════════════════════════════════════════
|
|
280
|
+
describe('VS Code adapter install', () => {
|
|
281
|
+
let tempDir;
|
|
282
|
+
beforeEach(async () => {
|
|
283
|
+
tempDir = await mkdtemp(join(tmpdir(), 'opencastle-vscode-test-'));
|
|
284
|
+
});
|
|
285
|
+
afterEach(async () => {
|
|
286
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
287
|
+
});
|
|
288
|
+
it('creates all expected framework directories', async () => {
|
|
289
|
+
const adapter = await IDE_ADAPTERS['vscode']();
|
|
290
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_EMPTY, EMPTY_REPO_INFO);
|
|
291
|
+
const githubDir = join(tempDir, '.github');
|
|
292
|
+
expect(existsSync(join(githubDir, 'copilot-instructions.md'))).toBe(true);
|
|
293
|
+
expect(existsSync(join(githubDir, 'agents'))).toBe(true);
|
|
294
|
+
expect(existsSync(join(githubDir, 'instructions'))).toBe(true);
|
|
295
|
+
expect(existsSync(join(githubDir, 'skills'))).toBe(true);
|
|
296
|
+
expect(existsSync(join(githubDir, 'agent-workflows'))).toBe(true);
|
|
297
|
+
expect(existsSync(join(githubDir, 'prompts'))).toBe(true);
|
|
298
|
+
expect(existsSync(join(githubDir, 'customizations'))).toBe(true);
|
|
299
|
+
expect(existsSync(join(tempDir, '.vscode', 'mcp.json'))).toBe(true);
|
|
300
|
+
});
|
|
301
|
+
it('excludes content-engineer and database-engineer agents when no CMS/DB', async () => {
|
|
302
|
+
const adapter = await IDE_ADAPTERS['vscode']();
|
|
303
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_EMPTY, EMPTY_REPO_INFO);
|
|
304
|
+
const agentsDir = join(tempDir, '.github', 'agents');
|
|
305
|
+
const agents = await readdir(agentsDir);
|
|
306
|
+
expect(agents).not.toContain('content-engineer.agent.md');
|
|
307
|
+
expect(agents).not.toContain('database-engineer.agent.md');
|
|
308
|
+
// Others should still be present
|
|
309
|
+
expect(agents).toContain('developer.agent.md');
|
|
310
|
+
expect(agents).toContain('team-lead.agent.md');
|
|
311
|
+
expect(agents).toContain('architect.agent.md');
|
|
312
|
+
});
|
|
313
|
+
it('includes content-engineer when CMS tool is selected', async () => {
|
|
314
|
+
const adapter = await IDE_ADAPTERS['vscode']();
|
|
315
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_SANITY_LINEAR, EMPTY_REPO_INFO);
|
|
316
|
+
const agents = await readdir(join(tempDir, '.github', 'agents'));
|
|
317
|
+
expect(agents).toContain('content-engineer.agent.md');
|
|
318
|
+
expect(agents).not.toContain('database-engineer.agent.md');
|
|
319
|
+
});
|
|
320
|
+
it('includes database-engineer when DB tool is selected', async () => {
|
|
321
|
+
const adapter = await IDE_ADAPTERS['vscode']();
|
|
322
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_SUPABASE_SLACK, EMPTY_REPO_INFO);
|
|
323
|
+
const agents = await readdir(join(tempDir, '.github', 'agents'));
|
|
324
|
+
expect(agents).toContain('database-engineer.agent.md');
|
|
325
|
+
expect(agents).not.toContain('content-engineer.agent.md');
|
|
326
|
+
});
|
|
327
|
+
it('excludes unselected plugin skills from skills directory', async () => {
|
|
328
|
+
const adapter = await IDE_ADAPTERS['vscode']();
|
|
329
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_SANITY_LINEAR, EMPTY_REPO_INFO);
|
|
330
|
+
const skillsDir = join(tempDir, '.github', 'skills');
|
|
331
|
+
const skills = await readdir(skillsDir);
|
|
332
|
+
// Selected plugin skills should be present
|
|
333
|
+
expect(skills).toContain('sanity');
|
|
334
|
+
expect(skills).toContain('linear');
|
|
335
|
+
// Unselected plugin skills should NOT be present
|
|
336
|
+
expect(skills).not.toContain('supabase');
|
|
337
|
+
expect(skills).not.toContain('slack');
|
|
338
|
+
expect(skills).not.toContain('vercel');
|
|
339
|
+
// Core skills (non-plugin) should always be present
|
|
340
|
+
expect(skills).toContain('accessibility-standards');
|
|
341
|
+
expect(skills).toContain('self-improvement');
|
|
342
|
+
expect(skills).toContain('testing-workflow');
|
|
343
|
+
});
|
|
344
|
+
it('excludes unselected core skills that map to plugins', async () => {
|
|
345
|
+
const adapter = await IDE_ADAPTERS['vscode']();
|
|
346
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_EMPTY, EMPTY_REPO_INFO);
|
|
347
|
+
const skills = await readdir(join(tempDir, '.github', 'skills'));
|
|
348
|
+
// Plugin-linked skill directories should be absent if tool not selected
|
|
349
|
+
// (The core skills directory names don't match plugin IDs — they're separate)
|
|
350
|
+
// But plugin SKILL.md dirs should not exist
|
|
351
|
+
expect(skills).not.toContain('sanity');
|
|
352
|
+
expect(skills).not.toContain('linear');
|
|
353
|
+
expect(skills).not.toContain('supabase');
|
|
354
|
+
});
|
|
355
|
+
it('injects plugin tools into agent frontmatter', async () => {
|
|
356
|
+
const adapter = await IDE_ADAPTERS['vscode']();
|
|
357
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_SANITY_LINEAR, EMPTY_REPO_INFO);
|
|
358
|
+
// Read content-engineer agent — should have sanity tools injected
|
|
359
|
+
const contentEngineer = await readFile(join(tempDir, '.github', 'agents', 'content-engineer.agent.md'), 'utf8');
|
|
360
|
+
expect(contentEngineer).toContain("'sanity/get_schema'");
|
|
361
|
+
expect(contentEngineer).toContain("'sanity/query_documents'");
|
|
362
|
+
expect(contentEngineer).toContain("'sanity/deploy_schema'");
|
|
363
|
+
// Read team-lead agent — should have linear tools injected
|
|
364
|
+
const teamLead = await readFile(join(tempDir, '.github', 'agents', 'team-lead.agent.md'), 'utf8');
|
|
365
|
+
expect(teamLead).toContain("'linear/create_issue'");
|
|
366
|
+
expect(teamLead).toContain("'linear/list_issues'");
|
|
367
|
+
expect(teamLead).toContain("'linear/update_issue'");
|
|
368
|
+
});
|
|
369
|
+
it('does NOT inject tools when no plugins selected', async () => {
|
|
370
|
+
const adapter = await IDE_ADAPTERS['vscode']();
|
|
371
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_EMPTY, EMPTY_REPO_INFO);
|
|
372
|
+
const teamLead = await readFile(join(tempDir, '.github', 'agents', 'team-lead.agent.md'), 'utf8');
|
|
373
|
+
expect(teamLead).not.toContain('linear/');
|
|
374
|
+
expect(teamLead).not.toContain('sanity/');
|
|
375
|
+
expect(teamLead).not.toContain('supabase/');
|
|
376
|
+
});
|
|
377
|
+
it('generates VS Code MCP config with correct format (servers + inputs)', async () => {
|
|
378
|
+
const adapter = await IDE_ADAPTERS['vscode']();
|
|
379
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_SANITY_LINEAR, EMPTY_REPO_INFO);
|
|
380
|
+
const mcpConfig = await readJson(join(tempDir, '.vscode', 'mcp.json'));
|
|
381
|
+
// VS Code format uses "servers" key
|
|
382
|
+
expect(mcpConfig).toHaveProperty('servers');
|
|
383
|
+
const servers = mcpConfig.servers;
|
|
384
|
+
expect(servers).toHaveProperty('Sanity');
|
|
385
|
+
expect(servers).toHaveProperty('Linear');
|
|
386
|
+
// Sanity uses HTTP
|
|
387
|
+
const sanityServer = servers.Sanity;
|
|
388
|
+
expect(sanityServer.type).toBe('http');
|
|
389
|
+
expect(sanityServer.url).toBe('https://mcp.sanity.io');
|
|
390
|
+
// Linear uses stdio
|
|
391
|
+
const linearServer = servers.Linear;
|
|
392
|
+
expect(linearServer.type).toBe('stdio');
|
|
393
|
+
expect(linearServer.command).toBe('npx');
|
|
394
|
+
expect(linearServer.args).toContain('-y');
|
|
395
|
+
expect(linearServer.args).toContain('@mseep/linear-mcp');
|
|
396
|
+
});
|
|
397
|
+
it('generates empty MCP config when no tools selected', async () => {
|
|
398
|
+
const adapter = await IDE_ADAPTERS['vscode']();
|
|
399
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_EMPTY, EMPTY_REPO_INFO);
|
|
400
|
+
const mcpConfig = await readJson(join(tempDir, '.vscode', 'mcp.json'));
|
|
401
|
+
expect(mcpConfig).toHaveProperty('servers');
|
|
402
|
+
const servers = mcpConfig.servers;
|
|
403
|
+
expect(Object.keys(servers)).toHaveLength(0);
|
|
404
|
+
});
|
|
405
|
+
it('fills skill-matrix.json with selected DB and CMS', async () => {
|
|
406
|
+
const adapter = await IDE_ADAPTERS['vscode']();
|
|
407
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_FULL, EMPTY_REPO_INFO);
|
|
408
|
+
const skillMatrix = await readFile(join(tempDir, '.github', 'customizations', 'agents', 'skill-matrix.json'), 'utf8');
|
|
409
|
+
const data = JSON.parse(skillMatrix);
|
|
410
|
+
expect(data.bindings.database.entries).toEqual(expect.arrayContaining([expect.objectContaining({ name: 'Supabase', skill: 'supabase-database' })]));
|
|
411
|
+
expect(data.bindings.cms.entries).toEqual(expect.arrayContaining([expect.objectContaining({ name: 'Sanity', skill: 'sanity-cms' })]));
|
|
412
|
+
});
|
|
413
|
+
it('leaves skill-matrix.json database/cms slots empty when none selected', async () => {
|
|
414
|
+
const adapter = await IDE_ADAPTERS['vscode']();
|
|
415
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_EMPTY, EMPTY_REPO_INFO);
|
|
416
|
+
const skillMatrix = await readFile(join(tempDir, '.github', 'customizations', 'agents', 'skill-matrix.json'), 'utf8');
|
|
417
|
+
const data = JSON.parse(skillMatrix);
|
|
418
|
+
expect(data.bindings.database.entries).toEqual([]);
|
|
419
|
+
expect(data.bindings.cms.entries).toEqual([]);
|
|
420
|
+
});
|
|
421
|
+
it('getManagedPaths returns expected structure', async () => {
|
|
422
|
+
const adapter = await IDE_ADAPTERS['vscode']();
|
|
423
|
+
const paths = adapter.getManagedPaths();
|
|
424
|
+
expect(paths.framework).toContain('.github/copilot-instructions.md');
|
|
425
|
+
expect(paths.framework).toContain('.github/agents/');
|
|
426
|
+
expect(paths.framework).toContain('.github/instructions/');
|
|
427
|
+
expect(paths.framework).toContain('.github/skills/');
|
|
428
|
+
expect(paths.framework).toContain('.github/agent-workflows/');
|
|
429
|
+
expect(paths.framework).toContain('.github/prompts/');
|
|
430
|
+
expect(paths.customizable).toContain('.github/customizations/');
|
|
431
|
+
expect(paths.customizable).toContain('.vscode/mcp.json');
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
// ═══════════════════════════════════════════════════════════════
|
|
435
|
+
// § 4 Cursor Adapter — .mdc Conversion & Format Validation
|
|
436
|
+
// ═══════════════════════════════════════════════════════════════
|
|
437
|
+
describe('Cursor adapter install', () => {
|
|
438
|
+
let tempDir;
|
|
439
|
+
beforeEach(async () => {
|
|
440
|
+
tempDir = await mkdtemp(join(tmpdir(), 'opencastle-cursor-test-'));
|
|
441
|
+
});
|
|
442
|
+
afterEach(async () => {
|
|
443
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
444
|
+
});
|
|
445
|
+
it('creates .cursorrules with intro text', async () => {
|
|
446
|
+
const adapter = await IDE_ADAPTERS['cursor']();
|
|
447
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_EMPTY, EMPTY_REPO_INFO);
|
|
448
|
+
const content = await readFile(join(tempDir, '.cursorrules'), 'utf8');
|
|
449
|
+
expect(content).toContain('# Project Instructions');
|
|
450
|
+
expect(content).toContain('.cursor/rules/');
|
|
451
|
+
});
|
|
452
|
+
it('converts instruction files to .mdc with alwaysApply: true', async () => {
|
|
453
|
+
const adapter = await IDE_ADAPTERS['cursor']();
|
|
454
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_EMPTY, EMPTY_REPO_INFO);
|
|
455
|
+
const rulesDir = join(tempDir, '.cursor', 'rules');
|
|
456
|
+
const generalMdc = await readFile(join(rulesDir, 'general.mdc'), 'utf8');
|
|
457
|
+
// Should have .mdc frontmatter
|
|
458
|
+
expect(generalMdc).toMatch(/^---\n/);
|
|
459
|
+
expect(generalMdc).toContain('alwaysApply: true');
|
|
460
|
+
// Should still contain the original body content
|
|
461
|
+
expect(generalMdc).toContain('Coding Standards');
|
|
462
|
+
});
|
|
463
|
+
it('converts agent files to .mdc in agents/ subdirectory', async () => {
|
|
464
|
+
const adapter = await IDE_ADAPTERS['cursor']();
|
|
465
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_EMPTY, EMPTY_REPO_INFO);
|
|
466
|
+
const agentsDir = join(tempDir, '.cursor', 'rules', 'agents');
|
|
467
|
+
const agents = await readdir(agentsDir);
|
|
468
|
+
// .agent.md → .mdc
|
|
469
|
+
expect(agents).toContain('developer.mdc');
|
|
470
|
+
expect(agents).toContain('team-lead.mdc');
|
|
471
|
+
expect(agents).not.toContain('content-engineer.mdc'); // no CMS
|
|
472
|
+
expect(agents).not.toContain('database-engineer.mdc'); // no DB
|
|
473
|
+
// Validate .mdc structure
|
|
474
|
+
const devAgent = await readFile(join(agentsDir, 'developer.mdc'), 'utf8');
|
|
475
|
+
expect(devAgent).toMatch(/^---\n/);
|
|
476
|
+
expect(devAgent).toContain('description:');
|
|
477
|
+
});
|
|
478
|
+
it('includes content-engineer.mdc when CMS selected', async () => {
|
|
479
|
+
const adapter = await IDE_ADAPTERS['cursor']();
|
|
480
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_SANITY_LINEAR, EMPTY_REPO_INFO);
|
|
481
|
+
const agents = await readdir(join(tempDir, '.cursor', 'rules', 'agents'));
|
|
482
|
+
expect(agents).toContain('content-engineer.mdc');
|
|
483
|
+
});
|
|
484
|
+
it('converts skills to .mdc in skills/ subdirectory', async () => {
|
|
485
|
+
const adapter = await IDE_ADAPTERS['cursor']();
|
|
486
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_SANITY_LINEAR, EMPTY_REPO_INFO);
|
|
487
|
+
const skillsDir = join(tempDir, '.cursor', 'rules', 'skills');
|
|
488
|
+
const skills = await readdir(skillsDir);
|
|
489
|
+
// Core skills should be present
|
|
490
|
+
expect(skills).toContain('self-improvement.mdc');
|
|
491
|
+
expect(skills).toContain('testing-workflow.mdc');
|
|
492
|
+
// Selected plugin skills as .mdc
|
|
493
|
+
expect(skills).toContain('sanity.mdc');
|
|
494
|
+
expect(skills).toContain('linear.mdc');
|
|
495
|
+
// Unselected plugin skills should not be present
|
|
496
|
+
expect(skills).not.toContain('supabase.mdc');
|
|
497
|
+
expect(skills).not.toContain('slack.mdc');
|
|
498
|
+
});
|
|
499
|
+
it('generates Cursor MCP config with mcpServers format', async () => {
|
|
500
|
+
const adapter = await IDE_ADAPTERS['cursor']();
|
|
501
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_SANITY_LINEAR, EMPTY_REPO_INFO);
|
|
502
|
+
const mcpConfig = await readJson(join(tempDir, '.cursor', 'mcp.json'));
|
|
503
|
+
// Cursor format uses "mcpServers" key (not "servers")
|
|
504
|
+
expect(mcpConfig).toHaveProperty('mcpServers');
|
|
505
|
+
expect(mcpConfig).not.toHaveProperty('servers');
|
|
506
|
+
const servers = mcpConfig.mcpServers;
|
|
507
|
+
// HTTP servers get url only (no type field)
|
|
508
|
+
expect(servers.Sanity).toBeDefined();
|
|
509
|
+
expect(servers.Sanity.url).toBe('https://mcp.sanity.io');
|
|
510
|
+
expect(servers.Sanity).not.toHaveProperty('type');
|
|
511
|
+
// stdio servers get command + args (no type field)
|
|
512
|
+
expect(servers.Linear).toBeDefined();
|
|
513
|
+
expect(servers.Linear.command).toBe('npx');
|
|
514
|
+
expect(servers.Linear.args).toContain('@mseep/linear-mcp');
|
|
515
|
+
expect(servers.Linear).not.toHaveProperty('type');
|
|
516
|
+
});
|
|
517
|
+
it('getManagedPaths returns expected Cursor paths', async () => {
|
|
518
|
+
const adapter = await IDE_ADAPTERS['cursor']();
|
|
519
|
+
const paths = adapter.getManagedPaths();
|
|
520
|
+
expect(paths.framework).toContain('.cursorrules');
|
|
521
|
+
expect(paths.framework).toContain('.cursor/rules/agents/');
|
|
522
|
+
expect(paths.framework).toContain('.cursor/rules/skills/');
|
|
523
|
+
expect(paths.framework).toContain('.cursor/rules/general.mdc');
|
|
524
|
+
expect(paths.framework).toContain('.cursor/rules/ai-optimization.mdc');
|
|
525
|
+
expect(paths.customizable).toContain('.cursor/rules/customizations/');
|
|
526
|
+
expect(paths.customizable).toContain('.cursor/mcp.json');
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
// ═══════════════════════════════════════════════════════════════
|
|
530
|
+
// § 5 Claude Code Adapter — Single-File Root Document
|
|
531
|
+
// ═══════════════════════════════════════════════════════════════
|
|
532
|
+
describe('Claude Code adapter install', () => {
|
|
533
|
+
let tempDir;
|
|
534
|
+
beforeEach(async () => {
|
|
535
|
+
tempDir = await mkdtemp(join(tmpdir(), 'opencastle-claude-test-'));
|
|
536
|
+
});
|
|
537
|
+
afterEach(async () => {
|
|
538
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
539
|
+
});
|
|
540
|
+
it('creates CLAUDE.md with embedded instructions', async () => {
|
|
541
|
+
const adapter = await IDE_ADAPTERS['claude-code']();
|
|
542
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_EMPTY, EMPTY_REPO_INFO);
|
|
543
|
+
expect(existsSync(join(tempDir, 'CLAUDE.md'))).toBe(true);
|
|
544
|
+
const content = await readFile(join(tempDir, 'CLAUDE.md'), 'utf8');
|
|
545
|
+
// Should contain merged instruction content
|
|
546
|
+
expect(content).toContain('# Project Instructions');
|
|
547
|
+
expect(content).toContain('Coding Standards');
|
|
548
|
+
expect(content).toContain('.claude/skills/');
|
|
549
|
+
expect(content).toContain('.claude/agents/');
|
|
550
|
+
});
|
|
551
|
+
it('CLAUDE.md lists all non-excluded agents', async () => {
|
|
552
|
+
const adapter = await IDE_ADAPTERS['claude-code']();
|
|
553
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_EMPTY, EMPTY_REPO_INFO);
|
|
554
|
+
const content = await readFile(join(tempDir, 'CLAUDE.md'), 'utf8');
|
|
555
|
+
expect(content).toContain('## Agent Definitions');
|
|
556
|
+
expect(content).toContain('**Developer**');
|
|
557
|
+
expect(content).toContain('**Team Lead**');
|
|
558
|
+
// Should NOT list excluded agents
|
|
559
|
+
expect(content).not.toContain('**Content Engineer**');
|
|
560
|
+
expect(content).not.toContain('**Database Engineer**');
|
|
561
|
+
});
|
|
562
|
+
it('CLAUDE.md includes content engineer when CMS selected', async () => {
|
|
563
|
+
const adapter = await IDE_ADAPTERS['claude-code']();
|
|
564
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_SANITY_LINEAR, EMPTY_REPO_INFO);
|
|
565
|
+
const content = await readFile(join(tempDir, 'CLAUDE.md'), 'utf8');
|
|
566
|
+
expect(content).toContain('**Content Engineer**');
|
|
567
|
+
expect(content).not.toContain('**Database Engineer**');
|
|
568
|
+
});
|
|
569
|
+
it('CLAUDE.md lists available skills (including selected plugins)', async () => {
|
|
570
|
+
const adapter = await IDE_ADAPTERS['claude-code']();
|
|
571
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_SANITY_LINEAR, EMPTY_REPO_INFO);
|
|
572
|
+
const content = await readFile(join(tempDir, 'CLAUDE.md'), 'utf8');
|
|
573
|
+
expect(content).toContain('## Available Skills');
|
|
574
|
+
expect(content).toContain('**self-improvement**');
|
|
575
|
+
expect(content).toContain('**sanity**');
|
|
576
|
+
expect(content).toContain('**linear**');
|
|
577
|
+
// Unselected plugin skills should NOT appear in skill index
|
|
578
|
+
expect(content).not.toMatch(/\*\*supabase\*\*/);
|
|
579
|
+
});
|
|
580
|
+
it('strips frontmatter from agent files in .claude/agents/', async () => {
|
|
581
|
+
const adapter = await IDE_ADAPTERS['claude-code']();
|
|
582
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_EMPTY, EMPTY_REPO_INFO);
|
|
583
|
+
const agentsDir = join(tempDir, '.claude', 'agents');
|
|
584
|
+
const agents = await readdir(agentsDir);
|
|
585
|
+
expect(agents).toContain('developer.agent.md');
|
|
586
|
+
expect(agents).not.toContain('content-engineer.agent.md');
|
|
587
|
+
expect(agents).not.toContain('database-engineer.agent.md');
|
|
588
|
+
const devAgent = await readFile(join(agentsDir, 'developer.agent.md'), 'utf8');
|
|
589
|
+
// Should NOT start with frontmatter
|
|
590
|
+
expect(devAgent).not.toMatch(/^---\n/);
|
|
591
|
+
// Should contain the body content (starts with comment or heading)
|
|
592
|
+
expect(devAgent).toContain('Developer');
|
|
593
|
+
});
|
|
594
|
+
it('creates skills as flat .md files stripped of frontmatter', async () => {
|
|
595
|
+
const adapter = await IDE_ADAPTERS['claude-code']();
|
|
596
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_SANITY_LINEAR, EMPTY_REPO_INFO);
|
|
597
|
+
const skillsDir = join(tempDir, '.claude', 'skills');
|
|
598
|
+
const skills = await readdir(skillsDir);
|
|
599
|
+
expect(skills).toContain('self-improvement.md');
|
|
600
|
+
expect(skills).toContain('sanity.md');
|
|
601
|
+
expect(skills).toContain('linear.md');
|
|
602
|
+
expect(skills).not.toContain('supabase.md');
|
|
603
|
+
// Verify frontmatter is stripped
|
|
604
|
+
const skillContent = await readFile(join(skillsDir, 'self-improvement.md'), 'utf8');
|
|
605
|
+
expect(skillContent).not.toMatch(/^---\n/);
|
|
606
|
+
});
|
|
607
|
+
it('generates Claude Code MCP config with mcpServers format', async () => {
|
|
608
|
+
const adapter = await IDE_ADAPTERS['claude-code']();
|
|
609
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_SANITY_LINEAR, EMPTY_REPO_INFO);
|
|
610
|
+
const mcpConfig = await readJson(join(tempDir, '.claude', 'mcp.json'));
|
|
611
|
+
expect(mcpConfig).toHaveProperty('mcpServers');
|
|
612
|
+
expect(mcpConfig).not.toHaveProperty('servers');
|
|
613
|
+
});
|
|
614
|
+
it('creates prompts in .claude/commands/', async () => {
|
|
615
|
+
const adapter = await IDE_ADAPTERS['claude-code']();
|
|
616
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_EMPTY, EMPTY_REPO_INFO);
|
|
617
|
+
const commandsDir = join(tempDir, '.claude', 'commands');
|
|
618
|
+
expect(existsSync(commandsDir)).toBe(true);
|
|
619
|
+
const commands = await readdir(commandsDir);
|
|
620
|
+
// Should have prompt files
|
|
621
|
+
expect(commands.length).toBeGreaterThan(0);
|
|
622
|
+
// All should be .md files
|
|
623
|
+
expect(commands.every((f) => f.endsWith('.md'))).toBe(true);
|
|
624
|
+
});
|
|
625
|
+
it('creates workflows as commands with workflow- prefix', async () => {
|
|
626
|
+
const adapter = await IDE_ADAPTERS['claude-code']();
|
|
627
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_EMPTY, EMPTY_REPO_INFO);
|
|
628
|
+
const commandsDir = join(tempDir, '.claude', 'commands');
|
|
629
|
+
const commands = await readdir(commandsDir);
|
|
630
|
+
// Workflow files should have the "workflow-" prefix
|
|
631
|
+
const workflows = commands.filter((f) => f.startsWith('workflow-'));
|
|
632
|
+
expect(workflows.length).toBeGreaterThan(0);
|
|
633
|
+
});
|
|
634
|
+
it('getManagedPaths includes CLAUDE.md and .claude dirs', async () => {
|
|
635
|
+
const adapter = await IDE_ADAPTERS['claude-code']();
|
|
636
|
+
const paths = adapter.getManagedPaths();
|
|
637
|
+
expect(paths.framework).toContain('CLAUDE.md');
|
|
638
|
+
expect(paths.framework).toContain('.claude/agents/');
|
|
639
|
+
expect(paths.framework).toContain('.claude/skills/');
|
|
640
|
+
expect(paths.framework).toContain('.claude/commands/');
|
|
641
|
+
expect(paths.customizable).toContain('.claude/customizations/');
|
|
642
|
+
expect(paths.customizable).toContain('.claude/mcp.json');
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
// ═══════════════════════════════════════════════════════════════
|
|
646
|
+
// § 6 OpenCode Adapter — Single-File Root Document
|
|
647
|
+
// ═══════════════════════════════════════════════════════════════
|
|
648
|
+
describe('OpenCode adapter install', () => {
|
|
649
|
+
let tempDir;
|
|
650
|
+
beforeEach(async () => {
|
|
651
|
+
tempDir = await mkdtemp(join(tmpdir(), 'opencastle-opencode-test-'));
|
|
652
|
+
});
|
|
653
|
+
afterEach(async () => {
|
|
654
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
655
|
+
});
|
|
656
|
+
it('creates AGENTS.md with embedded instructions', async () => {
|
|
657
|
+
const adapter = await IDE_ADAPTERS['opencode']();
|
|
658
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_EMPTY, EMPTY_REPO_INFO);
|
|
659
|
+
expect(existsSync(join(tempDir, 'AGENTS.md'))).toBe(true);
|
|
660
|
+
const content = await readFile(join(tempDir, 'AGENTS.md'), 'utf8');
|
|
661
|
+
expect(content).toContain('# Project Instructions');
|
|
662
|
+
expect(content).toContain('.opencode/skills/');
|
|
663
|
+
expect(content).toContain('.opencode/agents/');
|
|
664
|
+
});
|
|
665
|
+
it('creates files in .opencode/ directory structure', async () => {
|
|
666
|
+
const adapter = await IDE_ADAPTERS['opencode']();
|
|
667
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_SANITY_LINEAR, EMPTY_REPO_INFO);
|
|
668
|
+
expect(existsSync(join(tempDir, '.opencode', 'agents'))).toBe(true);
|
|
669
|
+
expect(existsSync(join(tempDir, '.opencode', 'skills'))).toBe(true);
|
|
670
|
+
expect(existsSync(join(tempDir, '.opencode', 'prompts'))).toBe(true);
|
|
671
|
+
expect(existsSync(join(tempDir, '.opencode', 'workflows'))).toBe(true);
|
|
672
|
+
expect(existsSync(join(tempDir, '.opencode', 'customizations'))).toBe(true);
|
|
673
|
+
});
|
|
674
|
+
it('generates OpenCode MCP config with mcp format', async () => {
|
|
675
|
+
const adapter = await IDE_ADAPTERS['opencode']();
|
|
676
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_SANITY_LINEAR, EMPTY_REPO_INFO);
|
|
677
|
+
const mcpConfig = await readJson(join(tempDir, 'opencode.json'));
|
|
678
|
+
// OpenCode format uses "mcp" key
|
|
679
|
+
expect(mcpConfig).toHaveProperty('mcp');
|
|
680
|
+
expect(mcpConfig).not.toHaveProperty('servers');
|
|
681
|
+
expect(mcpConfig).not.toHaveProperty('mcpServers');
|
|
682
|
+
const mcp = mcpConfig.mcp;
|
|
683
|
+
// HTTP servers → type: 'remote'
|
|
684
|
+
expect(mcp.Sanity).toBeDefined();
|
|
685
|
+
expect(mcp.Sanity.type).toBe('remote');
|
|
686
|
+
expect(mcp.Sanity.url).toBe('https://mcp.sanity.io');
|
|
687
|
+
// stdio servers → type: 'local', command as array
|
|
688
|
+
expect(mcp.Linear).toBeDefined();
|
|
689
|
+
expect(mcp.Linear.type).toBe('local');
|
|
690
|
+
expect(mcp.Linear.command).toEqual(['npx', '-y', '@mseep/linear-mcp']);
|
|
691
|
+
});
|
|
692
|
+
it('workflows do NOT have prefix in opencode adapter', async () => {
|
|
693
|
+
const adapter = await IDE_ADAPTERS['opencode']();
|
|
694
|
+
await adapter.install(PKG_ROOT, tempDir, STACK_EMPTY, EMPTY_REPO_INFO);
|
|
695
|
+
const wfDir = join(tempDir, '.opencode', 'workflows');
|
|
696
|
+
if (existsSync(wfDir)) {
|
|
697
|
+
const workflows = await readdir(wfDir);
|
|
698
|
+
// OpenCode config has workflowPrefix: '' — no prefix
|
|
699
|
+
const prefixed = workflows.filter((f) => f.startsWith('workflow-'));
|
|
700
|
+
expect(prefixed).toHaveLength(0);
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
it('getManagedPaths includes AGENTS.md and .opencode dirs', async () => {
|
|
704
|
+
const adapter = await IDE_ADAPTERS['opencode']();
|
|
705
|
+
const paths = adapter.getManagedPaths();
|
|
706
|
+
expect(paths.framework).toContain('AGENTS.md');
|
|
707
|
+
expect(paths.framework).toContain('.opencode/agents/');
|
|
708
|
+
expect(paths.framework).toContain('.opencode/skills/');
|
|
709
|
+
expect(paths.framework).toContain('.opencode/prompts/');
|
|
710
|
+
expect(paths.framework).toContain('.opencode/workflows/');
|
|
711
|
+
expect(paths.customizable).toContain('.opencode/customizations/');
|
|
712
|
+
expect(paths.customizable).toContain('opencode.json');
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
// ═══════════════════════════════════════════════════════════════
|
|
716
|
+
// § 7 Cross-Adapter MCP Format Consistency
|
|
717
|
+
// ═══════════════════════════════════════════════════════════════
|
|
718
|
+
describe('MCP config format per IDE', () => {
|
|
719
|
+
let tempDir;
|
|
720
|
+
beforeEach(async () => {
|
|
721
|
+
tempDir = await mkdtemp(join(tmpdir(), 'opencastle-mcp-test-'));
|
|
722
|
+
});
|
|
723
|
+
afterEach(async () => {
|
|
724
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
725
|
+
});
|
|
726
|
+
const stack = STACK_SANITY_LINEAR;
|
|
727
|
+
it('all IDEs include the same MCP servers (same tools = same servers)', async () => {
|
|
728
|
+
const serversByIde = {};
|
|
729
|
+
for (const ideId of ['vscode', 'cursor', 'claude-code', 'opencode']) {
|
|
730
|
+
const dir = await mkdtemp(join(tmpdir(), `opencastle-mcp-${ideId}-`));
|
|
731
|
+
try {
|
|
732
|
+
const adapter = await IDE_ADAPTERS[ideId]();
|
|
733
|
+
await adapter.install(PKG_ROOT, dir, stack, EMPTY_REPO_INFO);
|
|
734
|
+
const paths = {
|
|
735
|
+
vscode: join(dir, '.vscode', 'mcp.json'),
|
|
736
|
+
cursor: join(dir, '.cursor', 'mcp.json'),
|
|
737
|
+
'claude-code': join(dir, '.claude', 'mcp.json'),
|
|
738
|
+
opencode: join(dir, 'opencode.json'),
|
|
739
|
+
};
|
|
740
|
+
const config = await readJson(paths[ideId]);
|
|
741
|
+
const containerKey = ideId === 'opencode' ? 'mcp' :
|
|
742
|
+
ideId === 'vscode' ? 'servers' :
|
|
743
|
+
'mcpServers';
|
|
744
|
+
const servers = config[containerKey];
|
|
745
|
+
serversByIde[ideId] = Object.keys(servers).sort();
|
|
746
|
+
}
|
|
747
|
+
finally {
|
|
748
|
+
await rm(dir, { recursive: true, force: true });
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
// All IDEs should have the same server names
|
|
752
|
+
const vsCodeServers = serversByIde['vscode'];
|
|
753
|
+
expect(serversByIde['cursor']).toEqual(vsCodeServers);
|
|
754
|
+
expect(serversByIde['claude-code']).toEqual(vsCodeServers);
|
|
755
|
+
expect(serversByIde['opencode']).toEqual(vsCodeServers);
|
|
756
|
+
});
|
|
757
|
+
});
|
|
758
|
+
// ═══════════════════════════════════════════════════════════════
|
|
759
|
+
// § 8 Cross-Adapter Agent/Skill Parity
|
|
760
|
+
// ═══════════════════════════════════════════════════════════════
|
|
761
|
+
describe('agent and skill parity across adapters', () => {
|
|
762
|
+
let tempDir;
|
|
763
|
+
beforeEach(async () => {
|
|
764
|
+
tempDir = await mkdtemp(join(tmpdir(), 'opencastle-parity-test-'));
|
|
765
|
+
});
|
|
766
|
+
afterEach(async () => {
|
|
767
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
768
|
+
});
|
|
769
|
+
it('all IDEs install the same number of agents for a given stack', async () => {
|
|
770
|
+
const agentCountByIde = {};
|
|
771
|
+
for (const ideId of ['vscode', 'cursor', 'claude-code', 'opencode']) {
|
|
772
|
+
const dir = await mkdtemp(join(tmpdir(), `opencastle-parity-${ideId}-`));
|
|
773
|
+
try {
|
|
774
|
+
const adapter = await IDE_ADAPTERS[ideId]();
|
|
775
|
+
await adapter.install(PKG_ROOT, dir, STACK_SANITY_LINEAR, EMPTY_REPO_INFO);
|
|
776
|
+
const agentPaths = {
|
|
777
|
+
vscode: join(dir, '.github', 'agents'),
|
|
778
|
+
cursor: join(dir, '.cursor', 'rules', 'agents'),
|
|
779
|
+
'claude-code': join(dir, '.claude', 'agents'),
|
|
780
|
+
opencode: join(dir, '.opencode', 'agents'),
|
|
781
|
+
};
|
|
782
|
+
const agents = await readdir(agentPaths[ideId]);
|
|
783
|
+
agentCountByIde[ideId] = agents.length;
|
|
784
|
+
}
|
|
785
|
+
finally {
|
|
786
|
+
await rm(dir, { recursive: true, force: true });
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
// All IDEs should have the same agent count
|
|
790
|
+
const vscodeCount = agentCountByIde['vscode'];
|
|
791
|
+
expect(agentCountByIde['cursor']).toBe(vscodeCount);
|
|
792
|
+
expect(agentCountByIde['claude-code']).toBe(vscodeCount);
|
|
793
|
+
expect(agentCountByIde['opencode']).toBe(vscodeCount);
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
// ═══════════════════════════════════════════════════════════════
|
|
797
|
+
// § 9 Idempotency — Re-install Does Not Duplicate
|
|
798
|
+
// ═══════════════════════════════════════════════════════════════
|
|
799
|
+
describe('install idempotency', () => {
|
|
800
|
+
let tempDir;
|
|
801
|
+
beforeEach(async () => {
|
|
802
|
+
tempDir = await mkdtemp(join(tmpdir(), 'opencastle-idempotent-test-'));
|
|
803
|
+
});
|
|
804
|
+
afterEach(async () => {
|
|
805
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
806
|
+
});
|
|
807
|
+
it('second install skips already-existing files (scaffold-once semantics)', async () => {
|
|
808
|
+
const adapter = await IDE_ADAPTERS['vscode']();
|
|
809
|
+
// First install
|
|
810
|
+
const firstResult = await adapter.install(PKG_ROOT, tempDir, STACK_SANITY_LINEAR, EMPTY_REPO_INFO);
|
|
811
|
+
expect(firstResult.created.length).toBeGreaterThan(0);
|
|
812
|
+
// Second install — same stack
|
|
813
|
+
const secondResult = await adapter.install(PKG_ROOT, tempDir, STACK_SANITY_LINEAR, EMPTY_REPO_INFO);
|
|
814
|
+
// Created files should now be skipped
|
|
815
|
+
expect(secondResult.created.length).toBe(0);
|
|
816
|
+
expect(secondResult.skipped.length).toBeGreaterThan(0);
|
|
817
|
+
});
|
|
818
|
+
});
|
|
819
|
+
// ═══════════════════════════════════════════════════════════════
|
|
820
|
+
// § 10 Full Stack — Complex Configuration
|
|
821
|
+
// ═══════════════════════════════════════════════════════════════
|
|
822
|
+
describe('full stack configuration', () => {
|
|
823
|
+
let tempDir;
|
|
824
|
+
beforeEach(async () => {
|
|
825
|
+
tempDir = await mkdtemp(join(tmpdir(), 'opencastle-fullstack-test-'));
|
|
826
|
+
});
|
|
827
|
+
afterEach(async () => {
|
|
828
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
829
|
+
});
|
|
830
|
+
it('installs with sanity + supabase + vercel + linear + slack', async () => {
|
|
831
|
+
const stack = {
|
|
832
|
+
ides: ['vscode'],
|
|
833
|
+
techTools: ['sanity', 'supabase', 'vercel'],
|
|
834
|
+
teamTools: ['linear', 'slack'],
|
|
835
|
+
};
|
|
836
|
+
const adapter = await IDE_ADAPTERS['vscode']();
|
|
837
|
+
const result = await adapter.install(PKG_ROOT, tempDir, stack, EMPTY_REPO_INFO);
|
|
838
|
+
expect(result.created.length).toBeGreaterThan(0);
|
|
839
|
+
// Both conditional agents should be included
|
|
840
|
+
const agents = await readdir(join(tempDir, '.github', 'agents'));
|
|
841
|
+
expect(agents).toContain('content-engineer.agent.md');
|
|
842
|
+
expect(agents).toContain('database-engineer.agent.md');
|
|
843
|
+
// All 5 plugin skills should be installed
|
|
844
|
+
const skills = await readdir(join(tempDir, '.github', 'skills'));
|
|
845
|
+
expect(skills).toContain('sanity');
|
|
846
|
+
expect(skills).toContain('supabase');
|
|
847
|
+
expect(skills).toContain('vercel');
|
|
848
|
+
expect(skills).toContain('linear');
|
|
849
|
+
expect(skills).toContain('slack');
|
|
850
|
+
// MCP config should have all 5 servers
|
|
851
|
+
const mcpConfig = await readJson(join(tempDir, '.vscode', 'mcp.json'));
|
|
852
|
+
const servers = mcpConfig.servers;
|
|
853
|
+
expect(Object.keys(servers).sort()).toEqual(['Linear', 'Sanity', 'Slack', 'Supabase', 'Vercel'].sort());
|
|
854
|
+
// Agent tool injection — content-engineer should have sanity tools
|
|
855
|
+
const ceContent = await readFile(join(tempDir, '.github', 'agents', 'content-engineer.agent.md'), 'utf8');
|
|
856
|
+
expect(ceContent).toContain("'sanity/get_schema'");
|
|
857
|
+
// Agent tool injection — database-engineer should have supabase tools
|
|
858
|
+
const deContent = await readFile(join(tempDir, '.github', 'agents', 'database-engineer.agent.md'), 'utf8');
|
|
859
|
+
expect(deContent).toContain("'supabase/apply_migration'");
|
|
860
|
+
// Skill matrix should be filled
|
|
861
|
+
const skillMatrix = await readFile(join(tempDir, '.github', 'customizations', 'agents', 'skill-matrix.json'), 'utf8');
|
|
862
|
+
const matrixData = JSON.parse(skillMatrix);
|
|
863
|
+
expect(matrixData.bindings.database.entries).toEqual(expect.arrayContaining([expect.objectContaining({ name: 'Supabase', skill: 'supabase-database' })]));
|
|
864
|
+
expect(matrixData.bindings.cms.entries).toEqual(expect.arrayContaining([expect.objectContaining({ name: 'Sanity', skill: 'sanity-cms' })]));
|
|
865
|
+
});
|
|
866
|
+
it('auto-detected vercel in repoInfo adds Vercel MCP server without explicit selection', async () => {
|
|
867
|
+
const stack = {
|
|
868
|
+
ides: ['vscode'],
|
|
869
|
+
techTools: ['sanity'],
|
|
870
|
+
teamTools: [],
|
|
871
|
+
};
|
|
872
|
+
const repoInfo = { deployment: ['vercel'] };
|
|
873
|
+
const adapter = await IDE_ADAPTERS['vscode']();
|
|
874
|
+
await adapter.install(PKG_ROOT, tempDir, stack, repoInfo);
|
|
875
|
+
const mcpConfig = await readJson(join(tempDir, '.vscode', 'mcp.json'));
|
|
876
|
+
const servers = mcpConfig.servers;
|
|
877
|
+
expect(servers).toHaveProperty('Vercel');
|
|
878
|
+
expect(servers).toHaveProperty('Sanity');
|
|
879
|
+
});
|
|
880
|
+
});
|
|
881
|
+
//# sourceMappingURL=init.test.js.map
|