bunosh 0.4.13 → 0.5.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/README.md +93 -565
- package/bunosh.js +33 -154
- package/package.json +1 -1
- package/src/error-formatter.js +80 -0
- package/src/program.js +85 -304
package/src/program.js
CHANGED
|
@@ -5,6 +5,7 @@ const traverse = traverseDefault.default || traverseDefault;
|
|
|
5
5
|
import color from "chalk";
|
|
6
6
|
import { readFileSync, existsSync, writeFileSync } from 'fs';
|
|
7
7
|
import { yell } from './io.js';
|
|
8
|
+
import { formatError } from './error-formatter.js';
|
|
8
9
|
import cprint from "./font.js";
|
|
9
10
|
import { handleCompletion, detectCurrentShell, installCompletion, getCompletionPaths } from './completion.js';
|
|
10
11
|
import { upgradeCommand } from './upgrade.js';
|
|
@@ -32,30 +33,25 @@ export const banner = () => {
|
|
|
32
33
|
} catch (e) {
|
|
33
34
|
}
|
|
34
35
|
console.log(color.gray('🍲 ', color.yellowBright.bold('BUNOSH'), color.yellow(version)));
|
|
35
|
-
|
|
36
36
|
console.log();
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
function createGradientAscii(asciiArt) {
|
|
40
40
|
const lines = asciiArt.split('\n');
|
|
41
41
|
|
|
42
|
-
// Yellow RGB (255, 220, 0) to Brown RGB (139, 69, 19)
|
|
43
42
|
const startColor = { r: 255, g: 220, b: 0 };
|
|
44
43
|
const endColor = { r: 139, g: 69, b: 19 };
|
|
45
44
|
|
|
46
45
|
return lines.map((line, index) => {
|
|
47
|
-
// Block characters should always be brown
|
|
48
46
|
if (line.includes('░') || line.includes('▒') || line.includes('▓')) {
|
|
49
47
|
return `\x1b[38;2;139;69;19m${line}\x1b[0m`;
|
|
50
48
|
}
|
|
51
49
|
|
|
52
|
-
// Create smooth gradient for other characters
|
|
53
50
|
const progress = index / (lines.length - 1);
|
|
54
51
|
const r = Math.round(startColor.r + (endColor.r - startColor.r) * progress);
|
|
55
52
|
const g = Math.round(startColor.g + (endColor.g - startColor.g) * progress);
|
|
56
53
|
const b = Math.round(startColor.b + (endColor.b - startColor.b) * progress);
|
|
57
54
|
|
|
58
|
-
// Use true color escape sequence
|
|
59
55
|
return `\x1b[38;2;${r};${g};${b}m${line}\x1b[0m`;
|
|
60
56
|
}).join('\n');
|
|
61
57
|
}
|
|
@@ -67,10 +63,9 @@ export default async function bunosh(commands, sources) {
|
|
|
67
63
|
|
|
68
64
|
const internalCommands = [];
|
|
69
65
|
|
|
70
|
-
|
|
66
|
+
|
|
71
67
|
program.configureHelp({
|
|
72
68
|
commandDescription: _cmd => {
|
|
73
|
-
// Show banner and description
|
|
74
69
|
banner();
|
|
75
70
|
return ' ';
|
|
76
71
|
},
|
|
@@ -79,28 +74,23 @@ export default async function bunosh(commands, sources) {
|
|
|
79
74
|
visibleGlobalOptions: _opt => [],
|
|
80
75
|
visibleOptions: _opt => [],
|
|
81
76
|
visibleCommands: cmd => {
|
|
82
|
-
// Hide all commands from default listing - we'll show them in custom sections
|
|
83
77
|
return [];
|
|
84
78
|
},
|
|
85
79
|
subcommandTerm: (cmd) => color.white.bold(cmd.name()),
|
|
86
80
|
subcommandDescription: (cmd) => color.gray(cmd.description()),
|
|
87
81
|
});
|
|
88
82
|
|
|
89
|
-
// Don't show global help after error - individual commands will show their own help
|
|
90
83
|
// program.showHelpAfterError();
|
|
91
84
|
program.showSuggestionAfterError(true);
|
|
92
85
|
program.addHelpCommand(false);
|
|
93
86
|
|
|
94
|
-
// Override the program's error output formatting
|
|
95
87
|
program.configureOutput({
|
|
96
88
|
writeErr: (str) => {
|
|
97
|
-
// Replace "error:" with red "Error:" in error output
|
|
98
89
|
process.stderr.write(str.replace(/^error:/g, color.red('Error') + ':'));
|
|
99
90
|
},
|
|
100
91
|
writeOut: (str) => process.stdout.write(str)
|
|
101
92
|
});
|
|
102
93
|
|
|
103
|
-
// Parse AST and comments for each source
|
|
104
94
|
const comments = {};
|
|
105
95
|
const namespaceSources = {};
|
|
106
96
|
|
|
@@ -115,7 +105,6 @@ export default async function bunosh(commands, sources) {
|
|
|
115
105
|
attachComment: true,
|
|
116
106
|
});
|
|
117
107
|
|
|
118
|
-
// Store AST for this command
|
|
119
108
|
if (!namespaceSources[cmdInfo.namespace || '']) {
|
|
120
109
|
namespaceSources[cmdInfo.namespace || ''] = {
|
|
121
110
|
ast: ast,
|
|
@@ -123,27 +112,22 @@ export default async function bunosh(commands, sources) {
|
|
|
123
112
|
};
|
|
124
113
|
}
|
|
125
114
|
|
|
126
|
-
// Extract comments for this command
|
|
127
115
|
const fnName = cmdInfo.namespace ? cmdName.split(':')[1] : cmdName;
|
|
128
116
|
if (fnName) {
|
|
129
117
|
comments[cmdName] = extractCommentForFunction(ast, cmdInfo.source, fnName);
|
|
130
118
|
}
|
|
131
119
|
} catch (parseError) {
|
|
132
|
-
// Re-throw with more specific error information
|
|
133
120
|
parseError.code = 'BABEL_PARSER_SYNTAX_ERROR';
|
|
134
121
|
throw parseError;
|
|
135
122
|
}
|
|
136
123
|
}
|
|
137
124
|
}
|
|
138
125
|
|
|
139
|
-
// Collect all commands (bunosh + namespace commands + npm scripts) and sort them
|
|
140
126
|
const allCommands = [];
|
|
141
127
|
|
|
142
|
-
// Add bunosh commands (including namespaced ones)
|
|
143
128
|
Object.keys(commands).forEach((cmdName) => {
|
|
144
129
|
const sourceInfo = sources[cmdName];
|
|
145
130
|
if (sourceInfo && sourceInfo.namespace) {
|
|
146
|
-
// This is a namespaced command
|
|
147
131
|
allCommands.push({
|
|
148
132
|
type: 'namespace',
|
|
149
133
|
name: cmdName,
|
|
@@ -151,26 +135,21 @@ export default async function bunosh(commands, sources) {
|
|
|
151
135
|
data: commands[cmdName]
|
|
152
136
|
});
|
|
153
137
|
} else {
|
|
154
|
-
// Regular bunosh command
|
|
155
138
|
allCommands.push({ type: 'bunosh', name: cmdName, data: commands[cmdName] });
|
|
156
139
|
}
|
|
157
140
|
});
|
|
158
141
|
|
|
159
142
|
|
|
160
|
-
|
|
161
|
-
// Sort all commands alphabetically by name
|
|
143
|
+
|
|
162
144
|
allCommands.sort((a, b) => a.name.localeCompare(b.name));
|
|
163
145
|
|
|
164
|
-
// Organize commands by namespace for help display
|
|
165
146
|
const commandsByNamespace = {
|
|
166
|
-
'': [],
|
|
167
|
-
'dev': [],
|
|
147
|
+
'': [],
|
|
148
|
+
'dev': [],
|
|
168
149
|
};
|
|
169
150
|
|
|
170
|
-
// Categorize commands by namespace
|
|
171
151
|
allCommands.forEach(cmd => {
|
|
172
152
|
if (cmd.type === 'namespace' && cmd.namespace) {
|
|
173
|
-
// Group by namespace, defaulting to 'dev' for unknown namespaces
|
|
174
153
|
const namespace = cmd.namespace || 'dev';
|
|
175
154
|
if (!commandsByNamespace[namespace]) {
|
|
176
155
|
commandsByNamespace[namespace] = [];
|
|
@@ -181,170 +160,111 @@ export default async function bunosh(commands, sources) {
|
|
|
181
160
|
}
|
|
182
161
|
});
|
|
183
162
|
|
|
184
|
-
// Process all commands in sorted order
|
|
185
163
|
allCommands.forEach((cmdData) => {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const ast = namespaceSource?.ast || babelParser.parse(fnBody, { comment: true, tokens: true });
|
|
194
|
-
const args = parseArgs(ast, fnName);
|
|
195
|
-
const opts = parseOpts(ast, fnName);
|
|
196
|
-
const comment = comments[fnName];
|
|
197
|
-
|
|
198
|
-
const commandName = prepareCommandName(fnName);
|
|
199
|
-
|
|
200
|
-
const command = program.command(commandName);
|
|
201
|
-
if (comment) {
|
|
202
|
-
command.description(comment);
|
|
203
|
-
}
|
|
204
|
-
command.hook('preAction', (_thisCommand) => {
|
|
205
|
-
process.env.BUNOSH_COMMAND_STARTED = true;
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
let argsAndOptsDescription = [];
|
|
209
|
-
|
|
210
|
-
Object.entries(args).forEach(([arg, value]) => {
|
|
211
|
-
if (value === undefined) {
|
|
212
|
-
argsAndOptsDescription.push(`<${arg}>`);
|
|
213
|
-
return command.argument(`<${arg}>`);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (value === null) {
|
|
217
|
-
argsAndOptsDescription.push(`[${arg}]`);
|
|
218
|
-
return command.argument(`[${arg}]`, '', null);
|
|
219
|
-
}
|
|
164
|
+
const isNamespaced = cmdData.type === 'namespace' && cmdData.namespace;
|
|
165
|
+
const fnName = isNamespaced
|
|
166
|
+
? (sources[cmdData.name].originalFnName || cmdData.name.split(':')[1])
|
|
167
|
+
: cmdData.name;
|
|
168
|
+
const namespace = isNamespaced ? cmdData.namespace : '';
|
|
169
|
+
const fnBody = commands[isNamespaced ? cmdData.name : fnName].toString();
|
|
170
|
+
const namespaceSource = namespaceSources[namespace];
|
|
220
171
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
172
|
+
const ast = namespaceSource?.ast || babelParser.parse(fnBody, { comment: true, tokens: true });
|
|
173
|
+
const args = parseArgs(ast, fnName);
|
|
174
|
+
const opts = parseOpts(ast, fnName);
|
|
175
|
+
const comment = comments[isNamespaced ? cmdData.name : fnName];
|
|
224
176
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
177
|
+
let commandName;
|
|
178
|
+
if (isNamespaced && cmdData.name.includes(':')) {
|
|
179
|
+
commandName = cmdData.name.split(':')[0] + ':' + toKebabCase(cmdData.name.split(':')[1]).replace(/^[^:]+:/, '');
|
|
180
|
+
} else {
|
|
181
|
+
commandName = prepareCommandName(isNamespaced ? cmdData.name : fnName);
|
|
182
|
+
}
|
|
230
183
|
|
|
231
|
-
|
|
232
|
-
|
|
184
|
+
const command = program.command(commandName);
|
|
185
|
+
if (comment) {
|
|
186
|
+
command.description(comment);
|
|
187
|
+
}
|
|
188
|
+
command.hook('preAction', (_thisCommand) => {
|
|
189
|
+
process.env.BUNOSH_COMMAND_STARTED = true;
|
|
233
190
|
|
|
234
|
-
|
|
191
|
+
const isBun = typeof Bun !== 'undefined';
|
|
192
|
+
const runtime = isBun ? 'Bun' : 'Node.js';
|
|
193
|
+
const runtimeColor = isBun ? color.red : color.green;
|
|
235
194
|
|
|
236
|
-
let
|
|
195
|
+
let runtimeVersion;
|
|
196
|
+
if (isBun) {
|
|
197
|
+
runtimeVersion = Bun.version;
|
|
198
|
+
} else {
|
|
199
|
+
runtimeVersion = process.version;
|
|
200
|
+
}
|
|
237
201
|
|
|
238
|
-
|
|
202
|
+
console.log(color.gray(`Runtime: `, runtimeColor.bold(runtime), color.gray(` (${runtimeVersion})`)));
|
|
203
|
+
console.log();
|
|
204
|
+
})
|
|
239
205
|
|
|
240
|
-
|
|
206
|
+
let argsAndOptsDescription = [];
|
|
241
207
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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);
|
|
271
|
-
|
|
272
|
-
const command = program.command(commandName);
|
|
273
|
-
if (comment) {
|
|
274
|
-
command.description(comment);
|
|
208
|
+
Object.entries(args).forEach(([arg, value]) => {
|
|
209
|
+
if (value === undefined) {
|
|
210
|
+
argsAndOptsDescription.push(`<${arg}>`);
|
|
211
|
+
return command.argument(`<${arg}>`);
|
|
275
212
|
}
|
|
276
|
-
command.hook('preAction', (_thisCommand) => {
|
|
277
|
-
process.env.BUNOSH_COMMAND_STARTED = true;
|
|
278
|
-
})
|
|
279
|
-
|
|
280
|
-
let argsAndOptsDescription = [];
|
|
281
|
-
|
|
282
|
-
Object.entries(args).forEach(([arg, value]) => {
|
|
283
|
-
if (value === undefined) {
|
|
284
|
-
argsAndOptsDescription.push(`<${arg}>`);
|
|
285
|
-
return command.argument(`<${arg}>`);
|
|
286
|
-
}
|
|
287
213
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
214
|
+
if (value === null) {
|
|
215
|
+
argsAndOptsDescription.push(`[${arg}]`);
|
|
216
|
+
return command.argument(`[${arg}]`, '', null);
|
|
217
|
+
}
|
|
292
218
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
219
|
+
argsAndOptsDescription.push(`[${arg}=${value}]`);
|
|
220
|
+
command.argument(`[${arg}]`, ``, value);
|
|
221
|
+
});
|
|
296
222
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
223
|
+
Object.entries(opts).forEach(([opt, value]) => {
|
|
224
|
+
if (value === false || value === null) {
|
|
225
|
+
argsAndOptsDescription.push(`--${opt}`);
|
|
226
|
+
return command.option(`--${opt}`);
|
|
227
|
+
}
|
|
302
228
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
229
|
+
argsAndOptsDescription.push(`--${opt}=${value}`);
|
|
230
|
+
command.option(`--${opt} [${opt}]`, "", value);
|
|
231
|
+
});
|
|
306
232
|
|
|
307
|
-
|
|
233
|
+
let description = comment?.split('\n')[0] || '';
|
|
308
234
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
235
|
+
if (comment && argsAndOptsDescription.length) {
|
|
236
|
+
const separator = isNamespaced ? '\n ' : '\n ▹ ';
|
|
237
|
+
description += `${separator}${color.gray(`bunosh ${commandName}`)} ${color.blue(argsAndOptsDescription.join(' ').trim())}`;
|
|
238
|
+
}
|
|
312
239
|
|
|
313
|
-
|
|
240
|
+
command.description(description);
|
|
314
241
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
command.showHelpAfterError();
|
|
242
|
+
command.configureHelp({
|
|
243
|
+
commandDescription: () => comment || '',
|
|
244
|
+
commandUsage: cmd => `bunosh ${cmd.name()}${argsAndOptsDescription.length ? ' ' + argsAndOptsDescription.join(' ').trim() : ''}`,
|
|
245
|
+
showGlobalOptions: false,
|
|
246
|
+
visibleGlobalOptions: () => [],
|
|
247
|
+
visibleOptions: () => [],
|
|
248
|
+
visibleCommands: () => []
|
|
249
|
+
});
|
|
250
|
+
command.showHelpAfterError();
|
|
325
251
|
|
|
326
|
-
|
|
327
|
-
}
|
|
252
|
+
command.action(createCommandAction(commands[isNamespaced ? cmdData.name : fnName], args, opts));
|
|
328
253
|
});
|
|
329
254
|
|
|
330
|
-
// Helper function to create command action with proper argument transformation
|
|
331
255
|
function createCommandAction(commandFn, args, opts) {
|
|
332
256
|
return async (...commanderArgs) => {
|
|
333
|
-
// Transform Commander.js arguments to match function signature
|
|
334
257
|
const transformedArgs = [];
|
|
335
258
|
let argIndex = 0;
|
|
336
259
|
|
|
337
|
-
// Add positional arguments
|
|
338
260
|
Object.keys(args).forEach((argName) => {
|
|
339
|
-
if (argIndex < commanderArgs.length - 1) {
|
|
261
|
+
if (argIndex < commanderArgs.length - 1) {
|
|
340
262
|
transformedArgs.push(commanderArgs[argIndex++]);
|
|
341
263
|
} else {
|
|
342
|
-
// Use default value if not provided
|
|
343
264
|
transformedArgs.push(args[argName]);
|
|
344
265
|
}
|
|
345
266
|
});
|
|
346
267
|
|
|
347
|
-
// Handle options object
|
|
348
268
|
const optionsObj = commanderArgs[commanderArgs.length - 1];
|
|
349
269
|
if (optionsObj && typeof optionsObj === 'object') {
|
|
350
270
|
Object.keys(opts).forEach((optName) => {
|
|
@@ -352,27 +272,21 @@ export default async function bunosh(commands, sources) {
|
|
|
352
272
|
if (optionsObj[dasherizedOpt] !== undefined) {
|
|
353
273
|
transformedArgs.push(optionsObj[dasherizedOpt]);
|
|
354
274
|
} else {
|
|
355
|
-
// Use default value
|
|
356
275
|
transformedArgs.push(opts[optName]);
|
|
357
276
|
}
|
|
358
277
|
});
|
|
359
278
|
}
|
|
360
279
|
|
|
361
|
-
// Call the original function with transformed arguments
|
|
362
280
|
try {
|
|
363
281
|
return await commandFn(...transformedArgs);
|
|
364
282
|
} catch (error) {
|
|
365
|
-
|
|
366
|
-
console.error(`\n❌ Error: ${error.message}`);
|
|
367
|
-
if (error.stack && process.env.BUNOSH_DEBUG) {
|
|
368
|
-
console.error(error.stack);
|
|
369
|
-
}
|
|
283
|
+
console.error('\n' + formatError(error));
|
|
370
284
|
process.exit(1);
|
|
371
285
|
}
|
|
372
286
|
};
|
|
373
287
|
}
|
|
374
288
|
|
|
375
|
-
|
|
289
|
+
|
|
376
290
|
const editCmd = program.command('edit')
|
|
377
291
|
.description('Open the bunosh file in your editor. $EDITOR or \'code\' is used.')
|
|
378
292
|
.action(async () => {
|
|
@@ -417,7 +331,6 @@ export default async function bunosh(commands, sources) {
|
|
|
417
331
|
.option('-s, --shell <shell>', 'Specify shell instead of auto-detection (bash, zsh, fish)')
|
|
418
332
|
.action((options) => {
|
|
419
333
|
try {
|
|
420
|
-
// Detect current shell or use specified shell
|
|
421
334
|
const shell = options.shell || detectCurrentShell();
|
|
422
335
|
|
|
423
336
|
if (!shell) {
|
|
@@ -431,10 +344,8 @@ export default async function bunosh(commands, sources) {
|
|
|
431
344
|
console.log(`🐚 Detected shell: ${color.bold(shell)}`);
|
|
432
345
|
console.log();
|
|
433
346
|
|
|
434
|
-
// Get paths for this shell
|
|
435
347
|
const paths = getCompletionPaths(shell);
|
|
436
348
|
|
|
437
|
-
// Check if already installed
|
|
438
349
|
if (!options.force && existsSync(paths.completionFile)) {
|
|
439
350
|
console.log(`⚠️ Completion already installed at: ${paths.completionFile}`);
|
|
440
351
|
console.log(' Use --force to overwrite, or run:');
|
|
@@ -442,11 +353,9 @@ export default async function bunosh(commands, sources) {
|
|
|
442
353
|
process.exit(0);
|
|
443
354
|
}
|
|
444
355
|
|
|
445
|
-
// Install completion
|
|
446
356
|
console.log('🔧 Installing completion...');
|
|
447
357
|
const result = installCompletion(shell);
|
|
448
358
|
|
|
449
|
-
// Report success
|
|
450
359
|
console.log(`✅ Completion installed: ${color.green(paths.completionFile)}`);
|
|
451
360
|
|
|
452
361
|
if (result.configFile && result.added) {
|
|
@@ -487,10 +396,8 @@ export default async function bunosh(commands, sources) {
|
|
|
487
396
|
internalCommands.push(upgradeCmd);
|
|
488
397
|
|
|
489
398
|
|
|
490
|
-
// Add organized command help sections
|
|
491
399
|
let helpText = '';
|
|
492
400
|
|
|
493
|
-
// Main Commands (no namespace)
|
|
494
401
|
if (commandsByNamespace[''].length > 0) {
|
|
495
402
|
const mainCommands = commandsByNamespace[''].map(cmd => {
|
|
496
403
|
const processedName = cmd.type === 'bunosh' ? toKebabCase(cmd.name) : cmd.name;
|
|
@@ -502,7 +409,6 @@ export default async function bunosh(commands, sources) {
|
|
|
502
409
|
return ` ${color.white.bold(paddedName)}`;
|
|
503
410
|
}
|
|
504
411
|
|
|
505
|
-
// Handle multi-line descriptions with proper indentation
|
|
506
412
|
const lines = description.split('\n');
|
|
507
413
|
const firstLine = ` ${color.white.bold(paddedName)} ${lines[0]}`;
|
|
508
414
|
const indentedLines = lines.slice(1).map(line =>
|
|
@@ -517,24 +423,19 @@ ${mainCommands}
|
|
|
517
423
|
`;
|
|
518
424
|
}
|
|
519
425
|
|
|
520
|
-
// Dev Commands (dev namespace)
|
|
521
426
|
if (commandsByNamespace.dev.length > 0) {
|
|
522
427
|
const devCommands = commandsByNamespace.dev.map(cmd => {
|
|
523
428
|
let processedName;
|
|
524
429
|
if (cmd.type === 'namespace') {
|
|
525
|
-
// For namespaced commands, handle the name properly
|
|
526
430
|
if (cmd.name.includes(':')) {
|
|
527
|
-
// If cmd.name already has namespace (like 'dev:devFn'), only process the part after the colon
|
|
528
431
|
const [namespace, functionName] = cmd.name.split(':');
|
|
529
432
|
processedName = `${namespace}:${toKebabCase(functionName).replace(/^[^:]+:/, '')}`;
|
|
530
433
|
} else {
|
|
531
|
-
// If cmd.name doesn't have namespace, use toKebabCase which will add it
|
|
532
434
|
processedName = toKebabCase(cmd.name);
|
|
533
435
|
}
|
|
534
436
|
} else {
|
|
535
437
|
processedName = cmd.name;
|
|
536
438
|
}
|
|
537
|
-
// Debug removed
|
|
538
439
|
const cmdObj = program.commands.find(c => c.name() === processedName);
|
|
539
440
|
const description = cmdObj ? cmdObj.description() : '';
|
|
540
441
|
const paddedName = processedName.padEnd(22);
|
|
@@ -543,7 +444,6 @@ ${mainCommands}
|
|
|
543
444
|
return ` ${color.white.bold(paddedName)}`;
|
|
544
445
|
}
|
|
545
446
|
|
|
546
|
-
// Handle multi-line descriptions with proper indentation
|
|
547
447
|
const lines = description.split('\n');
|
|
548
448
|
const firstLine = ` ${color.white.bold(paddedName)} ${lines[0]}`;
|
|
549
449
|
const indentedLines = lines.slice(1).map(line =>
|
|
@@ -558,7 +458,6 @@ ${devCommands}
|
|
|
558
458
|
`;
|
|
559
459
|
}
|
|
560
460
|
|
|
561
|
-
// Add other namespace sections dynamically
|
|
562
461
|
Object.keys(commandsByNamespace).forEach(namespace => {
|
|
563
462
|
if (namespace && namespace !== 'dev' && commandsByNamespace[namespace].length > 0) {
|
|
564
463
|
const namespaceName = namespace.charAt(0).toUpperCase() + namespace.slice(1) + ' Commands';
|
|
@@ -571,7 +470,6 @@ ${devCommands}
|
|
|
571
470
|
return ` ${color.white.bold(paddedName)}`;
|
|
572
471
|
}
|
|
573
472
|
|
|
574
|
-
// Handle multi-line descriptions with proper indentation
|
|
575
473
|
const lines = description.split('\n');
|
|
576
474
|
const firstLine = ` ${color.white.bold(paddedName)} ${lines[0]}`;
|
|
577
475
|
const indentedLines = lines.slice(1).map(line =>
|
|
@@ -587,8 +485,6 @@ ${namespaceCommands}
|
|
|
587
485
|
}
|
|
588
486
|
});
|
|
589
487
|
|
|
590
|
-
|
|
591
|
-
// Special Commands
|
|
592
488
|
helpText += color.dim(`Special Commands:
|
|
593
489
|
${color.bold('bunosh edit')} 📝 Edit bunosh file with $EDITOR
|
|
594
490
|
${color.bold('bunosh export:scripts')} 📥 Export commands to package.json
|
|
@@ -606,28 +502,7 @@ ${namespaceCommands}
|
|
|
606
502
|
process.exit(1);
|
|
607
503
|
});
|
|
608
504
|
|
|
609
|
-
|
|
610
|
-
// Handle --version option before parsing
|
|
611
|
-
if (process.argv.includes('--version')) {
|
|
612
|
-
let version = '';
|
|
613
|
-
// For compiled binaries, check if version is embedded at build time
|
|
614
|
-
if (typeof BUNOSH_VERSION !== 'undefined') {
|
|
615
|
-
version = BUNOSH_VERSION;
|
|
616
|
-
} else {
|
|
617
|
-
// For development, try to read from package.json
|
|
618
|
-
try {
|
|
619
|
-
const pkgPath = new URL('../package.json', import.meta.url);
|
|
620
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
621
|
-
version = pkg.version;
|
|
622
|
-
} catch (e) {
|
|
623
|
-
version = '0.1.5'; // fallback to current version
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
console.log(version);
|
|
627
|
-
process.exit(0);
|
|
628
|
-
}
|
|
629
505
|
|
|
630
|
-
// Show help if no command provided
|
|
631
506
|
if (process.argv.length === 2) {
|
|
632
507
|
program.outputHelp();
|
|
633
508
|
return program;
|
|
@@ -636,50 +511,7 @@ ${namespaceCommands}
|
|
|
636
511
|
program.parse(process.argv);
|
|
637
512
|
}
|
|
638
513
|
|
|
639
|
-
function fetchComments() {
|
|
640
|
-
const comments = {};
|
|
641
|
-
|
|
642
|
-
let startFromLine = 0;
|
|
643
|
-
|
|
644
|
-
traverse(completeAst, {
|
|
645
|
-
FunctionDeclaration(path) {
|
|
646
|
-
const functionName = path.node.id && path.node.id.name;
|
|
647
|
-
|
|
648
|
-
const commentSource = source
|
|
649
|
-
.split("\n")
|
|
650
|
-
.slice(startFromLine, path.node?.loc?.start?.line)
|
|
651
|
-
.join("\n");
|
|
652
|
-
const matches = commentSource.match(
|
|
653
|
-
/\/\*\*\s([\s\S]*)\\*\/\s*export/,
|
|
654
|
-
);
|
|
655
|
-
|
|
656
|
-
if (matches && matches[1]) {
|
|
657
|
-
comments[functionName] = matches[1]
|
|
658
|
-
.replace(/^\s*\*\s*/gm, "")
|
|
659
|
-
.replace(/\s*\*\*\s*$/gm, "")
|
|
660
|
-
.trim()
|
|
661
|
-
.replace(/^@.*$/gm, "")
|
|
662
|
-
.trim();
|
|
663
|
-
} else {
|
|
664
|
-
// Check for comments attached to the first statement in the function body
|
|
665
|
-
const firstStatement = path.node?.body?.body?.[0];
|
|
666
|
-
const leadingComments = firstStatement?.leadingComments;
|
|
667
|
-
|
|
668
|
-
if (leadingComments && leadingComments.length > 0) {
|
|
669
|
-
comments[functionName] = leadingComments[0].value.trim();
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
startFromLine = path.node?.loc?.end?.line;
|
|
674
|
-
},
|
|
675
|
-
});
|
|
676
|
-
|
|
677
|
-
return comments;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
514
|
function prepareCommandName(name) {
|
|
681
|
-
// name is already the final command name (could be namespaced or not)
|
|
682
|
-
// For namespaced commands, only transform the function part (after the last colon)
|
|
683
515
|
const lastColonIndex = name.lastIndexOf(':');
|
|
684
516
|
if (lastColonIndex !== -1) {
|
|
685
517
|
const namespace = name.substring(0, lastColonIndex);
|
|
@@ -687,21 +519,18 @@ function prepareCommandName(name) {
|
|
|
687
519
|
return `${namespace}:${toKebabCase(commandPart)}`;
|
|
688
520
|
}
|
|
689
521
|
|
|
690
|
-
// For non-namespaced commands, just convert to kebab-case
|
|
691
522
|
return toKebabCase(name);
|
|
692
523
|
}
|
|
693
524
|
|
|
694
525
|
function toKebabCase(name) {
|
|
695
526
|
const parts = name.split(/(?=[A-Z])/);
|
|
696
527
|
|
|
697
|
-
// If there are multiple parts, treat first part as namespace with colon
|
|
698
528
|
if (parts.length > 1) {
|
|
699
529
|
const namespace = parts[0].toLowerCase();
|
|
700
530
|
const command = parts.slice(1).join("-").toLowerCase();
|
|
701
531
|
return `${namespace}:${command}`;
|
|
702
532
|
}
|
|
703
533
|
|
|
704
|
-
// Single word, just return lowercase
|
|
705
534
|
return name.toLowerCase();
|
|
706
535
|
}
|
|
707
536
|
|
|
@@ -710,22 +539,6 @@ function camelToDasherize(camelCaseString) {
|
|
|
710
539
|
}
|
|
711
540
|
|
|
712
541
|
|
|
713
|
-
function parseDocBlock(funcName, code) {
|
|
714
|
-
const regex = new RegExp(
|
|
715
|
-
`\\/\\*\\*([\\s\\S]*?)\\*\\/\\s*export\\s+function\\s+${funcName}\\s*\\(`,
|
|
716
|
-
);
|
|
717
|
-
const match = code.match(regex);
|
|
718
|
-
|
|
719
|
-
if (match && match[1]) {
|
|
720
|
-
return match[1]
|
|
721
|
-
.replace(/^\s*\*\s*/gm, "")
|
|
722
|
-
.split("\n")[0]
|
|
723
|
-
.trim();
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
return null;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
542
|
function exportFn(commands) {
|
|
730
543
|
if (!existsSync(BUNOSHFILE)) {
|
|
731
544
|
console.error(`${BUNOSHFILE} file not found, can\'t export its commands.`);
|
|
@@ -773,19 +586,17 @@ function extractCommentForFunction(ast, source, fnName) {
|
|
|
773
586
|
|
|
774
587
|
const functionStartLine = path.node.loc.start.line;
|
|
775
588
|
|
|
776
|
-
// Find the JSDoc comment that immediately precedes this function
|
|
777
589
|
if (ast.comments) {
|
|
778
590
|
for (const astComment of ast.comments) {
|
|
779
591
|
if (astComment.type === 'CommentBlock' && astComment.value.startsWith('*')) {
|
|
780
592
|
const commentEndLine = astComment.loc.end.line;
|
|
781
593
|
|
|
782
|
-
// Check if this comment is immediately before the function
|
|
783
594
|
if (commentEndLine === functionStartLine - 1) {
|
|
784
595
|
comment = astComment.value
|
|
785
|
-
.replace(/^\s*\*\s*/gm, '')
|
|
786
|
-
.replace(/^\s*@.*$/gm, '')
|
|
787
|
-
.replace(/\n\s*\n/g, '\n')
|
|
788
|
-
.replace(/^\*\s*/, '')
|
|
596
|
+
.replace(/^\s*\*\s*/gm, '')
|
|
597
|
+
.replace(/^\s*@.*$/gm, '')
|
|
598
|
+
.replace(/\n\s*\n/g, '\n')
|
|
599
|
+
.replace(/^\*\s*/, '')
|
|
789
600
|
.trim();
|
|
790
601
|
break;
|
|
791
602
|
}
|
|
@@ -793,7 +604,6 @@ function extractCommentForFunction(ast, source, fnName) {
|
|
|
793
604
|
}
|
|
794
605
|
}
|
|
795
606
|
|
|
796
|
-
// If no JSDoc comment found, check for comments attached to the first statement in the function body
|
|
797
607
|
if (!comment) {
|
|
798
608
|
const firstStatement = path.node?.body?.body?.[0];
|
|
799
609
|
const statementLeadingComments = firstStatement?.leadingComments;
|
|
@@ -813,7 +623,7 @@ function parseArgs(ast, fnName) {
|
|
|
813
623
|
|
|
814
624
|
traverse(ast, {
|
|
815
625
|
FunctionDeclaration(path) {
|
|
816
|
-
if (path.node.id
|
|
626
|
+
if (path.node.id?.name !== fnName) return;
|
|
817
627
|
|
|
818
628
|
const params = path.node.params
|
|
819
629
|
.filter((node) => {
|
|
@@ -840,7 +650,7 @@ function parseOpts(ast, fnName) {
|
|
|
840
650
|
|
|
841
651
|
traverse(ast, {
|
|
842
652
|
FunctionDeclaration(path) {
|
|
843
|
-
if (path.node.id
|
|
653
|
+
if (path.node.id?.name !== fnName) return;
|
|
844
654
|
|
|
845
655
|
const node = path.node.params.pop();
|
|
846
656
|
if (!node) return;
|
|
@@ -877,9 +687,6 @@ function parseOpts(ast, fnName) {
|
|
|
877
687
|
return functionOpts;
|
|
878
688
|
}
|
|
879
689
|
|
|
880
|
-
/**
|
|
881
|
-
* Represents a parsed Bunosh command with all its metadata
|
|
882
|
-
*/
|
|
883
690
|
export class BunoshCommand {
|
|
884
691
|
constructor(name, namespace, args, opts, comment, fn) {
|
|
885
692
|
this.name = name;
|
|
@@ -890,16 +697,10 @@ export class BunoshCommand {
|
|
|
890
697
|
this.function = fn;
|
|
891
698
|
}
|
|
892
699
|
|
|
893
|
-
/**
|
|
894
|
-
* Get the full command name (namespace:name if namespace exists)
|
|
895
|
-
*/
|
|
896
700
|
get fullName() {
|
|
897
701
|
return this.namespace ? `${this.namespace}:${this.name}` : this.name;
|
|
898
702
|
}
|
|
899
703
|
|
|
900
|
-
/**
|
|
901
|
-
* Get the command name in kebab-case for CLI usage
|
|
902
|
-
*/
|
|
903
704
|
get cliName() {
|
|
904
705
|
if (this.namespace) {
|
|
905
706
|
return `${this.namespace}:${camelToDasherize(this.name)}`;
|
|
@@ -907,32 +708,18 @@ export class BunoshCommand {
|
|
|
907
708
|
return camelToDasherize(this.name);
|
|
908
709
|
}
|
|
909
710
|
|
|
910
|
-
/**
|
|
911
|
-
* Get all parameter names (args + opts)
|
|
912
|
-
*/
|
|
913
711
|
get allParams() {
|
|
914
712
|
return [...Object.keys(this.args), ...Object.keys(this.opts)];
|
|
915
713
|
}
|
|
916
714
|
|
|
917
|
-
/**
|
|
918
|
-
* Get required parameter names
|
|
919
|
-
*/
|
|
920
715
|
get requiredParams() {
|
|
921
716
|
return Object.keys(this.args).filter(arg => this.args[arg] === undefined);
|
|
922
717
|
}
|
|
923
718
|
}
|
|
924
719
|
|
|
925
|
-
/**
|
|
926
|
-
* Process commands and sources to extract structured command information
|
|
927
|
-
* This reuses all the existing parsing logic from the main bunosh function
|
|
928
|
-
* @param {Object} commands - Commands object from Bunoshfile
|
|
929
|
-
* @param {Object} sources - Sources object containing comments and metadata
|
|
930
|
-
* @returns {Array<BunoshCommand>} Array of parsed BunoshCommand objects
|
|
931
|
-
*/
|
|
932
720
|
export function processCommands(commands, sources) {
|
|
933
721
|
const parsedCommands = [];
|
|
934
722
|
|
|
935
|
-
// Parse AST and comments for each source (same as in main bunosh function)
|
|
936
723
|
const comments = {};
|
|
937
724
|
const namespaceSources = {};
|
|
938
725
|
|
|
@@ -947,7 +734,6 @@ export function processCommands(commands, sources) {
|
|
|
947
734
|
attachComment: true,
|
|
948
735
|
});
|
|
949
736
|
|
|
950
|
-
// Store AST for this command
|
|
951
737
|
if (!namespaceSources[cmdInfo.namespace || '']) {
|
|
952
738
|
namespaceSources[cmdInfo.namespace || ''] = {
|
|
953
739
|
ast: ast,
|
|
@@ -955,7 +741,6 @@ export function processCommands(commands, sources) {
|
|
|
955
741
|
};
|
|
956
742
|
}
|
|
957
743
|
|
|
958
|
-
// Extract comments for this command
|
|
959
744
|
const fnName = cmdInfo.namespace ? cmdName.split(':')[1] : cmdName;
|
|
960
745
|
if (fnName) {
|
|
961
746
|
comments[cmdName] = extractCommentForFunction(ast, cmdInfo.source, fnName);
|
|
@@ -967,7 +752,6 @@ export function processCommands(commands, sources) {
|
|
|
967
752
|
}
|
|
968
753
|
}
|
|
969
754
|
|
|
970
|
-
// Process each command using the same logic as the main bunosh function
|
|
971
755
|
Object.entries(commands).forEach(([cmdName, cmdFn]) => {
|
|
972
756
|
const sourceInfo = sources[cmdName];
|
|
973
757
|
const originalFnName = sourceInfo?.originalFnName || cmdName.split(':')[1] || cmdName;
|
|
@@ -975,13 +759,11 @@ export function processCommands(commands, sources) {
|
|
|
975
759
|
const namespaceSource = namespaceSources[namespace];
|
|
976
760
|
const comment = comments[cmdName];
|
|
977
761
|
|
|
978
|
-
// Parse function using the same logic as the main bunosh function
|
|
979
762
|
const fnBody = cmdFn.toString();
|
|
980
763
|
const ast = namespaceSource?.ast || babelParser.parse(fnBody, { comment: true, tokens: true });
|
|
981
764
|
const args = parseArgs(ast, originalFnName);
|
|
982
765
|
const opts = parseOpts(ast, originalFnName);
|
|
983
766
|
|
|
984
|
-
// Extract the actual command name without namespace
|
|
985
767
|
const commandName = originalFnName;
|
|
986
768
|
|
|
987
769
|
parsedCommands.push(new BunoshCommand(
|
|
@@ -997,5 +779,4 @@ export function processCommands(commands, sources) {
|
|
|
997
779
|
return parsedCommands;
|
|
998
780
|
}
|
|
999
781
|
|
|
1000
|
-
// Export parsing functions for use in MCP server
|
|
1001
782
|
export { parseArgs, parseOpts, extractCommentForFunction };
|