ccraft 1.0.11 → 1.0.12
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/package.json +1 -1
- package/src/commands/create.js +12 -79
- package/src/commands/install.js +19 -110
- package/src/commands/update.js +1 -12
- package/src/ui/tasks.js +12 -32
- package/src/utils/analysis-cache.js +2 -32
- package/src/utils/api-client.js +6 -8
- package/src/utils/api-file-writer.js +2 -6
- package/src/utils/claude-scorer.js +0 -101
package/package.json
CHANGED
package/src/commands/create.js
CHANGED
|
@@ -3,13 +3,12 @@ import { mkdirSync, existsSync, writeFileSync, readdirSync } from 'fs';
|
|
|
3
3
|
import { execFileSync } from 'child_process';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import ora from 'ora';
|
|
6
|
-
import { gatherCreateProfile
|
|
6
|
+
import { gatherCreateProfile } from '../prompts/gather.js';
|
|
7
7
|
import { themedInput } from '../ui/prompts.js';
|
|
8
8
|
import { callGenerate, ApiError } from '../utils/api-client.js';
|
|
9
9
|
import { runPreflight } from '../utils/preflight.js';
|
|
10
10
|
import { writeApiFiles, buildFileList } from '../utils/api-file-writer.js';
|
|
11
11
|
import { setupMcps } from '../utils/mcp-setup.js';
|
|
12
|
-
import { scoreWithClaude } from '../utils/claude-scorer.js';
|
|
13
12
|
import { optimizeSettings } from '../utils/claude-optimizer.js';
|
|
14
13
|
import { rewriteClaudeMd } from '../utils/claude-rewriter.js';
|
|
15
14
|
import { detectProject } from '../utils/detect-project.js';
|
|
@@ -19,13 +18,12 @@ import { platformCmd } from '../utils/run-claude.js';
|
|
|
19
18
|
import { runBootstrap } from '../utils/bootstrap-runner.js';
|
|
20
19
|
import {
|
|
21
20
|
writeAnalysisCache,
|
|
22
|
-
writeUserProfile,
|
|
23
21
|
updateManifest,
|
|
24
22
|
readAnalysisCache,
|
|
25
23
|
promoteCache,
|
|
26
24
|
cleanupAnalysisCache,
|
|
27
25
|
} from '../utils/analysis-cache.js';
|
|
28
|
-
import { VERSION
|
|
26
|
+
import { VERSION } from '../constants.js';
|
|
29
27
|
import * as logger from '../utils/logger.js';
|
|
30
28
|
|
|
31
29
|
// UI modules
|
|
@@ -46,7 +44,7 @@ function renderCreatePhase(number, name, totalPhases) {
|
|
|
46
44
|
console.log();
|
|
47
45
|
}
|
|
48
46
|
|
|
49
|
-
function countTotalItems(summary
|
|
47
|
+
function countTotalItems(summary) {
|
|
50
48
|
const countBucket = (bucket) => {
|
|
51
49
|
if (!bucket) return 0;
|
|
52
50
|
return Object.values(bucket).reduce(
|
|
@@ -54,13 +52,7 @@ function countTotalItems(summary, selectedCandidateIds) {
|
|
|
54
52
|
0,
|
|
55
53
|
);
|
|
56
54
|
};
|
|
57
|
-
|
|
58
|
-
if (selectedCandidateIds === null) {
|
|
59
|
-
total += countBucket(summary.candidates);
|
|
60
|
-
} else {
|
|
61
|
-
total += selectedCandidateIds.length;
|
|
62
|
-
}
|
|
63
|
-
return total;
|
|
55
|
+
return countBucket(summary.guaranteed) + countBucket(summary.candidates);
|
|
64
56
|
}
|
|
65
57
|
|
|
66
58
|
// ── Main command ─────────────────────────────────────────────────────────────
|
|
@@ -170,35 +162,6 @@ export async function runCreate(options = {}) {
|
|
|
170
162
|
: `Created ${chalk.bold(name)}/ (git init skipped — git not available).`);
|
|
171
163
|
}
|
|
172
164
|
|
|
173
|
-
// ── Persona selection ────────────────────────────────────────────
|
|
174
|
-
let persona;
|
|
175
|
-
if (options.yes) {
|
|
176
|
-
persona = 'developer'; // non-interactive: always developer defaults
|
|
177
|
-
} else if (options.pro) {
|
|
178
|
-
persona = 'developer'; // interactive developer mode: skip persona prompt
|
|
179
|
-
} else {
|
|
180
|
-
persona = await gatherPersona();
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const isVibe = persona === 'vibe';
|
|
184
|
-
|
|
185
|
-
// Build user profile
|
|
186
|
-
let userProfile;
|
|
187
|
-
if (options.yes) {
|
|
188
|
-
userProfile = {
|
|
189
|
-
intents: ['implementing', 'debugging', 'refactoring', 'testing', 'reviewing'],
|
|
190
|
-
sourceControl: 'github',
|
|
191
|
-
documentTools: [],
|
|
192
|
-
};
|
|
193
|
-
logger.info('Non-interactive mode — using default profile.');
|
|
194
|
-
} else if (isVibe) {
|
|
195
|
-
userProfile = { ...VIBE_DEFAULTS };
|
|
196
|
-
logger.info('Vibe mode — using streamlined defaults.');
|
|
197
|
-
} else {
|
|
198
|
-
userProfile = await gatherUserProfile();
|
|
199
|
-
}
|
|
200
|
-
userProfile.persona = persona;
|
|
201
|
-
|
|
202
165
|
// ================================================================
|
|
203
166
|
// STEP 3: Configuration & Install
|
|
204
167
|
// ================================================================
|
|
@@ -259,7 +222,6 @@ export async function runCreate(options = {}) {
|
|
|
259
222
|
let apiResponse;
|
|
260
223
|
try {
|
|
261
224
|
apiResponse = await callGenerate(
|
|
262
|
-
userProfile,
|
|
263
225
|
{
|
|
264
226
|
...syntheticProjectInfo,
|
|
265
227
|
detectedFiles: [],
|
|
@@ -292,33 +254,14 @@ export async function runCreate(options = {}) {
|
|
|
292
254
|
console.log();
|
|
293
255
|
renderComponentBreakdown(summary);
|
|
294
256
|
|
|
295
|
-
//
|
|
296
|
-
|
|
297
|
-
|
|
257
|
+
// Auto-install all MCPs returned by the server
|
|
258
|
+
const selectedMcps = mcpConfigs || [];
|
|
259
|
+
const mcpKeys = {};
|
|
298
260
|
const securityConfig = { addSecurityGitignore: true };
|
|
299
261
|
|
|
300
|
-
|
|
301
|
-
selectedMcps = mcpConfigs.filter(
|
|
302
|
-
(m) => m.tier === 'core' || m.tier === 'role' || m.tier === 'stack' || m.tier === 'auto' || m.recommended,
|
|
303
|
-
);
|
|
304
|
-
} else {
|
|
305
|
-
const mcpResult = await gatherMcpConfig(mcpConfigs);
|
|
306
|
-
selectedMcps = mcpResult.selectedMcps;
|
|
307
|
-
mcpKeys = mcpResult.mcpKeys;
|
|
308
|
-
|
|
309
|
-
const finalSummary = { ...summary, mcps: selectedMcps.map((m) => ({ id: m.id, tier: m.tier })) };
|
|
310
|
-
const proceed = await confirmInstallation(finalSummary);
|
|
311
|
-
if (!proceed) {
|
|
312
|
-
console.log();
|
|
313
|
-
logger.info('Cancelled.');
|
|
314
|
-
process.exit(0);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Cache analysis and profile
|
|
262
|
+
// Cache analysis
|
|
319
263
|
try {
|
|
320
264
|
writeAnalysisCache(targetDir, syntheticProjectInfo, syntheticDetected, null);
|
|
321
|
-
writeUserProfile(targetDir, userProfile);
|
|
322
265
|
} catch (cacheErr) {
|
|
323
266
|
logger.debug(`Analysis cache write failed: ${cacheErr.message}`);
|
|
324
267
|
}
|
|
@@ -326,12 +269,10 @@ export async function runCreate(options = {}) {
|
|
|
326
269
|
// Write files
|
|
327
270
|
let results;
|
|
328
271
|
let filesToWrite;
|
|
329
|
-
let selectedCandidateIds = null;
|
|
330
272
|
|
|
331
273
|
try {
|
|
332
274
|
const installCtx = await runInstallTasks({
|
|
333
275
|
apiResponse,
|
|
334
|
-
selectedCandidateIds: null,
|
|
335
276
|
targetDir,
|
|
336
277
|
selectedMcps,
|
|
337
278
|
mcpKeys,
|
|
@@ -339,16 +280,14 @@ export async function runCreate(options = {}) {
|
|
|
339
280
|
detected: syntheticDetected,
|
|
340
281
|
buildFileList,
|
|
341
282
|
writeApiFiles,
|
|
342
|
-
scoreWithClaude,
|
|
343
283
|
});
|
|
344
284
|
|
|
345
285
|
results = installCtx.results;
|
|
346
286
|
filesToWrite = installCtx.filesToWrite;
|
|
347
|
-
selectedCandidateIds = installCtx.selectedCandidateIds ?? null;
|
|
348
287
|
} catch {
|
|
349
288
|
// Fallback to direct write
|
|
350
289
|
const spinnerWrite = ora('Writing configuration files...').start();
|
|
351
|
-
filesToWrite = buildFileList(apiResponse
|
|
290
|
+
filesToWrite = buildFileList(apiResponse);
|
|
352
291
|
results = await writeApiFiles(filesToWrite, targetDir, {
|
|
353
292
|
force: true,
|
|
354
293
|
selectedMcpIds: selectedMcps.map((m) => m.id),
|
|
@@ -458,7 +397,6 @@ export async function runCreate(options = {}) {
|
|
|
458
397
|
// Overwrite cache with real data
|
|
459
398
|
const detected = { ...fsDetected, ...projectInfo };
|
|
460
399
|
writeAnalysisCache(targetDir, projectInfo, detected, null);
|
|
461
|
-
writeUserProfile(targetDir, userProfile);
|
|
462
400
|
} catch (err) {
|
|
463
401
|
logger.debug(`Post-bootstrap analysis failed: ${err.message}`);
|
|
464
402
|
}
|
|
@@ -493,26 +431,21 @@ export async function runCreate(options = {}) {
|
|
|
493
431
|
}
|
|
494
432
|
|
|
495
433
|
// ── Success ──────────────────────────────────────────────────────
|
|
496
|
-
const totalItems = countTotalItems(summary
|
|
434
|
+
const totalItems = countTotalItems(summary);
|
|
497
435
|
const mcpsNeedingKeys = mcpResults.filter((r) => r.status === 'needs-key');
|
|
498
436
|
|
|
499
437
|
renderSuccessCard({
|
|
500
438
|
totalItems,
|
|
501
439
|
mcpCount: selectedMcps.length,
|
|
502
440
|
mcpsNeedingKeys,
|
|
503
|
-
persona,
|
|
504
441
|
});
|
|
505
442
|
|
|
506
443
|
console.log();
|
|
507
444
|
if (bootstrapSucceeded) {
|
|
508
|
-
logger.success(
|
|
509
|
-
? `Project ${chalk.bold(name)} is ready! Start Claude and describe what you want.`
|
|
510
|
-
: `Project ${chalk.bold(name)} created and bootstrapped!`);
|
|
445
|
+
logger.success(`Project ${chalk.bold(name)} created and bootstrapped!`);
|
|
511
446
|
console.log(chalk.dim(` cd ${name} && claude`));
|
|
512
447
|
} else {
|
|
513
|
-
logger.success(
|
|
514
|
-
? `Project ${chalk.bold(name)} is ready! Run Claude to finish setup.`
|
|
515
|
-
: `Project ${chalk.bold(name)} created with Claude configuration.`);
|
|
448
|
+
logger.success(`Project ${chalk.bold(name)} created with Claude configuration.`);
|
|
516
449
|
console.log(chalk.dim(` cd ${name} && claude -p "/bootstrap:auto ${description}"`));
|
|
517
450
|
}
|
|
518
451
|
console.log();
|
package/src/commands/install.js
CHANGED
|
@@ -11,27 +11,21 @@ import {
|
|
|
11
11
|
} from '../utils/existing-setup.js';
|
|
12
12
|
import {
|
|
13
13
|
gatherProjectPath,
|
|
14
|
-
gatherPersona,
|
|
15
|
-
gatherUserProfile,
|
|
16
|
-
gatherMcpConfig,
|
|
17
|
-
gatherSecurityConfig,
|
|
18
14
|
confirmInstallation,
|
|
19
15
|
} from '../prompts/gather.js';
|
|
20
16
|
import { callGenerate, ApiError } from '../utils/api-client.js';
|
|
21
17
|
import { writeApiFiles, buildFileList } from '../utils/api-file-writer.js';
|
|
22
18
|
import { setupMcps } from '../utils/mcp-setup.js';
|
|
23
|
-
import { scoreWithClaude } from '../utils/claude-scorer.js';
|
|
24
19
|
import { optimizeSettings } from '../utils/claude-optimizer.js';
|
|
25
20
|
import { runPreflight } from '../utils/preflight.js';
|
|
26
21
|
import {
|
|
27
22
|
writeAnalysisCache,
|
|
28
|
-
writeUserProfile,
|
|
29
23
|
updateManifest,
|
|
30
24
|
readAnalysisCache,
|
|
31
25
|
promoteCache,
|
|
32
26
|
cleanupAnalysisCache,
|
|
33
27
|
} from '../utils/analysis-cache.js';
|
|
34
|
-
import {
|
|
28
|
+
import { VERSION } from '../constants.js';
|
|
35
29
|
import * as logger from '../utils/logger.js';
|
|
36
30
|
|
|
37
31
|
// UI modules
|
|
@@ -40,16 +34,14 @@ import { renderPhaseHeader } from '../ui/phase-header.js';
|
|
|
40
34
|
import { renderProjectCard, renderSuccessCard } from '../ui/cards.js';
|
|
41
35
|
import { renderComponentBreakdown, renderMcpStatus, renderFileResults } from '../ui/tables.js';
|
|
42
36
|
import { runExistingSetupTasks, runAnalysisTasks, runInstallTasks, runVerifyTasks, runFinalizeTasks } from '../ui/tasks.js';
|
|
43
|
-
import { colors } from '../ui/theme.js';
|
|
44
|
-
import { dotPad } from '../ui/format.js';
|
|
45
37
|
|
|
46
38
|
/**
|
|
47
39
|
* Main install command — 5-phase orchestrator.
|
|
48
40
|
*
|
|
49
|
-
* Phase 1:
|
|
41
|
+
* Phase 1: Preflight (env check + project path)
|
|
50
42
|
* Phase 2: Project Discovery (Claude analysis + filesystem scan)
|
|
51
|
-
* Phase 3:
|
|
52
|
-
* Phase 4:
|
|
43
|
+
* Phase 3: Generate & Install (server API + confirmation + file writing)
|
|
44
|
+
* Phase 4: MCP Verification
|
|
53
45
|
* Phase 5: Finalization (settings optimization + CLAUDE.md rewrite + success)
|
|
54
46
|
*/
|
|
55
47
|
export async function runInstall(options = {}) {
|
|
@@ -73,33 +65,6 @@ export async function runInstall(options = {}) {
|
|
|
73
65
|
targetDir = resolve(options.dir || process.cwd());
|
|
74
66
|
}
|
|
75
67
|
|
|
76
|
-
// ── Persona selection ────────────────────────────────────────────
|
|
77
|
-
renderPhaseHeader(1);
|
|
78
|
-
|
|
79
|
-
let persona;
|
|
80
|
-
if (options.yes) {
|
|
81
|
-
persona = 'developer'; // non-interactive: always developer defaults
|
|
82
|
-
} else if (options.pro) {
|
|
83
|
-
persona = 'developer'; // interactive developer mode: skip persona prompt
|
|
84
|
-
} else {
|
|
85
|
-
persona = await gatherPersona();
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const isVibe = persona === 'vibe';
|
|
89
|
-
|
|
90
|
-
// ── User profile ───────────────────────────────────────────────
|
|
91
|
-
let userProfile;
|
|
92
|
-
if (options.yes) {
|
|
93
|
-
userProfile = { intents: ['implementing', 'debugging', 'refactoring', 'testing', 'reviewing'], sourceControl: 'github', documentTools: [] };
|
|
94
|
-
logger.info('Non-interactive mode — using default profile (web, all intents, github).');
|
|
95
|
-
} else if (isVibe) {
|
|
96
|
-
userProfile = { ...VIBE_DEFAULTS };
|
|
97
|
-
logger.info('Vibe mode — using streamlined defaults.');
|
|
98
|
-
} else {
|
|
99
|
-
userProfile = await gatherUserProfile();
|
|
100
|
-
}
|
|
101
|
-
userProfile.persona = persona;
|
|
102
|
-
|
|
103
68
|
// ================================================================
|
|
104
69
|
// PHASE 2: Project Discovery
|
|
105
70
|
// ================================================================
|
|
@@ -197,10 +162,9 @@ export async function runInstall(options = {}) {
|
|
|
197
162
|
detected = { ...fsDetected, ...projectInfo };
|
|
198
163
|
}
|
|
199
164
|
|
|
200
|
-
// Cache analysis
|
|
165
|
+
// Cache analysis for later phases and future update runs
|
|
201
166
|
try {
|
|
202
167
|
writeAnalysisCache(targetDir, projectInfo, detected, existingContext);
|
|
203
|
-
writeUserProfile(targetDir, userProfile);
|
|
204
168
|
} catch (cacheErr) {
|
|
205
169
|
logger.debug(`Analysis cache write failed: ${cacheErr.message}`);
|
|
206
170
|
}
|
|
@@ -209,24 +173,6 @@ export async function runInstall(options = {}) {
|
|
|
209
173
|
console.log();
|
|
210
174
|
renderProjectCard(projectInfo);
|
|
211
175
|
|
|
212
|
-
// Preset alias injection
|
|
213
|
-
if (options.preset) {
|
|
214
|
-
const alias = PRESET_ALIASES[options.preset];
|
|
215
|
-
if (!alias) {
|
|
216
|
-
logger.error(
|
|
217
|
-
`Unknown preset ${chalk.bold(options.preset)}. ` +
|
|
218
|
-
`Available presets: ${Object.keys(PRESET_ALIASES).map((p) => colors.success(p)).join(', ')}`
|
|
219
|
-
);
|
|
220
|
-
process.exit(1);
|
|
221
|
-
}
|
|
222
|
-
for (const fw of alias.frameworks) {
|
|
223
|
-
if (!projectInfo.frameworks.includes(fw)) projectInfo.frameworks.push(fw);
|
|
224
|
-
}
|
|
225
|
-
for (const lang of alias.languages) {
|
|
226
|
-
if (!projectInfo.languages.includes(lang)) projectInfo.languages.push(lang);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
176
|
// ================================================================
|
|
231
177
|
// PHASE 3: Configuration
|
|
232
178
|
// ================================================================
|
|
@@ -237,7 +183,6 @@ export async function runInstall(options = {}) {
|
|
|
237
183
|
let apiResponse;
|
|
238
184
|
try {
|
|
239
185
|
apiResponse = await callGenerate(
|
|
240
|
-
userProfile,
|
|
241
186
|
{
|
|
242
187
|
name: projectInfo.name,
|
|
243
188
|
projectType: projectInfo.projectType,
|
|
@@ -258,7 +203,7 @@ export async function runInstall(options = {}) {
|
|
|
258
203
|
detectedFiles: detected._rootFiles || [],
|
|
259
204
|
databases: projectInfo.databases || detected.databases || [],
|
|
260
205
|
},
|
|
261
|
-
{
|
|
206
|
+
{ projectPath: targetDir },
|
|
262
207
|
);
|
|
263
208
|
spinner3.succeed('Server returned configuration.');
|
|
264
209
|
} catch (err) {
|
|
@@ -272,27 +217,17 @@ export async function runInstall(options = {}) {
|
|
|
272
217
|
|
|
273
218
|
const { summary, mcpConfigs } = apiResponse;
|
|
274
219
|
|
|
275
|
-
// Display
|
|
220
|
+
// Display component summary
|
|
276
221
|
console.log();
|
|
277
222
|
renderComponentBreakdown(summary);
|
|
278
223
|
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (options.yes || isVibe) {
|
|
285
|
-
selectedMcps = mcpConfigs.filter(
|
|
286
|
-
(m) => m.tier === 'core' || m.tier === 'role' || m.tier === 'stack' || m.tier === 'auto' || m.recommended
|
|
287
|
-
);
|
|
288
|
-
securityConfig = { addSecurityGitignore: true };
|
|
289
|
-
} else {
|
|
290
|
-
const mcpResult = await gatherMcpConfig(mcpConfigs);
|
|
291
|
-
selectedMcps = mcpResult.selectedMcps;
|
|
292
|
-
mcpKeys = mcpResult.mcpKeys;
|
|
293
|
-
|
|
294
|
-
securityConfig = gatherSecurityConfig(detected);
|
|
224
|
+
// All MCPs are auto-installed; no interactive selection
|
|
225
|
+
const selectedMcps = mcpConfigs || [];
|
|
226
|
+
const mcpKeys = {};
|
|
227
|
+
const securityConfig = { addSecurityGitignore: true };
|
|
295
228
|
|
|
229
|
+
// Confirmation gate (skipped in non-interactive mode)
|
|
230
|
+
if (!options.yes) {
|
|
296
231
|
const finalSummary = { ...summary, mcps: selectedMcps.map((m) => ({ id: m.id, tier: m.tier })) };
|
|
297
232
|
const proceed = await confirmInstallation(finalSummary);
|
|
298
233
|
if (!proceed) {
|
|
@@ -309,12 +244,10 @@ export async function runInstall(options = {}) {
|
|
|
309
244
|
|
|
310
245
|
let results;
|
|
311
246
|
let filesToWrite;
|
|
312
|
-
let selectedCandidateIds = null;
|
|
313
247
|
|
|
314
248
|
try {
|
|
315
249
|
const installCtx = await runInstallTasks({
|
|
316
250
|
apiResponse,
|
|
317
|
-
selectedCandidateIds: null,
|
|
318
251
|
targetDir,
|
|
319
252
|
selectedMcps,
|
|
320
253
|
mcpKeys,
|
|
@@ -322,29 +255,14 @@ export async function runInstall(options = {}) {
|
|
|
322
255
|
detected,
|
|
323
256
|
buildFileList,
|
|
324
257
|
writeApiFiles,
|
|
325
|
-
scoreWithClaude,
|
|
326
258
|
});
|
|
327
259
|
|
|
328
260
|
results = installCtx.results;
|
|
329
261
|
filesToWrite = installCtx.filesToWrite;
|
|
330
|
-
selectedCandidateIds = installCtx.selectedCandidateIds ?? null;
|
|
331
262
|
} catch (installErr) {
|
|
332
263
|
// Fallback to sequential if task runner fails
|
|
333
|
-
const candidateCount = apiResponse.candidates?.items?.length || 0;
|
|
334
|
-
|
|
335
|
-
if (apiResponse.prompts?.scoring && candidateCount > 0) {
|
|
336
|
-
const spinner4 = ora(`Evaluating ${candidateCount} optional candidates...`).start();
|
|
337
|
-
const scoreResult = await scoreWithClaude(apiResponse.prompts.scoring, targetDir);
|
|
338
|
-
if (scoreResult.selected) {
|
|
339
|
-
selectedCandidateIds = scoreResult.selected;
|
|
340
|
-
spinner4.succeed(`Selected ${selectedCandidateIds.length}/${candidateCount} candidates.`);
|
|
341
|
-
} else {
|
|
342
|
-
spinner4.warn(`Scoring unavailable — including all ${candidateCount} candidates.`);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
264
|
const spinnerWrite = ora('Writing configuration files...').start();
|
|
347
|
-
filesToWrite = buildFileList(apiResponse
|
|
265
|
+
filesToWrite = buildFileList(apiResponse);
|
|
348
266
|
results = await writeApiFiles(filesToWrite, targetDir, {
|
|
349
267
|
force: true,
|
|
350
268
|
selectedMcpIds: selectedMcps.map((m) => m.id),
|
|
@@ -434,18 +352,17 @@ export async function runInstall(options = {}) {
|
|
|
434
352
|
}
|
|
435
353
|
|
|
436
354
|
// ── Success ──────────────────────────────────────────────────────
|
|
437
|
-
const totalItems = countTotalItems(summary
|
|
355
|
+
const totalItems = countTotalItems(summary);
|
|
438
356
|
const mcpsNeedingKeys = mcpResults.filter((r) => r.status === 'needs-key');
|
|
439
357
|
|
|
440
358
|
renderSuccessCard({
|
|
441
359
|
totalItems,
|
|
442
360
|
mcpCount: selectedMcps.length,
|
|
443
361
|
mcpsNeedingKeys,
|
|
444
|
-
persona,
|
|
445
362
|
});
|
|
446
363
|
|
|
447
364
|
console.log();
|
|
448
|
-
logger.success(
|
|
365
|
+
logger.success('Done! Claude Code is ready.');
|
|
449
366
|
console.log();
|
|
450
367
|
} catch (err) {
|
|
451
368
|
if (
|
|
@@ -475,9 +392,9 @@ export async function runInstall(options = {}) {
|
|
|
475
392
|
}
|
|
476
393
|
|
|
477
394
|
/**
|
|
478
|
-
* Count total installed items from
|
|
395
|
+
* Count total installed items from the summary.
|
|
479
396
|
*/
|
|
480
|
-
function countTotalItems(summary
|
|
397
|
+
function countTotalItems(summary) {
|
|
481
398
|
const countBucket = (bucket) => {
|
|
482
399
|
if (!bucket) return 0;
|
|
483
400
|
return Object.values(bucket).reduce(
|
|
@@ -486,13 +403,5 @@ function countTotalItems(summary, selectedCandidateIds) {
|
|
|
486
403
|
);
|
|
487
404
|
};
|
|
488
405
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
if (selectedCandidateIds === null) {
|
|
492
|
-
total += countBucket(summary.candidates);
|
|
493
|
-
} else {
|
|
494
|
-
total += selectedCandidateIds.length;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
return total;
|
|
406
|
+
return countBucket(summary.guaranteed) + countBucket(summary.candidates);
|
|
498
407
|
}
|
package/src/commands/update.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* calls /api/update to get delta components, and installs only what's new.
|
|
4
4
|
*
|
|
5
5
|
* Flow:
|
|
6
|
-
* 1. Load stored
|
|
6
|
+
* 1. Load stored analysis from .claude/.claude-craft/
|
|
7
7
|
* 2. Re-run project analysis (detectProject + analyzeWithClaude)
|
|
8
8
|
* 3. Extract installed relative paths from manifest
|
|
9
9
|
* 4. Call /api/update — server returns only new/unlocked components
|
|
@@ -16,7 +16,6 @@ import ora from 'ora';
|
|
|
16
16
|
import { detectProject } from '../utils/detect-project.js';
|
|
17
17
|
import { analyzeWithClaude } from '../utils/claude-analyzer.js';
|
|
18
18
|
import {
|
|
19
|
-
readUserProfile,
|
|
20
19
|
readPermanentAnalysis,
|
|
21
20
|
readInstalledManifest,
|
|
22
21
|
mergePermanentManifest,
|
|
@@ -135,18 +134,9 @@ export async function runUpdate(options = {}) {
|
|
|
135
134
|
});
|
|
136
135
|
|
|
137
136
|
// ── Guard: must have a previous install ───────────────────────────
|
|
138
|
-
const storedProfile = readUserProfile(targetDir);
|
|
139
137
|
const previousAnalysis = readPermanentAnalysis(targetDir);
|
|
140
138
|
const installedManifest = readInstalledManifest(targetDir);
|
|
141
139
|
|
|
142
|
-
if (!storedProfile) {
|
|
143
|
-
logger.error(
|
|
144
|
-
'No stored user profile found. Re-run: ' + chalk.bold('ccraft install') +
|
|
145
|
-
' to rebuild the profile cache.',
|
|
146
|
-
);
|
|
147
|
-
process.exit(1);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
140
|
if (!previousAnalysis) {
|
|
151
141
|
logger.error(
|
|
152
142
|
'No stored project analysis found. Re-run: ' + chalk.bold('ccraft install') +
|
|
@@ -231,7 +221,6 @@ export async function runUpdate(options = {}) {
|
|
|
231
221
|
let updateResponse;
|
|
232
222
|
try {
|
|
233
223
|
updateResponse = await callUpdate(
|
|
234
|
-
storedProfile,
|
|
235
224
|
currentProjectInfo,
|
|
236
225
|
previousAnalysis,
|
|
237
226
|
installedRelativePaths,
|
package/src/ui/tasks.js
CHANGED
|
@@ -104,7 +104,6 @@ export async function runAnalysisTasks(targetDir, { analyzeWithClaude, detectPro
|
|
|
104
104
|
*/
|
|
105
105
|
export async function runInstallTasks({
|
|
106
106
|
apiResponse,
|
|
107
|
-
selectedCandidateIds,
|
|
108
107
|
targetDir,
|
|
109
108
|
selectedMcps,
|
|
110
109
|
mcpKeys,
|
|
@@ -112,43 +111,24 @@ export async function runInstallTasks({
|
|
|
112
111
|
detected,
|
|
113
112
|
buildFileList,
|
|
114
113
|
writeApiFiles,
|
|
115
|
-
scoreWithClaude,
|
|
116
114
|
}) {
|
|
117
115
|
const ctx = {};
|
|
118
116
|
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const taskList = [];
|
|
123
|
-
|
|
124
|
-
if (hasScoring) {
|
|
125
|
-
taskList.push({
|
|
126
|
-
title: `Evaluating ${candidateCount} optional candidates with Claude`,
|
|
117
|
+
const taskList = [
|
|
118
|
+
{
|
|
119
|
+
title: 'Writing configuration files',
|
|
127
120
|
task: async (ctx) => {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
121
|
+
ctx.filesToWrite = buildFileList(apiResponse);
|
|
122
|
+
ctx.results = await writeApiFiles(ctx.filesToWrite, targetDir, {
|
|
123
|
+
force: true,
|
|
124
|
+
selectedMcpIds: selectedMcps.map((m) => m.id),
|
|
125
|
+
mcpKeys,
|
|
126
|
+
securityConfig,
|
|
127
|
+
detected,
|
|
128
|
+
});
|
|
134
129
|
},
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
taskList.push({
|
|
139
|
-
title: 'Writing configuration files',
|
|
140
|
-
task: async (ctx) => {
|
|
141
|
-
const ids = ctx.selectedCandidateIds ?? selectedCandidateIds;
|
|
142
|
-
ctx.filesToWrite = buildFileList(apiResponse, ids);
|
|
143
|
-
ctx.results = await writeApiFiles(ctx.filesToWrite, targetDir, {
|
|
144
|
-
force: true,
|
|
145
|
-
selectedMcpIds: selectedMcps.map((m) => m.id),
|
|
146
|
-
mcpKeys,
|
|
147
|
-
securityConfig,
|
|
148
|
-
detected,
|
|
149
|
-
});
|
|
150
130
|
},
|
|
151
|
-
|
|
131
|
+
];
|
|
152
132
|
|
|
153
133
|
const tasks = createTaskRunner(taskList);
|
|
154
134
|
await tasks.run(ctx);
|
|
@@ -16,7 +16,7 @@ const CACHE_DIR = '.claude/.claude-craft-temp';
|
|
|
16
16
|
const PERMANENT_DIR = '.claude/.claude-craft';
|
|
17
17
|
|
|
18
18
|
/** Files promoted from temp → permanent after install. */
|
|
19
|
-
const PERMANENT_FILES = ['project-analysis.json', 'project-context.md', 'installed-manifest.json'
|
|
19
|
+
const PERMANENT_FILES = ['project-analysis.json', 'project-context.md', 'installed-manifest.json'];
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Resolve the temp cache directory path for a given target directory.
|
|
@@ -315,37 +315,7 @@ export function formatRichProjectContext(projectInfo, detected) {
|
|
|
315
315
|
return lines.join('\n').trimEnd();
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
-
|
|
319
|
-
* Write user profile to the temp cache directory.
|
|
320
|
-
* Called alongside writeAnalysisCache during install.
|
|
321
|
-
*
|
|
322
|
-
* @param {string} targetDir - Project root
|
|
323
|
-
* @param {object} userProfile - { intents, sourceControl, documentTools }
|
|
324
|
-
*/
|
|
325
|
-
export function writeUserProfile(targetDir, userProfile) {
|
|
326
|
-
const dir = cachePath(targetDir);
|
|
327
|
-
mkdirSync(dir, { recursive: true });
|
|
328
|
-
writeFileSync(join(dir, 'user-profile.json'), JSON.stringify(userProfile, null, 2), 'utf8');
|
|
329
|
-
logger.debug('User profile written to analysis cache.');
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* Read the stored user profile from the permanent .claude/.claude-craft/ directory.
|
|
334
|
-
* Returns null if not found.
|
|
335
|
-
*
|
|
336
|
-
* @param {string} targetDir - Project root
|
|
337
|
-
* @returns {object|null}
|
|
338
|
-
*/
|
|
339
|
-
export function readUserProfile(targetDir) {
|
|
340
|
-
const filePath = join(permanentPath(targetDir), 'user-profile.json');
|
|
341
|
-
try {
|
|
342
|
-
if (!existsSync(filePath)) return null;
|
|
343
|
-
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
344
|
-
} catch (err) {
|
|
345
|
-
logger.debug(`Failed to read user profile: ${err.message}`);
|
|
346
|
-
return null;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
318
|
+
// writeUserProfile and readUserProfile removed — user profile no longer used.
|
|
349
319
|
|
|
350
320
|
/**
|
|
351
321
|
* Read the permanent project analysis JSON.
|
package/src/utils/api-client.js
CHANGED
|
@@ -45,12 +45,11 @@ export function saveConfig(config) {
|
|
|
45
45
|
/**
|
|
46
46
|
* Call POST /api/generate on the server.
|
|
47
47
|
*
|
|
48
|
-
* @param {object} profile - { intents, sourceControl, documentTools }
|
|
49
48
|
* @param {object} analysis - Project analysis data
|
|
50
|
-
* @param {object} [options] - {
|
|
49
|
+
* @param {object} [options] - { projectPath }
|
|
51
50
|
* @returns {Promise<{ files, summary, mcpConfigs, serverVersion }>}
|
|
52
51
|
*/
|
|
53
|
-
export async function callGenerate(
|
|
52
|
+
export async function callGenerate(analysis, options = {}) {
|
|
54
53
|
const config = loadConfig();
|
|
55
54
|
if (!config?.apiKey) {
|
|
56
55
|
throw new ApiError(
|
|
@@ -75,7 +74,7 @@ export async function callGenerate(profile, analysis, options = {}) {
|
|
|
75
74
|
'X-Claude-Craft-Version': VERSION,
|
|
76
75
|
'X-Claude-Craft-Api-Version': '1',
|
|
77
76
|
},
|
|
78
|
-
body: JSON.stringify({
|
|
77
|
+
body: JSON.stringify({ analysis, options }),
|
|
79
78
|
signal: controller.signal,
|
|
80
79
|
});
|
|
81
80
|
|
|
@@ -131,13 +130,12 @@ export async function callGenerate(profile, analysis, options = {}) {
|
|
|
131
130
|
* Call POST /api/update on the server.
|
|
132
131
|
* Returns delta components (new files not already installed) + change summary.
|
|
133
132
|
*
|
|
134
|
-
* @param {object} profile - { intents, sourceControl, documentTools }
|
|
135
133
|
* @param {object} currentAnalysis - Freshly computed project analysis
|
|
136
134
|
* @param {object} previousAnalysis - Previously stored project analysis
|
|
137
135
|
* @param {string[]} installedRelativePaths - Relative file paths already on disk
|
|
138
|
-
* @returns {Promise<{ changes, guaranteed,
|
|
136
|
+
* @returns {Promise<{ changes, guaranteed, mcpConfigs, summary }>}
|
|
139
137
|
*/
|
|
140
|
-
export async function callUpdate(
|
|
138
|
+
export async function callUpdate(currentAnalysis, previousAnalysis, installedRelativePaths) {
|
|
141
139
|
const config = loadConfig();
|
|
142
140
|
if (!config?.apiKey) {
|
|
143
141
|
throw new ApiError(
|
|
@@ -162,7 +160,7 @@ export async function callUpdate(profile, currentAnalysis, previousAnalysis, ins
|
|
|
162
160
|
'X-Claude-Craft-Version': VERSION,
|
|
163
161
|
'X-Claude-Craft-Api-Version': '1',
|
|
164
162
|
},
|
|
165
|
-
body: JSON.stringify({
|
|
163
|
+
body: JSON.stringify({ currentAnalysis, previousAnalysis, installedPaths: installedRelativePaths }),
|
|
166
164
|
signal: controller.signal,
|
|
167
165
|
});
|
|
168
166
|
|
|
@@ -132,19 +132,15 @@ export async function writeApiFiles(files, targetDir, opts = {}) {
|
|
|
132
132
|
/**
|
|
133
133
|
* Build a flat file list from a V3 API response.
|
|
134
134
|
*
|
|
135
|
-
* @param {object} apiResponse -
|
|
136
|
-
* @param {string[]|null} selectedCandidateIds - IDs selected by Claude, or null for all
|
|
135
|
+
* @param {object} apiResponse - Response with guaranteed.files and candidates.items
|
|
137
136
|
* @returns {Array<{relativePath: string, content: string, type: string}>}
|
|
138
137
|
*/
|
|
139
|
-
export function buildFileList(apiResponse
|
|
138
|
+
export function buildFileList(apiResponse) {
|
|
140
139
|
const files = [...(apiResponse.guaranteed?.files || [])];
|
|
141
140
|
|
|
142
141
|
const candidates = apiResponse.candidates?.items || [];
|
|
143
|
-
const selectedSet = selectedCandidateIds ? new Set(selectedCandidateIds) : null;
|
|
144
142
|
|
|
145
143
|
for (const candidate of candidates) {
|
|
146
|
-
// Skip unselected candidates (null = include all)
|
|
147
|
-
if (selectedSet && !selectedSet.has(candidate.id)) continue;
|
|
148
144
|
|
|
149
145
|
if (Array.isArray(candidate.files) && candidate.files.length > 0) {
|
|
150
146
|
// Multi-file candidates (skills with references/)
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Claude-powered scoring for optional candidates.
|
|
3
|
-
*
|
|
4
|
-
* Uses the local Claude CLI to evaluate which optional candidates
|
|
5
|
-
* should be included in the configuration.
|
|
6
|
-
*/
|
|
7
|
-
import { isClaudeAvailable, runClaude } from './run-claude.js';
|
|
8
|
-
import { extractJsonObject } from './json-extract.js';
|
|
9
|
-
import * as logger from './logger.js';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Score optional candidates using Claude.
|
|
13
|
-
*
|
|
14
|
-
* @param {string} scoringPrompt - Prompt generated by the server
|
|
15
|
-
* @param {string} targetDir - Project root (cwd for Claude)
|
|
16
|
-
* @returns {Promise<{ selected: string[], reasoning: string }>}
|
|
17
|
-
* Falls back to selecting ALL candidates if Claude is unavailable.
|
|
18
|
-
*/
|
|
19
|
-
export async function scoreWithClaude(scoringPrompt, targetDir) {
|
|
20
|
-
if (!isClaudeAvailable()) {
|
|
21
|
-
logger.warn('Claude CLI not available — including all optional candidates.');
|
|
22
|
-
return { selected: null, reasoning: 'Claude unavailable — inclusive fallback' };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
try {
|
|
26
|
-
const output = await runClaude([
|
|
27
|
-
'-p',
|
|
28
|
-
'--max-turns', '3',
|
|
29
|
-
], { cwd: targetDir, stdinInput: scoringPrompt });
|
|
30
|
-
|
|
31
|
-
// Parse the scoring result directly (no JSON wrapper to extract)
|
|
32
|
-
const result = parseScoreResult(output);
|
|
33
|
-
if (result) {
|
|
34
|
-
return result;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
logger.warn('Claude did not return valid scoring JSON — including all candidates.');
|
|
38
|
-
logger.debug(`Raw output (first 500 chars): ${(output || '').slice(0, 500)}`);
|
|
39
|
-
return { selected: null, reasoning: 'Parse failure — inclusive fallback' };
|
|
40
|
-
} catch (err) {
|
|
41
|
-
if (err.killed) {
|
|
42
|
-
logger.warn('Claude scoring timed out — including all candidates.');
|
|
43
|
-
} else {
|
|
44
|
-
logger.warn('Claude scoring failed — including all candidates.');
|
|
45
|
-
}
|
|
46
|
-
return { selected: null, reasoning: 'Error — inclusive fallback' };
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Parse Claude's scoring response.
|
|
52
|
-
* Handles both raw JSON and JSON embedded in markdown fences.
|
|
53
|
-
*/
|
|
54
|
-
function parseScoreResult(text) {
|
|
55
|
-
if (!text) return null;
|
|
56
|
-
|
|
57
|
-
const tryParse = (obj) => {
|
|
58
|
-
// V3 format: { selections: [{ id, category, include, reason }] }
|
|
59
|
-
if (Array.isArray(obj.selections)) {
|
|
60
|
-
const selected = obj.selections.filter((s) => s.include).map((s) => s.id);
|
|
61
|
-
const reasoning = obj.selections
|
|
62
|
-
.filter((s) => s.reason)
|
|
63
|
-
.map((s) => `${s.id}: ${s.include ? 'included' : 'excluded'} — ${s.reason}`)
|
|
64
|
-
.join('; ');
|
|
65
|
-
return { selected, reasoning };
|
|
66
|
-
}
|
|
67
|
-
// Fallback: { selected: ["id1", "id2"], reasoning: "..." }
|
|
68
|
-
if (Array.isArray(obj.selected)) {
|
|
69
|
-
return { selected: obj.selected, reasoning: obj.reasoning || '' };
|
|
70
|
-
}
|
|
71
|
-
return null;
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
// Try direct JSON parse
|
|
75
|
-
try {
|
|
76
|
-
const result = tryParse(JSON.parse(text.trim()));
|
|
77
|
-
if (result) return result;
|
|
78
|
-
} catch {
|
|
79
|
-
// Not direct JSON
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Try extracting JSON from markdown code fence
|
|
83
|
-
const fenceMatch = text.match(/```(?:json)?\s*\n([\s\S]*?)```/);
|
|
84
|
-
if (fenceMatch) {
|
|
85
|
-
try {
|
|
86
|
-
const result = tryParse(JSON.parse(fenceMatch[1].trim()));
|
|
87
|
-
if (result) return result;
|
|
88
|
-
} catch {
|
|
89
|
-
// Invalid JSON in fence
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Use brace-balanced extraction
|
|
94
|
-
const obj = extractJsonObject(text, 'selections') || extractJsonObject(text, 'selected');
|
|
95
|
-
if (obj) {
|
|
96
|
-
const result = tryParse(obj);
|
|
97
|
-
if (result) return result;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return null;
|
|
101
|
-
}
|