opencastle 0.7.0 → 0.8.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.
Files changed (93) hide show
  1. package/README.md +30 -3
  2. package/bin/cli.mjs +2 -0
  3. package/dist/cli/adapters/claude-code.d.ts +2 -5
  4. package/dist/cli/adapters/claude-code.d.ts.map +1 -1
  5. package/dist/cli/adapters/claude-code.js +12 -251
  6. package/dist/cli/adapters/claude-code.js.map +1 -1
  7. package/dist/cli/adapters/cursor.d.ts.map +1 -1
  8. package/dist/cli/adapters/cursor.js +3 -17
  9. package/dist/cli/adapters/cursor.js.map +1 -1
  10. package/dist/cli/adapters/frontmatter.d.ts +26 -0
  11. package/dist/cli/adapters/frontmatter.d.ts.map +1 -0
  12. package/dist/cli/adapters/frontmatter.js +40 -0
  13. package/dist/cli/adapters/frontmatter.js.map +1 -0
  14. package/dist/cli/adapters/index.d.ts +5 -0
  15. package/dist/cli/adapters/index.d.ts.map +1 -0
  16. package/dist/cli/adapters/index.js +9 -0
  17. package/dist/cli/adapters/index.js.map +1 -0
  18. package/dist/cli/adapters/opencode.d.ts +2 -5
  19. package/dist/cli/adapters/opencode.d.ts.map +1 -1
  20. package/dist/cli/adapters/opencode.js +12 -250
  21. package/dist/cli/adapters/opencode.js.map +1 -1
  22. package/dist/cli/adapters/single-file-base.d.ts +40 -0
  23. package/dist/cli/adapters/single-file-base.d.ts.map +1 -0
  24. package/dist/cli/adapters/single-file-base.js +246 -0
  25. package/dist/cli/adapters/single-file-base.js.map +1 -0
  26. package/dist/cli/dashboard.d.ts.map +1 -1
  27. package/dist/cli/dashboard.js +3 -2
  28. package/dist/cli/dashboard.js.map +1 -1
  29. package/dist/cli/detect.d.ts.map +1 -1
  30. package/dist/cli/detect.js +13 -11
  31. package/dist/cli/detect.js.map +1 -1
  32. package/dist/cli/doctor.d.ts +3 -0
  33. package/dist/cli/doctor.d.ts.map +1 -0
  34. package/dist/cli/doctor.js +205 -0
  35. package/dist/cli/doctor.js.map +1 -0
  36. package/dist/cli/init.d.ts.map +1 -1
  37. package/dist/cli/init.js +31 -19
  38. package/dist/cli/init.js.map +1 -1
  39. package/dist/cli/run/schema.d.ts +1 -5
  40. package/dist/cli/run/schema.d.ts.map +1 -1
  41. package/dist/cli/run/schema.js +6 -330
  42. package/dist/cli/run/schema.js.map +1 -1
  43. package/dist/cli/run.d.ts.map +1 -1
  44. package/dist/cli/run.js +14 -1
  45. package/dist/cli/run.js.map +1 -1
  46. package/dist/cli/types.d.ts +0 -5
  47. package/dist/cli/types.d.ts.map +1 -1
  48. package/dist/cli/update.d.ts.map +1 -1
  49. package/dist/cli/update.js +4 -17
  50. package/dist/cli/update.js.map +1 -1
  51. package/package.json +7 -2
  52. package/src/cli/adapters/claude-code.ts +13 -304
  53. package/src/cli/adapters/cursor.ts +3 -23
  54. package/src/cli/adapters/frontmatter.ts +47 -0
  55. package/src/cli/adapters/index.ts +13 -0
  56. package/src/cli/adapters/opencode.ts +12 -301
  57. package/src/cli/adapters/single-file-base.ts +320 -0
  58. package/src/cli/dashboard.ts +3 -2
  59. package/src/cli/detect.ts +19 -15
  60. package/src/cli/doctor.ts +235 -0
  61. package/src/cli/init.ts +31 -24
  62. package/src/cli/run/schema.ts +7 -365
  63. package/src/cli/run.ts +17 -1
  64. package/src/cli/types.ts +0 -6
  65. package/src/cli/update.ts +5 -23
  66. package/src/dashboard/dist/_astro/{index.CWVzbF4T.css → index.Bnq19_1M.css} +1 -1
  67. package/src/dashboard/dist/index.html +170 -11
  68. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  69. package/src/dashboard/seed-data/reviews.ndjson +6 -0
  70. package/src/dashboard/src/pages/index.astro +213 -10
  71. package/src/dashboard/src/styles/dashboard.css +196 -0
  72. package/src/orchestrator/agent-workflows/bug-fix.md +2 -2
  73. package/src/orchestrator/agent-workflows/data-pipeline.md +8 -8
  74. package/src/orchestrator/agent-workflows/database-migration.md +2 -2
  75. package/src/orchestrator/agent-workflows/feature-implementation.md +12 -5
  76. package/src/orchestrator/agent-workflows/performance-optimization.md +2 -2
  77. package/src/orchestrator/agent-workflows/refactoring.md +2 -2
  78. package/src/orchestrator/agent-workflows/schema-changes.md +2 -2
  79. package/src/orchestrator/agent-workflows/security-audit.md +2 -2
  80. package/src/orchestrator/agents/data-expert.agent.md +2 -2
  81. package/src/orchestrator/agents/researcher.agent.md +0 -16
  82. package/src/orchestrator/agents/team-lead.agent.md +17 -6
  83. package/src/orchestrator/customizations/AGENT-PERFORMANCE.md +1 -3
  84. package/src/orchestrator/prompts/bootstrap-customizations.prompt.md +1 -1
  85. package/src/orchestrator/prompts/bug-fix.prompt.md +11 -6
  86. package/src/orchestrator/prompts/implement-feature.prompt.md +9 -4
  87. package/src/orchestrator/prompts/quick-refinement.prompt.md +9 -5
  88. package/src/orchestrator/prompts/resolve-pr-comments.prompt.md +18 -4
  89. package/src/orchestrator/skills/agent-hooks/SKILL.md +4 -2
  90. package/src/orchestrator/skills/fast-review/SKILL.md +15 -4
  91. package/src/orchestrator/skills/self-improvement/SKILL.md +1 -1
  92. package/src/orchestrator/skills/validation-gates/SKILL.md +152 -15
  93. package/src/orchestrator/prompts/metrics-report.prompt.md +0 -144
