ccraft 1.0.12 → 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 CHANGED
@@ -39,10 +39,10 @@ That's it. Your `.claude/` directory is now populated with agents, skills, rules
39
39
  ### Starting a New Project?
40
40
 
41
41
  ```bash
42
- ccraft create
42
+ ccraft install --name my-app --description "REST API for inventory management"
43
43
  ```
44
44
 
45
- The `create` command scaffolds a new project from scratch, runs Claude Code's bootstrap, then generates the full `.claude/` configuration on top.
45
+ `ccraft install` auto-detects whether you're working with an existing project or starting fresh. If the target directory is empty or doesn't exist, it switches to new-project mode — scaffolds the directory, runs Claude Code's bootstrap, then generates the full `.claude/` configuration on top.
46
46
 
47
47
  ## How It Works
48
48
 
@@ -106,7 +106,9 @@ ccraft auth sk-xxxxxxxxxxxx --server https://custom-server.example.com
106
106
 
107
107
  ### `ccraft install`
108
108
 
109
- Generate Claude Code configuration for an existing project. The main command most users need.
109
+ Generate Claude Code configuration. Auto-detects whether you're configuring an existing project or creating a new one.
110
+
111
+ **Existing project** (target directory has files):
110
112
 
111
113
  ```bash
112
114
  ccraft install # Interactive mode
@@ -115,18 +117,27 @@ ccraft install -p nextjs # Apply a framework preset
115
117
  ccraft install -d /path/to/project # Target a specific directory
116
118
  ```
117
119
 
118
- **Available presets:** `nextjs`, `go-api`, `python`, `rust`, `aspnet`, `cmake`
119
-
120
- ### `ccraft create`
121
-
122
- Scaffold a new project from scratch with Claude Code configuration built in.
120
+ **New project** (target directory is empty/missing, or `--name`/`--description` provided):
123
121
 
124
122
  ```bash
125
- ccraft create # Interactive mode
126
- ccraft create -y --name my-app # Non-interactive with project name
127
- ccraft create -y --name my-app --description "REST API for inventory management"
123
+ ccraft install --name my-app --description "REST API for inventory management"
124
+ ccraft install -d ./new-project # Empty dir triggers new-project mode
125
+ ccraft install -y --name my-app # Non-interactive new project
128
126
  ```
129
127
 
128
+ In new-project mode, ccraft creates the directory, initializes git, generates `.claude/` configuration, then runs Claude Code's `/bootstrap:auto` to scaffold the project.
129
+
130
+ **Options:**
131
+
132
+ | Flag | Description |
133
+ |------|-------------|
134
+ | `-y, --yes` | Accept all defaults (non-interactive) |
135
+ | `-n, --name <name>` | Project name (triggers new-project mode) |
136
+ | `--description <text>` | Project description (triggers new-project mode) |
137
+ | `-p, --preset <preset>` | Apply a framework preset (`nextjs`, `go-api`, `python`, `rust`, `aspnet`, `cmake`) |
138
+ | `--pro` | Developer mode — skip persona selection, show all options |
139
+ | `-d, --dir <path>` | Target directory (default: cwd) |
140
+
130
141
  ### `ccraft update`
131
142
 
132
143
  Re-analyze your project and install new components for any stack changes. Run this after adding new frameworks or dependencies.
@@ -35,7 +35,6 @@ loadEnvFile(resolvePath(cliRoot, '.env'));
35
35
  import { Command } from 'commander';
36
36
  import { VERSION, PRESET_ALIASES } from '../src/constants.js';
37
37
  import { runInstall } from '../src/commands/install.js';
38
- import { runCreate } from '../src/commands/create.js';
39
38
  import { runUpdate } from '../src/commands/update.js';
40
39
  import { runAuth } from '../src/commands/auth.js';
41
40
  import { runLogout } from '../src/commands/logout.js';
@@ -58,20 +57,12 @@ program
58
57
  .description('Remove stored API key')
59
58
  .action(runLogout);
60
59
 
61
- program
62
- .command('create')
63
- .description('Create a new project from scratch with Claude scaffolding')
64
- .option('-y, --yes', 'Accept all defaults (non-interactive)')
65
- .option('-n, --name <name>', 'Project name (non-interactive mode)')
66
- .option('--description <text>', 'Project description (non-interactive mode)')
67
- .option('--pro', 'Developer mode — skip persona selection, show all options')
68
- .option('-d, --dir <path>', 'Parent directory to create the project in (default: cwd)')
69
- .action(runCreate);
70
-
71
60
  program
72
61
  .command('install')
73
- .description('Generate Claude Code configuration files in the current project')
62
+ .description('Generate Claude Code configuration auto-detects new vs existing projects')
74
63
  .option('-y, --yes', 'Accept all defaults (non-interactive)')
