banana-code 1.2.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 +246 -0
- package/banana.js +5464 -0
- package/lib/agenticRunner.js +1884 -0
- package/lib/borderRenderer.js +41 -0
- package/lib/commandRunner.js +205 -0
- package/lib/completer.js +286 -0
- package/lib/config.js +301 -0
- package/lib/contextBuilder.js +324 -0
- package/lib/diffViewer.js +295 -0
- package/lib/fileManager.js +224 -0
- package/lib/historyManager.js +124 -0
- package/lib/hookManager.js +1143 -0
- package/lib/imageHandler.js +268 -0
- package/lib/inlineComplete.js +192 -0
- package/lib/interactivePicker.js +254 -0
- package/lib/lmStudio.js +226 -0
- package/lib/markdownRenderer.js +423 -0
- package/lib/mcpClient.js +288 -0
- package/lib/modelRegistry.js +350 -0
- package/lib/monkeyModels.js +97 -0
- package/lib/oauthOpenAI.js +167 -0
- package/lib/parser.js +134 -0
- package/lib/promptManager.js +96 -0
- package/lib/providerClients.js +1014 -0
- package/lib/providerManager.js +130 -0
- package/lib/providerStore.js +413 -0
- package/lib/statusBar.js +283 -0
- package/lib/streamHandler.js +306 -0
- package/lib/subAgentManager.js +406 -0
- package/lib/tokenCounter.js +132 -0
- package/lib/visionAnalyzer.js +163 -0
- package/lib/watcher.js +138 -0
- package/models.json +57 -0
- package/package.json +42 -0
- package/prompts/base.md +23 -0
- package/prompts/code-agent-glm.md +16 -0
- package/prompts/code-agent-gptoss.md +25 -0
- package/prompts/code-agent-nemotron.md +17 -0
- package/prompts/code-agent-qwen.md +20 -0
- package/prompts/code-agent.md +70 -0
- package/prompts/plan.md +44 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff Viewer - Show colorful diffs for file changes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { diffLines, createPatch } = require('diff');
|
|
6
|
+
|
|
7
|
+
// ANSI colors (fallback if chalk not available)
|
|
8
|
+
const colors = {
|
|
9
|
+
reset: '\x1b[0m',
|
|
10
|
+
dim: '\x1b[2m',
|
|
11
|
+
green: '\x1b[38;5;120m',
|
|
12
|
+
red: '\x1b[38;5;210m',
|
|
13
|
+
cyan: '\x1b[38;5;51m',
|
|
14
|
+
yellow: '\x1b[38;5;226m',
|
|
15
|
+
banana: '\x1b[38;5;220m',
|
|
16
|
+
bananaGlow: '\x1b[38;5;228m',
|
|
17
|
+
orange: '\x1b[38;5;220m',
|
|
18
|
+
gray: '\x1b[38;5;245m',
|
|
19
|
+
white: '\x1b[38;5;255m',
|
|
20
|
+
magenta: '\x1b[38;5;183m',
|
|
21
|
+
bgGreen: '\x1b[48;5;22m',
|
|
22
|
+
bgRed: '\x1b[48;5;52m'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Format a line number with padding
|
|
27
|
+
*/
|
|
28
|
+
function formatLineNum(num, width = 4) {
|
|
29
|
+
return String(num).padStart(width, ' ');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Generate a unified diff view
|
|
34
|
+
*/
|
|
35
|
+
function generateDiff(oldContent, newContent, filePath) {
|
|
36
|
+
const patch = createPatch(filePath, oldContent || '', newContent, 'old', 'new');
|
|
37
|
+
return patch;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create a pretty diff display for terminal
|
|
42
|
+
*/
|
|
43
|
+
function prettyDiff(oldContent, newContent, filePath, maxLines = 50) {
|
|
44
|
+
const differences = diffLines(oldContent || '', newContent);
|
|
45
|
+
const lines = [];
|
|
46
|
+
let lineCount = 0;
|
|
47
|
+
let hasChanges = false;
|
|
48
|
+
|
|
49
|
+
// Header
|
|
50
|
+
lines.push(`${colors.cyan}${'─'.repeat(60)}${colors.reset}`);
|
|
51
|
+
lines.push(`${colors.cyan}│${colors.reset} ${colors.white}${filePath}${colors.reset}`);
|
|
52
|
+
lines.push(`${colors.cyan}${'─'.repeat(60)}${colors.reset}`);
|
|
53
|
+
|
|
54
|
+
let oldLineNum = 1;
|
|
55
|
+
let newLineNum = 1;
|
|
56
|
+
|
|
57
|
+
for (const part of differences) {
|
|
58
|
+
const partLines = part.value.split('\n');
|
|
59
|
+
// Remove empty last line from split
|
|
60
|
+
if (partLines[partLines.length - 1] === '') {
|
|
61
|
+
partLines.pop();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const line of partLines) {
|
|
65
|
+
if (lineCount >= maxLines) {
|
|
66
|
+
lines.push(`${colors.gray} ... ${differences.length - lineCount} more changes ...${colors.reset}`);
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (part.added) {
|
|
71
|
+
hasChanges = true;
|
|
72
|
+
lines.push(`${colors.green}+ ${formatLineNum(newLineNum)}${colors.reset} ${colors.bgGreen}${line}${colors.reset}`);
|
|
73
|
+
newLineNum++;
|
|
74
|
+
} else if (part.removed) {
|
|
75
|
+
hasChanges = true;
|
|
76
|
+
lines.push(`${colors.red}- ${formatLineNum(oldLineNum)}${colors.reset} ${colors.bgRed}${line}${colors.reset}`);
|
|
77
|
+
oldLineNum++;
|
|
78
|
+
} else {
|
|
79
|
+
// Context line (unchanged) - show less of these
|
|
80
|
+
if (lineCount < 5 || lineCount > differences.length - 5) {
|
|
81
|
+
lines.push(`${colors.gray} ${formatLineNum(oldLineNum)}${colors.reset} ${line}`);
|
|
82
|
+
} else if (lines[lines.length - 1] !== '...') {
|
|
83
|
+
lines.push(`${colors.gray} ...${colors.reset}`);
|
|
84
|
+
}
|
|
85
|
+
oldLineNum++;
|
|
86
|
+
newLineNum++;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
lineCount++;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (lineCount >= maxLines) break;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!hasChanges) {
|
|
96
|
+
lines.push(`${colors.gray} (no changes)${colors.reset}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
lines.push(`${colors.cyan}${'─'.repeat(60)}${colors.reset}`);
|
|
100
|
+
|
|
101
|
+
return lines.join('\n');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Show a new file creation preview
|
|
106
|
+
*/
|
|
107
|
+
function showNewFile(content, filePath, maxLines = 30) {
|
|
108
|
+
const lines = content.split('\n');
|
|
109
|
+
const output = [];
|
|
110
|
+
|
|
111
|
+
output.push(`${colors.green}CREATE${colors.reset} ${colors.white}${filePath}${colors.reset}`);
|
|
112
|
+
output.push(`${colors.cyan}${'─'.repeat(60)}${colors.reset}`);
|
|
113
|
+
|
|
114
|
+
for (let i = 0; i < Math.min(lines.length, maxLines); i++) {
|
|
115
|
+
output.push(`${colors.green}+ ${formatLineNum(i + 1)}${colors.reset} ${lines[i]}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (lines.length > maxLines) {
|
|
119
|
+
output.push(`${colors.gray} ... ${lines.length - maxLines} more lines ...${colors.reset}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
output.push(`${colors.cyan}${'─'.repeat(60)}${colors.reset}`);
|
|
123
|
+
output.push(`${colors.dim}${lines.length} lines${colors.reset}`);
|
|
124
|
+
|
|
125
|
+
return output.join('\n');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Show a file deletion preview
|
|
130
|
+
*/
|
|
131
|
+
function showDeleteFile(content, filePath, maxLines = 15) {
|
|
132
|
+
const lines = content ? content.split('\n') : [];
|
|
133
|
+
const output = [];
|
|
134
|
+
|
|
135
|
+
output.push(`${colors.red}DELETE${colors.reset} ${colors.white}${filePath}${colors.reset}`);
|
|
136
|
+
output.push(`${colors.cyan}${'─'.repeat(60)}${colors.reset}`);
|
|
137
|
+
|
|
138
|
+
for (let i = 0; i < Math.min(lines.length, maxLines); i++) {
|
|
139
|
+
output.push(`${colors.red}- ${formatLineNum(i + 1)}${colors.reset} ${colors.dim}${lines[i]}${colors.reset}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (lines.length > maxLines) {
|
|
143
|
+
output.push(`${colors.gray} ... ${lines.length - maxLines} more lines ...${colors.reset}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
output.push(`${colors.cyan}${'─'.repeat(60)}${colors.reset}`);
|
|
147
|
+
|
|
148
|
+
return output.join('\n');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Show an edit preview
|
|
153
|
+
*/
|
|
154
|
+
function showEdit(oldContent, newContent, filePath) {
|
|
155
|
+
return prettyDiff(oldContent, newContent, filePath);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get change summary (for compact display)
|
|
160
|
+
*/
|
|
161
|
+
function getChangeSummary(oldContent, newContent) {
|
|
162
|
+
const differences = diffLines(oldContent || '', newContent);
|
|
163
|
+
let added = 0;
|
|
164
|
+
let removed = 0;
|
|
165
|
+
|
|
166
|
+
for (const part of differences) {
|
|
167
|
+
const lineCount = part.value.split('\n').length - 1;
|
|
168
|
+
if (part.added) added += lineCount;
|
|
169
|
+
if (part.removed) removed += lineCount;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return { added, removed };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Format operation for display
|
|
177
|
+
*/
|
|
178
|
+
function formatOperation(operation, existingContent = null) {
|
|
179
|
+
const { action, path, content } = operation;
|
|
180
|
+
|
|
181
|
+
switch (action) {
|
|
182
|
+
case 'create':
|
|
183
|
+
return showNewFile(content, path);
|
|
184
|
+
|
|
185
|
+
case 'delete':
|
|
186
|
+
return showDeleteFile(existingContent, path);
|
|
187
|
+
|
|
188
|
+
case 'edit':
|
|
189
|
+
if (existingContent === null) {
|
|
190
|
+
// No existing content, treat as create
|
|
191
|
+
return showNewFile(content, path);
|
|
192
|
+
}
|
|
193
|
+
return showEdit(existingContent, content, path);
|
|
194
|
+
|
|
195
|
+
default:
|
|
196
|
+
return `Unknown action: ${action}`;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Format multiple operations in a batched view with summary
|
|
202
|
+
*/
|
|
203
|
+
function formatOperationsBatch(operations, fileManager) {
|
|
204
|
+
const output = [];
|
|
205
|
+
const summary = { created: 0, modified: 0, deleted: 0, additions: 0, deletions: 0 };
|
|
206
|
+
|
|
207
|
+
// Header
|
|
208
|
+
output.push(`\n${colors.banana}${'═'.repeat(60)}${colors.reset}`);
|
|
209
|
+
output.push(`${colors.banana} PROPOSED CHANGES (${operations.length} files)${colors.reset}`);
|
|
210
|
+
output.push(`${colors.banana}${'═'.repeat(60)}${colors.reset}\n`);
|
|
211
|
+
|
|
212
|
+
// Process each operation
|
|
213
|
+
for (let i = 0; i < operations.length; i++) {
|
|
214
|
+
const op = operations[i];
|
|
215
|
+
const existingContent = fileManager.exists(op.path)
|
|
216
|
+
? fileManager.readFile(op.path).content
|
|
217
|
+
: null;
|
|
218
|
+
|
|
219
|
+
// Get change stats
|
|
220
|
+
if (op.action === 'create') {
|
|
221
|
+
summary.created++;
|
|
222
|
+
const lines = op.content.split('\n').length;
|
|
223
|
+
summary.additions += lines;
|
|
224
|
+
} else if (op.action === 'delete') {
|
|
225
|
+
summary.deleted++;
|
|
226
|
+
if (existingContent) {
|
|
227
|
+
summary.deletions += existingContent.split('\n').length;
|
|
228
|
+
}
|
|
229
|
+
} else if (op.action === 'edit') {
|
|
230
|
+
summary.modified++;
|
|
231
|
+
const changes = getChangeSummary(existingContent, op.content);
|
|
232
|
+
summary.additions += changes.added;
|
|
233
|
+
summary.deletions += changes.removed;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// File number indicator
|
|
237
|
+
output.push(`${colors.dim}[${i + 1}/${operations.length}]${colors.reset}`);
|
|
238
|
+
output.push(formatOperation(op, existingContent));
|
|
239
|
+
output.push('');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Summary footer
|
|
243
|
+
output.push(`${colors.banana}${'═'.repeat(60)}${colors.reset}`);
|
|
244
|
+
|
|
245
|
+
const parts = [];
|
|
246
|
+
if (summary.created > 0) parts.push(`${colors.green}+${summary.created} new${colors.reset}`);
|
|
247
|
+
if (summary.modified > 0) parts.push(`${colors.yellow}~${summary.modified} modified${colors.reset}`);
|
|
248
|
+
if (summary.deleted > 0) parts.push(`${colors.red}-${summary.deleted} deleted${colors.reset}`);
|
|
249
|
+
|
|
250
|
+
output.push(`${colors.white} Summary:${colors.reset} ${parts.join(' | ')}`);
|
|
251
|
+
output.push(`${colors.dim} Lines: ${colors.green}+${summary.additions}${colors.reset} ${colors.red}-${summary.deletions}${colors.reset}`);
|
|
252
|
+
output.push(`${colors.banana}${'═'.repeat(60)}${colors.reset}\n`);
|
|
253
|
+
|
|
254
|
+
return output.join('\n');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Show a compact summary of changes (for confirmation prompts)
|
|
259
|
+
*/
|
|
260
|
+
function formatCompactSummary(operations, fileManager) {
|
|
261
|
+
const lines = [`\n${colors.cyan}Changes:${colors.reset}`];
|
|
262
|
+
|
|
263
|
+
for (const op of operations) {
|
|
264
|
+
const icon = op.action === 'create' ? '✚' : op.action === 'delete' ? '✖' : '✎';
|
|
265
|
+
const iconColor = op.action === 'create' ? colors.green : op.action === 'delete' ? colors.red : colors.yellow;
|
|
266
|
+
|
|
267
|
+
let detail = '';
|
|
268
|
+
if (op.action === 'edit' && fileManager.exists(op.path)) {
|
|
269
|
+
const existing = fileManager.readFile(op.path).content;
|
|
270
|
+
const { added, removed } = getChangeSummary(existing, op.content);
|
|
271
|
+
detail = ` ${colors.dim}(${colors.green}+${added}${colors.dim}/${colors.red}-${removed}${colors.dim})${colors.reset}`;
|
|
272
|
+
} else if (op.action === 'create') {
|
|
273
|
+
const lineCount = op.content.split('\n').length;
|
|
274
|
+
detail = ` ${colors.dim}(${lineCount} lines)${colors.reset}`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
lines.push(` ${iconColor}${icon}${colors.reset} ${op.path}${detail}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
lines.push('');
|
|
281
|
+
return lines.join('\n');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
module.exports = {
|
|
285
|
+
generateDiff,
|
|
286
|
+
prettyDiff,
|
|
287
|
+
showNewFile,
|
|
288
|
+
showDeleteFile,
|
|
289
|
+
showEdit,
|
|
290
|
+
getChangeSummary,
|
|
291
|
+
formatOperation,
|
|
292
|
+
formatOperationsBatch,
|
|
293
|
+
formatCompactSummary,
|
|
294
|
+
colors
|
|
295
|
+
};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Manager - Read/write files with backup support
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
// File extensions to always skip
|
|
9
|
+
const BINARY_EXTENSIONS = [
|
|
10
|
+
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.webp', '.svg',
|
|
11
|
+
'.mp3', '.mp4', '.wav', '.avi', '.mov', '.mkv',
|
|
12
|
+
'.zip', '.tar', '.gz', '.rar', '.7z',
|
|
13
|
+
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
|
|
14
|
+
'.exe', '.dll', '.so', '.dylib',
|
|
15
|
+
'.woff', '.woff2', '.ttf', '.eot', '.otf',
|
|
16
|
+
'.lock', '.bin', '.dat'
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
// Max file size to read (100KB)
|
|
20
|
+
const MAX_FILE_SIZE = 100 * 1024;
|
|
21
|
+
|
|
22
|
+
class FileManager {
|
|
23
|
+
constructor(projectDir) {
|
|
24
|
+
this.projectDir = projectDir;
|
|
25
|
+
this.backupDir = path.join(projectDir, '.banana', 'backups');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Ensure backup directory exists
|
|
30
|
+
*/
|
|
31
|
+
ensureBackupDir() {
|
|
32
|
+
if (!fs.existsSync(this.backupDir)) {
|
|
33
|
+
fs.mkdirSync(this.backupDir, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if file should be skipped (binary, too large, etc.)
|
|
39
|
+
*/
|
|
40
|
+
shouldSkipFile(filePath) {
|
|
41
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
42
|
+
if (BINARY_EXTENSIONS.includes(ext)) {
|
|
43
|
+
return { skip: true, reason: 'binary file' };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const stats = fs.statSync(filePath);
|
|
48
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
49
|
+
return { skip: true, reason: `file too large (${Math.round(stats.size / 1024)}KB)` };
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
return { skip: true, reason: 'cannot read file stats' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { skip: false };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Read a file's contents
|
|
60
|
+
*/
|
|
61
|
+
readFile(filePath) {
|
|
62
|
+
const fullPath = path.isAbsolute(filePath)
|
|
63
|
+
? filePath
|
|
64
|
+
: path.join(this.projectDir, filePath);
|
|
65
|
+
|
|
66
|
+
const skipCheck = this.shouldSkipFile(fullPath);
|
|
67
|
+
if (skipCheck.skip) {
|
|
68
|
+
return { success: false, error: skipCheck.reason };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
73
|
+
return { success: true, content, path: fullPath };
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return { success: false, error: error.message };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Write content to a file (with backup)
|
|
81
|
+
*/
|
|
82
|
+
writeFile(filePath, content) {
|
|
83
|
+
const fullPath = path.isAbsolute(filePath)
|
|
84
|
+
? filePath
|
|
85
|
+
: path.join(this.projectDir, filePath);
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
// Create backup if file exists
|
|
89
|
+
if (fs.existsSync(fullPath)) {
|
|
90
|
+
this.ensureBackupDir();
|
|
91
|
+
const timestamp = Date.now();
|
|
92
|
+
const relativePath = path.relative(this.projectDir, fullPath);
|
|
93
|
+
const backupName = `${relativePath.replace(/[/\\]/g, '_')}.${timestamp}.bak`;
|
|
94
|
+
const backupPath = path.join(this.backupDir, backupName);
|
|
95
|
+
|
|
96
|
+
fs.copyFileSync(fullPath, backupPath);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Ensure directory exists
|
|
100
|
+
const dir = path.dirname(fullPath);
|
|
101
|
+
if (!fs.existsSync(dir)) {
|
|
102
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Write the file
|
|
106
|
+
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
107
|
+
|
|
108
|
+
return { success: true, path: fullPath, isNew: !fs.existsSync(fullPath) };
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return { success: false, error: error.message };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Delete a file (with backup)
|
|
116
|
+
*/
|
|
117
|
+
deleteFile(filePath) {
|
|
118
|
+
const fullPath = path.isAbsolute(filePath)
|
|
119
|
+
? filePath
|
|
120
|
+
: path.join(this.projectDir, filePath);
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
if (!fs.existsSync(fullPath)) {
|
|
124
|
+
return { success: false, error: 'File does not exist' };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Create backup before deleting
|
|
128
|
+
this.ensureBackupDir();
|
|
129
|
+
const timestamp = Date.now();
|
|
130
|
+
const relativePath = path.relative(this.projectDir, fullPath);
|
|
131
|
+
const backupName = `${relativePath.replace(/[/\\]/g, '_')}.${timestamp}.deleted.bak`;
|
|
132
|
+
const backupPath = path.join(this.backupDir, backupName);
|
|
133
|
+
|
|
134
|
+
fs.copyFileSync(fullPath, backupPath);
|
|
135
|
+
fs.unlinkSync(fullPath);
|
|
136
|
+
|
|
137
|
+
return { success: true, path: fullPath };
|
|
138
|
+
} catch (error) {
|
|
139
|
+
return { success: false, error: error.message };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get list of backups
|
|
145
|
+
*/
|
|
146
|
+
getBackups() {
|
|
147
|
+
if (!fs.existsSync(this.backupDir)) {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const files = fs.readdirSync(this.backupDir);
|
|
153
|
+
return files
|
|
154
|
+
.map(file => {
|
|
155
|
+
const stats = fs.statSync(path.join(this.backupDir, file));
|
|
156
|
+
return {
|
|
157
|
+
name: file,
|
|
158
|
+
path: path.join(this.backupDir, file),
|
|
159
|
+
timestamp: stats.mtime
|
|
160
|
+
};
|
|
161
|
+
})
|
|
162
|
+
.sort((a, b) => b.timestamp - a.timestamp);
|
|
163
|
+
} catch {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Restore most recent backup for a file
|
|
170
|
+
*/
|
|
171
|
+
restoreLatest(filePath) {
|
|
172
|
+
const relativePath = path.relative(this.projectDir,
|
|
173
|
+
path.isAbsolute(filePath) ? filePath : path.join(this.projectDir, filePath)
|
|
174
|
+
);
|
|
175
|
+
const searchPrefix = relativePath.replace(/[/\\]/g, '_') + '.';
|
|
176
|
+
|
|
177
|
+
const backups = this.getBackups().filter(b => b.name.startsWith(searchPrefix));
|
|
178
|
+
|
|
179
|
+
if (backups.length === 0) {
|
|
180
|
+
return { success: false, error: 'No backups found for this file' };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const latestBackup = backups[0];
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const content = fs.readFileSync(latestBackup.path, 'utf-8');
|
|
187
|
+
const targetPath = path.isAbsolute(filePath)
|
|
188
|
+
? filePath
|
|
189
|
+
: path.join(this.projectDir, filePath);
|
|
190
|
+
|
|
191
|
+
fs.writeFileSync(targetPath, content, 'utf-8');
|
|
192
|
+
|
|
193
|
+
return { success: true, restored: latestBackup.name };
|
|
194
|
+
} catch (error) {
|
|
195
|
+
return { success: false, error: error.message };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Check if a path exists
|
|
201
|
+
*/
|
|
202
|
+
exists(filePath) {
|
|
203
|
+
const fullPath = path.isAbsolute(filePath)
|
|
204
|
+
? filePath
|
|
205
|
+
: path.join(this.projectDir, filePath);
|
|
206
|
+
return fs.existsSync(fullPath);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Check if path is a directory
|
|
211
|
+
*/
|
|
212
|
+
isDirectory(filePath) {
|
|
213
|
+
const fullPath = path.isAbsolute(filePath)
|
|
214
|
+
? filePath
|
|
215
|
+
: path.join(this.projectDir, filePath);
|
|
216
|
+
try {
|
|
217
|
+
return fs.statSync(fullPath).isDirectory();
|
|
218
|
+
} catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
module.exports = FileManager;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command history manager with readline integration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
class HistoryManager {
|
|
9
|
+
constructor(bananaDir, maxHistory = 100) {
|
|
10
|
+
this.historyFile = path.join(bananaDir, 'command_history.txt');
|
|
11
|
+
this.maxHistory = maxHistory;
|
|
12
|
+
this.history = [];
|
|
13
|
+
this.historyIndex = -1;
|
|
14
|
+
this.currentInput = '';
|
|
15
|
+
this.load();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
load() {
|
|
19
|
+
try {
|
|
20
|
+
let filePath = this.historyFile;
|
|
21
|
+
// Migrate from .ripley/ history if .banana/ history doesn't exist
|
|
22
|
+
if (!fs.existsSync(filePath)) {
|
|
23
|
+
const legacyPath = filePath.replace(/\.banana/, '.ripley');
|
|
24
|
+
if (legacyPath !== filePath && fs.existsSync(legacyPath)) {
|
|
25
|
+
filePath = legacyPath;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (fs.existsSync(filePath)) {
|
|
29
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
30
|
+
this.history = content.split('\n').filter(line => line.trim());
|
|
31
|
+
// Keep only recent history
|
|
32
|
+
if (this.history.length > this.maxHistory) {
|
|
33
|
+
this.history = this.history.slice(-this.maxHistory);
|
|
34
|
+
}
|
|
35
|
+
// If we read from legacy path, save to new path
|
|
36
|
+
if (filePath !== this.historyFile) {
|
|
37
|
+
this.save();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
this.history = [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
save() {
|
|
46
|
+
try {
|
|
47
|
+
const dir = path.dirname(this.historyFile);
|
|
48
|
+
if (!fs.existsSync(dir)) {
|
|
49
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
fs.writeFileSync(this.historyFile, this.history.join('\n'));
|
|
52
|
+
} catch {
|
|
53
|
+
// Ignore save errors
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
add(command) {
|
|
58
|
+
if (!command.trim()) return;
|
|
59
|
+
|
|
60
|
+
// Don't add duplicates of the last command
|
|
61
|
+
if (this.history.length > 0 && this.history[this.history.length - 1] === command) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.history.push(command);
|
|
66
|
+
|
|
67
|
+
// Trim history if too long
|
|
68
|
+
if (this.history.length > this.maxHistory) {
|
|
69
|
+
this.history = this.history.slice(-this.maxHistory);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this.resetIndex();
|
|
73
|
+
this.save();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
resetIndex() {
|
|
77
|
+
this.historyIndex = this.history.length;
|
|
78
|
+
this.currentInput = '';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
up(currentLine) {
|
|
82
|
+
if (this.historyIndex === this.history.length) {
|
|
83
|
+
this.currentInput = currentLine;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (this.historyIndex > 0) {
|
|
87
|
+
this.historyIndex--;
|
|
88
|
+
return this.history[this.historyIndex];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return this.history[0] || currentLine;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
down(currentLine) {
|
|
95
|
+
if (this.historyIndex < this.history.length - 1) {
|
|
96
|
+
this.historyIndex++;
|
|
97
|
+
return this.history[this.historyIndex];
|
|
98
|
+
} else if (this.historyIndex === this.history.length - 1) {
|
|
99
|
+
this.historyIndex = this.history.length;
|
|
100
|
+
return this.currentInput;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return currentLine;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
search(prefix) {
|
|
107
|
+
const matches = this.history.filter(cmd =>
|
|
108
|
+
cmd.toLowerCase().startsWith(prefix.toLowerCase())
|
|
109
|
+
);
|
|
110
|
+
return matches;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
getRecent(count = 10) {
|
|
114
|
+
return this.history.slice(-count);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
clear() {
|
|
118
|
+
this.history = [];
|
|
119
|
+
this.resetIndex();
|
|
120
|
+
this.save();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = HistoryManager;
|