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.
- package/README.md +30 -3
- package/bin/cli.mjs +2 -0
- package/dist/cli/adapters/claude-code.d.ts +2 -5
- package/dist/cli/adapters/claude-code.d.ts.map +1 -1
- package/dist/cli/adapters/claude-code.js +12 -251
- package/dist/cli/adapters/claude-code.js.map +1 -1
- package/dist/cli/adapters/cursor.d.ts.map +1 -1
- package/dist/cli/adapters/cursor.js +3 -17
- package/dist/cli/adapters/cursor.js.map +1 -1
- package/dist/cli/adapters/frontmatter.d.ts +26 -0
- package/dist/cli/adapters/frontmatter.d.ts.map +1 -0
- package/dist/cli/adapters/frontmatter.js +40 -0
- package/dist/cli/adapters/frontmatter.js.map +1 -0
- package/dist/cli/adapters/index.d.ts +5 -0
- package/dist/cli/adapters/index.d.ts.map +1 -0
- package/dist/cli/adapters/index.js +9 -0
- package/dist/cli/adapters/index.js.map +1 -0
- package/dist/cli/adapters/opencode.d.ts +2 -5
- package/dist/cli/adapters/opencode.d.ts.map +1 -1
- package/dist/cli/adapters/opencode.js +12 -250
- package/dist/cli/adapters/opencode.js.map +1 -1
- package/dist/cli/adapters/single-file-base.d.ts +40 -0
- package/dist/cli/adapters/single-file-base.d.ts.map +1 -0
- package/dist/cli/adapters/single-file-base.js +246 -0
- package/dist/cli/adapters/single-file-base.js.map +1 -0
- package/dist/cli/dashboard.d.ts.map +1 -1
- package/dist/cli/dashboard.js +3 -2
- package/dist/cli/dashboard.js.map +1 -1
- package/dist/cli/detect.d.ts.map +1 -1
- package/dist/cli/detect.js +13 -11
- package/dist/cli/detect.js.map +1 -1
- package/dist/cli/doctor.d.ts +3 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/doctor.js +205 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +31 -19
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/run/schema.d.ts +1 -5
- package/dist/cli/run/schema.d.ts.map +1 -1
- package/dist/cli/run/schema.js +6 -330
- package/dist/cli/run/schema.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +14 -1
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/types.d.ts +0 -5
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +4 -17
- package/dist/cli/update.js.map +1 -1
- package/package.json +7 -2
- package/src/cli/adapters/claude-code.ts +13 -304
- package/src/cli/adapters/cursor.ts +3 -23
- package/src/cli/adapters/frontmatter.ts +47 -0
- package/src/cli/adapters/index.ts +13 -0
- package/src/cli/adapters/opencode.ts +12 -301
- package/src/cli/adapters/single-file-base.ts +320 -0
- package/src/cli/dashboard.ts +3 -2
- package/src/cli/detect.ts +19 -15
- package/src/cli/doctor.ts +235 -0
- package/src/cli/init.ts +31 -24
- package/src/cli/run/schema.ts +7 -365
- package/src/cli/run.ts +17 -1
- package/src/cli/types.ts +0 -6
- package/src/cli/update.ts +5 -23
- package/src/dashboard/dist/_astro/{index.CWVzbF4T.css → index.Bnq19_1M.css} +1 -1
- package/src/dashboard/dist/index.html +170 -11
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/dashboard/seed-data/reviews.ndjson +6 -0
- package/src/dashboard/src/pages/index.astro +213 -10
- package/src/dashboard/src/styles/dashboard.css +196 -0
- package/src/orchestrator/agent-workflows/bug-fix.md +2 -2
- package/src/orchestrator/agent-workflows/data-pipeline.md +8 -8
- package/src/orchestrator/agent-workflows/database-migration.md +2 -2
- package/src/orchestrator/agent-workflows/feature-implementation.md +12 -5
- package/src/orchestrator/agent-workflows/performance-optimization.md +2 -2
- package/src/orchestrator/agent-workflows/refactoring.md +2 -2
- package/src/orchestrator/agent-workflows/schema-changes.md +2 -2
- package/src/orchestrator/agent-workflows/security-audit.md +2 -2
- package/src/orchestrator/agents/data-expert.agent.md +2 -2
- package/src/orchestrator/agents/researcher.agent.md +0 -16
- package/src/orchestrator/agents/team-lead.agent.md +17 -6
- package/src/orchestrator/customizations/AGENT-PERFORMANCE.md +1 -3
- package/src/orchestrator/prompts/bootstrap-customizations.prompt.md +1 -1
- package/src/orchestrator/prompts/bug-fix.prompt.md +11 -6
- package/src/orchestrator/prompts/implement-feature.prompt.md +9 -4
- package/src/orchestrator/prompts/quick-refinement.prompt.md +9 -5
- package/src/orchestrator/prompts/resolve-pr-comments.prompt.md +18 -4
- package/src/orchestrator/skills/agent-hooks/SKILL.md +4 -2
- package/src/orchestrator/skills/fast-review/SKILL.md +15 -4
- package/src/orchestrator/skills/self-improvement/SKILL.md +1 -1
- package/src/orchestrator/skills/validation-gates/SKILL.md +152 -15
- 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
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
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) =>
|
|
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
|
|
136
|
+
const adapter = await IDE_ADAPTERS[ide]()
|
|
152
137
|
const managed = adapter.getManagedPaths()
|
|
153
|
-
console.log(` ${c.dim(`[dry-run] ${
|
|
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
|
|
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++
|