package/src/cli/detect.ts CHANGED
@@ -221,15 +221,17 @@ export async function detectRepoInfo(projectRoot: string): Promise<RepoInfo> {
221
221
  }
222
222
  }
223
223
 
224
- // ── 3. Detect by config files ───────────────────────────────
225
- await detectCategory(projectRoot, FRAMEWORKS, info, 'frameworks');
226
- await detectCategory(projectRoot, DATABASES, info, 'databases');
227
- await detectCategory(projectRoot, CMS_PLATFORMS, info, 'cms');
228
- await detectCategory(projectRoot, DEPLOYMENT, info, 'deployment');
229
- await detectCategory(projectRoot, TESTING, info, 'testing');
230
- await detectCategory(projectRoot, CICD, info, 'cicd');
231
- await detectCategory(projectRoot, STYLING, info, 'styling');
232
- await detectCategory(projectRoot, AUTH, info, 'auth');
224
+ // ── 3. Detect by config files (parallel) ─────────────────────
225
+ await Promise.all([
226
+ detectCategory(projectRoot, FRAMEWORKS, info, 'frameworks'),
227
+ detectCategory(projectRoot, DATABASES, info, 'databases'),
228
+ detectCategory(projectRoot, CMS_PLATFORMS, info, 'cms'),
229
+ detectCategory(projectRoot, DEPLOYMENT, info, 'deployment'),
230
+ detectCategory(projectRoot, TESTING, info, 'testing'),
231
+ detectCategory(projectRoot, CICD, info, 'cicd'),
232
+ detectCategory(projectRoot, STYLING, info, 'styling'),
233
+ detectCategory(projectRoot, AUTH, info, 'auth'),
234
+ ]);
233
235
 
