bunosh 0.4.1 → 0.4.7

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
@@ -7,152 +7,200 @@ import { readFileSync, existsSync, writeFileSync } from 'fs';
7
7
  import { yell } from './io.js';
8
8
  import cprint from "./font.js";
9
9
  import { handleCompletion, detectCurrentShell, installCompletion, getCompletionPaths } from './completion.js';
10
- import { upgradeExecutable, isExecutable, getCurrentVersion } from './upgrade.js';
10
+ import { upgradeCommand } from './upgrade.js';
11
11
 
12
12
  export const BUNOSHFILE = `Bunoshfile.js`;
13
13
 
14
14
  export const banner = () => {
15
- const asciiArt = cprint('Bunosh', { symbol: '⯀' });
16
- console.log(createGradientAscii(asciiArt));
17
- console.log(color.gray('🍲 Your exceptional task runner'));
18
-
19
- // Try to get version from package.json
15
+ const logoArt =
16
+ ` .::=-=-___.
17
+ .:+*##*-**:___.
18
+ :**#%*-+#####*++*
19
+ :-+**++*########*+
20
+ \▒░░░░:----▒▒▒▒░/
21
+ \▒▒▒▒▒▒▒▒▒▒▒░░/
22
+ \▓▓▓▓▓▓▓▓░░░/
23
+ \▓▓▓▓▓▓░░░/ `;
24
+
25
+ console.log(createGradientAscii(logoArt));
26
+
27
+ let version = '';
20
28
  try {
21
- // First try relative to src directory
22
29
  const pkgPath = new URL('../package.json', import.meta.url);
23
30
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
24
- console.log(`Version: ${color.bold(pkg.version)}`);
31
+ version = pkg.version;
25
32
  } catch (e) {
26
- // Ignore if version can't be read
27
33
  }
34
+ console.log(color.gray('🍲 ', color.yellowBright.bold('BUNOSH'), color.yellow(version)));
35
+
28
36
  console.log();
29
37
  };
30
38
 
31
39
  function createGradientAscii(asciiArt) {
32
40
  const lines = asciiArt.split('\n');
33
- const colors = [
34
- color.bold.yellow,
35
- color.bold.green,
36
- color.bold.greenBright,
37
- color.bold.cyan,
38
- color.bold.blue
39
- ];
40
-
41
+
42
+ // Yellow RGB (255, 220, 0) to Brown RGB (139, 69, 19)
43
+ const startColor = { r: 255, g: 220, b: 0 };
44
+ const endColor = { r: 139, g: 69, b: 19 };
45
+
41
46
  return lines.map((line, index) => {
42
- // Create smooth gradient by interpolating between colors
47
+ // Block characters should always be brown
48
+ if (line.includes('░') || line.includes('▒') || line.includes('▓')) {
49
+ return `\x1b[38;2;139;69;19m${line}\x1b[0m`;
50
+ }
51
+
52
+ // Create smooth gradient for other characters
43
53
  const progress = index / (lines.length - 1);
44
- const colorIndex = progress * (colors.length - 1);
45
- const lowerIndex = Math.floor(colorIndex);
46
- const upperIndex = Math.min(lowerIndex + 1, colors.length - 1);
47
- const factor = colorIndex - lowerIndex;
48
-
49
- // For smoother transition, we'll use the closest color
50
- const color = factor < 0.5 ? colors[lowerIndex] : colors[upperIndex];
51
- return color(line);
54
+ const r = Math.round(startColor.r + (endColor.r - startColor.r) * progress);
55
+ const g = Math.round(startColor.g + (endColor.g - startColor.g) * progress);
56
+ const b = Math.round(startColor.b + (endColor.b - startColor.b) * progress);
57
+
58
+ // Use true color escape sequence
59
+ return `\x1b[38;2;${r};${g};${b}m${line}\x1b[0m`;
52
60
  }).join('\n');
53
61
  }
54
62
 
