@xelth/eck-snapshot 5.9.0 → 6.4.1
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/README.md +267 -190
- package/package.json +15 -2
- package/scripts/mcp-eck-core.js +61 -13
- package/setup.json +119 -81
- package/src/cli/cli.js +235 -385
- package/src/cli/commands/createSnapshot.js +336 -122
- package/src/cli/commands/recon.js +244 -0
- package/src/cli/commands/setupMcp.js +278 -19
- package/src/cli/commands/trainTokens.js +42 -32
- package/src/cli/commands/updateSnapshot.js +128 -76
- package/src/core/depthConfig.js +54 -0
- package/src/core/skeletonizer.js +71 -18
- package/src/templates/architect-prompt.template.md +34 -0
- package/src/templates/multiAgent.md +43 -10
- package/src/templates/opencode/coder.template.md +44 -17
- package/src/templates/opencode/junior-architect.template.md +45 -15
- package/src/templates/skeleton-instruction.md +1 -1
- package/src/utils/aiHeader.js +57 -27
- package/src/utils/claudeMdGenerator.js +136 -78
- package/src/utils/fileUtils.js +1023 -1016
- package/src/utils/gitUtils.js +12 -8
- package/src/utils/opencodeAgentsGenerator.js +8 -2
- package/src/utils/projectDetector.js +66 -21
- package/src/utils/tokenEstimator.js +11 -7
- package/src/cli/commands/consilium.js +0 -86
- package/src/cli/commands/detectProfiles.js +0 -98
- package/src/cli/commands/envSync.js +0 -319
- package/src/cli/commands/generateProfileGuide.js +0 -144
- package/src/cli/commands/pruneSnapshot.js +0 -106
- package/src/cli/commands/restoreSnapshot.js +0 -173
- package/src/cli/commands/setupGemini.js +0 -149
- package/src/cli/commands/setupGemini.test.js +0 -115
- package/src/cli/commands/showFile.js +0 -39
- package/src/services/claudeCliService.js +0 -626
- package/src/services/claudeCliService.test.js +0 -267
|
@@ -1,319 +0,0 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import crypto from 'crypto';
|
|
5
|
-
import zlib from 'zlib';
|
|
6
|
-
import { promisify } from 'util';
|
|
7
|
-
import chalk from 'chalk';
|
|
8
|
-
import ora from 'ora';
|
|
9
|
-
import inquirer from 'inquirer';
|
|
10
|
-
|
|
11
|
-
const gzip = promisify(zlib.gzip);
|
|
12
|
-
const gunzip = promisify(zlib.gunzip);
|
|
13
|
-
|
|
14
|
-
const SYNC_FILENAME = '.eck-sync.enc';
|
|
15
|
-
const ECK_DIR = '.eck';
|
|
16
|
-
|
|
17
|
-
// Files to include (relative to .eck/). Snapshots excluded intentionally.
|
|
18
|
-
const INCLUDE_FILES = [
|
|
19
|
-
'anchor',
|
|
20
|
-
'claude-mcp-config.json',
|
|
21
|
-
'CONTEXT.md',
|
|
22
|
-
'ENVIRONMENT.md',
|
|
23
|
-
'JOURNAL.md',
|
|
24
|
-
'OPERATIONS.md',
|
|
25
|
-
'ROADMAP.md',
|
|
26
|
-
'TECH_DEBT.md',
|
|
27
|
-
'update_seq',
|
|
28
|
-
];
|
|
29
|
-
|
|
30
|
-
// Crypto constants
|
|
31
|
-
const PBKDF2_ITERATIONS = 100_000;
|
|
32
|
-
const PBKDF2_DIGEST = 'sha512';
|
|
33
|
-
const KEY_LENGTH = 32;
|
|
34
|
-
const SALT_LENGTH = 16;
|
|
35
|
-
const IV_LENGTH = 12;
|
|
36
|
-
const AUTH_TAG_LENGTH = 16;
|
|
37
|
-
const ALGORITHM = 'aes-256-gcm';
|
|
38
|
-
const FORMAT_VERSION = 1;
|
|
39
|
-
|
|
40
|
-
// ── Password ────────────────────────────────────────────────────────
|
|
41
|
-
|
|
42
|
-
async function getPassword(action = 'encrypt') {
|
|
43
|
-
const envKey = process.env.ECK_SYNC_KEY;
|
|
44
|
-
if (envKey) {
|
|
45
|
-
if (envKey.length < 4) throw new Error('ECK_SYNC_KEY must be at least 4 characters');
|
|
46
|
-
return envKey;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const verb = action === 'encrypt' ? 'encrypt' : 'decrypt';
|
|
50
|
-
const { password } = await inquirer.prompt([{
|
|
51
|
-
type: 'password',
|
|
52
|
-
name: 'password',
|
|
53
|
-
message: `Enter password to ${verb} .eck/ environment:`,
|
|
54
|
-
mask: '*',
|
|
55
|
-
validate: (input) => input.length >= 4 || 'Password must be at least 4 characters',
|
|
56
|
-
}]);
|
|
57
|
-
|
|
58
|
-
if (action === 'encrypt') {
|
|
59
|
-
const { confirm } = await inquirer.prompt([{
|
|
60
|
-
type: 'password',
|
|
61
|
-
name: 'confirm',
|
|
62
|
-
message: 'Confirm password:',
|
|
63
|
-
mask: '*',
|
|
64
|
-
}]);
|
|
65
|
-
if (password !== confirm) throw new Error('Passwords do not match');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return password;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ── Encryption ──────────────────────────────────────────────────────
|
|
72
|
-
|
|
73
|
-
function encrypt(plainBuffer, password) {
|
|
74
|
-
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
75
|
-
const key = crypto.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, KEY_LENGTH, PBKDF2_DIGEST);
|
|
76
|
-
const iv = crypto.randomBytes(IV_LENGTH);
|
|
77
|
-
|
|
78
|
-
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
79
|
-
const encrypted = Buffer.concat([cipher.update(plainBuffer), cipher.final()]);
|
|
80
|
-
const authTag = cipher.getAuthTag();
|
|
81
|
-
|
|
82
|
-
// Binary: salt(16) + iv(12) + authTag(16) + ciphertext
|
|
83
|
-
return Buffer.concat([salt, iv, authTag, encrypted]);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function decrypt(encBuffer, password) {
|
|
87
|
-
const minSize = SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH + 1;
|
|
88
|
-
if (encBuffer.length < minSize) {
|
|
89
|
-
throw new Error('Encrypted file is too small or corrupted');
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
let offset = 0;
|
|
93
|
-
const salt = encBuffer.subarray(offset, offset += SALT_LENGTH);
|
|
94
|
-
const iv = encBuffer.subarray(offset, offset += IV_LENGTH);
|
|
95
|
-
const authTag = encBuffer.subarray(offset, offset += AUTH_TAG_LENGTH);
|
|
96
|
-
const ciphertext = encBuffer.subarray(offset);
|
|
97
|
-
|
|
98
|
-
const key = crypto.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, KEY_LENGTH, PBKDF2_DIGEST);
|
|
99
|
-
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
100
|
-
decipher.setAuthTag(authTag);
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
104
|
-
} catch {
|
|
105
|
-
throw new Error('Decryption failed. Wrong password or corrupted file.');
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ── Path templating ─────────────────────────────────────────────────
|
|
110
|
-
|
|
111
|
-
function templatizePaths(content, projectRoot, homeDir, filename) {
|
|
112
|
-
let result = content;
|
|
113
|
-
|
|
114
|
-
if (filename.endsWith('.json')) {
|
|
115
|
-
// JSON files store backslashes escaped: C:\\Users\\...
|
|
116
|
-
const projEsc = projectRoot.replace(/\\/g, '\\\\');
|
|
117
|
-
const homeEsc = homeDir.replace(/\\/g, '\\\\');
|
|
118
|
-
result = result.replaceAll(projEsc, '{{PROJECT_ROOT}}');
|
|
119
|
-
result = result.replaceAll(homeEsc, '{{HOME}}');
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Forward-slash and raw variants
|
|
123
|
-
const projFwd = projectRoot.replace(/\\/g, '/');
|
|
124
|
-
const homeFwd = homeDir.replace(/\\/g, '/');
|
|
125
|
-
result = result.replaceAll(projectRoot, '{{PROJECT_ROOT}}');
|
|
126
|
-
result = result.replaceAll(projFwd, '{{PROJECT_ROOT}}');
|
|
127
|
-
result = result.replaceAll(homeDir, '{{HOME}}');
|
|
128
|
-
result = result.replaceAll(homeFwd, '{{HOME}}');
|
|
129
|
-
|
|
130
|
-
return result;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function resolveTemplates(content, projectRoot, homeDir, filename) {
|
|
134
|
-
let result = content;
|
|
135
|
-
|
|
136
|
-
if (filename.endsWith('.json')) {
|
|
137
|
-
// JSON needs escaped backslashes on Windows
|
|
138
|
-
const projEsc = projectRoot.replace(/\\/g, '\\\\');
|
|
139
|
-
const homeEsc = homeDir.replace(/\\/g, '\\\\');
|
|
140
|
-
result = result.replaceAll('{{PROJECT_ROOT}}', projEsc);
|
|
141
|
-
result = result.replaceAll('{{HOME}}', homeEsc);
|
|
142
|
-
} else {
|
|
143
|
-
result = result.replaceAll('{{PROJECT_ROOT}}', projectRoot);
|
|
144
|
-
result = result.replaceAll('{{HOME}}', homeDir);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return result;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// ── Push ─────────────────────────────────────────────────────────────
|
|
151
|
-
|
|
152
|
-
export async function envPush(options = {}) {
|
|
153
|
-
const projectRoot = process.cwd();
|
|
154
|
-
const eckDir = path.join(projectRoot, ECK_DIR);
|
|
155
|
-
const outputPath = path.join(projectRoot, SYNC_FILENAME);
|
|
156
|
-
const homeDir = os.homedir();
|
|
157
|
-
const spinner = ora();
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
// Verify .eck/ exists
|
|
161
|
-
spinner.start('Checking .eck/ directory...');
|
|
162
|
-
try {
|
|
163
|
-
await fs.access(eckDir);
|
|
164
|
-
} catch {
|
|
165
|
-
spinner.fail('.eck/ directory not found');
|
|
166
|
-
console.log(chalk.yellow('Run eck-snapshot first to create .eck/ context files.'));
|
|
167
|
-
process.exit(1);
|
|
168
|
-
}
|
|
169
|
-
spinner.succeed('.eck/ directory found');
|
|
170
|
-
|
|
171
|
-
// Read included files
|
|
172
|
-
spinner.start('Reading .eck/ files...');
|
|
173
|
-
const payload = {
|
|
174
|
-
version: FORMAT_VERSION,
|
|
175
|
-
timestamp: new Date().toISOString(),
|
|
176
|
-
files: {},
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
let fileCount = 0;
|
|
180
|
-
for (const filename of INCLUDE_FILES) {
|
|
181
|
-
const filePath = path.join(eckDir, filename);
|
|
182
|
-
try {
|
|
183
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
184
|
-
const templated = templatizePaths(content, projectRoot, homeDir, filename);
|
|
185
|
-
payload.files[filename] = { content: templated, encoding: 'utf8' };
|
|
186
|
-
fileCount++;
|
|
187
|
-
if (options.verbose) {
|
|
188
|
-
console.log(chalk.gray(` + ${filename} (${Buffer.byteLength(content)} bytes)`));
|
|
189
|
-
}
|
|
190
|
-
} catch {
|
|
191
|
-
if (options.verbose) {
|
|
192
|
-
console.log(chalk.gray(` - ${filename} (not found, skipping)`));
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (fileCount === 0) {
|
|
198
|
-
spinner.fail('No .eck/ files found to pack');
|
|
199
|
-
process.exit(1);
|
|
200
|
-
}
|
|
201
|
-
spinner.succeed(`Read ${fileCount} files from .eck/`);
|
|
202
|
-
|
|
203
|
-
// Compress
|
|
204
|
-
spinner.start('Compressing...');
|
|
205
|
-
const jsonStr = JSON.stringify(payload, null, 2);
|
|
206
|
-
const compressed = await gzip(Buffer.from(jsonStr, 'utf-8'));
|
|
207
|
-
spinner.succeed(`Compressed: ${Buffer.byteLength(jsonStr)} -> ${compressed.length} bytes`);
|
|
208
|
-
|
|
209
|
-
// Encrypt
|
|
210
|
-
const password = await getPassword('encrypt');
|
|
211
|
-
spinner.start('Encrypting...');
|
|
212
|
-
const encrypted = encrypt(compressed, password);
|
|
213
|
-
spinner.succeed(`Encrypted: ${encrypted.length} bytes`);
|
|
214
|
-
|
|
215
|
-
// Write
|
|
216
|
-
await fs.writeFile(outputPath, encrypted);
|
|
217
|
-
|
|
218
|
-
console.log(chalk.green.bold('\nEnvironment pushed successfully!'));
|
|
219
|
-
console.log(chalk.gray(` Files packed: ${fileCount}`));
|
|
220
|
-
console.log(chalk.gray(` Output size: ${encrypted.length} bytes`));
|
|
221
|
-
console.log(chalk.gray(` Output file: ${SYNC_FILENAME}`));
|
|
222
|
-
console.log(chalk.yellow('\nCommit .eck-sync.enc to git to share across machines.'));
|
|
223
|
-
|
|
224
|
-
} catch (error) {
|
|
225
|
-
spinner.fail(`Push failed: ${error.message}`);
|
|
226
|
-
if (options.verbose) console.error(error.stack);
|
|
227
|
-
process.exit(1);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// ── Pull ─────────────────────────────────────────────────────────────
|
|
232
|
-
|
|
233
|
-
export async function envPull(options = {}) {
|
|
234
|
-
const projectRoot = process.cwd();
|
|
235
|
-
const eckDir = path.join(projectRoot, ECK_DIR);
|
|
236
|
-
const inputPath = path.join(projectRoot, SYNC_FILENAME);
|
|
237
|
-
const homeDir = os.homedir();
|
|
238
|
-
const spinner = ora();
|
|
239
|
-
|
|
240
|
-
try {
|
|
241
|
-
// Verify .eck-sync.enc exists
|
|
242
|
-
spinner.start(`Checking ${SYNC_FILENAME}...`);
|
|
243
|
-
try {
|
|
244
|
-
await fs.access(inputPath);
|
|
245
|
-
} catch {
|
|
246
|
-
spinner.fail(`${SYNC_FILENAME} not found in project root`);
|
|
247
|
-
console.log(chalk.yellow('Run "eck-snapshot env push" first, or pull the file from git.'));
|
|
248
|
-
process.exit(1);
|
|
249
|
-
}
|
|
250
|
-
spinner.succeed(`${SYNC_FILENAME} found`);
|
|
251
|
-
|
|
252
|
-
// Conflict check
|
|
253
|
-
if (!options.force) {
|
|
254
|
-
try {
|
|
255
|
-
await fs.access(eckDir);
|
|
256
|
-
const { action } = await inquirer.prompt([{
|
|
257
|
-
type: 'list',
|
|
258
|
-
name: 'action',
|
|
259
|
-
message: '.eck/ directory already exists. What would you like to do?',
|
|
260
|
-
choices: [
|
|
261
|
-
{ name: 'Overwrite existing files', value: 'overwrite' },
|
|
262
|
-
{ name: 'Cancel', value: 'cancel' },
|
|
263
|
-
],
|
|
264
|
-
}]);
|
|
265
|
-
if (action === 'cancel') {
|
|
266
|
-
console.log(chalk.yellow('Pull cancelled.'));
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
} catch {
|
|
270
|
-
// .eck/ doesn't exist, proceed
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Read + decrypt
|
|
275
|
-
spinner.start('Reading encrypted file...');
|
|
276
|
-
const encrypted = await fs.readFile(inputPath);
|
|
277
|
-
spinner.succeed(`Read ${encrypted.length} bytes`);
|
|
278
|
-
|
|
279
|
-
const password = await getPassword('decrypt');
|
|
280
|
-
spinner.start('Decrypting...');
|
|
281
|
-
const compressed = decrypt(encrypted, password);
|
|
282
|
-
spinner.succeed('Decrypted');
|
|
283
|
-
|
|
284
|
-
// Decompress + parse
|
|
285
|
-
spinner.start('Decompressing...');
|
|
286
|
-
const jsonBuffer = await gunzip(compressed);
|
|
287
|
-
const payload = JSON.parse(jsonBuffer.toString('utf-8'));
|
|
288
|
-
spinner.succeed('Decompressed');
|
|
289
|
-
|
|
290
|
-
if (payload.version !== FORMAT_VERSION) {
|
|
291
|
-
throw new Error(`Unsupported format version: ${payload.version} (expected ${FORMAT_VERSION})`);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Restore files
|
|
295
|
-
await fs.mkdir(eckDir, { recursive: true });
|
|
296
|
-
|
|
297
|
-
const fileNames = Object.keys(payload.files);
|
|
298
|
-
spinner.start(`Restoring ${fileNames.length} files...`);
|
|
299
|
-
|
|
300
|
-
for (const filename of fileNames) {
|
|
301
|
-
const { content } = payload.files[filename];
|
|
302
|
-
const resolved = resolveTemplates(content, projectRoot, homeDir, filename);
|
|
303
|
-
await fs.writeFile(path.join(eckDir, filename), resolved, 'utf-8');
|
|
304
|
-
if (options.verbose) {
|
|
305
|
-
console.log(chalk.gray(` + ${filename}`));
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
spinner.succeed(`Restored ${fileNames.length} files to .eck/`);
|
|
309
|
-
|
|
310
|
-
console.log(chalk.green.bold('\nEnvironment pulled successfully!'));
|
|
311
|
-
console.log(chalk.gray(` Files restored: ${fileNames.length}`));
|
|
312
|
-
console.log(chalk.gray(` Packed at: ${payload.timestamp}`));
|
|
313
|
-
|
|
314
|
-
} catch (error) {
|
|
315
|
-
spinner.fail(`Pull failed: ${error.message}`);
|
|
316
|
-
if (options.verbose) console.error(error.stack);
|
|
317
|
-
process.exit(1);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import ora from 'ora';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
import { loadSetupConfig } from '../../config.js';
|
|
6
|
-
import { scanDirectoryRecursively, generateDirectoryTree, initializeEckManifest, loadConfig } from '../../utils/fileUtils.js';
|
|
7
|
-
|
|
8
|
-
function buildPrompt(projectPath) {
|
|
9
|
-
const normalizedPath = path.resolve(projectPath);
|
|
10
|
-
return `You are a code architect helping a developer curate manual context profiles for a repository.
|
|
11
|
-
Project root: ${normalizedPath}
|
|
12
|
-
|
|
13
|
-
Use the project directory tree provided separately to identify logical groupings of files that should travel together during focused work.
|
|
14
|
-
|
|
15
|
-
Instructions:
|
|
16
|
-
1. Propose profile names that reflect the responsibilities or layers of the codebase.
|
|
17
|
-
2. For each profile, add a "description" field explaining what the profile covers.
|
|
18
|
-
3. For each profile, produce "include" and "exclude" arrays of glob patterns using proper micromatch syntax:
|
|
19
|
-
|
|
20
|
-
CORRECT glob patterns:
|
|
21
|
-
✓ "src/**/*" - all files recursively in src/
|
|
22
|
-
✓ "src/**/*.js" - all JS files recursively in src/
|
|
23
|
-
✓ "**/node_modules/**" - node_modules anywhere
|
|
24
|
-
✓ "**/*.test.js" - test files anywhere
|
|
25
|
-
✓ "packages/**/package.json" - all package.json in packages subdirs
|
|
26
|
-
|
|
27
|
-
INCORRECT patterns (DO NOT USE):
|
|
28
|
-
✗ "src//" - double slash is invalid
|
|
29
|
-
✗ "src/**/" - trailing slash is incorrect
|
|
30
|
-
✗ "/node_modules/" - leading/trailing slashes don't work as expected
|
|
31
|
-
✗ "src/.js" - missing ** means only root level
|
|
32
|
-
|
|
33
|
-
4. Always include a sensible catch-all profile (for example, "default") if one is not obvious.
|
|
34
|
-
5. Call out generated assets, tests, or vendor files in "exclude" arrays when appropriate.
|
|
35
|
-
6. Return **only** valid JSON. Do not wrap the response in markdown fences or add commentary.
|
|
36
|
-
|
|
37
|
-
Example profile structure:
|
|
38
|
-
{
|
|
39
|
-
"backend": {
|
|
40
|
-
"description": "Backend API and services",
|
|
41
|
-
"include": ["src/api/**/*", "src/services/**/*"],
|
|
42
|
-
"exclude": ["**/*.test.js", "**/node_modules/**"]
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function buildGuideContent({ prompt, directoryTree }) {
|
|
49
|
-
const timestamp = new Date().toISOString();
|
|
50
|
-
const trimmedTree = directoryTree.trimEnd();
|
|
51
|
-
|
|
52
|
-
return [
|
|
53
|
-
'# Profile Generation Guide',
|
|
54
|
-
'',
|
|
55
|
-
`Generated: ${timestamp}`,
|
|
56
|
-
'',
|
|
57
|
-
'## How to Use',
|
|
58
|
-
'- Copy the prompt below into your AI assistant or follow it yourself.',
|
|
59
|
-
'- When using an AI, paste the directory tree afterward so it has full project context.',
|
|
60
|
-
"- Review the suggested profiles, then save the JSON to `.eck/profiles.json` when you are satisfied.",
|
|
61
|
-
'',
|
|
62
|
-
'## Recommended Prompt',
|
|
63
|
-
'```text',
|
|
64
|
-
prompt.trimEnd(),
|
|
65
|
-
'```',
|
|
66
|
-
'',
|
|
67
|
-
'## Project Directory Tree',
|
|
68
|
-
'```text',
|
|
69
|
-
trimmedTree,
|
|
70
|
-
'```',
|
|
71
|
-
''
|
|
72
|
-
].join('\n');
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export async function generateProfileGuide(repoPath = process.cwd(), options = {}) {
|
|
76
|
-
const spinner = ora('Preparing profile generation guide...').start();
|
|
77
|
-
const projectPath = path.resolve(repoPath);
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
spinner.text = 'Ensuring .eck manifest directory is initialized...';
|
|
81
|
-
await initializeEckManifest(projectPath);
|
|
82
|
-
|
|
83
|
-
spinner.text = 'Loading configuration...';
|
|
84
|
-
const setupConfig = await loadSetupConfig();
|
|
85
|
-
const userConfig = await loadConfig(options.config);
|
|
86
|
-
const combinedConfig = {
|
|
87
|
-
...userConfig,
|
|
88
|
-
...(setupConfig.fileFiltering || {}),
|
|
89
|
-
...(setupConfig.performance || {})
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
spinner.text = 'Scanning repository files...';
|
|
93
|
-
const allFiles = await scanDirectoryRecursively(projectPath, combinedConfig, projectPath);
|
|
94
|
-
|
|
95
|
-
spinner.text = 'Building directory tree...';
|
|
96
|
-
const maxDepth = Number(combinedConfig.maxDepth ?? 10);
|
|
97
|
-
const directoryTree = await generateDirectoryTree(projectPath, '', allFiles, 0, Number.isFinite(maxDepth) ? maxDepth : 10, combinedConfig);
|
|
98
|
-
|
|
99
|
-
if (!directoryTree) {
|
|
100
|
-
throw new Error('Failed to generate directory tree or project is empty.');
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// 1. Create the Guide Markdown
|
|
104
|
-
const prompt = buildPrompt(projectPath);
|
|
105
|
-
const guideContent = buildGuideContent({ prompt, directoryTree });
|
|
106
|
-
const guidePath = path.join(projectPath, '.eck', 'profile_generation_guide.md');
|
|
107
|
-
|
|
108
|
-
await fs.mkdir(path.dirname(guidePath), { recursive: true });
|
|
109
|
-
spinner.text = 'Writing guide to .eck/profile_generation_guide.md...';
|
|
110
|
-
await fs.writeFile(guidePath, guideContent, 'utf-8');
|
|
111
|
-
|
|
112
|
-
// 2. Ensure profiles.json exists (or create a stub)
|
|
113
|
-
const profilesPath = path.join(projectPath, '.eck', 'profiles.json');
|
|
114
|
-
let profilesCreated = false;
|
|
115
|
-
try {
|
|
116
|
-
await fs.access(profilesPath);
|
|
117
|
-
} catch {
|
|
118
|
-
// File doesn't exist, create a stub for easy pasting
|
|
119
|
-
const stubContent = {
|
|
120
|
-
"_instruction": "PASTE THE JSON RESPONSE FROM THE AI HERE",
|
|
121
|
-
"example_profile": {
|
|
122
|
-
"description": "Example profile structure",
|
|
123
|
-
"include": ["src/**"],
|
|
124
|
-
"exclude": ["**/*.test.js"]
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
await fs.writeFile(profilesPath, JSON.stringify(stubContent, null, 2));
|
|
128
|
-
profilesCreated = true;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
spinner.succeed(`Profile generation guide saved to ${guidePath}`);
|
|
132
|
-
|
|
133
|
-
// 3. Print clear instructions
|
|
134
|
-
console.log(chalk.cyan('\n📋 Next Steps (Workflow):'));
|
|
135
|
-
console.log(`1. Open: ${chalk.bold('.eck/profile_generation_guide.md')}`);
|
|
136
|
-
console.log('2. Copy the PROMPT + TREE content and paste it into an AI (Gemini 1.5 Pro, Claude Opus, ChatGPT).');
|
|
137
|
-
console.log('3. Copy the JSON response from the AI.');
|
|
138
|
-
console.log(`4. Paste the JSON into: ${chalk.bold('.eck/profiles.json')} ${profilesCreated ? '(I created this file for you)' : '(File exists)'}`);
|
|
139
|
-
console.log('\n✅ Once saved, run: ' + chalk.green('eck-snapshot --profile <profile_name>'));
|
|
140
|
-
} catch (error) {
|
|
141
|
-
spinner.fail(`Failed to generate profile guide: ${error.message}`);
|
|
142
|
-
throw error;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import ora from 'ora';
|
|
4
|
-
import { executePrompt as askClaude } from '../../services/claudeCliService.js';
|
|
5
|
-
import { parseSnapshotContent, parseSize, formatSize } from '../../utils/fileUtils.js';
|
|
6
|
-
|
|
7
|
-
function extractJson(text) {
|
|
8
|
-
const match = text.match(/```(json)?([\s\S]*?)```/);
|
|
9
|
-
if (match && match[2]) {
|
|
10
|
-
return match[2].trim();
|
|
11
|
-
}
|
|
12
|
-
const firstBracket = text.indexOf('[');
|
|
13
|
-
const lastBracket = text.lastIndexOf(']');
|
|
14
|
-
if (firstBracket !== -1 && lastBracket !== -1 && lastBracket > firstBracket) {
|
|
15
|
-
return text.substring(firstBracket, lastBracket + 1).trim();
|
|
16
|
-
}
|
|
17
|
-
return text.trim();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export async function pruneSnapshot(snapshotFile, options) {
|
|
21
|
-
const spinner = ora('Starting snapshot pruning process...').start();
|
|
22
|
-
try {
|
|
23
|
-
const targetSize = parseSize(options.targetSize);
|
|
24
|
-
spinner.text = `Reading snapshot file: ${snapshotFile}`;
|
|
25
|
-
const snapshotContent = await fs.readFile(snapshotFile, 'utf-8');
|
|
26
|
-
const snapshotHeader = snapshotContent.split('--- File: /')[0];
|
|
27
|
-
const files = parseSnapshotContent(snapshotContent);
|
|
28
|
-
|
|
29
|
-
if (files.length === 0) {
|
|
30
|
-
spinner.warn('No files found in the snapshot.');
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const currentSize = Buffer.byteLength(snapshotContent, 'utf-8');
|
|
35
|
-
if (currentSize <= targetSize) {
|
|
36
|
-
spinner.succeed(`Snapshot is already smaller than the target size. (${formatSize(currentSize)} < ${formatSize(targetSize)})`);
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
spinner.text = 'Asking AI to rank files by importance...';
|
|
41
|
-
const filePaths = files.map(f => f.path);
|
|
42
|
-
|
|
43
|
-
const prompt = `Return a JSON array ranking these file paths by importance (most important first).
|
|
44
|
-
|
|
45
|
-
Important files: package.json, index.js, main entry points, core logic, configuration
|
|
46
|
-
Less important: tests, documentation, examples
|
|
47
|
-
|
|
48
|
-
Files to rank:
|
|
49
|
-
${filePaths.join('\n')}
|
|
50
|
-
|
|
51
|
-
Return format (NOTHING else, no markdown, no explanations, ONLY the array):
|
|
52
|
-
["file1", "file2", "file3"]`;
|
|
53
|
-
|
|
54
|
-
const aiResponseObject = await askClaude(prompt);
|
|
55
|
-
const rawText = aiResponseObject.response || aiResponseObject.response_text || aiResponseObject.result;
|
|
56
|
-
const cleanedJson = extractJson(rawText);
|
|
57
|
-
|
|
58
|
-
let rankedFiles;
|
|
59
|
-
try {
|
|
60
|
-
rankedFiles = JSON.parse(cleanedJson);
|
|
61
|
-
if (!Array.isArray(rankedFiles) || rankedFiles.some(item => typeof item !== 'string')) {
|
|
62
|
-
throw new Error('AI response is not an array of strings.');
|
|
63
|
-
}
|
|
64
|
-
} catch (e) {
|
|
65
|
-
spinner.fail(`Failed to parse AI's file ranking: ${e.message}`);
|
|
66
|
-
console.error('Received from AI:', cleanedJson);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
spinner.text = 'Building pruned snapshot...';
|
|
71
|
-
const fileMap = new Map(files.map(f => [f.path, f.content]));
|
|
72
|
-
let newSnapshotContent = snapshotHeader;
|
|
73
|
-
let newSize = Buffer.byteLength(newSnapshotContent, 'utf-8');
|
|
74
|
-
let filesIncluded = 0;
|
|
75
|
-
|
|
76
|
-
for (const filePath of rankedFiles) {
|
|
77
|
-
if (fileMap.has(filePath)) {
|
|
78
|
-
const fileContent = fileMap.get(filePath);
|
|
79
|
-
const fileEntry = `--- File: /${filePath} ---\n\n${fileContent}\n\n`;
|
|
80
|
-
const entrySize = Buffer.byteLength(fileEntry, 'utf-8');
|
|
81
|
-
|
|
82
|
-
if (newSize + entrySize > targetSize) {
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
newSnapshotContent += fileEntry;
|
|
87
|
-
newSize += entrySize;
|
|
88
|
-
filesIncluded++;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const outputFilename = `${path.basename(snapshotFile, path.extname(snapshotFile))}_pruned_${options.targetSize}${path.extname(snapshotFile)}`;
|
|
93
|
-
const outputPath = path.join(path.dirname(snapshotFile), outputFilename);
|
|
94
|
-
|
|
95
|
-
await fs.writeFile(outputPath, newSnapshotContent);
|
|
96
|
-
|
|
97
|
-
spinner.succeed('Snapshot pruning complete!');
|
|
98
|
-
console.log(`- Original Size: ${formatSize(currentSize)}`);
|
|
99
|
-
console.log(`- New Size: ${formatSize(newSize)}`);
|
|
100
|
-
console.log(`- Files Included: ${filesIncluded} / ${files.length}`);
|
|
101
|
-
console.log(`- Pruned snapshot saved to: ${outputPath}`);
|
|
102
|
-
|
|
103
|
-
} catch (error) {
|
|
104
|
-
spinner.fail(`An error occurred during pruning: ${error.message}`);
|
|
105
|
-
}
|
|
106
|
-
}
|