234
236
  // ── 4. Detect from package.json deps ────────────────────────
235
237
  await detectFromPackageJson(projectRoot, info);
@@ -241,12 +243,14 @@ export async function detectRepoInfo(projectRoot: string): Promise<RepoInfo> {
241
243
  '.claude/mcp.json',
242
244
  'mcp.json',
243
245
  ];
244
- for (const p of mcpPaths) {
245
- if (await fileExists(resolve(projectRoot, p))) {
246
- info.mcpConfig = true;
247
- info.configFiles.push(p);
248
- }
249
- }
246
+ await Promise.all(
247
+ mcpPaths.map(async (p) => {
248
+ if (await fileExists(resolve(projectRoot, p))) {
249
+ info.mcpConfig = true;
250
+ info.configFiles.push(p);
251
+ }
252
+ })
253
+ );
250
254
 
251
255
  // ── 6. Check for TypeScript ─────────────────────────────────
252
256
  const tsConfigPath = resolve(projectRoot, 'tsconfig.json');
@@ -0,0 +1,235 @@
1
+ import { resolve } from 'node:path';
2
+ import { readFile, access, readdir } from 'node:fs/promises';
3
+ import { existsSync } from 'node:fs';
4
+ import { readManifest } from './manifest.js';
5
+ import { getRequiredMcpEnvVars } from './stack-config.js';
6
+ import type { CliContext, Manifest, IdeChoice } from './types.js';
7
+
8
+ // ── Styled output helpers ─────────────────────────────────────
9
+
10
+ const PASS = '\x1b[32m✓\x1b[0m';
11
+ const FAIL = '\x1b[31m✗\x1b[0m';
12
+ const WARN = '\x1b[33m!\x1b[0m';
13
+ const DIM = (s: string) => `\x1b[2m${s}\x1b[0m`;
14
+ const BOLD = (s: string) => `\x1b[1m${s}\x1b[0m`;
15
+
16
+ interface CheckResult {
17
+ ok: boolean;
18
+ label: string;
19
+ detail?: string;
20
+ warning?: boolean;
21
+ }
22
+
23
+ // ── Individual checks ─────────────────────────────────────────
24
+
25
+ function checkManifest(manifest: Manifest | null): CheckResult {
26
+ if (!manifest) {
27
+ return { ok: false, label: 'OpenCastle manifest (.opencastle.json)', detail: 'Not found. Run "npx opencastle init" first.' };
28
+ }
29
+ return { ok: true, label: 'OpenCastle manifest (.opencastle.json)', detail: `v${manifest.version}, IDE: ${manifest.ides?.join(', ') ?? manifest.ide}` };
30
+ }
31
+
32
+ async function checkCustomizations(projectRoot: string): Promise<CheckResult> {
33
+ const dir = resolve(projectRoot, '.github', 'customizations');
34
+ if (!existsSync(dir)) {
35
+ return { ok: false, label: 'Customizations directory', detail: '.github/customizations/ not found' };
36
+ }
37
+ const files = await readdir(dir).catch(() => []);
38
+ return { ok: true, label: 'Customizations directory', detail: `${files.length} entries` };
39
+ }
40
+
41
+ async function checkSkillMatrix(projectRoot: string): Promise<CheckResult> {
42
+ const path = resolve(projectRoot, '.github', 'customizations', 'agents', 'skill-matrix.md');
43
+ if (!existsSync(path)) {
44
+ return { ok: false, label: 'Skill matrix', detail: 'File not found at .github/customizations/agents/skill-matrix.md' };
45
+ }
46
+ const content = await readFile(path, 'utf8');
47
+ // Look for empty capability slots (pattern: | `domain` | | |)
48
+ const emptySlots = content.match(/\| `\w+`\s*\|\s*\|\s*\|/g);
49
+ if (emptySlots && emptySlots.length > 0) {
50
+ return { ok: true, label: 'Skill matrix', detail: `${emptySlots.length} unresolved capability slot(s)`, warning: true };
51
+ }
52
+ return { ok: true, label: 'Skill matrix', detail: 'All capability slots populated' };
53
+ }
54
+
55
+ async function checkInstructions(projectRoot: string): Promise<CheckResult> {
56
+ const dir = resolve(projectRoot, '.github', 'instructions');
57
+ if (!existsSync(dir)) {
58
+ return { ok: false, label: 'Instruction files', detail: '.github/instructions/ not found' };
59
+ }
60
+ const files = await readdir(dir).catch(() => []);
61
+ const mdFiles = files.filter((f) => f.endsWith('.md'));
62
+ if (mdFiles.length === 0) {
63
+ return { ok: false, label: 'Instruction files', detail: 'No .md files in .github/instructions/' };
64
+ }
65
+ return { ok: true, label: 'Instruction files', detail: `${mdFiles.length} instruction files` };
66
+ }
67
+
68
+ async function checkAgents(projectRoot: string): Promise<CheckResult> {
69
+ const dir = resolve(projectRoot, '.github', 'customizations', 'agents');
70
+ if (!existsSync(dir)) {
71
+ return { ok: false, label: 'Agent definitions', detail: 'agents/ directory not found in customizations' };
72
+ }
73
+ const files = await readdir(dir).catch(() => []);
74
+ const agentFiles = files.filter((f) => f.endsWith('.agent.md'));
75
+ if (agentFiles.length === 0) {
76
+ return { ok: false, label: 'Agent definitions', detail: 'No .agent.md files found' };
77
+ }
78
+ return { ok: true, label: 'Agent definitions', detail: `${agentFiles.length} agents` };
79
+ }
80
+
81
+ async function checkSkills(projectRoot: string): Promise<CheckResult> {
82
+ const dir = resolve(projectRoot, '.github', 'skills');
83
+ if (!existsSync(dir)) {
84
+ return { ok: false, label: 'Skills directory', detail: '.github/skills/ not found' };
85
+ }
86
+ const entries = await readdir(dir).catch(() => []);
87
+ return { ok: true, label: 'Skills directory', detail: `${entries.length} skills` };
88
+ }
89
+
90
+ async function checkLogs(projectRoot: string): Promise<CheckResult> {
91
+ const dir = resolve(projectRoot, '.github', 'customizations', 'logs');
92
+ if (!existsSync(dir)) {
93
+ return { ok: false, label: 'Observability logs', detail: 'logs/ directory not found — dashboard will be empty' };
94
+ }
95
+ const required = ['sessions.ndjson', 'delegations.ndjson', 'reviews.ndjson', 'panels.ndjson', 'disputes.ndjson'];
96
+ const missing = required.filter((f) => !existsSync(resolve(dir, f)));
97
+ if (missing.length > 0) {
98
+ return { ok: true, label: 'Observability logs', detail: `Missing: ${missing.join(', ')}`, warning: true };
99
+ }
100
+ return { ok: true, label: 'Observability logs', detail: 'All log files present' };
101
+ }
102
+
103
+ function checkMcpEnvVars(manifest: Manifest | null): CheckResult {
104
+ if (!manifest?.stack) {
105
+ return { ok: true, label: 'MCP environment variables', detail: 'No stack config (skipped)' };
106
+ }
107
+ const required = getRequiredMcpEnvVars(manifest.stack, manifest.repoInfo);
108
+ if (required.length === 0) {
109
+ return { ok: true, label: 'MCP environment variables', detail: 'No env vars required' };
110
+ }
111
+ const missing = required.filter((r) => !process.env[r.envVar]);
112
+ if (missing.length > 0) {
113
+ const names = missing.map((m) => m.envVar).join(', ');
114
+ return { ok: false, label: 'MCP environment variables', detail: `Missing: ${names}` };
115
+ }
116
+ return { ok: true, label: 'MCP environment variables', detail: `${required.length} var(s) set` };
117
+ }
118
+
119
+ async function checkDotEnv(projectRoot: string, manifest: Manifest | null): Promise<CheckResult> {
120
+ const envPath = resolve(projectRoot, '.env');
121
+ if (!existsSync(envPath)) {
122
+ if (manifest?.stack) {
123
+ const required = getRequiredMcpEnvVars(manifest.stack, manifest.repoInfo);
124
+ if (required.length > 0) {
125
+ return { ok: true, label: '.env file', detail: 'Not found — consider creating one for MCP secrets', warning: true };
126
+ }
127
+ }
128
+ return { ok: true, label: '.env file', detail: 'Not found (not required)' };
129
+ }
130
+ return { ok: true, label: '.env file', detail: 'Present' };
131
+ }
132
+
133
+ async function checkIdeConfigs(projectRoot: string, manifest: Manifest | null): Promise<CheckResult> {
134
+ if (!manifest) {
135
+ return { ok: false, label: 'IDE configuration files', detail: 'No manifest — cannot check' };
136
+ }
137
+ const ides = manifest.ides ?? [manifest.ide];
138
+ const checks: Array<{ ide: string; file: string; found: boolean }> = [];
139
+
140
+ for (const ide of ides) {
141
+ let configFile: string;
142
+ switch (ide as IdeChoice) {
143
+ case 'vscode':
144
+ configFile = '.github/copilot-instructions.md';
145
+ break;
146
+ case 'cursor':
147
+ configFile = '.cursor/rules/opencastle.mdc';
148
+ break;
149
+ case 'claude-code':
150
+ configFile = '.claude/settings.json';
151
+ break;
152
+ case 'opencode':
153
+ configFile = '.opencode/agents.md';
154
+ break;
155
+ default:
156
+ continue;
157
+ }
158
+ checks.push({ ide: ide as string, file: configFile, found: existsSync(resolve(projectRoot, configFile)) });
159
+ }
160
+
161
+ const missing = checks.filter((c) => !c.found);
162
+ if (missing.length > 0) {
163
+ return { ok: false, label: 'IDE configuration files', detail: `Missing: ${missing.map((m) => `${m.ide} (${m.file})`).join(', ')}` };
164
+ }
165
+ return { ok: true, label: 'IDE configuration files', detail: `${checks.length} IDE(s) configured` };
166
+ }
167
+
168
+ async function checkMcpConfig(projectRoot: string, manifest: Manifest | null): Promise<CheckResult> {
169
+ if (!manifest) {
170
+ return { ok: false, label: 'MCP configuration', detail: 'No manifest — cannot check' };
171
+ }
172
+ const ides = manifest.ides ?? [manifest.ide];
173
+ const mcpPaths: Record<string, string> = {
174
+ vscode: '.vscode/mcp.json',
175
+ cursor: '.cursor/mcp.json',
176
+ 'claude-code': '.claude/mcp.json',
177
+ opencode: 'mcp.json',
178
+ };
179
+
180
+ const found: string[] = [];
181
+ for (const ide of ides) {
182
+ const path = mcpPaths[ide as string];
183
+ if (path && existsSync(resolve(projectRoot, path))) {
184
+ found.push(ide as string);
185
+ }
186
+ }
187
+ if (found.length === 0 && ides.length > 0) {
188
+ return { ok: true, label: 'MCP configuration', detail: 'No MCP config files found (MCP tools will not be available)', warning: true };
189
+ }
190
+ return { ok: true, label: 'MCP configuration', detail: `${found.length} MCP config(s)` };
191
+ }
192
+
193
+ // ── Main doctor command ───────────────────────────────────────
194
+
195
+ export default async function doctor({ args: _args }: CliContext): Promise<void> {
196
+ const projectRoot = process.cwd();
197
+
198
+ console.log(`\n 🏰 ${BOLD('OpenCastle Doctor')}\n`);
199
+ console.log(` ${DIM('Checking your setup...')}\n`);
200
+
201
+ const manifest = await readManifest(projectRoot);
202
+
203
+ const results: CheckResult[] = [
204
+ checkManifest(manifest),
205
+ await checkCustomizations(projectRoot),
206
+ await checkInstructions(projectRoot),
207
+ await checkAgents(projectRoot),
208
+ await checkSkills(projectRoot),
209
+ await checkSkillMatrix(projectRoot),
210
+ await checkLogs(projectRoot),
211
+ await checkIdeConfigs(projectRoot, manifest),
212
+ await checkMcpConfig(projectRoot, manifest),
213
+ checkMcpEnvVars(manifest),
214
+ await checkDotEnv(projectRoot, manifest),
215
+ ];
216
+
217
+ for (const r of results) {
218
+ const icon = r.ok ? (r.warning ? WARN : PASS) : FAIL;
219
+ const detail = r.detail ? ` ${DIM(r.detail)}` : '';
220
+ console.log(` ${icon} ${r.label}${detail}`);
221
+ }
222
+
223
+ const failures = results.filter((r) => !r.ok);
224
+ const warnings = results.filter((r) => r.ok && r.warning);
225
+
226
+ console.log();
227
+ if (failures.length > 0) {
228
+ console.log(` ${BOLD(`${failures.length} issue(s) found.`)} Run "npx opencastle init" to fix.\n`);
229
+ process.exit(1);
230
+ } else if (warnings.length > 0) {
231
+ console.log(` ${BOLD('All checks passed')} with ${warnings.length} warning(s).\n`);
232
+ } else {
233
+ console.log(` ${BOLD('All checks passed.')} Your setup is healthy.\n`);
234
+ }
235
+ }
package/src/cli/init.ts CHANGED
@@ -8,24 +8,9 @@ import { updateGitignore } from './gitignore.js'
8
8
  import { getRequiredMcpEnvVars } from './stack-config.js'
