bunosh 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +225 -328
- package/bunosh.js +173 -48
- package/index.js +3 -3
- package/package.json +1 -1
- package/src/init.js +2 -2
- package/src/io.js +90 -21
- package/src/printer.js +14 -2
- package/src/program.js +321 -21
- package/src/task.js +41 -43
- package/src/tasks/exec.js +14 -2
- package/src/tasks/fetch.js +4 -3
- package/src/tasks/shell.js +37 -5
- package/src/open-editor.js +0 -95
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
256
|
-
|
|
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 &&
|
|
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 (!
|
|
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 (!
|
|
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(
|
|
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
|
-
|
|
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 (!
|
|
859
|
+
if (!existsSync('package.json')) {
|
|
583
860
|
return {};
|
|
584
861
|
}
|
|
585
862
|
|
|
586
|
-
const pkg = JSON.parse(
|
|
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
|
+
}
|
package/src/task.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
2
|
import Printer from './printer.js';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// Use global objects created in bunosh.js
|
|
5
|
+
export const TaskStatus = globalThis._bunoshGlobalTaskStatus || {
|
|
5
6
|
RUNNING: 'running',
|
|
6
7
|
FAIL: 'fail',
|
|
7
8
|
SUCCESS: 'success',
|
|
8
9
|
WARNING: 'warning'
|
|
9
10
|
};
|
|
10
11
|
|
|
12
|
+
// Initialize local array and also keep global synced
|
|
11
13
|
export const tasksExecuted = [];
|
|
12
14
|
export const runningTasks = new Map();
|
|
13
15
|
|
|
@@ -45,34 +47,9 @@ export function prints() {
|
|
|
45
47
|
globalSilenceMode = false;
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
const startTime = Date.now();
|
|
49
|
-
|
|
50
|
-
process.on('exit', (code) => {
|
|
51
|
-
if (!process.env.BUNOSH_COMMAND_STARTED) return;
|
|
52
|
-
|
|
53
|
-
const totalTime = Date.now() - startTime;
|
|
54
|
-
const tasksFailed = tasksExecuted.filter(ti => ti.result?.status === TaskStatus.FAIL).length;
|
|
55
|
-
const tasksWarning = tasksExecuted.filter(ti => ti.result?.status === TaskStatus.WARNING).length;
|
|
56
|
-
|
|
57
|
-
// Check if we're in test environment
|
|
58
|
-
const isTestEnvironment = process.env.NODE_ENV === 'test' ||
|
|
59
|
-
typeof Bun?.jest !== 'undefined' ||
|
|
60
|
-
process.argv.some(arg => arg.includes('vitest') || arg.includes('jest') || arg.includes('--test') || arg.includes('test:'));
|
|
61
|
-
|
|
62
|
-
// Set exit code to 1 if any tasks failed AND we're not in ignoreFailures mode AND not in test environment
|
|
63
|
-
// Note: if stopOnFailuresMode is true, we would have already exited immediately
|
|
64
|
-
if (tasksFailed > 0 && !stopOnFailuresMode && !isTestEnvironment) {
|
|
65
|
-
process.exitCode = 1;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const finalExitCode = (tasksFailed > 0 && !stopOnFailuresMode && !isTestEnvironment) ? 1 : code;
|
|
69
|
-
const success = finalExitCode === 0;
|
|
70
|
-
|
|
71
|
-
console.log(`\n🍲 ${success ? '' : 'FAIL '}Exit Code: ${finalExitCode} | Tasks: ${tasksExecuted.length}${tasksFailed ? ` | Failed: ${tasksFailed}` : ''}${tasksWarning ? ` | Warnings: ${tasksWarning}` : ''} | Time: ${totalTime}ms`);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
50
|
export function getRunningTaskCount() {
|
|
75
|
-
|
|
51
|
+
// Only count top-level tasks (tasks without a parent)
|
|
52
|
+
return Array.from(runningTasks.values()).filter(task => !task.parentId).length;
|
|
76
53
|
}
|
|
77
54
|
|
|
78
55
|
export function getCurrentTaskId() {
|
|
@@ -80,14 +57,32 @@ export function getCurrentTaskId() {
|
|
|
80
57
|
}
|
|
81
58
|
|
|
82
59
|
export function getTaskPrefix(taskId) {
|
|
83
|
-
const
|
|
60
|
+
const taskInfo = runningTasks.get(taskId);
|
|
61
|
+
if (!taskInfo) return '';
|
|
62
|
+
|
|
63
|
+
// Only show prefixes for top-level tasks when there are multiple top-level tasks
|
|
64
|
+
if (taskInfo.parentId) {
|
|
65
|
+
// This is a child task, never show prefix
|
|
66
|
+
return '';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// For top-level tasks, calculate position among other top-level tasks
|
|
70
|
+
const topLevelTasks = Array.from(runningTasks.values()).filter(task => !task.parentId);
|
|
71
|
+
const taskNumber = topLevelTasks.findIndex(task => task.id === taskId) + 1;
|
|
84
72
|
return getRunningTaskCount() > 1 ? `❰${taskNumber}❱` : '';
|
|
85
73
|
}
|
|
86
74
|
|
|
87
|
-
|
|
88
|
-
|
|
75
|
+
|
|
76
|
+
export function createTaskInfo(name, parentId = null) {
|
|
77
|
+
const taskInfo = new TaskInfo(name, Date.now(), TaskStatus.RUNNING, parentId);
|
|
89
78
|
runningTasks.set(taskInfo.id, taskInfo);
|
|
90
79
|
tasksExecuted.push(taskInfo);
|
|
80
|
+
|
|
81
|
+
// Also add to global array for exit handler
|
|
82
|
+
if (globalThis._bunoshGlobalTasksExecuted) {
|
|
83
|
+
globalThis._bunoshGlobalTasksExecuted.push(taskInfo);
|
|
84
|
+
}
|
|
85
|
+
|
|
91
86
|
return taskInfo;
|
|
92
87
|
}
|
|
93
88
|
|
|
@@ -106,11 +101,12 @@ export function finishTaskInfo(taskInfo, success = true, error = null, output =
|
|
|
106
101
|
}
|
|
107
102
|
|
|
108
103
|
export class TaskInfo {
|
|
109
|
-
constructor(name, startTime, status) {
|
|
104
|
+
constructor(name, startTime, status, parentId = null) {
|
|
110
105
|
this.id = `task-${++taskCounter}-${Math.random().toString(36).substring(7)}`;
|
|
111
106
|
this.name = name;
|
|
112
107
|
this.startTime = startTime;
|
|
113
108
|
this.status = status;
|
|
109
|
+
this.parentId = parentId;
|
|
114
110
|
}
|
|
115
111
|
}
|
|
116
112
|
|
|
@@ -120,10 +116,7 @@ export async function tryTask(name, fn, isSilent = false) {
|
|
|
120
116
|
name = fn.toString().slice(0, 50).replace(/\s+/g, ' ').trim();
|
|
121
117
|
}
|
|
122
118
|
|
|
123
|
-
const taskInfo =
|
|
124
|
-
|
|
125
|
-
tasksExecuted.push(taskInfo);
|
|
126
|
-
runningTasks.set(taskInfo.id, taskInfo);
|
|
119
|
+
const taskInfo = createTaskInfo(name);
|
|
127
120
|
|
|
128
121
|
const shouldPrint = !globalSilenceMode && !isSilent;
|
|
129
122
|
const printer = new Printer('task', taskInfo.id);
|
|
@@ -166,10 +159,7 @@ export async function task(name, fn, isSilent = false) {
|
|
|
166
159
|
name = fn.toString().slice(0, 50).replace(/\s+/g, ' ').trim();
|
|
167
160
|
}
|
|
168
161
|
|
|
169
|
-
const taskInfo =
|
|
170
|
-
|
|
171
|
-
tasksExecuted.push(taskInfo);
|
|
172
|
-
runningTasks.set(taskInfo.id, taskInfo);
|
|
162
|
+
const taskInfo = createTaskInfo(name);
|
|
173
163
|
|
|
174
164
|
const shouldPrint = !globalSilenceMode && !isSilent;
|
|
175
165
|
const printer = new Printer('task', taskInfo.id);
|
|
@@ -183,11 +173,16 @@ export async function task(name, fn, isSilent = false) {
|
|
|
183
173
|
const endTime = Date.now();
|
|
184
174
|
const duration = endTime - taskInfo.startTime;
|
|
185
175
|
|
|
176
|
+
// Check if result is a TaskResult instance
|
|
177
|
+
if (result && result.constructor && result.constructor.name === 'TaskResult') {
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
|
|
186
181
|
taskInfo.status = TaskStatus.SUCCESS;
|
|
187
182
|
taskInfo.duration = duration;
|
|
188
183
|
taskInfo.result = { status: TaskStatus.SUCCESS, output: result };
|
|
189
184
|
|
|
190
|
-
|
|
185
|
+
printer.finish(name);
|
|
191
186
|
runningTasks.delete(taskInfo.id);
|
|
192
187
|
|
|
193
188
|
return result;
|
|
@@ -199,7 +194,7 @@ export async function task(name, fn, isSilent = false) {
|
|
|
199
194
|
taskInfo.duration = duration;
|
|
200
195
|
taskInfo.result = { status: TaskStatus.FAIL, output: err.message };
|
|
201
196
|
|
|
202
|
-
|
|
197
|
+
printer.error(name, err);
|
|
203
198
|
runningTasks.delete(taskInfo.id);
|
|
204
199
|
|
|
205
200
|
// Don't exit during testing
|
|
@@ -216,11 +211,14 @@ export async function task(name, fn, isSilent = false) {
|
|
|
216
211
|
if (stopFailToggle && !isTestEnvironment) {
|
|
217
212
|
process.exit(1);
|
|
218
213
|
}
|
|
219
|
-
|
|
214
|
+
|
|
220
215
|
throw err;
|
|
221
216
|
}
|
|
222
217
|
}
|
|
223
218
|
|
|
219
|
+
// Add try method to task function
|
|
220
|
+
task.try = tryTask;
|
|
221
|
+
|
|
224
222
|
export class SilentTaskWrapper {
|
|
225
223
|
constructor() {
|
|
226
224
|
this.silent = true;
|
package/src/tasks/exec.js
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
|
-
import { TaskResult, createTaskInfo, finishTaskInfo } from '../task.js';
|
|
1
|
+
import { TaskResult, createTaskInfo, finishTaskInfo, getCurrentTaskId } from '../task.js';
|
|
2
2
|
import Printer from '../printer.js';
|
|
3
3
|
|
|
4
4
|
const isBun = typeof Bun !== 'undefined';
|
|
5
5
|
|
|
6
6
|
export default function exec(strings, ...values) {
|
|
7
|
+
// Check if called as regular function instead of template literal
|
|
8
|
+
if (!Array.isArray(strings)) {
|
|
9
|
+
// If first argument is a string, treat it as the command
|
|
10
|
+
if (typeof strings === 'string') {
|
|
11
|
+
strings = [strings];
|
|
12
|
+
values = [];
|
|
13
|
+
} else {
|
|
14
|
+
throw new Error('exec() must be called as a template literal: exec`command` or exec("command")');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
7
18
|
const cmd = strings.reduce((accumulator, str, i) => {
|
|
8
19
|
return accumulator + str + (values[i] || '');
|
|
9
20
|
}, '');
|
|
@@ -16,7 +27,8 @@ export default function exec(strings, ...values) {
|
|
|
16
27
|
if (cwd) extraInfo.cwd = cwd;
|
|
17
28
|
if (envs) extraInfo.env = envs;
|
|
18
29
|
|
|
19
|
-
const
|
|
30
|
+
const currentTaskId = getCurrentTaskId();
|
|
31
|
+
const taskInfo = createTaskInfo(cmd, currentTaskId);
|
|
20
32
|
const printer = new Printer('exec', taskInfo.id);
|
|
21
33
|
printer.start(cmd, extraInfo);
|
|
22
34
|
|
package/src/tasks/fetch.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TaskResult, createTaskInfo, finishTaskInfo } from '../task.js';
|
|
1
|
+
import { TaskResult, createTaskInfo, finishTaskInfo, getCurrentTaskId } from '../task.js';
|
|
2
2
|
import Printer from '../printer.js';
|
|
3
3
|
|
|
4
4
|
export default async function httpFetch() {
|
|
@@ -6,7 +6,8 @@ export default async function httpFetch() {
|
|
|
6
6
|
const method = arguments[1]?.method || 'GET';
|
|
7
7
|
const taskName = `${method} ${url}`;
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const currentTaskId = getCurrentTaskId();
|
|
10
|
+
const taskInfo = createTaskInfo(taskName, currentTaskId);
|
|
10
11
|
const printer = new Printer('fetch', taskInfo.id);
|
|
11
12
|
printer.start(taskName);
|
|
12
13
|
|
|
@@ -20,7 +21,7 @@ export default async function httpFetch() {
|
|
|
20
21
|
const lines = textDecoder.decode(chunk, { stream: true }).toString().split('\n');
|
|
21
22
|
for (const line of lines) {
|
|
22
23
|
if (line.trim()) {
|
|
23
|
-
printer.
|
|
24
|
+
printer.output(line);
|
|
24
25
|
output += line + '\n';
|
|
25
26
|
}
|
|
26
27
|
}
|