bunosh 0.3.1 → 0.4.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
@@ -3,8 +3,7 @@ import babelParser from "@babel/parser";
3
3
  import traverseDefault from "@babel/traverse";
4
4
  const traverse = traverseDefault.default || traverseDefault;
5
5
  import color from "chalk";
6
- import fs from 'fs';
7
- import openEditor from './open-editor.js';
6
+ import { readFileSync, existsSync, writeFileSync } from 'fs';
8
7
  import { yell } from './io.js';
9
8
  import cprint from "./font.js";
10
9
  import { handleCompletion, detectCurrentShell, installCompletion, getCompletionPaths } from './completion.js';
@@ -13,12 +12,47 @@ import { upgradeExecutable, isExecutable, getCurrentVersion } from './upgrade.js
13
12
  export const BUNOSHFILE = `Bunoshfile.js`;
14
13
 
15
14
  export const banner = () => {
16
- console.log(cprint('Bunosh', { symbol: '⯀' }));
15
+ const asciiArt = cprint('Bunosh', { symbol: '⯀' });
16
+ console.log(createGradientAscii(asciiArt));
17
17
  console.log(color.gray('🍲 Your exceptional task runner'));
18
+
19
+ // Try to get version from package.json
20
+ try {
21
+ // First try relative to src directory
22
+ const pkgPath = new URL('../package.json', import.meta.url);
23
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
24
+ console.log(`Version: ${color.bold(pkg.version)}`);
25
+ } catch (e) {
26
+ // Ignore if version can't be read
27
+ }
18
28
  console.log();
19
29
  };
20
30
 
21
- export default function bunosh(commands, source) {
31
+ function createGradientAscii(asciiArt) {
32
+ 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
+ return lines.map((line, index) => {
42
+ // Create smooth gradient by interpolating between colors
43
+ 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);
52
+ }).join('\n');
53
+ }
54
+
55
+ export default async function bunosh(commands, source) {
22
56
  const program = new Command();
23
57
  program.option('--bunoshfile <path>', 'Path to the Bunoshfile');
24
58
 
@@ -27,19 +61,33 @@ export default function bunosh(commands, source) {
27
61
  // Load npm scripts from package.json
28
62
  const npmScripts = loadNpmScripts();
29
63
 
64
+ // Load personal commands from $HOME/Bunoshfile.js
65
+ const { tasks: homeTasks, source: homeSource } = await loadHomeTasks();
66
+
30
67
  program.configureHelp({
31
68
  commandDescription: _cmd => {
32
69
  // Show banner and description
33
70
  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
+
34
82
  return ` Commands are loaded from exported functions in ${color.bold(BUNOSHFILE)}`;
35
83
  },
36
- commandUsage: usg => 'bunosh <command> <args> [options]',
84
+ commandUsage: usg => 'bunosh [-e <code>] <command> <args> [options]',
37
85
  showGlobalOptions: false,
38
86
  visibleGlobalOptions: _opt => [],
39
87
  visibleOptions: _opt => [],
40
88
  visibleCommands: cmd => {
41
89
  const commands = cmd.commands.filter(c => !internalCommands.includes(c));
42
- return commands.filter(c => !c.name().startsWith('npm:'));
90
+ return commands.filter(c => !c.name().startsWith('npm:') && !c.name().startsWith('my:'));
43
91
  },
44
92
  subcommandTerm: (cmd) => color.white.bold(cmd.name()),
45
93
  subcommandDescription: (cmd) => color.gray(cmd.description()),
@@ -65,8 +113,9 @@ export default function bunosh(commands, source) {
65
113
  }
66
114
 
67
115
  const comments = fetchComments();
116
+ const homeComments = fetchHomeComments();
68
117
 
69
- // Collect all commands (bunosh + npm scripts) and sort them
118
+ // Collect all commands (bunosh + personal commands + npm scripts) and sort them
70
119
  const allCommands = [];
71
120
 
72
121
  // Add bunosh commands
@@ -74,6 +123,13 @@ export default function bunosh(commands, source) {
74
123
  allCommands.push({ type: 'bunosh', name: fnName, data: commands[fnName] });
75
124
  });
76
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 });
130
+ }
131
+ });
132
+
77
133
  // Add npm scripts
