ccraft 1.0.12 → 1.0.14
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 -90
- package/src/prompts/gather.js +43 -0
- package/src/ui/phase-header.js +9 -3
- package/src/commands/create.js +0 -501
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
|
|
42
|
+
ccraft install --name my-app --description "REST API for inventory management"
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
|
|
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
|
|
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
|
-
**
|
|
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
|
|
126
|
-
ccraft
|
|
127
|
-
ccraft
|
|
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.
|
package/bin/claude-craft.js
CHANGED
|
@@ -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
|
|
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
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,13 +13,17 @@ import {
|
|
|
11
13
|
} from '../utils/existing-setup.js';
|
|
12
14
|
import {
|
|
13
15
|
gatherProjectPath,
|
|
16
|
+
gatherCreateProfile,
|
|
17
|
+
gatherMcpKeys,
|
|
14
18
|
confirmInstallation,
|
|
15
19
|
} from '../prompts/gather.js';
|
|
20
|
+
import { themedInput } from '../ui/prompts.js';
|
|
16
21
|
import { callGenerate, ApiError } from '../utils/api-client.js';
|
|
17
22
|
import { writeApiFiles, buildFileList } from '../utils/api-file-writer.js';
|
|
18
23
|
import { setupMcps } from '../utils/mcp-setup.js';
|
|
19
24
|
import { optimizeSettings } from '../utils/claude-optimizer.js';
|
|
20
25
|
import { runPreflight } from '../utils/preflight.js';
|
|
26
|
+
import { platformCmd } from '../utils/run-claude.js';
|
|
21
27
|
import {
|
|
22
28
|
writeAnalysisCache,
|
|
23
29
|
updateManifest,
|
|
@@ -35,17 +41,47 @@ import { renderProjectCard, renderSuccessCard } from '../ui/cards.js';
|
|
|
35
41
|
import { renderComponentBreakdown, renderMcpStatus, renderFileResults } from '../ui/tables.js';
|
|
36
42
|
import { runExistingSetupTasks, runAnalysisTasks, runInstallTasks, runVerifyTasks, runFinalizeTasks } from '../ui/tasks.js';
|
|
37
43
|
|
|
44
|
+
// ── Create-mode detection ───────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
const IGNORED_ENTRIES = new Set(['.git', '.DS_Store', 'Thumbs.db', '.gitkeep']);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Returns true if the directory doesn't exist or contains only ignored files.
|
|
50
|
+
*/
|
|
51
|
+
function isEmptyDir(dirPath) {
|
|
52
|
+
if (!existsSync(dirPath)) return true;
|
|
53
|
+
const entries = readdirSync(dirPath).filter((f) => !IGNORED_ENTRIES.has(f));
|
|
54
|
+
return entries.length === 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
38
57
|
/**
|
|
39
|
-
*
|
|
58
|
+
* Determine whether we're in create mode (new project) or install mode (existing project).
|
|
40
59
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
|
|
60
|
+
* Create mode triggers when:
|
|
61
|
+
* - --name or --description flag is provided (explicit intent)
|
|
62
|
+
* - Target directory doesn't exist
|
|
63
|
+
* - Target directory is empty (ignoring .git, .DS_Store, etc.)
|
|
64
|
+
*/
|
|
65
|
+
function isCreateMode(dirPath, options) {
|
|
66
|
+
if (options.name || options.description) return true;
|
|
67
|
+
return isEmptyDir(dirPath);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Main install command — unified orchestrator for both new and existing projects.
|
|
72
|
+
*
|
|
73
|
+
* Auto-detects mode:
|
|
74
|
+
* - Create mode (empty/missing dir): gather profile → mkdir → git init → synthetic
|
|
75
|
+
* analysis → server → write files → bootstrap → re-analyze → finalize
|
|
76
|
+
* - Install mode (existing project): discover → analyze → server → confirm →
|
|
77
|
+
* write files → MCP verify → finalize
|
|
46
78
|
*/
|
|
47
79
|
export async function runInstall(options = {}) {
|
|
48
80
|
let targetDir;
|
|
81
|
+
let createMode = false;
|
|
82
|
+
let createName = '';
|
|
83
|
+
let createDescription = '';
|
|
84
|
+
|
|
49
85
|
try {
|
|
50
86
|
// ================================================================
|
|
51
87
|
// PHASE 1: Welcome & Setup
|
|
@@ -58,61 +94,230 @@ export async function runInstall(options = {}) {
|
|
|
58
94
|
requireClaude: true,
|
|
59
95
|
});
|
|
60
96
|
|
|
61
|
-
// ──
|
|
62
|
-
if (!options.yes && !options.dir) {
|
|
97
|
+
// ── Resolve target directory + detect mode ──────────────────────
|
|
98
|
+
if (!options.yes && !options.dir && !options.name && !options.description) {
|
|
99
|
+
// Interactive, no flags — ask for path first, then detect mode
|
|
63
100
|
targetDir = await gatherProjectPath();
|
|
64
101
|
} else {
|
|
65
102
|
targetDir = resolve(options.dir || process.cwd());
|
|
66
103
|
}
|
|
67
104
|
|
|
105
|
+
createMode = isCreateMode(targetDir, options);
|
|
106
|
+
|
|
107
|
+
if (createMode) {
|
|
108
|
+
// ── Create mode: gather profile + mkdir + git init ────────────
|
|
109
|
+
let name, description, projectType;
|
|
110
|
+
|
|
111
|
+
if (options.yes) {
|
|
112
|
+
name = options.name || 'my-project';
|
|
113
|
+
description = options.description || 'A new project';
|
|
114
|
+
projectType = 'monolith';
|
|
115
|
+
logger.info(`New project mode — creating ${chalk.bold(name)} (monolith).`);
|
|
116
|
+
} else if (options.name || options.description) {
|
|
117
|
+
// Partial flags provided — fill in the rest interactively
|
|
118
|
+
const profile = await gatherCreateProfile();
|
|
119
|
+
name = options.name || profile.name;
|
|
120
|
+
description = options.description || profile.description;
|
|
121
|
+
projectType = profile.projectType;
|
|
122
|
+
} else {
|
|
123
|
+
// Empty dir detected — prompt for project details
|
|
124
|
+
logger.info('Empty directory detected — switching to new project mode.');
|
|
125
|
+
const profile = await gatherCreateProfile();
|
|
126
|
+
name = profile.name;
|
|
127
|
+
description = profile.description;
|
|
128
|
+
projectType = profile.projectType;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Resolve target directory for new project
|
|
132
|
+
const parentDir = resolve(options.dir || process.cwd());
|
|
133
|
+
let useCurrentDir = false;
|
|
134
|
+
|
|
135
|
+
if (!name) {
|
|
136
|
+
// Empty name = use current directory
|
|
137
|
+
targetDir = parentDir;
|
|
138
|
+
useCurrentDir = true;
|
|
139
|
+
const contents = readdirSync(targetDir).filter((f) => !IGNORED_ENTRIES.has(f));
|
|
140
|
+
if (contents.length > 0) {
|
|
141
|
+
logger.error(`Current directory ${chalk.bold(targetDir)} is not empty. Cannot create a project here.`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
name = basename(targetDir) || 'my-project';
|
|
145
|
+
} else {
|
|
146
|
+
// Named project — re-prompt if directory already exists and is non-empty
|
|
147
|
+
targetDir = join(parentDir, name);
|
|
148
|
+
while (existsSync(targetDir) && !isEmptyDir(targetDir)) {
|
|
149
|
+
logger.warn(`Directory ${chalk.bold(name)} already exists and is not empty.`);
|
|
150
|
+
const newName = await themedInput({
|
|
151
|
+
message: 'Enter a different project name:',
|
|
152
|
+
hint: 'Letters, numbers, dots, hyphens, underscores only.',
|
|
153
|
+
validate: (v) => {
|
|
154
|
+
const t = v.trim();
|
|
155
|
+
if (!t) return 'Name is required.';
|
|
156
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(t)) return 'Only letters, numbers, dots, hyphens, and underscores allowed.';
|
|
157
|
+
return true;
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
name = newName.trim();
|
|
161
|
+
targetDir = join(parentDir, name);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Create directory + git init
|
|
166
|
+
const spinner1 = ora(useCurrentDir ? 'Initializing project in current directory...' : 'Creating project directory...').start();
|
|
167
|
+
if (!useCurrentDir && !existsSync(targetDir)) {
|
|
168
|
+
mkdirSync(targetDir, { recursive: true });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
writeFileSync(
|
|
172
|
+
join(targetDir, '.gitignore'),
|
|
173
|
+
'node_modules/\ndist/\nbuild/\n.env\n.env.*\n!.env.example\n*.log\n.DS_Store\nThumbs.db\n',
|
|
174
|
+
'utf8',
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const { file, args } = platformCmd('git', ['init']);
|
|
179
|
+
execFileSync(file, args, { cwd: targetDir, stdio: 'pipe', windowsHide: true });
|
|
180
|
+
spinner1.succeed(useCurrentDir
|
|
181
|
+
? `Initialized project in ${chalk.bold(targetDir)} with git.`
|
|
182
|
+
: `Created ${chalk.bold(name)}/ with git initialized.`);
|
|
183
|
+
} catch {
|
|
184
|
+
spinner1.succeed(useCurrentDir
|
|
185
|
+
? `Initialized project in ${chalk.bold(targetDir)} (git init skipped — git not available).`
|
|
186
|
+
: `Created ${chalk.bold(name)}/ (git init skipped — git not available).`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
createName = name;
|
|
190
|
+
createDescription = description;
|
|
191
|
+
// projectType is captured in the closure for Phase 2
|
|
192
|
+
options._createProjectType = projectType;
|
|
193
|
+
}
|
|
194
|
+
|
|
68
195
|
// ================================================================
|
|
69
196
|
// PHASE 2: Project Discovery
|
|
70
197
|
// ================================================================
|
|
71
|
-
|
|
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
|
-
}
|
|
198
|
+
const phaseOpts = createMode ? { totalPhases: 6 } : {};
|
|
199
|
+
renderPhaseHeader(2, phaseOpts);
|
|
85
200
|
|
|
86
|
-
// ── Project analysis ────────────────────────────────────────────
|
|
87
201
|
let projectInfo;
|
|
88
202
|
let detected;
|
|
203
|
+
let existingContext = null;
|
|
89
204
|
|
|
90
|
-
|
|
91
|
-
|
|
205
|
+
if (createMode) {
|
|
206
|
+
// ── Create mode: synthetic analysis from description ──────────
|
|
207
|
+
const { analyzeDescription } = await import('../utils/description-analyzer.js');
|
|
208
|
+
|
|
209
|
+
let descAnalysis = null;
|
|
210
|
+
{
|
|
211
|
+
const spinnerAnalyze = ora('Analyzing your requirements...').start();
|
|
212
|
+
const { analysis, failReason } = await analyzeDescription(createDescription, options._createProjectType);
|
|
213
|
+
if (analysis) {
|
|
214
|
+
descAnalysis = analysis;
|
|
215
|
+
const parts = [];
|
|
216
|
+
if (analysis.frameworks.length) parts.push(analysis.frameworks.join(', '));
|
|
217
|
+
if (analysis.languages.length) parts.push(analysis.languages.join(', '));
|
|
218
|
+
if (analysis.databases.length) parts.push(analysis.databases.join(', '));
|
|
219
|
+
spinnerAnalyze.succeed(
|
|
220
|
+
parts.length
|
|
221
|
+
? `Detected stack: ${chalk.bold(parts.join(' + '))}`
|
|
222
|
+
: 'Requirements analyzed.',
|
|
223
|
+
);
|
|
224
|
+
} else {
|
|
225
|
+
spinnerAnalyze.info(`Stack inference skipped${failReason ? ` (${failReason})` : ''} — using defaults.`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
92
228
|
|
|
93
|
-
|
|
229
|
+
projectInfo = {
|
|
230
|
+
name: createName,
|
|
231
|
+
description: createDescription,
|
|
232
|
+
projectType: options._createProjectType || 'monolith',
|
|
233
|
+
languages: descAnalysis?.languages?.length ? descAnalysis.languages : [],
|
|
234
|
+
frameworks: descAnalysis?.frameworks?.length ? descAnalysis.frameworks : [],
|
|
235
|
+
codeStyle: descAnalysis?.codeStyle?.length ? descAnalysis.codeStyle : [],
|
|
236
|
+
cicd: descAnalysis?.cicd?.length ? descAnalysis.cicd : [],
|
|
237
|
+
subprojects: descAnalysis?.subprojects?.length ? descAnalysis.subprojects : [],
|
|
238
|
+
architecture: descAnalysis?.architecture || '',
|
|
239
|
+
buildCommands: descAnalysis?.buildCommands || {},
|
|
240
|
+
complexity: descAnalysis?.complexity ?? 0.3,
|
|
241
|
+
metrics: descAnalysis?.metrics || null,
|
|
242
|
+
entryPoints: descAnalysis?.entryPoints || [],
|
|
243
|
+
coreModules: descAnalysis?.coreModules || [],
|
|
244
|
+
testFramework: descAnalysis?.testFramework || '',
|
|
245
|
+
packageManager: descAnalysis?.packageManager || '',
|
|
246
|
+
languageDistribution: descAnalysis?.languageDistribution || null,
|
|
247
|
+
};
|
|
94
248
|
|
|
95
|
-
|
|
96
|
-
projectInfo
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
249
|
+
detected = {
|
|
250
|
+
...projectInfo,
|
|
251
|
+
sensitiveFiles: { found: [], gitignoreCovers: true },
|
|
252
|
+
_rootFiles: [],
|
|
253
|
+
databases: descAnalysis?.databases?.length ? descAnalysis.databases : [],
|
|
254
|
+
};
|
|
255
|
+
} else {
|
|
256
|
+
// ── Install mode: real project analysis ───────────────────────
|
|
257
|
+
try {
|
|
258
|
+
const setupCtx = await runExistingSetupTasks(targetDir, {
|
|
259
|
+
detectExistingSetup,
|
|
260
|
+
extractExistingContext,
|
|
261
|
+
removeExistingSetup,
|
|
262
|
+
});
|
|
263
|
+
existingContext = setupCtx.existingContext || null;
|
|
264
|
+
} catch (setupErr) {
|
|
265
|
+
logger.debug(`Existing setup check failed: ${setupErr.message}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const ctx = await runAnalysisTasks(targetDir, { analyzeWithClaude, detectProject, existingContext });
|
|
270
|
+
|
|
271
|
+
const { claudeAnalysis, claudeFailReason, fsDetected } = ctx;
|
|
272
|
+
|
|
273
|
+
if (claudeAnalysis) {
|
|
274
|
+
projectInfo = {
|
|
275
|
+
name: claudeAnalysis.name || fsDetected.name || 'my-project',
|
|
276
|
+
description: claudeAnalysis.description || fsDetected.description || '',
|
|
277
|
+
projectType: claudeAnalysis.projectType || fsDetected.projectType || 'monolith',
|
|
278
|
+
languages: claudeAnalysis.languages.length ? claudeAnalysis.languages : (fsDetected.languages.length ? fsDetected.languages : ['JavaScript']),
|
|
279
|
+
frameworks: claudeAnalysis.frameworks.length ? claudeAnalysis.frameworks : fsDetected.frameworks,
|
|
280
|
+
codeStyle: claudeAnalysis.codeStyle.length ? claudeAnalysis.codeStyle : fsDetected.codeStyle,
|
|
281
|
+
cicd: claudeAnalysis.cicd.length ? claudeAnalysis.cicd : fsDetected.cicd,
|
|
282
|
+
subprojects: claudeAnalysis.subprojects.length ? claudeAnalysis.subprojects : fsDetected.subprojects,
|
|
283
|
+
architecture: claudeAnalysis.architecture || '',
|
|
284
|
+
buildCommands: claudeAnalysis.buildCommands || {},
|
|
285
|
+
complexity: claudeAnalysis.complexity ?? 0.5,
|
|
286
|
+
metrics: claudeAnalysis.metrics || null,
|
|
287
|
+
entryPoints: claudeAnalysis.entryPoints || [],
|
|
288
|
+
coreModules: claudeAnalysis.coreModules || [],
|
|
289
|
+
testFramework: claudeAnalysis.testFramework || '',
|
|
290
|
+
packageManager: claudeAnalysis.packageManager || fsDetected.packageManager || '',
|
|
291
|
+
languageDistribution: claudeAnalysis.languageDistribution || fsDetected.languageDistribution || null,
|
|
292
|
+
};
|
|
293
|
+
} else {
|
|
294
|
+
projectInfo = {
|
|
295
|
+
name: fsDetected.name || targetDir.split(/[/\\]/).filter(Boolean).pop() || 'my-project',
|
|
296
|
+
description: fsDetected.description || '',
|
|
297
|
+
projectType: fsDetected.projectType || 'monolith',
|
|
298
|
+
languages: fsDetected.languages.length ? fsDetected.languages : ['JavaScript'],
|
|
299
|
+
frameworks: fsDetected.frameworks,
|
|
300
|
+
codeStyle: fsDetected.codeStyle,
|
|
301
|
+
cicd: fsDetected.cicd,
|
|
302
|
+
subprojects: fsDetected.subprojects,
|
|
303
|
+
architecture: '',
|
|
304
|
+
buildCommands: {},
|
|
305
|
+
complexity: 0.5,
|
|
306
|
+
metrics: null,
|
|
307
|
+
entryPoints: [],
|
|
308
|
+
coreModules: [],
|
|
309
|
+
testFramework: '',
|
|
310
|
+
packageManager: fsDetected.packageManager || '',
|
|
311
|
+
languageDistribution: fsDetected.languageDistribution || null,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
detected = { ...fsDetected, ...projectInfo };
|
|
316
|
+
} catch (analysisErr) {
|
|
317
|
+
// Fallback if task runner fails — run sequentially with spinner
|
|
318
|
+
const spinner = ora('Analyzing project...').start();
|
|
319
|
+
const fsDetected = await detectProject(targetDir);
|
|
320
|
+
spinner.succeed('Project scanned.');
|
|
116
321
|
projectInfo = {
|
|
117
322
|
name: fsDetected.name || targetDir.split(/[/\\]/).filter(Boolean).pop() || 'my-project',
|
|
118
323
|
description: fsDetected.description || '',
|
|
@@ -132,34 +337,12 @@ export async function runInstall(options = {}) {
|
|
|
132
337
|
packageManager: fsDetected.packageManager || '',
|
|
133
338
|
languageDistribution: fsDetected.languageDistribution || null,
|
|
134
339
|
};
|
|
340
|
+
detected = { ...fsDetected, ...projectInfo };
|
|
135
341
|
}
|
|
136
342
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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 };
|
|
343
|
+
// Display results (install mode only — create mode has no real project to show)
|
|
344
|
+
console.log();
|
|
345
|
+
renderProjectCard(projectInfo);
|
|
163
346
|
}
|
|
164
347
|
|
|
165
348
|
// Cache analysis for later phases and future update runs
|
|
@@ -169,14 +352,10 @@ export async function runInstall(options = {}) {
|
|
|
169
352
|
logger.debug(`Analysis cache write failed: ${cacheErr.message}`);
|
|
170
353
|
}
|
|
171
354
|
|
|
172
|
-
// Display results
|
|
173
|
-
console.log();
|
|
174
|
-
renderProjectCard(projectInfo);
|
|
175
|
-
|
|
176
355
|
// ================================================================
|
|
177
356
|
// PHASE 3: Configuration
|
|
178
357
|
// ================================================================
|
|
179
|
-
renderPhaseHeader(3);
|
|
358
|
+
renderPhaseHeader(3, phaseOpts);
|
|
180
359
|
|
|
181
360
|
const spinner3 = ora('Calling claude-craft server...').start();
|
|
182
361
|
|
|
@@ -223,11 +402,11 @@ export async function runInstall(options = {}) {
|
|
|
223
402
|
|
|
224
403
|
// All MCPs are auto-installed; no interactive selection
|
|
225
404
|
const selectedMcps = mcpConfigs || [];
|
|
226
|
-
|
|
405
|
+
let mcpKeys = {};
|
|
227
406
|
const securityConfig = { addSecurityGitignore: true };
|
|
228
407
|
|
|
229
|
-
// Confirmation gate (skipped in non-interactive mode)
|
|
230
|
-
if (!options.yes) {
|
|
408
|
+
// Confirmation gate (skipped in create mode and non-interactive mode)
|
|
409
|
+
if (!createMode && !options.yes) {
|
|
231
410
|
const finalSummary = { ...summary, mcps: selectedMcps.map((m) => ({ id: m.id, tier: m.tier })) };
|
|
232
411
|
const proceed = await confirmInstallation(finalSummary);
|
|
233
412
|
if (!proceed) {
|
|
@@ -240,7 +419,12 @@ export async function runInstall(options = {}) {
|
|
|
240
419
|
// ================================================================
|
|
241
420
|
// PHASE 4: Installation
|
|
242
421
|
// ================================================================
|
|
243
|
-
renderPhaseHeader(4);
|
|
422
|
+
renderPhaseHeader(4, phaseOpts);
|
|
423
|
+
|
|
424
|
+
// Prompt for API keys for MCPs that need them
|
|
425
|
+
if (!options.yes) {
|
|
426
|
+
mcpKeys = await gatherMcpKeys(selectedMcps);
|
|
427
|
+
}
|
|
244
428
|
|
|
245
429
|
let results;
|
|
246
430
|
let filesToWrite;
|
|
@@ -309,10 +493,81 @@ export async function runInstall(options = {}) {
|
|
|
309
493
|
}
|
|
310
494
|
|
|
311
495
|
// ================================================================
|
|
312
|
-
//
|
|
496
|
+
// CREATE MODE: Bootstrap
|
|
313
497
|
// ================================================================
|
|
314
|
-
|
|
498
|
+
let bootstrapSucceeded = true;
|
|
499
|
+
|
|
500
|
+
if (createMode) {
|
|
501
|
+
renderPhaseHeader(5, { totalPhases: 6, name: 'Bootstrap' });
|
|
315
502
|
|
|
503
|
+
console.log(chalk.dim(' Handing off to Claude to scaffold your project...'));
|
|
504
|
+
console.log(chalk.dim(' This may take several minutes. Activity log:'));
|
|
505
|
+
console.log();
|
|
506
|
+
|
|
507
|
+
try {
|
|
508
|
+
const { runBootstrap } = await import('../utils/bootstrap-runner.js');
|
|
509
|
+
await runBootstrap(targetDir, createDescription);
|
|
510
|
+
} catch (err) {
|
|
511
|
+
bootstrapSucceeded = false;
|
|
512
|
+
console.log();
|
|
513
|
+
logger.warn('Bootstrap did not complete: ' + err.message);
|
|
514
|
+
logger.info('Your .claude/ configuration is still intact. You can run /bootstrap:auto manually inside the project.');
|
|
515
|
+
console.log();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ================================================================
|
|
520
|
+
// FINALIZATION
|
|
521
|
+
// ================================================================
|
|
522
|
+
renderPhaseHeader(createMode ? 6 : 5, createMode ? { totalPhases: 6, name: 'Finalization' } : {});
|
|
523
|
+
|
|
524
|
+
// Create mode: re-analyze after bootstrap
|
|
525
|
+
if (createMode && bootstrapSucceeded) {
|
|
526
|
+
try {
|
|
527
|
+
const spinnerReanalyze = ora('Re-analyzing project...').start();
|
|
528
|
+
const fsDetected = await detectProject(targetDir);
|
|
529
|
+
|
|
530
|
+
let reanalyzedInfo;
|
|
531
|
+
try {
|
|
532
|
+
const { analysis } = await analyzeWithClaude(targetDir);
|
|
533
|
+
if (analysis) {
|
|
534
|
+
reanalyzedInfo = {
|
|
535
|
+
name: analysis.name || fsDetected.name || createName,
|
|
536
|
+
description: analysis.description || fsDetected.description || createDescription,
|
|
537
|
+
projectType: analysis.projectType || fsDetected.projectType || options._createProjectType,
|
|
538
|
+
languages: analysis.languages?.length ? analysis.languages : fsDetected.languages,
|
|
539
|
+
frameworks: analysis.frameworks?.length ? analysis.frameworks : fsDetected.frameworks,
|
|
540
|
+
codeStyle: analysis.codeStyle?.length ? analysis.codeStyle : fsDetected.codeStyle,
|
|
541
|
+
cicd: analysis.cicd?.length ? analysis.cicd : fsDetected.cicd,
|
|
542
|
+
subprojects: analysis.subprojects?.length ? analysis.subprojects : fsDetected.subprojects,
|
|
543
|
+
architecture: analysis.architecture || '',
|
|
544
|
+
buildCommands: analysis.buildCommands || {},
|
|
545
|
+
complexity: analysis.complexity ?? 0.5,
|
|
546
|
+
metrics: analysis.metrics || null,
|
|
547
|
+
entryPoints: analysis.entryPoints || [],
|
|
548
|
+
coreModules: analysis.coreModules || [],
|
|
549
|
+
testFramework: analysis.testFramework || '',
|
|
550
|
+
packageManager: analysis.packageManager || fsDetected.packageManager || '',
|
|
551
|
+
languageDistribution: analysis.languageDistribution || fsDetected.languageDistribution || null,
|
|
552
|
+
};
|
|
553
|
+
} else {
|
|
554
|
+
reanalyzedInfo = buildProjectInfoFromFs(fsDetected, createName, createDescription, options._createProjectType);
|
|
555
|
+
}
|
|
556
|
+
} catch {
|
|
557
|
+
reanalyzedInfo = buildProjectInfoFromFs(fsDetected, createName, createDescription, options._createProjectType);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
spinnerReanalyze.succeed('Project re-analyzed.');
|
|
561
|
+
|
|
562
|
+
// Overwrite cache with real data
|
|
563
|
+
const reanalyzedDetected = { ...fsDetected, ...reanalyzedInfo };
|
|
564
|
+
writeAnalysisCache(targetDir, reanalyzedInfo, reanalyzedDetected, null);
|
|
565
|
+
} catch (err) {
|
|
566
|
+
logger.debug(`Post-bootstrap analysis failed: ${err.message}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Shared finalization — optimize settings + rewrite CLAUDE.md
|
|
316
571
|
try {
|
|
317
572
|
const finCtx = await runFinalizeTasks({
|
|
318
573
|
targetDir,
|
|
@@ -321,7 +576,6 @@ export async function runInstall(options = {}) {
|
|
|
321
576
|
rewriteClaudeMd,
|
|
322
577
|
});
|
|
323
578
|
|
|
324
|
-
// Show what was replaced
|
|
325
579
|
const opt = finCtx.optimizationResult;
|
|
326
580
|
if (opt?.status === 'ok' && opt.applied > 0 && opt.replacements?.length > 0) {
|
|
327
581
|
for (const label of opt.replacements) {
|
|
@@ -329,9 +583,6 @@ export async function runInstall(options = {}) {
|
|
|
329
583
|
}
|
|
330
584
|
}
|
|
331
585
|
} catch {
|
|
332
|
-
// Fallback to sequential
|
|
333
|
-
// toolkit-usage.md no longer generated — capability-map.md covers routing
|
|
334
|
-
|
|
335
586
|
const spinner7 = ora('Optimizing settings...').start();
|
|
336
587
|
const optResult = optimizeSettings(targetDir);
|
|
337
588
|
|
|
@@ -362,7 +613,17 @@ export async function runInstall(options = {}) {
|
|
|
362
613
|
});
|
|
363
614
|
|
|
364
615
|
console.log();
|
|
365
|
-
|
|
616
|
+
if (createMode) {
|
|
617
|
+
if (bootstrapSucceeded) {
|
|
618
|
+
logger.success(`Project ${chalk.bold(createName)} created and bootstrapped!`);
|
|
619
|
+
console.log(chalk.dim(` cd ${createName} && claude`));
|
|
620
|
+
} else {
|
|
621
|
+
logger.success(`Project ${chalk.bold(createName)} created with Claude configuration.`);
|
|
622
|
+
console.log(chalk.dim(` cd ${createName} && claude -p "/bootstrap:auto ${createDescription}"`));
|
|
623
|
+
}
|
|
624
|
+
} else {
|
|
625
|
+
logger.success('Done! Claude Code is ready.');
|
|
626
|
+
}
|
|
366
627
|
console.log();
|
|
367
628
|
} catch (err) {
|
|
368
629
|
if (
|
|
@@ -405,3 +666,28 @@ function countTotalItems(summary) {
|
|
|
405
666
|
|
|
406
667
|
return countBucket(summary.guaranteed) + countBucket(summary.candidates);
|
|
407
668
|
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Build projectInfo from filesystem detection only (fallback for create mode re-analysis).
|
|
672
|
+
*/
|
|
673
|
+
function buildProjectInfoFromFs(fsDetected, name, description, projectType) {
|
|
674
|
+
return {
|
|
675
|
+
name: fsDetected.name || name,
|
|
676
|
+
description: fsDetected.description || description,
|
|
677
|
+
projectType: fsDetected.projectType || projectType,
|
|
678
|
+
languages: fsDetected.languages?.length ? fsDetected.languages : [],
|
|
679
|
+
frameworks: fsDetected.frameworks || [],
|
|
680
|
+
codeStyle: fsDetected.codeStyle || [],
|
|
681
|
+
cicd: fsDetected.cicd || [],
|
|
682
|
+
subprojects: fsDetected.subprojects || [],
|
|
683
|
+
architecture: '',
|
|
684
|
+
buildCommands: {},
|
|
685
|
+
complexity: 0.5,
|
|
686
|
+
metrics: null,
|
|
687
|
+
entryPoints: [],
|
|
688
|
+
coreModules: [],
|
|
689
|
+
testFramework: '',
|
|
690
|
+
packageManager: fsDetected.packageManager || '',
|
|
691
|
+
languageDistribution: fsDetected.languageDistribution || null,
|
|
692
|
+
};
|
|
693
|
+
}
|
package/src/prompts/gather.js
CHANGED
|
@@ -227,6 +227,49 @@ export async function gatherMcpConfig(scoredMcps) {
|
|
|
227
227
|
return { selectedMcps, mcpKeys };
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
// ── MCP API key collection (standalone) ──────────────────────────────
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Prompt the user for API keys for MCPs that require them.
|
|
234
|
+
* Returns { [mcpId]: { KEY_NAME: 'value' } }.
|
|
235
|
+
*/
|
|
236
|
+
export async function gatherMcpKeys(selectedMcps) {
|
|
237
|
+
const mcpKeys = {};
|
|
238
|
+
const needKeys = selectedMcps.filter((m) => m.requiresKey);
|
|
239
|
+
|
|
240
|
+
if (needKeys.length === 0) return mcpKeys;
|
|
241
|
+
|
|
242
|
+
console.log(chalk.dim('\n Some MCP servers require API keys.\n'));
|
|
243
|
+
|
|
244
|
+
for (const mcp of needKeys) {
|
|
245
|
+
const keyDefs = mcp.keyNames || [{ name: mcp.keyName, description: mcp.keyDescription }];
|
|
246
|
+
const collected = {};
|
|
247
|
+
|
|
248
|
+
for (const keyDef of keyDefs) {
|
|
249
|
+
const key = await themedPassword({
|
|
250
|
+
message: `${mcp.id} — ${keyDef.description}:`,
|
|
251
|
+
hint: `Press Enter to skip. You can set ${keyDef.name} as an env variable later.`,
|
|
252
|
+
mask: '*',
|
|
253
|
+
});
|
|
254
|
+
if (key && key.trim()) {
|
|
255
|
+
const { warning } = validateApiKeyFormat(mcp.id, keyDef.name, key.trim());
|
|
256
|
+
if (warning) {
|
|
257
|
+
console.log(chalk.yellow(` ⚠ ${warning}`));
|
|
258
|
+
}
|
|
259
|
+
collected[keyDef.name] = key.trim();
|
|
260
|
+
} else {
|
|
261
|
+
console.log(chalk.dim(` Skipped — set ${keyDef.name} env var later`));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (Object.keys(collected).length > 0) {
|
|
266
|
+
mcpKeys[mcp.id] = collected;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return mcpKeys;
|
|
271
|
+
}
|
|
272
|
+
|
|
230
273
|
// ── Security configuration ────────────────────────────────────────────
|
|
231
274
|
|
|
232
275
|
export function gatherSecurityConfig(detected) {
|
package/src/ui/phase-header.js
CHANGED
|
@@ -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 ${
|
|
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} `;
|
package/src/commands/create.js
DELETED
|
@@ -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
|
-
}
|