bunosh 0.3.2 → 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
+ }
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
- export const TaskStatus = {
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
- return runningTasks.size;
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 taskNumber = Array.from(runningTasks.keys()).indexOf(taskId) + 1;
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
- export function createTaskInfo(name) {
88
- const taskInfo = new TaskInfo(name, Date.now(), TaskStatus.RUNNING);
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 = new TaskInfo(name, Date.now(), TaskStatus.RUNNING);
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 = new TaskInfo(name, Date.now(), TaskStatus.RUNNING);
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
- if (shouldPrint) printer.finish(name);
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
- if (shouldPrint) printer.error(name, err);
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 taskInfo = createTaskInfo(cmd);
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
 
@@ -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 taskInfo = createTaskInfo(taskName);
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.print(line, 'output');
24
+ printer.output(line);
24
25
  output += line + '\n';
25
26
  }
26
27
  }