@xelth/eck-snapshot 2.2.0 → 4.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/LICENSE +21 -0
- package/README.md +119 -225
- package/index.js +14 -776
- package/package.json +25 -7
- package/setup.json +805 -0
- package/src/cli/cli.js +427 -0
- package/src/cli/commands/askGpt.js +29 -0
- package/src/cli/commands/autoDocs.js +150 -0
- package/src/cli/commands/consilium.js +86 -0
- package/src/cli/commands/createSnapshot.js +601 -0
- package/src/cli/commands/detectProfiles.js +98 -0
- package/src/cli/commands/detectProject.js +112 -0
- package/src/cli/commands/generateProfileGuide.js +91 -0
- package/src/cli/commands/pruneSnapshot.js +106 -0
- package/src/cli/commands/restoreSnapshot.js +173 -0
- package/src/cli/commands/setupGemini.js +149 -0
- package/src/cli/commands/setupGemini.test.js +115 -0
- package/src/cli/commands/trainTokens.js +38 -0
- package/src/config.js +81 -0
- package/src/services/authService.js +20 -0
- package/src/services/claudeCliService.js +621 -0
- package/src/services/claudeCliService.test.js +267 -0
- package/src/services/dispatcherService.js +33 -0
- package/src/services/gptService.js +302 -0
- package/src/services/gptService.test.js +120 -0
- package/src/templates/agent-prompt.template.md +29 -0
- package/src/templates/architect-prompt.template.md +50 -0
- package/src/templates/envScanRequest.md +4 -0
- package/src/templates/gitWorkflow.md +32 -0
- package/src/templates/multiAgent.md +164 -0
- package/src/templates/vectorMode.md +22 -0
- package/src/utils/aiHeader.js +303 -0
- package/src/utils/fileUtils.js +928 -0
- package/src/utils/projectDetector.js +704 -0
- package/src/utils/tokenEstimator.js +198 -0
- package/.ecksnapshot.config.js +0 -35
|
@@ -0,0 +1,928 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execa } from 'execa';
|
|
4
|
+
import ignore from 'ignore';
|
|
5
|
+
import { detectProjectType, getProjectSpecificFiltering } from './projectDetector.js';
|
|
6
|
+
import { executePrompt as askClaude } from '../services/claudeCliService.js';
|
|
7
|
+
import { getProfile, loadSetupConfig } from '../config.js';
|
|
8
|
+
import micromatch from 'micromatch';
|
|
9
|
+
|
|
10
|
+
export function parseSize(sizeStr) {
|
|
11
|
+
const units = { B: 1, KB: 1024, MB: 1024 ** 2, GB: 1024 ** 3 };
|
|
12
|
+
const match = sizeStr.match(/^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB)?$/i);
|
|
13
|
+
if (!match) throw new Error(`Invalid size format: ${sizeStr}`);
|
|
14
|
+
const [, size, unit = 'B'] = match;
|
|
15
|
+
return Math.floor(parseFloat(size) * units[unit.toUpperCase()]);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function formatSize(bytes) {
|
|
19
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
20
|
+
let size = bytes;
|
|
21
|
+
let unitIndex = 0;
|
|
22
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
23
|
+
size /= 1024;
|
|
24
|
+
unitIndex++;
|
|
25
|
+
}
|
|
26
|
+
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function matchesPattern(filePath, patterns) {
|
|
30
|
+
const fileName = path.basename(filePath);
|
|
31
|
+
return patterns.some(pattern => {
|
|
32
|
+
const regexPattern = '^' + pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$';
|
|
33
|
+
try {
|
|
34
|
+
const regex = new RegExp(regexPattern);
|
|
35
|
+
return regex.test(fileName);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.warn(`⚠️ Invalid regex pattern in config: "${pattern}"`);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function checkGitAvailability() {
|
|
44
|
+
try {
|
|
45
|
+
await execa('git', ['--version']);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
throw new Error('Git is not installed or not available in PATH');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function checkGitRepository(repoPath) {
|
|
52
|
+
try {
|
|
53
|
+
await execa('git', ['rev-parse', '--git-dir'], { cwd: repoPath });
|
|
54
|
+
return true;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function scanDirectoryRecursively(dirPath, config, relativeTo = dirPath, projectType = null) {
|
|
61
|
+
const files = [];
|
|
62
|
+
|
|
63
|
+
// Get project-specific filtering if not provided
|
|
64
|
+
if (!projectType) {
|
|
65
|
+
const detection = await detectProjectType(relativeTo);
|
|
66
|
+
projectType = detection.type;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const projectSpecific = await getProjectSpecificFiltering(projectType);
|
|
70
|
+
|
|
71
|
+
// Merge project-specific filters with global config
|
|
72
|
+
const effectiveConfig = {
|
|
73
|
+
...config,
|
|
74
|
+
dirsToIgnore: [...(config.dirsToIgnore || []), ...(projectSpecific.dirsToIgnore || [])],
|
|
75
|
+
filesToIgnore: [...(config.filesToIgnore || []), ...(projectSpecific.filesToIgnore || [])],
|
|
76
|
+
extensionsToIgnore: [...(config.extensionsToIgnore || []), ...(projectSpecific.extensionsToIgnore || [])]
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
81
|
+
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
84
|
+
const relativePath = path.relative(relativeTo, fullPath).replace(/\\/g, '/');
|
|
85
|
+
|
|
86
|
+
if (effectiveConfig.dirsToIgnore.some(dir =>
|
|
87
|
+
entry.name === dir.replace('/', '') ||
|
|
88
|
+
relativePath.startsWith(dir)
|
|
89
|
+
)) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!effectiveConfig.includeHidden && entry.name.startsWith('.')) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (entry.isDirectory()) {
|
|
98
|
+
const subFiles = await scanDirectoryRecursively(fullPath, effectiveConfig, relativeTo, projectType);
|
|
99
|
+
files.push(...subFiles);
|
|
100
|
+
} else {
|
|
101
|
+
if (effectiveConfig.extensionsToIgnore.includes(path.extname(entry.name)) ||
|
|
102
|
+
matchesPattern(relativePath, effectiveConfig.filesToIgnore)) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
files.push(relativePath);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.warn(`⚠️ Warning: Could not read directory: ${dirPath} - ${error.message}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return files;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function loadGitignore(repoPath) {
|
|
117
|
+
try {
|
|
118
|
+
const gitignoreContent = await fs.readFile(path.join(repoPath, '.gitignore'), 'utf-8');
|
|
119
|
+
const ig = ignore().add(gitignoreContent);
|
|
120
|
+
console.log('✅ .gitignore patterns loaded');
|
|
121
|
+
return ig;
|
|
122
|
+
} catch {
|
|
123
|
+
console.log('ℹ️ No .gitignore file found or could not be read');
|
|
124
|
+
return ignore();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function readFileWithSizeCheck(filePath, maxFileSize) {
|
|
129
|
+
try {
|
|
130
|
+
const stats = await fs.stat(filePath);
|
|
131
|
+
if (stats.size > maxFileSize) {
|
|
132
|
+
throw new Error(`File too large: ${formatSize(stats.size)}`);
|
|
133
|
+
}
|
|
134
|
+
return await fs.readFile(filePath, 'utf-8');
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (error.message.includes('too large')) throw error;
|
|
137
|
+
throw new Error(`Could not read file: ${error.message}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function generateDirectoryTree(dir, prefix = '', allFiles, depth = 0, maxDepth = 10, config) {
|
|
142
|
+
if (depth > maxDepth) return '';
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
146
|
+
const sortedEntries = entries.sort((a, b) => {
|
|
147
|
+
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
148
|
+
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
149
|
+
return a.name.localeCompare(b.name);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
let tree = '';
|
|
153
|
+
const validEntries = [];
|
|
154
|
+
|
|
155
|
+
for (const entry of sortedEntries) {
|
|
156
|
+
if (config.dirsToIgnore.some(d => entry.name.includes(d.replace('/', '')))) continue;
|
|
157
|
+
const fullPath = path.join(dir, entry.name);
|
|
158
|
+
const relativePath = path.relative(process.cwd(), fullPath).replace(/\\/g, '/');
|
|
159
|
+
if (entry.isDirectory() || allFiles.includes(relativePath)) {
|
|
160
|
+
validEntries.push({ entry, fullPath, relativePath });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (let i = 0; i < validEntries.length; i++) {
|
|
165
|
+
const { entry, fullPath, relativePath } = validEntries[i];
|
|
166
|
+
const isLast = i === validEntries.length - 1;
|
|
167
|
+
|
|
168
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
169
|
+
const nextPrefix = prefix + (isLast ? ' ' : '│ ');
|
|
170
|
+
|
|
171
|
+
if (entry.isDirectory()) {
|
|
172
|
+
tree += `${prefix}${connector}${entry.name}/\n`;
|
|
173
|
+
tree += await generateDirectoryTree(fullPath, nextPrefix, allFiles, depth + 1, maxDepth, config);
|
|
174
|
+
} else {
|
|
175
|
+
tree += `${prefix}${connector}${entry.name}\n`;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return tree;
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.warn(`⚠️ Warning: Could not read directory: ${dir}`);
|
|
182
|
+
return '';
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function parseSnapshotContent(content) {
|
|
187
|
+
const files = [];
|
|
188
|
+
const fileRegex = /--- File: \/(.+) ---/g;
|
|
189
|
+
const sections = content.split(fileRegex);
|
|
190
|
+
|
|
191
|
+
for (let i = 1; i < sections.length; i += 2) {
|
|
192
|
+
const filePath = sections[i].trim();
|
|
193
|
+
let fileContent = sections[i + 1] || '';
|
|
194
|
+
|
|
195
|
+
if (fileContent.startsWith('\n\n')) {
|
|
196
|
+
fileContent = fileContent.substring(2);
|
|
197
|
+
}
|
|
198
|
+
if (fileContent.endsWith('\n\n')) {
|
|
199
|
+
fileContent = fileContent.substring(0, fileContent.length - 2);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
files.push({ path: filePath, content: fileContent });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return files;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function filterFilesToRestore(files, options) {
|
|
209
|
+
let filtered = files;
|
|
210
|
+
|
|
211
|
+
if (options.include) {
|
|
212
|
+
const includePatterns = Array.isArray(options.include) ?
|
|
213
|
+
options.include : [options.include];
|
|
214
|
+
filtered = filtered.filter(file =>
|
|
215
|
+
includePatterns.some(pattern => {
|
|
216
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
217
|
+
return regex.test(file.path);
|
|
218
|
+
})
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (options.exclude) {
|
|
223
|
+
const excludePatterns = Array.isArray(options.exclude) ?
|
|
224
|
+
options.exclude : [options.exclude];
|
|
225
|
+
filtered = filtered.filter(file =>
|
|
226
|
+
!excludePatterns.some(pattern => {
|
|
227
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
228
|
+
return regex.test(file.path);
|
|
229
|
+
})
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return filtered;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function validateFilePaths(files, targetDir) {
|
|
237
|
+
const invalidFiles = [];
|
|
238
|
+
|
|
239
|
+
for (const file of files) {
|
|
240
|
+
const normalizedPath = path.normalize(file.path);
|
|
241
|
+
if (normalizedPath.includes('..') ||
|
|
242
|
+
normalizedPath.startsWith('/') ||
|
|
243
|
+
normalizedPath.includes('\0') ||
|
|
244
|
+
/[<>:"|?*]/.test(normalizedPath)) {
|
|
245
|
+
invalidFiles.push(file.path);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return invalidFiles;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export async function loadConfig(configPath) {
|
|
253
|
+
const { DEFAULT_CONFIG } = await import('../config.js');
|
|
254
|
+
let config = { ...DEFAULT_CONFIG };
|
|
255
|
+
|
|
256
|
+
if (configPath) {
|
|
257
|
+
try {
|
|
258
|
+
const configModule = await import(path.resolve(configPath));
|
|
259
|
+
config = { ...config, ...configModule.default };
|
|
260
|
+
console.log(`✅ Configuration loaded from: ${configPath}`);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.warn(`⚠️ Warning: Could not load config file: ${configPath}`);
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
const possibleConfigs = [
|
|
266
|
+
'.ecksnapshot.config.js',
|
|
267
|
+
'.ecksnapshot.config.mjs',
|
|
268
|
+
'ecksnapshot.config.js'
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
for (const configFile of possibleConfigs) {
|
|
272
|
+
try {
|
|
273
|
+
await fs.access(configFile);
|
|
274
|
+
const configModule = await import(path.resolve(configFile));
|
|
275
|
+
config = { ...config, ...configModule.default };
|
|
276
|
+
console.log(`✅ Configuration loaded from: ${configFile}`);
|
|
277
|
+
break;
|
|
278
|
+
} catch {
|
|
279
|
+
// Config file doesn't exist, continue
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return config;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function generateTimestamp() {
|
|
288
|
+
const now = new Date();
|
|
289
|
+
const YYYY = now.getFullYear();
|
|
290
|
+
const MM = String(now.getMonth() + 1).padStart(2, '0');
|
|
291
|
+
const DD = String(now.getDate()).padStart(2, '0');
|
|
292
|
+
const hh = String(now.getHours()).padStart(2, '0');
|
|
293
|
+
const mm = String(now.getMinutes()).padStart(2, '0');
|
|
294
|
+
const ss = String(now.getSeconds()).padStart(2, '0');
|
|
295
|
+
return `${YYYY}-${MM}-${DD}_${hh}-${mm}-${ss}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function sanitizeForFilename(text) {
|
|
299
|
+
return text
|
|
300
|
+
.toLowerCase()
|
|
301
|
+
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
|
302
|
+
.replace(/[^a-z0-9-]/g, '') // Remove invalid characters
|
|
303
|
+
.substring(0, 50); // Truncate to a reasonable length
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Displays project detection information in a user-friendly format
|
|
308
|
+
* @param {object} detection - Project detection result
|
|
309
|
+
*/
|
|
310
|
+
export function displayProjectInfo(detection) {
|
|
311
|
+
console.log('\n🔍 Project Detection Results:');
|
|
312
|
+
console.log(` Type: ${detection.type} (confidence: ${(detection.confidence * 100).toFixed(0)}%)`);
|
|
313
|
+
|
|
314
|
+
if (detection.details) {
|
|
315
|
+
const details = detection.details;
|
|
316
|
+
|
|
317
|
+
switch (detection.type) {
|
|
318
|
+
case 'android':
|
|
319
|
+
console.log(` Language: ${details.language || 'unknown'}`);
|
|
320
|
+
if (details.packageName) {
|
|
321
|
+
console.log(` Package: ${details.packageName}`);
|
|
322
|
+
}
|
|
323
|
+
if (details.sourceDirs && details.sourceDirs.length > 0) {
|
|
324
|
+
console.log(` Source dirs: ${details.sourceDirs.join(', ')}`);
|
|
325
|
+
}
|
|
326
|
+
if (details.libFiles && details.libFiles.length > 0) {
|
|
327
|
+
console.log(` Libraries: ${details.libFiles.length} .aar/.jar files`);
|
|
328
|
+
}
|
|
329
|
+
break;
|
|
330
|
+
|
|
331
|
+
case 'nodejs':
|
|
332
|
+
if (details.name) {
|
|
333
|
+
console.log(` Package: ${details.name}@${details.version || '?'}`);
|
|
334
|
+
}
|
|
335
|
+
if (details.framework) {
|
|
336
|
+
console.log(` Framework: ${details.framework}`);
|
|
337
|
+
}
|
|
338
|
+
if (details.hasTypescript) {
|
|
339
|
+
console.log(` TypeScript: enabled`);
|
|
340
|
+
}
|
|
341
|
+
break;
|
|
342
|
+
|
|
343
|
+
case 'nodejs-monorepo':
|
|
344
|
+
if (details.name) {
|
|
345
|
+
console.log(` Project: ${details.name}@${details.version || '?'}`);
|
|
346
|
+
}
|
|
347
|
+
if (details.monorepoTool) {
|
|
348
|
+
console.log(` Monorepo tool: ${details.monorepoTool}`);
|
|
349
|
+
}
|
|
350
|
+
if (details.workspaceCount) {
|
|
351
|
+
console.log(` Workspaces: ${details.workspaceCount}`);
|
|
352
|
+
}
|
|
353
|
+
if (details.framework) {
|
|
354
|
+
console.log(` Framework: ${details.framework}`);
|
|
355
|
+
}
|
|
356
|
+
break;
|
|
357
|
+
|
|
358
|
+
case 'python-poetry':
|
|
359
|
+
case 'python-pip':
|
|
360
|
+
case 'python-conda':
|
|
361
|
+
if (details.name) {
|
|
362
|
+
console.log(` Project: ${details.name}@${details.version || '?'}`);
|
|
363
|
+
}
|
|
364
|
+
if (details.packageManager) {
|
|
365
|
+
console.log(` Package manager: ${details.packageManager}`);
|
|
366
|
+
}
|
|
367
|
+
if (details.dependencies) {
|
|
368
|
+
console.log(` Dependencies: ${details.dependencies}`);
|
|
369
|
+
}
|
|
370
|
+
if (details.hasVirtualEnv) {
|
|
371
|
+
console.log(` Virtual environment: detected`);
|
|
372
|
+
}
|
|
373
|
+
break;
|
|
374
|
+
|
|
375
|
+
case 'django':
|
|
376
|
+
if (details.name) {
|
|
377
|
+
console.log(` Project: ${details.name}`);
|
|
378
|
+
}
|
|
379
|
+
console.log(` Framework: Django`);
|
|
380
|
+
if (details.djangoApps && details.djangoApps.length > 0) {
|
|
381
|
+
console.log(` Django apps: ${details.djangoApps.join(', ')}`);
|
|
382
|
+
}
|
|
383
|
+
if (details.hasVirtualEnv) {
|
|
384
|
+
console.log(` Virtual environment: detected`);
|
|
385
|
+
}
|
|
386
|
+
break;
|
|
387
|
+
|
|
388
|
+
case 'flask':
|
|
389
|
+
if (details.name) {
|
|
390
|
+
console.log(` Project: ${details.name}`);
|
|
391
|
+
}
|
|
392
|
+
console.log(` Framework: Flask`);
|
|
393
|
+
if (details.hasVirtualEnv) {
|
|
394
|
+
console.log(` Virtual environment: detected`);
|
|
395
|
+
}
|
|
396
|
+
break;
|
|
397
|
+
|
|
398
|
+
case 'rust':
|
|
399
|
+
if (details.name) {
|
|
400
|
+
console.log(` Package: ${details.name}@${details.version || '?'}`);
|
|
401
|
+
}
|
|
402
|
+
if (details.edition) {
|
|
403
|
+
console.log(` Rust edition: ${details.edition}`);
|
|
404
|
+
}
|
|
405
|
+
if (details.isWorkspace) {
|
|
406
|
+
console.log(` Cargo workspace: detected`);
|
|
407
|
+
}
|
|
408
|
+
break;
|
|
409
|
+
|
|
410
|
+
case 'go':
|
|
411
|
+
if (details.module) {
|
|
412
|
+
console.log(` Module: ${details.module}`);
|
|
413
|
+
}
|
|
414
|
+
if (details.goVersion) {
|
|
415
|
+
console.log(` Go version: ${details.goVersion}`);
|
|
416
|
+
}
|
|
417
|
+
break;
|
|
418
|
+
|
|
419
|
+
case 'dotnet':
|
|
420
|
+
if (details.language) {
|
|
421
|
+
console.log(` Language: ${details.language}`);
|
|
422
|
+
}
|
|
423
|
+
if (details.projectFiles && details.projectFiles.length > 0) {
|
|
424
|
+
console.log(` Project files: ${details.projectFiles.join(', ')}`);
|
|
425
|
+
}
|
|
426
|
+
if (details.hasSolution) {
|
|
427
|
+
console.log(` Solution: detected`);
|
|
428
|
+
}
|
|
429
|
+
break;
|
|
430
|
+
|
|
431
|
+
case 'flutter':
|
|
432
|
+
if (details.name) {
|
|
433
|
+
console.log(` App: ${details.name}@${details.version || '?'}`);
|
|
434
|
+
}
|
|
435
|
+
break;
|
|
436
|
+
|
|
437
|
+
case 'react-native':
|
|
438
|
+
if (details.name) {
|
|
439
|
+
console.log(` App: ${details.name}@${details.version || '?'}`);
|
|
440
|
+
}
|
|
441
|
+
if (details.reactNativeVersion) {
|
|
442
|
+
console.log(` React Native: ${details.reactNativeVersion}`);
|
|
443
|
+
}
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (detection.allDetections && detection.allDetections.length > 1) {
|
|
449
|
+
console.log(` Other possibilities: ${detection.allDetections.slice(1).map(d => d.type).join(', ')}`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
console.log('');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Parses YAML-like content from ENVIRONMENT.md
|
|
457
|
+
* @param {string} content - The raw content of ENVIRONMENT.md
|
|
458
|
+
* @returns {object} Parsed key-value pairs
|
|
459
|
+
*/
|
|
460
|
+
function parseEnvironmentYaml(content) {
|
|
461
|
+
const result = {};
|
|
462
|
+
const lines = content.split('\n');
|
|
463
|
+
|
|
464
|
+
for (const line of lines) {
|
|
465
|
+
const trimmed = line.trim();
|
|
466
|
+
if (trimmed && !trimmed.startsWith('#') && trimmed.includes(':')) {
|
|
467
|
+
const [key, ...valueParts] = trimmed.split(':');
|
|
468
|
+
const value = valueParts.join(':').trim();
|
|
469
|
+
|
|
470
|
+
// Remove quotes if present
|
|
471
|
+
const cleanValue = value.replace(/^["']|["']$/g, '');
|
|
472
|
+
result[key.trim()] = cleanValue;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return result;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Loads and processes the .eck directory manifest
|
|
481
|
+
* @param {string} repoPath - Path to the repository
|
|
482
|
+
* @returns {Promise<object|null>} The eck manifest object or null if no .eck directory
|
|
483
|
+
*/
|
|
484
|
+
export async function loadProjectEckManifest(repoPath) {
|
|
485
|
+
const eckDir = path.join(repoPath, '.eck');
|
|
486
|
+
|
|
487
|
+
try {
|
|
488
|
+
// Check if .eck directory exists
|
|
489
|
+
const eckStats = await fs.stat(eckDir);
|
|
490
|
+
if (!eckStats.isDirectory()) {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
console.log('📋 Found .eck directory - loading project manifest...');
|
|
495
|
+
|
|
496
|
+
const manifest = {
|
|
497
|
+
environment: {},
|
|
498
|
+
context: '',
|
|
499
|
+
operations: '',
|
|
500
|
+
journal: ''
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
// Define the files to check
|
|
504
|
+
const files = [
|
|
505
|
+
{ name: 'ENVIRONMENT.md', key: 'environment', parser: parseEnvironmentYaml },
|
|
506
|
+
{ name: 'CONTEXT.md', key: 'context', parser: content => content },
|
|
507
|
+
{ name: 'OPERATIONS.md', key: 'operations', parser: content => content },
|
|
508
|
+
{ name: 'JOURNAL.md', key: 'journal', parser: content => content }
|
|
509
|
+
];
|
|
510
|
+
|
|
511
|
+
// Process each file
|
|
512
|
+
for (const file of files) {
|
|
513
|
+
const filePath = path.join(eckDir, file.name);
|
|
514
|
+
try {
|
|
515
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
516
|
+
manifest[file.key] = file.parser(content.trim());
|
|
517
|
+
console.log(` ✅ Loaded ${file.name}`);
|
|
518
|
+
} catch (error) {
|
|
519
|
+
// File doesn't exist or can't be read - that's okay, use default
|
|
520
|
+
console.log(` ⚠️ ${file.name} not found or unreadable`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return manifest;
|
|
525
|
+
} catch (error) {
|
|
526
|
+
// .eck directory doesn't exist - that's normal
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Ensures that 'snapshots/' is added to the target project's .gitignore file
|
|
533
|
+
* @param {string} repoPath - Path to the repository
|
|
534
|
+
*/
|
|
535
|
+
export async function ensureSnapshotsInGitignore(repoPath) {
|
|
536
|
+
const gitignorePath = path.join(repoPath, '.gitignore');
|
|
537
|
+
const entryToAdd = '.eck/';
|
|
538
|
+
const comment = '# Added by eck-snapshot to ignore metadata directory';
|
|
539
|
+
|
|
540
|
+
try {
|
|
541
|
+
// Check if the repo is a Git repository first
|
|
542
|
+
const isGitRepo = await checkGitRepository(repoPath);
|
|
543
|
+
if (!isGitRepo) {
|
|
544
|
+
// Not a Git repo, skip .gitignore modification
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
let gitignoreContent = '';
|
|
549
|
+
let fileExists = true;
|
|
550
|
+
|
|
551
|
+
// Try to read existing .gitignore file
|
|
552
|
+
try {
|
|
553
|
+
gitignoreContent = await fs.readFile(gitignorePath, 'utf-8');
|
|
554
|
+
} catch (error) {
|
|
555
|
+
// File doesn't exist, we'll create it
|
|
556
|
+
fileExists = false;
|
|
557
|
+
gitignoreContent = '';
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Check if 'snapshots/' is already in the file
|
|
561
|
+
const lines = gitignoreContent.split('\n');
|
|
562
|
+
const hasSnapshotsEntry = lines.some(line => line.trim() === entryToAdd);
|
|
563
|
+
|
|
564
|
+
if (!hasSnapshotsEntry) {
|
|
565
|
+
// Add the entry
|
|
566
|
+
let newContent = gitignoreContent;
|
|
567
|
+
|
|
568
|
+
// If file exists and doesn't end with newline, add one
|
|
569
|
+
if (fileExists && gitignoreContent && !gitignoreContent.endsWith('\n')) {
|
|
570
|
+
newContent += '\n';
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Add comment and entry
|
|
574
|
+
if (fileExists && gitignoreContent) {
|
|
575
|
+
newContent += '\n';
|
|
576
|
+
}
|
|
577
|
+
newContent += comment + '\n' + entryToAdd + '\n';
|
|
578
|
+
|
|
579
|
+
await fs.writeFile(gitignorePath, newContent);
|
|
580
|
+
console.log(`✅ Added '${entryToAdd}' to .gitignore`);
|
|
581
|
+
}
|
|
582
|
+
} catch (error) {
|
|
583
|
+
// Silently fail - don't break the snapshot process if gitignore update fails
|
|
584
|
+
console.warn(`⚠️ Warning: Could not update .gitignore: ${error.message}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Helper function to determine if a string is a glob pattern
|
|
589
|
+
function isGlob(str) {
|
|
590
|
+
return str.includes('*') || str.includes('?') || str.includes('{');
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Applies advanced profile filtering (multi-profile, exclusion, and ad-hoc globs) to a file list.
|
|
595
|
+
*/
|
|
596
|
+
export async function applyProfileFilter(allFiles, profileString, repoPath) {
|
|
597
|
+
const profileParts = profileString.split(',').map(p => p.trim()).filter(Boolean);
|
|
598
|
+
|
|
599
|
+
const includeGlobs = [];
|
|
600
|
+
const excludeGlobs = [];
|
|
601
|
+
const includeNames = [];
|
|
602
|
+
const excludeNames = [];
|
|
603
|
+
|
|
604
|
+
// Step 1: Differentiate between profile names and ad-hoc glob patterns
|
|
605
|
+
for (const part of profileParts) {
|
|
606
|
+
const isNegative = part.startsWith('-');
|
|
607
|
+
const pattern = isNegative ? part.substring(1) : part;
|
|
608
|
+
|
|
609
|
+
if (isGlob(pattern)) {
|
|
610
|
+
if (isNegative) {
|
|
611
|
+
excludeGlobs.push(pattern);
|
|
612
|
+
} else {
|
|
613
|
+
includeGlobs.push(pattern);
|
|
614
|
+
}
|
|
615
|
+
} else {
|
|
616
|
+
if (isNegative) {
|
|
617
|
+
excludeNames.push(pattern);
|
|
618
|
+
} else {
|
|
619
|
+
includeNames.push(pattern);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
let workingFiles = [];
|
|
625
|
+
let finalIncludes = [...includeGlobs];
|
|
626
|
+
let finalExcludes = [...excludeGlobs];
|
|
627
|
+
|
|
628
|
+
// Step 2: Load patterns from specified profile names
|
|
629
|
+
const allProfileNames = [...new Set([...includeNames, ...excludeNames])];
|
|
630
|
+
const profiles = new Map();
|
|
631
|
+
for (const name of allProfileNames) {
|
|
632
|
+
const profile = await getProfile(name, repoPath);
|
|
633
|
+
if (profile) {
|
|
634
|
+
profiles.set(name, profile);
|
|
635
|
+
} else {
|
|
636
|
+
// This is an ad-hoc glob, not a profile, so no warning is needed.
|
|
637
|
+
if (!isGlob(name)) {
|
|
638
|
+
console.warn(`⚠️ Warning: Profile '${name}' not found and will be skipped.`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
for (const name of includeNames) {
|
|
644
|
+
if (profiles.has(name)) {
|
|
645
|
+
finalIncludes.push(...(profiles.get(name).include || []));
|
|
646
|
+
finalExcludes.push(...(profiles.get(name).exclude || []));
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
for (const name of excludeNames) {
|
|
650
|
+
if (profiles.has(name)) {
|
|
651
|
+
finalExcludes.push(...(profiles.get(name).include || []));
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Step 3: Apply the filtering logic
|
|
656
|
+
if (finalIncludes.length > 0) {
|
|
657
|
+
workingFiles = micromatch(allFiles, finalIncludes);
|
|
658
|
+
} else if (includeNames.length > 0 && includeGlobs.length === 0) {
|
|
659
|
+
workingFiles = [];
|
|
660
|
+
} else {
|
|
661
|
+
workingFiles = allFiles;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (finalExcludes.length > 0) {
|
|
665
|
+
workingFiles = micromatch.not(workingFiles, finalExcludes);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return workingFiles;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Automatically initializes the .eck manifest directory, attempting dynamic generation via Claude.
|
|
673
|
+
* @param {string} projectPath - Path to the project
|
|
674
|
+
*/
|
|
675
|
+
export async function initializeEckManifest(projectPath) {
|
|
676
|
+
const eckDir = path.join(projectPath, '.eck');
|
|
677
|
+
|
|
678
|
+
// Load setup configuration to check AI generation settings
|
|
679
|
+
let aiGenerationEnabled = false;
|
|
680
|
+
try {
|
|
681
|
+
const setupConfig = await loadSetupConfig();
|
|
682
|
+
aiGenerationEnabled = setupConfig?.aiInstructions?.manifestInitialization?.aiGenerationEnabled ?? false;
|
|
683
|
+
} catch (error) {
|
|
684
|
+
// If setup config fails to load, default to disabled
|
|
685
|
+
console.warn(` ⚠️ Could not load setup config: ${error.message}. AI generation disabled.`);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
try {
|
|
689
|
+
// Check if .eck directory already exists and has all required files
|
|
690
|
+
let needsInitialization = false;
|
|
691
|
+
try {
|
|
692
|
+
const eckStats = await fs.stat(eckDir);
|
|
693
|
+
if (eckStats.isDirectory()) {
|
|
694
|
+
// Directory exists, check if all required files are present
|
|
695
|
+
const requiredFiles = ['ENVIRONMENT.md', 'CONTEXT.md', 'OPERATIONS.md', 'JOURNAL.md'];
|
|
696
|
+
for (const fileName of requiredFiles) {
|
|
697
|
+
try {
|
|
698
|
+
await fs.stat(path.join(eckDir, fileName));
|
|
699
|
+
} catch (error) {
|
|
700
|
+
console.log(` ℹ️ Missing ${fileName}, initialization needed`);
|
|
701
|
+
needsInitialization = true;
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (!needsInitialization) {
|
|
706
|
+
// All files exist, no need to initialize
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
} catch (error) {
|
|
711
|
+
// Directory doesn't exist, we'll create it
|
|
712
|
+
needsInitialization = true;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Create .eck directory
|
|
716
|
+
await fs.mkdir(eckDir, { recursive: true });
|
|
717
|
+
console.log('📋 Initializing .eck manifest directory...');
|
|
718
|
+
|
|
719
|
+
// --- NEW HYBRID LOGIC ---
|
|
720
|
+
// 1. Run static analysis first to gather facts.
|
|
721
|
+
let staticFacts = {};
|
|
722
|
+
try {
|
|
723
|
+
staticFacts = await detectProjectType(projectPath);
|
|
724
|
+
console.log(` 🔍 Static analysis complete. Detected type: ${staticFacts.type}`);
|
|
725
|
+
} catch (e) {
|
|
726
|
+
console.warn(` ⚠️ Static project detection failed: ${e.message}. Proceeding with generic prompts.`);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Prevent AI hallucination by removing low-confidence "other possibilities"
|
|
730
|
+
if (staticFacts && staticFacts.allDetections) {
|
|
731
|
+
delete staticFacts.allDetections;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const staticFactsJson = JSON.stringify(staticFacts, null, 2);
|
|
735
|
+
// --- END NEW LOGIC ---
|
|
736
|
+
|
|
737
|
+
// Template files with their content
|
|
738
|
+
const templateFiles = [
|
|
739
|
+
{
|
|
740
|
+
name: 'ENVIRONMENT.md',
|
|
741
|
+
prompt: `Given these static project analysis facts:\n${staticFactsJson}\n\nGenerate the raw YAML key-value content for an .eck/ENVIRONMENT.md file. Only include detected facts. DO NOT add any keys that are not present in the facts. DO NOT add conversational text or markdown wrappers. Your response MUST start directly with a YAML key (e.g., 'project_type: ...').`,
|
|
742
|
+
content: `# This file is for environment overrides. Add agent-specific settings here.\nagent_id: local_dev\n` // Simple static fallback
|
|
743
|
+
},
|
|
744
|
+
{
|
|
745
|
+
name: 'CONTEXT.md',
|
|
746
|
+
prompt: `Given these static project analysis facts:\n${staticFactsJson}\n\nGenerate the raw Markdown content ONLY for a .eck/CONTEXT.md file. Use the facts to write ## Description, ## Architecture, and ## Key Technologies. DO NOT add conversational text (like "Here is the file..."). Your response MUST start *directly* with the '# Project Overview' heading.`,
|
|
747
|
+
content: `# Project Overview
|
|
748
|
+
|
|
749
|
+
## Description
|
|
750
|
+
Brief description of what this project does and its main purpose.
|
|
751
|
+
|
|
752
|
+
## Architecture
|
|
753
|
+
High-level overview of the system architecture, key components, and how they interact.
|
|
754
|
+
|
|
755
|
+
## Key Technologies
|
|
756
|
+
- Technology 1
|
|
757
|
+
- Technology 2
|
|
758
|
+
- Technology 3
|
|
759
|
+
|
|
760
|
+
## Important Notes
|
|
761
|
+
Any crucial information that developers should know when working on this project.
|
|
762
|
+
`
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
name: 'OPERATIONS.md',
|
|
766
|
+
prompt: `Given these static project analysis facts (especially package.json scripts):
|
|
767
|
+
${staticFactsJson}
|
|
768
|
+
|
|
769
|
+
Generate the raw Markdown content ONLY for a .eck/OPERATIONS.md file. DO NOT add conversational text. Your response MUST start *directly* with the '# Common Operations' heading. List commands for ## Development Setup, ## Running the Project, and ## Testing.`,
|
|
770
|
+
content: `# Common Operations
|
|
771
|
+
|
|
772
|
+
## Development Setup
|
|
773
|
+
\`\`\`bash
|
|
774
|
+
# Setup commands
|
|
775
|
+
npm install
|
|
776
|
+
# or yarn install
|
|
777
|
+
\`\`\`
|
|
778
|
+
|
|
779
|
+
## Running the Project
|
|
780
|
+
\`\`\`bash
|
|
781
|
+
# Development mode
|
|
782
|
+
npm run dev
|
|
783
|
+
|
|
784
|
+
# Production build
|
|
785
|
+
npm run build
|
|
786
|
+
\`\`\`
|
|
787
|
+
|
|
788
|
+
## Testing
|
|
789
|
+
\`\`\`bash
|
|
790
|
+
# Run tests
|
|
791
|
+
npm test
|
|
792
|
+
|
|
793
|
+
# Run tests in watch mode
|
|
794
|
+
npm run test:watch
|
|
795
|
+
\`\`\`
|
|
796
|
+
|
|
797
|
+
## Deployment
|
|
798
|
+
\`\`\`bash
|
|
799
|
+
# Deployment commands
|
|
800
|
+
npm run deploy
|
|
801
|
+
\`\`\`
|
|
802
|
+
|
|
803
|
+
## Troubleshooting
|
|
804
|
+
Common issues and their solutions.
|
|
805
|
+
`
|
|
806
|
+
},
|
|
807
|
+
{
|
|
808
|
+
name: 'JOURNAL.md',
|
|
809
|
+
content: `# Development Journal
|
|
810
|
+
|
|
811
|
+
## Recent Changes
|
|
812
|
+
Track significant changes, decisions, and progress here.
|
|
813
|
+
|
|
814
|
+
---
|
|
815
|
+
|
|
816
|
+
### YYYY-MM-DD - Project Started
|
|
817
|
+
- Initial project setup
|
|
818
|
+
- Added basic structure
|
|
819
|
+
`
|
|
820
|
+
},
|
|
821
|
+
{
|
|
822
|
+
name: 'ROADMAP.md',
|
|
823
|
+
prompt: `Given these static project analysis facts:\n${staticFactsJson}\n\nGenerate the raw Markdown content ONLY for a .eck/ROADMAP.md file. DO NOT add conversational text. Start *directly* with '# Project Roadmap'. Propose 1-2 *plausible* placeholder items for ## Current Sprint/Phase and ## Next Phase based on the project type.`,
|
|
824
|
+
content: `# Project Roadmap
|
|
825
|
+
|
|
826
|
+
## Current Sprint/Phase
|
|
827
|
+
- [ ] Feature 1
|
|
828
|
+
- [ ] Feature 2
|
|
829
|
+
- [ ] Bug fix 1
|
|
830
|
+
|
|
831
|
+
## Next Phase
|
|
832
|
+
- [ ] Future feature 1
|
|
833
|
+
- [ ] Future feature 2
|
|
834
|
+
|
|
835
|
+
## Long-term Goals
|
|
836
|
+
- [ ] Major milestone 1
|
|
837
|
+
- [ ] Major milestone 2
|
|
838
|
+
|
|
839
|
+
## Completed
|
|
840
|
+
- [x] Project initialization
|
|
841
|
+
`
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
name: 'TECH_DEBT.md',
|
|
845
|
+
prompt: `Generate the raw Markdown content ONLY for a .eck/TECH_DEBT.md file. DO NOT add conversational text. Start *directly* with '# Technical Debt'. Propose 1-2 *common* placeholder items for ## Code Quality Issues and ## Refactoring Opportunities.`,
|
|
846
|
+
content: `# Technical Debt
|
|
847
|
+
|
|
848
|
+
## Current Technical Debt
|
|
849
|
+
Track technical debt, refactoring needs, and code quality issues.
|
|
850
|
+
|
|
851
|
+
### Code Quality Issues
|
|
852
|
+
- Issue 1: Description and priority
|
|
853
|
+
- Issue 2: Description and priority
|
|
854
|
+
|
|
855
|
+
### Refactoring Opportunities
|
|
856
|
+
- Opportunity 1: Description and impact
|
|
857
|
+
- Opportunity 2: Description and impact
|
|
858
|
+
|
|
859
|
+
### Performance Issues
|
|
860
|
+
- Performance issue 1: Description and impact
|
|
861
|
+
- Performance issue 2: Description and impact
|
|
862
|
+
|
|
863
|
+
### Security Concerns
|
|
864
|
+
- Security concern 1: Description and priority
|
|
865
|
+
- Security concern 2: Description and priority
|
|
866
|
+
|
|
867
|
+
## Resolved
|
|
868
|
+
- [x] Resolved issue 1
|
|
869
|
+
`
|
|
870
|
+
}
|
|
871
|
+
];
|
|
872
|
+
|
|
873
|
+
// Create each template file (only if it doesn't exist)
|
|
874
|
+
for (const file of templateFiles) {
|
|
875
|
+
const filePath = path.join(eckDir, file.name);
|
|
876
|
+
|
|
877
|
+
// Skip if file already exists
|
|
878
|
+
try {
|
|
879
|
+
await fs.stat(filePath);
|
|
880
|
+
console.log(` ✅ ${file.name} already exists, skipping`);
|
|
881
|
+
continue;
|
|
882
|
+
} catch (error) {
|
|
883
|
+
// File doesn't exist, create it
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
let fileContent = file.content; // Start with fallback
|
|
887
|
+
let generatedByAI = false;
|
|
888
|
+
|
|
889
|
+
// For files with a prompt, try to dynamically generate (only if enabled)
|
|
890
|
+
if (file.prompt && aiGenerationEnabled) {
|
|
891
|
+
try {
|
|
892
|
+
console.log(` 🧠 Attempting to auto-generate ${file.name} via Claude...`);
|
|
893
|
+
const aiResponseObject = await askClaude(file.prompt); // Use the prompt
|
|
894
|
+
const rawText = aiResponseObject.result; // Handle Claude response
|
|
895
|
+
|
|
896
|
+
if (!rawText || typeof rawText.replace !== 'function') {
|
|
897
|
+
throw new Error(`AI returned invalid content type: ${typeof rawText}`);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// Basic cleanup of potential markdown code blocks from Claude
|
|
901
|
+
const cleanedResponse = rawText.replace(/^```(markdown|yaml)?\n|```$/g, '').trim();
|
|
902
|
+
|
|
903
|
+
if (cleanedResponse) {
|
|
904
|
+
fileContent = cleanedResponse;
|
|
905
|
+
generatedByAI = true;
|
|
906
|
+
console.log(` ✨ AI successfully generated ${file.name}`);
|
|
907
|
+
} else {
|
|
908
|
+
throw new Error('AI returned empty content.');
|
|
909
|
+
}
|
|
910
|
+
} catch (error) {
|
|
911
|
+
console.warn(` ⚠️ AI generation failed for ${file.name}: ${error.message}. Using static template.`);
|
|
912
|
+
// fileContent is already set to the fallback
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
await fs.writeFile(filePath, fileContent);
|
|
917
|
+
if (!generatedByAI) {
|
|
918
|
+
console.log(` ✅ Created ${file.name} (static template)`);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
console.log('📋 .eck manifest initialized! Edit the files to provide project-specific context.');
|
|
923
|
+
|
|
924
|
+
} catch (error) {
|
|
925
|
+
// Silently fail - don't break the snapshot process if manifest initialization fails
|
|
926
|
+
console.warn(`⚠️ Warning: Could not initialize .eck manifest: ${error.message}`);
|
|
927
|
+
}
|
|
928
|
+
}
|