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.
- package/bin/claude-craft.js +85 -0
- package/package.json +39 -0
- package/src/commands/auth.js +43 -0
- package/src/commands/create.js +543 -0
- package/src/commands/install.js +480 -0
- package/src/commands/logout.js +24 -0
- package/src/commands/update.js +339 -0
- package/src/constants.js +299 -0
- package/src/generators/directories.js +30 -0
- package/src/generators/metadata.js +57 -0
- package/src/generators/security.js +39 -0
- package/src/prompts/gather.js +308 -0
- package/src/ui/brand.js +62 -0
- package/src/ui/cards.js +179 -0
- package/src/ui/format.js +55 -0
- package/src/ui/phase-header.js +20 -0
- package/src/ui/prompts.js +56 -0
- package/src/ui/tables.js +89 -0
- package/src/ui/tasks.js +258 -0
- package/src/ui/theme.js +83 -0
- package/src/utils/analysis-cache.js +519 -0
- package/src/utils/api-client.js +253 -0
- package/src/utils/api-file-writer.js +197 -0
- package/src/utils/bootstrap-runner.js +148 -0
- package/src/utils/claude-analyzer.js +255 -0
- package/src/utils/claude-optimizer.js +341 -0
- package/src/utils/claude-rewriter.js +553 -0
- package/src/utils/claude-scorer.js +101 -0
- package/src/utils/description-analyzer.js +116 -0
- package/src/utils/detect-project.js +1276 -0
- package/src/utils/existing-setup.js +341 -0
- package/src/utils/file-writer.js +64 -0
- package/src/utils/json-extract.js +56 -0
- package/src/utils/logger.js +27 -0
- package/src/utils/mcp-setup.js +461 -0
- package/src/utils/preflight.js +112 -0
- package/src/utils/prompt-api-key.js +59 -0
- package/src/utils/run-claude.js +152 -0
- package/src/utils/security.js +82 -0
- package/src/utils/toolkit-rule-generator.js +364 -0
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analysis cache — bridges data between install steps 2/5 and steps 7/8.
|
|
3
|
+
*
|
|
4
|
+
* Persists project analysis and installed file manifest to a temp directory
|
|
5
|
+
* so that steps 7 (optimization) and 8 (CLAUDE.md rewrite) can access
|
|
6
|
+
* pre-computed context without re-analyzing or re-reading the project.
|
|
7
|
+
*
|
|
8
|
+
* Temp directory: .claude/.claude-craft-temp/ — cleaned up after install.
|
|
9
|
+
* Permanent directory: .claude/.claude-craft/ — kept as project overview data.
|
|
10
|
+
*/
|
|
11
|
+
import { join, basename } from 'path';
|
|
12
|
+
import { mkdirSync, writeFileSync, readFileSync, rmSync, existsSync, copyFileSync } from 'fs';
|
|
13
|
+
import * as logger from './logger.js';
|
|
14
|
+
|
|
15
|
+
const CACHE_DIR = '.claude/.claude-craft-temp';
|
|
16
|
+
const PERMANENT_DIR = '.claude/.claude-craft';
|
|
17
|
+
|
|
18
|
+
/** Files promoted from temp → permanent after install. */
|
|
19
|
+
const PERMANENT_FILES = ['project-analysis.json', 'project-context.md', 'installed-manifest.json', 'user-profile.json'];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the temp cache directory path for a given target directory.
|
|
23
|
+
*/
|
|
24
|
+
function cachePath(targetDir) {
|
|
25
|
+
return join(targetDir, CACHE_DIR);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the permanent data directory path for a given target directory.
|
|
30
|
+
*/
|
|
31
|
+
function permanentPath(targetDir) {
|
|
32
|
+
return join(targetDir, PERMANENT_DIR);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Write analysis data to temp cache directory.
|
|
37
|
+
* Called after step 2 completes.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} targetDir - Project root
|
|
40
|
+
* @param {object} projectInfo - Merged analysis from Claude + filesystem detection
|
|
41
|
+
* @param {object} detected - Raw filesystem detection result
|
|
42
|
+
* @param {object|null} existingContext - Context from previous Claude setup (optional)
|
|
43
|
+
*/
|
|
44
|
+
export function writeAnalysisCache(targetDir, projectInfo, detected, existingContext = null) {
|
|
45
|
+
const dir = cachePath(targetDir);
|
|
46
|
+
mkdirSync(dir, { recursive: true });
|
|
47
|
+
|
|
48
|
+
// Full analysis JSON
|
|
49
|
+
writeFileSync(
|
|
50
|
+
join(dir, 'project-analysis.json'),
|
|
51
|
+
JSON.stringify(projectInfo, null, 2),
|
|
52
|
+
'utf8',
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Rich human-readable context for embedding in prompts
|
|
56
|
+
let contextMd = formatRichProjectContext(projectInfo, detected);
|
|
57
|
+
|
|
58
|
+
// Append prior setup context if available
|
|
59
|
+
if (existingContext?.summary) {
|
|
60
|
+
const priorLines = ['\n### Prior Installation Context'];
|
|
61
|
+
if (existingContext.summary.projectContext) {
|
|
62
|
+
priorLines.push(existingContext.summary.projectContext);
|
|
63
|
+
}
|
|
64
|
+
if (existingContext.summary.preserveNotes) {
|
|
65
|
+
priorLines.push(`Note: ${existingContext.summary.preserveNotes}`);
|
|
66
|
+
}
|
|
67
|
+
if (existingContext.summary.customizations?.length) {
|
|
68
|
+
priorLines.push(`Customizations: ${existingContext.summary.customizations.join('; ')}`);
|
|
69
|
+
}
|
|
70
|
+
contextMd += '\n' + priorLines.join('\n');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
writeFileSync(join(dir, 'project-context.md'), contextMd, 'utf8');
|
|
74
|
+
|
|
75
|
+
// Store previous CLAUDE.md content for the rewriter to reference
|
|
76
|
+
if (existingContext?.claudeMdContent) {
|
|
77
|
+
writeFileSync(
|
|
78
|
+
join(dir, 'previous-claude-md.md'),
|
|
79
|
+
existingContext.claudeMdContent,
|
|
80
|
+
'utf8',
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
logger.debug('Analysis cache written to ' + CACHE_DIR);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Update the installed manifest after step 5.
|
|
89
|
+
* Stores file paths, statuses, categories, and first-line summaries
|
|
90
|
+
* so steps 7/8 know exactly what was written without re-reading.
|
|
91
|
+
*
|
|
92
|
+
* @param {string} targetDir - Project root
|
|
93
|
+
* @param {Array<{path: string, status: string}>} results - Step 5 write results
|
|
94
|
+
* @param {Array<{relativePath: string, content: string, type: string}>} files - Files from buildFileList
|
|
95
|
+
*/
|
|
96
|
+
export function updateManifest(targetDir, results, files) {
|
|
97
|
+
const dir = cachePath(targetDir);
|
|
98
|
+
if (!existsSync(dir)) {
|
|
99
|
+
mkdirSync(dir, { recursive: true });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Build a content lookup from the files array
|
|
103
|
+
const contentMap = new Map();
|
|
104
|
+
for (const f of files) {
|
|
105
|
+
if (f.relativePath && f.content) {
|
|
106
|
+
contentMap.set(f.relativePath, f.content);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const counts = {
|
|
111
|
+
agents: 0,
|
|
112
|
+
skills: 0,
|
|
113
|
+
rules: 0,
|
|
114
|
+
commands: 0,
|
|
115
|
+
workflows: 0,
|
|
116
|
+
hooks: 0,
|
|
117
|
+
mcps: 0,
|
|
118
|
+
other: 0,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const manifestFiles = results.map((r) => {
|
|
122
|
+
const rel = r.path;
|
|
123
|
+
const category = categorizeByPath(rel);
|
|
124
|
+
counts[category] = (counts[category] || 0) + 1;
|
|
125
|
+
|
|
126
|
+
const content = contentMap.get(rel) || '';
|
|
127
|
+
const firstLine = extractFirstLine(content);
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
relativePath: rel,
|
|
131
|
+
status: r.status || 'created',
|
|
132
|
+
category,
|
|
133
|
+
firstLine,
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const manifest = {
|
|
138
|
+
writtenAt: new Date().toISOString(),
|
|
139
|
+
files: manifestFiles,
|
|
140
|
+
counts,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
writeFileSync(
|
|
144
|
+
join(dir, 'installed-manifest.json'),
|
|
145
|
+
JSON.stringify(manifest, null, 2),
|
|
146
|
+
'utf8',
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
logger.debug('Installed manifest updated with ' + manifestFiles.length + ' files.');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Read all cached analysis data.
|
|
154
|
+
* Returns null if cache is missing or corrupted (triggers legacy fallback).
|
|
155
|
+
*
|
|
156
|
+
* @param {string} targetDir - Project root
|
|
157
|
+
* @returns {{ projectInfo: object, projectContext: string, manifest: object|null } | null}
|
|
158
|
+
*/
|
|
159
|
+
export function readAnalysisCache(targetDir) {
|
|
160
|
+
const dir = cachePath(targetDir);
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const analysisPath = join(dir, 'project-analysis.json');
|
|
164
|
+
const contextPath = join(dir, 'project-context.md');
|
|
165
|
+
|
|
166
|
+
if (!existsSync(analysisPath) || !existsSync(contextPath)) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const projectInfo = JSON.parse(readFileSync(analysisPath, 'utf8'));
|
|
171
|
+
const projectContext = readFileSync(contextPath, 'utf8');
|
|
172
|
+
|
|
173
|
+
// Manifest may not exist yet (only written after step 5)
|
|
174
|
+
let manifest = null;
|
|
175
|
+
const manifestPath = join(dir, 'installed-manifest.json');
|
|
176
|
+
if (existsSync(manifestPath)) {
|
|
177
|
+
manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { projectInfo, projectContext, manifest };
|
|
181
|
+
} catch (err) {
|
|
182
|
+
logger.debug(`Failed to read analysis cache: ${err.message}`);
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Format project info into a rich human-readable context string.
|
|
189
|
+
* Used for embedding directly into Claude prompts to avoid tool calls.
|
|
190
|
+
*
|
|
191
|
+
* @param {object} projectInfo - Full project analysis object
|
|
192
|
+
* @param {object} detected - Filesystem detection result
|
|
193
|
+
* @returns {string} Formatted markdown context block
|
|
194
|
+
*/
|
|
195
|
+
export function formatRichProjectContext(projectInfo, detected) {
|
|
196
|
+
const lines = [];
|
|
197
|
+
|
|
198
|
+
// Header
|
|
199
|
+
lines.push(`## Project: ${projectInfo.name || '(unnamed)'}`);
|
|
200
|
+
if (projectInfo.description) {
|
|
201
|
+
lines.push(projectInfo.description);
|
|
202
|
+
}
|
|
203
|
+
lines.push('');
|
|
204
|
+
|
|
205
|
+
// Tech stack
|
|
206
|
+
lines.push('### Tech Stack');
|
|
207
|
+
if (projectInfo.languageDistribution) {
|
|
208
|
+
const distStr = Object.entries(projectInfo.languageDistribution)
|
|
209
|
+
.sort(([, a], [, b]) => b - a)
|
|
210
|
+
.map(([lang, pct]) => `${lang} (${pct}%)`)
|
|
211
|
+
.join(', ');
|
|
212
|
+
lines.push(`- Languages: ${distStr}`);
|
|
213
|
+
} else if (projectInfo.languages?.length) {
|
|
214
|
+
lines.push(`- Languages: ${projectInfo.languages.join(', ')}`);
|
|
215
|
+
}
|
|
216
|
+
if (projectInfo.frameworks?.length) {
|
|
217
|
+
lines.push(`- Frameworks: ${projectInfo.frameworks.join(', ')}`);
|
|
218
|
+
}
|
|
219
|
+
lines.push(`- Type: ${projectInfo.projectType || 'monolith'}`);
|
|
220
|
+
if (projectInfo.architecture) {
|
|
221
|
+
lines.push(`- Architecture: ${projectInfo.architecture}`);
|
|
222
|
+
}
|
|
223
|
+
const cplx = projectInfo.complexity ?? 0.5;
|
|
224
|
+
const cplxLabel = cplx >= 0.7 ? 'complex' : cplx >= 0.4 ? 'moderate' : 'simple';
|
|
225
|
+
lines.push(`- Complexity: ${cplx.toFixed(2)} (${cplxLabel})`);
|
|
226
|
+
if (projectInfo.packageManager) {
|
|
227
|
+
lines.push(`- Package manager: ${projectInfo.packageManager}`);
|
|
228
|
+
}
|
|
229
|
+
if (projectInfo.testFramework) {
|
|
230
|
+
lines.push(`- Test framework: ${projectInfo.testFramework}`);
|
|
231
|
+
}
|
|
232
|
+
if (projectInfo.codeStyle?.length) {
|
|
233
|
+
lines.push(`- Code style: ${projectInfo.codeStyle.join(', ')}`);
|
|
234
|
+
}
|
|
235
|
+
if (projectInfo.cicd?.length) {
|
|
236
|
+
lines.push(`- CI/CD: ${projectInfo.cicd.join(', ')}`);
|
|
237
|
+
}
|
|
238
|
+
lines.push('');
|
|
239
|
+
|
|
240
|
+
// Build commands
|
|
241
|
+
if (projectInfo.buildCommands) {
|
|
242
|
+
const cmds = Object.entries(projectInfo.buildCommands).filter(([, v]) => v);
|
|
243
|
+
if (cmds.length > 0) {
|
|
244
|
+
lines.push('### Build Commands');
|
|
245
|
+
for (const [key, val] of cmds) {
|
|
246
|
+
lines.push(`- ${key}: \`${val}\``);
|
|
247
|
+
}
|
|
248
|
+
lines.push('');
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Metrics
|
|
253
|
+
if (projectInfo.metrics) {
|
|
254
|
+
const m = projectInfo.metrics;
|
|
255
|
+
const parts = [];
|
|
256
|
+
if (m.totalFiles) parts.push(`${m.totalFiles} files`);
|
|
257
|
+
if (m.totalDirs) parts.push(`${m.totalDirs} directories`);
|
|
258
|
+
if (m.maxDepth) parts.push(`max depth ${m.maxDepth}`);
|
|
259
|
+
if (m.dependencyCount) parts.push(`${m.dependencyCount} dependencies`);
|
|
260
|
+
if (m.testFileCount) parts.push(`${m.testFileCount} test files`);
|
|
261
|
+
if (m.sourceFileCount) parts.push(`${m.sourceFileCount} source files`);
|
|
262
|
+
if (m.estimatedTestCoverage && m.estimatedTestCoverage !== 'unknown') {
|
|
263
|
+
parts.push(`~${m.estimatedTestCoverage} coverage`);
|
|
264
|
+
}
|
|
265
|
+
if (parts.length > 0) {
|
|
266
|
+
lines.push('### Metrics');
|
|
267
|
+
lines.push(`- ${parts.join(', ')}`);
|
|
268
|
+
lines.push('');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Entry points
|
|
273
|
+
if (projectInfo.entryPoints?.length) {
|
|
274
|
+
lines.push('### Entry Points');
|
|
275
|
+
for (const ep of projectInfo.entryPoints) {
|
|
276
|
+
const cmd = ep.command ? ` (\`${ep.command}\`)` : '';
|
|
277
|
+
lines.push(`- ${ep.type}: ${ep.path}${cmd}`);
|
|
278
|
+
}
|
|
279
|
+
lines.push('');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Core modules
|
|
283
|
+
if (projectInfo.coreModules?.length) {
|
|
284
|
+
lines.push('### Core Modules');
|
|
285
|
+
for (const mod of projectInfo.coreModules) {
|
|
286
|
+
lines.push(`- ${mod.path} — ${mod.purpose}`);
|
|
287
|
+
}
|
|
288
|
+
lines.push('');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Subprojects
|
|
292
|
+
if (projectInfo.subprojects?.length) {
|
|
293
|
+
lines.push('### Subprojects');
|
|
294
|
+
for (const sub of projectInfo.subprojects) {
|
|
295
|
+
const fws = sub.frameworks?.length ? ` (${sub.frameworks.join(', ')})` : '';
|
|
296
|
+
lines.push(`- ${sub.path}: ${sub.languages?.join(', ') || 'unknown'}${fws}`);
|
|
297
|
+
}
|
|
298
|
+
lines.push('');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Sensitive files (from detected)
|
|
302
|
+
if (detected?.sensitiveFiles?.length) {
|
|
303
|
+
lines.push('### Sensitive Files Detected');
|
|
304
|
+
const capped = detected.sensitiveFiles.slice(0, 10);
|
|
305
|
+
for (const sf of capped) {
|
|
306
|
+
const covered = sf.gitignored ? ' (gitignored)' : ' (NOT gitignored)';
|
|
307
|
+
lines.push(`- ${sf.file}${covered}`);
|
|
308
|
+
}
|
|
309
|
+
if (detected.sensitiveFiles.length > 10) {
|
|
310
|
+
lines.push(`- ... and ${detected.sensitiveFiles.length - 10} more`);
|
|
311
|
+
}
|
|
312
|
+
lines.push('');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return lines.join('\n').trimEnd();
|
|
316
|
+
}
|
|
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 - { role, 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
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Read the permanent project analysis JSON.
|
|
352
|
+
* Returns null if not found.
|
|
353
|
+
*
|
|
354
|
+
* @param {string} targetDir - Project root
|
|
355
|
+
* @returns {object|null}
|
|
356
|
+
*/
|
|
357
|
+
export function readPermanentAnalysis(targetDir) {
|
|
358
|
+
const filePath = join(permanentPath(targetDir), 'project-analysis.json');
|
|
359
|
+
try {
|
|
360
|
+
if (!existsSync(filePath)) return null;
|
|
361
|
+
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
362
|
+
} catch (err) {
|
|
363
|
+
logger.debug(`Failed to read permanent analysis: ${err.message}`);
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Read the permanent installed manifest.
|
|
370
|
+
* Returns null if not found.
|
|
371
|
+
*
|
|
372
|
+
* @param {string} targetDir - Project root
|
|
373
|
+
* @returns {object|null}
|
|
374
|
+
*/
|
|
375
|
+
export function readInstalledManifest(targetDir) {
|
|
376
|
+
const filePath = join(permanentPath(targetDir), 'installed-manifest.json');
|
|
377
|
+
try {
|
|
378
|
+
if (!existsSync(filePath)) return null;
|
|
379
|
+
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
380
|
+
} catch (err) {
|
|
381
|
+
logger.debug(`Failed to read installed manifest: ${err.message}`);
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Merge new install results into the permanent manifest without removing existing entries.
|
|
388
|
+
* Used by the update command to record newly installed files.
|
|
389
|
+
*
|
|
390
|
+
* @param {string} targetDir - Project root
|
|
391
|
+
* @param {Array<{path: string, status: string}>} newResults - Newly written file results
|
|
392
|
+
* @param {Array<{relativePath: string, content: string}>} newFiles - Newly written file objects
|
|
393
|
+
*/
|
|
394
|
+
export function mergePermanentManifest(targetDir, newResults, newFiles) {
|
|
395
|
+
const dest = permanentPath(targetDir);
|
|
396
|
+
const manifestPath = join(dest, 'installed-manifest.json');
|
|
397
|
+
|
|
398
|
+
// Build content lookup for first-line extraction
|
|
399
|
+
const contentMap = new Map();
|
|
400
|
+
for (const f of newFiles) {
|
|
401
|
+
if (f.relativePath && f.content) contentMap.set(f.relativePath, f.content);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Read existing manifest or start fresh
|
|
405
|
+
let existing = { writtenAt: new Date().toISOString(), files: [], counts: {} };
|
|
406
|
+
try {
|
|
407
|
+
if (existsSync(manifestPath)) {
|
|
408
|
+
existing = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
409
|
+
}
|
|
410
|
+
} catch {
|
|
411
|
+
// start fresh if corrupted
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Build set of already-recorded paths to avoid duplicates
|
|
415
|
+
const recordedPaths = new Set((existing.files || []).map((f) => f.relativePath));
|
|
416
|
+
|
|
417
|
+
const counts = { ...existing.counts };
|
|
418
|
+
|
|
419
|
+
for (const r of newResults) {
|
|
420
|
+
if (recordedPaths.has(r.path)) continue;
|
|
421
|
+
|
|
422
|
+
const category = categorizeByPath(r.path);
|
|
423
|
+
counts[category] = (counts[category] || 0) + 1;
|
|
424
|
+
|
|
425
|
+
const content = contentMap.get(r.path) || '';
|
|
426
|
+
existing.files.push({
|
|
427
|
+
relativePath: r.path,
|
|
428
|
+
status: r.status || 'created',
|
|
429
|
+
category,
|
|
430
|
+
firstLine: extractFirstLine(content),
|
|
431
|
+
});
|
|
432
|
+
recordedPaths.add(r.path);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
existing.counts = counts;
|
|
436
|
+
existing.updatedAt = new Date().toISOString();
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
mkdirSync(dest, { recursive: true });
|
|
440
|
+
writeFileSync(manifestPath, JSON.stringify(existing, null, 2), 'utf8');
|
|
441
|
+
logger.debug(`Permanent manifest updated with ${newResults.length} new file(s).`);
|
|
442
|
+
} catch (err) {
|
|
443
|
+
logger.debug(`Failed to update permanent manifest: ${err.message}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Promote keeper files from temp cache → permanent .claude/.claude-craft/ directory.
|
|
449
|
+
* Call this before cleanupAnalysisCache() so the data survives the temp wipe.
|
|
450
|
+
*
|
|
451
|
+
* Promoted: project-analysis.json, project-context.md, installed-manifest.json
|
|
452
|
+
* Discarded: previous-claude-md.md (upgrade-specific, not needed after install)
|
|
453
|
+
*
|
|
454
|
+
* @param {string} targetDir - Project root
|
|
455
|
+
*/
|
|
456
|
+
export function promoteCache(targetDir) {
|
|
457
|
+
const src = cachePath(targetDir);
|
|
458
|
+
const dest = permanentPath(targetDir);
|
|
459
|
+
|
|
460
|
+
try {
|
|
461
|
+
mkdirSync(dest, { recursive: true });
|
|
462
|
+
|
|
463
|
+
for (const file of PERMANENT_FILES) {
|
|
464
|
+
const srcFile = join(src, file);
|
|
465
|
+
if (existsSync(srcFile)) {
|
|
466
|
+
copyFileSync(srcFile, join(dest, file));
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
logger.debug(`Project overview data saved to ${PERMANENT_DIR}`);
|
|
471
|
+
} catch (err) {
|
|
472
|
+
logger.debug(`Failed to promote cache to permanent directory: ${err.message}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Remove the temp cache directory.
|
|
478
|
+
* Safe to call even if directory doesn't exist.
|
|
479
|
+
*
|
|
480
|
+
* @param {string} targetDir - Project root
|
|
481
|
+
*/
|
|
482
|
+
export function cleanupAnalysisCache(targetDir) {
|
|
483
|
+
const dir = cachePath(targetDir);
|
|
484
|
+
try {
|
|
485
|
+
rmSync(dir, { recursive: true, force: true });
|
|
486
|
+
logger.debug('Analysis cache cleaned up.');
|
|
487
|
+
} catch {
|
|
488
|
+
// Ignore cleanup errors
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// ── Internal helpers ─────────────────────────────────────────────
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Categorize a file by its path prefix.
|
|
496
|
+
*/
|
|
497
|
+
function categorizeByPath(relativePath) {
|
|
498
|
+
if (relativePath.includes('/agents/') || relativePath.includes('\\agents\\')) return 'agents';
|
|
499
|
+
if (relativePath.includes('/skills/') || relativePath.includes('\\skills\\')) return 'skills';
|
|
500
|
+
if (relativePath.includes('/rules/') || relativePath.includes('\\rules\\')) return 'rules';
|
|
501
|
+
if (relativePath.includes('/commands/') || relativePath.includes('\\commands\\')) return 'commands';
|
|
502
|
+
if (relativePath.includes('/workflows/') || relativePath.includes('\\workflows\\')) return 'workflows';
|
|
503
|
+
if (relativePath.includes('/hooks/') || relativePath.includes('\\hooks\\') || relativePath.endsWith('hooks.json')) return 'hooks';
|
|
504
|
+
if (relativePath.includes('settings.json') && relativePath.includes('mcpServers')) return 'mcps';
|
|
505
|
+
return 'other';
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Extract the first meaningful line from file content.
|
|
510
|
+
* Prefers the first # heading, falls back to first non-empty line.
|
|
511
|
+
*/
|
|
512
|
+
function extractFirstLine(content) {
|
|
513
|
+
if (!content) return '';
|
|
514
|
+
const lines = content.split('\n');
|
|
515
|
+
const heading = lines.find((l) => l.startsWith('# '));
|
|
516
|
+
if (heading) return heading.replace(/^#+\s*/, '').trim();
|
|
517
|
+
const nonEmpty = lines.find((l) => l.trim() && !l.startsWith('<!--'));
|
|
518
|
+
return nonEmpty?.trim().slice(0, 100) || '';
|
|
519
|
+
}
|