64
+ .option('-n, --name <name>', 'Project name (triggers new-project mode)')
65
+ .option('--description <text>', 'Project description (triggers new-project mode)')
75
66
  .option(`-p, --preset <preset>`, `Apply a framework preset (${Object.keys(PRESET_ALIASES).join(', ')})`)
76
67
  .option('--pro', 'Developer mode — skip persona selection, show all options')
77
68
  .option('-d, --dir <path>', 'Target directory (default: cwd)')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccraft",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
4
4
  "description": "Intelligent Claude Code project configurator — role-aware agents, skills, rules, MCPs, and workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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,13 +13,16 @@ import {
11
13
  } from '../utils/existing-setup.js';
12
14
  import {
13
15
  gatherProjectPath,
16
+ gatherCreateProfile,
14
17
  confirmInstallation,
15
18
  } from '../prompts/gather.js';
19
+ import { themedInput } from '../ui/prompts.js';
16
20
  import { callGenerate, ApiError } from '../utils/api-client.js';
17
21
  import { writeApiFiles, buildFileList } from '../utils/api-file-writer.js';
18
22
  import { setupMcps } from '../utils/mcp-setup.js';
19
23
  import { optimizeSettings } from '../utils/claude-optimizer.js';
20
24
  import { runPreflight } from '../utils/preflight.js';
25
+ import { platformCmd } from '../utils/run-claude.js';
21
26
  import {
22
27
  writeAnalysisCache,
23
28
  updateManifest,
@@ -35,17 +40,47 @@ import { renderProjectCard, renderSuccessCard } from '../ui/cards.js';
35
40
  import { renderComponentBreakdown, renderMcpStatus, renderFileResults } from '../ui/tables.js';
36
41
  import { runExistingSetupTasks, runAnalysisTasks, runInstallTasks, runVerifyTasks, runFinalizeTasks } from '../ui/tasks.js';
37
42
 
43
+ // ── Create-mode detection ───────────────────────────────────────────────────
44
+
45
+ const IGNORED_ENTRIES = new Set(['.git', '.DS_Store', 'Thumbs.db', '.gitkeep']);
46
+
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).
58
+ *
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
+
38
69
  /**
39
- * Main install command — 5-phase orchestrator.
70
+ * Main install command — unified orchestrator for both new and existing projects.
40
71
  *
41
- * Phase 1: Preflight (env check + project path)
42
- * Phase 2: Project Discovery (Claude analysis + filesystem scan)
43
- * Phase 3: Generate & Install (server API + confirmation + file writing)
44
- * Phase 4: MCP Verification
45
- * Phase 5: Finalization (settings optimization + CLAUDE.md rewrite + success)
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
46
77
  */
47
78
  export async function runInstall(options = {}) {
48
79
  let targetDir;
80
+ let createMode = false;
81
+ let createName = '';
82
+ let createDescription = '';
83
+
49
84
  try {
50
85
  // ================================================================
51
86
  // PHASE 1: Welcome & Setup
@@ -58,61 +93,230 @@ export async function runInstall(options = {}) {
58
93
  requireClaude: true,
59
94
  });
60
95
 
61
- // ── Project path ────────────────────────────────────────────────
62
- 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
63
99
  targetDir = await gatherProjectPath();
64
100
  } else {
65
101
  targetDir = resolve(options.dir || process.cwd());
66
102
  }
67
103
 
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
+ }
163
+
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
+ }
169
+
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
+ );
175
+
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;
192
+ }
193
+
68
194
  // ================================================================
69
195
  // PHASE 2: Project Discovery
70
196
  // ================================================================
71
- renderPhaseHeader(2);
72
-
73
- // ── Check for existing Claude setup ─────────────────────────────
74
- let existingContext = null;
75
- try {
76
- const setupCtx = await runExistingSetupTasks(targetDir, {
77
- detectExistingSetup,
78
- extractExistingContext,
79
- removeExistingSetup,
80
- });
81
- existingContext = setupCtx.existingContext || null;
82
- } catch (setupErr) {
83
- logger.debug(`Existing setup check failed: ${setupErr.message}`);
84
- }
197
+ const phaseOpts = createMode ? { totalPhases: 6 } : {};
198
+ renderPhaseHeader(2, phaseOpts);
85
199
 
86
- // ── Project analysis ────────────────────────────────────────────
87
200
  let projectInfo;
88
201
  let detected;
202
+ let existingContext = null;
89
203
 
90
- try {
91
- 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
+ }
92
227
 
93
- 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
+ };
94
247
 
