ccraft 1.0.0

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.
Files changed (40) hide show
  1. package/bin/claude-craft.js +85 -0
  2. package/package.json +39 -0
  3. package/src/commands/auth.js +43 -0
  4. package/src/commands/create.js +543 -0
  5. package/src/commands/install.js +480 -0
  6. package/src/commands/logout.js +24 -0
  7. package/src/commands/update.js +339 -0
  8. package/src/constants.js +299 -0
  9. package/src/generators/directories.js +30 -0
  10. package/src/generators/metadata.js +57 -0
  11. package/src/generators/security.js +39 -0
  12. package/src/prompts/gather.js +308 -0
  13. package/src/ui/brand.js +62 -0
  14. package/src/ui/cards.js +179 -0
  15. package/src/ui/format.js +55 -0
  16. package/src/ui/phase-header.js +20 -0
  17. package/src/ui/prompts.js +56 -0
  18. package/src/ui/tables.js +89 -0
  19. package/src/ui/tasks.js +258 -0
  20. package/src/ui/theme.js +83 -0
  21. package/src/utils/analysis-cache.js +519 -0
  22. package/src/utils/api-client.js +253 -0
  23. package/src/utils/api-file-writer.js +197 -0
  24. package/src/utils/bootstrap-runner.js +148 -0
  25. package/src/utils/claude-analyzer.js +255 -0
  26. package/src/utils/claude-optimizer.js +341 -0
  27. package/src/utils/claude-rewriter.js +553 -0
  28. package/src/utils/claude-scorer.js +101 -0
  29. package/src/utils/description-analyzer.js +116 -0
  30. package/src/utils/detect-project.js +1276 -0
  31. package/src/utils/existing-setup.js +341 -0
  32. package/src/utils/file-writer.js +64 -0
  33. package/src/utils/json-extract.js +56 -0
  34. package/src/utils/logger.js +27 -0
  35. package/src/utils/mcp-setup.js +461 -0
  36. package/src/utils/preflight.js +112 -0
  37. package/src/utils/prompt-api-key.js +59 -0
  38. package/src/utils/run-claude.js +152 -0
  39. package/src/utils/security.js +82 -0
  40. package/src/utils/toolkit-rule-generator.js +364 -0
