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/install.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { resolve } from 'path';
|
|
1
|
+
import { resolve, join, basename } from 'path';
|
|
2
|
+
import { mkdirSync, existsSync, writeFileSync, readdirSync } from 'fs';
|
|
3
|
+
import { execFileSync } from 'child_process';
|
|
2
4
|
import chalk from 'chalk';
|
|
3
5
|
import ora from 'ora';
|
|
4
6
|
import { detectProject } from '../utils/detect-project.js';
|
|
@@ -11,27 +13,24 @@ import {
|
|
|
11
13
|
} from '../utils/existing-setup.js';
|
|
12
14
|
import {
|
|
13
15
|
gatherProjectPath,
|
|
14
|
-
|
|
15
|
-
gatherUserProfile,
|
|
16
|
-
gatherMcpConfig,
|
|
17
|
-
gatherSecurityConfig,
|
|
16
|
+
gatherCreateProfile,
|
|
18
17
|
confirmInstallation,
|
|
19
18
|
} from '../prompts/gather.js';
|
|
19
|
+
import { themedInput } from '../ui/prompts.js';
|
|
20
20
|
import { callGenerate, ApiError } from '../utils/api-client.js';
|
|
21
21
|
import { writeApiFiles, buildFileList } from '../utils/api-file-writer.js';
|
|
22
22
|
import { setupMcps } from '../utils/mcp-setup.js';
|
|
23
|
-
import { scoreWithClaude } from '../utils/claude-scorer.js';
|
|
24
23
|
import { optimizeSettings } from '../utils/claude-optimizer.js';
|
|
25
24
|
import { runPreflight } from '../utils/preflight.js';
|
|
25
|
+
import { platformCmd } from '../utils/run-claude.js';
|
|
26
26
|
import {
|
|
27
27
|
writeAnalysisCache,
|
|
28
|
-
writeUserProfile,
|
|
29
28
|
updateManifest,
|
|
30
29
|
readAnalysisCache,
|
|
31
30
|
promoteCache,
|
|
32
31
|
cleanupAnalysisCache,
|
|
33
32
|
} from '../utils/analysis-cache.js';
|
|
34
|
-
import {
|
|
33
|
+
import { VERSION } from '../constants.js';
|
|
35
34
|
import * as logger from '../utils/logger.js';
|
|
36
35
|
|
|
37
36
|
// UI modules
|
|
@@ -40,20 +39,48 @@ import { renderPhaseHeader } from '../ui/phase-header.js';
|
|
|
40
39
|
import { renderProjectCard, renderSuccessCard } from '../ui/cards.js';
|
|
41
40
|
import { renderComponentBreakdown, renderMcpStatus, renderFileResults } from '../ui/tables.js';
|
|
42
41
|
import { runExistingSetupTasks, runAnalysisTasks, runInstallTasks, runVerifyTasks, runFinalizeTasks } from '../ui/tasks.js';
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
|
|
43
|
+
// ── Create-mode detection ───────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
const IGNORED_ENTRIES = new Set(['.git', '.DS_Store', 'Thumbs.db', '.gitkeep']);
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
|
-
*
|
|
48
|
+
* Returns true if the directory doesn't exist or contains only ignored files.
|
|
49
|
+
*/
|
|
50
|
+
function isEmptyDir(dirPath) {
|
|
51
|
+
if (!existsSync(dirPath)) return true;
|
|
52
|
+
const entries = readdirSync(dirPath).filter((f) => !IGNORED_ENTRIES.has(f));
|
|
53
|
+
return entries.length === 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Determine whether we're in create mode (new project) or install mode (existing project).
|
|
48
58
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
|
|
59
|
+
* Create mode triggers when:
|
|
60
|
+
* - --name or --description flag is provided (explicit intent)
|
|
61
|
+
* - Target directory doesn't exist
|
|
62
|
+
* - Target directory is empty (ignoring .git, .DS_Store, etc.)
|
|
63
|
+
*/
|
|
64
|
+
function isCreateMode(dirPath, options) {
|
|
65
|
+
if (options.name || options.description) return true;
|
|
66
|
+
return isEmptyDir(dirPath);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Main install command — unified orchestrator for both new and existing projects.
|
|
71
|
+
*
|
|
72
|
+
* Auto-detects mode:
|
|
73
|
+
* - Create mode (empty/missing dir): gather profile → mkdir → git init → synthetic
|
|
74
|
+
* analysis → server → write files → bootstrap → re-analyze → finalize
|
|
75
|
+
* - Install mode (existing project): discover → analyze → server → confirm →
|
|
76
|
+
* write files → MCP verify → finalize
|
|
54
77
|
*/
|
|
55
78
|
export async function runInstall(options = {}) {
|
|
56
79
|
let targetDir;
|
|
80
|
+
let createMode = false;
|
|
81
|
+
let createName = '';
|
|
82
|
+
let createDescription = '';
|
|
83
|
+
|
|
57
84
|
try {
|
|
58
85
|
// ================================================================
|
|
59
86
|
// PHASE 1: Welcome & Setup
|
|
@@ -66,88 +93,230 @@ export async function runInstall(options = {}) {
|
|
|
66
93
|
requireClaude: true,
|
|
67
94
|
});
|
|
68
95
|
|
|
69
|
-
// ──
|
|
70
|
-
if (!options.yes && !options.dir) {
|
|
96
|
+
// ── Resolve target directory + detect mode ──────────────────────
|
|
97
|
+
if (!options.yes && !options.dir && !options.name && !options.description) {
|
|
98
|
+
// Interactive, no flags — ask for path first, then detect mode
|
|
71
99
|
targetDir = await gatherProjectPath();
|
|
72
100
|
} else {
|
|
73
101
|
targetDir = resolve(options.dir || process.cwd());
|
|
74
102
|
}
|
|
75
103
|
|
|
76
|
-
|
|
77
|
-
|
|
104
|
+
createMode = isCreateMode(targetDir, options);
|
|
105
|
+
|
|
106
|
+
if (createMode) {
|
|
107
|
+
// ── Create mode: gather profile + mkdir + git init ────────────
|
|
108
|
+
let name, description, projectType;
|
|
109
|
+
|
|
110
|
+
if (options.yes) {
|
|
111
|
+
name = options.name || 'my-project';
|
|
112
|
+
description = options.description || 'A new project';
|
|
113
|
+
projectType = 'monolith';
|
|
114
|
+
logger.info(`New project mode — creating ${chalk.bold(name)} (monolith).`);
|
|
115
|
+
} else if (options.name || options.description) {
|
|
116
|
+
// Partial flags provided — fill in the rest interactively
|
|
117
|
+
const profile = await gatherCreateProfile();
|
|
118
|
+
name = options.name || profile.name;
|
|
119
|
+
description = options.description || profile.description;
|
|
120
|
+
projectType = profile.projectType;
|
|
121
|
+
} else {
|
|
122
|
+
// Empty dir detected — prompt for project details
|
|
123
|
+
logger.info('Empty directory detected — switching to new project mode.');
|
|
124
|
+
const profile = await gatherCreateProfile();
|
|
125
|
+
name = profile.name;
|
|
126
|
+
description = profile.description;
|
|
127
|
+
projectType = profile.projectType;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Resolve target directory for new project
|
|
131
|
+
const parentDir = resolve(options.dir || process.cwd());
|
|
132
|
+
let useCurrentDir = false;
|
|
133
|
+
|
|
134
|
+
if (!name) {
|
|
135
|
+
// Empty name = use current directory
|
|
136
|
+
targetDir = parentDir;
|
|
137
|
+
useCurrentDir = true;
|
|
138
|
+
const contents = readdirSync(targetDir).filter((f) => !IGNORED_ENTRIES.has(f));
|
|
139
|
+
if (contents.length > 0) {
|
|
140
|
+
logger.error(`Current directory ${chalk.bold(targetDir)} is not empty. Cannot create a project here.`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
name = basename(targetDir) || 'my-project';
|
|
144
|
+
} else {
|
|
145
|
+
// Named project — re-prompt if directory already exists and is non-empty
|
|
146
|
+
targetDir = join(parentDir, name);
|
|
147
|
+
while (existsSync(targetDir) && !isEmptyDir(targetDir)) {
|
|
148
|
+
logger.warn(`Directory ${chalk.bold(name)} already exists and is not empty.`);
|
|
149
|
+
const newName = await themedInput({
|
|
150
|
+
message: 'Enter a different project name:',
|
|
151
|
+
hint: 'Letters, numbers, dots, hyphens, underscores only.',
|
|
152
|
+
validate: (v) => {
|
|
153
|
+
const t = v.trim();
|
|
154
|
+
if (!t) return 'Name is required.';
|
|
155
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(t)) return 'Only letters, numbers, dots, hyphens, and underscores allowed.';
|
|
156
|
+
return true;
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
name = newName.trim();
|
|
160
|
+
targetDir = join(parentDir, name);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
78
163
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
} else {
|
|
85
|
-
persona = await gatherPersona();
|
|
86
|
-
}
|
|
164
|
+
// Create directory + git init
|
|
165
|
+
const spinner1 = ora(useCurrentDir ? 'Initializing project in current directory...' : 'Creating project directory...').start();
|
|
166
|
+
if (!useCurrentDir && !existsSync(targetDir)) {
|
|
167
|
+
mkdirSync(targetDir, { recursive: true });
|
|
168
|
+
}
|
|
87
169
|
|
|
88
|
-
|
|
170
|
+
writeFileSync(
|
|
171
|
+
join(targetDir, '.gitignore'),
|
|
172
|
+
'node_modules/\ndist/\nbuild/\n.env\n.env.*\n!.env.example\n*.log\n.DS_Store\nThumbs.db\n',
|
|
173
|
+
'utf8',
|
|
174
|
+
);
|
|
89
175
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
176
|
+
try {
|
|
177
|
+
const { file, args } = platformCmd('git', ['init']);
|
|
178
|
+
execFileSync(file, args, { cwd: targetDir, stdio: 'pipe', windowsHide: true });
|
|
179
|
+
spinner1.succeed(useCurrentDir
|
|
180
|
+
? `Initialized project in ${chalk.bold(targetDir)} with git.`
|
|
181
|
+
: `Created ${chalk.bold(name)}/ with git initialized.`);
|
|
182
|
+
} catch {
|
|
183
|
+
spinner1.succeed(useCurrentDir
|
|
184
|
+
? `Initialized project in ${chalk.bold(targetDir)} (git init skipped — git not available).`
|
|
185
|
+
: `Created ${chalk.bold(name)}/ (git init skipped — git not available).`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
createName = name;
|
|
189
|
+
createDescription = description;
|
|
190
|
+
// projectType is captured in the closure for Phase 2
|
|
191
|
+
options._createProjectType = projectType;
|
|
100
192
|
}
|
|
101
|
-
userProfile.persona = persona;
|
|
102
193
|
|
|
103
194
|
// ================================================================
|
|
104
195
|
// PHASE 2: Project Discovery
|
|
105
196
|
// ================================================================
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// ── Check for existing Claude setup ─────────────────────────────
|
|
109
|
-
let existingContext = null;
|
|
110
|
-
try {
|
|
111
|
-
const setupCtx = await runExistingSetupTasks(targetDir, {
|
|
112
|
-
detectExistingSetup,
|
|
113
|
-
extractExistingContext,
|
|
114
|
-
removeExistingSetup,
|
|
115
|
-
});
|
|
116
|
-
existingContext = setupCtx.existingContext || null;
|
|
117
|
-
} catch (setupErr) {
|
|
118
|
-
logger.debug(`Existing setup check failed: ${setupErr.message}`);
|
|
119
|
-
}
|
|
197
|
+
const phaseOpts = createMode ? { totalPhases: 6 } : {};
|
|
198
|
+
renderPhaseHeader(2, phaseOpts);
|
|
120
199
|
|
|
121
|
-
// ── Project analysis ────────────────────────────────────────────
|
|
122
200
|
let projectInfo;
|
|
123
201
|
let detected;
|
|
202
|
+
let existingContext = null;
|
|
124
203
|
|
|
125
|
-
|
|
126
|
-
|
|
204
|
+
if (createMode) {
|
|
205
|
+
// ── Create mode: synthetic analysis from description ──────────
|
|
206
|
+
const { analyzeDescription } = await import('../utils/description-analyzer.js');
|
|
207
|
+
|
|
208
|
+
let descAnalysis = null;
|
|
209
|
+
{
|
|
210
|
+
const spinnerAnalyze = ora('Analyzing your requirements...').start();
|
|
211
|
+
const { analysis, failReason } = await analyzeDescription(createDescription, options._createProjectType);
|
|
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
|
+
}
|
|
127
227
|
|
|
128
|
-
|
|
228
|
+
projectInfo = {
|
|
229
|
+
name: createName,
|
|
230
|
+
description: createDescription,
|
|
231
|
+
projectType: options._createProjectType || 'monolith',
|
|
232
|
+
languages: descAnalysis?.languages?.length ? descAnalysis.languages : [],
|
|
233
|
+
frameworks: descAnalysis?.frameworks?.length ? descAnalysis.frameworks : [],
|
|
234
|
+
codeStyle: descAnalysis?.codeStyle?.length ? descAnalysis.codeStyle : [],
|
|
235
|
+
cicd: descAnalysis?.cicd?.length ? descAnalysis.cicd : [],
|
|
236
|
+
subprojects: descAnalysis?.subprojects?.length ? descAnalysis.subprojects : [],
|
|
237
|
+
architecture: descAnalysis?.architecture || '',
|
|
238
|
+
buildCommands: descAnalysis?.buildCommands || {},
|
|
239
|
+
complexity: descAnalysis?.complexity ?? 0.3,
|
|
240
|
+
metrics: descAnalysis?.metrics || null,
|
|
241
|
+
entryPoints: descAnalysis?.entryPoints || [],
|
|
242
|
+
coreModules: descAnalysis?.coreModules || [],
|
|
243
|
+
testFramework: descAnalysis?.testFramework || '',
|
|
244
|
+
packageManager: descAnalysis?.packageManager || '',
|
|
245
|
+
languageDistribution: descAnalysis?.languageDistribution || null,
|
|
246
|
+
};
|
|
129
247
|
|
|
130
|
-
|
|
131
|
-
projectInfo
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
248
|
+
detected = {
|
|
249
|
+
...projectInfo,
|
|
250
|
+
sensitiveFiles: { found: [], gitignoreCovers: true },
|
|
251
|
+
_rootFiles: [],
|
|
252
|
+
databases: descAnalysis?.databases?.length ? descAnalysis.databases : [],
|
|
253
|
+
};
|
|
254
|
+
} else {
|
|
255
|
+
// ── Install mode: real project analysis ───────────────────────
|
|
256
|
+
try {
|
|
257
|
+
const setupCtx = await runExistingSetupTasks(targetDir, {
|
|
258
|
+
detectExistingSetup,
|
|
259
|
+
extractExistingContext,
|
|
260
|
+
removeExistingSetup,
|
|
261
|
+
});
|
|
262
|
+
existingContext = setupCtx.existingContext || null;
|
|
263
|
+
} catch (setupErr) {
|
|
264
|
+
logger.debug(`Existing setup check failed: ${setupErr.message}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const ctx = await runAnalysisTasks(targetDir, { analyzeWithClaude, detectProject, existingContext });
|
|
269
|
+
|
|
270
|
+
const { claudeAnalysis, claudeFailReason, fsDetected } = ctx;
|
|
271
|
+
|
|
272
|
+
if (claudeAnalysis) {
|
|
273
|
+
projectInfo = {
|
|
274
|
+
name: claudeAnalysis.name || fsDetected.name || 'my-project',
|
|
275
|
+
description: claudeAnalysis.description || fsDetected.description || '',
|
|
276
|
+
projectType: claudeAnalysis.projectType || fsDetected.projectType || 'monolith',
|
|
277
|
+
languages: claudeAnalysis.languages.length ? claudeAnalysis.languages : (fsDetected.languages.length ? fsDetected.languages : ['JavaScript']),
|
|
278
|
+
frameworks: claudeAnalysis.frameworks.length ? claudeAnalysis.frameworks : fsDetected.frameworks,
|
|
279
|
+
codeStyle: claudeAnalysis.codeStyle.length ? claudeAnalysis.codeStyle : fsDetected.codeStyle,
|
|
280
|
+
cicd: claudeAnalysis.cicd.length ? claudeAnalysis.cicd : fsDetected.cicd,
|
|
281
|
+
subprojects: claudeAnalysis.subprojects.length ? claudeAnalysis.subprojects : fsDetected.subprojects,
|
|
282
|
+
architecture: claudeAnalysis.architecture || '',
|
|
283
|
+
buildCommands: claudeAnalysis.buildCommands || {},
|
|
284
|
+
complexity: claudeAnalysis.complexity ?? 0.5,
|
|
285
|
+
metrics: claudeAnalysis.metrics || null,
|
|
286
|
+
entryPoints: claudeAnalysis.entryPoints || [],
|
|
287
|
+
coreModules: claudeAnalysis.coreModules || [],
|
|
288
|
+
testFramework: claudeAnalysis.testFramework || '',
|
|
289
|
+
packageManager: claudeAnalysis.packageManager || fsDetected.packageManager || '',
|
|
290
|
+
languageDistribution: claudeAnalysis.languageDistribution || fsDetected.languageDistribution || null,
|
|
291
|
+
};
|
|
292
|
+
} else {
|
|
293
|
+
projectInfo = {
|
|
294
|
+
name: fsDetected.name || targetDir.split(/[/\\]/).filter(Boolean).pop() || 'my-project',
|
|
295
|
+
description: fsDetected.description || '',
|
|
296
|
+
projectType: fsDetected.projectType || 'monolith',
|
|
297
|
+
languages: fsDetected.languages.length ? fsDetected.languages : ['JavaScript'],
|
|
298
|
+
frameworks: fsDetected.frameworks,
|
|
299
|
+
codeStyle: fsDetected.codeStyle,
|
|
300
|
+
cicd: fsDetected.cicd,
|
|
301
|
+
subprojects: fsDetected.subprojects,
|
|
302
|
+
architecture: '',
|
|
303
|
+
buildCommands: {},
|
|
304
|
+
complexity: 0.5,
|
|
305
|
+
metrics: null,
|
|
306
|
+
entryPoints: [],
|
|
307
|
+
coreModules: [],
|
|
308
|
+
testFramework: '',
|
|
309
|
+
packageManager: fsDetected.packageManager || '',
|
|
310
|
+
languageDistribution: fsDetected.languageDistribution || null,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
detected = { ...fsDetected, ...projectInfo };
|
|
315
|
+
} catch (analysisErr) {
|
|
316
|
+
// Fallback if task runner fails — run sequentially with spinner
|
|
317
|
+
const spinner = ora('Analyzing project...').start();
|
|
318
|
+
const fsDetected = await detectProject(targetDir);
|
|
319
|
+
spinner.succeed('Project scanned.');
|
|
151
320
|
projectInfo = {
|
|
152
321
|
name: fsDetected.name || targetDir.split(/[/\\]/).filter(Boolean).pop() || 'my-project',
|
|
153
322
|
description: fsDetected.description || '',
|
|
@@ -167,77 +336,31 @@ export async function runInstall(options = {}) {
|
|
|
167
336
|
packageManager: fsDetected.packageManager || '',
|
|
168
337
|
languageDistribution: fsDetected.languageDistribution || null,
|
|
169
338
|
};
|
|
339
|
+
detected = { ...fsDetected, ...projectInfo };
|
|
170
340
|
}
|
|
171
341
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const spinner = ora('Analyzing project...').start();
|
|
176
|
-
const fsDetected = await detectProject(targetDir);
|
|
177
|
-
spinner.succeed('Project scanned.');
|
|
178
|
-
projectInfo = {
|
|
179
|
-
name: fsDetected.name || targetDir.split(/[/\\]/).filter(Boolean).pop() || 'my-project',
|
|
180
|
-
description: fsDetected.description || '',
|
|
181
|
-
projectType: fsDetected.projectType || 'monolith',
|
|
182
|
-
languages: fsDetected.languages.length ? fsDetected.languages : ['JavaScript'],
|
|
183
|
-
frameworks: fsDetected.frameworks,
|
|
184
|
-
codeStyle: fsDetected.codeStyle,
|
|
185
|
-
cicd: fsDetected.cicd,
|
|
186
|
-
subprojects: fsDetected.subprojects,
|
|
187
|
-
architecture: '',
|
|
188
|
-
buildCommands: {},
|
|
189
|
-
complexity: 0.5,
|
|
190
|
-
metrics: null,
|
|
191
|
-
entryPoints: [],
|
|
192
|
-
coreModules: [],
|
|
193
|
-
testFramework: '',
|
|
194
|
-
packageManager: fsDetected.packageManager || '',
|
|
195
|
-
languageDistribution: fsDetected.languageDistribution || null,
|
|
196
|
-
};
|
|
197
|
-
detected = { ...fsDetected, ...projectInfo };
|
|
342
|
+
// Display results (install mode only — create mode has no real project to show)
|
|
343
|
+
console.log();
|
|
344
|
+
renderProjectCard(projectInfo);
|
|
198
345
|
}
|
|
199
346
|
|
|
200
|
-
// Cache analysis
|
|
347
|
+
// Cache analysis for later phases and future update runs
|
|
201
348
|
try {
|
|
202
349
|
writeAnalysisCache(targetDir, projectInfo, detected, existingContext);
|
|
203
|
-
writeUserProfile(targetDir, userProfile);
|
|
204
350
|
} catch (cacheErr) {
|
|
205
351
|
logger.debug(`Analysis cache write failed: ${cacheErr.message}`);
|
|
206
352
|
}
|
|
207
353
|
|
|
208
|
-
// Display results
|
|
209
|
-
console.log();
|
|
210
|
-
renderProjectCard(projectInfo);
|
|
211
|
-
|
|
212
|
-
// Preset alias injection
|
|
213
|
-
if (options.preset) {
|
|
214
|
-
const alias = PRESET_ALIASES[options.preset];
|
|
215
|
-
if (!alias) {
|
|
216
|
-
logger.error(
|
|
217
|
-
`Unknown preset ${chalk.bold(options.preset)}. ` +
|
|
218
|
-
`Available presets: ${Object.keys(PRESET_ALIASES).map((p) => colors.success(p)).join(', ')}`
|
|
219
|
-
);
|
|
220
|
-
process.exit(1);
|
|
221
|
-
}
|
|
222
|
-
for (const fw of alias.frameworks) {
|
|
223
|
-
if (!projectInfo.frameworks.includes(fw)) projectInfo.frameworks.push(fw);
|
|
224
|
-
}
|
|
225
|
-
for (const lang of alias.languages) {
|
|
226
|
-
if (!projectInfo.languages.includes(lang)) projectInfo.languages.push(lang);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
354
|
// ================================================================
|
|
231
355
|
// PHASE 3: Configuration
|
|
232
356
|
// ================================================================
|
|
233
|
-
renderPhaseHeader(3);
|
|
357
|
+
renderPhaseHeader(3, phaseOpts);
|
|
234
358
|
|
|
235
359
|
const spinner3 = ora('Calling claude-craft server...').start();
|
|
236
360
|
|
|
237
361
|
let apiResponse;
|
|
238
362
|
try {
|
|
239
363
|
apiResponse = await callGenerate(
|
|
240
|
-
userProfile,
|
|
241
364
|
{
|
|
242
365
|
name: projectInfo.name,
|
|
243
366
|
projectType: projectInfo.projectType,
|
|
@@ -258,7 +381,7 @@ export async function runInstall(options = {}) {
|
|
|
258
381
|
detectedFiles: detected._rootFiles || [],
|
|
259
382
|
databases: projectInfo.databases || detected.databases || [],
|
|
260
383
|
},
|
|
261
|
-
{
|
|
384
|
+
{ projectPath: targetDir },
|
|
262
385
|
);
|
|
263
386
|
spinner3.succeed('Server returned configuration.');
|
|
264
387
|
} catch (err) {
|
|
@@ -272,27 +395,17 @@ export async function runInstall(options = {}) {
|
|
|
272
395
|
|
|
273
396
|
const { summary, mcpConfigs } = apiResponse;
|
|
274
397
|
|
|
275
|
-
// Display
|
|
398
|
+
// Display component summary
|
|
276
399
|
console.log();
|
|
277
400
|
renderComponentBreakdown(summary);
|
|
278
401
|
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (options.yes || isVibe) {
|
|
285
|
-
selectedMcps = mcpConfigs.filter(
|
|
286
|
-
(m) => m.tier === 'core' || m.tier === 'role' || m.tier === 'stack' || m.tier === 'auto' || m.recommended
|
|
287
|
-
);
|
|
288
|
-
securityConfig = { addSecurityGitignore: true };
|
|
289
|
-
} else {
|
|
290
|
-
const mcpResult = await gatherMcpConfig(mcpConfigs);
|
|
291
|
-
selectedMcps = mcpResult.selectedMcps;
|
|
292
|
-
mcpKeys = mcpResult.mcpKeys;
|
|
293
|
-
|
|
294
|
-
securityConfig = gatherSecurityConfig(detected);
|
|
402
|
+
// All MCPs are auto-installed; no interactive selection
|
|
403
|
+
const selectedMcps = mcpConfigs || [];
|
|
404
|
+
const mcpKeys = {};
|
|
405
|
+
const securityConfig = { addSecurityGitignore: true };
|
|
295
406
|
|
|
407
|
+
// Confirmation gate (skipped in create mode and non-interactive mode)
|
|
408
|
+
if (!createMode && !options.yes) {
|
|
296
409
|
const finalSummary = { ...summary, mcps: selectedMcps.map((m) => ({ id: m.id, tier: m.tier })) };
|
|
297
410
|
const proceed = await confirmInstallation(finalSummary);
|
|
298
411
|
if (!proceed) {
|
|
@@ -305,16 +418,14 @@ export async function runInstall(options = {}) {
|
|
|
305
418
|
// ================================================================
|
|
306
419
|
// PHASE 4: Installation
|
|
307
420
|
// ================================================================
|
|
308
|
-
renderPhaseHeader(4);
|
|
421
|
+
renderPhaseHeader(4, phaseOpts);
|
|
309
422
|
|
|
310
423
|
let results;
|
|
311
424
|
let filesToWrite;
|
|
312
|
-
let selectedCandidateIds = null;
|
|
313
425
|
|
|
314
426
|
try {
|
|
315
427
|
const installCtx = await runInstallTasks({
|
|
316
428
|
apiResponse,
|
|
317
|
-
selectedCandidateIds: null,
|
|
318
429
|
targetDir,
|
|
319
430
|
selectedMcps,
|
|
320
431
|
mcpKeys,
|
|
@@ -322,29 +433,14 @@ export async function runInstall(options = {}) {
|
|
|
322
433
|
detected,
|
|
323
434
|
buildFileList,
|
|
324
435
|
writeApiFiles,
|
|
325
|
-
scoreWithClaude,
|
|
326
436
|
});
|
|
327
437
|
|
|
328
438
|
results = installCtx.results;
|
|
329
439
|
filesToWrite = installCtx.filesToWrite;
|
|
330
|
-
selectedCandidateIds = installCtx.selectedCandidateIds ?? null;
|
|
331
440
|
} catch (installErr) {
|
|
332
441
|
// Fallback to sequential if task runner fails
|
|
333
|
-
const candidateCount = apiResponse.candidates?.items?.length || 0;
|
|
334
|
-
|
|
335
|
-
if (apiResponse.prompts?.scoring && candidateCount > 0) {
|
|
336
|
-
const spinner4 = ora(`Evaluating ${candidateCount} optional candidates...`).start();
|
|
337
|
-
const scoreResult = await scoreWithClaude(apiResponse.prompts.scoring, targetDir);
|
|
338
|
-
if (scoreResult.selected) {
|
|
339
|
-
selectedCandidateIds = scoreResult.selected;
|
|
340
|
-
spinner4.succeed(`Selected ${selectedCandidateIds.length}/${candidateCount} candidates.`);
|
|
341
|
-
} else {
|
|
342
|
-
spinner4.warn(`Scoring unavailable — including all ${candidateCount} candidates.`);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
442
|
const spinnerWrite = ora('Writing configuration files...').start();
|
|
347
|
-
filesToWrite = buildFileList(apiResponse
|
|
443
|
+
filesToWrite = buildFileList(apiResponse);
|
|
348
444
|
results = await writeApiFiles(filesToWrite, targetDir, {
|
|
349
445
|
force: true,
|
|
350
446
|
selectedMcpIds: selectedMcps.map((m) => m.id),
|
|
@@ -391,10 +487,81 @@ export async function runInstall(options = {}) {
|
|
|
391
487
|
}
|
|
392
488
|
|
|
393
489
|
// ================================================================
|
|
394
|
-
//
|
|
490
|
+
// CREATE MODE: Bootstrap
|
|
491
|
+
// ================================================================
|
|
492
|
+
let bootstrapSucceeded = true;
|
|
493
|
+
|
|
494
|
+
if (createMode) {
|
|
495
|
+
renderPhaseHeader(5, { totalPhases: 6, name: 'Bootstrap' });
|
|
496
|
+
|
|
497
|
+
console.log(chalk.dim(' Handing off to Claude to scaffold your project...'));
|
|
498
|
+
console.log(chalk.dim(' This may take several minutes. Activity log:'));
|
|
499
|
+
console.log();
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
const { runBootstrap } = await import('../utils/bootstrap-runner.js');
|
|
503
|
+
await runBootstrap(targetDir, createDescription);
|
|
504
|
+
} catch (err) {
|
|
505
|
+
bootstrapSucceeded = false;
|
|
506
|
+
console.log();
|
|
507
|
+
logger.warn('Bootstrap did not complete: ' + err.message);
|
|
508
|
+
logger.info('Your .claude/ configuration is still intact. You can run /bootstrap:auto manually inside the project.');
|
|
509
|
+
console.log();
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ================================================================
|
|
514
|
+
// FINALIZATION
|
|
395
515
|
// ================================================================
|
|
396
|
-
renderPhaseHeader(5);
|
|
516
|
+
renderPhaseHeader(createMode ? 6 : 5, createMode ? { totalPhases: 6, name: 'Finalization' } : {});
|
|
397
517
|
|
|
518
|
+
// Create mode: re-analyze after bootstrap
|
|
519
|
+
if (createMode && bootstrapSucceeded) {
|
|
520
|
+
try {
|
|
521
|
+
const spinnerReanalyze = ora('Re-analyzing project...').start();
|
|
522
|
+
const fsDetected = await detectProject(targetDir);
|
|
523
|
+
|
|
524
|
+
let reanalyzedInfo;
|
|
525
|
+
try {
|
|
526
|
+
const { analysis } = await analyzeWithClaude(targetDir);
|
|
527
|
+
if (analysis) {
|
|
528
|
+
reanalyzedInfo = {
|
|
529
|
+
name: analysis.name || fsDetected.name || createName,
|
|
530
|
+
description: analysis.description || fsDetected.description || createDescription,
|
|
531
|
+
projectType: analysis.projectType || fsDetected.projectType || options._createProjectType,
|
|
532
|
+
languages: analysis.languages?.length ? analysis.languages : fsDetected.languages,
|
|
533
|
+
frameworks: analysis.frameworks?.length ? analysis.frameworks : fsDetected.frameworks,
|
|
534
|
+
codeStyle: analysis.codeStyle?.length ? analysis.codeStyle : fsDetected.codeStyle,
|
|
535
|
+
cicd: analysis.cicd?.length ? analysis.cicd : fsDetected.cicd,
|
|
536
|
+
subprojects: analysis.subprojects?.length ? analysis.subprojects : fsDetected.subprojects,
|
|
537
|
+
architecture: analysis.architecture || '',
|
|
538
|
+
buildCommands: analysis.buildCommands || {},
|
|
539
|
+
complexity: analysis.complexity ?? 0.5,
|
|
540
|
+
metrics: analysis.metrics || null,
|
|
541
|
+
entryPoints: analysis.entryPoints || [],
|
|
542
|
+
coreModules: analysis.coreModules || [],
|
|
543
|
+
testFramework: analysis.testFramework || '',
|
|
544
|
+
packageManager: analysis.packageManager || fsDetected.packageManager || '',
|
|
545
|
+
languageDistribution: analysis.languageDistribution || fsDetected.languageDistribution || null,
|
|
546
|
+
};
|
|
547
|
+
} else {
|
|
548
|
+
reanalyzedInfo = buildProjectInfoFromFs(fsDetected, createName, createDescription, options._createProjectType);
|
|
549
|
+
}
|
|
550
|
+
} catch {
|
|
551
|
+
reanalyzedInfo = buildProjectInfoFromFs(fsDetected, createName, createDescription, options._createProjectType);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
spinnerReanalyze.succeed('Project re-analyzed.');
|
|
555
|
+
|
|
556
|
+
// Overwrite cache with real data
|
|
557
|
+
const reanalyzedDetected = { ...fsDetected, ...reanalyzedInfo };
|
|
558
|
+
writeAnalysisCache(targetDir, reanalyzedInfo, reanalyzedDetected, null);
|
|
559
|
+
} catch (err) {
|
|
560
|
+
logger.debug(`Post-bootstrap analysis failed: ${err.message}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Shared finalization — optimize settings + rewrite CLAUDE.md
|
|
398
565
|
try {
|
|
399
566
|
const finCtx = await runFinalizeTasks({
|
|
400
567
|
targetDir,
|
|
@@ -403,7 +570,6 @@ export async function runInstall(options = {}) {
|
|
|
403
570
|
rewriteClaudeMd,
|
|
404
571
|
});
|
|
405
572
|
|
|
406
|
-
// Show what was replaced
|
|
407
573
|
const opt = finCtx.optimizationResult;
|
|
408
574
|
if (opt?.status === 'ok' && opt.applied > 0 && opt.replacements?.length > 0) {
|
|
409
575
|
for (const label of opt.replacements) {
|
|
@@ -411,9 +577,6 @@ export async function runInstall(options = {}) {
|
|
|
411
577
|
}
|
|
412
578
|
}
|
|
413
579
|
} catch {
|
|
414
|
-
// Fallback to sequential
|
|
415
|
-
// toolkit-usage.md no longer generated — capability-map.md covers routing
|
|
416
|
-
|
|
417
580
|
const spinner7 = ora('Optimizing settings...').start();
|
|
418
581
|
const optResult = optimizeSettings(targetDir);
|
|
419
582
|
|
|
@@ -434,18 +597,27 @@ export async function runInstall(options = {}) {
|
|
|
434
597
|
}
|
|
435
598
|
|
|
436
599
|
// ── Success ──────────────────────────────────────────────────────
|
|
437
|
-
const totalItems = countTotalItems(summary
|
|
600
|
+
const totalItems = countTotalItems(summary);
|
|
438
601
|
const mcpsNeedingKeys = mcpResults.filter((r) => r.status === 'needs-key');
|
|
439
602
|
|
|
440
603
|
renderSuccessCard({
|
|
441
604
|
totalItems,
|
|
442
605
|
mcpCount: selectedMcps.length,
|
|
443
606
|
mcpsNeedingKeys,
|
|
444
|
-
persona,
|
|
445
607
|
});
|
|
446
608
|
|
|
447
609
|
console.log();
|
|
448
|
-
|
|
610
|
+
if (createMode) {
|
|
611
|
+
if (bootstrapSucceeded) {
|
|
612
|
+
logger.success(`Project ${chalk.bold(createName)} created and bootstrapped!`);
|
|
613
|
+
console.log(chalk.dim(` cd ${createName} && claude`));
|
|
614
|
+
} else {
|
|
615
|
+
logger.success(`Project ${chalk.bold(createName)} created with Claude configuration.`);
|
|
616
|
+
console.log(chalk.dim(` cd ${createName} && claude -p "/bootstrap:auto ${createDescription}"`));
|
|
617
|
+
}
|
|
618
|
+
} else {
|
|
619
|
+
logger.success('Done! Claude Code is ready.');
|
|
620
|
+
}
|
|
449
621
|
console.log();
|
|
450
622
|
} catch (err) {
|
|
451
623
|
if (
|
|
@@ -475,9 +647,9 @@ export async function runInstall(options = {}) {
|
|
|
475
647
|
}
|
|
476
648
|
|
|
477
649
|
/**
|
|
478
|
-
* Count total installed items from
|
|
650
|
+
* Count total installed items from the summary.
|
|
479
651
|
*/
|
|
480
|
-
function countTotalItems(summary
|
|
652
|
+
function countTotalItems(summary) {
|
|
481
653
|
const countBucket = (bucket) => {
|
|
482
654
|
if (!bucket) return 0;
|
|
483
655
|
return Object.values(bucket).reduce(
|
|
@@ -486,13 +658,30 @@ function countTotalItems(summary, selectedCandidateIds) {
|
|
|
486
658
|
);
|
|
487
659
|
};
|
|
488
660
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
if (selectedCandidateIds === null) {
|
|
492
|
-
total += countBucket(summary.candidates);
|
|
493
|
-
} else {
|
|
494
|
-
total += selectedCandidateIds.length;
|
|
495
|
-
}
|
|
661
|
+
return countBucket(summary.guaranteed) + countBucket(summary.candidates);
|
|
662
|
+
}
|
|
496
663
|
|
|
497
|
-
|
|
664
|
+
/**
|
|
665
|
+
* Build projectInfo from filesystem detection only (fallback for create mode re-analysis).
|
|
666
|
+
*/
|
|
667
|
+
function buildProjectInfoFromFs(fsDetected, name, description, projectType) {
|
|
668
|
+
return {
|
|
669
|
+
name: fsDetected.name || name,
|
|
670
|
+
description: fsDetected.description || description,
|
|
671
|
+
projectType: fsDetected.projectType || projectType,
|
|
672
|
+
languages: fsDetected.languages?.length ? fsDetected.languages : [],
|
|
673
|
+
frameworks: fsDetected.frameworks || [],
|
|
674
|
+
codeStyle: fsDetected.codeStyle || [],
|
|
675
|
+
cicd: fsDetected.cicd || [],
|
|
676
|
+
subprojects: fsDetected.subprojects || [],
|
|
677
|
+
architecture: '',
|
|
678
|
+
buildCommands: {},
|
|
679
|
+
complexity: 0.5,
|
|
680
|
+
metrics: null,
|
|
681
|
+
entryPoints: [],
|
|
682
|
+
coreModules: [],
|
|
683
|
+
testFramework: '',
|
|
684
|
+
packageManager: fsDetected.packageManager || '',
|
|
685
|
+
languageDistribution: fsDetected.languageDistribution || null,
|
|
686
|
+
};
|
|
498
687
|
}
|