95
- if (claudeAnalysis) {
96
- projectInfo = {
97
- name: claudeAnalysis.name || fsDetected.name || 'my-project',
98
- description: claudeAnalysis.description || fsDetected.description || '',
99
- projectType: claudeAnalysis.projectType || fsDetected.projectType || 'monolith',
100
- languages: claudeAnalysis.languages.length ? claudeAnalysis.languages : (fsDetected.languages.length ? fsDetected.languages : ['JavaScript']),
101
- frameworks: claudeAnalysis.frameworks.length ? claudeAnalysis.frameworks : fsDetected.frameworks,
102
- codeStyle: claudeAnalysis.codeStyle.length ? claudeAnalysis.codeStyle : fsDetected.codeStyle,
103
- cicd: claudeAnalysis.cicd.length ? claudeAnalysis.cicd : fsDetected.cicd,
104
- subprojects: claudeAnalysis.subprojects.length ? claudeAnalysis.subprojects : fsDetected.subprojects,
105
- architecture: claudeAnalysis.architecture || '',
106
- buildCommands: claudeAnalysis.buildCommands || {},
107
- complexity: claudeAnalysis.complexity ?? 0.5,
108
- metrics: claudeAnalysis.metrics || null,
109
- entryPoints: claudeAnalysis.entryPoints || [],
110
- coreModules: claudeAnalysis.coreModules || [],
111
- testFramework: claudeAnalysis.testFramework || '',
112
- packageManager: claudeAnalysis.packageManager || fsDetected.packageManager || '',
113
- languageDistribution: claudeAnalysis.languageDistribution || fsDetected.languageDistribution || null,
114
- };
115
- } 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.');
116
320
  projectInfo = {
117
321
  name: fsDetected.name || targetDir.split(/[/\\]/).filter(Boolean).pop() || 'my-project',
118
322
  description: fsDetected.description || '',
@@ -132,34 +336,12 @@ export async function runInstall(options = {}) {
132
336
  packageManager: fsDetected.packageManager || '',
133
337
  languageDistribution: fsDetected.languageDistribution || null,
134
338
  };
339
+ detected = { ...fsDetected, ...projectInfo };
135
340
  }
136
341
 
137
- detected = { ...fsDetected, ...projectInfo };
138
- } catch (analysisErr) {
139
- // Fallback if task runner fails — run sequentially with spinner
140
- const spinner = ora('Analyzing project...').start();
141
- const fsDetected = await detectProject(targetDir);
142
- spinner.succeed('Project scanned.');
143
- projectInfo = {
144
- name: fsDetected.name || targetDir.split(/[/\\]/).filter(Boolean).pop() || 'my-project',
145
- description: fsDetected.description || '',
146
- projectType: fsDetected.projectType || 'monolith',
147
- languages: fsDetected.languages.length ? fsDetected.languages : ['JavaScript'],
148
- frameworks: fsDetected.frameworks,
149
- codeStyle: fsDetected.codeStyle,
150
- cicd: fsDetected.cicd,
151
- subprojects: fsDetected.subprojects,
152
- architecture: '',
153
- buildCommands: {},
154
- complexity: 0.5,
155
- metrics: null,
156
- entryPoints: [],
157
- coreModules: [],
158
- testFramework: '',
159
- packageManager: fsDetected.packageManager || '',
160
- languageDistribution: fsDetected.languageDistribution || null,
161
- };
162
- detected = { ...fsDetected, ...projectInfo };
342
+ // Display results (install mode only — create mode has no real project to show)
343
+ console.log();
344
+ renderProjectCard(projectInfo);
163
345
  }
164
346
 
165
347
  // Cache analysis for later phases and future update runs
@@ -169,14 +351,10 @@ export async function runInstall(options = {}) {
169
351
  logger.debug(`Analysis cache write failed: ${cacheErr.message}`);
170
352
  }
171
353
 
172
- // Display results
173
- console.log();
174
- renderProjectCard(projectInfo);
175
-
176
354
  // ================================================================
177
355
  // PHASE 3: Configuration
178
356
  // ================================================================
179
- renderPhaseHeader(3);
357
+ renderPhaseHeader(3, phaseOpts);
180
358
 
181
359
  const spinner3 = ora('Calling claude-craft server...').start();
182
360
 
