bunosh 0.4.1 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +236 -60
- package/bunosh.js +207 -65
- package/package.json +7 -5
- package/src/completion.js +15 -2
- package/src/formatters/console.js +10 -5
- package/src/formatters/factory.js +30 -0
- package/src/io.js +5 -0
- package/src/mcp-server.js +575 -0
- package/src/printer.js +1 -1
- package/src/program.js +564 -486
- package/src/task.js +72 -9
- package/src/tasks/ai.js +10 -2
- package/src/tasks/exec.js +31 -9
- package/src/tasks/fetch.js +11 -3
- package/src/tasks/shell.js +18 -4
- package/src/upgrade.js +279 -199
package/src/program.js
CHANGED
|
@@ -7,152 +7,200 @@ import { readFileSync, existsSync, writeFileSync } from 'fs';
|
|
|
7
7
|
import { yell } from './io.js';
|
|
8
8
|
import cprint from "./font.js";
|
|
9
9
|
import { handleCompletion, detectCurrentShell, installCompletion, getCompletionPaths } from './completion.js';
|
|
10
|
-
import {
|
|
10
|
+
import { upgradeCommand } from './upgrade.js';
|
|
11
11
|
|
|
12
12
|
export const BUNOSHFILE = `Bunoshfile.js`;
|
|
13
13
|
|
|
14
14
|
export const banner = () => {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
const logoArt =
|
|
16
|
+
` .::=-=-___.
|
|
17
|
+
.:+*##*-**:___.
|
|
18
|
+
:**#%*-+#####*++*
|
|
19
|
+
:-+**++*########*+
|
|
20
|
+
\▒░░░░:----▒▒▒▒░/
|
|
21
|
+
\▒▒▒▒▒▒▒▒▒▒▒░░/
|
|
22
|
+
\▓▓▓▓▓▓▓▓░░░/
|
|
23
|
+
\▓▓▓▓▓▓░░░/ `;
|
|
24
|
+
|
|
25
|
+
console.log(createGradientAscii(logoArt));
|
|
26
|
+
|
|
27
|
+
let version = '';
|
|
20
28
|
try {
|
|
21
|
-
// First try relative to src directory
|
|
22
29
|
const pkgPath = new URL('../package.json', import.meta.url);
|
|
23
30
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
24
|
-
|
|
31
|
+
version = pkg.version;
|
|
25
32
|
} catch (e) {
|
|
26
|
-
// Ignore if version can't be read
|
|
27
33
|
}
|
|
34
|
+
console.log(color.gray('🍲 ', color.yellowBright.bold('BUNOSH'), color.yellow(version)));
|
|
35
|
+
|
|
28
36
|
console.log();
|
|
29
37
|
};
|
|
30
38
|
|
|
31
39
|
function createGradientAscii(asciiArt) {
|
|
32
40
|
const lines = asciiArt.split('\n');
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
color.bold.blue
|
|
39
|
-
];
|
|
40
|
-
|
|
41
|
+
|
|
42
|
+
// Yellow RGB (255, 220, 0) to Brown RGB (139, 69, 19)
|
|
43
|
+
const startColor = { r: 255, g: 220, b: 0 };
|
|
44
|
+
const endColor = { r: 139, g: 69, b: 19 };
|
|
45
|
+
|
|
41
46
|
return lines.map((line, index) => {
|
|
42
|
-
//
|
|
47
|
+
// Block characters should always be brown
|
|
48
|
+
if (line.includes('░') || line.includes('▒') || line.includes('▓')) {
|
|
49
|
+
return `\x1b[38;2;139;69;19m${line}\x1b[0m`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Create smooth gradient for other characters
|
|
43
53
|
const progress = index / (lines.length - 1);
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const color = factor < 0.5 ? colors[lowerIndex] : colors[upperIndex];
|
|
51
|
-
return color(line);
|
|
54
|
+
const r = Math.round(startColor.r + (endColor.r - startColor.r) * progress);
|
|
55
|
+
const g = Math.round(startColor.g + (endColor.g - startColor.g) * progress);
|
|
56
|
+
const b = Math.round(startColor.b + (endColor.b - startColor.b) * progress);
|
|
57
|
+
|
|
58
|
+
// Use true color escape sequence
|
|
59
|
+
return `\x1b[38;2;${r};${g};${b}m${line}\x1b[0m`;
|
|
52
60
|
}).join('\n');
|
|
53
61
|
}
|
|
54
62
|
|
|
55
|
-
export default async function bunosh(commands,
|
|
63
|
+
export default async function bunosh(commands, sources) {
|
|
56
64
|
const program = new Command();
|
|
57
65
|
program.option('--bunoshfile <path>', 'Path to the Bunoshfile');
|
|
66
|
+
program.option('--env-file <path>', 'Path to environment file');
|
|
58
67
|
|
|
59
68
|
const internalCommands = [];
|
|
60
69
|
|
|
61
|
-
|
|
62
|
-
const npmScripts = loadNpmScripts();
|
|
63
|
-
|
|
64
|
-
// Load personal commands from $HOME/Bunoshfile.js
|
|
65
|
-
const { tasks: homeTasks, source: homeSource } = await loadHomeTasks();
|
|
66
|
-
|
|
70
|
+
|
|
67
71
|
program.configureHelp({
|
|
68
72
|
commandDescription: _cmd => {
|
|
69
73
|
// Show banner and description
|
|
70
74
|
banner();
|
|
71
|
-
|
|
72
|
-
// Try to get version from current directory's package.json for help display
|
|
73
|
-
try {
|
|
74
|
-
if (existsSync('package.json')) {
|
|
75
|
-
const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
|
|
76
|
-
console.log(`Version: ${color.bold(pkg.version)}`);
|
|
77
|
-
}
|
|
78
|
-
} catch {
|
|
79
|
-
// Ignore if version can't be read
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return ` Commands are loaded from exported functions in ${color.bold(BUNOSHFILE)}`;
|
|
75
|
+
return ' ';
|
|
83
76
|
},
|
|
84
|
-
commandUsage: usg => 'bunosh
|
|
77
|
+
commandUsage: usg => 'bunosh <command> <args> [options]',
|
|
85
78
|
showGlobalOptions: false,
|
|
86
79
|
visibleGlobalOptions: _opt => [],
|
|
87
80
|
visibleOptions: _opt => [],
|
|
88
81
|
visibleCommands: cmd => {
|
|
89
|
-
|
|
90
|
-
return
|
|
82
|
+
// Hide all commands from default listing - we'll show them in custom sections
|
|
83
|
+
return [];
|
|
91
84
|
},
|
|
92
85
|
subcommandTerm: (cmd) => color.white.bold(cmd.name()),
|
|
93
86
|
subcommandDescription: (cmd) => color.gray(cmd.description()),
|
|
94
87
|
});
|
|
95
88
|
|
|
96
|
-
|
|
89
|
+
// Don't show global help after error - individual commands will show their own help
|
|
90
|
+
// program.showHelpAfterError();
|
|
97
91
|
program.showSuggestionAfterError(true);
|
|
98
92
|
program.addHelpCommand(false);
|
|
99
93
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
});
|
|
109
|
-
} catch (parseError) {
|
|
110
|
-
// Re-throw with more specific error information
|
|
111
|
-
parseError.code = 'BABEL_PARSER_SYNTAX_ERROR';
|
|
112
|
-
throw parseError;
|
|
113
|
-
}
|
|
94
|
+
// Override the program's error output formatting
|
|
95
|
+
program.configureOutput({
|
|
96
|
+
writeErr: (str) => {
|
|
97
|
+
// Replace "error:" with red "Error:" in error output
|
|
98
|
+
process.stderr.write(str.replace(/^error:/g, color.red('Error') + ':'));
|
|
99
|
+
},
|
|
100
|
+
writeOut: (str) => process.stdout.write(str)
|
|
101
|
+
});
|
|
114
102
|
|
|
115
|
-
|
|
116
|
-
const
|
|
103
|
+
// Parse AST and comments for each source
|
|
104
|
+
const comments = {};
|
|
105
|
+
const namespaceSources = {};
|
|
117
106
|
|
|
118
|
-
|
|
119
|
-
|
|
107
|
+
for (const [cmdName, cmdInfo] of Object.entries(sources)) {
|
|
108
|
+
if (cmdInfo.source) {
|
|
109
|
+
try {
|
|
110
|
+
const ast = babelParser.parse(cmdInfo.source, {
|
|
111
|
+
sourceType: "module",
|
|
112
|
+
ranges: true,
|
|
113
|
+
tokens: true,
|
|
114
|
+
comments: true,
|
|
115
|
+
attachComment: true,
|
|
116
|
+
});
|
|
120
117
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
118
|
+
// Store AST for this command
|
|
119
|
+
if (!namespaceSources[cmdInfo.namespace || '']) {
|
|
120
|
+
namespaceSources[cmdInfo.namespace || ''] = {
|
|
121
|
+
ast: ast,
|
|
122
|
+
source: cmdInfo.source
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
// Extract comments for this command
|
|
127
|
+
const fnName = cmdInfo.namespace ? cmdName.split(':')[1] : cmdName;
|
|
128
|
+
if (fnName) {
|
|
129
|
+
comments[cmdName] = extractCommentForFunction(ast, cmdInfo.source, fnName);
|
|
130
|
+
}
|
|
131
|
+
} catch (parseError) {
|
|
132
|
+
// Re-throw with more specific error information
|
|
133
|
+
parseError.code = 'BABEL_PARSER_SYNTAX_ERROR';
|
|
134
|
+
throw parseError;
|
|
135
|
+
}
|
|
130
136
|
}
|
|
131
|
-
}
|
|
137
|
+
}
|
|
132
138
|
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
139
|
+
// Collect all commands (bunosh + namespace commands + npm scripts) and sort them
|
|
140
|
+
const allCommands = [];
|
|
141
|
+
|
|
142
|
+
// Add bunosh commands (including namespaced ones)
|
|
143
|
+
Object.keys(commands).forEach((cmdName) => {
|
|
144
|
+
const sourceInfo = sources[cmdName];
|
|
145
|
+
if (sourceInfo && sourceInfo.namespace) {
|
|
146
|
+
// This is a namespaced command
|
|
147
|
+
allCommands.push({
|
|
148
|
+
type: 'namespace',
|
|
149
|
+
name: cmdName,
|
|
150
|
+
namespace: sourceInfo.namespace,
|
|
151
|
+
data: commands[cmdName]
|
|
152
|
+
});
|
|
153
|
+
} else {
|
|
154
|
+
// Regular bunosh command
|
|
155
|
+
allCommands.push({ type: 'bunosh', name: cmdName, data: commands[cmdName] });
|
|
156
|
+
}
|
|
136
157
|
});
|
|
137
158
|
|
|
159
|
+
|
|
160
|
+
|
|
138
161
|
// Sort all commands alphabetically by name
|
|
139
162
|
allCommands.sort((a, b) => a.name.localeCompare(b.name));
|
|
140
163
|
|
|
164
|
+
// Organize commands by namespace for help display
|
|
165
|
+
const commandsByNamespace = {
|
|
166
|
+
'': [], // Main commands (no namespace)
|
|
167
|
+
'dev': [], // Dev commands
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Categorize commands by namespace
|
|
171
|
+
allCommands.forEach(cmd => {
|
|
172
|
+
if (cmd.type === 'namespace' && cmd.namespace) {
|
|
173
|
+
// Group by namespace, defaulting to 'dev' for unknown namespaces
|
|
174
|
+
const namespace = cmd.namespace || 'dev';
|
|
175
|
+
if (!commandsByNamespace[namespace]) {
|
|
176
|
+
commandsByNamespace[namespace] = [];
|
|
177
|
+
}
|
|
178
|
+
commandsByNamespace[namespace].push(cmd);
|
|
179
|
+
} else {
|
|
180
|
+
commandsByNamespace[''].push(cmd);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
141
184
|
// Process all commands in sorted order
|
|
142
185
|
allCommands.forEach((cmdData) => {
|
|
143
|
-
if (cmdData.type === 'bunosh') {
|
|
186
|
+
if (cmdData.type === 'bunosh' || (cmdData.type === 'namespace' && !cmdData.namespace)) {
|
|
187
|
+
// Handle main bunosh commands (no namespace)
|
|
144
188
|
const fnName = cmdData.name;
|
|
145
189
|
const fnBody = commands[fnName].toString();
|
|
190
|
+
const sourceInfo = sources[fnName];
|
|
191
|
+
const namespaceSource = namespaceSources[''];
|
|
146
192
|
|
|
147
|
-
const ast =
|
|
148
|
-
const args = parseArgs();
|
|
149
|
-
const opts = parseOpts();
|
|
150
|
-
|
|
193
|
+
const ast = namespaceSource?.ast || babelParser.parse(fnBody, { comment: true, tokens: true });
|
|
194
|
+
const args = parseArgs(ast, fnName);
|
|
195
|
+
const opts = parseOpts(ast, fnName);
|
|
151
196
|
const comment = comments[fnName];
|
|
152
197
|
|
|
153
198
|
const commandName = prepareCommandName(fnName);
|
|
154
199
|
|
|
155
200
|
const command = program.command(commandName);
|
|
201
|
+
if (comment) {
|
|
202
|
+
command.description(comment);
|
|
203
|
+
}
|
|
156
204
|
command.hook('preAction', (_thisCommand) => {
|
|
157
205
|
process.env.BUNOSH_COMMAND_STARTED = true;
|
|
158
206
|
})
|
|
@@ -187,115 +235,51 @@ export default async function bunosh(commands, source) {
|
|
|
187
235
|
|
|
188
236
|
let description = comment?.split('\n')[0] || '';
|
|
189
237
|
|
|
190
|
-
if (comment && argsAndOptsDescription.length) description += `\n
|
|
238
|
+
if (comment && argsAndOptsDescription.length) description += `\n ▹ ${color.gray(`bunosh ${commandName}`)} ${color.blue(argsAndOptsDescription.join(' ').trim())}`;
|
|
191
239
|
|
|
192
240
|
command.description(description);
|
|
193
|
-
command.action(commands[fnName].bind(commands));
|
|
194
|
-
|
|
195
|
-
function fetchFnAst() {
|
|
196
|
-
let hasFnInSource = false;
|
|
197
|
-
|
|
198
|
-
traverse(completeAst, {
|
|
199
|
-
FunctionDeclaration(path) {
|
|
200
|
-
if (path.node.id.name == fnName) {
|
|
201
|
-
hasFnInSource = true;
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
},
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
if (hasFnInSource) return completeAst;
|
|
208
|
-
|
|
209
|
-
return babelParser.parse(fnBody, { comment: true, tokens: true });
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function parseArgs() {
|
|
213
|
-
const functionArguments = {};
|
|
214
241
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (path.node.id.name !== fnName) return;
|
|
245
|
-
|
|
246
|
-
const node = path.node.params.pop();
|
|
247
|
-
if (!node) return;
|
|
248
|
-
if (
|
|
249
|
-
!node.type === "AssignmentPattern" &&
|
|
250
|
-
node.right.type === "ObjectExpression"
|
|
251
|
-
)
|
|
252
|
-
return;
|
|
253
|
-
|
|
254
|
-
node?.right?.properties?.forEach((p) => {
|
|
255
|
-
if (
|
|
256
|
-
["NumericLiteral", "StringLiteral", "BooleanLiteral"].includes(
|
|
257
|
-
p.value.type,
|
|
258
|
-
)
|
|
259
|
-
) {
|
|
260
|
-
functionOpts[camelToDasherize(p.key.name)] = p.value.value;
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if (p.value.type === "NullLiteral") {
|
|
265
|
-
functionOpts[camelToDasherize(p.key.name)] = null;
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (p.value.type == "UnaryExpression" && p.value.operator == "!") {
|
|
270
|
-
functionOpts[camelToDasherize(p.key.name)] =
|
|
271
|
-
!p.value.argument.value;
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
},
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
return functionOpts;
|
|
279
|
-
}
|
|
280
|
-
} else if (cmdData.type === 'home') {
|
|
281
|
-
// Handle personal commands with my: prefix
|
|
282
|
-
const originalFnName = cmdData.name.replace('my:', ''); // Remove my: prefix for internal usage
|
|
283
|
-
const fnBody = cmdData.data.toString();
|
|
284
|
-
const homeAst = fetchHomeFnAst(originalFnName, cmdData.source);
|
|
285
|
-
const homeArgs = parseHomeArgs(originalFnName, homeAst);
|
|
286
|
-
const homeOpts = parseHomeOpts(originalFnName, homeAst);
|
|
287
|
-
const homeComment = homeComments[originalFnName];
|
|
288
|
-
|
|
289
|
-
const commandName = cmdData.name; // Keep the full my: prefix for command name
|
|
242
|
+
// Custom error handling for missing arguments - show command help without banner
|
|
243
|
+
command.configureHelp({
|
|
244
|
+
commandDescription: () => comment || '',
|
|
245
|
+
commandUsage: cmd => `bunosh ${cmd.name()}${argsAndOptsDescription.length ? ' ' + argsAndOptsDescription.join(' ').trim() : ''}`,
|
|
246
|
+
showGlobalOptions: false,
|
|
247
|
+
visibleGlobalOptions: () => [],
|
|
248
|
+
visibleOptions: () => [],
|
|
249
|
+
visibleCommands: () => []
|
|
250
|
+
});
|
|
251
|
+
command.showHelpAfterError();
|
|
252
|
+
|
|
253
|
+
command.action(createCommandAction(commands[fnName], args, opts));
|
|
254
|
+
} else if (cmdData.type === 'namespace') {
|
|
255
|
+
// Handle namespaced commands
|
|
256
|
+
const sourceInfo = sources[cmdData.name];
|
|
257
|
+
const originalFnName = sourceInfo.originalFnName || cmdData.name.split(':')[1]; // Get original function name
|
|
258
|
+
const namespace = cmdData.namespace;
|
|
259
|
+
const fnBody = commands[cmdData.name].toString();
|
|
260
|
+
const namespaceSource = namespaceSources[namespace];
|
|
261
|
+
|
|
262
|
+
const ast = namespaceSource?.ast || babelParser.parse(fnBody, { comment: true, tokens: true });
|
|
263
|
+
const args = parseArgs(ast, originalFnName);
|
|
264
|
+
const opts = parseOpts(ast, originalFnName);
|
|
265
|
+
const comment = comments[cmdData.name];
|
|
266
|
+
|
|
267
|
+
// For namespaced commands, only transform the function part to kebab-case
|
|
268
|
+
const commandName = cmdData.name.includes(':')
|
|
269
|
+
? cmdData.name.split(':')[0] + ':' + toKebabCase(cmdData.name.split(':')[1]).replace(/^[^:]+:/, '')
|
|
270
|
+
: prepareCommandName(cmdData.name);
|
|
290
271
|
|
|
291
272
|
const command = program.command(commandName);
|
|
273
|
+
if (comment) {
|
|
274
|
+
command.description(comment);
|
|
275
|
+
}
|
|
292
276
|
command.hook('preAction', (_thisCommand) => {
|
|
293
277
|
process.env.BUNOSH_COMMAND_STARTED = true;
|
|
294
|
-
})
|
|
278
|
+
})
|
|
295
279
|
|
|
296
280
|
let argsAndOptsDescription = [];
|
|
297
281
|
|
|
298
|
-
Object.entries(
|
|
282
|
+
Object.entries(args).forEach(([arg, value]) => {
|
|
299
283
|
if (value === undefined) {
|
|
300
284
|
argsAndOptsDescription.push(`<${arg}>`);
|
|
301
285
|
return command.argument(`<${arg}>`);
|
|
@@ -310,7 +294,7 @@ export default async function bunosh(commands, source) {
|
|
|
310
294
|
command.argument(`[${arg}]`, ``, value);
|
|
311
295
|
});
|
|
312
296
|
|
|
313
|
-
Object.entries(
|
|
297
|
+
Object.entries(opts).forEach(([opt, value]) => {
|
|
314
298
|
if (value === false || value === null) {
|
|
315
299
|
argsAndOptsDescription.push(`--${opt}`);
|
|
316
300
|
return command.option(`--${opt}`);
|
|
@@ -320,42 +304,75 @@ export default async function bunosh(commands, source) {
|
|
|
320
304
|
command.option(`--${opt} [${opt}]`, "", value);
|
|
321
305
|
});
|
|
322
306
|
|
|
323
|
-
let description =
|
|
307
|
+
let description = comment?.split('\n')[0] || '';
|
|
324
308
|
|
|
325
|
-
if (
|
|
309
|
+
if (comment && argsAndOptsDescription.length) {
|
|
326
310
|
description += `\n ${color.gray(`bunosh ${commandName}`)} ${color.blue(argsAndOptsDescription.join(' ').trim())}`;
|
|
327
311
|
}
|
|
328
312
|
|
|
329
313
|
command.description(description);
|
|
330
|
-
command.action(cmdData.data.bind(homeTasks));
|
|
331
|
-
} else if (cmdData.type === 'npm') {
|
|
332
|
-
// Handle npm scripts
|
|
333
|
-
const { scriptName, scriptCommand } = cmdData.data;
|
|
334
|
-
const commandName = `npm:${scriptName}`;
|
|
335
|
-
const command = program.command(commandName);
|
|
336
|
-
command.description(color.gray(scriptCommand)); // Use script command as description
|
|
337
314
|
|
|
338
|
-
//
|
|
339
|
-
command.
|
|
315
|
+
// Custom error handling for missing arguments - show command help without banner
|
|
316
|
+
command.configureHelp({
|
|
317
|
+
commandDescription: () => comment || '',
|
|
318
|
+
commandUsage: cmd => `bunosh ${cmd.name()}${argsAndOptsDescription.length ? ' ' + argsAndOptsDescription.join(' ').trim() : ''}`,
|
|
319
|
+
showGlobalOptions: false,
|
|
320
|
+
visibleGlobalOptions: () => [],
|
|
321
|
+
visibleOptions: () => [],
|
|
322
|
+
visibleCommands: () => []
|
|
323
|
+
});
|
|
324
|
+
command.showHelpAfterError();
|
|
325
|
+
|
|
326
|
+
command.action(createCommandAction(commands[cmdData.name], args, opts));
|
|
340
327
|
}
|
|
341
328
|
});
|
|
342
329
|
|
|
343
|
-
// Helper function to create
|
|
344
|
-
function
|
|
345
|
-
return async () => {
|
|
346
|
-
//
|
|
347
|
-
const
|
|
330
|
+
// Helper function to create command action with proper argument transformation
|
|
331
|
+
function createCommandAction(commandFn, args, opts) {
|
|
332
|
+
return async (...commanderArgs) => {
|
|
333
|
+
// Transform Commander.js arguments to match function signature
|
|
334
|
+
const transformedArgs = [];
|
|
335
|
+
let argIndex = 0;
|
|
336
|
+
|
|
337
|
+
// Add positional arguments
|
|
338
|
+
Object.keys(args).forEach((argName) => {
|
|
339
|
+
if (argIndex < commanderArgs.length - 1) { // -1 because last arg is options object
|
|
340
|
+
transformedArgs.push(commanderArgs[argIndex++]);
|
|
341
|
+
} else {
|
|
342
|
+
// Use default value if not provided
|
|
343
|
+
transformedArgs.push(args[argName]);
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Handle options object
|
|
348
|
+
const optionsObj = commanderArgs[commanderArgs.length - 1];
|
|
349
|
+
if (optionsObj && typeof optionsObj === 'object') {
|
|
350
|
+
Object.keys(opts).forEach((optName) => {
|
|
351
|
+
const dasherizedOpt = optName.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
352
|
+
if (optionsObj[dasherizedOpt] !== undefined) {
|
|
353
|
+
transformedArgs.push(optionsObj[dasherizedOpt]);
|
|
354
|
+
} else {
|
|
355
|
+
// Use default value
|
|
356
|
+
transformedArgs.push(opts[optName]);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Call the original function with transformed arguments
|
|
348
362
|
try {
|
|
349
|
-
|
|
350
|
-
const result = await exec(['npm run ', ''], scriptName);
|
|
351
|
-
return result;
|
|
363
|
+
return await commandFn(...transformedArgs);
|
|
352
364
|
} catch (error) {
|
|
353
|
-
|
|
365
|
+
// Handle errors thrown from functions properly
|
|
366
|
+
console.error(`\n❌ Error: ${error.message}`);
|
|
367
|
+
if (error.stack && process.env.BUNOSH_DEBUG) {
|
|
368
|
+
console.error(error.stack);
|
|
369
|
+
}
|
|
354
370
|
process.exit(1);
|
|
355
371
|
}
|
|
356
372
|
};
|
|
357
373
|
}
|
|
358
374
|
|
|
375
|
+
|
|
359
376
|
const editCmd = program.command('edit')
|
|
360
377
|
.description('Open the bunosh file in your editor. $EDITOR or \'code\' is used.')
|
|
361
378
|
.action(async () => {
|
|
@@ -460,138 +477,133 @@ export default async function bunosh(commands, source) {
|
|
|
460
477
|
internalCommands.push(setupCompletionCmd);
|
|
461
478
|
|
|
462
479
|
const upgradeCmd = program.command('upgrade')
|
|
463
|
-
.description('Upgrade bunosh to the latest version
|
|
480
|
+
.description('Upgrade bunosh to the latest version')
|
|
464
481
|
.option('-f, --force', 'Force upgrade even if already on latest version')
|
|
465
482
|
.option('--check', 'Check for updates without upgrading')
|
|
466
483
|
.action(async (options) => {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
console.log('📦 Bunosh is installed via npm.');
|
|
470
|
-
console.log('To upgrade, run: ' + color.bold('npm update -g bunosh'));
|
|
471
|
-
process.exit(0);
|
|
472
|
-
}
|
|
484
|
+
await upgradeCommand(options);
|
|
485
|
+
});
|
|
473
486
|
|
|
474
|
-
|
|
475
|
-
console.log(`📍 Current version: ${color.bold(currentVersion)}`);
|
|
487
|
+
internalCommands.push(upgradeCmd);
|
|
476
488
|
|
|
477
|
-
if (options.check) {
|
|
478
|
-
console.log('🔍 Checking for updates...');
|
|
479
|
-
try {
|
|
480
|
-
const { getLatestRelease, isNewerVersion } = await import('./upgrade.js');
|
|
481
|
-
const release = await getLatestRelease();
|
|
482
|
-
const latestVersion = release.tag_name;
|
|
483
489
|
|
|
484
|
-
|
|
490
|
+
// Add organized command help sections
|
|
491
|
+
let helpText = '';
|
|
485
492
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
console.error(`❌ Failed to check for updates: ${error.message}`);
|
|
494
|
-
process.exit(1);
|
|
495
|
-
}
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
493
|
+
// Main Commands (no namespace)
|
|
494
|
+
if (commandsByNamespace[''].length > 0) {
|
|
495
|
+
const mainCommands = commandsByNamespace[''].map(cmd => {
|
|
496
|
+
const processedName = cmd.type === 'bunosh' ? toKebabCase(cmd.name) : cmd.name;
|
|
497
|
+
const cmdObj = program.commands.find(c => c.name() === processedName);
|
|
498
|
+
const description = cmdObj ? cmdObj.description() : '';
|
|
499
|
+
const paddedName = processedName.padEnd(22);
|
|
498
500
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
+
if (!description) {
|
|
502
|
+
return ` ${color.white.bold(paddedName)}`;
|
|
503
|
+
}
|
|
501
504
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
lastMessage = message;
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
});
|
|
505
|
+
// Handle multi-line descriptions with proper indentation
|
|
506
|
+
const lines = description.split('\n');
|
|
507
|
+
const firstLine = ` ${color.white.bold(paddedName)} ${lines[0]}`;
|
|
508
|
+
const indentedLines = lines.slice(1).map(line =>
|
|
509
|
+
line.trim() ? ` ${line}` : ''
|
|
510
|
+
).filter(line => line);
|
|
512
511
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
console.log();
|
|
518
|
-
console.log(`💡 Run ${color.bold('bunosh --version')} to verify the new version.`);
|
|
519
|
-
} else {
|
|
520
|
-
console.log(`✅ ${color.green(result.message)}`);
|
|
521
|
-
if (!options.force) {
|
|
522
|
-
console.log(` Use ${color.bold('--force')} to reinstall the current version.`);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
512
|
+
return [firstLine, ...indentedLines].join('\n');
|
|
513
|
+
}).join('\n');
|
|
514
|
+
helpText += `Commands:
|
|
515
|
+
${mainCommands}
|
|
525
516
|
|
|
526
|
-
|
|
527
|
-
|
|
517
|
+
`;
|
|
518
|
+
}
|
|
528
519
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
520
|
+
// Dev Commands (dev namespace)
|
|
521
|
+
if (commandsByNamespace.dev.length > 0) {
|
|
522
|
+
const devCommands = commandsByNamespace.dev.map(cmd => {
|
|
523
|
+
let processedName;
|
|
524
|
+
if (cmd.type === 'namespace') {
|
|
525
|
+
// For namespaced commands, handle the name properly
|
|
526
|
+
if (cmd.name.includes(':')) {
|
|
527
|
+
// If cmd.name already has namespace (like 'dev:devFn'), only process the part after the colon
|
|
528
|
+
const [namespace, functionName] = cmd.name.split(':');
|
|
529
|
+
processedName = `${namespace}:${toKebabCase(functionName).replace(/^[^:]+:/, '')}`;
|
|
530
|
+
} else {
|
|
531
|
+
// If cmd.name doesn't have namespace, use toKebabCase which will add it
|
|
532
|
+
processedName = toKebabCase(cmd.name);
|
|
538
533
|
}
|
|
534
|
+
} else {
|
|
535
|
+
processedName = cmd.name;
|
|
536
|
+
}
|
|
537
|
+
// Debug removed
|
|
538
|
+
const cmdObj = program.commands.find(c => c.name() === processedName);
|
|
539
|
+
const description = cmdObj ? cmdObj.description() : '';
|
|
540
|
+
const paddedName = processedName.padEnd(22);
|
|
539
541
|
|
|
540
|
-
|
|
542
|
+
if (!description) {
|
|
543
|
+
return ` ${color.white.bold(paddedName)}`;
|
|
541
544
|
}
|
|
542
|
-
});
|
|
543
545
|
|
|
544
|
-
|
|
546
|
+
// Handle multi-line descriptions with proper indentation
|
|
547
|
+
const lines = description.split('\n');
|
|
548
|
+
const firstLine = ` ${color.white.bold(paddedName)} ${lines[0]}`;
|
|
549
|
+
const indentedLines = lines.slice(1).map(line =>
|
|
550
|
+
line.trim() ? ` ${line}` : ''
|
|
551
|
+
).filter(line => line);
|
|
545
552
|
|
|
546
|
-
|
|
547
|
-
const homeTaskNamesForHelp = Object.keys(homeTasks).filter(key => typeof homeTasks[key] === 'function');
|
|
548
|
-
if (homeTaskNamesForHelp.length > 0) {
|
|
549
|
-
const homeCommandsList = homeTaskNamesForHelp.sort().map(taskName => {
|
|
550
|
-
const commandName = `my:${taskName}`;
|
|
551
|
-
const taskComment = homeComments[taskName] || '';
|
|
552
|
-
const description = taskComment ? taskComment.split('\n')[0] : 'Personal command';
|
|
553
|
-
return ` ${color.white.bold(commandName.padEnd(18))} ${color.gray(description)}`;
|
|
553
|
+
return [firstLine, ...indentedLines].join('\n');
|
|
554
554
|
}).join('\n');
|
|
555
|
+
helpText += `Dev Commands:
|
|
556
|
+
${devCommands}
|
|
555
557
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
My Commands (from ~/${BUNOSHFILE}):
|
|
559
|
-
${homeCommandsList}
|
|
560
|
-
`);
|
|
558
|
+
`;
|
|
561
559
|
}
|
|
562
560
|
|
|
563
|
-
// Add
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
561
|
+
// Add other namespace sections dynamically
|
|
562
|
+
Object.keys(commandsByNamespace).forEach(namespace => {
|
|
563
|
+
if (namespace && namespace !== 'dev' && commandsByNamespace[namespace].length > 0) {
|
|
564
|
+
const namespaceName = namespace.charAt(0).toUpperCase() + namespace.slice(1) + ' Commands';
|
|
565
|
+
const namespaceCommands = commandsByNamespace[namespace].map(cmd => {
|
|
566
|
+
const cmdObj = program.commands.find(c => c.name() === cmd.name);
|
|
567
|
+
const description = cmdObj ? cmdObj.description() : '';
|
|
568
|
+
const paddedName = cmd.name.padEnd(22);
|
|
569
|
+
|
|
570
|
+
if (!description) {
|
|
571
|
+
return ` ${color.white.bold(paddedName)}`;
|
|
572
|
+
}
|
|
573
573
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
`)
|
|
577
|
-
|
|
574
|
+
// Handle multi-line descriptions with proper indentation
|
|
575
|
+
const lines = description.split('\n');
|
|
576
|
+
const firstLine = ` ${color.white.bold(paddedName)} ${lines[0]}`;
|
|
577
|
+
const indentedLines = lines.slice(1).map(line =>
|
|
578
|
+
line.trim() ? ` ${line}` : ''
|
|
579
|
+
).filter(line => line);
|
|
578
580
|
|
|
579
|
-
|
|
581
|
+
return [firstLine, ...indentedLines].join('\n');
|
|
582
|
+
}).join('\n');
|
|
583
|
+
helpText += `${namespaceName}:
|
|
584
|
+
${namespaceCommands}
|
|
580
585
|
|
|
581
|
-
|
|
586
|
+
`;
|
|
587
|
+
}
|
|
588
|
+
});
|
|
582
589
|
|
|
583
|
-
📝 Edit bunosh file: ${color.bold('bunosh edit')}
|
|
584
|
-
📥 Export commands as scripts to package.json: ${color.bold('bunosh export:scripts')}
|
|
585
|
-
🦾 Upgrade bunosh: ${color.bold('bunosh upgrade')}
|
|
586
590
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
${color.bold('bunosh
|
|
591
|
+
// Special Commands
|
|
592
|
+
helpText += color.dim(`Special Commands:
|
|
593
|
+
${color.bold('bunosh edit')} 📝 Edit bunosh file with $EDITOR
|
|
594
|
+
${color.bold('bunosh export:scripts')} 📥 Export commands to package.json
|
|
595
|
+
${color.bold('bunosh upgrade')} 🦾 Upgrade bunosh
|
|
596
|
+
${color.bold('bunosh -e "say(\'Hi\')"')} 🔧 Run inline Bunosh script
|
|
597
|
+
${color.bold('bunosh --bunoshfile …')} 🥧 Load custom Bunoshfile from path
|
|
598
|
+
${color.bold('bunosh --env-file …')} 🔧 Load custom environment file
|
|
590
599
|
`);
|
|
591
600
|
|
|
601
|
+
program.addHelpText('after', helpText);
|
|
602
|
+
|
|
592
603
|
program.on("command:*", (cmd) => {
|
|
593
|
-
console.
|
|
604
|
+
console.error(`\nUnknown command ${cmd}\n`);
|
|
594
605
|
program.outputHelp();
|
|
606
|
+
process.exit(1);
|
|
595
607
|
});
|
|
596
608
|
|
|
597
609
|
// Show help if no command provided
|
|
@@ -601,8 +613,9 @@ Execute JavaScript:
|
|
|
601
613
|
}
|
|
602
614
|
|
|
603
615
|
program.parse(process.argv);
|
|
616
|
+
}
|
|
604
617
|
|
|
605
|
-
|
|
618
|
+
function fetchComments() {
|
|
606
619
|
const comments = {};
|
|
607
620
|
|
|
608
621
|
let startFromLine = 0;
|
|
@@ -643,157 +656,32 @@ Execute JavaScript:
|
|
|
643
656
|
return comments;
|
|
644
657
|
}
|
|
645
658
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
sourceType: "module",
|
|
655
|
-
ranges: true,
|
|
656
|
-
tokens: true,
|
|
657
|
-
comments: true,
|
|
658
|
-
attachComment: true,
|
|
659
|
-
});
|
|
660
|
-
} catch (parseError) {
|
|
661
|
-
console.warn('Warning: Could not parse home Bunoshfile for comments:', parseError.message);
|
|
662
|
-
return {};
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
let startFromLine = 0;
|
|
666
|
-
|
|
667
|
-
traverse(homeCompleteAst, {
|
|
668
|
-
FunctionDeclaration(path) {
|
|
669
|
-
const functionName = path.node.id && path.node.id.name;
|
|
670
|
-
|
|
671
|
-
const commentSource = homeSource
|
|
672
|
-
.split("\n")
|
|
673
|
-
.slice(startFromLine, path.node?.loc?.start?.line)
|
|
674
|
-
.join("\n");
|
|
675
|
-
const matches = commentSource.match(
|
|
676
|
-
/\/\*\*\s([\s\S]*)\\*\/\s*export/,
|
|
677
|
-
);
|
|
678
|
-
|
|
679
|
-
if (matches && matches[1]) {
|
|
680
|
-
homeComments[functionName] = matches[1]
|
|
681
|
-
.replace(/^\s*\*\s*/gm, "")
|
|
682
|
-
.replace(/\s*\*\*\s*$/gm, "")
|
|
683
|
-
.trim()
|
|
684
|
-
.replace(/^@.*$/gm, "")
|
|
685
|
-
.trim();
|
|
686
|
-
} else {
|
|
687
|
-
// Check for comments attached to the first statement in the function body
|
|
688
|
-
const firstStatement = path.node?.body?.body?.[0];
|
|
689
|
-
const leadingComments = firstStatement?.leadingComments;
|
|
690
|
-
|
|
691
|
-
if (leadingComments && leadingComments.length > 0) {
|
|
692
|
-
homeComments[functionName] = leadingComments[0].value.trim();
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
startFromLine = path.node?.loc?.end?.line;
|
|
697
|
-
},
|
|
698
|
-
});
|
|
699
|
-
|
|
700
|
-
return homeComments;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
function fetchHomeFnAst(fnName, source) {
|
|
704
|
-
try {
|
|
705
|
-
return babelParser.parse(source, {
|
|
706
|
-
sourceType: "module",
|
|
707
|
-
ranges: true,
|
|
708
|
-
tokens: true,
|
|
709
|
-
comments: true,
|
|
710
|
-
attachComment: true,
|
|
711
|
-
});
|
|
712
|
-
} catch (parseError) {
|
|
713
|
-
console.warn('Warning: Could not parse home function AST:', parseError.message);
|
|
714
|
-
return null;
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
function parseHomeArgs(fnName, ast) {
|
|
719
|
-
if (!ast) return {};
|
|
720
|
-
|
|
721
|
-
const functionArguments = {};
|
|
722
|
-
|
|
723
|
-
traverse(ast, {
|
|
724
|
-
FunctionDeclaration(path) {
|
|
725
|
-
if (path.node.id.name !== fnName) return;
|
|
726
|
-
|
|
727
|
-
const params = path.node.params
|
|
728
|
-
.filter((node) => {
|
|
729
|
-
return node?.right?.type !== "ObjectExpression";
|
|
730
|
-
})
|
|
731
|
-
.forEach((param) => {
|
|
732
|
-
if (param.type === "AssignmentPattern") {
|
|
733
|
-
functionArguments[param.left.name] = param.right.value;
|
|
734
|
-
return;
|
|
735
|
-
}
|
|
736
|
-
if (!param.name) return;
|
|
737
|
-
|
|
738
|
-
return functionArguments[param.name] = null;
|
|
739
|
-
});
|
|
740
|
-
},
|
|
741
|
-
});
|
|
742
|
-
|
|
743
|
-
return functionArguments;
|
|
659
|
+
function prepareCommandName(name) {
|
|
660
|
+
// name is already the final command name (could be namespaced or not)
|
|
661
|
+
// For namespaced commands, only transform the function part (after the last colon)
|
|
662
|
+
const lastColonIndex = name.lastIndexOf(':');
|
|
663
|
+
if (lastColonIndex !== -1) {
|
|
664
|
+
const namespace = name.substring(0, lastColonIndex);
|
|
665
|
+
const commandPart = name.substring(lastColonIndex + 1);
|
|
666
|
+
return `${namespace}:${toKebabCase(commandPart)}`;
|
|
744
667
|
}
|
|
745
668
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
let functionOpts = {};
|
|
750
|
-
|
|
751
|
-
traverse(ast, {
|
|
752
|
-
FunctionDeclaration(path) {
|
|
753
|
-
if (path.node.id.name !== fnName) return;
|
|
754
|
-
|
|
755
|
-
const node = path.node.params.pop();
|
|
756
|
-
if (!node) return;
|
|
757
|
-
if (
|
|
758
|
-
!node.type === "AssignmentPattern" &&
|
|
759
|
-
node.right.type === "ObjectExpression"
|
|
760
|
-
)
|
|
761
|
-
return;
|
|
762
|
-
|
|
763
|
-
node?.right?.properties?.forEach((p) => {
|
|
764
|
-
if (
|
|
765
|
-
["NumericLiteral", "StringLiteral", "BooleanLiteral"].includes(
|
|
766
|
-
p.value.type,
|
|
767
|
-
)
|
|
768
|
-
) {
|
|
769
|
-
functionOpts[camelToDasherize(p.key.name)] = p.value.value;
|
|
770
|
-
return;
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
if (p.value.type === "NullLiteral") {
|
|
774
|
-
functionOpts[camelToDasherize(p.key.name)] = null;
|
|
775
|
-
return;
|
|
776
|
-
}
|
|
669
|
+
// For non-namespaced commands, just convert to kebab-case
|
|
670
|
+
return toKebabCase(name);
|
|
671
|
+
}
|
|
777
672
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
!p.value.argument.value;
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
783
|
-
});
|
|
784
|
-
},
|
|
785
|
-
});
|
|
673
|
+
function toKebabCase(name) {
|
|
674
|
+
const parts = name.split(/(?=[A-Z])/);
|
|
786
675
|
|
|
787
|
-
|
|
676
|
+
// If there are multiple parts, treat first part as namespace with colon
|
|
677
|
+
if (parts.length > 1) {
|
|
678
|
+
const namespace = parts[0].toLowerCase();
|
|
679
|
+
const command = parts.slice(1).join("-").toLowerCase();
|
|
680
|
+
return `${namespace}:${command}`;
|
|
788
681
|
}
|
|
789
|
-
}
|
|
790
682
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
.split(/(?=[A-Z])/)
|
|
794
|
-
.join("-")
|
|
795
|
-
.toLowerCase();
|
|
796
|
-
return name.replace("-", ":");
|
|
683
|
+
// Single word, just return lowercase
|
|
684
|
+
return name.toLowerCase();
|
|
797
685
|
}
|
|
798
686
|
|
|
799
687
|
function camelToDasherize(camelCaseString) {
|
|
@@ -854,49 +742,239 @@ function exportFn(commands) {
|
|
|
854
742
|
return true;
|
|
855
743
|
}
|
|
856
744
|
|
|
857
|
-
function loadNpmScripts() {
|
|
858
|
-
try {
|
|
859
|
-
if (!existsSync('package.json')) {
|
|
860
|
-
return {};
|
|
861
|
-
}
|
|
862
745
|
|
|
863
|
-
|
|
864
|
-
|
|
746
|
+
function extractCommentForFunction(ast, source, fnName) {
|
|
747
|
+
let comment = '';
|
|
748
|
+
|
|
749
|
+
traverse(ast, {
|
|
750
|
+
FunctionDeclaration(path) {
|
|
751
|
+
if (path.node.id?.name !== fnName) return;
|
|
752
|
+
|
|
753
|
+
const functionStartLine = path.node.loc.start.line;
|
|
754
|
+
|
|
755
|
+
// Find the JSDoc comment that immediately precedes this function
|
|
756
|
+
if (ast.comments) {
|
|
757
|
+
for (const astComment of ast.comments) {
|
|
758
|
+
if (astComment.type === 'CommentBlock' && astComment.value.startsWith('*')) {
|
|
759
|
+
const commentEndLine = astComment.loc.end.line;
|
|
865
760
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
761
|
+
// Check if this comment is immediately before the function
|
|
762
|
+
if (commentEndLine === functionStartLine - 1) {
|
|
763
|
+
comment = astComment.value
|
|
764
|
+
.replace(/^\s*\*\s*/gm, '') // Remove leading * and spaces
|
|
765
|
+
.replace(/^\s*@.*$/gm, '') // Remove @param and other @ tags
|
|
766
|
+
.replace(/\n\s*\n/g, '\n') // Remove excessive empty lines
|
|
767
|
+
.replace(/^\*\s*/, '') // Remove any remaining leading *
|
|
768
|
+
.trim();
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
871
773
|
}
|
|
872
|
-
});
|
|
873
774
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
775
|
+
// If no JSDoc comment found, check for comments attached to the first statement in the function body
|
|
776
|
+
if (!comment) {
|
|
777
|
+
const firstStatement = path.node?.body?.body?.[0];
|
|
778
|
+
const statementLeadingComments = firstStatement?.leadingComments;
|
|
779
|
+
|
|
780
|
+
if (statementLeadingComments && statementLeadingComments.length > 0) {
|
|
781
|
+
comment = statementLeadingComments[0].value.trim();
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
},
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
return comment;
|
|
879
788
|
}
|
|
880
789
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
790
|
+
function parseArgs(ast, fnName) {
|
|
791
|
+
const functionArguments = {};
|
|
792
|
+
|
|
793
|
+
traverse(ast, {
|
|
794
|
+
FunctionDeclaration(path) {
|
|
795
|
+
if (path.node.id.name !== fnName) return;
|
|
796
|
+
|
|
797
|
+
const params = path.node.params
|
|
798
|
+
.filter((node) => {
|
|
799
|
+
return node?.right?.type !== "ObjectExpression";
|
|
800
|
+
})
|
|
801
|
+
.forEach((param) => {
|
|
802
|
+
if (param.type === "AssignmentPattern") {
|
|
803
|
+
functionArguments[param.left.name] = param.right.value;
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
if (!param.name) return;
|
|
888
807
|
|
|
889
|
-
|
|
890
|
-
|
|
808
|
+
return functionArguments[param.name] = undefined;
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
},
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
return functionArguments;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function parseOpts(ast, fnName) {
|
|
818
|
+
let functionOpts = {};
|
|
819
|
+
|
|
820
|
+
traverse(ast, {
|
|
821
|
+
FunctionDeclaration(path) {
|
|
822
|
+
if (path.node.id.name !== fnName) return;
|
|
823
|
+
|
|
824
|
+
const node = path.node.params.pop();
|
|
825
|
+
if (!node) return;
|
|
826
|
+
if (
|
|
827
|
+
node.type !== "AssignmentPattern" ||
|
|
828
|
+
node.right.type !== "ObjectExpression"
|
|
829
|
+
)
|
|
830
|
+
return;
|
|
831
|
+
|
|
832
|
+
node?.right?.properties?.forEach((p) => {
|
|
833
|
+
if (
|
|
834
|
+
["NumericLiteral", "StringLiteral", "BooleanLiteral"].includes(
|
|
835
|
+
p.value.type,
|
|
836
|
+
)
|
|
837
|
+
) {
|
|
838
|
+
functionOpts[camelToDasherize(p.key.name)] = p.value.value;
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (p.value.type === "NullLiteral") {
|
|
843
|
+
functionOpts[camelToDasherize(p.key.name)] = null;
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (p.value.type == "UnaryExpression" && p.value.operator == "!") {
|
|
848
|
+
functionOpts[camelToDasherize(p.key.name)] =
|
|
849
|
+
!p.value.argument.value;
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
},
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
return functionOpts;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* Represents a parsed Bunosh command with all its metadata
|
|
861
|
+
*/
|
|
862
|
+
export class BunoshCommand {
|
|
863
|
+
constructor(name, namespace, args, opts, comment, fn) {
|
|
864
|
+
this.name = name;
|
|
865
|
+
this.namespace = namespace || '';
|
|
866
|
+
this.args = args;
|
|
867
|
+
this.opts = opts;
|
|
868
|
+
this.comment = comment;
|
|
869
|
+
this.function = fn;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Get the full command name (namespace:name if namespace exists)
|
|
874
|
+
*/
|
|
875
|
+
get fullName() {
|
|
876
|
+
return this.namespace ? `${this.namespace}:${this.name}` : this.name;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Get the command name in kebab-case for CLI usage
|
|
881
|
+
*/
|
|
882
|
+
get cliName() {
|
|
883
|
+
if (this.namespace) {
|
|
884
|
+
return `${this.namespace}:${camelToDasherize(this.name)}`;
|
|
891
885
|
}
|
|
886
|
+
return camelToDasherize(this.name);
|
|
887
|
+
}
|
|
892
888
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
889
|
+
/**
|
|
890
|
+
* Get all parameter names (args + opts)
|
|
891
|
+
*/
|
|
892
|
+
get allParams() {
|
|
893
|
+
return [...Object.keys(this.args), ...Object.keys(this.opts)];
|
|
894
|
+
}
|
|
896
895
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
896
|
+
/**
|
|
897
|
+
* Get required parameter names
|
|
898
|
+
*/
|
|
899
|
+
get requiredParams() {
|
|
900
|
+
return Object.keys(this.args).filter(arg => this.args[arg] === undefined);
|
|
901
901
|
}
|
|
902
902
|
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Process commands and sources to extract structured command information
|
|
906
|
+
* This reuses all the existing parsing logic from the main bunosh function
|
|
907
|
+
* @param {Object} commands - Commands object from Bunoshfile
|
|
908
|
+
* @param {Object} sources - Sources object containing comments and metadata
|
|
909
|
+
* @returns {Array<BunoshCommand>} Array of parsed BunoshCommand objects
|
|
910
|
+
*/
|
|
911
|
+
export function processCommands(commands, sources) {
|
|
912
|
+
const parsedCommands = [];
|
|
913
|
+
|
|
914
|
+
// Parse AST and comments for each source (same as in main bunosh function)
|
|
915
|
+
const comments = {};
|
|
916
|
+
const namespaceSources = {};
|
|
917
|
+
|
|
918
|
+
for (const [cmdName, cmdInfo] of Object.entries(sources)) {
|
|
919
|
+
if (cmdInfo.source) {
|
|
920
|
+
try {
|
|
921
|
+
const ast = babelParser.parse(cmdInfo.source, {
|
|
922
|
+
sourceType: "module",
|
|
923
|
+
ranges: true,
|
|
924
|
+
tokens: true,
|
|
925
|
+
comments: true,
|
|
926
|
+
attachComment: true,
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
// Store AST for this command
|
|
930
|
+
if (!namespaceSources[cmdInfo.namespace || '']) {
|
|
931
|
+
namespaceSources[cmdInfo.namespace || ''] = {
|
|
932
|
+
ast: ast,
|
|
933
|
+
source: cmdInfo.source
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Extract comments for this command
|
|
938
|
+
const fnName = cmdInfo.namespace ? cmdName.split(':')[1] : cmdName;
|
|
939
|
+
if (fnName) {
|
|
940
|
+
comments[cmdName] = extractCommentForFunction(ast, cmdInfo.source, fnName);
|
|
941
|
+
}
|
|
942
|
+
} catch (parseError) {
|
|
943
|
+
parseError.code = 'BABEL_PARSER_SYNTAX_ERROR';
|
|
944
|
+
throw parseError;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// Process each command using the same logic as the main bunosh function
|
|
950
|
+
Object.entries(commands).forEach(([cmdName, cmdFn]) => {
|
|
951
|
+
const sourceInfo = sources[cmdName];
|
|
952
|
+
const originalFnName = sourceInfo?.originalFnName || cmdName.split(':')[1] || cmdName;
|
|
953
|
+
const namespace = sourceInfo?.namespace || '';
|
|
954
|
+
const namespaceSource = namespaceSources[namespace];
|
|
955
|
+
const comment = comments[cmdName];
|
|
956
|
+
|
|
957
|
+
// Parse function using the same logic as the main bunosh function
|
|
958
|
+
const fnBody = cmdFn.toString();
|
|
959
|
+
const ast = namespaceSource?.ast || babelParser.parse(fnBody, { comment: true, tokens: true });
|
|
960
|
+
const args = parseArgs(ast, originalFnName);
|
|
961
|
+
const opts = parseOpts(ast, originalFnName);
|
|
962
|
+
|
|
963
|
+
// Extract the actual command name without namespace
|
|
964
|
+
const commandName = originalFnName;
|
|
965
|
+
|
|
966
|
+
parsedCommands.push(new BunoshCommand(
|
|
967
|
+
commandName,
|
|
968
|
+
namespace,
|
|
969
|
+
args,
|
|
970
|
+
opts,
|
|
971
|
+
comment,
|
|
972
|
+
cmdFn
|
|
973
|
+
));
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
return parsedCommands;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// Export parsing functions for use in MCP server
|
|
980
|
+
export { parseArgs, parseOpts, extractCommentForFunction };
|