@xelth/eck-snapshot 6.0.7 → 6.0.9
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.
Potentially problematic release.
This version of @xelth/eck-snapshot might be problematic. Click here for more details.
- package/package.json +1 -1
- package/setup.json +5 -16
- package/src/cli/commands/createSnapshot.js +10 -0
- package/src/cli/commands/setupMcp.js +57 -0
- package/src/utils/claudeMdGenerator.js +16 -6
- package/src/utils/fileUtils.js +944 -942
package/src/utils/fileUtils.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
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
|
-
import { minimatch } from 'minimatch';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Scanner for detecting and redacting secrets (API keys, tokens)
|
|
13
|
-
*/
|
|
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
|
+
import { minimatch } from 'minimatch';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Scanner for detecting and redacting secrets (API keys, tokens)
|
|
13
|
+
*/
|
|
14
14
|
export const SecretScanner = {
|
|
15
15
|
patterns: [
|
|
16
16
|
// Service-specific patterns
|
|
@@ -91,783 +91,783 @@ export const SecretScanner = {
|
|
|
91
91
|
};
|
|
92
92
|
}
|
|
93
93
|
};
|
|
94
|
-
|
|
95
|
-
export function parseSize(sizeStr) {
|
|
96
|
-
const units = { B: 1, KB: 1024, MB: 1024 ** 2, GB: 1024 ** 3 };
|
|
97
|
-
const match = sizeStr.match(/^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB)?$/i);
|
|
98
|
-
if (!match) throw new Error(`Invalid size format: ${sizeStr}`);
|
|
99
|
-
const [, size, unit = 'B'] = match;
|
|
100
|
-
return Math.floor(parseFloat(size) * units[unit.toUpperCase()]);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function formatSize(bytes) {
|
|
104
|
-
const units = ['B', 'KB', 'MB', 'GB'];
|
|
105
|
-
let size = bytes;
|
|
106
|
-
let unitIndex = 0;
|
|
107
|
-
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
108
|
-
size /= 1024;
|
|
109
|
-
unitIndex++;
|
|
110
|
-
}
|
|
111
|
-
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function matchesPattern(filePath, patterns) {
|
|
115
|
-
const fileName = path.basename(filePath);
|
|
116
|
-
return patterns.some(pattern => {
|
|
117
|
-
const regexPattern = '^' + pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$';
|
|
118
|
-
try {
|
|
119
|
-
const regex = new RegExp(regexPattern);
|
|
120
|
-
return regex.test(fileName);
|
|
121
|
-
} catch (e) {
|
|
122
|
-
console.warn(`⚠️ Invalid regex pattern in config: "${pattern}"`);
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Checks if a file matches confidential patterns using minimatch
|
|
130
|
-
* @param {string} fileName - The file name to check
|
|
131
|
-
* @param {array} patterns - Array of glob patterns to match against
|
|
132
|
-
* @returns {boolean} True if the file matches any pattern
|
|
133
|
-
*/
|
|
134
|
-
function matchesConfidentialPattern(fileName, patterns) {
|
|
135
|
-
return patterns.some(pattern => minimatch(fileName, pattern, { nocase: true }));
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Applies smart filtering for files within the .eck directory.
|
|
140
|
-
* Includes documentation files while excluding confidential files.
|
|
141
|
-
* @param {string} fileName - The file name to check
|
|
142
|
-
* @param {object} eckConfig - The eckDirectoryFiltering config object
|
|
143
|
-
* @returns {object} { include: boolean, isConfidential: boolean }
|
|
144
|
-
*/
|
|
145
|
-
export function applyEckDirectoryFiltering(fileName, eckConfig) {
|
|
146
|
-
if (!eckConfig || !eckConfig.enabled) {
|
|
147
|
-
return { include: false, isConfidential: false }; // .eck filtering disabled, exclude all
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const { confidentialPatterns = [], alwaysIncludePatterns = [] } = eckConfig;
|
|
151
|
-
|
|
152
|
-
// First check if file matches confidential patterns
|
|
153
|
-
const isConfidential = matchesConfidentialPattern(fileName, confidentialPatterns);
|
|
154
|
-
if (isConfidential) {
|
|
155
|
-
return { include: false, isConfidential: true };
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Check if file matches always-include patterns
|
|
159
|
-
if (matchesPattern(fileName, alwaysIncludePatterns)) {
|
|
160
|
-
return { include: true, isConfidential: false };
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Default: exclude files not in the include list
|
|
164
|
-
return { include: false, isConfidential: false };
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export async function checkGitRepository(repoPath) {
|
|
168
|
-
try {
|
|
169
|
-
await execa('git', ['rev-parse', '--git-dir'], { cwd: repoPath });
|
|
170
|
-
return true;
|
|
171
|
-
} catch (error) {
|
|
172
|
-
return false;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export async function scanDirectoryRecursively(dirPath, config, relativeTo = dirPath, projectType = null, trackConfidential = false) {
|
|
177
|
-
const files = [];
|
|
178
|
-
const confidentialFiles = [];
|
|
179
|
-
|
|
180
|
-
// Get project-specific filtering if not provided
|
|
181
|
-
if (!projectType) {
|
|
182
|
-
const detection = await detectProjectType(relativeTo);
|
|
183
|
-
projectType = detection.type;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const projectSpecific = await getProjectSpecificFiltering(projectType);
|
|
187
|
-
|
|
188
|
-
// Merge project-specific filters with global config
|
|
189
|
-
const effectiveConfig = {
|
|
190
|
-
...config,
|
|
191
|
-
dirsToIgnore: [...(config.dirsToIgnore || []), ...(projectSpecific.dirsToIgnore || [])],
|
|
192
|
-
filesToIgnore: [...(config.filesToIgnore || []), ...(projectSpecific.filesToIgnore || [])],
|
|
193
|
-
extensionsToIgnore: [...(config.extensionsToIgnore || []), ...(projectSpecific.extensionsToIgnore || [])]
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
try {
|
|
197
|
-
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
198
|
-
|
|
199
|
-
for (const entry of entries) {
|
|
200
|
-
const fullPath = path.join(dirPath, entry.name);
|
|
201
|
-
const relativePath = path.relative(relativeTo, fullPath).replace(/\\/g, '/');
|
|
202
|
-
|
|
203
|
-
// --- GLOBAL HARD IGNORES (Zero-Config Safety) ---
|
|
204
|
-
// Explicitly skip heavy/system directories and lockfiles everywhere
|
|
205
|
-
if (entry.isDirectory()) {
|
|
206
|
-
if (entry.name === 'node_modules' ||
|
|
207
|
-
entry.name === '.git' ||
|
|
208
|
-
entry.name === '.idea' ||
|
|
209
|
-
entry.name === '.vscode') {
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
} else {
|
|
213
|
-
if (entry.name === 'package-lock.json' ||
|
|
214
|
-
entry.name === 'yarn.lock' ||
|
|
215
|
-
entry.name === 'pnpm-lock.yaml' ||
|
|
216
|
-
entry.name === 'go.sum') {
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
// -----------------------------------------------
|
|
221
|
-
|
|
222
|
-
// Special handling for .eck directory - never ignore it when tracking confidential files
|
|
223
|
-
const isEckDirectory = entry.name === '.eck' && entry.isDirectory();
|
|
224
|
-
const isInsideEck = relativePath.startsWith('.eck/');
|
|
225
|
-
|
|
226
|
-
if (effectiveConfig.dirsToIgnore.some(dir =>
|
|
227
|
-
entry.name === dir.replace('/', '') ||
|
|
228
|
-
relativePath.startsWith(dir)
|
|
229
|
-
) && !isEckDirectory && !isInsideEck) {
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (!effectiveConfig.includeHidden && entry.name.startsWith('.') && !isEckDirectory && !isInsideEck) {
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (entry.isDirectory()) {
|
|
238
|
-
const subResult = await scanDirectoryRecursively(fullPath, effectiveConfig, relativeTo, projectType, trackConfidential);
|
|
239
|
-
if (trackConfidential) {
|
|
240
|
-
files.push(...subResult.files);
|
|
241
|
-
confidentialFiles.push(...subResult.confidentialFiles);
|
|
242
|
-
} else {
|
|
243
|
-
files.push(...subResult);
|
|
244
|
-
}
|
|
245
|
-
} else {
|
|
246
|
-
// Apply smart filtering for files inside .eck directory
|
|
247
|
-
if (isInsideEck) {
|
|
248
|
-
const eckConfig = effectiveConfig.eckDirectoryFiltering;
|
|
249
|
-
const filterResult = applyEckDirectoryFiltering(entry.name, eckConfig);
|
|
250
|
-
|
|
251
|
-
if (trackConfidential && filterResult.isConfidential) {
|
|
252
|
-
confidentialFiles.push(relativePath);
|
|
253
|
-
} else if (filterResult.include) {
|
|
254
|
-
files.push(relativePath);
|
|
255
|
-
}
|
|
256
|
-
} else {
|
|
257
|
-
// Normal filtering for non-.eck files
|
|
258
|
-
if (effectiveConfig.extensionsToIgnore.includes(path.extname(entry.name)) ||
|
|
259
|
-
matchesPattern(relativePath, effectiveConfig.filesToIgnore)) {
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
262
|
-
files.push(relativePath);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
} catch (error) {
|
|
267
|
-
console.warn(`⚠️ Warning: Could not read directory: ${dirPath} - ${error.message}`);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
return trackConfidential ? { files, confidentialFiles } : files;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
export async function loadGitignore(repoPath) {
|
|
274
|
-
try {
|
|
275
|
-
const gitignoreContent = await fs.readFile(path.join(repoPath, '.gitignore'), 'utf-8');
|
|
276
|
-
const ig = ignore().add(gitignoreContent);
|
|
277
|
-
return ig;
|
|
278
|
-
} catch {
|
|
279
|
-
console.log('ℹ️ No .gitignore file found or could not be read');
|
|
280
|
-
return ignore();
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
export async function readFileWithSizeCheck(filePath, maxFileSize) {
|
|
285
|
-
try {
|
|
286
|
-
const stats = await fs.stat(filePath);
|
|
287
|
-
if (stats.size > maxFileSize) {
|
|
288
|
-
throw new Error(`File too large: ${formatSize(stats.size)}`);
|
|
289
|
-
}
|
|
290
|
-
return await fs.readFile(filePath, 'utf-8');
|
|
291
|
-
} catch (error) {
|
|
292
|
-
if (error.message.includes('too large')) throw error;
|
|
293
|
-
throw new Error(`Could not read file: ${error.message}`);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
export async function generateDirectoryTree(dir, prefix = '', allFiles, depth = 0, maxDepth = 10, config) {
|
|
298
|
-
if (depth > maxDepth) return '';
|
|
299
|
-
|
|
300
|
-
try {
|
|
301
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
302
|
-
const sortedEntries = entries.sort((a, b) => {
|
|
303
|
-
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
304
|
-
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
305
|
-
return a.name.localeCompare(b.name);
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
let tree = '';
|
|
309
|
-
const validEntries = [];
|
|
310
|
-
|
|
311
|
-
for (const entry of sortedEntries) {
|
|
312
|
-
// Skip hidden directories and files (starting with '.')
|
|
313
|
-
// EXCEPT: Allow .eck to be visible
|
|
314
|
-
if (entry.name.startsWith('.') && entry.name !== '.eck') {
|
|
315
|
-
continue;
|
|
316
|
-
}
|
|
317
|
-
if (config.dirsToIgnore.some(d => entry.name.includes(d.replace('/', '')))) continue;
|
|
318
|
-
const fullPath = path.join(dir, entry.name);
|
|
319
|
-
const relativePath = path.relative(process.cwd(), fullPath).replace(/\\/g, '/');
|
|
320
|
-
|
|
321
|
-
// FORCE VISIBILITY for .eck files in the tree
|
|
322
|
-
// Even if they are gitignored (not in allFiles), we want the Architect to see they exist
|
|
323
|
-
const isInsideEck = relativePath.startsWith('.eck/') || relativePath === '.eck';
|
|
324
|
-
|
|
325
|
-
if (entry.isDirectory() || allFiles.includes(relativePath) || isInsideEck) {
|
|
326
|
-
validEntries.push({ entry, fullPath, relativePath });
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
for (let i = 0; i < validEntries.length; i++) {
|
|
331
|
-
const { entry, fullPath, relativePath } = validEntries[i];
|
|
332
|
-
const isLast = i === validEntries.length - 1;
|
|
333
|
-
|
|
334
|
-
const connector = isLast ? '└── ' : '├── ';
|
|
335
|
-
const nextPrefix = prefix + (isLast ? ' ' : '│ ');
|
|
336
|
-
|
|
337
|
-
if (entry.isDirectory()) {
|
|
338
|
-
tree += `${prefix}${connector}${entry.name}/\n`;
|
|
339
|
-
|
|
340
|
-
// RECURSION CONTROL:
|
|
341
|
-
// If we are currently inside .eck, do NOT recurse deeper into subdirectories (like snapshots, logs).
|
|
342
|
-
// We want to see that 'snapshots/' exists, but not list its contents.
|
|
343
|
-
const isInsideEckRoot = path.basename(dir) === '.eck';
|
|
344
|
-
|
|
345
|
-
if (!isInsideEckRoot) {
|
|
346
|
-
tree += await generateDirectoryTree(fullPath, nextPrefix, allFiles, depth + 1, maxDepth, config);
|
|
347
|
-
}
|
|
348
|
-
} else {
|
|
349
|
-
tree += `${prefix}${connector}${entry.name}\n`;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
return tree;
|
|
354
|
-
} catch (error) {
|
|
355
|
-
console.warn(`⚠️ Warning: Could not read directory: ${dir}`);
|
|
356
|
-
return '';
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
export function parseSnapshotContent(content) {
|
|
361
|
-
const files = [];
|
|
362
|
-
const fileRegex = /--- File: \/(.+) ---/g;
|
|
363
|
-
const sections = content.split(fileRegex);
|
|
364
|
-
|
|
365
|
-
for (let i = 1; i < sections.length; i += 2) {
|
|
366
|
-
const filePath = sections[i].trim();
|
|
367
|
-
let fileContent = sections[i + 1] || '';
|
|
368
|
-
|
|
369
|
-
if (fileContent.startsWith('\n\n')) {
|
|
370
|
-
fileContent = fileContent.substring(2);
|
|
371
|
-
}
|
|
372
|
-
if (fileContent.endsWith('\n\n')) {
|
|
373
|
-
fileContent = fileContent.substring(0, fileContent.length - 2);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
files.push({ path: filePath, content: fileContent });
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
return files;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
export function filterFilesToRestore(files, options) {
|
|
383
|
-
let filtered = files;
|
|
384
|
-
|
|
385
|
-
if (options.include) {
|
|
386
|
-
const includePatterns = Array.isArray(options.include) ?
|
|
387
|
-
options.include : [options.include];
|
|
388
|
-
filtered = filtered.filter(file =>
|
|
389
|
-
includePatterns.some(pattern => {
|
|
390
|
-
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
391
|
-
return regex.test(file.path);
|
|
392
|
-
})
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
if (options.exclude) {
|
|
397
|
-
const excludePatterns = Array.isArray(options.exclude) ?
|
|
398
|
-
options.exclude : [options.exclude];
|
|
399
|
-
filtered = filtered.filter(file =>
|
|
400
|
-
!excludePatterns.some(pattern => {
|
|
401
|
-
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
402
|
-
return regex.test(file.path);
|
|
403
|
-
})
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
return filtered;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
export function validateFilePaths(files, targetDir) {
|
|
411
|
-
const invalidFiles = [];
|
|
412
|
-
|
|
413
|
-
for (const file of files) {
|
|
414
|
-
const normalizedPath = path.normalize(file.path);
|
|
415
|
-
if (normalizedPath.includes('..') ||
|
|
416
|
-
normalizedPath.startsWith('/') ||
|
|
417
|
-
normalizedPath.includes('\0') ||
|
|
418
|
-
/[<>:"|?*]/.test(normalizedPath)) {
|
|
419
|
-
invalidFiles.push(file.path);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
return invalidFiles;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
export async function loadConfig(configPath) {
|
|
427
|
-
const { DEFAULT_CONFIG } = await import('../config.js');
|
|
428
|
-
let config = { ...DEFAULT_CONFIG };
|
|
429
|
-
|
|
430
|
-
if (configPath) {
|
|
431
|
-
try {
|
|
432
|
-
const configModule = await import(path.resolve(configPath));
|
|
433
|
-
config = { ...config, ...configModule.default };
|
|
434
|
-
console.log(`✅ Configuration loaded from: ${configPath}`);
|
|
435
|
-
} catch (error) {
|
|
436
|
-
console.warn(`⚠️ Warning: Could not load config file: ${configPath}`);
|
|
437
|
-
}
|
|
438
|
-
} else {
|
|
439
|
-
const possibleConfigs = [
|
|
440
|
-
'.ecksnapshot.config.js',
|
|
441
|
-
'.ecksnapshot.config.mjs',
|
|
442
|
-
'ecksnapshot.config.js'
|
|
443
|
-
];
|
|
444
|
-
|
|
445
|
-
for (const configFile of possibleConfigs) {
|
|
446
|
-
try {
|
|
447
|
-
await fs.access(configFile);
|
|
448
|
-
const configModule = await import(path.resolve(configFile));
|
|
449
|
-
config = { ...config, ...configModule.default };
|
|
450
|
-
console.log(`✅ Configuration loaded from: ${configFile}`);
|
|
451
|
-
break;
|
|
452
|
-
} catch {
|
|
453
|
-
// Config file doesn't exist, continue
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
return config;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
export function generateTimestamp() {
|
|
462
|
-
const now = new Date();
|
|
463
|
-
const YY = String(now.getFullYear()).slice(-2);
|
|
464
|
-
const MM = String(now.getMonth() + 1).padStart(2, '0');
|
|
465
|
-
const DD = String(now.getDate()).padStart(2, '0');
|
|
466
|
-
const hh = String(now.getHours()).padStart(2, '0');
|
|
467
|
-
const mm = String(now.getMinutes()).padStart(2, '0');
|
|
468
|
-
// Compact format: YY-MM-DD_HH-mm (no seconds)
|
|
469
|
-
return `${YY}-${MM}-${DD}_${hh}-${mm}`;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
/**
|
|
473
|
-
* Generates a short repo name with capitalized first 3 and last 2 characters
|
|
474
|
-
* Example: "Snapshot" -> "SnaOt", "MyProject" -> "MyPrjt"
|
|
475
|
-
* @param {string} repoName - The repository name
|
|
476
|
-
* @returns {string} Shortened repo name (Start3 + End2)
|
|
477
|
-
*/
|
|
478
|
-
export function getShortRepoName(repoName) {
|
|
479
|
-
if (!repoName) return '';
|
|
480
|
-
if (repoName.length <= 5) {
|
|
481
|
-
return repoName.toUpperCase();
|
|
482
|
-
}
|
|
483
|
-
const start = repoName.substring(0, 3);
|
|
484
|
-
const end = repoName.substring(repoName.length - 2);
|
|
485
|
-
return (start + end).toUpperCase();
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/**
|
|
489
|
-
* Displays project detection information in a user-friendly format
|
|
490
|
-
* @param {object} detection - Project detection result
|
|
491
|
-
*/
|
|
492
|
-
export function displayProjectInfo(detection) {
|
|
493
|
-
console.log('\n🔍 Project Detection Results:');
|
|
494
|
-
console.log(` Type: ${detection.type} (confidence: ${(detection.confidence * 100).toFixed(0)}%)`);
|
|
495
|
-
|
|
496
|
-
if (detection.details) {
|
|
497
|
-
const details = detection.details;
|
|
498
|
-
|
|
499
|
-
switch (detection.type) {
|
|
500
|
-
case 'android':
|
|
501
|
-
console.log(` Language: ${details.language || 'unknown'}`);
|
|
502
|
-
if (details.packageName) {
|
|
503
|
-
console.log(` Package: ${details.packageName}`);
|
|
504
|
-
}
|
|
505
|
-
if (details.sourceDirs && details.sourceDirs.length > 0) {
|
|
506
|
-
console.log(` Source dirs: ${details.sourceDirs.join(', ')}`);
|
|
507
|
-
}
|
|
508
|
-
if (details.libFiles && details.libFiles.length > 0) {
|
|
509
|
-
console.log(` Libraries: ${details.libFiles.length} .aar/.jar files`);
|
|
510
|
-
}
|
|
511
|
-
break;
|
|
512
|
-
|
|
513
|
-
case 'nodejs':
|
|
514
|
-
if (details.name) {
|
|
515
|
-
console.log(` Package: ${details.name}@${details.version || '?'}`);
|
|
516
|
-
}
|
|
517
|
-
if (details.framework) {
|
|
518
|
-
console.log(` Framework: ${details.framework}`);
|
|
519
|
-
}
|
|
520
|
-
if (details.hasTypescript) {
|
|
521
|
-
console.log(` TypeScript: enabled`);
|
|
522
|
-
}
|
|
523
|
-
break;
|
|
524
|
-
|
|
525
|
-
case 'nodejs-monorepo':
|
|
526
|
-
if (details.name) {
|
|
527
|
-
console.log(` Project: ${details.name}@${details.version || '?'}`);
|
|
528
|
-
}
|
|
529
|
-
if (details.monorepoTool) {
|
|
530
|
-
console.log(` Monorepo tool: ${details.monorepoTool}`);
|
|
531
|
-
}
|
|
532
|
-
if (details.workspaceCount) {
|
|
533
|
-
console.log(` Workspaces: ${details.workspaceCount}`);
|
|
534
|
-
}
|
|
535
|
-
if (details.framework) {
|
|
536
|
-
console.log(` Framework: ${details.framework}`);
|
|
537
|
-
}
|
|
538
|
-
break;
|
|
539
|
-
|
|
540
|
-
case 'python-poetry':
|
|
541
|
-
case 'python-pip':
|
|
542
|
-
case 'python-conda':
|
|
543
|
-
if (details.name) {
|
|
544
|
-
console.log(` Project: ${details.name}@${details.version || '?'}`);
|
|
545
|
-
}
|
|
546
|
-
if (details.packageManager) {
|
|
547
|
-
console.log(` Package manager: ${details.packageManager}`);
|
|
548
|
-
}
|
|
549
|
-
if (details.dependencies) {
|
|
550
|
-
console.log(` Dependencies: ${details.dependencies}`);
|
|
551
|
-
}
|
|
552
|
-
if (details.hasVirtualEnv) {
|
|
553
|
-
console.log(` Virtual environment: detected`);
|
|
554
|
-
}
|
|
555
|
-
break;
|
|
556
|
-
|
|
557
|
-
case 'django':
|
|
558
|
-
if (details.name) {
|
|
559
|
-
console.log(` Project: ${details.name}`);
|
|
560
|
-
}
|
|
561
|
-
console.log(` Framework: Django`);
|
|
562
|
-
if (details.djangoApps && details.djangoApps.length > 0) {
|
|
563
|
-
console.log(` Django apps: ${details.djangoApps.join(', ')}`);
|
|
564
|
-
}
|
|
565
|
-
if (details.hasVirtualEnv) {
|
|
566
|
-
console.log(` Virtual environment: detected`);
|
|
567
|
-
}
|
|
568
|
-
break;
|
|
569
|
-
|
|
570
|
-
case 'flask':
|
|
571
|
-
if (details.name) {
|
|
572
|
-
console.log(` Project: ${details.name}`);
|
|
573
|
-
}
|
|
574
|
-
console.log(` Framework: Flask`);
|
|
575
|
-
if (details.hasVirtualEnv) {
|
|
576
|
-
console.log(` Virtual environment: detected`);
|
|
577
|
-
}
|
|
578
|
-
break;
|
|
579
|
-
|
|
580
|
-
case 'rust':
|
|
581
|
-
if (details.name) {
|
|
582
|
-
console.log(` Package: ${details.name}@${details.version || '?'}`);
|
|
583
|
-
}
|
|
584
|
-
if (details.edition) {
|
|
585
|
-
console.log(` Rust edition: ${details.edition}`);
|
|
586
|
-
}
|
|
587
|
-
if (details.isWorkspace) {
|
|
588
|
-
console.log(` Cargo workspace: detected`);
|
|
589
|
-
}
|
|
590
|
-
break;
|
|
591
|
-
|
|
592
|
-
case 'go':
|
|
593
|
-
if (details.module) {
|
|
594
|
-
console.log(` Module: ${details.module}`);
|
|
595
|
-
}
|
|
596
|
-
if (details.goVersion) {
|
|
597
|
-
console.log(` Go version: ${details.goVersion}`);
|
|
598
|
-
}
|
|
599
|
-
break;
|
|
600
|
-
|
|
601
|
-
case 'dotnet':
|
|
602
|
-
if (details.language) {
|
|
603
|
-
console.log(` Language: ${details.language}`);
|
|
604
|
-
}
|
|
605
|
-
if (details.projectFiles && details.projectFiles.length > 0) {
|
|
606
|
-
console.log(` Project files: ${details.projectFiles.join(', ')}`);
|
|
607
|
-
}
|
|
608
|
-
if (details.hasSolution) {
|
|
609
|
-
console.log(` Solution: detected`);
|
|
610
|
-
}
|
|
611
|
-
break;
|
|
612
|
-
|
|
613
|
-
case 'flutter':
|
|
614
|
-
if (details.name) {
|
|
615
|
-
console.log(` App: ${details.name}@${details.version || '?'}`);
|
|
616
|
-
}
|
|
617
|
-
break;
|
|
618
|
-
|
|
619
|
-
case 'react-native':
|
|
620
|
-
if (details.name) {
|
|
621
|
-
console.log(` App: ${details.name}@${details.version || '?'}`);
|
|
622
|
-
}
|
|
623
|
-
if (details.reactNativeVersion) {
|
|
624
|
-
console.log(` React Native: ${details.reactNativeVersion}`);
|
|
625
|
-
}
|
|
626
|
-
break;
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
if (detection.allDetections && detection.allDetections.length > 1) {
|
|
631
|
-
console.log(` Other possibilities: ${detection.allDetections.slice(1).map(d => d.type).join(', ')}`);
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
console.log('');
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
/**
|
|
638
|
-
* Parses YAML-like content from ENVIRONMENT.md
|
|
639
|
-
* @param {string} content - The raw content of ENVIRONMENT.md
|
|
640
|
-
* @returns {object} Parsed key-value pairs
|
|
641
|
-
*/
|
|
642
|
-
function parseEnvironmentYaml(content) {
|
|
643
|
-
const result = {};
|
|
644
|
-
const lines = content.split('\n');
|
|
645
|
-
|
|
646
|
-
for (const line of lines) {
|
|
647
|
-
const trimmed = line.trim();
|
|
648
|
-
if (trimmed && !trimmed.startsWith('#') && trimmed.includes(':')) {
|
|
649
|
-
const [key, ...valueParts] = trimmed.split(':');
|
|
650
|
-
const value = valueParts.join(':').trim();
|
|
651
|
-
|
|
652
|
-
// Remove quotes if present
|
|
653
|
-
const cleanValue = value.replace(/^["']|["']$/g, '');
|
|
654
|
-
result[key.trim()] = cleanValue;
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
return result;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
/**
|
|
662
|
-
* Loads and processes the .eck directory manifest
|
|
663
|
-
* @param {string} repoPath - Path to the repository
|
|
664
|
-
* @returns {Promise<object|null>} The eck manifest object or null if no .eck directory
|
|
665
|
-
*/
|
|
666
|
-
export async function loadProjectEckManifest(repoPath) {
|
|
667
|
-
const eckDir = path.join(repoPath, '.eck');
|
|
668
|
-
|
|
669
|
-
try {
|
|
670
|
-
// Check if .eck directory exists
|
|
671
|
-
const eckStats = await fs.stat(eckDir);
|
|
672
|
-
if (!eckStats.isDirectory()) {
|
|
673
|
-
return null;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
console.log('📋 Found .eck directory - loading project manifest...');
|
|
677
|
-
|
|
678
|
-
const manifest = {
|
|
679
|
-
environment: {},
|
|
680
|
-
context: '',
|
|
681
|
-
operations: '',
|
|
682
|
-
journal: '',
|
|
683
|
-
roadmap: '',
|
|
684
|
-
techDebt: ''
|
|
685
|
-
};
|
|
686
|
-
|
|
687
|
-
// Define the files to check
|
|
688
|
-
const files = [
|
|
689
|
-
{ name: 'ENVIRONMENT.md', key: 'environment', parser: parseEnvironmentYaml },
|
|
690
|
-
{ name: 'CONTEXT.md', key: 'context', parser: content => content },
|
|
691
|
-
{ name: 'OPERATIONS.md', key: 'operations', parser: content => content },
|
|
692
|
-
{ name: 'JOURNAL.md', key: 'journal', parser: content => content },
|
|
693
|
-
{ name: 'ROADMAP.md', key: 'roadmap', parser: content => content },
|
|
694
|
-
{ name: 'TECH_DEBT.md', key: 'techDebt', parser: content => content }
|
|
695
|
-
];
|
|
696
|
-
|
|
697
|
-
// Process each file
|
|
698
|
-
for (const file of files) {
|
|
699
|
-
const filePath = path.join(eckDir, file.name);
|
|
700
|
-
try {
|
|
701
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
702
|
-
manifest[file.key] = file.parser(content.trim());
|
|
703
|
-
console.log(` ✅ Loaded ${file.name}`);
|
|
704
|
-
} catch (error) {
|
|
705
|
-
// File doesn't exist or can't be read - that's okay, use default
|
|
706
|
-
console.log(` ⚠️ ${file.name} not found or unreadable`);
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
return manifest;
|
|
711
|
-
} catch (error) {
|
|
712
|
-
// .eck directory doesn't exist - that's normal
|
|
713
|
-
return null;
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
/**
|
|
718
|
-
* Ensures that 'snapshots/' is added to the target project's .gitignore file
|
|
719
|
-
* @param {string} repoPath - Path to the repository
|
|
720
|
-
*/
|
|
721
|
-
export async function ensureSnapshotsInGitignore(repoPath) {
|
|
722
|
-
const gitignorePath = path.join(repoPath, '.gitignore');
|
|
723
|
-
const entryToAdd = '.eck/';
|
|
724
|
-
const comment = '# Added by eck-snapshot to ignore metadata directory';
|
|
725
|
-
|
|
726
|
-
try {
|
|
727
|
-
// Check if the repo is a Git repository first
|
|
728
|
-
const isGitRepo = await checkGitRepository(repoPath);
|
|
729
|
-
if (!isGitRepo) {
|
|
730
|
-
// Not a Git repo, skip .gitignore modification
|
|
731
|
-
return;
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
let gitignoreContent = '';
|
|
735
|
-
let fileExists = true;
|
|
736
|
-
|
|
737
|
-
// Try to read existing .gitignore file
|
|
738
|
-
try {
|
|
739
|
-
gitignoreContent = await fs.readFile(gitignorePath, 'utf-8');
|
|
740
|
-
} catch (error) {
|
|
741
|
-
// File doesn't exist, we'll create it
|
|
742
|
-
fileExists = false;
|
|
743
|
-
gitignoreContent = '';
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
// Check if 'snapshots/' is already in the file
|
|
747
|
-
const lines = gitignoreContent.split('\n');
|
|
748
|
-
const hasSnapshotsEntry = lines.some(line => line.trim() === entryToAdd);
|
|
749
|
-
|
|
750
|
-
if (!hasSnapshotsEntry) {
|
|
751
|
-
// Add the entry
|
|
752
|
-
let newContent = gitignoreContent;
|
|
753
|
-
|
|
754
|
-
// If file exists and doesn't end with newline, add one
|
|
755
|
-
if (fileExists && gitignoreContent && !gitignoreContent.endsWith('\n')) {
|
|
756
|
-
newContent += '\n';
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
// Add comment and entry
|
|
760
|
-
if (fileExists && gitignoreContent) {
|
|
761
|
-
newContent += '\n';
|
|
762
|
-
}
|
|
763
|
-
newContent += comment + '\n' + entryToAdd + '\n';
|
|
764
|
-
|
|
765
|
-
await fs.writeFile(gitignorePath, newContent);
|
|
766
|
-
console.log(`✅ Added '${entryToAdd}' to .gitignore`);
|
|
767
|
-
}
|
|
768
|
-
} catch (error) {
|
|
769
|
-
// Silently fail - don't break the snapshot process if gitignore update fails
|
|
770
|
-
console.warn(`⚠️ Warning: Could not update .gitignore: ${error.message}`);
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
// Helper function to determine if a string is a glob pattern
|
|
775
|
-
function isGlob(str) {
|
|
776
|
-
return str.includes('*') || str.includes('?') || str.includes('{');
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
/**
|
|
780
|
-
* Applies advanced profile filtering (multi-profile, exclusion, and ad-hoc globs) to a file list.
|
|
781
|
-
*/
|
|
782
|
-
export async function applyProfileFilter(allFiles, profileString, repoPath) {
|
|
783
|
-
const profileParts = profileString.split(',').map(p => p.trim()).filter(Boolean);
|
|
784
|
-
|
|
785
|
-
const includeGlobs = [];
|
|
786
|
-
const excludeGlobs = [];
|
|
787
|
-
const includeNames = [];
|
|
788
|
-
const excludeNames = [];
|
|
789
|
-
|
|
790
|
-
// Step 1: Differentiate between profile names and ad-hoc glob patterns
|
|
791
|
-
for (const part of profileParts) {
|
|
792
|
-
const isNegative = part.startsWith('-');
|
|
793
|
-
const pattern = isNegative ? part.substring(1) : part;
|
|
794
|
-
|
|
795
|
-
if (isGlob(pattern)) {
|
|
796
|
-
if (isNegative) {
|
|
797
|
-
excludeGlobs.push(pattern);
|
|
798
|
-
} else {
|
|
799
|
-
includeGlobs.push(pattern);
|
|
800
|
-
}
|
|
801
|
-
} else {
|
|
802
|
-
if (isNegative) {
|
|
803
|
-
excludeNames.push(pattern);
|
|
804
|
-
} else {
|
|
805
|
-
includeNames.push(pattern);
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
let workingFiles = [];
|
|
811
|
-
let finalIncludes = [...includeGlobs];
|
|
812
|
-
let finalExcludes = [...excludeGlobs];
|
|
813
|
-
|
|
814
|
-
// Step 2: Load patterns from specified profile names
|
|
815
|
-
const allProfileNames = [...new Set([...includeNames, ...excludeNames])];
|
|
816
|
-
const profiles = new Map();
|
|
817
|
-
const notFoundProfiles = [];
|
|
818
|
-
|
|
819
|
-
for (const name of allProfileNames) {
|
|
820
|
-
const profile = await getProfile(name, repoPath);
|
|
821
|
-
if (profile) {
|
|
822
|
-
profiles.set(name, profile);
|
|
823
|
-
} else {
|
|
824
|
-
// This is an ad-hoc glob, not a profile, so no warning is needed.
|
|
825
|
-
if (!isGlob(name)) {
|
|
826
|
-
notFoundProfiles.push(name);
|
|
827
|
-
console.warn(`⚠️ Warning: Profile '${name}' not found and will be skipped.`);
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
for (const name of includeNames) {
|
|
833
|
-
if (profiles.has(name)) {
|
|
834
|
-
finalIncludes.push(...(profiles.get(name).include || []));
|
|
835
|
-
finalExcludes.push(...(profiles.get(name).exclude || []));
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
for (const name of excludeNames) {
|
|
839
|
-
if (profiles.has(name)) {
|
|
840
|
-
finalExcludes.push(...(profiles.get(name).include || []));
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
// Step 3: Apply the filtering logic
|
|
845
|
-
if (finalIncludes.length > 0) {
|
|
846
|
-
workingFiles = micromatch(allFiles, finalIncludes);
|
|
847
|
-
} else if (includeNames.length > 0 && includeGlobs.length === 0) {
|
|
848
|
-
workingFiles = [];
|
|
849
|
-
} else {
|
|
850
|
-
workingFiles = allFiles;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
if (finalExcludes.length > 0) {
|
|
854
|
-
workingFiles = micromatch.not(workingFiles, finalExcludes);
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
return {
|
|
858
|
-
files: workingFiles,
|
|
859
|
-
notFoundProfiles,
|
|
860
|
-
foundProfiles: Array.from(profiles.keys())
|
|
861
|
-
};
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
/**
|
|
865
|
-
* Automatically initializes the .eck manifest directory, attempting dynamic generation via Claude.
|
|
866
|
-
* @param {string} projectPath - Path to the project
|
|
867
|
-
*/
|
|
868
|
-
export async function initializeEckManifest(projectPath) {
|
|
869
|
-
const eckDir = path.join(projectPath, '.eck');
|
|
870
|
-
|
|
94
|
+
|
|
95
|
+
export function parseSize(sizeStr) {
|
|
96
|
+
const units = { B: 1, KB: 1024, MB: 1024 ** 2, GB: 1024 ** 3 };
|
|
97
|
+
const match = sizeStr.match(/^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB)?$/i);
|
|
98
|
+
if (!match) throw new Error(`Invalid size format: ${sizeStr}`);
|
|
99
|
+
const [, size, unit = 'B'] = match;
|
|
100
|
+
return Math.floor(parseFloat(size) * units[unit.toUpperCase()]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function formatSize(bytes) {
|
|
104
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
105
|
+
let size = bytes;
|
|
106
|
+
let unitIndex = 0;
|
|
107
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
108
|
+
size /= 1024;
|
|
109
|
+
unitIndex++;
|
|
110
|
+
}
|
|
111
|
+
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function matchesPattern(filePath, patterns) {
|
|
115
|
+
const fileName = path.basename(filePath);
|
|
116
|
+
return patterns.some(pattern => {
|
|
117
|
+
const regexPattern = '^' + pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$';
|
|
118
|
+
try {
|
|
119
|
+
const regex = new RegExp(regexPattern);
|
|
120
|
+
return regex.test(fileName);
|
|
121
|
+
} catch (e) {
|
|
122
|
+
console.warn(`⚠️ Invalid regex pattern in config: "${pattern}"`);
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Checks if a file matches confidential patterns using minimatch
|
|
130
|
+
* @param {string} fileName - The file name to check
|
|
131
|
+
* @param {array} patterns - Array of glob patterns to match against
|
|
132
|
+
* @returns {boolean} True if the file matches any pattern
|
|
133
|
+
*/
|
|
134
|
+
function matchesConfidentialPattern(fileName, patterns) {
|
|
135
|
+
return patterns.some(pattern => minimatch(fileName, pattern, { nocase: true }));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Applies smart filtering for files within the .eck directory.
|
|
140
|
+
* Includes documentation files while excluding confidential files.
|
|
141
|
+
* @param {string} fileName - The file name to check
|
|
142
|
+
* @param {object} eckConfig - The eckDirectoryFiltering config object
|
|
143
|
+
* @returns {object} { include: boolean, isConfidential: boolean }
|
|
144
|
+
*/
|
|
145
|
+
export function applyEckDirectoryFiltering(fileName, eckConfig) {
|
|
146
|
+
if (!eckConfig || !eckConfig.enabled) {
|
|
147
|
+
return { include: false, isConfidential: false }; // .eck filtering disabled, exclude all
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const { confidentialPatterns = [], alwaysIncludePatterns = [] } = eckConfig;
|
|
151
|
+
|
|
152
|
+
// First check if file matches confidential patterns
|
|
153
|
+
const isConfidential = matchesConfidentialPattern(fileName, confidentialPatterns);
|
|
154
|
+
if (isConfidential) {
|
|
155
|
+
return { include: false, isConfidential: true };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Check if file matches always-include patterns
|
|
159
|
+
if (matchesPattern(fileName, alwaysIncludePatterns)) {
|
|
160
|
+
return { include: true, isConfidential: false };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Default: exclude files not in the include list
|
|
164
|
+
return { include: false, isConfidential: false };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function checkGitRepository(repoPath) {
|
|
168
|
+
try {
|
|
169
|
+
await execa('git', ['rev-parse', '--git-dir'], { cwd: repoPath });
|
|
170
|
+
return true;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function scanDirectoryRecursively(dirPath, config, relativeTo = dirPath, projectType = null, trackConfidential = false) {
|
|
177
|
+
const files = [];
|
|
178
|
+
const confidentialFiles = [];
|
|
179
|
+
|
|
180
|
+
// Get project-specific filtering if not provided
|
|
181
|
+
if (!projectType) {
|
|
182
|
+
const detection = await detectProjectType(relativeTo);
|
|
183
|
+
projectType = detection.type;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const projectSpecific = await getProjectSpecificFiltering(projectType);
|
|
187
|
+
|
|
188
|
+
// Merge project-specific filters with global config
|
|
189
|
+
const effectiveConfig = {
|
|
190
|
+
...config,
|
|
191
|
+
dirsToIgnore: [...(config.dirsToIgnore || []), ...(projectSpecific.dirsToIgnore || [])],
|
|
192
|
+
filesToIgnore: [...(config.filesToIgnore || []), ...(projectSpecific.filesToIgnore || [])],
|
|
193
|
+
extensionsToIgnore: [...(config.extensionsToIgnore || []), ...(projectSpecific.extensionsToIgnore || [])]
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
198
|
+
|
|
199
|
+
for (const entry of entries) {
|
|
200
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
201
|
+
const relativePath = path.relative(relativeTo, fullPath).replace(/\\/g, '/');
|
|
202
|
+
|
|
203
|
+
// --- GLOBAL HARD IGNORES (Zero-Config Safety) ---
|
|
204
|
+
// Explicitly skip heavy/system directories and lockfiles everywhere
|
|
205
|
+
if (entry.isDirectory()) {
|
|
206
|
+
if (entry.name === 'node_modules' ||
|
|
207
|
+
entry.name === '.git' ||
|
|
208
|
+
entry.name === '.idea' ||
|
|
209
|
+
entry.name === '.vscode') {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
if (entry.name === 'package-lock.json' ||
|
|
214
|
+
entry.name === 'yarn.lock' ||
|
|
215
|
+
entry.name === 'pnpm-lock.yaml' ||
|
|
216
|
+
entry.name === 'go.sum') {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// -----------------------------------------------
|
|
221
|
+
|
|
222
|
+
// Special handling for .eck directory - never ignore it when tracking confidential files
|
|
223
|
+
const isEckDirectory = entry.name === '.eck' && entry.isDirectory();
|
|
224
|
+
const isInsideEck = relativePath.startsWith('.eck/');
|
|
225
|
+
|
|
226
|
+
if (effectiveConfig.dirsToIgnore.some(dir =>
|
|
227
|
+
entry.name === dir.replace('/', '') ||
|
|
228
|
+
relativePath.startsWith(dir)
|
|
229
|
+
) && !isEckDirectory && !isInsideEck) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!effectiveConfig.includeHidden && entry.name.startsWith('.') && !isEckDirectory && !isInsideEck) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (entry.isDirectory()) {
|
|
238
|
+
const subResult = await scanDirectoryRecursively(fullPath, effectiveConfig, relativeTo, projectType, trackConfidential);
|
|
239
|
+
if (trackConfidential) {
|
|
240
|
+
files.push(...subResult.files);
|
|
241
|
+
confidentialFiles.push(...subResult.confidentialFiles);
|
|
242
|
+
} else {
|
|
243
|
+
files.push(...subResult);
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
// Apply smart filtering for files inside .eck directory
|
|
247
|
+
if (isInsideEck) {
|
|
248
|
+
const eckConfig = effectiveConfig.eckDirectoryFiltering;
|
|
249
|
+
const filterResult = applyEckDirectoryFiltering(entry.name, eckConfig);
|
|
250
|
+
|
|
251
|
+
if (trackConfidential && filterResult.isConfidential) {
|
|
252
|
+
confidentialFiles.push(relativePath);
|
|
253
|
+
} else if (filterResult.include) {
|
|
254
|
+
files.push(relativePath);
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
// Normal filtering for non-.eck files
|
|
258
|
+
if (effectiveConfig.extensionsToIgnore.includes(path.extname(entry.name)) ||
|
|
259
|
+
matchesPattern(relativePath, effectiveConfig.filesToIgnore)) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
files.push(relativePath);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
} catch (error) {
|
|
267
|
+
console.warn(`⚠️ Warning: Could not read directory: ${dirPath} - ${error.message}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return trackConfidential ? { files, confidentialFiles } : files;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export async function loadGitignore(repoPath) {
|
|
274
|
+
try {
|
|
275
|
+
const gitignoreContent = await fs.readFile(path.join(repoPath, '.gitignore'), 'utf-8');
|
|
276
|
+
const ig = ignore().add(gitignoreContent);
|
|
277
|
+
return ig;
|
|
278
|
+
} catch {
|
|
279
|
+
console.log('ℹ️ No .gitignore file found or could not be read');
|
|
280
|
+
return ignore();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export async function readFileWithSizeCheck(filePath, maxFileSize) {
|
|
285
|
+
try {
|
|
286
|
+
const stats = await fs.stat(filePath);
|
|
287
|
+
if (stats.size > maxFileSize) {
|
|
288
|
+
throw new Error(`File too large: ${formatSize(stats.size)}`);
|
|
289
|
+
}
|
|
290
|
+
return await fs.readFile(filePath, 'utf-8');
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (error.message.includes('too large')) throw error;
|
|
293
|
+
throw new Error(`Could not read file: ${error.message}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export async function generateDirectoryTree(dir, prefix = '', allFiles, depth = 0, maxDepth = 10, config) {
|
|
298
|
+
if (depth > maxDepth) return '';
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
302
|
+
const sortedEntries = entries.sort((a, b) => {
|
|
303
|
+
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
304
|
+
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
305
|
+
return a.name.localeCompare(b.name);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
let tree = '';
|
|
309
|
+
const validEntries = [];
|
|
310
|
+
|
|
311
|
+
for (const entry of sortedEntries) {
|
|
312
|
+
// Skip hidden directories and files (starting with '.')
|
|
313
|
+
// EXCEPT: Allow .eck to be visible
|
|
314
|
+
if (entry.name.startsWith('.') && entry.name !== '.eck') {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (config.dirsToIgnore.some(d => entry.name.includes(d.replace('/', '')))) continue;
|
|
318
|
+
const fullPath = path.join(dir, entry.name);
|
|
319
|
+
const relativePath = path.relative(process.cwd(), fullPath).replace(/\\/g, '/');
|
|
320
|
+
|
|
321
|
+
// FORCE VISIBILITY for .eck files in the tree
|
|
322
|
+
// Even if they are gitignored (not in allFiles), we want the Architect to see they exist
|
|
323
|
+
const isInsideEck = relativePath.startsWith('.eck/') || relativePath === '.eck';
|
|
324
|
+
|
|
325
|
+
if (entry.isDirectory() || allFiles.includes(relativePath) || isInsideEck) {
|
|
326
|
+
validEntries.push({ entry, fullPath, relativePath });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
for (let i = 0; i < validEntries.length; i++) {
|
|
331
|
+
const { entry, fullPath, relativePath } = validEntries[i];
|
|
332
|
+
const isLast = i === validEntries.length - 1;
|
|
333
|
+
|
|
334
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
335
|
+
const nextPrefix = prefix + (isLast ? ' ' : '│ ');
|
|
336
|
+
|
|
337
|
+
if (entry.isDirectory()) {
|
|
338
|
+
tree += `${prefix}${connector}${entry.name}/\n`;
|
|
339
|
+
|
|
340
|
+
// RECURSION CONTROL:
|
|
341
|
+
// If we are currently inside .eck, do NOT recurse deeper into subdirectories (like snapshots, logs).
|
|
342
|
+
// We want to see that 'snapshots/' exists, but not list its contents.
|
|
343
|
+
const isInsideEckRoot = path.basename(dir) === '.eck';
|
|
344
|
+
|
|
345
|
+
if (!isInsideEckRoot) {
|
|
346
|
+
tree += await generateDirectoryTree(fullPath, nextPrefix, allFiles, depth + 1, maxDepth, config);
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
tree += `${prefix}${connector}${entry.name}\n`;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return tree;
|
|
354
|
+
} catch (error) {
|
|
355
|
+
console.warn(`⚠️ Warning: Could not read directory: ${dir}`);
|
|
356
|
+
return '';
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function parseSnapshotContent(content) {
|
|
361
|
+
const files = [];
|
|
362
|
+
const fileRegex = /--- File: \/(.+) ---/g;
|
|
363
|
+
const sections = content.split(fileRegex);
|
|
364
|
+
|
|
365
|
+
for (let i = 1; i < sections.length; i += 2) {
|
|
366
|
+
const filePath = sections[i].trim();
|
|
367
|
+
let fileContent = sections[i + 1] || '';
|
|
368
|
+
|
|
369
|
+
if (fileContent.startsWith('\n\n')) {
|
|
370
|
+
fileContent = fileContent.substring(2);
|
|
371
|
+
}
|
|
372
|
+
if (fileContent.endsWith('\n\n')) {
|
|
373
|
+
fileContent = fileContent.substring(0, fileContent.length - 2);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
files.push({ path: filePath, content: fileContent });
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return files;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function filterFilesToRestore(files, options) {
|
|
383
|
+
let filtered = files;
|
|
384
|
+
|
|
385
|
+
if (options.include) {
|
|
386
|
+
const includePatterns = Array.isArray(options.include) ?
|
|
387
|
+
options.include : [options.include];
|
|
388
|
+
filtered = filtered.filter(file =>
|
|
389
|
+
includePatterns.some(pattern => {
|
|
390
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
391
|
+
return regex.test(file.path);
|
|
392
|
+
})
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (options.exclude) {
|
|
397
|
+
const excludePatterns = Array.isArray(options.exclude) ?
|
|
398
|
+
options.exclude : [options.exclude];
|
|
399
|
+
filtered = filtered.filter(file =>
|
|
400
|
+
!excludePatterns.some(pattern => {
|
|
401
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
402
|
+
return regex.test(file.path);
|
|
403
|
+
})
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return filtered;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function validateFilePaths(files, targetDir) {
|
|
411
|
+
const invalidFiles = [];
|
|
412
|
+
|
|
413
|
+
for (const file of files) {
|
|
414
|
+
const normalizedPath = path.normalize(file.path);
|
|
415
|
+
if (normalizedPath.includes('..') ||
|
|
416
|
+
normalizedPath.startsWith('/') ||
|
|
417
|
+
normalizedPath.includes('\0') ||
|
|
418
|
+
/[<>:"|?*]/.test(normalizedPath)) {
|
|
419
|
+
invalidFiles.push(file.path);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return invalidFiles;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export async function loadConfig(configPath) {
|
|
427
|
+
const { DEFAULT_CONFIG } = await import('../config.js');
|
|
428
|
+
let config = { ...DEFAULT_CONFIG };
|
|
429
|
+
|
|
430
|
+
if (configPath) {
|
|
431
|
+
try {
|
|
432
|
+
const configModule = await import(path.resolve(configPath));
|
|
433
|
+
config = { ...config, ...configModule.default };
|
|
434
|
+
console.log(`✅ Configuration loaded from: ${configPath}`);
|
|
435
|
+
} catch (error) {
|
|
436
|
+
console.warn(`⚠️ Warning: Could not load config file: ${configPath}`);
|
|
437
|
+
}
|
|
438
|
+
} else {
|
|
439
|
+
const possibleConfigs = [
|
|
440
|
+
'.ecksnapshot.config.js',
|
|
441
|
+
'.ecksnapshot.config.mjs',
|
|
442
|
+
'ecksnapshot.config.js'
|
|
443
|
+
];
|
|
444
|
+
|
|
445
|
+
for (const configFile of possibleConfigs) {
|
|
446
|
+
try {
|
|
447
|
+
await fs.access(configFile);
|
|
448
|
+
const configModule = await import(path.resolve(configFile));
|
|
449
|
+
config = { ...config, ...configModule.default };
|
|
450
|
+
console.log(`✅ Configuration loaded from: ${configFile}`);
|
|
451
|
+
break;
|
|
452
|
+
} catch {
|
|
453
|
+
// Config file doesn't exist, continue
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return config;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export function generateTimestamp() {
|
|
462
|
+
const now = new Date();
|
|
463
|
+
const YY = String(now.getFullYear()).slice(-2);
|
|
464
|
+
const MM = String(now.getMonth() + 1).padStart(2, '0');
|
|
465
|
+
const DD = String(now.getDate()).padStart(2, '0');
|
|
466
|
+
const hh = String(now.getHours()).padStart(2, '0');
|
|
467
|
+
const mm = String(now.getMinutes()).padStart(2, '0');
|
|
468
|
+
// Compact format: YY-MM-DD_HH-mm (no seconds)
|
|
469
|
+
return `${YY}-${MM}-${DD}_${hh}-${mm}`;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Generates a short repo name with capitalized first 3 and last 2 characters
|
|
474
|
+
* Example: "Snapshot" -> "SnaOt", "MyProject" -> "MyPrjt"
|
|
475
|
+
* @param {string} repoName - The repository name
|
|
476
|
+
* @returns {string} Shortened repo name (Start3 + End2)
|
|
477
|
+
*/
|
|
478
|
+
export function getShortRepoName(repoName) {
|
|
479
|
+
if (!repoName) return '';
|
|
480
|
+
if (repoName.length <= 5) {
|
|
481
|
+
return repoName.toUpperCase();
|
|
482
|
+
}
|
|
483
|
+
const start = repoName.substring(0, 3);
|
|
484
|
+
const end = repoName.substring(repoName.length - 2);
|
|
485
|
+
return (start + end).toUpperCase();
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Displays project detection information in a user-friendly format
|
|
490
|
+
* @param {object} detection - Project detection result
|
|
491
|
+
*/
|
|
492
|
+
export function displayProjectInfo(detection) {
|
|
493
|
+
console.log('\n🔍 Project Detection Results:');
|
|
494
|
+
console.log(` Type: ${detection.type} (confidence: ${(detection.confidence * 100).toFixed(0)}%)`);
|
|
495
|
+
|
|
496
|
+
if (detection.details) {
|
|
497
|
+
const details = detection.details;
|
|
498
|
+
|
|
499
|
+
switch (detection.type) {
|
|
500
|
+
case 'android':
|
|
501
|
+
console.log(` Language: ${details.language || 'unknown'}`);
|
|
502
|
+
if (details.packageName) {
|
|
503
|
+
console.log(` Package: ${details.packageName}`);
|
|
504
|
+
}
|
|
505
|
+
if (details.sourceDirs && details.sourceDirs.length > 0) {
|
|
506
|
+
console.log(` Source dirs: ${details.sourceDirs.join(', ')}`);
|
|
507
|
+
}
|
|
508
|
+
if (details.libFiles && details.libFiles.length > 0) {
|
|
509
|
+
console.log(` Libraries: ${details.libFiles.length} .aar/.jar files`);
|
|
510
|
+
}
|
|
511
|
+
break;
|
|
512
|
+
|
|
513
|
+
case 'nodejs':
|
|
514
|
+
if (details.name) {
|
|
515
|
+
console.log(` Package: ${details.name}@${details.version || '?'}`);
|
|
516
|
+
}
|
|
517
|
+
if (details.framework) {
|
|
518
|
+
console.log(` Framework: ${details.framework}`);
|
|
519
|
+
}
|
|
520
|
+
if (details.hasTypescript) {
|
|
521
|
+
console.log(` TypeScript: enabled`);
|
|
522
|
+
}
|
|
523
|
+
break;
|
|
524
|
+
|
|
525
|
+
case 'nodejs-monorepo':
|
|
526
|
+
if (details.name) {
|
|
527
|
+
console.log(` Project: ${details.name}@${details.version || '?'}`);
|
|
528
|
+
}
|
|
529
|
+
if (details.monorepoTool) {
|
|
530
|
+
console.log(` Monorepo tool: ${details.monorepoTool}`);
|
|
531
|
+
}
|
|
532
|
+
if (details.workspaceCount) {
|
|
533
|
+
console.log(` Workspaces: ${details.workspaceCount}`);
|
|
534
|
+
}
|
|
535
|
+
if (details.framework) {
|
|
536
|
+
console.log(` Framework: ${details.framework}`);
|
|
537
|
+
}
|
|
538
|
+
break;
|
|
539
|
+
|
|
540
|
+
case 'python-poetry':
|
|
541
|
+
case 'python-pip':
|
|
542
|
+
case 'python-conda':
|
|
543
|
+
if (details.name) {
|
|
544
|
+
console.log(` Project: ${details.name}@${details.version || '?'}`);
|
|
545
|
+
}
|
|
546
|
+
if (details.packageManager) {
|
|
547
|
+
console.log(` Package manager: ${details.packageManager}`);
|
|
548
|
+
}
|
|
549
|
+
if (details.dependencies) {
|
|
550
|
+
console.log(` Dependencies: ${details.dependencies}`);
|
|
551
|
+
}
|
|
552
|
+
if (details.hasVirtualEnv) {
|
|
553
|
+
console.log(` Virtual environment: detected`);
|
|
554
|
+
}
|
|
555
|
+
break;
|
|
556
|
+
|
|
557
|
+
case 'django':
|
|
558
|
+
if (details.name) {
|
|
559
|
+
console.log(` Project: ${details.name}`);
|
|
560
|
+
}
|
|
561
|
+
console.log(` Framework: Django`);
|
|
562
|
+
if (details.djangoApps && details.djangoApps.length > 0) {
|
|
563
|
+
console.log(` Django apps: ${details.djangoApps.join(', ')}`);
|
|
564
|
+
}
|
|
565
|
+
if (details.hasVirtualEnv) {
|
|
566
|
+
console.log(` Virtual environment: detected`);
|
|
567
|
+
}
|
|
568
|
+
break;
|
|
569
|
+
|
|
570
|
+
case 'flask':
|
|
571
|
+
if (details.name) {
|
|
572
|
+
console.log(` Project: ${details.name}`);
|
|
573
|
+
}
|
|
574
|
+
console.log(` Framework: Flask`);
|
|
575
|
+
if (details.hasVirtualEnv) {
|
|
576
|
+
console.log(` Virtual environment: detected`);
|
|
577
|
+
}
|
|
578
|
+
break;
|
|
579
|
+
|
|
580
|
+
case 'rust':
|
|
581
|
+
if (details.name) {
|
|
582
|
+
console.log(` Package: ${details.name}@${details.version || '?'}`);
|
|
583
|
+
}
|
|
584
|
+
if (details.edition) {
|
|
585
|
+
console.log(` Rust edition: ${details.edition}`);
|
|
586
|
+
}
|
|
587
|
+
if (details.isWorkspace) {
|
|
588
|
+
console.log(` Cargo workspace: detected`);
|
|
589
|
+
}
|
|
590
|
+
break;
|
|
591
|
+
|
|
592
|
+
case 'go':
|
|
593
|
+
if (details.module) {
|
|
594
|
+
console.log(` Module: ${details.module}`);
|
|
595
|
+
}
|
|
596
|
+
if (details.goVersion) {
|
|
597
|
+
console.log(` Go version: ${details.goVersion}`);
|
|
598
|
+
}
|
|
599
|
+
break;
|
|
600
|
+
|
|
601
|
+
case 'dotnet':
|
|
602
|
+
if (details.language) {
|
|
603
|
+
console.log(` Language: ${details.language}`);
|
|
604
|
+
}
|
|
605
|
+
if (details.projectFiles && details.projectFiles.length > 0) {
|
|
606
|
+
console.log(` Project files: ${details.projectFiles.join(', ')}`);
|
|
607
|
+
}
|
|
608
|
+
if (details.hasSolution) {
|
|
609
|
+
console.log(` Solution: detected`);
|
|
610
|
+
}
|
|
611
|
+
break;
|
|
612
|
+
|
|
613
|
+
case 'flutter':
|
|
614
|
+
if (details.name) {
|
|
615
|
+
console.log(` App: ${details.name}@${details.version || '?'}`);
|
|
616
|
+
}
|
|
617
|
+
break;
|
|
618
|
+
|
|
619
|
+
case 'react-native':
|
|
620
|
+
if (details.name) {
|
|
621
|
+
console.log(` App: ${details.name}@${details.version || '?'}`);
|
|
622
|
+
}
|
|
623
|
+
if (details.reactNativeVersion) {
|
|
624
|
+
console.log(` React Native: ${details.reactNativeVersion}`);
|
|
625
|
+
}
|
|
626
|
+
break;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (detection.allDetections && detection.allDetections.length > 1) {
|
|
631
|
+
console.log(` Other possibilities: ${detection.allDetections.slice(1).map(d => d.type).join(', ')}`);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
console.log('');
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Parses YAML-like content from ENVIRONMENT.md
|
|
639
|
+
* @param {string} content - The raw content of ENVIRONMENT.md
|
|
640
|
+
* @returns {object} Parsed key-value pairs
|
|
641
|
+
*/
|
|
642
|
+
function parseEnvironmentYaml(content) {
|
|
643
|
+
const result = {};
|
|
644
|
+
const lines = content.split('\n');
|
|
645
|
+
|
|
646
|
+
for (const line of lines) {
|
|
647
|
+
const trimmed = line.trim();
|
|
648
|
+
if (trimmed && !trimmed.startsWith('#') && trimmed.includes(':')) {
|
|
649
|
+
const [key, ...valueParts] = trimmed.split(':');
|
|
650
|
+
const value = valueParts.join(':').trim();
|
|
651
|
+
|
|
652
|
+
// Remove quotes if present
|
|
653
|
+
const cleanValue = value.replace(/^["']|["']$/g, '');
|
|
654
|
+
result[key.trim()] = cleanValue;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return result;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Loads and processes the .eck directory manifest
|
|
663
|
+
* @param {string} repoPath - Path to the repository
|
|
664
|
+
* @returns {Promise<object|null>} The eck manifest object or null if no .eck directory
|
|
665
|
+
*/
|
|
666
|
+
export async function loadProjectEckManifest(repoPath) {
|
|
667
|
+
const eckDir = path.join(repoPath, '.eck');
|
|
668
|
+
|
|
669
|
+
try {
|
|
670
|
+
// Check if .eck directory exists
|
|
671
|
+
const eckStats = await fs.stat(eckDir);
|
|
672
|
+
if (!eckStats.isDirectory()) {
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
console.log('📋 Found .eck directory - loading project manifest...');
|
|
677
|
+
|
|
678
|
+
const manifest = {
|
|
679
|
+
environment: {},
|
|
680
|
+
context: '',
|
|
681
|
+
operations: '',
|
|
682
|
+
journal: '',
|
|
683
|
+
roadmap: '',
|
|
684
|
+
techDebt: ''
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
// Define the files to check
|
|
688
|
+
const files = [
|
|
689
|
+
{ name: 'ENVIRONMENT.md', key: 'environment', parser: parseEnvironmentYaml },
|
|
690
|
+
{ name: 'CONTEXT.md', key: 'context', parser: content => content },
|
|
691
|
+
{ name: 'OPERATIONS.md', key: 'operations', parser: content => content },
|
|
692
|
+
{ name: 'JOURNAL.md', key: 'journal', parser: content => content },
|
|
693
|
+
{ name: 'ROADMAP.md', key: 'roadmap', parser: content => content },
|
|
694
|
+
{ name: 'TECH_DEBT.md', key: 'techDebt', parser: content => content }
|
|
695
|
+
];
|
|
696
|
+
|
|
697
|
+
// Process each file
|
|
698
|
+
for (const file of files) {
|
|
699
|
+
const filePath = path.join(eckDir, file.name);
|
|
700
|
+
try {
|
|
701
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
702
|
+
manifest[file.key] = file.parser(content.trim());
|
|
703
|
+
console.log(` ✅ Loaded ${file.name}`);
|
|
704
|
+
} catch (error) {
|
|
705
|
+
// File doesn't exist or can't be read - that's okay, use default
|
|
706
|
+
console.log(` ⚠️ ${file.name} not found or unreadable`);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return manifest;
|
|
711
|
+
} catch (error) {
|
|
712
|
+
// .eck directory doesn't exist - that's normal
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Ensures that 'snapshots/' is added to the target project's .gitignore file
|
|
719
|
+
* @param {string} repoPath - Path to the repository
|
|
720
|
+
*/
|
|
721
|
+
export async function ensureSnapshotsInGitignore(repoPath) {
|
|
722
|
+
const gitignorePath = path.join(repoPath, '.gitignore');
|
|
723
|
+
const entryToAdd = '.eck/';
|
|
724
|
+
const comment = '# Added by eck-snapshot to ignore metadata directory';
|
|
725
|
+
|
|
726
|
+
try {
|
|
727
|
+
// Check if the repo is a Git repository first
|
|
728
|
+
const isGitRepo = await checkGitRepository(repoPath);
|
|
729
|
+
if (!isGitRepo) {
|
|
730
|
+
// Not a Git repo, skip .gitignore modification
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
let gitignoreContent = '';
|
|
735
|
+
let fileExists = true;
|
|
736
|
+
|
|
737
|
+
// Try to read existing .gitignore file
|
|
738
|
+
try {
|
|
739
|
+
gitignoreContent = await fs.readFile(gitignorePath, 'utf-8');
|
|
740
|
+
} catch (error) {
|
|
741
|
+
// File doesn't exist, we'll create it
|
|
742
|
+
fileExists = false;
|
|
743
|
+
gitignoreContent = '';
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Check if 'snapshots/' is already in the file
|
|
747
|
+
const lines = gitignoreContent.split('\n');
|
|
748
|
+
const hasSnapshotsEntry = lines.some(line => line.trim() === entryToAdd);
|
|
749
|
+
|
|
750
|
+
if (!hasSnapshotsEntry) {
|
|
751
|
+
// Add the entry
|
|
752
|
+
let newContent = gitignoreContent;
|
|
753
|
+
|
|
754
|
+
// If file exists and doesn't end with newline, add one
|
|
755
|
+
if (fileExists && gitignoreContent && !gitignoreContent.endsWith('\n')) {
|
|
756
|
+
newContent += '\n';
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Add comment and entry
|
|
760
|
+
if (fileExists && gitignoreContent) {
|
|
761
|
+
newContent += '\n';
|
|
762
|
+
}
|
|
763
|
+
newContent += comment + '\n' + entryToAdd + '\n';
|
|
764
|
+
|
|
765
|
+
await fs.writeFile(gitignorePath, newContent);
|
|
766
|
+
console.log(`✅ Added '${entryToAdd}' to .gitignore`);
|
|
767
|
+
}
|
|
768
|
+
} catch (error) {
|
|
769
|
+
// Silently fail - don't break the snapshot process if gitignore update fails
|
|
770
|
+
console.warn(`⚠️ Warning: Could not update .gitignore: ${error.message}`);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Helper function to determine if a string is a glob pattern
|
|
775
|
+
function isGlob(str) {
|
|
776
|
+
return str.includes('*') || str.includes('?') || str.includes('{');
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Applies advanced profile filtering (multi-profile, exclusion, and ad-hoc globs) to a file list.
|
|
781
|
+
*/
|
|
782
|
+
export async function applyProfileFilter(allFiles, profileString, repoPath) {
|
|
783
|
+
const profileParts = profileString.split(',').map(p => p.trim()).filter(Boolean);
|
|
784
|
+
|
|
785
|
+
const includeGlobs = [];
|
|
786
|
+
const excludeGlobs = [];
|
|
787
|
+
const includeNames = [];
|
|
788
|
+
const excludeNames = [];
|
|
789
|
+
|
|
790
|
+
// Step 1: Differentiate between profile names and ad-hoc glob patterns
|
|
791
|
+
for (const part of profileParts) {
|
|
792
|
+
const isNegative = part.startsWith('-');
|
|
793
|
+
const pattern = isNegative ? part.substring(1) : part;
|
|
794
|
+
|
|
795
|
+
if (isGlob(pattern)) {
|
|
796
|
+
if (isNegative) {
|
|
797
|
+
excludeGlobs.push(pattern);
|
|
798
|
+
} else {
|
|
799
|
+
includeGlobs.push(pattern);
|
|
800
|
+
}
|
|
801
|
+
} else {
|
|
802
|
+
if (isNegative) {
|
|
803
|
+
excludeNames.push(pattern);
|
|
804
|
+
} else {
|
|
805
|
+
includeNames.push(pattern);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
let workingFiles = [];
|
|
811
|
+
let finalIncludes = [...includeGlobs];
|
|
812
|
+
let finalExcludes = [...excludeGlobs];
|
|
813
|
+
|
|
814
|
+
// Step 2: Load patterns from specified profile names
|
|
815
|
+
const allProfileNames = [...new Set([...includeNames, ...excludeNames])];
|
|
816
|
+
const profiles = new Map();
|
|
817
|
+
const notFoundProfiles = [];
|
|
818
|
+
|
|
819
|
+
for (const name of allProfileNames) {
|
|
820
|
+
const profile = await getProfile(name, repoPath);
|
|
821
|
+
if (profile) {
|
|
822
|
+
profiles.set(name, profile);
|
|
823
|
+
} else {
|
|
824
|
+
// This is an ad-hoc glob, not a profile, so no warning is needed.
|
|
825
|
+
if (!isGlob(name)) {
|
|
826
|
+
notFoundProfiles.push(name);
|
|
827
|
+
console.warn(`⚠️ Warning: Profile '${name}' not found and will be skipped.`);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
for (const name of includeNames) {
|
|
833
|
+
if (profiles.has(name)) {
|
|
834
|
+
finalIncludes.push(...(profiles.get(name).include || []));
|
|
835
|
+
finalExcludes.push(...(profiles.get(name).exclude || []));
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
for (const name of excludeNames) {
|
|
839
|
+
if (profiles.has(name)) {
|
|
840
|
+
finalExcludes.push(...(profiles.get(name).include || []));
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Step 3: Apply the filtering logic
|
|
845
|
+
if (finalIncludes.length > 0) {
|
|
846
|
+
workingFiles = micromatch(allFiles, finalIncludes);
|
|
847
|
+
} else if (includeNames.length > 0 && includeGlobs.length === 0) {
|
|
848
|
+
workingFiles = [];
|
|
849
|
+
} else {
|
|
850
|
+
workingFiles = allFiles;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (finalExcludes.length > 0) {
|
|
854
|
+
workingFiles = micromatch.not(workingFiles, finalExcludes);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
return {
|
|
858
|
+
files: workingFiles,
|
|
859
|
+
notFoundProfiles,
|
|
860
|
+
foundProfiles: Array.from(profiles.keys())
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Automatically initializes the .eck manifest directory, attempting dynamic generation via Claude.
|
|
866
|
+
* @param {string} projectPath - Path to the project
|
|
867
|
+
*/
|
|
868
|
+
export async function initializeEckManifest(projectPath) {
|
|
869
|
+
const eckDir = path.join(projectPath, '.eck');
|
|
870
|
+
|
|
871
871
|
// Load setup configuration to check AI generation settings and project context
|
|
872
872
|
let aiGenerationEnabled = false;
|
|
873
873
|
let setupConfig = null;
|
|
@@ -878,71 +878,73 @@ export async function initializeEckManifest(projectPath) {
|
|
|
878
878
|
// If setup config fails to load, default to disabled
|
|
879
879
|
console.warn(` ⚠️ Could not load setup config: ${error.message}. AI generation disabled.`);
|
|
880
880
|
}
|
|
881
|
-
|
|
882
|
-
try {
|
|
883
|
-
// Check if .eck directory already exists and has all required files
|
|
884
|
-
let needsInitialization = false;
|
|
885
|
-
try {
|
|
886
|
-
const eckStats = await fs.stat(eckDir);
|
|
887
|
-
if (eckStats.isDirectory()) {
|
|
888
|
-
// Directory exists, check if all required files are present
|
|
889
|
-
const requiredFiles = ['ENVIRONMENT.md', 'CONTEXT.md', 'OPERATIONS.md', 'JOURNAL.md'];
|
|
890
|
-
for (const fileName of requiredFiles) {
|
|
891
|
-
try {
|
|
892
|
-
await fs.stat(path.join(eckDir, fileName));
|
|
893
|
-
} catch (error) {
|
|
894
|
-
console.log(` ℹ️ Missing ${fileName}, initialization needed`);
|
|
895
|
-
needsInitialization = true;
|
|
896
|
-
break;
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
if (!needsInitialization) {
|
|
900
|
-
// All files exist, no need to initialize
|
|
901
|
-
return;
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
} catch (error) {
|
|
905
|
-
// Directory doesn't exist, we'll create it
|
|
906
|
-
needsInitialization = true;
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
// Create .eck directory
|
|
910
|
-
await fs.mkdir(eckDir, { recursive: true });
|
|
911
|
-
console.log('📋 Initializing .eck manifest directory...');
|
|
912
|
-
|
|
913
|
-
// --- NEW HYBRID LOGIC ---
|
|
914
|
-
// 1. Run static analysis first to gather facts.
|
|
915
|
-
let staticFacts = {};
|
|
916
|
-
try {
|
|
917
|
-
staticFacts = await detectProjectType(projectPath);
|
|
918
|
-
console.log(` 🔍 Static analysis complete. Detected type: ${staticFacts.type}`);
|
|
919
|
-
} catch (e) {
|
|
920
|
-
console.warn(` ⚠️ Static project detection failed: ${e.message}. Proceeding with generic prompts.`);
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
// Prevent AI hallucination by removing low-confidence "other possibilities"
|
|
924
|
-
if (staticFacts && staticFacts.allDetections) {
|
|
925
|
-
delete staticFacts.allDetections;
|
|
926
|
-
}
|
|
927
|
-
|
|
881
|
+
|
|
882
|
+
try {
|
|
883
|
+
// Check if .eck directory already exists and has all required files
|
|
884
|
+
let needsInitialization = false;
|
|
885
|
+
try {
|
|
886
|
+
const eckStats = await fs.stat(eckDir);
|
|
887
|
+
if (eckStats.isDirectory()) {
|
|
888
|
+
// Directory exists, check if all required files are present
|
|
889
|
+
const requiredFiles = ['ENVIRONMENT.md', 'CONTEXT.md', 'OPERATIONS.md', 'JOURNAL.md'];
|
|
890
|
+
for (const fileName of requiredFiles) {
|
|
891
|
+
try {
|
|
892
|
+
await fs.stat(path.join(eckDir, fileName));
|
|
893
|
+
} catch (error) {
|
|
894
|
+
console.log(` ℹ️ Missing ${fileName}, initialization needed`);
|
|
895
|
+
needsInitialization = true;
|
|
896
|
+
break;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
if (!needsInitialization) {
|
|
900
|
+
// All files exist, no need to initialize
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
} catch (error) {
|
|
905
|
+
// Directory doesn't exist, we'll create it
|
|
906
|
+
needsInitialization = true;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Create .eck directory
|
|
910
|
+
await fs.mkdir(eckDir, { recursive: true });
|
|
911
|
+
console.log('📋 Initializing .eck manifest directory...');
|
|
912
|
+
|
|
913
|
+
// --- NEW HYBRID LOGIC ---
|
|
914
|
+
// 1. Run static analysis first to gather facts.
|
|
915
|
+
let staticFacts = {};
|
|
916
|
+
try {
|
|
917
|
+
staticFacts = await detectProjectType(projectPath);
|
|
918
|
+
console.log(` 🔍 Static analysis complete. Detected type: ${staticFacts.type}`);
|
|
919
|
+
} catch (e) {
|
|
920
|
+
console.warn(` ⚠️ Static project detection failed: ${e.message}. Proceeding with generic prompts.`);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Prevent AI hallucination by removing low-confidence "other possibilities"
|
|
924
|
+
if (staticFacts && staticFacts.allDetections) {
|
|
925
|
+
delete staticFacts.allDetections;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
928
|
const staticFactsJson = JSON.stringify(staticFacts, null, 2);
|
|
929
929
|
// --- END NEW LOGIC ---
|
|
930
930
|
|
|
931
931
|
// Extract Context from setup.json if available
|
|
932
|
-
const projName = setupConfig?.projectContext?.name || staticFacts.type || 'project';
|
|
932
|
+
const projName = setupConfig?.projectContext?.name || path.basename(projectPath) || staticFacts.type || 'project';
|
|
933
933
|
const projType = setupConfig?.projectContext?.type || staticFacts.type || 'unknown';
|
|
934
|
-
const projStack = setupConfig?.projectContext?.architecture?.stack?.
|
|
934
|
+
const projStack = (setupConfig?.projectContext?.architecture?.stack?.length > 0)
|
|
935
|
+
? setupConfig.projectContext.architecture.stack.join(', ')
|
|
936
|
+
: 'TBD';
|
|
935
937
|
const projAi = setupConfig?.projectContext?.architecture?.aiIntegration || 'None';
|
|
936
938
|
|
|
937
939
|
// 3. Define smarter templates and prompts using setup.json context
|
|
938
|
-
const templateConfigs = {
|
|
940
|
+
const templateConfigs = {
|
|
939
941
|
'ENVIRONMENT.md': {
|
|
940
942
|
prompt: `Generate raw YAML for .eck/ENVIRONMENT.md based on these project facts:\n${staticFactsJson}\nInclude project_type, runtime, and agent_id: local_dev. NO markdown fences.`,
|
|
941
943
|
fallback: `project_type: ${projType}
|
|
942
944
|
agent_id: local_dev
|
|
943
945
|
# Generated from setup.json
|
|
944
946
|
`
|
|
945
|
-
},
|
|
947
|
+
},
|
|
946
948
|
'CONTEXT.md': {
|
|
947
949
|
prompt: `Analyze these project files and dependencies:\n${staticFactsJson}\nGenerate a professional # Project Overview in Markdown. Describe the actual architecture and purpose of this specific project. Be technical and concise. Start with '# Project Overview'.`,
|
|
948
950
|
fallback: `# Project Overview
|
|
@@ -957,23 +959,23 @@ Stack: ${projStack}
|
|
|
957
959
|
|
|
958
960
|
*(Auto-generated from setup.json)*
|
|
959
961
|
`
|
|
960
|
-
},
|
|
961
|
-
'OPERATIONS.md': {
|
|
962
|
-
prompt: `Look at the dependencies and files:\n${staticFactsJson}\nGenerate a Markdown guide for common operations (Setup, Run, Test, Build) using the correct commands for this tech stack. Start with '# Common Operations'.`,
|
|
963
|
-
fallback: `# [STUB: OPERATIONS.MD]
|
|
964
|
-
|
|
965
|
-
## 🚨 ATTENTION
|
|
966
|
-
**CODER:** Run \`npm run\`, check Makefile, or build files to identify REAL commands for Setup, Running, and Testing. Replace this stub with actual commands. Remove this notice.
|
|
967
|
-
|
|
968
|
-
## Setup
|
|
969
|
-
${staticFacts.type === 'nodejs' ? 'npm install' : 'TBD'}`
|
|
970
|
-
},
|
|
971
|
-
'ROADMAP.md': {
|
|
972
|
-
prompt: `Based on the project type (${staticFacts.type}), propose a 3-step roadmap. Start with '# Project Roadmap'.`,
|
|
973
|
-
fallback: `# [STUB: ROADMAP.MD]
|
|
974
|
-
|
|
975
|
-
**ARCHITECT:** Set a real roadmap based on user goals. **CODER:** Remove this stub marker once a real goal is added.`
|
|
976
|
-
},
|
|
962
|
+
},
|
|
963
|
+
'OPERATIONS.md': {
|
|
964
|
+
prompt: `Look at the dependencies and files:\n${staticFactsJson}\nGenerate a Markdown guide for common operations (Setup, Run, Test, Build) using the correct commands for this tech stack. Start with '# Common Operations'.`,
|
|
965
|
+
fallback: `# [STUB: OPERATIONS.MD]
|
|
966
|
+
|
|
967
|
+
## 🚨 ATTENTION
|
|
968
|
+
**CODER:** Run \`npm run\`, check Makefile, or build files to identify REAL commands for Setup, Running, and Testing. Replace this stub with actual commands. Remove this notice.
|
|
969
|
+
|
|
970
|
+
## Setup
|
|
971
|
+
${staticFacts.type === 'nodejs' ? 'npm install' : 'TBD'}`
|
|
972
|
+
},
|
|
973
|
+
'ROADMAP.md': {
|
|
974
|
+
prompt: `Based on the project type (${staticFacts.type}), propose a 3-step roadmap. Start with '# Project Roadmap'.`,
|
|
975
|
+
fallback: `# [STUB: ROADMAP.MD]
|
|
976
|
+
|
|
977
|
+
**ARCHITECT:** Set a real roadmap based on user goals. **CODER:** Remove this stub marker once a real goal is added.`
|
|
978
|
+
},
|
|
977
979
|
'TECH_DEBT.md': {
|
|
978
980
|
prompt: `Given this is a ${staticFacts.type} project, list 2-3 common technical debt items. Start with '# Technical Debt'.`,
|
|
979
981
|
fallback: `# [STUB: TECH_DEBT.MD]
|
|
@@ -995,87 +997,87 @@ Verify required build steps before deployment.
|
|
|
995
997
|
**CODER:** Update this checklist with actual build/deploy steps for this project.`
|
|
996
998
|
},
|
|
997
999
|
'RUNTIME_STATE.md': {
|
|
998
|
-
prompt: `Based on the project type (${staticFacts.type}), generate a template for RUNTIME_STATE.md. Start with '# Runtime State'.`,
|
|
999
|
-
fallback: `# Runtime State
|
|
1000
|
-
|
|
1001
|
-
## 🚨 ATTENTION CODER
|
|
1002
|
-
Always check this file and verify the actual runtime state (ports, running processes, env variables) BEFORE writing code. Update this file if ports or access methods change.
|
|
1003
|
-
|
|
1004
|
-
- **Server:** e.g., running on port 3210
|
|
1005
|
-
- **Services:** e.g., Scraper running on port 3211
|
|
1006
|
-
- **Auth:** e.g., admin@local / password
|
|
1007
|
-
- **Verification Commands:**
|
|
1008
|
-
- \`pm2 ls\`
|
|
1009
|
-
- \`curl http://localhost:3210/health\`
|
|
1010
|
-
`
|
|
1011
|
-
},
|
|
1012
|
-
'JOURNAL.md': {
|
|
1013
|
-
fallback: `# Development Journal
|
|
1014
|
-
|
|
1015
|
-
## Recent Changes
|
|
1016
|
-
---
|
|
1017
|
-
type: feat
|
|
1018
|
-
scope: project
|
|
1019
|
-
summary: Initial manifest generated (PENDING REVIEW)
|
|
1020
|
-
date: ${new Date().toISOString().split('T')[0]}
|
|
1021
|
-
---
|
|
1022
|
-
- NOTICE: Some .eck files are STUBS. They need manual or AI-assisted verification.`
|
|
1023
|
-
}
|
|
1024
|
-
};
|
|
1025
|
-
|
|
1026
|
-
// Create each template file (only if it doesn't exist)
|
|
1027
|
-
for (const [fileName, config] of Object.entries(templateConfigs)) {
|
|
1028
|
-
const filePath = path.join(eckDir, fileName);
|
|
1029
|
-
|
|
1030
|
-
// Skip if file already exists
|
|
1031
|
-
try {
|
|
1032
|
-
await fs.stat(filePath);
|
|
1033
|
-
console.log(` ✅ ${fileName} already exists, skipping`);
|
|
1034
|
-
continue;
|
|
1035
|
-
} catch (error) {
|
|
1036
|
-
// File doesn't exist, create it
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
let fileContent = config.fallback; // Start with stub fallback
|
|
1040
|
-
let generatedByAI = false;
|
|
1041
|
-
|
|
1042
|
-
// For files with a prompt, try to dynamically generate (only if enabled)
|
|
1043
|
-
if (config.prompt && aiGenerationEnabled) {
|
|
1044
|
-
try {
|
|
1045
|
-
console.log(` 🧠 Attempting to auto-generate ${fileName} via Claude...`);
|
|
1046
|
-
const aiResponseObject = await askClaude(config.prompt);
|
|
1047
|
-
const rawText = aiResponseObject.result;
|
|
1048
|
-
|
|
1049
|
-
if (!rawText || typeof rawText.replace !== 'function') {
|
|
1050
|
-
throw new Error(`AI returned invalid content type: ${typeof rawText}`);
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
// Basic cleanup of potential markdown code blocks from Claude
|
|
1054
|
-
const cleanedResponse = rawText.replace(/^```(markdown|yaml)?\n|```$/g, '').trim();
|
|
1055
|
-
|
|
1056
|
-
if (cleanedResponse) {
|
|
1057
|
-
fileContent = cleanedResponse;
|
|
1058
|
-
generatedByAI = true;
|
|
1059
|
-
console.log(` ✨ AI successfully generated ${fileName}`);
|
|
1060
|
-
} else {
|
|
1061
|
-
throw new Error('AI returned empty content.');
|
|
1062
|
-
}
|
|
1063
|
-
} catch (error) {
|
|
1064
|
-
console.warn(` ⚠️ AI generation failed for ${fileName}: ${error.message}. Using stub template.`);
|
|
1065
|
-
// fileContent is already set to the stub fallback
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
await fs.writeFile(filePath, fileContent);
|
|
1070
|
-
if (!generatedByAI) {
|
|
1071
|
-
console.log(` ✅ Created ${fileName} (stub template)`);
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
console.log('📋 .eck manifest initialized! Edit the files to provide project-specific context.');
|
|
1076
|
-
|
|
1077
|
-
} catch (error) {
|
|
1078
|
-
// Silently fail - don't break the snapshot process if manifest initialization fails
|
|
1079
|
-
console.warn(`⚠️ Warning: Could not initialize .eck manifest: ${error.message}`);
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1000
|
+
prompt: `Based on the project type (${staticFacts.type}), generate a template for RUNTIME_STATE.md. Start with '# Runtime State'.`,
|
|
1001
|
+
fallback: `# Runtime State
|
|
1002
|
+
|
|
1003
|
+
## 🚨 ATTENTION CODER
|
|
1004
|
+
Always check this file and verify the actual runtime state (ports, running processes, env variables) BEFORE writing code. Update this file if ports or access methods change.
|
|
1005
|
+
|
|
1006
|
+
- **Server:** e.g., running on port 3210
|
|
1007
|
+
- **Services:** e.g., Scraper running on port 3211
|
|
1008
|
+
- **Auth:** e.g., admin@local / password
|
|
1009
|
+
- **Verification Commands:**
|
|
1010
|
+
- \`pm2 ls\`
|
|
1011
|
+
- \`curl http://localhost:3210/health\`
|
|
1012
|
+
`
|
|
1013
|
+
},
|
|
1014
|
+
'JOURNAL.md': {
|
|
1015
|
+
fallback: `# Development Journal
|
|
1016
|
+
|
|
1017
|
+
## Recent Changes
|
|
1018
|
+
---
|
|
1019
|
+
type: feat
|
|
1020
|
+
scope: project
|
|
1021
|
+
summary: Initial manifest generated (PENDING REVIEW)
|
|
1022
|
+
date: ${new Date().toISOString().split('T')[0]}
|
|
1023
|
+
---
|
|
1024
|
+
- NOTICE: Some .eck files are STUBS. They need manual or AI-assisted verification.`
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
// Create each template file (only if it doesn't exist)
|
|
1029
|
+
for (const [fileName, config] of Object.entries(templateConfigs)) {
|
|
1030
|
+
const filePath = path.join(eckDir, fileName);
|
|
1031
|
+
|
|
1032
|
+
// Skip if file already exists
|
|
1033
|
+
try {
|
|
1034
|
+
await fs.stat(filePath);
|
|
1035
|
+
console.log(` ✅ ${fileName} already exists, skipping`);
|
|
1036
|
+
continue;
|
|
1037
|
+
} catch (error) {
|
|
1038
|
+
// File doesn't exist, create it
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
let fileContent = config.fallback; // Start with stub fallback
|
|
1042
|
+
let generatedByAI = false;
|
|
1043
|
+
|
|
1044
|
+
// For files with a prompt, try to dynamically generate (only if enabled)
|
|
1045
|
+
if (config.prompt && aiGenerationEnabled) {
|
|
1046
|
+
try {
|
|
1047
|
+
console.log(` 🧠 Attempting to auto-generate ${fileName} via Claude...`);
|
|
1048
|
+
const aiResponseObject = await askClaude(config.prompt);
|
|
1049
|
+
const rawText = aiResponseObject.result;
|
|
1050
|
+
|
|
1051
|
+
if (!rawText || typeof rawText.replace !== 'function') {
|
|
1052
|
+
throw new Error(`AI returned invalid content type: ${typeof rawText}`);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// Basic cleanup of potential markdown code blocks from Claude
|
|
1056
|
+
const cleanedResponse = rawText.replace(/^```(markdown|yaml)?\n|```$/g, '').trim();
|
|
1057
|
+
|
|
1058
|
+
if (cleanedResponse) {
|
|
1059
|
+
fileContent = cleanedResponse;
|
|
1060
|
+
generatedByAI = true;
|
|
1061
|
+
console.log(` ✨ AI successfully generated ${fileName}`);
|
|
1062
|
+
} else {
|
|
1063
|
+
throw new Error('AI returned empty content.');
|
|
1064
|
+
}
|
|
1065
|
+
} catch (error) {
|
|
1066
|
+
console.warn(` ⚠️ AI generation failed for ${fileName}: ${error.message}. Using stub template.`);
|
|
1067
|
+
// fileContent is already set to the stub fallback
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
await fs.writeFile(filePath, fileContent);
|
|
1072
|
+
if (!generatedByAI) {
|
|
1073
|
+
console.log(` ✅ Created ${fileName} (stub template)`);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
console.log('📋 .eck manifest initialized! Edit the files to provide project-specific context.');
|
|
1078
|
+
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
// Silently fail - don't break the snapshot process if manifest initialization fails
|
|
1081
|
+
console.warn(`⚠️ Warning: Could not initialize .eck manifest: ${error.message}`);
|
|
1082
|
+
}
|
|
1083
|
+
}
|