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