@@ -226,8 +404,8 @@ export async function runInstall(options = {}) {
226
404
  const mcpKeys = {};
227
405
  const securityConfig = { addSecurityGitignore: true };
228
406
 
229
- // Confirmation gate (skipped in non-interactive mode)
230
- if (!options.yes) {
407
+ // Confirmation gate (skipped in create mode and non-interactive mode)
408
+ if (!createMode && !options.yes) {
231
409
  const finalSummary = { ...summary, mcps: selectedMcps.map((m) => ({ id: m.id, tier: m.tier })) };
232
410
  const proceed = await confirmInstallation(finalSummary);
233
411
  if (!proceed) {
@@ -240,7 +418,7 @@ export async function runInstall(options = {}) {
240
418
  // ================================================================
241
419
  // PHASE 4: Installation
242
420
  // ================================================================
243
- renderPhaseHeader(4);
421
+ renderPhaseHeader(4, phaseOpts);
244
422
 
245
423
  let results;
246
424
  let filesToWrite;
@@ -309,10 +487,81 @@ export async function runInstall(options = {}) {
309
487
  }
310
488
 
311
489
  // ================================================================
312
- // PHASE 5: Finalization
490
+ // CREATE MODE: Bootstrap
313
491
  // ================================================================
314
- renderPhaseHeader(5);
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();
315
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
515
+ // ================================================================
516
+ renderPhaseHeader(createMode ? 6 : 5, createMode ? { totalPhases: 6, name: 'Finalization' } : {});
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
316
565
  try {
317
566
  const finCtx = await runFinalizeTasks({
318
567
  targetDir,
@@ -321,7 +570,6 @@ export async function runInstall(options = {}) {
321
570
  rewriteClaudeMd,
322
571
  });
323
572
 
324
- // Show what was replaced
325
573
  const opt = finCtx.optimizationResult;
326
574
  if (opt?.status === 'ok' && opt.applied > 0 && opt.replacements?.length > 0) {
327
575
  for (const label of opt.replacements) {
@@ -329,9 +577,6 @@ export async function runInstall(options = {}) {
329
577
  }
330
578
  }
331
579
  } catch {
332
- // Fallback to sequential
333
- // toolkit-usage.md no longer generated — capability-map.md covers routing
334
-
335
580
  const spinner7 = ora('Optimizing settings...').start();
336
581
  const optResult = optimizeSettings(targetDir);
337
582
 
@@ -362,7 +607,17 @@ export async function runInstall(options = {}) {
362
607
  });
363
608
 
364
609
  console.log();
365
- logger.success('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
+ }
366
621
  console.log();
367
622
  } catch (err) {
368
623
  if (
@@ -405,3 +660,28 @@ function countTotalItems(summary) {
405
660
 
406
661
  return countBucket(summary.guaranteed) + countBucket(summary.candidates);
407
662
  }
663
+
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
+ };
687
+ }
@@ -4,11 +4,17 @@ import { PHASES, TOTAL_PHASES, getTerminalWidth } from './theme.js';
4
4
  /**
5
5
  * Render a phase header like:
6
6
  * ── Phase 2 of 5 ── Project Discovery ──────────────────
7
+ *
8
+ * @param {number} phaseNumber — current phase number
9
+ * @param {object} [opts] — optional overrides
10
+ * @param {number} [opts.totalPhases] — override total phase count (default: TOTAL_PHASES)
11
+ * @param {string} [opts.name] — override phase name
7
12
  */
8
- export function renderPhaseHeader(phaseNumber) {
13
+ export function renderPhaseHeader(phaseNumber, opts = {}) {
14
+ const total = opts.totalPhases || TOTAL_PHASES;
9
15
  const phase = PHASES.find((p) => p.number === phaseNumber);
10
- const name = phase ? phase.name : `Phase ${phaseNumber}`;
11
- const label = `Phase ${phaseNumber} of ${TOTAL_PHASES}`;
16
+ const name = opts.name || (phase ? phase.name : `Phase ${phaseNumber}`);
17
+ const label = `Phase ${phaseNumber} of ${total}`;
12
18
 
13
19
  const w = Math.min(getTerminalWidth() - 4, 56);
14
20
  const inner = ` ${label} ── ${name} `;
@@ -1,501 +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 } 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 { optimizeSettings } from '../utils/claude-optimizer.js';
13
- import { rewriteClaudeMd } from '../utils/claude-rewriter.js';
14
- import { detectProject } from '../utils/detect-project.js';
15
- import { analyzeWithClaude } from '../utils/claude-analyzer.js';
16
- import { analyzeDescription } from '../utils/description-analyzer.js';
17
- import { platformCmd } from '../utils/run-claude.js';
18
- import { runBootstrap } from '../utils/bootstrap-runner.js';
19
- import {
20
- writeAnalysisCache,
21
- updateManifest,
22
- readAnalysisCache,
23
- promoteCache,
24
- cleanupAnalysisCache,
25
- } from '../utils/analysis-cache.js';
26
- import { VERSION } from '../constants.js';
27
- import * as logger from '../utils/logger.js';
28
-
29
- // UI modules
30
- import { renderBanner } from '../ui/brand.js';
31
- import { renderComponentBreakdown, renderMcpStatus, renderFileResults } from '../ui/tables.js';
32
- import { renderSuccessCard } from '../ui/cards.js';
33
- import { runInstallTasks, runVerifyTasks, runFinalizeTasks } from '../ui/tasks.js';
34
-
35
- // ── Helpers ──────────────────────────────────────────────────────────────────
36
-
37
- function renderCreatePhase(number, name, totalPhases) {
38
- const w = Math.min((process.stdout.columns || 80) - 4, 56);
39
- const label = `Phase ${number} of ${totalPhases}`;
40
- const inner = ` ${label} \u2500\u2500 ${name} `;
41
- const tailLen = Math.max(2, w - inner.length - 2);
42
- console.log();
43
- console.log(chalk.bold.cyan(` \u2500\u2500${inner}${'\u2500'.repeat(tailLen)}`));
44
- console.log();
45
- }
46
-
47
- function countTotalItems(summary) {
48
- const countBucket = (bucket) => {
49
- if (!bucket) return 0;
50
- return Object.values(bucket).reduce(
51
- (sum, arr) => sum + (Array.isArray(arr) ? arr.length : 0),
52
- 0,
53
- );
54
- };
55
- return countBucket(summary.guaranteed) + countBucket(summary.candidates);
56
- }
57
-
58
- // ── Main command ─────────────────────────────────────────────────────────────
59
-
60
- /**
61
- * Create command — build a new project from scratch.
62
- *
63
- * Step 1: Welcome & Setup (env check + API key)
64
- * Step 2: Project Setup (name, description, mkdir, git init)
65
- * Step 3: Configuration & Install (synthetic analysis → server → write .claude/)
66
- * Step 4: Bootstrap (hand off to Claude CLI /bootstrap:auto)
67
- * Step 5: Post-bootstrap Sync (re-analyze, rewrite CLAUDE.md, finalize)
68
- */
69
- export async function runCreate(options = {}) {
70
- const TOTAL_PHASES = 5;
71
- let targetDir;
72
-
73
- try {
74
- // ================================================================
75
- // STEP 1: Welcome & Setup
76
- // ================================================================
77
- renderBanner(VERSION);
78
- renderCreatePhase(1, 'Welcome & Setup', TOTAL_PHASES);
79
-
80
- // ── Pre-flight checks (Claude Code + API key + server) ──────
81
- await runPreflight({
82
- interactive: !options.yes,
83
- requireClaude: true,
84
- });
85
-
86
- // ================================================================
87
- // STEP 2: Project Setup
88
- // ================================================================
89
- renderCreatePhase(2, 'Project Setup', TOTAL_PHASES);
90
-
91
- let createProfile;
92
- if (options.yes) {
93
- createProfile = {
94
- name: options.name || 'my-project',
95
- description: options.description || 'A new project',
96
- projectType: 'monolith',
97
- };
98
- logger.info(`Non-interactive mode — creating ${chalk.bold(createProfile.name)} (monolith).`);
99
- } else {
100
- createProfile = await gatherCreateProfile();
101
- }
102
-
103
- let { name, description, projectType } = createProfile;
104
-
105
- // Resolve target directory
106
- const parentDir = resolve(options.dir || process.cwd());
107
- let useCurrentDir = false;
108
-
109
- if (!name) {
110
- // Empty name = use current directory — must be empty
111
- targetDir = parentDir;
112
- useCurrentDir = true;
113
- const contents = readdirSync(targetDir).filter((f) => f !== '.git');
114
- if (contents.length > 0) {
115
- logger.error(`Current directory ${chalk.bold(targetDir)} is not empty. Cannot create a project here.`);
116
- process.exit(1);
117
- }
118
- name = basename(targetDir) || 'my-project';
119
- } else {
120
- // Named project — re-prompt if directory already exists
121
- targetDir = join(parentDir, name);
122
- while (existsSync(targetDir)) {
123
- logger.warn(`Directory ${chalk.bold(name)} already exists.`);
124
- const newName = await themedInput({
125
- message: 'Enter a different project name:',
126
- hint: 'Letters, numbers, dots, hyphens, underscores only.',
127
- validate: (v) => {
128
- const t = v.trim();
129
- if (!t) return 'Name is required.';
130
- if (!/^[a-zA-Z0-9._-]+$/.test(t)) return 'Only letters, numbers, dots, hyphens, and underscores allowed.';
131
- return true;
132
- },
133
- });
134
- name = newName.trim();
135
- targetDir = join(parentDir, name);
136
- }
137
- }
138
-
139
- // Create directory + git init
140
- const spinner1 = ora(useCurrentDir ? 'Initializing project in current directory...' : 'Creating project directory...').start();
141
- if (!useCurrentDir) {
142
- mkdirSync(targetDir, { recursive: true });
143
- }
144
-
145
- // Write a minimal .gitignore
146
- writeFileSync(
147
- join(targetDir, '.gitignore'),
148
- 'node_modules/\ndist/\nbuild/\n.env\n.env.*\n!.env.example\n*.log\n.DS_Store\nThumbs.db\n',
149
- 'utf8',
150
- );
151
-
152
- // git init
153
- try {
154
- const { file, args } = platformCmd('git', ['init']);
155
- execFileSync(file, args, { cwd: targetDir, stdio: 'pipe', windowsHide: true });
156
- spinner1.succeed(useCurrentDir
157
- ? `Initialized project in ${chalk.bold(targetDir)} with git.`
158
- : `Created ${chalk.bold(name)}/ with git initialized.`);
159
- } catch {
160
- spinner1.succeed(useCurrentDir
161
- ? `Initialized project in ${chalk.bold(targetDir)} (git init skipped — git not available).`
162
- : `Created ${chalk.bold(name)}/ (git init skipped — git not available).`);
163
- }
164
-
165
- // ================================================================
166
- // STEP 3: Configuration & Install
167
- // ================================================================
168
- renderCreatePhase(3, 'Configuration', TOTAL_PHASES);
169
-
170
- // Analyze description with Claude to infer tech stack
171
- let descAnalysis = null;
172
- {
173
- const spinnerAnalyze = ora('Analyzing your requirements...').start();
174
- const { analysis, failReason } = await analyzeDescription(description, projectType);
175
- if (analysis) {
176
- descAnalysis = analysis;
177
- const parts = [];
178
- if (analysis.frameworks.length) parts.push(analysis.frameworks.join(', '));
179
- if (analysis.languages.length) parts.push(analysis.languages.join(', '));
180
- if (analysis.databases.length) parts.push(analysis.databases.join(', '));
181
- spinnerAnalyze.succeed(
182
- parts.length
183
- ? `Detected stack: ${chalk.bold(parts.join(' + '))}`
184
- : 'Requirements analyzed.',
185
- );
186
- } else {
187
- spinnerAnalyze.info(`Stack inference skipped${failReason ? ` (${failReason})` : ''} — using defaults.`);
188
- }
189
- }
190
-
191
- // Build project info — enriched with description analysis if available
192
- const syntheticProjectInfo = {
193
- name,
194
- description,
195
- projectType,
196
- languages: descAnalysis?.languages?.length ? descAnalysis.languages : [],
197
- frameworks: descAnalysis?.frameworks?.length ? descAnalysis.frameworks : [],
198
- codeStyle: descAnalysis?.codeStyle?.length ? descAnalysis.codeStyle : [],
199
- cicd: descAnalysis?.cicd?.length ? descAnalysis.cicd : [],
200
- subprojects: descAnalysis?.subprojects?.length ? descAnalysis.subprojects : [],
201
- architecture: descAnalysis?.architecture || '',
202
- buildCommands: descAnalysis?.buildCommands || {},
203
- complexity: descAnalysis?.complexity ?? 0.3,
204
- metrics: descAnalysis?.metrics || null,
205
- entryPoints: descAnalysis?.entryPoints || [],
206
- coreModules: descAnalysis?.coreModules || [],
207
- testFramework: descAnalysis?.testFramework || '',
208
- packageManager: descAnalysis?.packageManager || '',
209
- languageDistribution: descAnalysis?.languageDistribution || null,
210
- };
211
-
212
- const syntheticDetected = {
213
- ...syntheticProjectInfo,
214
- sensitiveFiles: { found: [], gitignoreCovers: true },
215
- _rootFiles: [],
216
- databases: descAnalysis?.databases?.length ? descAnalysis.databases : [],
217
- };
218
-
219
- // Call server
220
- const spinner3 = ora('Calling claude-craft server...').start();
221
-
222
- let apiResponse;
223
- try {
224
- apiResponse = await callGenerate(
225
- {
226
- ...syntheticProjectInfo,
227
- detectedFiles: [],
228
- databases: syntheticDetected.databases,
229
- },
230
- { projectPath: targetDir },
231
- );
232
- spinner3.succeed('Server returned configuration.');
233
- } catch (err) {
234
- spinner3.fail('Server request failed.');
235
- if (err instanceof ApiError) {
236
- logger.error(err.message);
237
- process.exit(1);
238
- }
239
- throw err;
240
- }
241
-
242
- const { summary, mcpConfigs } = apiResponse;
243
-
244
- // Validate we got components
245
- const guaranteedCount = summary?.guaranteed
246
- ? Object.values(summary.guaranteed).reduce((s, arr) => s + (Array.isArray(arr) ? arr.length : 0), 0)
247
- : 0;
248
-
249
- if (guaranteedCount === 0) {
250
- logger.warn('Server returned zero guaranteed components. Continuing with core defaults.');
251
- }
252
-
253
- // Display scoring summary
254
- console.log();
255
- renderComponentBreakdown(summary);
256
-
257
- // Auto-install all MCPs returned by the server
258
- const selectedMcps = mcpConfigs || [];
259
- const mcpKeys = {};
260
- const securityConfig = { addSecurityGitignore: true };
261
-
262
- // Cache analysis
263
- try {
264
- writeAnalysisCache(targetDir, syntheticProjectInfo, syntheticDetected, null);
265
- } catch (cacheErr) {
266
- logger.debug(`Analysis cache write failed: ${cacheErr.message}`);
267
- }
268
-
269
- // Write files
270
- let results;
271
- let filesToWrite;
272
-
273
- try {
274
- const installCtx = await runInstallTasks({
275
- apiResponse,
276
- targetDir,
277
- selectedMcps,
278
- mcpKeys,
279
- securityConfig,
280
- detected: syntheticDetected,
281
- buildFileList,
282
- writeApiFiles,
283
- });
284
-
285
- results = installCtx.results;
286
- filesToWrite = installCtx.filesToWrite;
287
- } catch {
288
- // Fallback to direct write
289
- const spinnerWrite = ora('Writing configuration files...').start();
290
- filesToWrite = buildFileList(apiResponse);
291
- results = await writeApiFiles(filesToWrite, targetDir, {
292
- force: true,
293
- selectedMcpIds: selectedMcps.map((m) => m.id),
294
- mcpKeys,
295
- securityConfig,
296
- detected: syntheticDetected,
297
- });
298
- spinnerWrite.succeed('Configuration generated.');
299
- }
300
-
301
- // Update manifest
302
- try {
303
- if (filesToWrite) updateManifest(targetDir, results, filesToWrite);
304
- } catch (cacheErr) {
305
- logger.debug(`Manifest update failed: ${cacheErr.message}`);
306
- }
307
-
308
- // Display file results
309
- renderFileResults(results);
310
-
311
- // MCP verification
312
- let mcpResults = [];
313
- if (selectedMcps.length > 0) {
314
- console.log();
315
- try {
316
- const verifyCtx = await runVerifyTasks(selectedMcps, mcpKeys, { setupMcps, targetDir });
317
- mcpResults = verifyCtx.mcpResults;
318
- } catch {
319
- const spinnerMcp = ora('Verifying MCP servers...').start();
320
- mcpResults = await setupMcps(selectedMcps, mcpKeys, {
321
- healthCheck: true,
322
- targetDir,
323
- onStatus: (id, status) => {
324
- if (status === 'verifying') spinnerMcp.text = `Verifying ${id}...`;
325
- else if (status === 'testing') spinnerMcp.text = `Health-checking ${id}...`;
326
- },
327
- });
328
- spinnerMcp.succeed('MCP verification complete.');
329
- }
330
-
331
- renderMcpStatus(mcpResults);
332
- }
333
-
334
- // ================================================================
335
- // STEP 4: Bootstrap
336
- // ================================================================
337
- renderCreatePhase(4, 'Bootstrap', TOTAL_PHASES);
338
-
339
- console.log(chalk.dim(' Handing off to Claude to scaffold your project...'));
340
- console.log(chalk.dim(' This may take several minutes. Activity log:'));
341
- console.log();
342
-
343
- let bootstrapSucceeded = true;
344
- try {
345
- await runBootstrap(targetDir, description);
346
- } catch (err) {
347
- bootstrapSucceeded = false;
348
- console.log();
349
- logger.warn('Bootstrap did not complete: ' + err.message);
350
- logger.info('Your .claude/ configuration is still intact. You can run /bootstrap:auto manually inside the project.');
351
- console.log();
352
- }
353
-
354
- // ================================================================
355
- // STEP 5: Post-Bootstrap Sync
356
- // ================================================================
357
- renderCreatePhase(5, 'Finalization', TOTAL_PHASES);
358
-
359
- if (bootstrapSucceeded) {
360
- // Re-analyze the now-real project
361
- try {
362
- const spinner5 = ora('Re-analyzing project...').start();
363
- const fsDetected = await detectProject(targetDir);
364
-
365
- let projectInfo;
366
- try {
367
- const { analysis } = await analyzeWithClaude(targetDir);
368
- if (analysis) {
369
- projectInfo = {
370
- name: analysis.name || fsDetected.name || name,
371
- description: analysis.description || fsDetected.description || description,
372
- projectType: analysis.projectType || fsDetected.projectType || projectType,
373
- languages: analysis.languages?.length ? analysis.languages : fsDetected.languages,
374
- frameworks: analysis.frameworks?.length ? analysis.frameworks : fsDetected.frameworks,
375
- codeStyle: analysis.codeStyle?.length ? analysis.codeStyle : fsDetected.codeStyle,
376
- cicd: analysis.cicd?.length ? analysis.cicd : fsDetected.cicd,
377
- subprojects: analysis.subprojects?.length ? analysis.subprojects : fsDetected.subprojects,
378
- architecture: analysis.architecture || '',
379
- buildCommands: analysis.buildCommands || {},
380
- complexity: analysis.complexity ?? 0.5,
381
- metrics: analysis.metrics || null,
382
- entryPoints: analysis.entryPoints || [],
383
- coreModules: analysis.coreModules || [],
384
- testFramework: analysis.testFramework || '',
385
- packageManager: analysis.packageManager || fsDetected.packageManager || '',
386
- languageDistribution: analysis.languageDistribution || fsDetected.languageDistribution || null,
387
- };
388
- } else {
389
- projectInfo = buildProjectInfoFromFs(fsDetected, name, description, projectType);
390
- }
391
- } catch {
392
- projectInfo = buildProjectInfoFromFs(fsDetected, name, description, projectType);
393
- }
394
-
395
- spinner5.succeed('Project re-analyzed.');
396
-
397
- // Overwrite cache with real data
398
- const detected = { ...fsDetected, ...projectInfo };
399
- writeAnalysisCache(targetDir, projectInfo, detected, null);
400
- } catch (err) {
401
- logger.debug(`Post-bootstrap analysis failed: ${err.message}`);
402
- }
403
- }
404
-
405
- // Finalize — optimize settings + rewrite CLAUDE.md
406
- try {
407
- await runFinalizeTasks({
408
- targetDir,
409
- readAnalysisCache,
410
- optimizeSettings,
411
- rewriteClaudeMd,
412
- });
413
- } catch {
414
- // Fallback to sequential
415
- const spinnerOpt = ora('Optimizing settings...').start();
416
- const optResult = optimizeSettings(targetDir);
417
- if (optResult.status === 'ok' && optResult.applied > 0) {
418
- spinnerOpt.succeed(`Optimized ${optResult.applied} setting(s).`);
419
- } else {
420
- spinnerOpt.succeed('Settings reviewed \u2014 no changes needed.');
421
- }
422
-
423
- const spinnerMd = ora('Rewriting CLAUDE.md...').start();
424
- const cache = readAnalysisCache(targetDir);
425
- const rewritten = await rewriteClaudeMd(targetDir, cache);
426
- if (rewritten) {
427
- spinnerMd.succeed('CLAUDE.md rewritten.');
428
- } else {
429
- spinnerMd.warn('CLAUDE.md rewrite skipped \u2014 using template version.');
430
- }
431
- }
432
-
433
- // ── Success ──────────────────────────────────────────────────────
434
- const totalItems = countTotalItems(summary);
435
- const mcpsNeedingKeys = mcpResults.filter((r) => r.status === 'needs-key');
436
-
437
- renderSuccessCard({
438
- totalItems,
439
- mcpCount: selectedMcps.length,
440
- mcpsNeedingKeys,
441
- });
442
-
443
- console.log();
444
- if (bootstrapSucceeded) {
445
- logger.success(`Project ${chalk.bold(name)} created and bootstrapped!`);
446
- console.log(chalk.dim(` cd ${name} && claude`));
447
- } else {
448
- logger.success(`Project ${chalk.bold(name)} created with Claude configuration.`);
449
- console.log(chalk.dim(` cd ${name} && claude -p "/bootstrap:auto ${description}"`));
450
- }
451
- console.log();
452
- } catch (err) {
453
- if (
454
- err &&
455
- (err.name === 'ExitPromptError' ||
456
- err.constructor?.name === 'ExitPromptError' ||
457
- err.message?.includes('User force closed'))
458
- ) {
459
- console.log();
460
- logger.info('Cancelled.');
461
- process.exit(0);
462
- }
463
-
464
- console.log();
465
- logger.error(err.message || String(err));
466
- process.exit(1);
467
- } finally {
468
- if (targetDir) {
469
- try {
470
- promoteCache(targetDir);
471
- cleanupAnalysisCache(targetDir);
472
- } catch {
473
- // Ignore cleanup errors
474
- }
475
- }
476
- }
477
- }
478
-
479
- // ── Helper: build projectInfo from filesystem detection only ─────────────────
480
-
481
- function buildProjectInfoFromFs(fsDetected, name, description, projectType) {
482
- return {
483
- name: fsDetected.name || name,
484
- description: fsDetected.description || description,
485
- projectType: fsDetected.projectType || projectType,
486
- languages: fsDetected.languages?.length ? fsDetected.languages : [],
487
- frameworks: fsDetected.frameworks || [],
488
- codeStyle: fsDetected.codeStyle || [],
489
- cicd: fsDetected.cicd || [],
490
- subprojects: fsDetected.subprojects || [],
491
- architecture: '',
492
- buildCommands: {},
493
- complexity: 0.5,
494
- metrics: null,
495
- entryPoints: [],
496
- coreModules: [],
497
- testFramework: '',
498
- packageManager: fsDetected.packageManager || '',
499
- languageDistribution: fsDetected.languageDistribution || null,
500
- };
501
- }