ccraft 1.0.11 → 1.0.13
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 +22 -11
- package/bin/claude-craft.js +3 -12
- package/package.json +1 -1
- package/src/commands/install.js +376 -187
- package/src/commands/update.js +1 -12
- package/src/ui/phase-header.js +9 -3
- package/src/ui/tasks.js +12 -32
- package/src/utils/analysis-cache.js +2 -32
- package/src/utils/api-client.js +6 -8
- package/src/utils/api-file-writer.js +2 -6
- package/src/commands/create.js +0 -568
- package/src/utils/claude-scorer.js +0 -101
package/src/commands/create.js
DELETED
|
@@ -1,568 +0,0 @@
|
|
|
1
|
-
import { resolve, join, basename } from 'path';
|
|
2
|
-
import { mkdirSync, existsSync, writeFileSync, readdirSync } from 'fs';
|
|
3
|
-
import { execFileSync } from 'child_process';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
import ora from 'ora';
|
|
6
|
-
import { gatherCreateProfile, gatherPersona, gatherUserProfile, gatherMcpConfig, confirmInstallation } from '../prompts/gather.js';
|
|
7
|
-
import { themedInput } from '../ui/prompts.js';
|
|
8
|
-
import { callGenerate, ApiError } from '../utils/api-client.js';
|
|
9
|
-
import { runPreflight } from '../utils/preflight.js';
|
|
10
|
-
import { writeApiFiles, buildFileList } from '../utils/api-file-writer.js';
|
|
11
|
-
import { setupMcps } from '../utils/mcp-setup.js';
|
|
12
|
-
import { scoreWithClaude } from '../utils/claude-scorer.js';
|
|
13
|
-
import { optimizeSettings } from '../utils/claude-optimizer.js';
|
|
14
|
-
import { rewriteClaudeMd } from '../utils/claude-rewriter.js';
|
|
15
|
-
import { detectProject } from '../utils/detect-project.js';
|
|
16
|
-
import { analyzeWithClaude } from '../utils/claude-analyzer.js';
|
|
17
|
-
import { analyzeDescription } from '../utils/description-analyzer.js';
|
|
18
|
-
import { platformCmd } from '../utils/run-claude.js';
|
|
19
|
-
import { runBootstrap } from '../utils/bootstrap-runner.js';
|
|
20
|
-
import {
|
|
21
|
-
writeAnalysisCache,
|
|
22
|
-
writeUserProfile,
|
|
23
|
-
updateManifest,
|
|
24
|
-
readAnalysisCache,
|
|
25
|
-
promoteCache,
|
|
26
|
-
cleanupAnalysisCache,
|
|
27
|
-
} from '../utils/analysis-cache.js';
|
|
28
|
-
import { VERSION, VIBE_DEFAULTS } from '../constants.js';
|
|
29
|
-
import * as logger from '../utils/logger.js';
|
|
30
|
-
|
|
31
|
-
// UI modules
|
|
32
|
-
import { renderBanner } from '../ui/brand.js';
|
|
33
|
-
import { renderComponentBreakdown, renderMcpStatus, renderFileResults } from '../ui/tables.js';
|
|
34
|
-
import { renderSuccessCard } from '../ui/cards.js';
|
|
35
|
-
import { runInstallTasks, runVerifyTasks, runFinalizeTasks } from '../ui/tasks.js';
|
|
36
|
-
|
|
37
|
-
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
38
|
-
|
|
39
|
-
function renderCreatePhase(number, name, totalPhases) {
|
|
40
|
-
const w = Math.min((process.stdout.columns || 80) - 4, 56);
|
|
41
|
-
const label = `Phase ${number} of ${totalPhases}`;
|
|
42
|
-
const inner = ` ${label} \u2500\u2500 ${name} `;
|
|
43
|
-
const tailLen = Math.max(2, w - inner.length - 2);
|
|
44
|
-
console.log();
|
|
45
|
-
console.log(chalk.bold.cyan(` \u2500\u2500${inner}${'\u2500'.repeat(tailLen)}`));
|
|
46
|
-
console.log();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function countTotalItems(summary, selectedCandidateIds) {
|
|
50
|
-
const countBucket = (bucket) => {
|
|
51
|
-
if (!bucket) return 0;
|
|
52
|
-
return Object.values(bucket).reduce(
|
|
53
|
-
(sum, arr) => sum + (Array.isArray(arr) ? arr.length : 0),
|
|
54
|
-
0,
|
|
55
|
-
);
|
|
56
|
-
};
|
|
57
|
-
let total = countBucket(summary.guaranteed);
|
|
58
|
-
if (selectedCandidateIds === null) {
|
|
59
|
-
total += countBucket(summary.candidates);
|
|
60
|
-
} else {
|
|
61
|
-
total += selectedCandidateIds.length;
|
|
62
|
-
}
|
|
63
|
-
return total;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ── Main command ─────────────────────────────────────────────────────────────
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Create command — build a new project from scratch.
|
|
70
|
-
*
|
|
71
|
-
* Step 1: Welcome & Setup (env check + API key)
|
|
72
|
-
* Step 2: Project Setup (name, description, mkdir, git init)
|
|
73
|
-
* Step 3: Configuration & Install (synthetic analysis → server → write .claude/)
|
|
74
|
-
* Step 4: Bootstrap (hand off to Claude CLI /bootstrap:auto)
|
|
75
|
-
* Step 5: Post-bootstrap Sync (re-analyze, rewrite CLAUDE.md, finalize)
|
|
76
|
-
*/
|
|
77
|
-
export async function runCreate(options = {}) {
|
|
78
|
-
const TOTAL_PHASES = 5;
|
|
79
|
-
let targetDir;
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
// ================================================================
|
|
83
|
-
// STEP 1: Welcome & Setup
|
|
84
|
-
// ================================================================
|
|
85
|
-
renderBanner(VERSION);
|
|
86
|
-
renderCreatePhase(1, 'Welcome & Setup', TOTAL_PHASES);
|
|
87
|
-
|
|
88
|
-
// ── Pre-flight checks (Claude Code + API key + server) ──────
|
|
89
|
-
await runPreflight({
|
|
90
|
-
interactive: !options.yes,
|
|
91
|
-
requireClaude: true,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// ================================================================
|
|
95
|
-
// STEP 2: Project Setup
|
|
96
|
-
// ================================================================
|
|
97
|
-
renderCreatePhase(2, 'Project Setup', TOTAL_PHASES);
|
|
98
|
-
|
|
99
|
-
let createProfile;
|
|
100
|
-
if (options.yes) {
|
|
101
|
-
createProfile = {
|
|
102
|
-
name: options.name || 'my-project',
|
|
103
|
-
description: options.description || 'A new project',
|
|
104
|
-
projectType: 'monolith',
|
|
105
|
-
};
|
|
106
|
-
logger.info(`Non-interactive mode — creating ${chalk.bold(createProfile.name)} (monolith).`);
|
|
107
|
-
} else {
|
|
108
|
-
createProfile = await gatherCreateProfile();
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
let { name, description, projectType } = createProfile;
|
|
112
|
-
|
|
113
|
-
// Resolve target directory
|
|
114
|
-
const parentDir = resolve(options.dir || process.cwd());
|
|
115
|
-
let useCurrentDir = false;
|
|
116
|
-
|
|
117
|
-
if (!name) {
|
|
118
|
-
// Empty name = use current directory — must be empty
|
|
119
|
-
targetDir = parentDir;
|
|
120
|
-
useCurrentDir = true;
|
|
121
|
-
const contents = readdirSync(targetDir).filter((f) => f !== '.git');
|
|
122
|
-
if (contents.length > 0) {
|
|
123
|
-
logger.error(`Current directory ${chalk.bold(targetDir)} is not empty. Cannot create a project here.`);
|
|
124
|
-
process.exit(1);
|
|
125
|
-
}
|
|
126
|
-
name = basename(targetDir) || 'my-project';
|
|
127
|
-
} else {
|
|
128
|
-
// Named project — re-prompt if directory already exists
|
|
129
|
-
targetDir = join(parentDir, name);
|
|
130
|
-
while (existsSync(targetDir)) {
|
|
131
|
-
logger.warn(`Directory ${chalk.bold(name)} already exists.`);
|
|
132
|
-
const newName = await themedInput({
|
|
133
|
-
message: 'Enter a different project name:',
|
|
134
|
-
hint: 'Letters, numbers, dots, hyphens, underscores only.',
|
|
135
|
-
validate: (v) => {
|
|
136
|
-
const t = v.trim();
|
|
137
|
-
if (!t) return 'Name is required.';
|
|
138
|
-
if (!/^[a-zA-Z0-9._-]+$/.test(t)) return 'Only letters, numbers, dots, hyphens, and underscores allowed.';
|
|
139
|
-
return true;
|
|
140
|
-
},
|
|
141
|
-
});
|
|
142
|
-
name = newName.trim();
|
|
143
|
-
targetDir = join(parentDir, name);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Create directory + git init
|
|
148
|
-
const spinner1 = ora(useCurrentDir ? 'Initializing project in current directory...' : 'Creating project directory...').start();
|
|
149
|
-
if (!useCurrentDir) {
|
|
150
|
-
mkdirSync(targetDir, { recursive: true });
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Write a minimal .gitignore
|
|
154
|
-
writeFileSync(
|
|
155
|
-
join(targetDir, '.gitignore'),
|
|
156
|
-
'node_modules/\ndist/\nbuild/\n.env\n.env.*\n!.env.example\n*.log\n.DS_Store\nThumbs.db\n',
|
|
157
|
-
'utf8',
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
// git init
|
|
161
|
-
try {
|
|
162
|
-
const { file, args } = platformCmd('git', ['init']);
|
|
163
|
-
execFileSync(file, args, { cwd: targetDir, stdio: 'pipe', windowsHide: true });
|
|
164
|
-
spinner1.succeed(useCurrentDir
|
|
165
|
-
? `Initialized project in ${chalk.bold(targetDir)} with git.`
|
|
166
|
-
: `Created ${chalk.bold(name)}/ with git initialized.`);
|
|
167
|
-
} catch {
|
|
168
|
-
spinner1.succeed(useCurrentDir
|
|
169
|
-
? `Initialized project in ${chalk.bold(targetDir)} (git init skipped — git not available).`
|
|
170
|
-
: `Created ${chalk.bold(name)}/ (git init skipped — git not available).`);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// ── Persona selection ────────────────────────────────────────────
|
|
174
|
-
let persona;
|
|
175
|
-
if (options.yes) {
|
|
176
|
-
persona = 'developer'; // non-interactive: always developer defaults
|
|
177
|
-
} else if (options.pro) {
|
|
178
|
-
persona = 'developer'; // interactive developer mode: skip persona prompt
|
|
179
|
-
} else {
|
|
180
|
-
persona = await gatherPersona();
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const isVibe = persona === 'vibe';
|
|
184
|
-
|
|
185
|
-
// Build user profile
|
|
186
|
-
let userProfile;
|
|
187
|
-
if (options.yes) {
|
|
188
|
-
userProfile = {
|
|
189
|
-
intents: ['implementing', 'debugging', 'refactoring', 'testing', 'reviewing'],
|
|
190
|
-
sourceControl: 'github',
|
|
191
|
-
documentTools: [],
|
|
192
|
-
};
|
|
193
|
-
logger.info('Non-interactive mode — using default profile.');
|
|
194
|
-
} else if (isVibe) {
|
|
195
|
-
userProfile = { ...VIBE_DEFAULTS };
|
|
196
|
-
logger.info('Vibe mode — using streamlined defaults.');
|
|
197
|
-
} else {
|
|
198
|
-
userProfile = await gatherUserProfile();
|
|
199
|
-
}
|
|
200
|
-
userProfile.persona = persona;
|
|
201
|
-
|
|
202
|
-
// ================================================================
|
|
203
|
-
// STEP 3: Configuration & Install
|
|
204
|
-
// ================================================================
|
|
205
|
-
renderCreatePhase(3, 'Configuration', TOTAL_PHASES);
|
|
206
|
-
|
|
207
|
-
// Analyze description with Claude to infer tech stack
|
|
208
|
-
let descAnalysis = null;
|
|
209
|
-
{
|
|
210
|
-
const spinnerAnalyze = ora('Analyzing your requirements...').start();
|
|
211
|
-
const { analysis, failReason } = await analyzeDescription(description, projectType);
|
|
212
|
-
if (analysis) {
|
|
213
|
-
descAnalysis = analysis;
|
|
214
|
-
const parts = [];
|
|
215
|
-
if (analysis.frameworks.length) parts.push(analysis.frameworks.join(', '));
|
|
216
|
-
if (analysis.languages.length) parts.push(analysis.languages.join(', '));
|
|
217
|
-
if (analysis.databases.length) parts.push(analysis.databases.join(', '));
|
|
218
|
-
spinnerAnalyze.succeed(
|
|
219
|
-
parts.length
|
|
220
|
-
? `Detected stack: ${chalk.bold(parts.join(' + '))}`
|
|
221
|
-
: 'Requirements analyzed.',
|
|
222
|
-
);
|
|
223
|
-
} else {
|
|
224
|
-
spinnerAnalyze.info(`Stack inference skipped${failReason ? ` (${failReason})` : ''} — using defaults.`);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Build project info — enriched with description analysis if available
|
|
229
|
-
const syntheticProjectInfo = {
|
|
230
|
-
name,
|
|
231
|
-
description,
|
|
232
|
-
projectType,
|
|
233
|
-
languages: descAnalysis?.languages?.length ? descAnalysis.languages : [],
|
|
234
|
-
frameworks: descAnalysis?.frameworks?.length ? descAnalysis.frameworks : [],
|
|
235
|
-
codeStyle: descAnalysis?.codeStyle?.length ? descAnalysis.codeStyle : [],
|
|
236
|
-
cicd: descAnalysis?.cicd?.length ? descAnalysis.cicd : [],
|
|
237
|
-
subprojects: descAnalysis?.subprojects?.length ? descAnalysis.subprojects : [],
|
|
238
|
-
architecture: descAnalysis?.architecture || '',
|
|
239
|
-
buildCommands: descAnalysis?.buildCommands || {},
|
|
240
|
-
complexity: descAnalysis?.complexity ?? 0.3,
|
|
241
|
-
metrics: descAnalysis?.metrics || null,
|
|
242
|
-
entryPoints: descAnalysis?.entryPoints || [],
|
|
243
|
-
coreModules: descAnalysis?.coreModules || [],
|
|
244
|
-
testFramework: descAnalysis?.testFramework || '',
|
|
245
|
-
packageManager: descAnalysis?.packageManager || '',
|
|
246
|
-
languageDistribution: descAnalysis?.languageDistribution || null,
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
const syntheticDetected = {
|
|
250
|
-
...syntheticProjectInfo,
|
|
251
|
-
sensitiveFiles: { found: [], gitignoreCovers: true },
|
|
252
|
-
_rootFiles: [],
|
|
253
|
-
databases: descAnalysis?.databases?.length ? descAnalysis.databases : [],
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
// Call server
|
|
257
|
-
const spinner3 = ora('Calling claude-craft server...').start();
|
|
258
|
-
|
|
259
|
-
let apiResponse;
|
|
260
|
-
try {
|
|
261
|
-
apiResponse = await callGenerate(
|
|
262
|
-
userProfile,
|
|
263
|
-
{
|
|
264
|
-
...syntheticProjectInfo,
|
|
265
|
-
detectedFiles: [],
|
|
266
|
-
databases: syntheticDetected.databases,
|
|
267
|
-
},
|
|
268
|
-
{ projectPath: targetDir },
|
|
269
|
-
);
|
|
270
|
-
spinner3.succeed('Server returned configuration.');
|
|
271
|
-
} catch (err) {
|
|
272
|
-
spinner3.fail('Server request failed.');
|
|
273
|
-
if (err instanceof ApiError) {
|
|
274
|
-
logger.error(err.message);
|
|
275
|
-
process.exit(1);
|
|
276
|
-
}
|
|
277
|
-
throw err;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const { summary, mcpConfigs } = apiResponse;
|
|
281
|
-
|
|
282
|
-
// Validate we got components
|
|
283
|
-
const guaranteedCount = summary?.guaranteed
|
|
284
|
-
? Object.values(summary.guaranteed).reduce((s, arr) => s + (Array.isArray(arr) ? arr.length : 0), 0)
|
|
285
|
-
: 0;
|
|
286
|
-
|
|
287
|
-
if (guaranteedCount === 0) {
|
|
288
|
-
logger.warn('Server returned zero guaranteed components. Continuing with core defaults.');
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Display scoring summary
|
|
292
|
-
console.log();
|
|
293
|
-
renderComponentBreakdown(summary);
|
|
294
|
-
|
|
295
|
-
// MCP selection — auto-select core/role/stack for new projects (no stack to score against)
|
|
296
|
-
let selectedMcps;
|
|
297
|
-
let mcpKeys = {};
|
|
298
|
-
const securityConfig = { addSecurityGitignore: true };
|
|
299
|
-
|
|
300
|
-
if (options.yes || isVibe) {
|
|
301
|
-
selectedMcps = mcpConfigs.filter(
|
|
302
|
-
(m) => m.tier === 'core' || m.tier === 'role' || m.tier === 'stack' || m.tier === 'auto' || m.recommended,
|
|
303
|
-
);
|
|
304
|
-
} else {
|
|
305
|
-
const mcpResult = await gatherMcpConfig(mcpConfigs);
|
|
306
|
-
selectedMcps = mcpResult.selectedMcps;
|
|
307
|
-
mcpKeys = mcpResult.mcpKeys;
|
|
308
|
-
|
|
309
|
-
const finalSummary = { ...summary, mcps: selectedMcps.map((m) => ({ id: m.id, tier: m.tier })) };
|
|
310
|
-
const proceed = await confirmInstallation(finalSummary);
|
|
311
|
-
if (!proceed) {
|
|
312
|
-
console.log();
|
|
313
|
-
logger.info('Cancelled.');
|
|
314
|
-
process.exit(0);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Cache analysis and profile
|
|
319
|
-
try {
|
|
320
|
-
writeAnalysisCache(targetDir, syntheticProjectInfo, syntheticDetected, null);
|
|
321
|
-
writeUserProfile(targetDir, userProfile);
|
|
322
|
-
} catch (cacheErr) {
|
|
323
|
-
logger.debug(`Analysis cache write failed: ${cacheErr.message}`);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Write files
|
|
327
|
-
let results;
|
|
328
|
-
let filesToWrite;
|
|
329
|
-
let selectedCandidateIds = null;
|
|
330
|
-
|
|
331
|
-
try {
|
|
332
|
-
const installCtx = await runInstallTasks({
|
|
333
|
-
apiResponse,
|
|
334
|
-
selectedCandidateIds: null,
|
|
335
|
-
targetDir,
|
|
336
|
-
selectedMcps,
|
|
337
|
-
mcpKeys,
|
|
338
|
-
securityConfig,
|
|
339
|
-
detected: syntheticDetected,
|
|
340
|
-
buildFileList,
|
|
341
|
-
writeApiFiles,
|
|
342
|
-
scoreWithClaude,
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
results = installCtx.results;
|
|
346
|
-
filesToWrite = installCtx.filesToWrite;
|
|
347
|
-
selectedCandidateIds = installCtx.selectedCandidateIds ?? null;
|
|
348
|
-
} catch {
|
|
349
|
-
// Fallback to direct write
|
|
350
|
-
const spinnerWrite = ora('Writing configuration files...').start();
|
|
351
|
-
filesToWrite = buildFileList(apiResponse, null);
|
|
352
|
-
results = await writeApiFiles(filesToWrite, targetDir, {
|
|
353
|
-
force: true,
|
|
354
|
-
selectedMcpIds: selectedMcps.map((m) => m.id),
|
|
355
|
-
mcpKeys,
|
|
356
|
-
securityConfig,
|
|
357
|
-
detected: syntheticDetected,
|
|
358
|
-
});
|
|
359
|
-
spinnerWrite.succeed('Configuration generated.');
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Update manifest
|
|
363
|
-
try {
|
|
364
|
-
if (filesToWrite) updateManifest(targetDir, results, filesToWrite);
|
|
365
|
-
} catch (cacheErr) {
|
|
366
|
-
logger.debug(`Manifest update failed: ${cacheErr.message}`);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// Display file results
|
|
370
|
-
renderFileResults(results);
|
|
371
|
-
|
|
372
|
-
// MCP verification
|
|
373
|
-
let mcpResults = [];
|
|
374
|
-
if (selectedMcps.length > 0) {
|
|
375
|
-
console.log();
|
|
376
|
-
try {
|
|
377
|
-
const verifyCtx = await runVerifyTasks(selectedMcps, mcpKeys, { setupMcps, targetDir });
|
|
378
|
-
mcpResults = verifyCtx.mcpResults;
|
|
379
|
-
} catch {
|
|
380
|
-
const spinnerMcp = ora('Verifying MCP servers...').start();
|
|
381
|
-
mcpResults = await setupMcps(selectedMcps, mcpKeys, {
|
|
382
|
-
healthCheck: true,
|
|
383
|
-
targetDir,
|
|
384
|
-
onStatus: (id, status) => {
|
|
385
|
-
if (status === 'verifying') spinnerMcp.text = `Verifying ${id}...`;
|
|
386
|
-
else if (status === 'testing') spinnerMcp.text = `Health-checking ${id}...`;
|
|
387
|
-
},
|
|
388
|
-
});
|
|
389
|
-
spinnerMcp.succeed('MCP verification complete.');
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
renderMcpStatus(mcpResults);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// ================================================================
|
|
396
|
-
// STEP 4: Bootstrap
|
|
397
|
-
// ================================================================
|
|
398
|
-
renderCreatePhase(4, 'Bootstrap', TOTAL_PHASES);
|
|
399
|
-
|
|
400
|
-
console.log(chalk.dim(' Handing off to Claude to scaffold your project...'));
|
|
401
|
-
console.log(chalk.dim(' This may take several minutes. Activity log:'));
|
|
402
|
-
console.log();
|
|
403
|
-
|
|
404
|
-
let bootstrapSucceeded = true;
|
|
405
|
-
try {
|
|
406
|
-
await runBootstrap(targetDir, description);
|
|
407
|
-
} catch (err) {
|
|
408
|
-
bootstrapSucceeded = false;
|
|
409
|
-
console.log();
|
|
410
|
-
logger.warn('Bootstrap did not complete: ' + err.message);
|
|
411
|
-
logger.info('Your .claude/ configuration is still intact. You can run /bootstrap:auto manually inside the project.');
|
|
412
|
-
console.log();
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// ================================================================
|
|
416
|
-
// STEP 5: Post-Bootstrap Sync
|
|
417
|
-
// ================================================================
|
|
418
|
-
renderCreatePhase(5, 'Finalization', TOTAL_PHASES);
|
|
419
|
-
|
|
420
|
-
if (bootstrapSucceeded) {
|
|
421
|
-
// Re-analyze the now-real project
|
|
422
|
-
try {
|
|
423
|
-
const spinner5 = ora('Re-analyzing project...').start();
|
|
424
|
-
const fsDetected = await detectProject(targetDir);
|
|
425
|
-
|
|
426
|
-
let projectInfo;
|
|
427
|
-
try {
|
|
428
|
-
const { analysis } = await analyzeWithClaude(targetDir);
|
|
429
|
-
if (analysis) {
|
|
430
|
-
projectInfo = {
|
|
431
|
-
name: analysis.name || fsDetected.name || name,
|
|
432
|
-
description: analysis.description || fsDetected.description || description,
|
|
433
|
-
projectType: analysis.projectType || fsDetected.projectType || projectType,
|
|
434
|
-
languages: analysis.languages?.length ? analysis.languages : fsDetected.languages,
|
|
435
|
-
frameworks: analysis.frameworks?.length ? analysis.frameworks : fsDetected.frameworks,
|
|
436
|
-
codeStyle: analysis.codeStyle?.length ? analysis.codeStyle : fsDetected.codeStyle,
|
|
437
|
-
cicd: analysis.cicd?.length ? analysis.cicd : fsDetected.cicd,
|
|
438
|
-
subprojects: analysis.subprojects?.length ? analysis.subprojects : fsDetected.subprojects,
|
|
439
|
-
architecture: analysis.architecture || '',
|
|
440
|
-
buildCommands: analysis.buildCommands || {},
|
|
441
|
-
complexity: analysis.complexity ?? 0.5,
|
|
442
|
-
metrics: analysis.metrics || null,
|
|
443
|
-
entryPoints: analysis.entryPoints || [],
|
|
444
|
-
coreModules: analysis.coreModules || [],
|
|
445
|
-
testFramework: analysis.testFramework || '',
|
|
446
|
-
packageManager: analysis.packageManager || fsDetected.packageManager || '',
|
|
447
|
-
languageDistribution: analysis.languageDistribution || fsDetected.languageDistribution || null,
|
|
448
|
-
};
|
|
449
|
-
} else {
|
|
450
|
-
projectInfo = buildProjectInfoFromFs(fsDetected, name, description, projectType);
|
|
451
|
-
}
|
|
452
|
-
} catch {
|
|
453
|
-
projectInfo = buildProjectInfoFromFs(fsDetected, name, description, projectType);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
spinner5.succeed('Project re-analyzed.');
|
|
457
|
-
|
|
458
|
-
// Overwrite cache with real data
|
|
459
|
-
const detected = { ...fsDetected, ...projectInfo };
|
|
460
|
-
writeAnalysisCache(targetDir, projectInfo, detected, null);
|
|
461
|
-
writeUserProfile(targetDir, userProfile);
|
|
462
|
-
} catch (err) {
|
|
463
|
-
logger.debug(`Post-bootstrap analysis failed: ${err.message}`);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// Finalize — optimize settings + rewrite CLAUDE.md
|
|
468
|
-
try {
|
|
469
|
-
await runFinalizeTasks({
|
|
470
|
-
targetDir,
|
|
471
|
-
readAnalysisCache,
|
|
472
|
-
optimizeSettings,
|
|
473
|
-
rewriteClaudeMd,
|
|
474
|
-
});
|
|
475
|
-
} catch {
|
|
476
|
-
// Fallback to sequential
|
|
477
|
-
const spinnerOpt = ora('Optimizing settings...').start();
|
|
478
|
-
const optResult = optimizeSettings(targetDir);
|
|
479
|
-
if (optResult.status === 'ok' && optResult.applied > 0) {
|
|
480
|
-
spinnerOpt.succeed(`Optimized ${optResult.applied} setting(s).`);
|
|
481
|
-
} else {
|
|
482
|
-
spinnerOpt.succeed('Settings reviewed \u2014 no changes needed.');
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
const spinnerMd = ora('Rewriting CLAUDE.md...').start();
|
|
486
|
-
const cache = readAnalysisCache(targetDir);
|
|
487
|
-
const rewritten = await rewriteClaudeMd(targetDir, cache);
|
|
488
|
-
if (rewritten) {
|
|
489
|
-
spinnerMd.succeed('CLAUDE.md rewritten.');
|
|
490
|
-
} else {
|
|
491
|
-
spinnerMd.warn('CLAUDE.md rewrite skipped \u2014 using template version.');
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// ── Success ──────────────────────────────────────────────────────
|
|
496
|
-
const totalItems = countTotalItems(summary, selectedCandidateIds);
|
|
497
|
-
const mcpsNeedingKeys = mcpResults.filter((r) => r.status === 'needs-key');
|
|
498
|
-
|
|
499
|
-
renderSuccessCard({
|
|
500
|
-
totalItems,
|
|
501
|
-
mcpCount: selectedMcps.length,
|
|
502
|
-
mcpsNeedingKeys,
|
|
503
|
-
persona,
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
console.log();
|
|
507
|
-
if (bootstrapSucceeded) {
|
|
508
|
-
logger.success(isVibe
|
|
509
|
-
? `Project ${chalk.bold(name)} is ready! Start Claude and describe what you want.`
|
|
510
|
-
: `Project ${chalk.bold(name)} created and bootstrapped!`);
|
|
511
|
-
console.log(chalk.dim(` cd ${name} && claude`));
|
|
512
|
-
} else {
|
|
513
|
-
logger.success(isVibe
|
|
514
|
-
? `Project ${chalk.bold(name)} is ready! Run Claude to finish setup.`
|
|
515
|
-
: `Project ${chalk.bold(name)} created with Claude configuration.`);
|
|
516
|
-
console.log(chalk.dim(` cd ${name} && claude -p "/bootstrap:auto ${description}"`));
|
|
517
|
-
}
|
|
518
|
-
console.log();
|
|
519
|
-
} catch (err) {
|
|
520
|
-
if (
|
|
521
|
-
err &&
|
|
522
|
-
(err.name === 'ExitPromptError' ||
|
|
523
|
-
err.constructor?.name === 'ExitPromptError' ||
|
|
524
|
-
err.message?.includes('User force closed'))
|
|
525
|
-
) {
|
|
526
|
-
console.log();
|
|
527
|
-
logger.info('Cancelled.');
|
|
528
|
-
process.exit(0);
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
console.log();
|
|
532
|
-
logger.error(err.message || String(err));
|
|
533
|
-
process.exit(1);
|
|
534
|
-
} finally {
|
|
535
|
-
if (targetDir) {
|
|
536
|
-
try {
|
|
537
|
-
promoteCache(targetDir);
|
|
538
|
-
cleanupAnalysisCache(targetDir);
|
|
539
|
-
} catch {
|
|
540
|
-
// Ignore cleanup errors
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// ── Helper: build projectInfo from filesystem detection only ─────────────────
|
|
547
|
-
|
|
548
|
-
function buildProjectInfoFromFs(fsDetected, name, description, projectType) {
|
|
549
|
-
return {
|
|
550
|
-
name: fsDetected.name || name,
|
|
551
|
-
description: fsDetected.description || description,
|
|
552
|
-
projectType: fsDetected.projectType || projectType,
|
|
553
|
-
languages: fsDetected.languages?.length ? fsDetected.languages : [],
|
|
554
|
-
frameworks: fsDetected.frameworks || [],
|
|
555
|
-
codeStyle: fsDetected.codeStyle || [],
|
|
556
|
-
cicd: fsDetected.cicd || [],
|
|
557
|
-
subprojects: fsDetected.subprojects || [],
|
|
558
|
-
architecture: '',
|
|
559
|
-
buildCommands: {},
|
|
560
|
-
complexity: 0.5,
|
|
561
|
-
metrics: null,
|
|
562
|
-
entryPoints: [],
|
|
563
|
-
coreModules: [],
|
|
564
|
-
testFramework: '',
|
|
565
|
-
packageManager: fsDetected.packageManager || '',
|
|
566
|
-
languageDistribution: fsDetected.languageDistribution || null,
|
|
567
|
-
};
|
|
568
|
-
}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Claude-powered scoring for optional candidates.
|
|
3
|
-
*
|
|
4
|
-
* Uses the local Claude CLI to evaluate which optional candidates
|
|
5
|
-
* should be included in the configuration.
|
|
6
|
-
*/
|
|
7
|
-
import { isClaudeAvailable, runClaude } from './run-claude.js';
|
|
8
|
-
import { extractJsonObject } from './json-extract.js';
|
|
9
|
-
import * as logger from './logger.js';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Score optional candidates using Claude.
|
|
13
|
-
*
|
|
14
|
-
* @param {string} scoringPrompt - Prompt generated by the server
|
|
15
|
-
* @param {string} targetDir - Project root (cwd for Claude)
|
|
16
|
-
* @returns {Promise<{ selected: string[], reasoning: string }>}
|
|
17
|
-
* Falls back to selecting ALL candidates if Claude is unavailable.
|
|
18
|
-
*/
|
|
19
|
-
export async function scoreWithClaude(scoringPrompt, targetDir) {
|
|
20
|
-
if (!isClaudeAvailable()) {
|
|
21
|
-
logger.warn('Claude CLI not available — including all optional candidates.');
|
|
22
|
-
return { selected: null, reasoning: 'Claude unavailable — inclusive fallback' };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
try {
|
|
26
|
-
const output = await runClaude([
|
|
27
|
-
'-p',
|
|
28
|
-
'--max-turns', '3',
|
|
29
|
-
], { cwd: targetDir, stdinInput: scoringPrompt });
|
|
30
|
-
|
|
31
|
-
// Parse the scoring result directly (no JSON wrapper to extract)
|
|
32
|
-
const result = parseScoreResult(output);
|
|
33
|
-
if (result) {
|
|
34
|
-
return result;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
logger.warn('Claude did not return valid scoring JSON — including all candidates.');
|
|
38
|
-
logger.debug(`Raw output (first 500 chars): ${(output || '').slice(0, 500)}`);
|
|
39
|
-
return { selected: null, reasoning: 'Parse failure — inclusive fallback' };
|
|
40
|
-
} catch (err) {
|
|
41
|
-
if (err.killed) {
|
|
42
|
-
logger.warn('Claude scoring timed out — including all candidates.');
|
|
43
|
-
} else {
|
|
44
|
-
logger.warn('Claude scoring failed — including all candidates.');
|
|
45
|
-
}
|
|
46
|
-
return { selected: null, reasoning: 'Error — inclusive fallback' };
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Parse Claude's scoring response.
|
|
52
|
-
* Handles both raw JSON and JSON embedded in markdown fences.
|
|
53
|
-
*/
|
|
54
|
-
function parseScoreResult(text) {
|
|
55
|
-
if (!text) return null;
|
|
56
|
-
|
|
57
|
-
const tryParse = (obj) => {
|
|
58
|
-
// V3 format: { selections: [{ id, category, include, reason }] }
|
|
59
|
-
if (Array.isArray(obj.selections)) {
|
|
60
|
-
const selected = obj.selections.filter((s) => s.include).map((s) => s.id);
|
|
61
|
-
const reasoning = obj.selections
|
|
62
|
-
.filter((s) => s.reason)
|
|
63
|
-
.map((s) => `${s.id}: ${s.include ? 'included' : 'excluded'} — ${s.reason}`)
|
|
64
|
-
.join('; ');
|
|
65
|
-
return { selected, reasoning };
|
|
66
|
-
}
|
|
67
|
-
// Fallback: { selected: ["id1", "id2"], reasoning: "..." }
|
|
68
|
-
if (Array.isArray(obj.selected)) {
|
|
69
|
-
return { selected: obj.selected, reasoning: obj.reasoning || '' };
|
|
70
|
-
}
|
|
71
|
-
return null;
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
// Try direct JSON parse
|
|
75
|
-
try {
|
|
76
|
-
const result = tryParse(JSON.parse(text.trim()));
|
|
77
|
-
if (result) return result;
|
|
78
|
-
} catch {
|
|
79
|
-
// Not direct JSON
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Try extracting JSON from markdown code fence
|
|
83
|
-
const fenceMatch = text.match(/```(?:json)?\s*\n([\s\S]*?)```/);
|
|
84
|
-
if (fenceMatch) {
|
|
85
|
-
try {
|
|
86
|
-
const result = tryParse(JSON.parse(fenceMatch[1].trim()));
|
|
87
|
-
if (result) return result;
|
|
88
|
-
} catch {
|
|
89
|
-
// Invalid JSON in fence
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Use brace-balanced extraction
|
|
94
|
-
const obj = extractJsonObject(text, 'selections') || extractJsonObject(text, 'selected');
|
|
95
|
-
if (obj) {
|
|
96
|
-
const result = tryParse(obj);
|
|
97
|
-
if (result) return result;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return null;
|
|
101
|
-
}
|