9
9
  import { TECH_PLUGINS, TEAM_PLUGINS } from '../orchestrator/plugins/index.js'
10
10
  import { detectRepoInfo, mergeStackIntoRepoInfo, formatRepoInfo } from './detect.js'
11
- import type { CliContext, IdeAdapter, IdeChoice, TechTool, TeamTool, StackConfig } from './types.js'
12
-
13
- const ADAPTERS: Record<string, () => Promise<IdeAdapter>> = {
14
- vscode: () => import('./adapters/vscode.js') as Promise<IdeAdapter>,
15
- cursor: () => import('./adapters/cursor.js') as Promise<IdeAdapter>,
16
- 'claude-code': () =>
17
- import('./adapters/claude-code.js') as Promise<IdeAdapter>,
18
- opencode: () =>
19
- import('./adapters/opencode.js') as Promise<IdeAdapter>,
20
- }
21
-
22
- /** IDE display labels */
23
- const IDE_DISPLAY: Record<IdeChoice, string> = {
24
- vscode: 'VS Code',
25
- cursor: 'Cursor',
26
- 'claude-code': 'Claude Code',
27
- opencode: 'OpenCode',
28
- }
11
+ import { IDE_ADAPTERS } from './adapters/index.js'
12
+ import { IDE_LABELS } from './types.js'
13
+ import type { CliContext, IdeChoice, TechTool, TeamTool, StackConfig } from './types.js'
29
14
 
