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,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* borderRenderer.js - Colored left-border prefix strings for message types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const RESET = '\x1b[0m';
|
|
6
|
+
|
|
7
|
+
const BORDERS = {
|
|
8
|
+
user: { char: '│', color: '\x1b[38;5;220m' }, // banana yellow
|
|
9
|
+
ai: { char: '│', color: '\x1b[38;5;75m' }, // blue/cyan
|
|
10
|
+
thinking: { char: '│', color: '\x1b[38;5;240m' }, // dim gray
|
|
11
|
+
tool: { char: '│', color: '\x1b[38;5;245m' }, // gray
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const PAD = ' ';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Returns a colored border prefix string for the given speaker type.
|
|
18
|
+
* e.g. " \x1b[38;5;220m│\x1b[0m "
|
|
19
|
+
*/
|
|
20
|
+
function prefix(speaker) {
|
|
21
|
+
const b = BORDERS[speaker];
|
|
22
|
+
if (!b) return PAD;
|
|
23
|
+
return `${PAD}${b.color}${b.char}${RESET} `;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Strip ANSI escape codes from a string for visual width calculation.
|
|
28
|
+
*/
|
|
29
|
+
function stripAnsi(str) {
|
|
30
|
+
// eslint-disable-next-line no-control-regex
|
|
31
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get the visual (display) width of the prefix for a speaker type.
|
|
36
|
+
*/
|
|
37
|
+
function prefixWidth(speaker) {
|
|
38
|
+
return stripAnsi(prefix(speaker)).length;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { prefix, stripAnsi, prefixWidth, BORDERS, PAD };
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Runner - Execute shell commands safely
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { spawn } = require('child_process');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
// Commands that are always blocked
|
|
9
|
+
const BLOCKED_COMMANDS = [
|
|
10
|
+
'rm -rf /', 'rm -rf /*', 'del /s /q c:',
|
|
11
|
+
'format c:', 'mkfs', ':(){:|:&};:'
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
// Commands that require confirmation
|
|
15
|
+
const DANGEROUS_PATTERNS = [
|
|
16
|
+
/^rm\s+-rf/i,
|
|
17
|
+
/^del\s+\/s/i,
|
|
18
|
+
/^rmdir\s+\/s/i,
|
|
19
|
+
/drop\s+database/i,
|
|
20
|
+
/drop\s+table/i,
|
|
21
|
+
/truncate\s+table/i
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
class CommandRunner {
|
|
25
|
+
constructor(projectDir) {
|
|
26
|
+
this.projectDir = projectDir;
|
|
27
|
+
this.isWindows = process.platform === 'win32';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if a command is blocked
|
|
32
|
+
*/
|
|
33
|
+
isBlocked(command) {
|
|
34
|
+
const lowerCmd = command.toLowerCase().trim();
|
|
35
|
+
|
|
36
|
+
// Check absolute blocks
|
|
37
|
+
for (const blocked of BLOCKED_COMMANDS) {
|
|
38
|
+
if (lowerCmd.includes(blocked)) {
|
|
39
|
+
return { blocked: true, reason: 'This command is blocked for safety' };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { blocked: false };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check if a command needs extra confirmation
|
|
48
|
+
*/
|
|
49
|
+
isDangerous(command) {
|
|
50
|
+
for (const pattern of DANGEROUS_PATTERNS) {
|
|
51
|
+
if (pattern.test(command)) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Run a command and return a promise with the result
|
|
60
|
+
*/
|
|
61
|
+
run(command, options = {}) {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const blockCheck = this.isBlocked(command);
|
|
64
|
+
if (blockCheck.blocked) {
|
|
65
|
+
reject(new Error(blockCheck.reason));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const shell = this.isWindows ? 'cmd.exe' : '/bin/sh';
|
|
70
|
+
const shellArg = this.isWindows ? '/c' : '-c';
|
|
71
|
+
|
|
72
|
+
const child = spawn(shell, [shellArg, command], {
|
|
73
|
+
cwd: options.cwd || this.projectDir,
|
|
74
|
+
env: { ...process.env, ...options.env },
|
|
75
|
+
stdio: options.stream ? 'inherit' : 'pipe'
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
let stdout = '';
|
|
79
|
+
let stderr = '';
|
|
80
|
+
|
|
81
|
+
if (!options.stream) {
|
|
82
|
+
child.stdout.on('data', (data) => {
|
|
83
|
+
stdout += data.toString();
|
|
84
|
+
if (options.onStdout) options.onStdout(data.toString());
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
child.stderr.on('data', (data) => {
|
|
88
|
+
stderr += data.toString();
|
|
89
|
+
if (options.onStderr) options.onStderr(data.toString());
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
child.on('close', (code) => {
|
|
94
|
+
resolve({
|
|
95
|
+
code,
|
|
96
|
+
stdout: stdout.trim(),
|
|
97
|
+
stderr: stderr.trim(),
|
|
98
|
+
success: code === 0
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
child.on('error', (error) => {
|
|
103
|
+
reject(error);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Handle timeout
|
|
107
|
+
if (options.timeout) {
|
|
108
|
+
setTimeout(() => {
|
|
109
|
+
child.kill('SIGTERM');
|
|
110
|
+
reject(new Error(`Command timed out after ${options.timeout}ms`));
|
|
111
|
+
}, options.timeout);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Run a command with streaming output
|
|
118
|
+
*/
|
|
119
|
+
runWithStream(command, options = {}) {
|
|
120
|
+
return this.run(command, { ...options, stream: true });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Run npm/pnpm/yarn command
|
|
125
|
+
*/
|
|
126
|
+
async runPackageManager(args, options = {}) {
|
|
127
|
+
// Detect package manager
|
|
128
|
+
const fs = require('fs');
|
|
129
|
+
|
|
130
|
+
let pm = 'npm';
|
|
131
|
+
if (fs.existsSync(path.join(this.projectDir, 'pnpm-lock.yaml'))) {
|
|
132
|
+
pm = 'pnpm';
|
|
133
|
+
} else if (fs.existsSync(path.join(this.projectDir, 'yarn.lock'))) {
|
|
134
|
+
pm = 'yarn';
|
|
135
|
+
} else if (fs.existsSync(path.join(this.projectDir, 'bun.lockb'))) {
|
|
136
|
+
pm = 'bun';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return this.run(`${pm} ${args}`, options);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Install dependencies
|
|
144
|
+
*/
|
|
145
|
+
async install(packages = [], options = {}) {
|
|
146
|
+
if (packages.length === 0) {
|
|
147
|
+
return this.runPackageManager('install', options);
|
|
148
|
+
}
|
|
149
|
+
return this.runPackageManager(`install ${packages.join(' ')}`, options);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Run a dev server
|
|
154
|
+
*/
|
|
155
|
+
async runDev(options = {}) {
|
|
156
|
+
return this.runPackageManager('run dev', { ...options, stream: true });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Run build
|
|
161
|
+
*/
|
|
162
|
+
async build(options = {}) {
|
|
163
|
+
return this.runPackageManager('run build', options);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Run tests
|
|
168
|
+
*/
|
|
169
|
+
async test(options = {}) {
|
|
170
|
+
return this.runPackageManager('test', options);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Git operations
|
|
175
|
+
*/
|
|
176
|
+
async git(args, options = {}) {
|
|
177
|
+
return this.run(`git ${args}`, options);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get current git status
|
|
182
|
+
*/
|
|
183
|
+
async gitStatus() {
|
|
184
|
+
try {
|
|
185
|
+
const result = await this.git('status --porcelain');
|
|
186
|
+
if (!result.success) {
|
|
187
|
+
return { isRepo: false, changes: [] };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const changes = result.stdout
|
|
191
|
+
.split('\n')
|
|
192
|
+
.filter(line => line.trim())
|
|
193
|
+
.map(line => ({
|
|
194
|
+
status: line.substring(0, 2).trim(),
|
|
195
|
+
file: line.substring(3)
|
|
196
|
+
}));
|
|
197
|
+
|
|
198
|
+
return { isRepo: true, changes };
|
|
199
|
+
} catch {
|
|
200
|
+
return { isRepo: false, changes: [] };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
module.exports = CommandRunner;
|
package/lib/completer.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tab completion for Banana Code
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
class Completer {
|
|
9
|
+
constructor(projectDir, contextBuilder) {
|
|
10
|
+
this.projectDir = projectDir;
|
|
11
|
+
this.contextBuilder = contextBuilder;
|
|
12
|
+
|
|
13
|
+
// All built-in slash commands from handleCommand() switch
|
|
14
|
+
this.builtinCommands = [
|
|
15
|
+
'/help', '/?', '/update', '/version', '/v',
|
|
16
|
+
'/files', '/ls', '/read', '/add', '/unread', '/remove',
|
|
17
|
+
'/tree', '/find', '/grep', '/search',
|
|
18
|
+
'/image',
|
|
19
|
+
'/git', '/status', '/diff', '/log',
|
|
20
|
+
'/clear', '/clearhistory',
|
|
21
|
+
'/save', '/load', '/sessions',
|
|
22
|
+
'/context', '/tokens',
|
|
23
|
+
'/think', '/steer', '/steering',
|
|
24
|
+
'/ctx', '/compact', '/stream',
|
|
25
|
+
'/watch', '/yolo', '/agent',
|
|
26
|
+
'/hooks',
|
|
27
|
+
'/work', '/code', '/plan', '/implement', '/ask', '/mode',
|
|
28
|
+
'/prompt', '/model', '/models', '/connect', '/mcp',
|
|
29
|
+
'/config', '/set', '/instructions',
|
|
30
|
+
'/run', '/exec', '/$',
|
|
31
|
+
'/undo', '/backups', '/restore',
|
|
32
|
+
'/exit', '/quit', '/q',
|
|
33
|
+
'/commands'
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// Load custom commands from all locations, deduplicated
|
|
37
|
+
this.commands = [...new Set([
|
|
38
|
+
...this.builtinCommands,
|
|
39
|
+
...this._loadGlobalCommandNames(),
|
|
40
|
+
...this._loadProjectCommandNames()
|
|
41
|
+
])];
|
|
42
|
+
|
|
43
|
+
this.configKeys = [
|
|
44
|
+
'compactMode', 'streamingEnabled', 'maxTokens',
|
|
45
|
+
'tokenWarningThreshold', 'autoSaveHistory', 'geminiApiKey', 'steeringEnabled'
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
complete(line) {
|
|
50
|
+
const trimmed = line.trim();
|
|
51
|
+
|
|
52
|
+
// Command completion
|
|
53
|
+
if (trimmed.startsWith('/')) {
|
|
54
|
+
return this.completeCommand(trimmed);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// @ file mention completion
|
|
58
|
+
if (trimmed.includes('@')) {
|
|
59
|
+
return this.completeFileMention(trimmed);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Default: no completions
|
|
63
|
+
return [[], line];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
completeCommand(line) {
|
|
67
|
+
const parts = line.split(/\s+/);
|
|
68
|
+
const cmd = parts[0].toLowerCase();
|
|
69
|
+
|
|
70
|
+
// If just "/" show all commands
|
|
71
|
+
if (cmd === '/') {
|
|
72
|
+
return [this.commands, '/'];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// If just the command, complete command names
|
|
76
|
+
if (parts.length === 1) {
|
|
77
|
+
const matches = this.commands.filter(c => c.startsWith(cmd));
|
|
78
|
+
return [matches, cmd];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Command-specific completions
|
|
82
|
+
const arg = parts.slice(1).join(' ');
|
|
83
|
+
|
|
84
|
+
switch (cmd) {
|
|
85
|
+
case '/read':
|
|
86
|
+
case '/unread':
|
|
87
|
+
case '/restore':
|
|
88
|
+
return this.completeFilePath(arg, cmd + ' ');
|
|
89
|
+
|
|
90
|
+
case '/set':
|
|
91
|
+
if (parts.length === 2) {
|
|
92
|
+
const matches = this.configKeys.filter(k =>
|
|
93
|
+
k.toLowerCase().startsWith(arg.toLowerCase())
|
|
94
|
+
);
|
|
95
|
+
return [matches.map(m => `${cmd} ${m}`), line];
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
|
|
99
|
+
case '/connect':
|
|
100
|
+
if (parts.length === 2) {
|
|
101
|
+
const options = ['anthropic', 'openai', 'openrouter', 'status', 'list', 'disconnect', 'use'];
|
|
102
|
+
const matches = options.filter(o => o.startsWith(arg.toLowerCase()));
|
|
103
|
+
return [matches.map(m => `${cmd} ${m}`), line];
|
|
104
|
+
}
|
|
105
|
+
if (parts.length === 3 && ['disconnect', 'use'].includes(parts[1].toLowerCase())) {
|
|
106
|
+
const providers = ['local', 'anthropic', 'openai', 'openrouter'];
|
|
107
|
+
const providerArg = parts[2].toLowerCase();
|
|
108
|
+
const matches = providers.filter(p => p.startsWith(providerArg));
|
|
109
|
+
return [matches.map(m => `${cmd} ${parts[1]} ${m}`), line];
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
|
|
113
|
+
case '/model':
|
|
114
|
+
case '/models':
|
|
115
|
+
if (parts.length === 2) {
|
|
116
|
+
const options = ['search'];
|
|
117
|
+
const matches = options.filter(o => o.startsWith(arg.toLowerCase()));
|
|
118
|
+
return [matches.map(m => `${cmd} ${m}`), line];
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
|
|
122
|
+
case '/think':
|
|
123
|
+
if (parts.length === 2) {
|
|
124
|
+
const options = ['off', 'low', 'medium', 'high'];
|
|
125
|
+
const matches = options.filter(o => o.startsWith(arg.toLowerCase()));
|
|
126
|
+
return [matches.map(m => `${cmd} ${m}`), line];
|
|
127
|
+
}
|
|
128
|
+
break;
|
|
129
|
+
|
|
130
|
+
case '/steer':
|
|
131
|
+
case '/steering':
|
|
132
|
+
if (parts.length === 2) {
|
|
133
|
+
const options = ['status', 'show', 'clear', 'on', 'off'];
|
|
134
|
+
const matches = options.filter(o => o.startsWith(arg.toLowerCase()));
|
|
135
|
+
return [matches.map(m => `${cmd} ${m}`), line];
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
|
|
139
|
+
case '/hooks':
|
|
140
|
+
if (parts.length === 2) {
|
|
141
|
+
const options = ['list', 'add', 'remove', 'toggle', 'test'];
|
|
142
|
+
const matches = options.filter(o => o.startsWith(arg.toLowerCase()));
|
|
143
|
+
return [matches.map(m => `${cmd} ${m}`), line];
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
|
|
147
|
+
case '/load':
|
|
148
|
+
return this.completeSessionName(arg, cmd + ' ');
|
|
149
|
+
|
|
150
|
+
case '/find':
|
|
151
|
+
case '/grep':
|
|
152
|
+
// No completion for search patterns
|
|
153
|
+
break;
|
|
154
|
+
|
|
155
|
+
case '/run':
|
|
156
|
+
return this.completeShellCommand(arg, cmd + ' ');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return [[], line];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
completeFilePath(partial, prefix = '') {
|
|
163
|
+
try {
|
|
164
|
+
const searchDir = partial.includes('/')
|
|
165
|
+
? path.dirname(partial)
|
|
166
|
+
: '.';
|
|
167
|
+
|
|
168
|
+
const searchBase = partial.includes('/')
|
|
169
|
+
? path.basename(partial)
|
|
170
|
+
: partial;
|
|
171
|
+
|
|
172
|
+
const fullSearchDir = path.join(this.projectDir, searchDir);
|
|
173
|
+
|
|
174
|
+
if (!fs.existsSync(fullSearchDir)) {
|
|
175
|
+
return [[], prefix + partial];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const entries = fs.readdirSync(fullSearchDir, { withFileTypes: true });
|
|
179
|
+
|
|
180
|
+
const matches = entries
|
|
181
|
+
.filter(e => {
|
|
182
|
+
// Skip hidden and common ignores
|
|
183
|
+
if (e.name.startsWith('.')) return false;
|
|
184
|
+
if (e.name === 'node_modules') return false;
|
|
185
|
+
return e.name.toLowerCase().startsWith(searchBase.toLowerCase());
|
|
186
|
+
})
|
|
187
|
+
.map(e => {
|
|
188
|
+
const relativePath = searchDir === '.'
|
|
189
|
+
? e.name
|
|
190
|
+
: path.join(searchDir, e.name).replace(/\\/g, '/');
|
|
191
|
+
return prefix + relativePath + (e.isDirectory() ? '/' : '');
|
|
192
|
+
})
|
|
193
|
+
.slice(0, 20); // Limit suggestions
|
|
194
|
+
|
|
195
|
+
return [matches, prefix + partial];
|
|
196
|
+
} catch {
|
|
197
|
+
return [[], prefix + partial];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
completeFileMention(line) {
|
|
202
|
+
// Find the @ mention being typed
|
|
203
|
+
const atIndex = line.lastIndexOf('@');
|
|
204
|
+
if (atIndex === -1) return [[], line];
|
|
205
|
+
|
|
206
|
+
const beforeAt = line.slice(0, atIndex);
|
|
207
|
+
const afterAt = line.slice(atIndex + 1);
|
|
208
|
+
|
|
209
|
+
// Get file completions
|
|
210
|
+
const [completions] = this.completeFilePath(afterAt);
|
|
211
|
+
|
|
212
|
+
// Format as @ mentions
|
|
213
|
+
const matches = completions.map(c => beforeAt + '@' + c);
|
|
214
|
+
|
|
215
|
+
return [matches, line];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
completeSessionName(partial, prefix = '') {
|
|
219
|
+
try {
|
|
220
|
+
const historyDir = path.join(this.projectDir, '.banana', 'history');
|
|
221
|
+
if (!fs.existsSync(historyDir)) return [[], prefix + partial];
|
|
222
|
+
|
|
223
|
+
const files = fs.readdirSync(historyDir)
|
|
224
|
+
.filter(f => f.endsWith('.json'))
|
|
225
|
+
.filter(f => f.toLowerCase().includes(partial.toLowerCase()))
|
|
226
|
+
.slice(0, 10);
|
|
227
|
+
|
|
228
|
+
return [files.map(f => prefix + f), prefix + partial];
|
|
229
|
+
} catch {
|
|
230
|
+
return [[], prefix + partial];
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
_loadGlobalCommandNames() {
|
|
235
|
+
const names = [];
|
|
236
|
+
const dirs = [
|
|
237
|
+
path.join(require('os').homedir(), '.banana', 'commands'),
|
|
238
|
+
path.join(require('os').homedir(), '.ripley', 'commands') // legacy
|
|
239
|
+
];
|
|
240
|
+
for (const dir of dirs) {
|
|
241
|
+
try {
|
|
242
|
+
if (!fs.existsSync(dir)) continue;
|
|
243
|
+
for (const f of fs.readdirSync(dir)) {
|
|
244
|
+
if (f.endsWith('.md')) {
|
|
245
|
+
const cmd = '/' + f.replace('.md', '').toLowerCase();
|
|
246
|
+
if (!names.includes(cmd)) names.push(cmd);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} catch {}
|
|
250
|
+
}
|
|
251
|
+
return names;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
_loadProjectCommandNames() {
|
|
255
|
+
try {
|
|
256
|
+
const commandsDir = path.join(this.projectDir, '.banana', 'commands');
|
|
257
|
+
if (!fs.existsSync(commandsDir)) return [];
|
|
258
|
+
return fs.readdirSync(commandsDir)
|
|
259
|
+
.filter(f => f.endsWith('.md'))
|
|
260
|
+
.map(f => '/' + f.replace('.md', '').toLowerCase())
|
|
261
|
+
.filter(cmd => !this.builtinCommands.includes(cmd)); // avoid duplicates
|
|
262
|
+
} catch {
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
completeShellCommand(partial, prefix = '') {
|
|
268
|
+
// Common commands
|
|
269
|
+
const commonCommands = [
|
|
270
|
+
'npm install', 'npm run', 'npm test', 'npm start', 'npm run dev', 'npm run build',
|
|
271
|
+
'pnpm install', 'pnpm run', 'pnpm test', 'pnpm dev', 'pnpm build',
|
|
272
|
+
'yarn install', 'yarn', 'yarn test', 'yarn dev', 'yarn build',
|
|
273
|
+
'git status', 'git add', 'git commit', 'git push', 'git pull', 'git log',
|
|
274
|
+
'ls', 'dir', 'cd', 'mkdir', 'rm', 'cat', 'echo',
|
|
275
|
+
'node', 'npx', 'tsx', 'ts-node'
|
|
276
|
+
];
|
|
277
|
+
|
|
278
|
+
const matches = commonCommands
|
|
279
|
+
.filter(c => c.toLowerCase().startsWith(partial.toLowerCase()))
|
|
280
|
+
.map(c => prefix + c);
|
|
281
|
+
|
|
282
|
+
return [matches, prefix + partial];
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
module.exports = Completer;
|