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/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
- '': [], // Main commands (no namespace)
166
- 'dev': [], // Dev commands
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
- if (cmdData.type === 'bunosh' || (cmdData.type === 'namespace' && !cmdData.namespace)) {
186
- // Handle main bunosh commands (no namespace)
187
- const fnName = cmdData.name;
188
- const fnBody = commands[fnName].toString();
189
- const sourceInfo = sources[fnName];
190
- const namespaceSource = namespaceSources[''];
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
- let description = comment?.split('\n')[0] || '';
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
- if (comment && argsAndOptsDescription.length) description += `\n ▹ ${color.gray(`bunosh ${commandName}`)} ${color.blue(argsAndOptsDescription.join(' ').trim())}`;
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
- command.description(description);
191
+ const isBun = typeof Bun !== 'undefined';
192
+ const runtime = isBun ? 'Bun' : 'Node.js';
193
+ const runtimeColor = isBun ? color.red : color.green;
254
194
 
255
- // Custom error handling for missing arguments - show command help without banner
256
- command.configureHelp({
257
- commandDescription: () => comment || '',
258
- commandUsage: cmd => `bunosh ${cmd.name()}${argsAndOptsDescription.length ? ' ' + argsAndOptsDescription.join(' ').trim() : ''}`,
259
- showGlobalOptions: false,
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
- const isBun = typeof Bun !== 'undefined';
293
- const runtime = isBun ? 'Bun' : 'Node.js';
294
- const runtimeColor = isBun ? color.red : color.green;
202
+ console.log(color.gray(`Runtime: `, runtimeColor.bold(runtime), color.gray(` (${runtimeVersion})`)));
203
+ console.log();
204
+ })
295
205
 
296
- let runtimeVersion;
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
- let argsAndOptsDescription = [];
308
-
309
- Object.entries(args).forEach(([arg, value]) => {
310
- if (value === undefined) {
311
- argsAndOptsDescription.push(`<${arg}>`);
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
- if (value === null) {
316
- argsAndOptsDescription.push(`[${arg}]`);
317
- return command.argument(`[${arg}]`, '', null);
318
- }
214
+ if (value === null) {
215
+ argsAndOptsDescription.push(`[${arg}]`);
216
+ return command.argument(`[${arg}]`, '', null);
217
+ }
319
218
 
320
- argsAndOptsDescription.push(`[${arg}=${value}]`);
321
- command.argument(`[${arg}]`, ``, value);
322
- });
219
+ argsAndOptsDescription.push(`[${arg}=${value}]`);
220
+ command.argument(`[${arg}]`, ``, value);
221
+ });
323
222
 
324
- Object.entries(opts).forEach(([opt, value]) => {
325
- if (value === false || value === null) {
326
- argsAndOptsDescription.push(`--${opt}`);
327
- return command.option(`--${opt}`);
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
- argsAndOptsDescription.push(`--${opt}=${value}`);
331
- command.option(`--${opt} [${opt}]`, "", value);
332
- });
229
+ argsAndOptsDescription.push(`--${opt}=${value}`);
230
+ command.option(`--${opt} [${opt}]`, "", value);
231
+ });
333
232
 
334
- let description = comment?.split('\n')[0] || '';
233
+ let description = comment?.split('\n')[0] || '';
335
234
 
336
- if (comment && argsAndOptsDescription.length) {
337
- description += `\n ${color.gray(`bunosh ${commandName}`)} ${color.blue(argsAndOptsDescription.join(' ').trim())}`;
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
- command.description(description);
240
+ command.description(description);
341
241
 
342
- // Custom error handling for missing arguments - show command help without banner
343
- command.configureHelp({
344
- commandDescription: () => comment || '',
345
- commandUsage: cmd => `bunosh ${cmd.name()}${argsAndOptsDescription.length ? ' ' + argsAndOptsDescription.join(' ').trim() : ''}`,
346
- showGlobalOptions: false,
347
- visibleGlobalOptions: () => [],
348
- visibleOptions: () => [],
349
- visibleCommands: () => []
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
- command.action(createCommandAction(commands[cmdData.name], args, opts));
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) { // -1 because last arg is options object
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
- // Handle errors thrown from functions properly
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, '') // Remove leading * and spaces
813
- .replace(/^\s*@.*$/gm, '') // Remove @param and other @ tags
814
- .replace(/\n\s*\n/g, '\n') // Remove excessive empty lines
815
- .replace(/^\*\s*/, '') // Remove any remaining leading *
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.name !== fnName) return;
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.name !== fnName) return;
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 };