78
134
  Object.entries(npmScripts).forEach(([scriptName, scriptCommand]) => {
79
135
  allCommands.push({ type: 'npm', name: `npm:${scriptName}`, data: { scriptName, scriptCommand } });
@@ -221,6 +277,57 @@ export default function bunosh(commands, source) {
221
277
 
222
278
  return functionOpts;
223
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
290
+
291
+ const command = program.command(commandName);
292
+ command.hook('preAction', (_thisCommand) => {
293
+ process.env.BUNOSH_COMMAND_STARTED = true;
294
+ });
295
+
296
+ let argsAndOptsDescription = [];
297
+
298
+ Object.entries(homeArgs).forEach(([arg, value]) => {
299
+ if (value === undefined) {
300
+ argsAndOptsDescription.push(`<${arg}>`);
301
+ return command.argument(`<${arg}>`);
302
+ }
303
+
304
+ if (value === null) {
305
+ argsAndOptsDescription.push(`[${arg}]`);
306
+ return command.argument(`[${arg}]`, '', null);
307
+ }
308
+
309
+ argsAndOptsDescription.push(`[${arg}=${value}]`);
310
+ command.argument(`[${arg}]`, ``, value);
311
+ });
312
+
313
+ Object.entries(homeOpts).forEach(([opt, value]) => {
314
+ if (value === false || value === null) {
315
+ argsAndOptsDescription.push(`--${opt}`);
316
+ return command.option(`--${opt}`);
317
+ }
318
+
319
+ argsAndOptsDescription.push(`--${opt}=${value}`);
320
+ command.option(`--${opt} [${opt}]`, "", value);
321
+ });
322
+
323
+ let description = homeComment?.split('\n')[0] || '';
324
+
325
+ if (homeComment && argsAndOptsDescription.length) {
326
+ description += `\n ${color.gray(`bunosh ${commandName}`)} ${color.blue(argsAndOptsDescription.join(' ').trim())}`;
327
+ }
328
+
329
+ command.description(description);
330
+ command.action(cmdData.data.bind(homeTasks));
224
331
  } else if (cmdData.type === 'npm') {
225
332
  // Handle npm scripts
226
333
  const { scriptName, scriptCommand } = cmdData.data;
@@ -252,15 +359,14 @@ export default function bunosh(commands, source) {
252
359
  const editCmd = program.command('edit')
253
360
  .description('Open the bunosh file in your editor. $EDITOR or \'code\' is used.')
254
361
  .action(async () => {
255
- try {
256
- await openEditor([{
257
- file: BUNOSHFILE,
258
- }]);
259
- } catch (error) {
260
- console.error(error.message);
261
- console.error('Set $EDITOR environment variable to use a different editor');
362
+ if (!Bun) {
363
+ console.log('Bun is not available');
262
364
  process.exit(1);
365
+ return;
263
366
  }
367
+ await Bun.openEditor([{
368
+ file: BUNOSHFILE,
369
+ }]);
264
370
  });
265
371
 
266
372
  internalCommands.push(editCmd);
@@ -312,7 +418,7 @@ export default function bunosh(commands, source) {
312
418
  const paths = getCompletionPaths(shell);
313
419
 
314
420
  // Check if already installed
315
- if (!options.force && fs.existsSync(paths.completionFile)) {
421
+ if (!options.force && existsSync(paths.completionFile)) {
316
422
  console.log(`⚠️ Completion already installed at: ${paths.completionFile}`);
317
423
  console.log(' Use --force to overwrite, or run:');
318
424
  console.log(` ${color.dim('rm')} ${paths.completionFile}`);
@@ -437,6 +543,23 @@ export default function bunosh(commands, source) {
437
543
 
438
544
  internalCommands.push(upgradeCmd);
439
545
 
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)}`;
554
+ }).join('\n');
555
+
556
+ program.addHelpText('after', `
557
+
558
+ My Commands (from ~/${BUNOSHFILE}):
559
+ ${homeCommandsList}
560
+ `);
561
+ }
562
+
440
563
  // Add npm scripts help section if npm scripts exist
441
564
  const npmScriptNamesForHelp = Object.keys(npmScripts);
442
565
  if (npmScriptNamesForHelp.length > 0) {
@@ -460,6 +583,10 @@ Special Commands:
460
583
  📝 Edit bunosh file: ${color.bold('bunosh edit')}
461
584
  📥 Export commands as scripts to package.json: ${color.bold('bunosh export:scripts')}
462
585
  🦾 Upgrade bunosh: ${color.bold('bunosh upgrade')}
586
+
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
463
590
  `);
464
591
 
465
592
  program.on("command:*", (cmd) => {
@@ -467,6 +594,12 @@ Special Commands:
467
594
  program.outputHelp();
468
595
  });
469
596
 
597
+ // Show help if no command provided
598
+ if (process.argv.length === 2) {
599
+ program.outputHelp();
600
+ return program;
601
+ }
602
+
470
603
  program.parse(process.argv);
471
604
 
472
605
  function fetchComments() {
@@ -509,6 +642,150 @@ Special Commands:
509
642
 
510
643
  return comments;
511
644
  }
645
+
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;
744
+ }
745
+
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
+ }
777
+
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
+ });
786
+
787
+ return functionOpts;
788
+ }
512
789
  }
