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.
@@ -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
- gatherPersona,
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 { PRESET_ALIASES, VERSION, VIBE_DEFAULTS } from '../constants.js';
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
- import { colors } from '../ui/theme.js';
44
- import { dotPad } from '../ui/format.js';
42
+
43
+ // ── Create-mode detection ───────────────────────────────────────────────────
44
+
45
+ const IGNORED_ENTRIES = new Set(['.git', '.DS_Store', 'Thumbs.db', '.gitkeep']);
45
46
 
46
47
  /**
47
- * Main install command 5-phase orchestrator.
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
- * Phase 1: Welcome & Setup (env check + user profile)
50
- * Phase 2: Project Discovery (Claude analysis + filesystem scan)
51
- * Phase 3: Configuration (server API + MCP selection + security + confirmation)
52
- * Phase 4: Installation (Claude scoring + file writing + MCP verification)
53
- * Phase 5: Finalization (settings optimization + CLAUDE.md rewrite + success)
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
- // ── Project path ────────────────────────────────────────────────
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
- // ── Persona selection ────────────────────────────────────────────
77
- renderPhaseHeader(1);
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
- let persona;
80
- if (options.yes) {
81
- persona = 'developer'; // non-interactive: always developer defaults
82
- } else if (options.pro) {
83
- persona = 'developer'; // interactive developer mode: skip persona prompt
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
- const isVibe = persona === 'vibe';
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
- // ── User profile ───────────────────────────────────────────────
91
- let userProfile;
92
- if (options.yes) {
93
- userProfile = { intents: ['implementing', 'debugging', 'refactoring', 'testing', 'reviewing'], sourceControl: 'github', documentTools: [] };
94
- logger.info('Non-interactive mode using default profile (web, all intents, github).');
95
- } else if (isVibe) {
96
- userProfile = { ...VIBE_DEFAULTS };
97
- logger.info('Vibe mode — using streamlined defaults.');
98
- } else {
99
- userProfile = await gatherUserProfile();
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
- renderPhaseHeader(2);
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
- try {
126
- const ctx = await runAnalysisTasks(targetDir, { analyzeWithClaude, detectProject, existingContext });
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
- const { claudeAnalysis, claudeFailReason, fsDetected } = ctx;
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
- if (claudeAnalysis) {
131
- projectInfo = {
132
- name: claudeAnalysis.name || fsDetected.name || 'my-project',
133
- description: claudeAnalysis.description || fsDetected.description || '',
134
- projectType: claudeAnalysis.projectType || fsDetected.projectType || 'monolith',
135
- languages: claudeAnalysis.languages.length ? claudeAnalysis.languages : (fsDetected.languages.length ? fsDetected.languages : ['JavaScript']),
136
- frameworks: claudeAnalysis.frameworks.length ? claudeAnalysis.frameworks : fsDetected.frameworks,
137
- codeStyle: claudeAnalysis.codeStyle.length ? claudeAnalysis.codeStyle : fsDetected.codeStyle,
138
- cicd: claudeAnalysis.cicd.length ? claudeAnalysis.cicd : fsDetected.cicd,
139
- subprojects: claudeAnalysis.subprojects.length ? claudeAnalysis.subprojects : fsDetected.subprojects,
140
- architecture: claudeAnalysis.architecture || '',
141
- buildCommands: claudeAnalysis.buildCommands || {},
142
- complexity: claudeAnalysis.complexity ?? 0.5,
143
- metrics: claudeAnalysis.metrics || null,
144
- entryPoints: claudeAnalysis.entryPoints || [],
145
- coreModules: claudeAnalysis.coreModules || [],
146
- testFramework: claudeAnalysis.testFramework || '',
147
- packageManager: claudeAnalysis.packageManager || fsDetected.packageManager || '',
148
- languageDistribution: claudeAnalysis.languageDistribution || fsDetected.languageDistribution || null,
149
- };
150
- } else {
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
- detected = { ...fsDetected, ...projectInfo };
173
- } catch (analysisErr) {
174
- // Fallback if task runner fails — run sequentially with spinner
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 and user profile for later phases and future update runs
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
- { preset: options.preset, projectPath: targetDir },
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 scoring summary
398
+ // Display component summary
276
399
  console.log();
277
400
  renderComponentBreakdown(summary);
278
401
 
279
- // MCP selection + credential setup + security + confirmation
280
- let selectedMcps;
281
- let mcpKeys = {};
282
- let securityConfig;
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, selectedCandidateIds);
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
- // PHASE 5: Finalization
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, selectedCandidateIds);
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
- logger.success(isVibe ? 'Done! Start Claude Code and describe what you want to build.' : 'Done! Claude Code is ready.');
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 guaranteed + selected candidates.
650
+ * Count total installed items from the summary.
479
651
  */
480
- function countTotalItems(summary, selectedCandidateIds) {
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
- let total = countBucket(summary.guaranteed);
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
- return total;
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
  }