30
15
  export default async function init({ pkgRoot, args }: CliContext): Promise<void> {
31
16
  const projectRoot = process.cwd()
@@ -135,7 +120,7 @@ export default async function init({ pkgRoot, args }: CliContext): Promise<void>
135
120
  // ── Merge user choices into detected info ────────────────────
136
121
  const combinedRepoInfo = mergeStackIntoRepoInfo(repoInfo, stack)
137
122
 
138
- const ideNames = ides.map((id) => IDE_DISPLAY[id as IdeChoice]).join(', ')
123
+ const ideNames = ides.map((id) => IDE_LABELS[id as IdeChoice]).join(', ')
139
124
  console.log(`\n Installing for ${c.cyan(ideNames)}...`)
140
125
  if (techTools.length > 0) {
141
126
  console.log(` Tech: ${c.green(techTools.join(', '))}`)
@@ -148,9 +133,9 @@ export default async function init({ pkgRoot, args }: CliContext): Promise<void>
148
133
  // ── Dry run ─────────────────────────────────────────────────────
149
134
  if (dryRun) {
150
135
  for (const ide of ides) {
151
- const adapter = await ADAPTERS[ide]()
136
+ const adapter = await IDE_ADAPTERS[ide]()
152
137
  const managed = adapter.getManagedPaths()
153
- console.log(` ${c.dim(`[dry-run] ${IDE_DISPLAY[ide as IdeChoice]} files:`)}\n`)
138
+ console.log(` ${c.dim(`[dry-run] ${IDE_LABELS[ide as IdeChoice]} files:`)}\n`)
154
139
  for (const p of managed.framework) {
155
140
  console.log(` ${c.green('+')} ${p}`)
156
141
  }
@@ -197,7 +182,7 @@ export default async function init({ pkgRoot, args }: CliContext): Promise<void>
197
182
  const allManagedPaths = { framework: [] as string[], customizable: [] as string[] }
198
183
 
199
184
  for (const ide of ides) {
200
- const adapter = await ADAPTERS[ide]()
185
+ const adapter = await IDE_ADAPTERS[ide]()
201
186
  const results = await adapter.install(pkgRoot, projectRoot, stack, combinedRepoInfo)
202
187
  totalCreated += results.created.length
203
188
  totalSkipped += results.skipped.length
@@ -228,7 +213,7 @@ export default async function init({ pkgRoot, args }: CliContext): Promise<void>
228
213
  console.log(` ${c.dim('→')} Skipped ${totalSkipped} existing files`)
229
214
  }
230
215
 
231
- // ── Env var notice ──────────────────────────────────────────────
216
+ // ── Env var notice + .env file generation ────────────────────
232
217
  const envVars = getRequiredMcpEnvVars(stack, combinedRepoInfo)
233
218
  if (envVars.length > 0) {
234
219
  console.log(`\n ${c.yellow('⚠')} Required environment variables for MCP servers:\n`)
@@ -236,6 +221,28 @@ export default async function init({ pkgRoot, args }: CliContext): Promise<void>
236
221
  console.log(` ${c.bold(envVar)}`)
237
222
  console.log(` ${c.dim('└')} ${c.dim(hint)}\n`)
238
223
  }
224
+
225
+ // Offer to create .env if it doesn't exist
226
+ const envPath = resolve(projectRoot, '.env')
227
+ if (!dryRun && !existsSync(envPath)) {
228
+ const createEnv = await confirm('Create a .env file with placeholders for these variables?', true)
229
+ if (createEnv) {
230
+ const { writeFile: writeEnvFile } = await import('node:fs/promises')
231
+ const lines = envVars.map(({ envVar, hint }) => `# ${hint}\n${envVar}=\n`)
232
+ await writeEnvFile(envPath, lines.join('\n') + '\n')
233
+ console.log(` ${c.green('✓')} Created .env with ${envVars.length} placeholder(s)`)
234
+ console.log(` ${c.dim('→')} Fill in the values, then reload your IDE\n`)
235
+ }
236
+ } else if (!dryRun && existsSync(envPath)) {
237
+ // Check which vars are already in .env
238
+ const envContent = await readFile(envPath, 'utf8')
239
+ const missing = envVars.filter(({ envVar }) => !envContent.includes(envVar))
240
+ if (missing.length > 0) {
241
+ console.log(` ${c.dim('→')} Your .env is missing: ${missing.map((m) => m.envVar).join(', ')}`)
242
+ } else {
243
+ console.log(` ${c.green('✓')} All required variables found in .env`)
244
+ }
245
+ }
239
246
  }
240
247
 
241
248
  // ── OAuth setup guides ────────────────────────────────────────
@@ -266,7 +273,7 @@ export default async function init({ pkgRoot, args }: CliContext): Promise<void>
266
273
  if (envVars.length > 0) {
267
274
  step++
268
275
  console.log(
269
- ` ${step}. Set the environment variable${envVars.length > 1 ? 's' : ''} listed above`
276
+ ` ${step}. Set the environment variable${envVars.length > 1 ? 's' : ''} listed above (in .env or your shell)`
270
277
  )
271
278
  }
272
279
  step++