55
- export default async function bunosh(commands, source) {
63
+ export default async function bunosh(commands, sources) {
56
64
  const program = new Command();
57
65
  program.option('--bunoshfile <path>', 'Path to the Bunoshfile');
66
+ program.option('--env-file <path>', 'Path to environment file');
58
67
 
59
68
  const internalCommands = [];
60
69
 
61
- // Load npm scripts from package.json
62
- const npmScripts = loadNpmScripts();
63
-
64
- // Load personal commands from $HOME/Bunoshfile.js
65
- const { tasks: homeTasks, source: homeSource } = await loadHomeTasks();
66
-
70
+
67
71
  program.configureHelp({
68
72
  commandDescription: _cmd => {
69
73
  // Show banner and description
70
74
  banner();
71
-
72
- // Try to get version from current directory's package.json for help display
73
- try {
74
- if (existsSync('package.json')) {
75
- const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
76
- console.log(`Version: ${color.bold(pkg.version)}`);
77
- }
78
- } catch {
79
- // Ignore if version can't be read
80
- }
81
-
82
- return ` Commands are loaded from exported functions in ${color.bold(BUNOSHFILE)}`;
75
+ return ' ';
83
76
  },
84
- commandUsage: usg => 'bunosh [-e <code>] <command> <args> [options]',
77
+ commandUsage: usg => 'bunosh <command> <args> [options]',
85
78
  showGlobalOptions: false,
86
79
  visibleGlobalOptions: _opt => [],
87
80
  visibleOptions: _opt => [],
88
81
  visibleCommands: cmd => {
89
- const commands = cmd.commands.filter(c => !internalCommands.includes(c));
90
- return commands.filter(c => !c.name().startsWith('npm:') && !c.name().startsWith('my:'));
82
+ // Hide all commands from default listing - we'll show them in custom sections
83
+ return [];
91
84
  },
92
85
  subcommandTerm: (cmd) => color.white.bold(cmd.name()),
93
86
  subcommandDescription: (cmd) => color.gray(cmd.description()),
94
87
  });
95
88
 
96
- program.showHelpAfterError();
89
+ // Don't show global help after error - individual commands will show their own help
90
+ // program.showHelpAfterError();
97
91
  program.showSuggestionAfterError(true);
98
92
  program.addHelpCommand(false);
99
93
 
100
- let completeAst;
101
- try {
102
- completeAst = babelParser.parse(source, {
103
- sourceType: "module",
104
- ranges: true,
105
- tokens: true,
106
- comments: true,
107
- attachComment: true,
108
- });
109
- } catch (parseError) {
110
- // Re-throw with more specific error information
111
- parseError.code = 'BABEL_PARSER_SYNTAX_ERROR';
112
- throw parseError;
113
- }
94
+ // Override the program's error output formatting
95
+ program.configureOutput({
96
+ writeErr: (str) => {
97
+ // Replace "error:" with red "Error:" in error output
98
+ process.stderr.write(str.replace(/^error:/g, color.red('Error') + ':'));
99
+ },
100
+ writeOut: (str) => process.stdout.write(str)
101
+ });
114
102
 
115
- const comments = fetchComments();
116
- const homeComments = fetchHomeComments();
103
+ // Parse AST and comments for each source
104
+ const comments = {};
105
+ const namespaceSources = {};
117
106
 
118
- // Collect all commands (bunosh + personal commands + npm scripts) and sort them
119
- const allCommands = [];
107
+ for (const [cmdName, cmdInfo] of Object.entries(sources)) {
108
+ if (cmdInfo.source) {
109
+ try {
110
+ const ast = babelParser.parse(cmdInfo.source, {
111
+ sourceType: "module",
112
+ ranges: true,
113
+ tokens: true,
114
+ comments: true,
115
+ attachComment: true,
116
+ });
120
117
 
121
- // Add bunosh commands
122
- Object.keys(commands).forEach((fnName) => {
123
- allCommands.push({ type: 'bunosh', name: fnName, data: commands[fnName] });
124
- });
118
+ // Store AST for this command
119
+ if (!namespaceSources[cmdInfo.namespace || '']) {
120
+ namespaceSources[cmdInfo.namespace || ''] = {
121
+ ast: ast,
122
+ source: cmdInfo.source
123
+ };
124
+ }
125
125
 
126
- // Add personal commands with my: prefix
127
- Object.keys(homeTasks).forEach((fnName) => {
128
- if (typeof homeTasks[fnName] === 'function') {
129
- allCommands.push({ type: 'home', name: `my:${fnName}`, data: homeTasks[fnName], source: homeSource });
126
+ // Extract comments for this command
127
+ const fnName = cmdInfo.namespace ? cmdName.split(':')[1] : cmdName;
128
+ if (fnName) {
129
+ comments[cmdName] = extractCommentForFunction(ast, cmdInfo.source, fnName);
130
+ }
131
+ } catch (parseError) {
132
+ // Re-throw with more specific error information
133
+ parseError.code = 'BABEL_PARSER_SYNTAX_ERROR';
134
+ throw parseError;
135
+ }
130
136
  }
131
- });
137
+ }
132
138
 
133
- // Add npm scripts
134
- Object.entries(npmScripts).forEach(([scriptName, scriptCommand]) => {
135
- allCommands.push({ type: 'npm', name: `npm:${scriptName}`, data: { scriptName, scriptCommand } });
139
+ // Collect all commands (bunosh + namespace commands + npm scripts) and sort them
140
+ const allCommands = [];
141
+
142
+ // Add bunosh commands (including namespaced ones)
143
+ Object.keys(commands).forEach((cmdName) => {
144
+ const sourceInfo = sources[cmdName];
145
+ if (sourceInfo && sourceInfo.namespace) {
146
+ // This is a namespaced command
147
+ allCommands.push({
148
+ type: 'namespace',
149
+ name: cmdName,
150
+ namespace: sourceInfo.namespace,
151
+ data: commands[cmdName]
152
+ });
153
+ } else {
154
+ // Regular bunosh command
155
+ allCommands.push({ type: 'bunosh', name: cmdName, data: commands[cmdName] });
156
+ }
136
157
  });
137
158
 
159
+
160
+
138
161
  // Sort all commands alphabetically by name
139
162
  allCommands.sort((a, b) => a.name.localeCompare(b.name));
140
163
 
164
+ // Organize commands by namespace for help display
165
+ const commandsByNamespace = {
166
+ '': [], // Main commands (no namespace)
167
+ 'dev': [], // Dev commands
168
+ };
169
+
170
+ // Categorize commands by namespace
171
+ allCommands.forEach(cmd => {
172
+ if (cmd.type === 'namespace' && cmd.namespace) {
173
+ // Group by namespace, defaulting to 'dev' for unknown namespaces
174
+ const namespace = cmd.namespace || 'dev';
175
+ if (!commandsByNamespace[namespace]) {
176
+ commandsByNamespace[namespace] = [];
177
+ }
178
+ commandsByNamespace[namespace].push(cmd);
179
+ } else {
180
+ commandsByNamespace[''].push(cmd);
181
+ }
182
+ });
183
+
141
184
  // Process all commands in sorted order
142
185
  allCommands.forEach((cmdData) => {
143
- if (cmdData.type === 'bunosh') {
186
+ if (cmdData.type === 'bunosh' || (cmdData.type === 'namespace' && !cmdData.namespace)) {
187
+ // Handle main bunosh commands (no namespace)
144
188
  const fnName = cmdData.name;
145
189
  const fnBody = commands[fnName].toString();
190
+ const sourceInfo = sources[fnName];
191
+ const namespaceSource = namespaceSources[''];
146
192
 
147
- const ast = fetchFnAst();
148
- const args = parseArgs();
149
- const opts = parseOpts();
150
-
193
+ const ast = namespaceSource?.ast || babelParser.parse(fnBody, { comment: true, tokens: true });
194
+ const args = parseArgs(ast, fnName);
195
+ const opts = parseOpts(ast, fnName);
151
196
  const comment = comments[fnName];
152
197
 
153
198
  const commandName = prepareCommandName(fnName);
154
199
 
155
200
  const command = program.command(commandName);
201
+ if (comment) {
202
+ command.description(comment);
203
+ }
156
204
  command.hook('preAction', (_thisCommand) => {
157
205
  process.env.BUNOSH_COMMAND_STARTED = true;
158
206
  })
@@ -187,115 +235,51 @@ export default async function bunosh(commands, source) {
187
235
 
188
236
  let description = comment?.split('\n')[0] || '';
189
237
 
190
- if (comment && argsAndOptsDescription.length) description += `\n ${color.gray(`bunosh ${commandName}`)} ${color.blue(argsAndOptsDescription.join(' ').trim())}`;
238
+ if (comment && argsAndOptsDescription.length) description += `\n${color.gray(`bunosh ${commandName}`)} ${color.blue(argsAndOptsDescription.join(' ').trim())}`;
191
239
 
192
240
  command.description(description);
193
- command.action(commands[fnName].bind(commands));
194
-
195
- function fetchFnAst() {
196
- let hasFnInSource = false;
197
-
198
- traverse(completeAst, {
199
- FunctionDeclaration(path) {
200
- if (path.node.id.name == fnName) {
201
- hasFnInSource = true;
202
- return;
203
- }
204
- },
205
- });
206
-
207
- if (hasFnInSource) return completeAst;
208
-
209
- return babelParser.parse(fnBody, { comment: true, tokens: true });
210
- }
211
-
212
- function parseArgs() {
213
- const functionArguments = {};
214
241
 
215
- traverse(ast, {
216
- FunctionDeclaration(path) {
217
- if (path.node.id.name !== fnName) return;
218
-
219
- const params = path.node.params
220
- .filter((node) => {
221
- return node?.right?.type !== "ObjectExpression";
222
- })
223
- .forEach((param) => {
224
- if (param.type === "AssignmentPattern") {
225
- functionArguments[param.left.name] = param.right.value;
226
- return;
227
- }
228
- if (!param.name) return;
229
-
230
- return functionArguments[param.name] = null;
231
- });
232
-
233
- },
234
- });
235
-
236
- return functionArguments;
237
- }
238
-
239
- function parseOpts() {
240
- let functionOpts = {};
241
-
242
- traverse(ast, {
243
- FunctionDeclaration(path) {
244
- if (path.node.id.name !== fnName) return;
245
-
246
- const node = path.node.params.pop();
247
- if (!node) return;
248
- if (
249
- !node.type === "AssignmentPattern" &&
250
- node.right.type === "ObjectExpression"
251
- )
252
- return;
253
-
254
- node?.right?.properties?.forEach((p) => {
255
- if (
256
- ["NumericLiteral", "StringLiteral", "BooleanLiteral"].includes(
257
- p.value.type,
258
- )
259
- ) {
260
- functionOpts[camelToDasherize(p.key.name)] = p.value.value;
261
- return;
262
- }
263
-
264
- if (p.value.type === "NullLiteral") {
265
- functionOpts[camelToDasherize(p.key.name)] = null;
266
- return;
267
- }
268
-
269
- if (p.value.type == "UnaryExpression" && p.value.operator == "!") {
270
- functionOpts[camelToDasherize(p.key.name)] =
271
- !p.value.argument.value;
272
- return;
273
- }
274
- });
275
- },
276
- });
277
-
278
- return functionOpts;
279
- }
280
- } else if (cmdData.type === 'home') {
281
- // Handle personal commands with my: prefix
282
- const originalFnName = cmdData.name.replace('my:', ''); // Remove my: prefix for internal usage
283
- const fnBody = cmdData.data.toString();
284
- const homeAst = fetchHomeFnAst(originalFnName, cmdData.source);
285
- const homeArgs = parseHomeArgs(originalFnName, homeAst);
286
- const homeOpts = parseHomeOpts(originalFnName, homeAst);
287
- const homeComment = homeComments[originalFnName];
288
-
289
- const commandName = cmdData.name; // Keep the full my: prefix for command name
242
+ // Custom error handling for missing arguments - show command help without banner
243
+ command.configureHelp({
244
+ commandDescription: () => comment || '',
245
+ commandUsage: cmd => `bunosh ${cmd.name()}${argsAndOptsDescription.length ? ' ' + argsAndOptsDescription.join(' ').trim() : ''}`,
246
+ showGlobalOptions: false,
247
+ visibleGlobalOptions: () => [],
248
+ visibleOptions: () => [],
249
+ visibleCommands: () => []
250
+ });
251
+ command.showHelpAfterError();
252
+
253
+ command.action(createCommandAction(commands[fnName], args, opts));
254
+ } else if (cmdData.type === 'namespace') {
255
+ // Handle namespaced commands
256
+ const sourceInfo = sources[cmdData.name];
257
+ const originalFnName = sourceInfo.originalFnName || cmdData.name.split(':')[1]; // Get original function name
258
+ const namespace = cmdData.namespace;
259
+ const fnBody = commands[cmdData.name].toString();
260
+ const namespaceSource = namespaceSources[namespace];
261
+
262
+ const ast = namespaceSource?.ast || babelParser.parse(fnBody, { comment: true, tokens: true });
263
+ const args = parseArgs(ast, originalFnName);
264
+ const opts = parseOpts(ast, originalFnName);
265
+ const comment = comments[cmdData.name];
266
+
267
+ // For namespaced commands, only transform the function part to kebab-case
268
+ const commandName = cmdData.name.includes(':')
269
+ ? cmdData.name.split(':')[0] + ':' + toKebabCase(cmdData.name.split(':')[1]).replace(/^[^:]+:/, '')
270
+ : prepareCommandName(cmdData.name);
290
271
 
291
272
  const command = program.command(commandName);
273
+ if (comment) {
274
+ command.description(comment);
275
+ }
292
276
  command.hook('preAction', (_thisCommand) => {
293
277
  process.env.BUNOSH_COMMAND_STARTED = true;
294
- });
278
+ })
295
279
 
296
280
  let argsAndOptsDescription = [];
297
281
 
298
- Object.entries(homeArgs).forEach(([arg, value]) => {
282
+ Object.entries(args).forEach(([arg, value]) => {
299
283
  if (value === undefined) {
300
284
  argsAndOptsDescription.push(`<${arg}>`);
301
285
  return command.argument(`<${arg}>`);
@@ -310,7 +294,7 @@ export default async function bunosh(commands, source) {
310
294
  command.argument(`[${arg}]`, ``, value);
311
295
  });
312
296
 
313
- Object.entries(homeOpts).forEach(([opt, value]) => {
297
+ Object.entries(opts).forEach(([opt, value]) => {
314
298
  if (value === false || value === null) {
315
299
  argsAndOptsDescription.push(`--${opt}`);
316
300
  return command.option(`--${opt}`);
@@ -320,42 +304,75 @@ export default async function bunosh(commands, source) {
320
304
  command.option(`--${opt} [${opt}]`, "", value);
321
305
  });
322
306
 
323
- let description = homeComment?.split('\n')[0] || '';
307
+ let description = comment?.split('\n')[0] || '';
324
308
 
325
- if (homeComment && argsAndOptsDescription.length) {
309
+ if (comment && argsAndOptsDescription.length) {
326
310
  description += `\n ${color.gray(`bunosh ${commandName}`)} ${color.blue(argsAndOptsDescription.join(' ').trim())}`;
327
311
  }
328
312
 
329
313
  command.description(description);
330
- command.action(cmdData.data.bind(homeTasks));
331
- } else if (cmdData.type === 'npm') {
332
- // Handle npm scripts
333
- const { scriptName, scriptCommand } = cmdData.data;
334
- const commandName = `npm:${scriptName}`;
335
- const command = program.command(commandName);
336
- command.description(color.gray(scriptCommand)); // Use script command as description
337
314
 
338
- // Create action with proper closure to capture scriptName
339
- command.action(createNpmScriptAction(scriptName));
315
+ // Custom error handling for missing arguments - show command help without banner
316
+ command.configureHelp({
317
+ commandDescription: () => comment || '',
318
+ commandUsage: cmd => `bunosh ${cmd.name()}${argsAndOptsDescription.length ? ' ' + argsAndOptsDescription.join(' ').trim() : ''}`,
319
+ showGlobalOptions: false,
320
+ visibleGlobalOptions: () => [],
321
+ visibleOptions: () => [],
322
+ visibleCommands: () => []
323
+ });
324
+ command.showHelpAfterError();
325
+
326
+ command.action(createCommandAction(commands[cmdData.name], args, opts));
340
327
  }
341
328
  });
342
329
 
343
- // Helper function to create npm script action with proper closure
344
- function createNpmScriptAction(scriptName) {
345
- return async () => {
346
- // Execute npm script using Bunosh's exec task
347
- const { exec } = await import('../index.js');
330
+ // Helper function to create command action with proper argument transformation
331
+ function createCommandAction(commandFn, args, opts) {
332
+ return async (...commanderArgs) => {
333
+ // Transform Commander.js arguments to match function signature
334
+ const transformedArgs = [];
335
+ let argIndex = 0;
336
+
337
+ // Add positional arguments
338
+ Object.keys(args).forEach((argName) => {
339
+ if (argIndex < commanderArgs.length - 1) { // -1 because last arg is options object
340
+ transformedArgs.push(commanderArgs[argIndex++]);
341
+ } else {
342
+ // Use default value if not provided
343
+ transformedArgs.push(args[argName]);
344
+ }
345
+ });
346
+
347
+ // Handle options object
348
+ const optionsObj = commanderArgs[commanderArgs.length - 1];
349
+ if (optionsObj && typeof optionsObj === 'object') {
350
+ Object.keys(opts).forEach((optName) => {
351
+ const dasherizedOpt = optName.replace(/([A-Z])/g, '-$1').toLowerCase();
352
+ if (optionsObj[dasherizedOpt] !== undefined) {
353
+ transformedArgs.push(optionsObj[dasherizedOpt]);
354
+ } else {
355
+ // Use default value
356
+ transformedArgs.push(opts[optName]);
357
+ }
358
+ });
359
+ }
360
+
361
+ // Call the original function with transformed arguments
348
362
  try {
349
- // Call exec with proper template literal simulation
350
- const result = await exec(['npm run ', ''], scriptName);
351
- return result;
363
+ return await commandFn(...transformedArgs);
352
364
  } catch (error) {
353
- console.error(`Failed to run npm script: ${scriptName}`);
365
+ // Handle errors thrown from functions properly
366
+ console.error(`\n❌ Error: ${error.message}`);
367
+ if (error.stack && process.env.BUNOSH_DEBUG) {
368
+ console.error(error.stack);
369
+ }
354
370
  process.exit(1);
355
371
  }
356
372
  };
357
373
  }
358
374
 
375
+
359
376
  const editCmd = program.command('edit')
360
377
  .description('Open the bunosh file in your editor. $EDITOR or \'code\' is used.')
361
378
  .action(async () => {
@@ -460,138 +477,133 @@ export default async function bunosh(commands, source) {
460
477
  internalCommands.push(setupCompletionCmd);
461
478
 
462
479
  const upgradeCmd = program.command('upgrade')
463
- .description('Upgrade bunosh to the latest version (single executable only)')
480
+ .description('Upgrade bunosh to the latest version')
464
481
  .option('-f, --force', 'Force upgrade even if already on latest version')
465
482
  .option('--check', 'Check for updates without upgrading')
466
483
  .action(async (options) => {
467
- try {
468
- if (!isExecutable()) {
469
- console.log('📦 Bunosh is installed via npm.');
470
- console.log('To upgrade, run: ' + color.bold('npm update -g bunosh'));
471
- process.exit(0);
472
- }
484
+ await upgradeCommand(options);
485
+ });
473
486
 
474
- const currentVersion = getCurrentVersion();
475
- console.log(`📍 Current version: ${color.bold(currentVersion)}`);
487
+ internalCommands.push(upgradeCmd);
476
488
 
477
- if (options.check) {
478
- console.log('🔍 Checking for updates...');
479
- try {
480
- const { getLatestRelease, isNewerVersion } = await import('./upgrade.js');
481
- const release = await getLatestRelease();
482
- const latestVersion = release.tag_name;
483
489
 
484
- console.log(`📦 Latest version: ${color.bold(latestVersion)}`);
490
+ // Add organized command help sections
491
+ let helpText = '';
485
492
 
486
- if (isNewerVersion(latestVersion, currentVersion)) {
487
- console.log(`✨ ${color.green('Update available!')} ${currentVersion} ${latestVersion}`);
488
- console.log('Run ' + color.bold('bunosh upgrade') + ' to update.');
489
- } else {
490
- console.log(`✅ ${color.green('You are on the latest version!')}`);
491
- }
492
- } catch (error) {
493
- console.error(`❌ Failed to check for updates: ${error.message}`);
494
- process.exit(1);
495
- }
496
- return;
497
- }
493
+ // Main Commands (no namespace)
494
+ if (commandsByNamespace[''].length > 0) {
495
+ const mainCommands = commandsByNamespace[''].map(cmd => {
496
+ const processedName = cmd.type === 'bunosh' ? toKebabCase(cmd.name) : cmd.name;
497
+ const cmdObj = program.commands.find(c => c.name() === processedName);
498
+ const description = cmdObj ? cmdObj.description() : '';
499
+ const paddedName = processedName.padEnd(22);
498
500
 
499
- console.log('⬆️ Starting upgrade process...');
500
- console.log();
501
+ if (!description) {
502
+ return ` ${color.white.bold(paddedName)}`;
503
+ }
501
504
 
502
- let lastMessage = '';
503
- const result = await upgradeExecutable({
504
- force: options.force,
505
- onProgress: (message) => {
506
- if (message !== lastMessage) {
507
- console.log(` ${message}`);
508
- lastMessage = message;
509
- }
510
- }
511
- });
505
+ // Handle multi-line descriptions with proper indentation
506
+ const lines = description.split('\n');
507
+ const firstLine = ` ${color.white.bold(paddedName)} ${lines[0]}`;
508
+ const indentedLines = lines.slice(1).map(line =>
509
+ line.trim() ? ` ${line}` : ''
510
+ ).filter(line => line);
512
511
 
513
- console.log();
514
- if (result.updated) {
515
- console.log(`🎉 ${color.green('Upgrade successful!')}`);
516
- console.log(` ${result.currentVersion} → ${color.bold(result.latestVersion)}`);
517
- console.log();
518
- console.log(`💡 Run ${color.bold('bunosh --version')} to verify the new version.`);
519
- } else {
520
- console.log(`✅ ${color.green(result.message)}`);
521
- if (!options.force) {
522
- console.log(` Use ${color.bold('--force')} to reinstall the current version.`);
523
- }
524
- }
512
+ return [firstLine, ...indentedLines].join('\n');
513
+ }).join('\n');
514
+ helpText += `Commands:
515
+ ${mainCommands}
525
516
 
526
- } catch (error) {
527
- console.error(`❌ Upgrade failed: ${error.message}`);
517
+ `;
518
+ }
528
519
 
529
- if (error.message.includes('Unsupported platform')) {
530
- console.log();
531
- console.log('💡 Supported platforms:');
532
- console.log(' • Linux x64');
533
- console.log(' • macOS ARM64 (Apple Silicon)');
534
- console.log(' • Windows x64');
535
- } else if (error.message.includes('GitHub API')) {
536
- console.log();
537
- console.log('💡 Try again later or check your internet connection.');
520
+ // Dev Commands (dev namespace)
521
+ if (commandsByNamespace.dev.length > 0) {
522
+ const devCommands = commandsByNamespace.dev.map(cmd => {
523
+ let processedName;
524
+ if (cmd.type === 'namespace') {
525
+ // For namespaced commands, handle the name properly
526
+ if (cmd.name.includes(':')) {
527
+ // If cmd.name already has namespace (like 'dev:devFn'), only process the part after the colon
528
+ const [namespace, functionName] = cmd.name.split(':');
529
+ processedName = `${namespace}:${toKebabCase(functionName).replace(/^[^:]+:/, '')}`;
530
+ } else {
531
+ // If cmd.name doesn't have namespace, use toKebabCase which will add it
532
+ processedName = toKebabCase(cmd.name);
538
533
  }
534
+ } else {
535
+ processedName = cmd.name;
536
+ }
537
+ // Debug removed
538
+ const cmdObj = program.commands.find(c => c.name() === processedName);
539
+ const description = cmdObj ? cmdObj.description() : '';
540
+ const paddedName = processedName.padEnd(22);
539
541
 
540
- process.exit(1);
542
+ if (!description) {
543
+ return ` ${color.white.bold(paddedName)}`;
541
544
  }
542
- });
543
545
 
544
- internalCommands.push(upgradeCmd);
546
+ // Handle multi-line descriptions with proper indentation
547
+ const lines = description.split('\n');
548
+ const firstLine = ` ${color.white.bold(paddedName)} ${lines[0]}`;
549
+ const indentedLines = lines.slice(1).map(line =>
550
+ line.trim() ? ` ${line}` : ''
551
+ ).filter(line => line);
545
552
 
546
- // Add personal commands help section if personal commands exist
547
- const homeTaskNamesForHelp = Object.keys(homeTasks).filter(key => typeof homeTasks[key] === 'function');
548
- if (homeTaskNamesForHelp.length > 0) {
549
- const homeCommandsList = homeTaskNamesForHelp.sort().map(taskName => {
550
- const commandName = `my:${taskName}`;
551
- const taskComment = homeComments[taskName] || '';
552
- const description = taskComment ? taskComment.split('\n')[0] : 'Personal command';
553
- return ` ${color.white.bold(commandName.padEnd(18))} ${color.gray(description)}`;
553
+ return [firstLine, ...indentedLines].join('\n');
554
554
  }).join('\n');
555
+ helpText += `Dev Commands:
556
+ ${devCommands}
555
557
 
556
- program.addHelpText('after', `
557
-
558
- My Commands (from ~/${BUNOSHFILE}):
559
- ${homeCommandsList}
560
- `);
558
+ `;
561
559
  }
562
560
 
563
- // Add npm scripts help section if npm scripts exist
564
- const npmScriptNamesForHelp = Object.keys(npmScripts);
565
- if (npmScriptNamesForHelp.length > 0) {
566
- const npmCommandsList = npmScriptNamesForHelp.sort().map(scriptName => {
567
- const commandName = `npm:${scriptName}`;
568
- const scriptCommand = npmScripts[scriptName];
569
- return ` ${color.white.bold(commandName.padEnd(18))} ${color.gray(scriptCommand)}`;
570
- }).join('\n');
571
-
572
- program.addHelpText('after', `
561
+ // Add other namespace sections dynamically
562
+ Object.keys(commandsByNamespace).forEach(namespace => {
563
+ if (namespace && namespace !== 'dev' && commandsByNamespace[namespace].length > 0) {
564
+ const namespaceName = namespace.charAt(0).toUpperCase() + namespace.slice(1) + ' Commands';
565
+ const namespaceCommands = commandsByNamespace[namespace].map(cmd => {
566
+ const cmdObj = program.commands.find(c => c.name() === cmd.name);
567
+ const description = cmdObj ? cmdObj.description() : '';
568
+ const paddedName = cmd.name.padEnd(22);
569
+
570
+ if (!description) {
571
+ return ` ${color.white.bold(paddedName)}`;
572
+ }
573
573
 
574
- NPM Scripts:
575
- ${npmCommandsList}
576
- `);
577
- }
574
+ // Handle multi-line descriptions with proper indentation
575
+ const lines = description.split('\n');
576
+ const firstLine = ` ${color.white.bold(paddedName)} ${lines[0]}`;
577
+ const indentedLines = lines.slice(1).map(line =>
578
+ line.trim() ? ` ${line}` : ''
579
+ ).filter(line => line);
578
580
 
579
- program.addHelpText('after', `
581
+ return [firstLine, ...indentedLines].join('\n');
582
+ }).join('\n');
583
+ helpText += `${namespaceName}:
584
+ ${namespaceCommands}
580
585
 
581
- Special Commands:
586
+ `;
587
+ }
588
+ });
582
589
 
583
- 📝 Edit bunosh file: ${color.bold('bunosh edit')}
584
- 📥 Export commands as scripts to package.json: ${color.bold('bunosh export:scripts')}
585
- 🦾 Upgrade bunosh: ${color.bold('bunosh upgrade')}
586
590
 
587
- Execute JavaScript:
588
- ${color.bold('bunosh -e "console.log(\'Hello\')"')} Execute inline JavaScript
589
- ${color.bold('bunosh -e < script.js')} Execute JavaScript from file
591
+ // Special Commands
592
+ helpText += color.dim(`Special Commands:
593
+ ${color.bold('bunosh edit')} 📝 Edit bunosh file with $EDITOR
594
+ ${color.bold('bunosh export:scripts')} 📥 Export commands to package.json
595
+ ${color.bold('bunosh upgrade')} 🦾 Upgrade bunosh
596
+ ${color.bold('bunosh -e "say(\'Hi\')"')} 🔧 Run inline Bunosh script
597
+ ${color.bold('bunosh --bunoshfile …')} 🥧 Load custom Bunoshfile from path
598
+ ${color.bold('bunosh --env-file …')} 🔧 Load custom environment file
590
599
  `);
591
600
 
601
+ program.addHelpText('after', helpText);
602
+
592
603
  program.on("command:*", (cmd) => {
593
- console.log(`\nUnknown command ${cmd}\n`);
604
+ console.error(`\nUnknown command ${cmd}\n`);
594
605
  program.outputHelp();
606
+ process.exit(1);
595
607
  });
596
608
 
597
609
  // Show help if no command provided
@@ -601,8 +613,9 @@ Execute JavaScript:
601
613
  }
602
614
 
603
615
  program.parse(process.argv);
616
+ }
604
617
 
605
- function fetchComments() {
618
+ function fetchComments() {
606
619
  const comments = {};
607
620
 
608
621
  let startFromLine = 0;
@@ -643,157 +656,32 @@ Execute JavaScript:
643
656
  return comments;
644
657
  }
645
658
 
646
- function fetchHomeComments() {
647
- if (!homeSource) return {};
648
-
649
- const homeComments = {};
650
- let homeCompleteAst;
651
-
652
- try {
653
- homeCompleteAst = babelParser.parse(homeSource, {
654
- sourceType: "module",
655
- ranges: true,
656
- tokens: true,
657
- comments: true,
658
- attachComment: true,
659
- });
660
- } catch (parseError) {
661
- console.warn('Warning: Could not parse home Bunoshfile for comments:', parseError.message);
662
- return {};
663
- }
664
-
665
- let startFromLine = 0;
666
-
667
- traverse(homeCompleteAst, {
668
- FunctionDeclaration(path) {
669
- const functionName = path.node.id && path.node.id.name;
670
-
671
- const commentSource = homeSource
672
- .split("\n")
673
- .slice(startFromLine, path.node?.loc?.start?.line)
674
- .join("\n");
675
- const matches = commentSource.match(
676
- /\/\*\*\s([\s\S]*)\\*\/\s*export/,
677
- );
678
-
679
- if (matches && matches[1]) {
680
- homeComments[functionName] = matches[1]
681
- .replace(/^\s*\*\s*/gm, "")
682
- .replace(/\s*\*\*\s*$/gm, "")
683
- .trim()
684
- .replace(/^@.*$/gm, "")
685
- .trim();
686
- } else {
687
- // Check for comments attached to the first statement in the function body
688
- const firstStatement = path.node?.body?.body?.[0];
689
- const leadingComments = firstStatement?.leadingComments;
690
-
691
- if (leadingComments && leadingComments.length > 0) {
692
- homeComments[functionName] = leadingComments[0].value.trim();
693
- }
694
- }
695
-
696
- startFromLine = path.node?.loc?.end?.line;
697
- },
698
- });
699
-
700
- return homeComments;
701
- }
702
-
703
- function fetchHomeFnAst(fnName, source) {
704
- try {
705
- return babelParser.parse(source, {
706
- sourceType: "module",
707
- ranges: true,
708
- tokens: true,
709
- comments: true,
710
- attachComment: true,
711
- });
712
- } catch (parseError) {
713
- console.warn('Warning: Could not parse home function AST:', parseError.message);
714
- return null;
715
- }
716
- }
717
-
718
- function parseHomeArgs(fnName, ast) {
719
- if (!ast) return {};
720
-
721
- const functionArguments = {};
722
-
723
- traverse(ast, {
724
- FunctionDeclaration(path) {
725
- if (path.node.id.name !== fnName) return;
726
-
727
- const params = path.node.params
728
- .filter((node) => {
729
- return node?.right?.type !== "ObjectExpression";
730
- })
731
- .forEach((param) => {
732
- if (param.type === "AssignmentPattern") {
733
- functionArguments[param.left.name] = param.right.value;
734
- return;
735
- }
736
- if (!param.name) return;
737
-
738
- return functionArguments[param.name] = null;
739
- });
740
- },
741
- });
742
-
743
- return functionArguments;
659
+ function prepareCommandName(name) {
660
+ // name is already the final command name (could be namespaced or not)
661
+ // For namespaced commands, only transform the function part (after the last colon)
662
+ const lastColonIndex = name.lastIndexOf(':');
663
+ if (lastColonIndex !== -1) {
664
+ const namespace = name.substring(0, lastColonIndex);
665
+ const commandPart = name.substring(lastColonIndex + 1);
666
+ return `${namespace}:${toKebabCase(commandPart)}`;
744
667
  }
745
668
 
746
- function parseHomeOpts(fnName, ast) {
747
- if (!ast) return {};
748
-
749
- let functionOpts = {};
750
-
751
- traverse(ast, {
752
- FunctionDeclaration(path) {
753
- if (path.node.id.name !== fnName) return;
754
-
755
- const node = path.node.params.pop();
756
- if (!node) return;
757
- if (
758
- !node.type === "AssignmentPattern" &&
759
- node.right.type === "ObjectExpression"
760
- )
761
- return;
762
-
763
- node?.right?.properties?.forEach((p) => {
764
- if (
765
- ["NumericLiteral", "StringLiteral", "BooleanLiteral"].includes(
766
- p.value.type,
767
- )
768
- ) {
769
- functionOpts[camelToDasherize(p.key.name)] = p.value.value;
770
- return;
771
- }
772
-
773
- if (p.value.type === "NullLiteral") {
774
- functionOpts[camelToDasherize(p.key.name)] = null;
775
- return;
776
- }
669
+ // For non-namespaced commands, just convert to kebab-case
670
+ return toKebabCase(name);
671
+ }
777
672
 
778
- if (p.value.type == "UnaryExpression" && p.value.operator == "!") {
779
- functionOpts[camelToDasherize(p.key.name)] =
780
- !p.value.argument.value;
781
- return;
782
- }
783
- });
784
- },
785
- });
673
+ function toKebabCase(name) {
674
+ const parts = name.split(/(?=[A-Z])/);
786
675
 
787
- return functionOpts;
676
+ // If there are multiple parts, treat first part as namespace with colon
677
+ if (parts.length > 1) {
678
+ const namespace = parts[0].toLowerCase();
679
+ const command = parts.slice(1).join("-").toLowerCase();
680
+ return `${namespace}:${command}`;
788
681
  }
789
- }
790
682
 
791
- function prepareCommandName(name) {
792
- name = name
793
- .split(/(?=[A-Z])/)
794
- .join("-")
795
- .toLowerCase();
796
- return name.replace("-", ":");
683
+ // Single word, just return lowercase
684
+ return name.toLowerCase();
797
685
  }
798
686
 
799
687
  function camelToDasherize(camelCaseString) {
@@ -854,49 +742,239 @@ function exportFn(commands) {
854
742
  return true;
855
743
  }
856
744
 
857
- function loadNpmScripts() {
858
- try {
859
- if (!existsSync('package.json')) {
860
- return {};
861
- }
862
745
 
863
- const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
864
- const scripts = pkg.scripts || {};
746
+ function extractCommentForFunction(ast, source, fnName) {
747
+ let comment = '';
748
+
749
+ traverse(ast, {
750
+ FunctionDeclaration(path) {
751
+ if (path.node.id?.name !== fnName) return;
752
+
753
+ const functionStartLine = path.node.loc.start.line;
754
+
755
+ // Find the JSDoc comment that immediately precedes this function
756
+ if (ast.comments) {
757
+ for (const astComment of ast.comments) {
758
+ if (astComment.type === 'CommentBlock' && astComment.value.startsWith('*')) {
759
+ const commentEndLine = astComment.loc.end.line;
865
760
 
866
- // Filter out bunosh scripts (scripts that contain "bunosh")
867
- const npmScripts = {};
868
- Object.entries(scripts).forEach(([name, command]) => {
869
- if (!command.includes('bunosh')) {
870
- npmScripts[name] = command;
761
+ // Check if this comment is immediately before the function
762
+ if (commentEndLine === functionStartLine - 1) {
763
+ comment = astComment.value
764
+ .replace(/^\s*\*\s*/gm, '') // Remove leading * and spaces
765
+ .replace(/^\s*@.*$/gm, '') // Remove @param and other @ tags
766
+ .replace(/\n\s*\n/g, '\n') // Remove excessive empty lines
767
+ .replace(/^\*\s*/, '') // Remove any remaining leading *
768
+ .trim();
769
+ break;
770
+ }
771
+ }
772
+ }
871
773
  }
872
- });
873
774
 
874
- return npmScripts;
875
- } catch (error) {
876
- console.warn('Warning: Could not load npm scripts from package.json:', error.message);
877
- return {};
878
- }
775
+ // If no JSDoc comment found, check for comments attached to the first statement in the function body
776
+ if (!comment) {
777
+ const firstStatement = path.node?.body?.body?.[0];
778
+ const statementLeadingComments = firstStatement?.leadingComments;
779
+
780
+ if (statementLeadingComments && statementLeadingComments.length > 0) {
781
+ comment = statementLeadingComments[0].value.trim();
782
+ }
783
+ }
784
+ },
785
+ });
786
+
787
+ return comment;
879
788
  }
880
789
 
881
- // Load personal commands from user's home directory
882
- async function loadHomeTasks() {
883
- try {
884
- const os = await import('os');
885
- const path = await import('path');
886
- const homeDir = os.homedir();
887
- const homeBunoshfile = path.join(homeDir, BUNOSHFILE);
790
+ function parseArgs(ast, fnName) {
791
+ const functionArguments = {};
792
+
793
+ traverse(ast, {
794
+ FunctionDeclaration(path) {
795
+ if (path.node.id.name !== fnName) return;
796
+
797
+ const params = path.node.params
798
+ .filter((node) => {
799
+ return node?.right?.type !== "ObjectExpression";
800
+ })
801
+ .forEach((param) => {
802
+ if (param.type === "AssignmentPattern") {
803
+ functionArguments[param.left.name] = param.right.value;
804
+ return;
805
+ }
806
+ if (!param.name) return;
888
807
 
889
- if (!existsSync(homeBunoshfile)) {
890
- return { tasks: {}, source: '' };
808
+ return functionArguments[param.name] = undefined;
809
+ });
810
+
811
+ },
812
+ });
813
+
814
+ return functionArguments;
815
+ }
816
+
817
+ function parseOpts(ast, fnName) {
818
+ let functionOpts = {};
819
+
820
+ traverse(ast, {
821
+ FunctionDeclaration(path) {
822
+ if (path.node.id.name !== fnName) return;
823
+
824
+ const node = path.node.params.pop();
825
+ if (!node) return;
826
+ if (
827
+ node.type !== "AssignmentPattern" ||
828
+ node.right.type !== "ObjectExpression"
829
+ )
830
+ return;
831
+
832
+ node?.right?.properties?.forEach((p) => {
833
+ if (
834
+ ["NumericLiteral", "StringLiteral", "BooleanLiteral"].includes(
835
+ p.value.type,
836
+ )
837
+ ) {
838
+ functionOpts[camelToDasherize(p.key.name)] = p.value.value;
839
+ return;
840
+ }
841
+
842
+ if (p.value.type === "NullLiteral") {
843
+ functionOpts[camelToDasherize(p.key.name)] = null;
844
+ return;
845
+ }
846
+
847
+ if (p.value.type == "UnaryExpression" && p.value.operator == "!") {
848
+ functionOpts[camelToDasherize(p.key.name)] =
849
+ !p.value.argument.value;
850
+ return;
851
+ }
852
+ });
853
+ },
854
+ });
855
+
856
+ return functionOpts;
857
+ }
858
+
859
+ /**
860
+ * Represents a parsed Bunosh command with all its metadata
861
+ */
862
+ export class BunoshCommand {
863
+ constructor(name, namespace, args, opts, comment, fn) {
864
+ this.name = name;
865
+ this.namespace = namespace || '';
866
+ this.args = args;
867
+ this.opts = opts;
868
+ this.comment = comment;
869
+ this.function = fn;
870
+ }
871
+
872
+ /**
873
+ * Get the full command name (namespace:name if namespace exists)
874
+ */
875
+ get fullName() {
876
+ return this.namespace ? `${this.namespace}:${this.name}` : this.name;
877
+ }
878
+
879
+ /**
880
+ * Get the command name in kebab-case for CLI usage
881
+ */
882
+ get cliName() {
883
+ if (this.namespace) {
884
+ return `${this.namespace}:${camelToDasherize(this.name)}`;
891
885
  }
886
+ return camelToDasherize(this.name);
887
+ }
892
888
 
893
- // Import the home Bunoshfile
894
- const homeTasks = await import(homeBunoshfile);
895
- const homeSource = readFileSync(homeBunoshfile, 'utf-8');
889
+ /**
890
+ * Get all parameter names (args + opts)
891
+ */
892
+ get allParams() {
893
+ return [...Object.keys(this.args), ...Object.keys(this.opts)];
894
+ }
896
895
 
897
- return { tasks: homeTasks, source: homeSource };
898
- } catch (error) {
899
- console.warn('Warning: Could not load personal commands:', error.message);
900
- return { tasks: {}, source: '' };
896
+ /**
897
+ * Get required parameter names
898
+ */
899
+ get requiredParams() {
900
+ return Object.keys(this.args).filter(arg => this.args[arg] === undefined);
901
901
  }
902
902
  }
903
+
904
+ /**
905
+ * Process commands and sources to extract structured command information
906
+ * This reuses all the existing parsing logic from the main bunosh function
907
+ * @param {Object} commands - Commands object from Bunoshfile
908
+ * @param {Object} sources - Sources object containing comments and metadata
909
+ * @returns {Array<BunoshCommand>} Array of parsed BunoshCommand objects
910
+ */
911
+ export function processCommands(commands, sources) {
912
+ const parsedCommands = [];
913
+
914
+ // Parse AST and comments for each source (same as in main bunosh function)
915
+ const comments = {};
916
+ const namespaceSources = {};
917
+
918
+ for (const [cmdName, cmdInfo] of Object.entries(sources)) {
919
+ if (cmdInfo.source) {
920
+ try {
921
+ const ast = babelParser.parse(cmdInfo.source, {
922
+ sourceType: "module",
923
+ ranges: true,
924
+ tokens: true,
925
+ comments: true,
926
+ attachComment: true,
927
+ });
928
+
929
+ // Store AST for this command
930
+ if (!namespaceSources[cmdInfo.namespace || '']) {
931
+ namespaceSources[cmdInfo.namespace || ''] = {
932
+ ast: ast,
933
+ source: cmdInfo.source
934
+ };
935
+ }
936
+
937
+ // Extract comments for this command
938
+ const fnName = cmdInfo.namespace ? cmdName.split(':')[1] : cmdName;
939
+ if (fnName) {
940
+ comments[cmdName] = extractCommentForFunction(ast, cmdInfo.source, fnName);
941
+ }
942
+ } catch (parseError) {
943
+ parseError.code = 'BABEL_PARSER_SYNTAX_ERROR';
944
+ throw parseError;
945
+ }
946
+ }
947
+ }
948
+
949
+ // Process each command using the same logic as the main bunosh function
950
+ Object.entries(commands).forEach(([cmdName, cmdFn]) => {
951
+ const sourceInfo = sources[cmdName];
952
+ const originalFnName = sourceInfo?.originalFnName || cmdName.split(':')[1] || cmdName;
953
+ const namespace = sourceInfo?.namespace || '';
954
+ const namespaceSource = namespaceSources[namespace];
955
+ const comment = comments[cmdName];
956
+
957
+ // Parse function using the same logic as the main bunosh function
958
+ const fnBody = cmdFn.toString();
959
+ const ast = namespaceSource?.ast || babelParser.parse(fnBody, { comment: true, tokens: true });
960
+ const args = parseArgs(ast, originalFnName);
961
+ const opts = parseOpts(ast, originalFnName);
962
+
963
+ // Extract the actual command name without namespace
964
+ const commandName = originalFnName;
965
+
966
+ parsedCommands.push(new BunoshCommand(
967
+ commandName,
968
+ namespace,
969
+ args,
970
+ opts,
971
+ comment,
972
+ cmdFn
973
+ ));
974
+ });
975
+
976
+ return parsedCommands;
977
+ }
978
+
979
+ // Export parsing functions for use in MCP server
980
+ export { parseArgs, parseOpts, extractCommentForFunction };