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/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
- '': [], // 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,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
- 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
-
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
- console.log(color.gray(`Runtime: `, runtimeColor.bold(runtime), color.gray(` (${runtimeVersion})`)));
304
- console.log();
305
- })
202
+ console.log(color.gray(`Runtime: `, runtimeColor.bold(runtime), color.gray(` (${runtimeVersion})`)));
203
+ console.log();
204
+ })
306
205
 
307
- let argsAndOptsDescription = [];
206
+ let argsAndOptsDescription = [];
308
207
 
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
- const optionsObj = commanderArgs[commanderArgs.length - 1];
376
- if (optionsObj && typeof optionsObj === 'object') {
377
- Object.keys(opts).forEach((optName) => {
378
- const dasherizedOpt = optName.replace(/([A-Z])/g, '-$1').toLowerCase();
379
- if (optionsObj[dasherizedOpt] !== undefined) {
380
- transformedArgs.push(optionsObj[dasherizedOpt]);
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
- // Use default value
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
- // 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
- }
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
- // Special Commands
619
- helpText += color.dim(`Special Commands:
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, '') // 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 *
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.name !== fnName) return;
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.name !== fnName) return;
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 };