bunosh 0.4.14 → 0.5.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 +92 -565
- package/bunosh.js +32 -193
- package/index.js +4 -3
- package/package.json +18 -2
- package/src/error-formatter.js +80 -0
- package/src/formatters/console.js +5 -1
- package/src/io.js +0 -5
- package/src/printer.js +29 -9
- package/src/program.js +131 -343
- package/src/task.js +8 -1
- package/src/tasks/exec.js +4 -248
- package/src/tasks/fetch.js +2 -1
- package/src/tasks/shell.js +194 -119
- package/src/upgrade.js +135 -30
- package/src/mcp-server.js +0 -575
package/src/program.js
CHANGED
|
@@ -5,9 +5,10 @@ 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
|
-
import { upgradeCommand } from './upgrade.js';
|
|
11
|
+
import { upgradeCommand, printUpgradeNoticeIfAvailable } from './upgrade.js';
|
|
11
12
|
|
|
12
13
|
export const BUNOSHFILE = `Bunoshfile.js`;
|
|
13
14
|
|
|
@@ -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,226 +160,139 @@ 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
|
-
|
|
292
|
-
const isBun = typeof Bun !== 'undefined';
|
|
293
|
-
const runtime = isBun ? 'Bun' : 'Node.js';
|
|
294
|
-
const runtimeColor = isBun ? color.red : color.green;
|
|
295
|
-
|
|
296
|
-
let runtimeVersion;
|
|
297
|
-
if (isBun) {
|
|
298
|
-
runtimeVersion = Bun.version;
|
|
299
|
-
} else {
|
|
300
|
-
runtimeVersion = process.version;
|
|
301
|
-
}
|
|
302
201
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
202
|
+
console.log(color.gray(`Runtime: `, runtimeColor.bold(runtime), color.gray(` (${runtimeVersion})`)));
|
|
203
|
+
console.log();
|
|
204
|
+
})
|
|
306
205
|
|
|
307
|
-
|
|
206
|
+
let argsAndOptsDescription = [];
|
|
308
207
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
268
|
+
const optNames = Object.keys(opts);
|
|
269
|
+
if (optNames.length > 0) {
|
|
270
|
+
const lastArg = commanderArgs[commanderArgs.length - 1];
|
|
271
|
+
const optionsObj = (lastArg && typeof lastArg.opts === 'function') ? lastArg.opts() : lastArg;
|
|
272
|
+
const mergedOpts = {};
|
|
273
|
+
optNames.forEach((optName) => {
|
|
274
|
+
const camelName = optName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
275
|
+
if (optionsObj && optionsObj[camelName] !== undefined) {
|
|
276
|
+
mergedOpts[camelName] = optionsObj[camelName];
|
|
277
|
+
} else if (optionsObj && optionsObj[optName] !== undefined) {
|
|
278
|
+
mergedOpts[camelName] = optionsObj[optName];
|
|
381
279
|
} else {
|
|
382
|
-
|
|
383
|
-
transformedArgs.push(opts[optName]);
|
|
280
|
+
mergedOpts[camelName] = opts[optName];
|
|
384
281
|
}
|
|
385
282
|
});
|
|
283
|
+
transformedArgs.push(mergedOpts);
|
|
386
284
|
}
|
|
387
285
|
|
|
388
|
-
// Call the original function with transformed arguments
|
|
389
286
|
try {
|
|
390
287
|
return await commandFn(...transformedArgs);
|
|
391
288
|
} catch (error) {
|
|
392
|
-
|
|
393
|
-
console.error(`\n❌ Error: ${error.message}`);
|
|
394
|
-
if (error.stack && process.env.BUNOSH_DEBUG) {
|
|
395
|
-
console.error(error.stack);
|
|
396
|
-
}
|
|
289
|
+
console.error('\n' + formatError(error));
|
|
397
290
|
process.exit(1);
|
|
398
291
|
}
|
|
399
292
|
};
|
|
400
293
|
}
|
|
401
294
|
|
|
402
|
-
|
|
295
|
+
|
|
403
296
|
const editCmd = program.command('edit')
|
|
404
297
|
.description('Open the bunosh file in your editor. $EDITOR or \'code\' is used.')
|
|
405
298
|
.action(async () => {
|
|
@@ -444,7 +337,6 @@ export default async function bunosh(commands, sources) {
|
|
|
444
337
|
.option('-s, --shell <shell>', 'Specify shell instead of auto-detection (bash, zsh, fish)')
|
|
445
338
|
.action((options) => {
|
|
446
339
|
try {
|
|
447
|
-
// Detect current shell or use specified shell
|
|
448
340
|
const shell = options.shell || detectCurrentShell();
|
|
449
341
|
|
|
450
342
|
if (!shell) {
|
|
@@ -458,10 +350,8 @@ export default async function bunosh(commands, sources) {
|
|
|
458
350
|
console.log(`🐚 Detected shell: ${color.bold(shell)}`);
|
|
459
351
|
console.log();
|
|
460
352
|
|
|
461
|
-
// Get paths for this shell
|
|
462
353
|
const paths = getCompletionPaths(shell);
|
|
463
354
|
|
|
464
|
-
// Check if already installed
|
|
465
355
|
if (!options.force && existsSync(paths.completionFile)) {
|
|
466
356
|
console.log(`⚠️ Completion already installed at: ${paths.completionFile}`);
|
|
467
357
|
console.log(' Use --force to overwrite, or run:');
|
|
@@ -469,11 +359,9 @@ export default async function bunosh(commands, sources) {
|
|
|
469
359
|
process.exit(0);
|
|
470
360
|
}
|
|
471
361
|
|
|
472
|
-
// Install completion
|
|
473
362
|
console.log('🔧 Installing completion...');
|
|
474
363
|
const result = installCompletion(shell);
|
|
475
364
|
|
|
476
|
-
// Report success
|
|
477
365
|
console.log(`✅ Completion installed: ${color.green(paths.completionFile)}`);
|
|
478
366
|
|
|
479
367
|
if (result.configFile && result.added) {
|
|
@@ -503,6 +391,23 @@ export default async function bunosh(commands, sources) {
|
|
|
503
391
|
|
|
504
392
|
internalCommands.push(setupCompletionCmd);
|
|
505
393
|
|
|
394
|
+
const SKILLS_REPO = 'DavertMik/bunosh-skills';
|
|
395
|
+
|
|
396
|
+
const installSkillsCmd = program.command('install-skills')
|
|
397
|
+
.description('Print the command to install Bunosh AI agent skills.')
|
|
398
|
+
.action(() => {
|
|
399
|
+
console.log();
|
|
400
|
+
console.log(`🤖 Install Bunosh AI agent skills (Claude Code, Cursor, Codex, ...):`);
|
|
401
|
+
console.log();
|
|
402
|
+
console.log(` ${color.bold(`npx skills add ${SKILLS_REPO}`)}`);
|
|
403
|
+
console.log();
|
|
404
|
+
console.log(color.dim(` Skills: bunosh-fundamentals, migrate-to-bunosh`));
|
|
405
|
+
console.log(color.dim(` ${SKILLS_REPO} · https://buno.sh`));
|
|
406
|
+
console.log();
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
internalCommands.push(installSkillsCmd);
|
|
410
|
+
|
|
506
411
|
const upgradeCmd = program.command('upgrade')
|
|
507
412
|
.description('Upgrade bunosh to the latest version')
|
|
508
413
|
.option('-f, --force', 'Force upgrade even if already on latest version')
|
|
@@ -514,10 +419,8 @@ export default async function bunosh(commands, sources) {
|
|
|
514
419
|
internalCommands.push(upgradeCmd);
|
|
515
420
|
|
|
516
421
|
|
|
517
|
-
// Add organized command help sections
|
|
518
422
|
let helpText = '';
|
|
519
423
|
|
|
520
|
-
// Main Commands (no namespace)
|
|
521
424
|
if (commandsByNamespace[''].length > 0) {
|
|
522
425
|
const mainCommands = commandsByNamespace[''].map(cmd => {
|
|
523
426
|
const processedName = cmd.type === 'bunosh' ? toKebabCase(cmd.name) : cmd.name;
|
|
@@ -529,9 +432,8 @@ export default async function bunosh(commands, sources) {
|
|
|
529
432
|
return ` ${color.white.bold(paddedName)}`;
|
|
530
433
|
}
|
|
531
434
|
|
|
532
|
-
// Handle multi-line descriptions with proper indentation
|
|
533
435
|
const lines = description.split('\n');
|
|
534
|
-
const firstLine = ` ${color.white.bold(paddedName)} ${lines[0]}`;
|
|
436
|
+
const firstLine = ` ${color.white.bold(paddedName)} ${color.dim(lines[0])}`;
|
|
535
437
|
const indentedLines = lines.slice(1).map(line =>
|
|
536
438
|
line.trim() ? ` ${line}` : ''
|
|
537
439
|
).filter(line => line);
|
|
@@ -544,24 +446,19 @@ ${mainCommands}
|
|
|
544
446
|
`;
|
|
545
447
|
}
|
|
546
448
|
|
|
547
|
-
// Dev Commands (dev namespace)
|
|
548
449
|
if (commandsByNamespace.dev.length > 0) {
|
|
549
450
|
const devCommands = commandsByNamespace.dev.map(cmd => {
|
|
550
451
|
let processedName;
|
|
551
452
|
if (cmd.type === 'namespace') {
|
|
552
|
-
// For namespaced commands, handle the name properly
|
|
553
453
|
if (cmd.name.includes(':')) {
|
|
554
|
-
// If cmd.name already has namespace (like 'dev:devFn'), only process the part after the colon
|
|
555
454
|
const [namespace, functionName] = cmd.name.split(':');
|
|
556
455
|
processedName = `${namespace}:${toKebabCase(functionName).replace(/^[^:]+:/, '')}`;
|
|
557
456
|
} else {
|
|
558
|
-
// If cmd.name doesn't have namespace, use toKebabCase which will add it
|
|
559
457
|
processedName = toKebabCase(cmd.name);
|
|
560
458
|
}
|
|
561
459
|
} else {
|
|
562
460
|
processedName = cmd.name;
|
|
563
461
|
}
|
|
564
|
-
// Debug removed
|
|
565
462
|
const cmdObj = program.commands.find(c => c.name() === processedName);
|
|
566
463
|
const description = cmdObj ? cmdObj.description() : '';
|
|
567
464
|
const paddedName = processedName.padEnd(22);
|
|
@@ -570,9 +467,8 @@ ${mainCommands}
|
|
|
570
467
|
return ` ${color.white.bold(paddedName)}`;
|
|
571
468
|
}
|
|
572
469
|
|
|
573
|
-
// Handle multi-line descriptions with proper indentation
|
|
574
470
|
const lines = description.split('\n');
|
|
575
|
-
const firstLine = ` ${color.white.bold(paddedName)} ${lines[0]}`;
|
|
471
|
+
const firstLine = ` ${color.white.bold(paddedName)} ${color.dim(lines[0])}`;
|
|
576
472
|
const indentedLines = lines.slice(1).map(line =>
|
|
577
473
|
line.trim() ? ` ${line}` : ''
|
|
578
474
|
).filter(line => line);
|
|
@@ -585,7 +481,6 @@ ${devCommands}
|
|
|
585
481
|
`;
|
|
586
482
|
}
|
|
587
483
|
|
|
588
|
-
// Add other namespace sections dynamically
|
|
589
484
|
Object.keys(commandsByNamespace).forEach(namespace => {
|
|
590
485
|
if (namespace && namespace !== 'dev' && commandsByNamespace[namespace].length > 0) {
|
|
591
486
|
const namespaceName = namespace.charAt(0).toUpperCase() + namespace.slice(1) + ' Commands';
|
|
@@ -598,9 +493,8 @@ ${devCommands}
|
|
|
598
493
|
return ` ${color.white.bold(paddedName)}`;
|
|
599
494
|
}
|
|
600
495
|
|
|
601
|
-
// Handle multi-line descriptions with proper indentation
|
|
602
496
|
const lines = description.split('\n');
|
|
603
|
-
const firstLine = ` ${color.white.bold(paddedName)} ${lines[0]}`;
|
|
497
|
+
const firstLine = ` ${color.white.bold(paddedName)} ${color.dim(lines[0])}`;
|
|
604
498
|
const indentedLines = lines.slice(1).map(line =>
|
|
605
499
|
line.trim() ? ` ${line}` : ''
|
|
606
500
|
).filter(line => line);
|
|
@@ -614,9 +508,10 @@ ${namespaceCommands}
|
|
|
614
508
|
}
|
|
615
509
|
});
|
|
616
510
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
511
|
+
const helpFlagRequested = process.argv.includes('--help') || process.argv.includes('-h');
|
|
512
|
+
|
|
513
|
+
if (helpFlagRequested) {
|
|
514
|
+
helpText += color.dim(`Special Commands:
|
|
620
515
|
${color.bold('bunosh edit')} 📝 Edit bunosh file with $EDITOR
|
|
621
516
|
${color.bold('bunosh export:scripts')} 📥 Export commands to package.json
|
|
622
517
|
${color.bold('bunosh upgrade')} 🦾 Upgrade bunosh
|
|
@@ -625,6 +520,13 @@ ${namespaceCommands}
|
|
|
625
520
|
${color.bold('bunosh --env-file …')} 🔧 Load custom environment file
|
|
626
521
|
`);
|
|
627
522
|
|
|
523
|
+
helpText += `
|
|
524
|
+
${color.bold('🤖 AI agent skills')} ${color.dim('(Claude Code, Cursor, Codex, ...)')}
|
|
525
|
+
${color.bold('npx skills add DavertMik/bunosh-skills')}
|
|
526
|
+
${color.dim('bunosh-fundamentals · migrate-to-bunosh — see "bunosh install-skills"')}
|
|
527
|
+
`;
|
|
528
|
+
}
|
|
529
|
+
|
|
628
530
|
program.addHelpText('after', helpText);
|
|
629
531
|
|
|
630
532
|
program.on("command:*", (cmd) => {
|
|
@@ -633,80 +535,17 @@ ${namespaceCommands}
|
|
|
633
535
|
process.exit(1);
|
|
634
536
|
});
|
|
635
537
|
|
|
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
538
|
|
|
657
|
-
// Show help if no command provided
|
|
658
539
|
if (process.argv.length === 2) {
|
|
659
540
|
program.outputHelp();
|
|
541
|
+
await printUpgradeNoticeIfAvailable();
|
|
660
542
|
return program;
|
|
661
543
|
}
|
|
662
544
|
|
|
663
545
|
program.parse(process.argv);
|
|
664
546
|
}
|
|
665
547
|
|
|
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
548
|
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
549
|
const lastColonIndex = name.lastIndexOf(':');
|
|
711
550
|
if (lastColonIndex !== -1) {
|
|
712
551
|
const namespace = name.substring(0, lastColonIndex);
|
|
@@ -714,21 +553,18 @@ function prepareCommandName(name) {
|
|
|
714
553
|
return `${namespace}:${toKebabCase(commandPart)}`;
|
|
715
554
|
}
|
|
716
555
|
|
|
717
|
-
// For non-namespaced commands, just convert to kebab-case
|
|
718
556
|
return toKebabCase(name);
|
|
719
557
|
}
|
|
720
558
|
|
|
721
559
|
function toKebabCase(name) {
|
|
722
560
|
const parts = name.split(/(?=[A-Z])/);
|
|
723
561
|
|
|
724
|
-
// If there are multiple parts, treat first part as namespace with colon
|
|
725
562
|
if (parts.length > 1) {
|
|
726
563
|
const namespace = parts[0].toLowerCase();
|
|
727
564
|
const command = parts.slice(1).join("-").toLowerCase();
|
|
728
565
|
return `${namespace}:${command}`;
|
|
729
566
|
}
|
|
730
567
|
|
|
731
|
-
// Single word, just return lowercase
|
|
732
568
|
return name.toLowerCase();
|
|
733
569
|
}
|
|
734
570
|
|
|
@@ -737,22 +573,6 @@ function camelToDasherize(camelCaseString) {
|
|
|
737
573
|
}
|
|
738
574
|
|
|
739
575
|
|
|
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
576
|
function exportFn(commands) {
|
|
757
577
|
if (!existsSync(BUNOSHFILE)) {
|
|
758
578
|
console.error(`${BUNOSHFILE} file not found, can\'t export its commands.`);
|
|
@@ -800,19 +620,17 @@ function extractCommentForFunction(ast, source, fnName) {
|
|
|
800
620
|
|
|
801
621
|
const functionStartLine = path.node.loc.start.line;
|
|
802
622
|
|
|
803
|
-
// Find the JSDoc comment that immediately precedes this function
|
|
804
623
|
if (ast.comments) {
|
|
805
624
|
for (const astComment of ast.comments) {
|
|
806
625
|
if (astComment.type === 'CommentBlock' && astComment.value.startsWith('*')) {
|
|
807
626
|
const commentEndLine = astComment.loc.end.line;
|
|
808
627
|
|
|
809
|
-
// Check if this comment is immediately before the function
|
|
810
628
|
if (commentEndLine === functionStartLine - 1) {
|
|
811
629
|
comment = astComment.value
|
|
812
|
-
.replace(/^\s*\*\s*/gm, '')
|
|
813
|
-
.replace(/^\s*@.*$/gm, '')
|
|
814
|
-
.replace(/\n\s*\n/g, '\n')
|
|
815
|
-
.replace(/^\*\s*/, '')
|
|
630
|
+
.replace(/^\s*\*\s*/gm, '')
|
|
631
|
+
.replace(/^\s*@.*$/gm, '')
|
|
632
|
+
.replace(/\n\s*\n/g, '\n')
|
|
633
|
+
.replace(/^\*\s*/, '')
|
|
816
634
|
.trim();
|
|
817
635
|
break;
|
|
818
636
|
}
|
|
@@ -820,7 +638,6 @@ function extractCommentForFunction(ast, source, fnName) {
|
|
|
820
638
|
}
|
|
821
639
|
}
|
|
822
640
|
|
|
823
|
-
// If no JSDoc comment found, check for comments attached to the first statement in the function body
|
|
824
641
|
if (!comment) {
|
|
825
642
|
const firstStatement = path.node?.body?.body?.[0];
|
|
826
643
|
const statementLeadingComments = firstStatement?.leadingComments;
|
|
@@ -840,7 +657,7 @@ function parseArgs(ast, fnName) {
|
|
|
840
657
|
|
|
841
658
|
traverse(ast, {
|
|
842
659
|
FunctionDeclaration(path) {
|
|
843
|
-
if (path.node.id
|
|
660
|
+
if (path.node.id?.name !== fnName) return;
|
|
844
661
|
|
|
845
662
|
const params = path.node.params
|
|
846
663
|
.filter((node) => {
|
|
@@ -867,7 +684,7 @@ function parseOpts(ast, fnName) {
|
|
|
867
684
|
|
|
868
685
|
traverse(ast, {
|
|
869
686
|
FunctionDeclaration(path) {
|
|
870
|
-
if (path.node.id
|
|
687
|
+
if (path.node.id?.name !== fnName) return;
|
|
871
688
|
|
|
872
689
|
const node = path.node.params.pop();
|
|
873
690
|
if (!node) return;
|
|
@@ -904,9 +721,6 @@ function parseOpts(ast, fnName) {
|
|
|
904
721
|
return functionOpts;
|
|
905
722
|
}
|
|
906
723
|
|
|
907
|
-
/**
|
|
908
|
-
* Represents a parsed Bunosh command with all its metadata
|
|
909
|
-
*/
|
|
910
724
|
export class BunoshCommand {
|
|
911
725
|
constructor(name, namespace, args, opts, comment, fn) {
|
|
912
726
|
this.name = name;
|
|
@@ -917,16 +731,10 @@ export class BunoshCommand {
|
|
|
917
731
|
this.function = fn;
|
|
918
732
|
}
|
|
919
733
|
|
|
920
|
-
/**
|
|
921
|
-
* Get the full command name (namespace:name if namespace exists)
|
|
922
|
-
*/
|
|
923
734
|
get fullName() {
|
|
924
735
|
return this.namespace ? `${this.namespace}:${this.name}` : this.name;
|
|
925
736
|
}
|
|
926
737
|
|
|
927
|
-
/**
|
|
928
|
-
* Get the command name in kebab-case for CLI usage
|
|
929
|
-
*/
|
|
930
738
|
get cliName() {
|
|
931
739
|
if (this.namespace) {
|
|
932
740
|
return `${this.namespace}:${camelToDasherize(this.name)}`;
|
|
@@ -934,32 +742,18 @@ export class BunoshCommand {
|
|
|
934
742
|
return camelToDasherize(this.name);
|
|
935
743
|
}
|
|
936
744
|
|
|
937
|
-
/**
|
|
938
|
-
* Get all parameter names (args + opts)
|
|
939
|
-
*/
|
|
940
745
|
get allParams() {
|
|
941
746
|
return [...Object.keys(this.args), ...Object.keys(this.opts)];
|
|
942
747
|
}
|
|
943
748
|
|
|
944
|
-
/**
|
|
945
|
-
* Get required parameter names
|
|
946
|
-
*/
|
|
947
749
|
get requiredParams() {
|
|
948
750
|
return Object.keys(this.args).filter(arg => this.args[arg] === undefined);
|
|
949
751
|
}
|
|
950
752
|
}
|
|
951
753
|
|
|
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
754
|
export function processCommands(commands, sources) {
|
|
960
755
|
const parsedCommands = [];
|
|
961
756
|
|
|
962
|
-
// Parse AST and comments for each source (same as in main bunosh function)
|
|
963
757
|
const comments = {};
|
|
964
758
|
const namespaceSources = {};
|
|
965
759
|
|
|
@@ -974,7 +768,6 @@ export function processCommands(commands, sources) {
|
|
|
974
768
|
attachComment: true,
|
|
975
769
|
});
|
|
976
770
|
|
|
977
|
-
// Store AST for this command
|
|
978
771
|
if (!namespaceSources[cmdInfo.namespace || '']) {
|
|
979
772
|
namespaceSources[cmdInfo.namespace || ''] = {
|
|
980
773
|
ast: ast,
|
|
@@ -982,7 +775,6 @@ export function processCommands(commands, sources) {
|
|
|
982
775
|
};
|
|
983
776
|
}
|
|
984
777
|
|
|
985
|
-
// Extract comments for this command
|
|
986
778
|
const fnName = cmdInfo.namespace ? cmdName.split(':')[1] : cmdName;
|
|
987
779
|
if (fnName) {
|
|
988
780
|
comments[cmdName] = extractCommentForFunction(ast, cmdInfo.source, fnName);
|
|
@@ -994,7 +786,6 @@ export function processCommands(commands, sources) {
|
|
|
994
786
|
}
|
|
995
787
|
}
|
|
996
788
|
|
|
997
|
-
// Process each command using the same logic as the main bunosh function
|
|
998
789
|
Object.entries(commands).forEach(([cmdName, cmdFn]) => {
|
|
999
790
|
const sourceInfo = sources[cmdName];
|
|
1000
791
|
const originalFnName = sourceInfo?.originalFnName || cmdName.split(':')[1] || cmdName;
|
|
@@ -1002,13 +793,11 @@ export function processCommands(commands, sources) {
|
|
|
1002
793
|
const namespaceSource = namespaceSources[namespace];
|
|
1003
794
|
const comment = comments[cmdName];
|
|
1004
795
|
|
|
1005
|
-
// Parse function using the same logic as the main bunosh function
|
|
1006
796
|
const fnBody = cmdFn.toString();
|
|
1007
797
|
const ast = namespaceSource?.ast || babelParser.parse(fnBody, { comment: true, tokens: true });
|
|
1008
798
|
const args = parseArgs(ast, originalFnName);
|
|
1009
799
|
const opts = parseOpts(ast, originalFnName);
|
|
1010
800
|
|
|
1011
|
-
// Extract the actual command name without namespace
|
|
1012
801
|
const commandName = originalFnName;
|
|
1013
802
|
|
|
1014
803
|
parsedCommands.push(new BunoshCommand(
|
|
@@ -1024,5 +813,4 @@ export function processCommands(commands, sources) {
|
|
|
1024
813
|
return parsedCommands;
|
|
1025
814
|
}
|
|
1026
815
|
|
|
1027
|
-
// Export parsing functions for use in MCP server
|
|
1028
816
|
export { parseArgs, parseOpts, extractCommentForFunction };
|