@@ -0,0 +1,543 @@
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, gatherMcpConfig, confirmInstallation } 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 { scoreWithClaude } from '../utils/claude-scorer.js';
13
+ import { optimizeSettings } from '../utils/claude-optimizer.js';
14
+ import { rewriteClaudeMd } from '../utils/claude-rewriter.js';
15
+ import { detectProject } from '../utils/detect-project.js';
16
+ import { analyzeWithClaude } from '../utils/claude-analyzer.js';
17
+ import { analyzeDescription } from '../utils/description-analyzer.js';
18
+ import { platformCmd } from '../utils/run-claude.js';
19
+ import { runBootstrap } from '../utils/bootstrap-runner.js';
20
+ import {
21
+ writeAnalysisCache,
22
+ writeUserProfile,
23
+ updateManifest,
24
+ readAnalysisCache,
25
+ promoteCache,
26
+ cleanupAnalysisCache,
27
+ } from '../utils/analysis-cache.js';
28
+ import { VERSION } from '../constants.js';
29
+ import * as logger from '../utils/logger.js';
30
+
31
+ // UI modules
32
+ import { renderBanner } from '../ui/brand.js';
33
+ import { renderComponentBreakdown, renderMcpStatus, renderFileResults } from '../ui/tables.js';
34
+ import { renderSuccessCard } from '../ui/cards.js';
35
+ import { runInstallTasks, runVerifyTasks, runFinalizeTasks } from '../ui/tasks.js';
36
+
37
+ // ── Helpers ──────────────────────────────────────────────────────────────────
38
+
39
+ function renderCreatePhase(number, name, totalPhases) {
40
+ const w = Math.min((process.stdout.columns || 80) - 4, 56);
41
+ const label = `Phase ${number} of ${totalPhases}`;
42
+ const inner = ` ${label} \u2500\u2500 ${name} `;
43
+ const tailLen = Math.max(2, w - inner.length - 2);
44
+ console.log();
45
+ console.log(chalk.bold.cyan(` \u2500\u2500${inner}${'\u2500'.repeat(tailLen)}`));
46
+ console.log();
47
+ }
48
+
49
+ function countTotalItems(summary, selectedCandidateIds) {
50
+ const countBucket = (bucket) => {
51
+ if (!bucket) return 0;
52
+ return Object.values(bucket).reduce(
53
+ (sum, arr) => sum + (Array.isArray(arr) ? arr.length : 0),
54
+ 0,
55
+ );
56
+ };
57
+ let total = countBucket(summary.guaranteed);
58
+ if (selectedCandidateIds === null) {
59
+ total += countBucket(summary.candidates);
60
+ } else {
61
+ total += selectedCandidateIds.length;
62
+ }
63
+ return total;
64
+ }
65
+
66
+ // ── Main command ─────────────────────────────────────────────────────────────
67
+
68
+ /**
69
+ * Create command — build a new project from scratch.
70
+ *
71
+ * Step 1: Welcome & Setup (env check + API key)
72
+ * Step 2: Project Setup (name, description, role, mkdir, git init)
73
+ * Step 3: Configuration & Install (synthetic analysis → server → write .claude/)
74
+ * Step 4: Bootstrap (hand off to Claude CLI /bootstrap:auto)
75
+ * Step 5: Post-bootstrap Sync (re-analyze, rewrite CLAUDE.md, finalize)
76
+ */
77
+ export async function runCreate(options = {}) {
78
+ const TOTAL_PHASES = 5;
79
+ let targetDir;
80
+
81
+ try {
82
+ // ================================================================
83
+ // STEP 1: Welcome & Setup
84
+ // ================================================================
85
+ renderBanner(VERSION);
86
+ renderCreatePhase(1, 'Welcome & Setup', TOTAL_PHASES);
87
+
88
+ // ── Pre-flight checks (Claude Code + API key + server) ──────
89
+ await runPreflight({
90
+ interactive: !options.yes,
91
+ requireClaude: true,
92
+ });
93
+
94
+ // ================================================================
95
+ // STEP 2: Project Setup
96
+ // ================================================================
97
+ renderCreatePhase(2, 'Project Setup', TOTAL_PHASES);
98
+
99
+ let createProfile;
100
+ if (options.yes) {
101
+ createProfile = {
102
+ name: options.name || 'my-project',
103
+ description: options.description || 'A new project',
104
+ role: 'web',
105
+ projectType: 'monolith',
106
+ };
107
+ logger.info(`Non-interactive mode — creating ${chalk.bold(createProfile.name)} (web, monolith).`);
108
+ } else {
109
+ createProfile = await gatherCreateProfile();
110
+ }
111
+
112
+ let { name, description, role, projectType } = createProfile;
113
+
114
+ // Resolve target directory
115
+ const parentDir = resolve(options.dir || process.cwd());
116
+ let useCurrentDir = false;
117
+
118
+ if (!name) {
119
+ // Empty name = use current directory — must be empty
120
+ targetDir = parentDir;
121
+ useCurrentDir = true;
122
+ const contents = readdirSync(targetDir).filter((f) => f !== '.git');
123
+ if (contents.length > 0) {
124
+ logger.error(`Current directory ${chalk.bold(targetDir)} is not empty. Cannot create a project here.`);
125
+ process.exit(1);
126
+ }
127
+ name = basename(targetDir) || 'my-project';
128
+ } else {
129
+ // Named project — re-prompt if directory already exists
130
+ targetDir = join(parentDir, name);
131
+ while (existsSync(targetDir)) {
132
+ logger.warn(`Directory ${chalk.bold(name)} already exists.`);
133
+ const newName = await themedInput({
134
+ message: 'Enter a different project name:',
135
+ hint: 'Letters, numbers, dots, hyphens, underscores only.',
136
+ validate: (v) => {
137
+ const t = v.trim();
138
+ if (!t) return 'Name is required.';
139
+ if (!/^[a-zA-Z0-9._-]+$/.test(t)) return 'Only letters, numbers, dots, hyphens, and underscores allowed.';
140
+ return true;
141
+ },
142
+ });
143
+ name = newName.trim();
144
+ targetDir = join(parentDir, name);
145
+ }
146
+ }
147
+
148
+ // Create directory + git init
149
+ const spinner1 = ora(useCurrentDir ? 'Initializing project in current directory...' : 'Creating project directory...').start();
150
+ if (!useCurrentDir) {
151
+ mkdirSync(targetDir, { recursive: true });
152
+ }
153
+
154
+ // Write a minimal .gitignore
155
+ writeFileSync(
156
+ join(targetDir, '.gitignore'),
157
+ 'node_modules/\ndist/\nbuild/\n.env\n.env.*\n!.env.example\n*.log\n.DS_Store\nThumbs.db\n',
158
+ 'utf8',
159
+ );
160
+
161
+ // git init
162
+ try {
163
+ const { file, args } = platformCmd('git', ['init']);
164
+ execFileSync(file, args, { cwd: targetDir, stdio: 'pipe', windowsHide: true });
165
+ spinner1.succeed(useCurrentDir
166
+ ? `Initialized project in ${chalk.bold(targetDir)} with git.`
167
+ : `Created ${chalk.bold(name)}/ with git initialized.`);
168
+ } catch {
169
+ spinner1.succeed(useCurrentDir
170
+ ? `Initialized project in ${chalk.bold(targetDir)} (git init skipped — git not available).`
171
+ : `Created ${chalk.bold(name)}/ (git init skipped — git not available).`);
172
+ }
173
+
174
+ // Build user profile from role
175
+ const userProfile = {
176
+ role,
177
+ intents: ['implementing', 'debugging', 'refactoring', 'testing', 'reviewing'],
178
+ sourceControl: 'github',
179
+ documentTools: [],
180
+ };
181
+
182
+ // ================================================================
183
+ // STEP 3: Configuration & Install
184
+ // ================================================================
185
+ renderCreatePhase(3, 'Configuration', TOTAL_PHASES);
186
+
187
+ // Analyze description with Claude to infer tech stack
188
+ let descAnalysis = null;
189
+ {
190
+ const spinnerAnalyze = ora('Analyzing your requirements...').start();
191
+ const { analysis, failReason } = await analyzeDescription(description, role, projectType);
192
+ if (analysis) {
193
+ descAnalysis = analysis;
194
+ const parts = [];
195
+ if (analysis.frameworks.length) parts.push(analysis.frameworks.join(', '));
196
+ if (analysis.languages.length) parts.push(analysis.languages.join(', '));
197
+ if (analysis.databases.length) parts.push(analysis.databases.join(', '));
198
+ spinnerAnalyze.succeed(
199
+ parts.length
200
+ ? `Detected stack: ${chalk.bold(parts.join(' + '))}`
201
+ : 'Requirements analyzed.',
202
+ );
203
+ } else {
204
+ spinnerAnalyze.info(`Stack inference skipped${failReason ? ` (${failReason})` : ''} — using defaults.`);
205
+ }
206
+ }
207
+
208
+ // Build project info — enriched with description analysis if available
209
+ const syntheticProjectInfo = {
210
+ name,
211
+ description,
212
+ projectType,
213
+ languages: descAnalysis?.languages?.length ? descAnalysis.languages : [],
214
+ frameworks: descAnalysis?.frameworks?.length ? descAnalysis.frameworks : [],
215
+ codeStyle: descAnalysis?.codeStyle?.length ? descAnalysis.codeStyle : [],
216
+ cicd: descAnalysis?.cicd?.length ? descAnalysis.cicd : [],
217
+ subprojects: descAnalysis?.subprojects?.length ? descAnalysis.subprojects : [],
218
+ architecture: descAnalysis?.architecture || '',
219
+ buildCommands: descAnalysis?.buildCommands || {},
220
+ complexity: descAnalysis?.complexity ?? 0.3,
221
+ metrics: descAnalysis?.metrics || null,
222
+ entryPoints: descAnalysis?.entryPoints || [],
223
+ coreModules: descAnalysis?.coreModules || [],
224
+ testFramework: descAnalysis?.testFramework || '',
225
+ packageManager: descAnalysis?.packageManager || '',
226
+ languageDistribution: descAnalysis?.languageDistribution || null,
227
+ };
228
+
229
+ const syntheticDetected = {
230
+ ...syntheticProjectInfo,
231
+ sensitiveFiles: { found: [], gitignoreCovers: true },
232
+ _rootFiles: [],
233
+ databases: descAnalysis?.databases?.length ? descAnalysis.databases : [],
234
+ };
235
+
236
+ // Call server
237
+ const spinner3 = ora('Calling claude-craft server...').start();
238
+
239
+ let apiResponse;
240
+ try {
241
+ apiResponse = await callGenerate(
242
+ userProfile,
243
+ {
244
+ ...syntheticProjectInfo,
245
+ detectedFiles: [],
246
+ databases: syntheticDetected.databases,
247
+ },
248
+ {},
249
+ );
250
+ spinner3.succeed('Server returned configuration.');
251
+ } catch (err) {
252
+ spinner3.fail('Server request failed.');
253
+ if (err instanceof ApiError) {
254
+ logger.error(err.message);
255
+ process.exit(1);
256
+ }
257
+ throw err;
258
+ }
259
+
260
+ const { summary, mcpConfigs } = apiResponse;
261
+
262
+ // Validate we got components
263
+ const guaranteedCount = summary?.guaranteed
264
+ ? Object.values(summary.guaranteed).reduce((s, arr) => s + (Array.isArray(arr) ? arr.length : 0), 0)
265
+ : 0;
266
+
267
+ if (guaranteedCount === 0) {
268
+ logger.warn('Server returned zero guaranteed components. Continuing with core defaults.');
269
+ }
270
+
271
+ // Display scoring summary
272
+ console.log();
273
+ renderComponentBreakdown(summary);
274
+
275
+ // MCP selection — auto-select core/role/stack for new projects (no stack to score against)
276
+ let selectedMcps;
277
+ let mcpKeys = {};
278
+ const securityConfig = { addSecurityGitignore: true };
279
+
280
+ if (options.yes) {
281
+ selectedMcps = mcpConfigs.filter(
282
+ (m) => m.tier === 'core' || m.tier === 'role' || m.tier === 'stack' || m.tier === 'auto' || m.recommended,
283
+ );
284
+ } else {
285
+ const mcpResult = await gatherMcpConfig(mcpConfigs);
286
+ selectedMcps = mcpResult.selectedMcps;
287
+ mcpKeys = mcpResult.mcpKeys;
288
+
289
+ const finalSummary = { ...summary, mcps: selectedMcps.map((m) => ({ id: m.id, tier: m.tier })) };
290
+ const proceed = await confirmInstallation(finalSummary);
291
+ if (!proceed) {
292
+ console.log();
293
+ logger.info('Cancelled.');
294
+ process.exit(0);
295
+ }
296
+ }
297
+
298
+ // Cache analysis and profile
299
+ try {
300
+ writeAnalysisCache(targetDir, syntheticProjectInfo, syntheticDetected, null);
301
+ writeUserProfile(targetDir, userProfile);
302
+ } catch (cacheErr) {
303
+ logger.debug(`Analysis cache write failed: ${cacheErr.message}`);
304
+ }
305
+
306
+ // Write files
307
+ let results;
308
+ let filesToWrite;
309
+ let selectedCandidateIds = null;
310
+
311
+ try {
312
+ const installCtx = await runInstallTasks({
313
+ apiResponse,
314
+ selectedCandidateIds: null,
315
+ targetDir,
316
+ selectedMcps,
317
+ mcpKeys,
318
+ securityConfig,
319
+ detected: syntheticDetected,
320
+ buildFileList,
321
+ writeApiFiles,
322
+ scoreWithClaude,
323
+ });
324
+
325
+ results = installCtx.results;
326
+ filesToWrite = installCtx.filesToWrite;
327
+ selectedCandidateIds = installCtx.selectedCandidateIds ?? null;
328
+ } catch {
329
+ // Fallback to direct write
330
+ const spinnerWrite = ora('Writing configuration files...').start();
331
+ filesToWrite = buildFileList(apiResponse, null);
332
+ results = await writeApiFiles(filesToWrite, targetDir, {
333
+ force: true,
334
+ selectedMcpIds: selectedMcps.map((m) => m.id),
335
+ mcpKeys,
336
+ securityConfig,
337
+ detected: syntheticDetected,
338
+ });
339
+ spinnerWrite.succeed('Configuration generated.');
340
+ }
341
+
342
+ // Update manifest
343
+ try {
344
+ if (filesToWrite) updateManifest(targetDir, results, filesToWrite);
345
+ } catch (cacheErr) {
346
+ logger.debug(`Manifest update failed: ${cacheErr.message}`);
347
+ }
348
+
349
+ // Display file results
350
+ renderFileResults(results);
351
+
352
+ // MCP verification
353
+ let mcpResults = [];
354
+ if (selectedMcps.length > 0) {
355
+ console.log();
356
+ try {
357
+ const verifyCtx = await runVerifyTasks(selectedMcps, mcpKeys, { setupMcps, targetDir });
358
+ mcpResults = verifyCtx.mcpResults;
359
+ } catch {
360
+ const spinnerMcp = ora('Verifying MCP servers...').start();
361
+ mcpResults = await setupMcps(selectedMcps, mcpKeys, {
362
+ healthCheck: true,
363
+ targetDir,
364
+ onStatus: (id, status) => {
365
+ if (status === 'verifying') spinnerMcp.text = `Verifying ${id}...`;
366
+ else if (status === 'testing') spinnerMcp.text = `Health-checking ${id}...`;
367
+ },
368
+ });
369
+ spinnerMcp.succeed('MCP verification complete.');
370
+ }
371
+
372
+ renderMcpStatus(mcpResults);
373
+ }
374
+
375
+ // ================================================================
376
+ // STEP 4: Bootstrap
377
+ // ================================================================
378
+ renderCreatePhase(4, 'Bootstrap', TOTAL_PHASES);
379
+
380
+ console.log(chalk.dim(' Handing off to Claude to scaffold your project...'));
381
+ console.log(chalk.dim(' This may take several minutes. Activity log:'));
382
+ console.log();
383
+
384
+ let bootstrapSucceeded = true;
385
+ try {
386
+ await runBootstrap(targetDir, description);
387
+ } catch (err) {
388
+ bootstrapSucceeded = false;
389
+ console.log();
390
+ logger.warn('Bootstrap did not complete: ' + err.message);
391
+ logger.info('Your .claude/ configuration is still intact. You can run /bootstrap:auto manually inside the project.');
392
+ console.log();
393
+ }
394
+
395
+ // ================================================================
396
+ // STEP 5: Post-Bootstrap Sync
397
+ // ================================================================
398
+ renderCreatePhase(5, 'Finalization', TOTAL_PHASES);
399
+
400
+ if (bootstrapSucceeded) {
401
+ // Re-analyze the now-real project
402
+ try {
403
+ const spinner5 = ora('Re-analyzing project...').start();
404
+ const fsDetected = await detectProject(targetDir);
405
+
406
+ let projectInfo;
407
+ try {
408
+ const { analysis } = await analyzeWithClaude(targetDir);
409
+ if (analysis) {
410
+ projectInfo = {
411
+ name: analysis.name || fsDetected.name || name,
412
+ description: analysis.description || fsDetected.description || description,
413
+ projectType: analysis.projectType || fsDetected.projectType || projectType,
414
+ languages: analysis.languages?.length ? analysis.languages : fsDetected.languages,
415
+ frameworks: analysis.frameworks?.length ? analysis.frameworks : fsDetected.frameworks,
416
+ codeStyle: analysis.codeStyle?.length ? analysis.codeStyle : fsDetected.codeStyle,
417
+ cicd: analysis.cicd?.length ? analysis.cicd : fsDetected.cicd,
418
+ subprojects: analysis.subprojects?.length ? analysis.subprojects : fsDetected.subprojects,
419
+ architecture: analysis.architecture || '',
420
+ buildCommands: analysis.buildCommands || {},
421
+ complexity: analysis.complexity ?? 0.5,
422
+ metrics: analysis.metrics || null,
423
+ entryPoints: analysis.entryPoints || [],
424
+ coreModules: analysis.coreModules || [],
425
+ testFramework: analysis.testFramework || '',
426
+ packageManager: analysis.packageManager || fsDetected.packageManager || '',
427
+ languageDistribution: analysis.languageDistribution || fsDetected.languageDistribution || null,
428
+ };
429
+ } else {
430
+ projectInfo = buildProjectInfoFromFs(fsDetected, name, description, projectType);
431
+ }
432
+ } catch {
433
+ projectInfo = buildProjectInfoFromFs(fsDetected, name, description, projectType);
434
+ }
435
+
436
+ spinner5.succeed('Project re-analyzed.');
437
+
438
+ // Overwrite cache with real data
439
+ const detected = { ...fsDetected, ...projectInfo };
440
+ writeAnalysisCache(targetDir, projectInfo, detected, null);
441
+ writeUserProfile(targetDir, userProfile);
442
+ } catch (err) {
443
+ logger.debug(`Post-bootstrap analysis failed: ${err.message}`);
444
+ }
445
+ }
446
+
447
+ // Finalize — optimize settings + rewrite CLAUDE.md
448
+ try {
449
+ await runFinalizeTasks({
450
+ targetDir,
451
+ readAnalysisCache,
452
+ optimizeSettings,
453
+ rewriteClaudeMd,
454
+ });
455
+ } catch {
456
+ // Fallback to sequential
457
+ const spinnerOpt = ora('Optimizing settings...').start();
458
+ const optResult = optimizeSettings(targetDir);
459
+ if (optResult.status === 'ok' && optResult.applied > 0) {
460
+ spinnerOpt.succeed(`Optimized ${optResult.applied} setting(s).`);
461
+ } else {
462
+ spinnerOpt.succeed('Settings reviewed \u2014 no changes needed.');
463
+ }
464
+
465
+ const spinnerMd = ora('Rewriting CLAUDE.md...').start();
466
+ const cache = readAnalysisCache(targetDir);
467
+ const rewritten = await rewriteClaudeMd(targetDir, cache);
468
+ if (rewritten) {
469
+ spinnerMd.succeed('CLAUDE.md rewritten.');
470
+ } else {
471
+ spinnerMd.warn('CLAUDE.md rewrite skipped \u2014 using template version.');
472
+ }
473
+ }
474
+
475
+ // ── Success ──────────────────────────────────────────────────────
476
+ const totalItems = countTotalItems(summary, selectedCandidateIds);
477
+ const mcpsNeedingKeys = mcpResults.filter((r) => r.status === 'needs-key');
478
+
479
+ renderSuccessCard({
480
+ totalItems,
481
+ mcpCount: selectedMcps.length,
482
+ mcpsNeedingKeys,
483
+ });
484
+
485
+ console.log();
486
+ if (bootstrapSucceeded) {
487
+ logger.success(`Project ${chalk.bold(name)} created and bootstrapped!`);
488
+ console.log(chalk.dim(` cd ${name} && claude`));
489
+ } else {
490
+ logger.success(`Project ${chalk.bold(name)} created with Claude configuration.`);
491
+ console.log(chalk.dim(` cd ${name} && claude -p "/bootstrap:auto ${description}"`));
492
+ }
493
+ console.log();
494
+ } catch (err) {
495
+ if (
496
+ err &&
497
+ (err.name === 'ExitPromptError' ||
498
+ err.constructor?.name === 'ExitPromptError' ||
499
+ err.message?.includes('User force closed'))
500
+ ) {
501
+ console.log();
502
+ logger.info('Cancelled.');
503
+ process.exit(0);
504
+ }
505
+
506
+ console.log();
507
+ logger.error(err.message || String(err));
508
+ process.exit(1);
509
+ } finally {
510
+ if (targetDir) {
511
+ try {
512
+ promoteCache(targetDir);
513
+ cleanupAnalysisCache(targetDir);
514
+ } catch {
515
+ // Ignore cleanup errors
516
+ }
517
+ }
518
+ }
519
+ }
520
+
521
+ // ── Helper: build projectInfo from filesystem detection only ─────────────────
522
+
523
+ function buildProjectInfoFromFs(fsDetected, name, description, projectType) {
524
+ return {
525
+ name: fsDetected.name || name,
526
+ description: fsDetected.description || description,
527
+ projectType: fsDetected.projectType || projectType,
528
+ languages: fsDetected.languages?.length ? fsDetected.languages : [],
529
+ frameworks: fsDetected.frameworks || [],
530
+ codeStyle: fsDetected.codeStyle || [],
531
+ cicd: fsDetected.cicd || [],
532
+ subprojects: fsDetected.subprojects || [],
533
+ architecture: '',
534
+ buildCommands: {},
535
+ complexity: 0.5,
536
+ metrics: null,
537
+ entryPoints: [],
538
+ coreModules: [],
539
+ testFramework: '',
540
+ packageManager: fsDetected.packageManager || '',
541
+ languageDistribution: fsDetected.languageDistribution || null,
542
+ };
543
+ }