513
790
 
514
791
  function prepareCommandName(name) {
@@ -541,17 +818,17 @@ function parseDocBlock(funcName, code) {
541
818
  }
542
819
 
543
820
  function exportFn(commands) {
544
- if (!fs.existsSync(BUNOSHFILE)) {
821
+ if (!existsSync(BUNOSHFILE)) {
545
822
  console.error(`${BUNOSHFILE} file not found, can\'t export its commands.`);
546
823
  return false;
547
824
  }
548
825
 
549
- if (!fs.existsSync('package.json')) {
826
+ if (!existsSync('package.json')) {
550
827
  console.error('package.json now found, can\'t set scripts.');
551
828
  return false;
552
829
  }
553
830
 
554
- const pkg = JSON.parse(fs.readFileSync('package.json').toString());
831
+ const pkg = JSON.parse(readFileSync('package.json').toString());
555
832
  if (!pkg.scripts) {
556
833
  pkg.scripts = {};
557
834
  }
@@ -564,7 +841,7 @@ function exportFn(commands) {
564
841
 
565
842
  pkg.scripts = {...pkg.scripts, ...scripts };
566
843
 
567
- fs.writeFileSync('package.json', JSON.stringify(pkg, null, 4));
844
+ writeFileSync('package.json', JSON.stringify(pkg, null, 4));
568
845
 
569
846
  console.log('Added scripts:');
570
847
  console.log();
@@ -579,11 +856,11 @@ function exportFn(commands) {
579
856
 
580
857
  function loadNpmScripts() {
581
858
  try {
582
- if (!fs.existsSync('package.json')) {
859
+ if (!existsSync('package.json')) {
583
860
  return {};
584
861
  }
585
862
 
586
- const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
863
+ const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
587
864
  const scripts = pkg.scripts || {};
588
865
 
589
866
  // Filter out bunosh scripts (scripts that contain "bunosh")
@@ -600,3 +877,26 @@ function loadNpmScripts() {
600
877
  return {};
601
878
  }
602
879
  }
880
+
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);
888
+
889
+ if (!existsSync(homeBunoshfile)) {
890
+ return { tasks: {}, source: '' };
891
+ }
892
+
893
+ // Import the home Bunoshfile
894
+ const homeTasks = await import(homeBunoshfile);
895
+ const homeSource = readFileSync(homeBunoshfile, 'utf-8');
896
+
897
+ return { tasks: homeTasks, source: homeSource };
898
+ } catch (error) {
899
+ console.warn('Warning: Could not load personal commands:', error.message);
900
+ return { tasks: {}, source: '' };
901
+ }
902
+ }