@nclamvn/vibecode-cli 1.3.0 → 1.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/.vibecode/learning/fixes.json +1 -0
- package/.vibecode/learning/preferences.json +1 -0
- package/README.md +310 -49
- package/bin/vibecode.js +103 -2
- package/package.json +4 -2
- package/src/agent/decomposition.js +476 -0
- package/src/agent/index.js +325 -0
- package/src/agent/memory.js +542 -0
- package/src/agent/orchestrator.js +713 -0
- package/src/agent/self-healing.js +516 -0
- package/src/commands/agent.js +255 -0
- package/src/commands/assist.js +413 -0
- package/src/commands/build.js +13 -3
- package/src/commands/debug.js +457 -0
- package/src/commands/go.js +387 -0
- package/src/commands/learn.js +294 -0
- package/src/commands/undo.js +281 -0
- package/src/commands/wizard.js +322 -0
- package/src/core/backup.js +325 -0
- package/src/core/learning.js +295 -0
- package/src/core/test-runner.js +38 -5
- package/src/debug/analyzer.js +329 -0
- package/src/debug/evidence.js +228 -0
- package/src/debug/fixer.js +348 -0
- package/src/debug/index.js +378 -0
- package/src/debug/verifier.js +346 -0
- package/src/index.js +62 -0
- package/src/ui/__tests__/error-translator.test.js +390 -0
- package/src/ui/dashboard.js +364 -0
- package/src/ui/error-translator.js +775 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// VIBECODE CLI - Backup Manager
|
|
3
|
+
// Phase H4: Undo/Rollback - Auto-backup before every action
|
|
4
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
import fs from 'fs-extra';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { createHash } from 'crypto';
|
|
9
|
+
|
|
10
|
+
const BACKUP_DIR = '.vibecode/backups';
|
|
11
|
+
const MAX_BACKUPS = 10;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* BackupManager - Creates and manages backups for undo functionality
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* const backup = new BackupManager();
|
|
18
|
+
* const id = await backup.createBackup('build');
|
|
19
|
+
* // ... do stuff ...
|
|
20
|
+
* await backup.restore(id); // Undo!
|
|
21
|
+
*/
|
|
22
|
+
export class BackupManager {
|
|
23
|
+
constructor(projectPath = process.cwd()) {
|
|
24
|
+
this.projectPath = projectPath;
|
|
25
|
+
this.backupPath = path.join(projectPath, BACKUP_DIR);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Initialize backup directory
|
|
30
|
+
*/
|
|
31
|
+
async init() {
|
|
32
|
+
await fs.ensureDir(this.backupPath);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create backup before action
|
|
37
|
+
* @param {string} actionName - Name of action (build, agent, go, etc.)
|
|
38
|
+
* @param {string[]} files - Specific files to backup (optional)
|
|
39
|
+
* @returns {string} Backup ID
|
|
40
|
+
*/
|
|
41
|
+
async createBackup(actionName, files = null) {
|
|
42
|
+
await this.init();
|
|
43
|
+
|
|
44
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
45
|
+
const backupId = `${timestamp}_${actionName}`;
|
|
46
|
+
const backupDir = path.join(this.backupPath, backupId);
|
|
47
|
+
|
|
48
|
+
await fs.ensureDir(backupDir);
|
|
49
|
+
|
|
50
|
+
// If specific files provided, backup only those
|
|
51
|
+
// Otherwise, backup common source files
|
|
52
|
+
const filesToBackup = files || await this.getSourceFiles();
|
|
53
|
+
|
|
54
|
+
const manifest = {
|
|
55
|
+
id: backupId,
|
|
56
|
+
action: actionName,
|
|
57
|
+
timestamp: new Date().toISOString(),
|
|
58
|
+
files: []
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
for (const file of filesToBackup) {
|
|
62
|
+
try {
|
|
63
|
+
const sourcePath = path.join(this.projectPath, file);
|
|
64
|
+
const stats = await fs.stat(sourcePath).catch(() => null);
|
|
65
|
+
|
|
66
|
+
if (stats && stats.isFile()) {
|
|
67
|
+
const content = await fs.readFile(sourcePath);
|
|
68
|
+
const hash = createHash('md5').update(content).digest('hex');
|
|
69
|
+
|
|
70
|
+
// Replace slashes with __ for flat storage
|
|
71
|
+
const backupFileName = file.replace(/[/\\]/g, '__');
|
|
72
|
+
const backupFilePath = path.join(backupDir, backupFileName);
|
|
73
|
+
|
|
74
|
+
await fs.writeFile(backupFilePath, content);
|
|
75
|
+
|
|
76
|
+
manifest.files.push({
|
|
77
|
+
path: file,
|
|
78
|
+
hash,
|
|
79
|
+
size: stats.size,
|
|
80
|
+
backupName: backupFileName
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
// Skip files that can't be backed up
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Save manifest
|
|
89
|
+
await fs.writeFile(
|
|
90
|
+
path.join(backupDir, 'manifest.json'),
|
|
91
|
+
JSON.stringify(manifest, null, 2)
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Cleanup old backups
|
|
95
|
+
await this.cleanupOldBackups();
|
|
96
|
+
|
|
97
|
+
return backupId;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get list of source files to backup
|
|
102
|
+
* @returns {string[]} File paths relative to project
|
|
103
|
+
*/
|
|
104
|
+
async getSourceFiles() {
|
|
105
|
+
const files = [];
|
|
106
|
+
const ignoreDirs = [
|
|
107
|
+
'node_modules', '.git', '.next', 'dist', 'build',
|
|
108
|
+
'.vibecode/backups', 'coverage', '.cache', '__pycache__'
|
|
109
|
+
];
|
|
110
|
+
const extensions = [
|
|
111
|
+
'.js', '.ts', '.tsx', '.jsx', '.json', '.css', '.scss',
|
|
112
|
+
'.html', '.md', '.vue', '.svelte', '.prisma', '.env'
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
const scan = async (dir, prefix = '') => {
|
|
116
|
+
try {
|
|
117
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
118
|
+
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
const fullPath = path.join(dir, entry.name);
|
|
121
|
+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
122
|
+
|
|
123
|
+
if (entry.isDirectory()) {
|
|
124
|
+
if (!ignoreDirs.includes(entry.name) && !entry.name.startsWith('.')) {
|
|
125
|
+
await scan(fullPath, relativePath);
|
|
126
|
+
}
|
|
127
|
+
} else if (entry.isFile()) {
|
|
128
|
+
const ext = path.extname(entry.name);
|
|
129
|
+
if (extensions.includes(ext)) {
|
|
130
|
+
files.push(relativePath);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
// Skip directories that can't be read
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
await scan(this.projectPath);
|
|
140
|
+
return files.slice(0, 100); // Limit to 100 files
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* List available backups
|
|
145
|
+
* @returns {Object[]} Array of backup manifests
|
|
146
|
+
*/
|
|
147
|
+
async listBackups() {
|
|
148
|
+
await this.init();
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const entries = await fs.readdir(this.backupPath, { withFileTypes: true });
|
|
152
|
+
const backups = [];
|
|
153
|
+
|
|
154
|
+
for (const entry of entries) {
|
|
155
|
+
if (entry.isDirectory()) {
|
|
156
|
+
const manifestPath = path.join(this.backupPath, entry.name, 'manifest.json');
|
|
157
|
+
try {
|
|
158
|
+
const manifest = await fs.readJson(manifestPath);
|
|
159
|
+
backups.push(manifest);
|
|
160
|
+
} catch {
|
|
161
|
+
// Skip invalid backups
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Sort by timestamp, newest first
|
|
167
|
+
return backups.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
168
|
+
} catch {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Restore from backup
|
|
175
|
+
* @param {string} backupId - Backup ID to restore
|
|
176
|
+
* @returns {Object} Restore result
|
|
177
|
+
*/
|
|
178
|
+
async restore(backupId) {
|
|
179
|
+
const backupDir = path.join(this.backupPath, backupId);
|
|
180
|
+
const manifestPath = path.join(backupDir, 'manifest.json');
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const manifest = await fs.readJson(manifestPath);
|
|
184
|
+
const restored = [];
|
|
185
|
+
|
|
186
|
+
for (const file of manifest.files) {
|
|
187
|
+
const backupFilePath = path.join(backupDir, file.backupName);
|
|
188
|
+
const targetPath = path.join(this.projectPath, file.path);
|
|
189
|
+
|
|
190
|
+
// Ensure directory exists
|
|
191
|
+
await fs.ensureDir(path.dirname(targetPath));
|
|
192
|
+
|
|
193
|
+
// Restore file
|
|
194
|
+
const content = await fs.readFile(backupFilePath);
|
|
195
|
+
await fs.writeFile(targetPath, content);
|
|
196
|
+
|
|
197
|
+
restored.push(file.path);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
success: true,
|
|
202
|
+
backupId,
|
|
203
|
+
action: manifest.action,
|
|
204
|
+
timestamp: manifest.timestamp,
|
|
205
|
+
filesRestored: restored.length,
|
|
206
|
+
files: restored
|
|
207
|
+
};
|
|
208
|
+
} catch (error) {
|
|
209
|
+
return {
|
|
210
|
+
success: false,
|
|
211
|
+
error: error.message
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Restore to N steps ago
|
|
218
|
+
* @param {number} steps - Number of steps back
|
|
219
|
+
* @returns {Object} Restore result
|
|
220
|
+
*/
|
|
221
|
+
async restoreSteps(steps = 1) {
|
|
222
|
+
const backups = await this.listBackups();
|
|
223
|
+
|
|
224
|
+
if (steps > backups.length) {
|
|
225
|
+
return {
|
|
226
|
+
success: false,
|
|
227
|
+
error: `Only ${backups.length} backups available`
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const backup = backups[steps - 1];
|
|
232
|
+
return await this.restore(backup.id);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get the most recent backup
|
|
237
|
+
* @returns {Object|null} Latest backup manifest or null
|
|
238
|
+
*/
|
|
239
|
+
async getLatestBackup() {
|
|
240
|
+
const backups = await this.listBackups();
|
|
241
|
+
return backups.length > 0 ? backups[0] : null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Cleanup old backups to maintain MAX_BACKUPS limit
|
|
246
|
+
*/
|
|
247
|
+
async cleanupOldBackups() {
|
|
248
|
+
const backups = await this.listBackups();
|
|
249
|
+
|
|
250
|
+
if (backups.length > MAX_BACKUPS) {
|
|
251
|
+
const toDelete = backups.slice(MAX_BACKUPS);
|
|
252
|
+
|
|
253
|
+
for (const backup of toDelete) {
|
|
254
|
+
const backupDir = path.join(this.backupPath, backup.id);
|
|
255
|
+
await fs.remove(backupDir);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Delete specific backup
|
|
262
|
+
* @param {string} backupId - Backup ID to delete
|
|
263
|
+
*/
|
|
264
|
+
async deleteBackup(backupId) {
|
|
265
|
+
const backupDir = path.join(this.backupPath, backupId);
|
|
266
|
+
await fs.remove(backupDir);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Clear all backups
|
|
271
|
+
*/
|
|
272
|
+
async clearAllBackups() {
|
|
273
|
+
await fs.remove(this.backupPath);
|
|
274
|
+
await this.init();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Get backup size in bytes
|
|
279
|
+
* @param {string} backupId - Backup ID
|
|
280
|
+
* @returns {number} Size in bytes
|
|
281
|
+
*/
|
|
282
|
+
async getBackupSize(backupId) {
|
|
283
|
+
const backupDir = path.join(this.backupPath, backupId);
|
|
284
|
+
let totalSize = 0;
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const files = await fs.readdir(backupDir);
|
|
288
|
+
for (const file of files) {
|
|
289
|
+
const stats = await fs.stat(path.join(backupDir, file));
|
|
290
|
+
totalSize += stats.size;
|
|
291
|
+
}
|
|
292
|
+
} catch {
|
|
293
|
+
// Ignore errors
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return totalSize;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Helper to wrap action with auto-backup
|
|
302
|
+
* @param {string} actionName - Name of action
|
|
303
|
+
* @param {Function} fn - Async function to execute
|
|
304
|
+
* @returns {*} Result of fn()
|
|
305
|
+
*/
|
|
306
|
+
export async function withBackup(actionName, fn) {
|
|
307
|
+
const backup = new BackupManager();
|
|
308
|
+
const backupId = await backup.createBackup(actionName);
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
return await fn();
|
|
312
|
+
} catch (error) {
|
|
313
|
+
// Error occurred - backup is available for restore
|
|
314
|
+
console.log(`\n💾 Backup created: ${backupId}`);
|
|
315
|
+
console.log(` Run 'vibecode undo' to restore previous state.\n`);
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Create backup manager instance
|
|
322
|
+
*/
|
|
323
|
+
export function createBackupManager(projectPath) {
|
|
324
|
+
return new BackupManager(projectPath);
|
|
325
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// VIBECODE CLI - Learning Engine
|
|
3
|
+
// Phase H5: AI learns from user feedback
|
|
4
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
|
|
10
|
+
const LEARNING_DIR = '.vibecode/learning';
|
|
11
|
+
const GLOBAL_LEARNING_DIR = path.join(os.homedir(), '.vibecode/learning');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Learning Engine - Records and retrieves learning data
|
|
15
|
+
*
|
|
16
|
+
* Features:
|
|
17
|
+
* - Records fix attempts and outcomes
|
|
18
|
+
* - Stores user preferences
|
|
19
|
+
* - Provides suggestions based on past successes
|
|
20
|
+
* - Anonymizes data for global storage
|
|
21
|
+
*/
|
|
22
|
+
export class LearningEngine {
|
|
23
|
+
constructor(projectPath = process.cwd()) {
|
|
24
|
+
this.projectPath = projectPath;
|
|
25
|
+
this.localPath = path.join(projectPath, LEARNING_DIR);
|
|
26
|
+
this.globalPath = GLOBAL_LEARNING_DIR;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Initialize learning directories
|
|
31
|
+
*/
|
|
32
|
+
async init() {
|
|
33
|
+
await fs.mkdir(this.localPath, { recursive: true });
|
|
34
|
+
await fs.mkdir(this.globalPath, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Record a fix attempt and its outcome
|
|
39
|
+
*/
|
|
40
|
+
async recordFix(fixData) {
|
|
41
|
+
await this.init();
|
|
42
|
+
|
|
43
|
+
const record = {
|
|
44
|
+
id: Date.now().toString(36),
|
|
45
|
+
timestamp: new Date().toISOString(),
|
|
46
|
+
errorType: fixData.errorType,
|
|
47
|
+
errorMessage: fixData.errorMessage?.substring(0, 200),
|
|
48
|
+
errorCategory: fixData.errorCategory,
|
|
49
|
+
fixApplied: fixData.fixApplied?.substring(0, 500),
|
|
50
|
+
success: fixData.success,
|
|
51
|
+
userFeedback: fixData.userFeedback,
|
|
52
|
+
userCorrection: fixData.userCorrection,
|
|
53
|
+
projectType: await this.detectProjectType(),
|
|
54
|
+
tags: fixData.tags || []
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Save to local project
|
|
58
|
+
const localFile = path.join(this.localPath, 'fixes.json');
|
|
59
|
+
const localFixes = await this.loadJson(localFile, []);
|
|
60
|
+
localFixes.push(record);
|
|
61
|
+
await this.saveJson(localFile, localFixes.slice(-100)); // Keep last 100
|
|
62
|
+
|
|
63
|
+
// Save to global (anonymized)
|
|
64
|
+
const globalFile = path.join(this.globalPath, 'fixes.json');
|
|
65
|
+
const globalFixes = await this.loadJson(globalFile, []);
|
|
66
|
+
globalFixes.push({
|
|
67
|
+
...record,
|
|
68
|
+
errorMessage: this.anonymize(record.errorMessage),
|
|
69
|
+
fixApplied: this.anonymize(record.fixApplied)
|
|
70
|
+
});
|
|
71
|
+
await this.saveJson(globalFile, globalFixes.slice(-500)); // Keep last 500
|
|
72
|
+
|
|
73
|
+
return record.id;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Record user preference
|
|
78
|
+
*/
|
|
79
|
+
async recordPreference(key, value, context = {}) {
|
|
80
|
+
await this.init();
|
|
81
|
+
|
|
82
|
+
const prefsFile = path.join(this.localPath, 'preferences.json');
|
|
83
|
+
const prefs = await this.loadJson(prefsFile, {});
|
|
84
|
+
|
|
85
|
+
if (!prefs[key]) {
|
|
86
|
+
prefs[key] = { values: [], contexts: [] };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
prefs[key].values.push(value);
|
|
90
|
+
prefs[key].contexts.push(context);
|
|
91
|
+
prefs[key].lastUsed = new Date().toISOString();
|
|
92
|
+
|
|
93
|
+
// Keep only recent values
|
|
94
|
+
prefs[key].values = prefs[key].values.slice(-20);
|
|
95
|
+
prefs[key].contexts = prefs[key].contexts.slice(-20);
|
|
96
|
+
|
|
97
|
+
await this.saveJson(prefsFile, prefs);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get suggestion based on learnings
|
|
102
|
+
*/
|
|
103
|
+
async getSuggestion(errorType, errorCategory) {
|
|
104
|
+
const localFixes = await this.loadJson(
|
|
105
|
+
path.join(this.localPath, 'fixes.json'),
|
|
106
|
+
[]
|
|
107
|
+
);
|
|
108
|
+
const globalFixes = await this.loadJson(
|
|
109
|
+
path.join(this.globalPath, 'fixes.json'),
|
|
110
|
+
[]
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// Find similar successful fixes
|
|
114
|
+
const allFixes = [...localFixes, ...globalFixes];
|
|
115
|
+
const similarFixes = allFixes.filter(f =>
|
|
116
|
+
f.success &&
|
|
117
|
+
(f.errorType === errorType || f.errorCategory === errorCategory)
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (similarFixes.length === 0) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Calculate confidence based on success rate
|
|
125
|
+
const totalSimilar = allFixes.filter(f =>
|
|
126
|
+
f.errorType === errorType || f.errorCategory === errorCategory
|
|
127
|
+
).length;
|
|
128
|
+
|
|
129
|
+
const successRate = similarFixes.length / totalSimilar;
|
|
130
|
+
|
|
131
|
+
// Get most recent successful fix
|
|
132
|
+
const recentFix = similarFixes.sort((a, b) =>
|
|
133
|
+
new Date(b.timestamp) - new Date(a.timestamp)
|
|
134
|
+
)[0];
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
suggestion: recentFix.fixApplied,
|
|
138
|
+
confidence: successRate,
|
|
139
|
+
basedOn: similarFixes.length,
|
|
140
|
+
lastUsed: recentFix.timestamp
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get user preference
|
|
146
|
+
*/
|
|
147
|
+
async getPreference(key, defaultValue = null) {
|
|
148
|
+
const prefsFile = path.join(this.localPath, 'preferences.json');
|
|
149
|
+
const prefs = await this.loadJson(prefsFile, {});
|
|
150
|
+
|
|
151
|
+
if (!prefs[key] || prefs[key].values.length === 0) {
|
|
152
|
+
return defaultValue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Return most common value
|
|
156
|
+
const counts = {};
|
|
157
|
+
for (const v of prefs[key].values) {
|
|
158
|
+
counts[v] = (counts[v] || 0) + 1;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]);
|
|
162
|
+
return sorted[0][0];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get learning statistics
|
|
167
|
+
*/
|
|
168
|
+
async getStats() {
|
|
169
|
+
const localFixes = await this.loadJson(
|
|
170
|
+
path.join(this.localPath, 'fixes.json'),
|
|
171
|
+
[]
|
|
172
|
+
);
|
|
173
|
+
const globalFixes = await this.loadJson(
|
|
174
|
+
path.join(this.globalPath, 'fixes.json'),
|
|
175
|
+
[]
|
|
176
|
+
);
|
|
177
|
+
const prefs = await this.loadJson(
|
|
178
|
+
path.join(this.localPath, 'preferences.json'),
|
|
179
|
+
{}
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const localSuccess = localFixes.filter(f => f.success).length;
|
|
183
|
+
const globalSuccess = globalFixes.filter(f => f.success).length;
|
|
184
|
+
|
|
185
|
+
// Group by error category
|
|
186
|
+
const byCategory = {};
|
|
187
|
+
for (const fix of localFixes) {
|
|
188
|
+
const cat = fix.errorCategory || 'unknown';
|
|
189
|
+
if (!byCategory[cat]) {
|
|
190
|
+
byCategory[cat] = { total: 0, success: 0 };
|
|
191
|
+
}
|
|
192
|
+
byCategory[cat].total++;
|
|
193
|
+
if (fix.success) byCategory[cat].success++;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
local: {
|
|
198
|
+
total: localFixes.length,
|
|
199
|
+
success: localSuccess,
|
|
200
|
+
rate: localFixes.length > 0 ? (localSuccess / localFixes.length * 100).toFixed(1) : '0'
|
|
201
|
+
},
|
|
202
|
+
global: {
|
|
203
|
+
total: globalFixes.length,
|
|
204
|
+
success: globalSuccess,
|
|
205
|
+
rate: globalFixes.length > 0 ? (globalSuccess / globalFixes.length * 100).toFixed(1) : '0'
|
|
206
|
+
},
|
|
207
|
+
byCategory,
|
|
208
|
+
preferences: Object.keys(prefs).length,
|
|
209
|
+
lastLearning: localFixes[localFixes.length - 1]?.timestamp || null
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Detect project type
|
|
215
|
+
*/
|
|
216
|
+
async detectProjectType() {
|
|
217
|
+
try {
|
|
218
|
+
const pkgPath = path.join(this.projectPath, 'package.json');
|
|
219
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8'));
|
|
220
|
+
|
|
221
|
+
if (pkg.dependencies?.next) return 'nextjs';
|
|
222
|
+
if (pkg.dependencies?.react) return 'react';
|
|
223
|
+
if (pkg.dependencies?.vue) return 'vue';
|
|
224
|
+
if (pkg.dependencies?.express) return 'express';
|
|
225
|
+
if (pkg.dependencies?.['@prisma/client']) return 'prisma';
|
|
226
|
+
|
|
227
|
+
return 'node';
|
|
228
|
+
} catch {
|
|
229
|
+
return 'unknown';
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Anonymize sensitive data for global storage
|
|
235
|
+
*/
|
|
236
|
+
anonymize(text) {
|
|
237
|
+
if (!text) return text;
|
|
238
|
+
return text
|
|
239
|
+
.replace(/\/Users\/[^\/\s]+/g, '/Users/***')
|
|
240
|
+
.replace(/\/home\/[^\/\s]+/g, '/home/***')
|
|
241
|
+
.replace(/C:\\Users\\[^\\]+/g, 'C:\\Users\\***')
|
|
242
|
+
.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '***@***.***')
|
|
243
|
+
.replace(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, '***.***.***.***')
|
|
244
|
+
.replace(/api[_-]?key[=:]\s*["']?[\w-]+["']?/gi, 'api_key=***')
|
|
245
|
+
.replace(/token[=:]\s*["']?[\w-]+["']?/gi, 'token=***')
|
|
246
|
+
.replace(/password[=:]\s*["']?[^"'\s]+["']?/gi, 'password=***');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Clear all local learnings
|
|
251
|
+
*/
|
|
252
|
+
async clearLocal() {
|
|
253
|
+
await this.saveJson(path.join(this.localPath, 'fixes.json'), []);
|
|
254
|
+
await this.saveJson(path.join(this.localPath, 'preferences.json'), {});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Load JSON file
|
|
259
|
+
*/
|
|
260
|
+
async loadJson(filePath, defaultValue) {
|
|
261
|
+
try {
|
|
262
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
263
|
+
return JSON.parse(content);
|
|
264
|
+
} catch {
|
|
265
|
+
return defaultValue;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Save JSON file
|
|
271
|
+
*/
|
|
272
|
+
async saveJson(filePath, data) {
|
|
273
|
+
await fs.writeFile(filePath, JSON.stringify(data, null, 2));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Singleton instance
|
|
278
|
+
let learningEngine = null;
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Get or create LearningEngine instance
|
|
282
|
+
*/
|
|
283
|
+
export function getLearningEngine(projectPath = process.cwd()) {
|
|
284
|
+
if (!learningEngine || learningEngine.projectPath !== projectPath) {
|
|
285
|
+
learningEngine = new LearningEngine(projectPath);
|
|
286
|
+
}
|
|
287
|
+
return learningEngine;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Create a new LearningEngine instance
|
|
292
|
+
*/
|
|
293
|
+
export function createLearningEngine(projectPath = process.cwd()) {
|
|
294
|
+
return new LearningEngine(projectPath);
|
|
295
|
+
}
|
package/src/core/test-runner.js
CHANGED
|
@@ -43,9 +43,10 @@ export async function runTests(projectPath) {
|
|
|
43
43
|
results.tests.push(npmTest);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// 3. Run npm run lint (if script exists)
|
|
46
|
+
// 3. Run npm run lint (if script exists) - soft fail (warnings only)
|
|
47
47
|
if (pkg.scripts?.lint) {
|
|
48
48
|
const npmLint = await runCommand('npm run lint', projectPath, 'npm lint');
|
|
49
|
+
npmLint.softFail = true; // Lint errors are warnings, don't block build
|
|
49
50
|
results.tests.push(npmLint);
|
|
50
51
|
}
|
|
51
52
|
|
|
@@ -70,11 +71,21 @@ export async function runTests(projectPath) {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
// 7. Aggregate results
|
|
74
|
+
// Separate hard tests from soft-fail tests (like lint)
|
|
75
|
+
const hardTests = results.tests.filter(t => !t.softFail);
|
|
76
|
+
const softTests = results.tests.filter(t => t.softFail);
|
|
77
|
+
|
|
73
78
|
results.summary.total = results.tests.length;
|
|
74
79
|
results.summary.passed = results.tests.filter(t => t.passed).length;
|
|
75
80
|
results.summary.failed = results.tests.filter(t => !t.passed).length;
|
|
76
|
-
results.
|
|
77
|
-
|
|
81
|
+
results.summary.warnings = softTests.filter(t => !t.passed).length;
|
|
82
|
+
|
|
83
|
+
// Only hard tests determine pass/fail
|
|
84
|
+
results.passed = hardTests.length === 0 || hardTests.every(t => t.passed);
|
|
85
|
+
|
|
86
|
+
// Collect errors, but mark soft-fail errors as warnings
|
|
87
|
+
results.errors = results.tests.filter(t => !t.passed && !t.softFail).flatMap(t => t.errors || []);
|
|
88
|
+
results.warnings = softTests.filter(t => !t.passed).flatMap(t => t.errors || []);
|
|
78
89
|
results.duration = Date.now() - startTime;
|
|
79
90
|
|
|
80
91
|
return results;
|
|
@@ -230,12 +241,17 @@ export function formatTestResults(results) {
|
|
|
230
241
|
const lines = [];
|
|
231
242
|
|
|
232
243
|
lines.push(`Tests: ${results.summary.passed}/${results.summary.total} passed`);
|
|
244
|
+
if (results.summary.warnings > 0) {
|
|
245
|
+
lines.push(`Warnings: ${results.summary.warnings} (lint)`);
|
|
246
|
+
}
|
|
233
247
|
lines.push(`Duration: ${(results.duration / 1000).toFixed(1)}s`);
|
|
234
248
|
|
|
235
|
-
|
|
249
|
+
// Show hard failures
|
|
250
|
+
const hardFailures = results.tests.filter(t => !t.passed && !t.softFail);
|
|
251
|
+
if (hardFailures.length > 0) {
|
|
236
252
|
lines.push('');
|
|
237
253
|
lines.push('Failed tests:');
|
|
238
|
-
for (const test of
|
|
254
|
+
for (const test of hardFailures) {
|
|
239
255
|
lines.push(` ❌ ${test.name}`);
|
|
240
256
|
for (const error of test.errors || []) {
|
|
241
257
|
const loc = error.file ? `${error.file}:${error.line || '?'}` : '';
|
|
@@ -244,5 +260,22 @@ export function formatTestResults(results) {
|
|
|
244
260
|
}
|
|
245
261
|
}
|
|
246
262
|
|
|
263
|
+
// Show soft failures (warnings)
|
|
264
|
+
const softFailures = results.tests.filter(t => !t.passed && t.softFail);
|
|
265
|
+
if (softFailures.length > 0) {
|
|
266
|
+
lines.push('');
|
|
267
|
+
lines.push('Warnings (non-blocking):');
|
|
268
|
+
for (const test of softFailures) {
|
|
269
|
+
lines.push(` ⚠️ ${test.name}`);
|
|
270
|
+
for (const error of (test.errors || []).slice(0, 3)) {
|
|
271
|
+
const loc = error.file ? `${error.file}:${error.line || '?'}` : '';
|
|
272
|
+
lines.push(` ${loc} ${error.message?.substring(0, 80) || ''}`);
|
|
273
|
+
}
|
|
274
|
+
if ((test.errors?.length || 0) > 3) {
|
|
275
|
+
lines.push(` ... and ${test.errors.length - 3} more`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
247
280
|
return lines.join('\n');
|
|
248
281
|
}
|