@xelth/eck-snapshot 5.9.0 → 6.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +321 -190
- package/index.js +1 -1
- package/package.json +15 -2
- package/scripts/mcp-eck-core.js +143 -13
- package/setup.json +119 -81
- package/src/cli/cli.js +256 -385
- package/src/cli/commands/createSnapshot.js +391 -175
- package/src/cli/commands/recon.js +308 -0
- package/src/cli/commands/setupMcp.js +280 -19
- package/src/cli/commands/trainTokens.js +42 -32
- package/src/cli/commands/updateSnapshot.js +136 -43
- package/src/core/depthConfig.js +54 -0
- package/src/core/skeletonizer.js +280 -21
- package/src/templates/architect-prompt.template.md +34 -0
- package/src/templates/multiAgent.md +68 -15
- package/src/templates/opencode/coder.template.md +53 -17
- package/src/templates/opencode/junior-architect.template.md +54 -15
- package/src/templates/skeleton-instruction.md +1 -1
- package/src/templates/update-prompt.template.md +2 -0
- package/src/utils/aiHeader.js +57 -27
- package/src/utils/claudeMdGenerator.js +182 -88
- package/src/utils/fileUtils.js +217 -149
- 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,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
|
-
}
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { SingleBar, Presets } from 'cli-progress';
|
|
4
|
-
import pLimit from 'p-limit';
|
|
5
|
-
import zlib from 'zlib';
|
|
6
|
-
import { promisify } from 'util';
|
|
7
|
-
import inquirer from 'inquirer';
|
|
8
|
-
|
|
9
|
-
import { parseSnapshotContent, filterFilesToRestore, validateFilePaths } from '../../utils/fileUtils.js';
|
|
10
|
-
|
|
11
|
-
const gunzip = promisify(zlib.gunzip);
|
|
12
|
-
|
|
13
|
-
export async function restoreSnapshot(snapshotFile, targetDir, options) {
|
|
14
|
-
const absoluteSnapshotPath = path.resolve(snapshotFile);
|
|
15
|
-
const absoluteTargetDir = path.resolve(targetDir);
|
|
16
|
-
|
|
17
|
-
console.log(`📄 Starting restore from snapshot: ${absoluteSnapshotPath}`);
|
|
18
|
-
console.log(`📁 Target directory: ${absoluteTargetDir}`);
|
|
19
|
-
|
|
20
|
-
try {
|
|
21
|
-
let rawContent;
|
|
22
|
-
|
|
23
|
-
if (snapshotFile.endsWith('.gz')) {
|
|
24
|
-
const compressedBuffer = await fs.readFile(absoluteSnapshotPath);
|
|
25
|
-
rawContent = (await gunzip(compressedBuffer)).toString('utf-8');
|
|
26
|
-
console.log('✅ Decompressed gzipped snapshot');
|
|
27
|
-
} else {
|
|
28
|
-
rawContent = await fs.readFile(absoluteSnapshotPath, 'utf-8');
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
let filesToRestore;
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
const jsonData = JSON.parse(rawContent);
|
|
35
|
-
if (jsonData.content) {
|
|
36
|
-
console.log('📄 Detected JSON format, extracting content');
|
|
37
|
-
filesToRestore = parseSnapshotContent(jsonData.content);
|
|
38
|
-
} else {
|
|
39
|
-
throw new Error('JSON format detected, but no "content" key found');
|
|
40
|
-
}
|
|
41
|
-
} catch (e) {
|
|
42
|
-
console.log('📄 Treating snapshot as plain text format');
|
|
43
|
-
filesToRestore = parseSnapshotContent(rawContent);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (filesToRestore.length === 0) {
|
|
47
|
-
console.warn('⚠️ No files found to restore in the snapshot');
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (options.include || options.exclude) {
|
|
52
|
-
filesToRestore = filterFilesToRestore(filesToRestore, options);
|
|
53
|
-
if (filesToRestore.length === 0) {
|
|
54
|
-
console.warn('⚠️ No files remaining after applying filters');
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const invalidFiles = validateFilePaths(filesToRestore, absoluteTargetDir);
|
|
60
|
-
if (invalidFiles.length > 0) {
|
|
61
|
-
console.error('❌ Invalid file paths detected (potential directory traversal):');
|
|
62
|
-
invalidFiles.forEach(file => console.error(` ${file}`));
|
|
63
|
-
process.exit(1);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
console.log(`📊 Found ${filesToRestore.length} files to restore`);
|
|
67
|
-
|
|
68
|
-
if (options.dryRun) {
|
|
69
|
-
console.log('\n🔍 Dry run mode - files that would be restored:');
|
|
70
|
-
filesToRestore.forEach(file => {
|
|
71
|
-
const fullPath = path.join(absoluteTargetDir, file.path);
|
|
72
|
-
console.log(` ${fullPath}`);
|
|
73
|
-
});
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (!options.force) {
|
|
78
|
-
const { confirm } = await inquirer.prompt([{
|
|
79
|
-
type: 'confirm',
|
|
80
|
-
name: 'confirm',
|
|
81
|
-
message: `You are about to write ${filesToRestore.length} files to ${absoluteTargetDir}. Existing files will be overwritten. Continue?`,
|
|
82
|
-
default: false
|
|
83
|
-
}]);
|
|
84
|
-
|
|
85
|
-
if (!confirm) {
|
|
86
|
-
console.log('🚫 Restore operation cancelled by user');
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
await fs.mkdir(absoluteTargetDir, { recursive: true });
|
|
92
|
-
|
|
93
|
-
const stats = {
|
|
94
|
-
totalFiles: filesToRestore.length,
|
|
95
|
-
restoredFiles: 0,
|
|
96
|
-
failedFiles: 0,
|
|
97
|
-
errors: []
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
const progressBar = options.verbose ? null : new SingleBar({
|
|
101
|
-
format: 'Restoring |{bar}| {percentage}% | {value}/{total} files',
|
|
102
|
-
barCompleteChar: '\u2588',
|
|
103
|
-
barIncompleteChar: '\u2591',
|
|
104
|
-
hideCursor: true
|
|
105
|
-
}, Presets.shades_classic);
|
|
106
|
-
|
|
107
|
-
if (progressBar) progressBar.start(filesToRestore.length, 0);
|
|
108
|
-
|
|
109
|
-
const limit = pLimit(options.concurrency || 10);
|
|
110
|
-
const filePromises = filesToRestore.map((file, index) =>
|
|
111
|
-
limit(async () => {
|
|
112
|
-
try {
|
|
113
|
-
const fullPath = path.join(absoluteTargetDir, file.path);
|
|
114
|
-
const dir = path.dirname(fullPath);
|
|
115
|
-
|
|
116
|
-
await fs.mkdir(dir, { recursive: true });
|
|
117
|
-
await fs.writeFile(fullPath, file.content, 'utf-8');
|
|
118
|
-
|
|
119
|
-
stats.restoredFiles++;
|
|
120
|
-
|
|
121
|
-
if (progressBar) {
|
|
122
|
-
progressBar.update(index + 1);
|
|
123
|
-
} else if (options.verbose) {
|
|
124
|
-
console.log(`✅ Restored: ${file.path}`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return { success: true, file: file.path };
|
|
128
|
-
} catch (error) {
|
|
129
|
-
stats.failedFiles++;
|
|
130
|
-
stats.errors.push({ file: file.path, error: error.message });
|
|
131
|
-
|
|
132
|
-
if (options.verbose) {
|
|
133
|
-
console.log(`❌ Failed to restore: ${file.path} - ${error.message}`);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return { success: false, file: file.path, error: error.message };
|
|
137
|
-
}
|
|
138
|
-
})
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
await Promise.allSettled(filePromises);
|
|
142
|
-
if (progressBar) progressBar.stop();
|
|
143
|
-
|
|
144
|
-
console.log('\n📊 Restore Summary');
|
|
145
|
-
console.log('='.repeat(50));
|
|
146
|
-
console.log(`🎉 Restore completed!`);
|
|
147
|
-
console.log(`✅ Successfully restored: ${stats.restoredFiles} files`);
|
|
148
|
-
|
|
149
|
-
if (stats.failedFiles > 0) {
|
|
150
|
-
console.log(`❌ Failed to restore: ${stats.failedFiles} files`);
|
|
151
|
-
if (stats.errors.length > 0) {
|
|
152
|
-
console.log('\n⚠️ Errors encountered:');
|
|
153
|
-
stats.errors.slice(0, 5).forEach(({ file, error }) => {
|
|
154
|
-
console.log(` ${file}: ${error}`);
|
|
155
|
-
});
|
|
156
|
-
if (stats.errors.length > 5) {
|
|
157
|
-
console.log(` ... and ${stats.errors.length - 5} more errors`);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
console.log(`📁 Target directory: ${absoluteTargetDir}`);
|
|
163
|
-
console.log('='.repeat(50));
|
|
164
|
-
|
|
165
|
-
} catch (error) {
|
|
166
|
-
console.error('\n❌ An error occurred during restore:');
|
|
167
|
-
console.error(error.message);
|
|
168
|
-
if (options.verbose) {
|
|
169
|
-
console.error(error.stack);
|
|
170
|
-
}
|
|
171
|
-
process.exit(1);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import which from 'which';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import fs from 'fs/promises';
|
|
4
|
-
import os from 'os';
|
|
5
|
-
import chalk from 'chalk';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Sets up claude.toml configuration for gemini-cli integration with dynamic paths
|
|
9
|
-
* @param {Object} options - Command options
|
|
10
|
-
*/
|
|
11
|
-
export async function setupGemini(options = {}) {
|
|
12
|
-
try {
|
|
13
|
-
console.log(chalk.blue('🔧 Setting up gemini-cli integration with dynamic paths...'));
|
|
14
|
-
|
|
15
|
-
// Check if gemini-cli is installed
|
|
16
|
-
let geminiCliPath;
|
|
17
|
-
try {
|
|
18
|
-
geminiCliPath = await which('gemini-cli');
|
|
19
|
-
console.log(chalk.green(`✅ Found gemini-cli at: ${geminiCliPath}`));
|
|
20
|
-
} catch (error) {
|
|
21
|
-
console.error(chalk.red('❌ gemini-cli not found in PATH'));
|
|
22
|
-
console.log(chalk.yellow('💡 Please install gemini-cli first:'));
|
|
23
|
-
console.log(chalk.cyan(' npm install -g gemini-cli'));
|
|
24
|
-
process.exit(1);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Get current working directory for dynamic path resolution
|
|
28
|
-
const currentDir = process.cwd();
|
|
29
|
-
const indexJsPath = path.join(currentDir, 'index.js');
|
|
30
|
-
|
|
31
|
-
// Verify index.js exists
|
|
32
|
-
try {
|
|
33
|
-
await fs.access(indexJsPath);
|
|
34
|
-
console.log(chalk.green(`✅ Found eck-snapshot index.js at: ${indexJsPath}`));
|
|
35
|
-
} catch (error) {
|
|
36
|
-
console.error(chalk.red(`❌ Could not find index.js at: ${indexJsPath}`));
|
|
37
|
-
console.log(chalk.yellow('💡 Make sure you are running this command from the eck-snapshot project directory'));
|
|
38
|
-
process.exit(1);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Create gemini tools directory
|
|
42
|
-
const homeDir = os.homedir();
|
|
43
|
-
const geminiToolsDir = path.join(homeDir, '.gemini', 'tools');
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
await fs.mkdir(geminiToolsDir, { recursive: true });
|
|
47
|
-
console.log(chalk.green(`✅ Created/verified gemini tools directory: ${geminiToolsDir}`));
|
|
48
|
-
} catch (error) {
|
|
49
|
-
console.error(chalk.red(`❌ Failed to create gemini tools directory: ${error.message}`));
|
|
50
|
-
process.exit(1);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Read environment variables from setup.json if available
|
|
54
|
-
let envVars = {};
|
|
55
|
-
try {
|
|
56
|
-
const setupJsonPath = path.join(currentDir, 'setup.json');
|
|
57
|
-
const setupContent = await fs.readFile(setupJsonPath, 'utf-8');
|
|
58
|
-
const setupData = JSON.parse(setupContent);
|
|
59
|
-
|
|
60
|
-
// Extract relevant environment variables
|
|
61
|
-
if (setupData.environmentDetection) {
|
|
62
|
-
envVars.ECK_SNAPSHOT_PATH = currentDir;
|
|
63
|
-
console.log(chalk.blue(`📋 Using project context from setup.json`));
|
|
64
|
-
}
|
|
65
|
-
} catch (error) {
|
|
66
|
-
console.log(chalk.yellow('⚠️ setup.json not found or invalid, using defaults'));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Generate claude.toml content with dynamic paths
|
|
70
|
-
const claudeTomlContent = generateClaudeToml(indexJsPath, envVars);
|
|
71
|
-
|
|
72
|
-
// Write claude.toml file
|
|
73
|
-
const claudeTomlPath = path.join(geminiToolsDir, 'claude.toml');
|
|
74
|
-
try {
|
|
75
|
-
await fs.writeFile(claudeTomlPath, claudeTomlContent, 'utf-8');
|
|
76
|
-
console.log(chalk.green(`✅ Generated claude.toml at: ${claudeTomlPath}`));
|
|
77
|
-
} catch (error) {
|
|
78
|
-
console.error(chalk.red(`❌ Failed to write claude.toml: ${error.message}`));
|
|
79
|
-
process.exit(1);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Success summary
|
|
83
|
-
console.log(chalk.green('\n🎉 Setup completed successfully!'));
|
|
84
|
-
console.log(chalk.blue('\n📋 Configuration summary:'));
|
|
85
|
-
console.log(chalk.cyan(` • gemini-cli: ${geminiCliPath}`));
|
|
86
|
-
console.log(chalk.cyan(` • eck-snapshot: ${indexJsPath}`));
|
|
87
|
-
console.log(chalk.cyan(` • claude.toml: ${claudeTomlPath}`));
|
|
88
|
-
|
|
89
|
-
if (Object.keys(envVars).length > 0) {
|
|
90
|
-
console.log(chalk.cyan(` • Environment variables: ${Object.keys(envVars).join(', ')}`));
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
console.log(chalk.blue('\n🚀 You can now use:'));
|
|
94
|
-
console.log(chalk.cyan(' gemini-cli claude "Your prompt here"'));
|
|
95
|
-
console.log(chalk.green('\n✨ Cross-platform path resolution is automatically handled!'));
|
|
96
|
-
|
|
97
|
-
} catch (error) {
|
|
98
|
-
console.error(chalk.red(`❌ Setup failed: ${error.message}`));
|
|
99
|
-
if (options.verbose) {
|
|
100
|
-
console.error(chalk.red('Stack trace:'), error.stack);
|
|
101
|
-
}
|
|
102
|
-
process.exit(1);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Generates claude.toml content with dynamic paths
|
|
108
|
-
* @param {string} indexJsPath - Path to eck-snapshot index.js
|
|
109
|
-
* @param {Object} envVars - Environment variables to include
|
|
110
|
-
* @returns {string} - Generated TOML content
|
|
111
|
-
*/
|
|
112
|
-
function generateClaudeToml(indexJsPath, envVars = {}) {
|
|
113
|
-
const envSection = Object.keys(envVars).length > 0
|
|
114
|
-
? `# Environment variables from setup.json
|
|
115
|
-
${Object.entries(envVars).map(([key, value]) => `${key} = "${value}"`).join('\n')}
|
|
116
|
-
|
|
117
|
-
`
|
|
118
|
-
: '';
|
|
119
|
-
|
|
120
|
-
return `# Claude.toml - Dynamic configuration for eck-snapshot integration
|
|
121
|
-
# Generated automatically by 'eck-snapshot setup-gemini'
|
|
122
|
-
# This file uses dynamic paths to work across WSL/Windows environments
|
|
123
|
-
|
|
124
|
-
${envSection}[claude]
|
|
125
|
-
# eck-snapshot integration for AI-powered repository analysis
|
|
126
|
-
name = "eck-snapshot"
|
|
127
|
-
description = "AI-powered repository snapshot and analysis tool with cross-platform support"
|
|
128
|
-
command = "node"
|
|
129
|
-
args = ["${indexJsPath}", "ask-claude"]
|
|
130
|
-
|
|
131
|
-
# Command examples:
|
|
132
|
-
# gemini-cli claude "Create a snapshot of the current project"
|
|
133
|
-
# gemini-cli claude "Analyze the database structure"
|
|
134
|
-
# gemini-cli claude "Generate a project overview"
|
|
135
|
-
|
|
136
|
-
[claude.metadata]
|
|
137
|
-
version = "5.0.0"
|
|
138
|
-
author = "eck-snapshot"
|
|
139
|
-
generated_at = "${new Date().toISOString()}"
|
|
140
|
-
platform = "${process.platform}"
|
|
141
|
-
node_version = "${process.version}"
|
|
142
|
-
working_directory = "${path.dirname(indexJsPath)}"
|
|
143
|
-
|
|
144
|
-
# Cross-platform compatibility notes:
|
|
145
|
-
# - Paths are automatically resolved using process.cwd()
|
|
146
|
-
# - Works in WSL, Windows, macOS, and Linux
|
|
147
|
-
# - No hardcoded /mnt/c/ paths required
|
|
148
|
-
`;
|
|
149
|
-
}
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
|
|
3
|
-
describe('setupGemini integration', () => {
|
|
4
|
-
it('should validate path resolution logic', () => {
|
|
5
|
-
// Test path join functionality that setupGemini uses
|
|
6
|
-
const currentDir = '/test/project';
|
|
7
|
-
const indexJsPath = `${currentDir}/index.js`;
|
|
8
|
-
|
|
9
|
-
expect(indexJsPath).toBe('/test/project/index.js');
|
|
10
|
-
expect(indexJsPath).toContain('index.js');
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it('should validate gemini tools directory structure', () => {
|
|
14
|
-
const homeDir = '/home/user';
|
|
15
|
-
const geminiToolsDir = `${homeDir}/.gemini/tools`;
|
|
16
|
-
const claudeTomlPath = `${geminiToolsDir}/claude.toml`;
|
|
17
|
-
|
|
18
|
-
expect(geminiToolsDir).toBe('/home/user/.gemini/tools');
|
|
19
|
-
expect(claudeTomlPath).toBe('/home/user/.gemini/tools/claude.toml');
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('should validate TOML content structure', () => {
|
|
23
|
-
const indexJsPath = '/test/project/index.js';
|
|
24
|
-
const envVars = { ECK_SNAPSHOT_PATH: '/test/project' };
|
|
25
|
-
|
|
26
|
-
// Test environment section generation
|
|
27
|
-
const envSection = Object.keys(envVars).length > 0
|
|
28
|
-
? `# Environment variables from setup.json\n${Object.entries(envVars).map(([key, value]) => `${key} = "${value}"`).join('\n')}\n\n`
|
|
29
|
-
: '';
|
|
30
|
-
|
|
31
|
-
// Test main TOML structure
|
|
32
|
-
const tomlContent = `# Claude.toml - Dynamic configuration for eck-snapshot integration
|
|
33
|
-
# Generated automatically by 'eck-snapshot setup-gemini'
|
|
34
|
-
|
|
35
|
-
${envSection}[claude]
|
|
36
|
-
name = "eck-snapshot"
|
|
37
|
-
description = "AI-powered repository snapshot and analysis tool with cross-platform support"
|
|
38
|
-
command = "node"
|
|
39
|
-
args = ["${indexJsPath}", "ask-claude"]
|
|
40
|
-
|
|
41
|
-
[claude.metadata]
|
|
42
|
-
version = "4.1.0"
|
|
43
|
-
author = "eck-snapshot"
|
|
44
|
-
platform = "${process.platform}"
|
|
45
|
-
working_directory = "${indexJsPath.replace('/index.js', '')}"`;
|
|
46
|
-
|
|
47
|
-
expect(tomlContent).toContain('[claude]');
|
|
48
|
-
expect(tomlContent).toContain('name = "eck-snapshot"');
|
|
49
|
-
expect(tomlContent).toContain(`args = ["${indexJsPath}", "ask-claude"]`);
|
|
50
|
-
expect(tomlContent).toContain('[claude.metadata]');
|
|
51
|
-
expect(tomlContent).toContain('ECK_SNAPSHOT_PATH = "/test/project"');
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('should handle cross-platform paths correctly', () => {
|
|
55
|
-
const testPaths = [
|
|
56
|
-
{ platform: 'windows', path: 'C:\\Users\\test\\project\\index.js' },
|
|
57
|
-
{ platform: 'unix', path: '/home/user/project/index.js' },
|
|
58
|
-
{ platform: 'wsl', path: '/mnt/c/Users/test/project/index.js' }
|
|
59
|
-
];
|
|
60
|
-
|
|
61
|
-
testPaths.forEach(({ platform, path }) => {
|
|
62
|
-
expect(path).toContain('index.js');
|
|
63
|
-
expect(path.length).toBeGreaterThan(0);
|
|
64
|
-
|
|
65
|
-
// Test that the path is absolute (platform-appropriate)
|
|
66
|
-
if (platform === 'windows') {
|
|
67
|
-
expect(path).toMatch(/^[A-Z]:\\/);
|
|
68
|
-
} else {
|
|
69
|
-
expect(path).toMatch(/^\//);
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('should validate error handling patterns', () => {
|
|
75
|
-
// Test error message patterns that setupGemini should handle
|
|
76
|
-
const errorPatterns = [
|
|
77
|
-
'gemini-cli not found in PATH',
|
|
78
|
-
'Could not find index.js',
|
|
79
|
-
'Failed to create gemini tools directory',
|
|
80
|
-
'Failed to write claude.toml'
|
|
81
|
-
];
|
|
82
|
-
|
|
83
|
-
errorPatterns.forEach(pattern => {
|
|
84
|
-
expect(pattern).toBeDefined();
|
|
85
|
-
expect(typeof pattern).toBe('string');
|
|
86
|
-
expect(pattern.length).toBeGreaterThan(0);
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('should test JSON parsing for setup.json', () => {
|
|
91
|
-
const validSetupData = {
|
|
92
|
-
environmentDetection: {
|
|
93
|
-
detected: true
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
const jsonString = JSON.stringify(validSetupData);
|
|
98
|
-
const parsed = JSON.parse(jsonString);
|
|
99
|
-
|
|
100
|
-
expect(parsed.environmentDetection).toBeDefined();
|
|
101
|
-
expect(parsed.environmentDetection.detected).toBe(true);
|
|
102
|
-
|
|
103
|
-
// Test invalid JSON handling pattern
|
|
104
|
-
const invalidJson = 'invalid json {';
|
|
105
|
-
let parseError = null;
|
|
106
|
-
try {
|
|
107
|
-
JSON.parse(invalidJson);
|
|
108
|
-
} catch (e) {
|
|
109
|
-
parseError = e;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
expect(parseError).toBeDefined();
|
|
113
|
-
expect(parseError.message).toContain('JSON');
|
|
114
|
-
});
|
|
115
|
-
});
|