@xelth/eck-snapshot 2.2.0 โ 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +119 -225
- package/index.js +14 -776
- package/package.json +25 -7
- package/setup.json +805 -0
- package/src/cli/cli.js +427 -0
- package/src/cli/commands/askGpt.js +29 -0
- package/src/cli/commands/autoDocs.js +150 -0
- package/src/cli/commands/consilium.js +86 -0
- package/src/cli/commands/createSnapshot.js +601 -0
- package/src/cli/commands/detectProfiles.js +98 -0
- package/src/cli/commands/detectProject.js +112 -0
- package/src/cli/commands/generateProfileGuide.js +91 -0
- package/src/cli/commands/pruneSnapshot.js +106 -0
- package/src/cli/commands/restoreSnapshot.js +173 -0
- package/src/cli/commands/setupGemini.js +149 -0
- package/src/cli/commands/setupGemini.test.js +115 -0
- package/src/cli/commands/trainTokens.js +38 -0
- package/src/config.js +81 -0
- package/src/services/authService.js +20 -0
- package/src/services/claudeCliService.js +621 -0
- package/src/services/claudeCliService.test.js +267 -0
- package/src/services/dispatcherService.js +33 -0
- package/src/services/gptService.js +302 -0
- package/src/services/gptService.test.js +120 -0
- package/src/templates/agent-prompt.template.md +29 -0
- package/src/templates/architect-prompt.template.md +50 -0
- package/src/templates/envScanRequest.md +4 -0
- package/src/templates/gitWorkflow.md +32 -0
- package/src/templates/multiAgent.md +164 -0
- package/src/templates/vectorMode.md +22 -0
- package/src/utils/aiHeader.js +303 -0
- package/src/utils/fileUtils.js +928 -0
- package/src/utils/projectDetector.js +704 -0
- package/src/utils/tokenEstimator.js +198 -0
- package/.ecksnapshot.config.js +0 -35
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { detectProjectType, getProjectSpecificFiltering } from '../../utils/projectDetector.js';
|
|
2
|
+
import { displayProjectInfo } from '../../utils/fileUtils.js';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Command to detect and display project information
|
|
7
|
+
* @param {string} projectPath - Path to the project
|
|
8
|
+
* @param {object} options - Command options
|
|
9
|
+
*/
|
|
10
|
+
export async function detectProject(projectPath = '.', options = {}) {
|
|
11
|
+
console.log(chalk.blue('๐ Detecting project type...\n'));
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
// Detect project type
|
|
15
|
+
const detection = await detectProjectType(projectPath);
|
|
16
|
+
displayProjectInfo(detection);
|
|
17
|
+
|
|
18
|
+
// Show filtering rules that would be applied
|
|
19
|
+
if (detection.type !== 'unknown') {
|
|
20
|
+
const filtering = await getProjectSpecificFiltering(detection.type);
|
|
21
|
+
|
|
22
|
+
if (filtering.filesToIgnore.length > 0 ||
|
|
23
|
+
filtering.dirsToIgnore.length > 0 ||
|
|
24
|
+
filtering.extensionsToIgnore.length > 0) {
|
|
25
|
+
console.log(chalk.yellow('๐ Project-specific filtering rules:'));
|
|
26
|
+
|
|
27
|
+
if (filtering.filesToIgnore.length > 0) {
|
|
28
|
+
console.log(` Files to ignore: ${filtering.filesToIgnore.join(', ')}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (filtering.dirsToIgnore.length > 0) {
|
|
32
|
+
console.log(` Directories to ignore: ${filtering.dirsToIgnore.join(', ')}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (filtering.extensionsToIgnore.length > 0) {
|
|
36
|
+
console.log(` Extensions to ignore: ${filtering.extensionsToIgnore.join(', ')}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log('');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Show Android parsing info if it's an Android project
|
|
44
|
+
if (detection.type === 'android') {
|
|
45
|
+
console.log(chalk.green('๐ค Android parsing supported via unified segmenter'));
|
|
46
|
+
console.log('');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Show verbose details if requested
|
|
50
|
+
if (options.verbose && detection.allDetections) {
|
|
51
|
+
console.log(chalk.blue('๐ All detection results:'));
|
|
52
|
+
for (const result of detection.allDetections) {
|
|
53
|
+
console.log(` ${result.type}: score ${result.score}, priority ${result.priority}`);
|
|
54
|
+
}
|
|
55
|
+
console.log('');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Provide suggestions
|
|
59
|
+
console.log(chalk.blue('๐ก Suggested commands:'));
|
|
60
|
+
|
|
61
|
+
if (detection.type === 'android') {
|
|
62
|
+
console.log(' eck-snapshot snapshot --profile android-core # Core Android files');
|
|
63
|
+
console.log(' eck-snapshot snapshot --profile android-config # Build configuration');
|
|
64
|
+
} else if (detection.type === 'nodejs') {
|
|
65
|
+
console.log(' eck-snapshot snapshot --profile backend # Backend code');
|
|
66
|
+
console.log(' eck-snapshot snapshot --profile frontend # Frontend code');
|
|
67
|
+
} else {
|
|
68
|
+
console.log(' eck-snapshot snapshot # Full project snapshot');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error(chalk.red('โ Error detecting project:'), error.message);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Command to test file parsing using the unified segmenter
|
|
79
|
+
* @param {string} filePath - Path to the file to test
|
|
80
|
+
* @param {object} options - Command options
|
|
81
|
+
*/
|
|
82
|
+
export async function testFileParsing(filePath, options = {}) {
|
|
83
|
+
console.log(chalk.blue(`๐งช Testing file parsing: ${filePath}\n`));
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const { segmentFile } = await import('../../core/segmenter.js');
|
|
87
|
+
const fs = await import('fs/promises');
|
|
88
|
+
|
|
89
|
+
// Read file content
|
|
90
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
91
|
+
console.log(chalk.blue(`๐ File size: ${content.length} characters`));
|
|
92
|
+
|
|
93
|
+
// Parse file using unified segmenter
|
|
94
|
+
const chunks = await segmentFile(filePath);
|
|
95
|
+
|
|
96
|
+
console.log(chalk.green(`\n๐ฏ Extracted ${chunks.length} chunks:`));
|
|
97
|
+
|
|
98
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
99
|
+
const chunk = chunks[i];
|
|
100
|
+
console.log(`\n${i + 1}. ${chalk.yellow(chunk.chunk_name)} (${chunk.chunk_type})`);
|
|
101
|
+
|
|
102
|
+
if (options.showContent) {
|
|
103
|
+
const preview = chunk.code.substring(0, 200);
|
|
104
|
+
console.log(chalk.gray(` Content preview: ${preview}${chunk.code.length > 200 ? '...' : ''}`));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error(chalk.red('โ Error parsing file:'), error.message);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { loadSetupConfig } from '../../config.js';
|
|
5
|
+
import { scanDirectoryRecursively, generateDirectoryTree, initializeEckManifest, loadConfig } from '../../utils/fileUtils.js';
|
|
6
|
+
|
|
7
|
+
function buildPrompt(projectPath) {
|
|
8
|
+
const normalizedPath = path.resolve(projectPath);
|
|
9
|
+
return `You are a code architect helping a developer curate manual context profiles for a repository.
|
|
10
|
+
Project root: ${normalizedPath}
|
|
11
|
+
|
|
12
|
+
Use the project directory tree provided separately to identify logical groupings of files that should travel together during focused work.
|
|
13
|
+
|
|
14
|
+
Instructions:
|
|
15
|
+
1. Propose profile names that reflect the responsibilities or layers of the codebase.
|
|
16
|
+
2. For each profile, produce an object with "include" and "exclude" arrays of glob patterns (minimize overlap, prefer directory-level globs).
|
|
17
|
+
3. Always include a sensible catch-all profile (for example, "default") if one is not obvious.
|
|
18
|
+
4. Call out generated assets, tests, or vendor files in "exclude" arrays when appropriate.
|
|
19
|
+
5. Return **only** valid JSON. Do not wrap the response in markdown fences or add commentary.
|
|
20
|
+
`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function buildGuideContent({ prompt, directoryTree }) {
|
|
24
|
+
const timestamp = new Date().toISOString();
|
|
25
|
+
const trimmedTree = directoryTree.trimEnd();
|
|
26
|
+
|
|
27
|
+
return [
|
|
28
|
+
'# Profile Generation Guide',
|
|
29
|
+
'',
|
|
30
|
+
`Generated: ${timestamp}`,
|
|
31
|
+
'',
|
|
32
|
+
'## How to Use',
|
|
33
|
+
'- Copy the prompt below into your AI assistant or follow it yourself.',
|
|
34
|
+
'- When using an AI, paste the directory tree afterward so it has full project context.',
|
|
35
|
+
"- Review the suggested profiles, then save the JSON to `.eck/profiles.json` when you are satisfied.",
|
|
36
|
+
'',
|
|
37
|
+
'## Recommended Prompt',
|
|
38
|
+
'```text',
|
|
39
|
+
prompt.trimEnd(),
|
|
40
|
+
'```',
|
|
41
|
+
'',
|
|
42
|
+
'## Project Directory Tree',
|
|
43
|
+
'```text',
|
|
44
|
+
trimmedTree,
|
|
45
|
+
'```',
|
|
46
|
+
''
|
|
47
|
+
].join('\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function generateProfileGuide(repoPath = process.cwd(), options = {}) {
|
|
51
|
+
const spinner = ora('Preparing profile generation guide...').start();
|
|
52
|
+
const projectPath = path.resolve(repoPath);
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
spinner.text = 'Ensuring .eck manifest directory is initialized...';
|
|
56
|
+
await initializeEckManifest(projectPath);
|
|
57
|
+
|
|
58
|
+
spinner.text = 'Loading configuration...';
|
|
59
|
+
const setupConfig = await loadSetupConfig();
|
|
60
|
+
const userConfig = await loadConfig(options.config);
|
|
61
|
+
const combinedConfig = {
|
|
62
|
+
...userConfig,
|
|
63
|
+
...(setupConfig.fileFiltering || {}),
|
|
64
|
+
...(setupConfig.performance || {})
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
spinner.text = 'Scanning repository files...';
|
|
68
|
+
const allFiles = await scanDirectoryRecursively(projectPath, combinedConfig, projectPath);
|
|
69
|
+
|
|
70
|
+
spinner.text = 'Building directory tree...';
|
|
71
|
+
const maxDepth = Number(combinedConfig.maxDepth ?? 10);
|
|
72
|
+
const directoryTree = await generateDirectoryTree(projectPath, '', allFiles, 0, Number.isFinite(maxDepth) ? maxDepth : 10, combinedConfig);
|
|
73
|
+
|
|
74
|
+
if (!directoryTree) {
|
|
75
|
+
throw new Error('Failed to generate directory tree or project is empty.');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const prompt = buildPrompt(projectPath);
|
|
79
|
+
const guideContent = buildGuideContent({ prompt, directoryTree });
|
|
80
|
+
const guidePath = path.join(projectPath, '.eck', 'profile_generation_guide.md');
|
|
81
|
+
|
|
82
|
+
await fs.mkdir(path.dirname(guidePath), { recursive: true });
|
|
83
|
+
spinner.text = 'Writing guide to .eck/profile_generation_guide.md...';
|
|
84
|
+
await fs.writeFile(guidePath, guideContent, 'utf-8');
|
|
85
|
+
|
|
86
|
+
spinner.succeed(`Profile generation guide saved to ${guidePath}`);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
spinner.fail(`Failed to generate profile guide: ${error.message}`);
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
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 = "4.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
|
+
}
|