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/bunosh.js CHANGED
@@ -1,63 +1,147 @@
1
1
  #!/usr/bin/env bun
2
+
3
+ // Set up global variables BEFORE any imports
4
+ globalThis._bunoshStartTime = Date.now();
5
+ globalThis._bunoshCommandCompleted = false;
6
+ globalThis._bunoshGlobalTasksExecuted = [];
7
+ globalThis._bunoshGlobalTaskStatus = {
8
+ RUNNING: 'running',
9
+ FAIL: 'fail',
10
+ SUCCESS: 'success',
11
+ WARNING: 'warning'
12
+ };
13
+
14
+ // Now import modules
15
+
2
16
  import program, { BUNOSHFILE, banner } from "./src/program.js";
3
17
  import { existsSync, readFileSync, statSync } from "fs";
4
18
  import init from "./src/init.js";
5
19
  import path from "path";
6
- import color from "chalk";
7
- import './index.js';
8
-
9
- // Parse --bunoshfile flag before importing tasks
10
- const bunoshfileIndex = process.argv.indexOf('--bunoshfile');
11
- let customBunoshfile = null;
12
- if (bunoshfileIndex !== -1 && bunoshfileIndex + 1 < process.argv.length) {
13
- customBunoshfile = process.argv[bunoshfileIndex + 1];
14
- // Remove the flag and its value from process.argv so it doesn't interfere with command parsing
15
- process.argv.splice(bunoshfileIndex, 2);
16
- }
17
20
 
18
- let tasksFile;
19
- if (customBunoshfile) {
20
- const resolvedPath = path.isAbsolute(customBunoshfile) ? customBunoshfile : path.resolve(customBunoshfile);
21
- // If it's a directory, append the default BUNOSHFILE
22
- if (existsSync(resolvedPath) && statSync(resolvedPath).isDirectory()) {
23
- tasksFile = path.join(resolvedPath, BUNOSHFILE);
24
- // Change working directory to the bunoshfile directory
25
- process.chdir(resolvedPath);
21
+
22
+ async function main() {
23
+
24
+ // Parse --bunoshfile flag before importing tasks
25
+ const bunoshfileIndex = process.argv.indexOf('--bunoshfile');
26
+ let customBunoshfile = null;
27
+ if (bunoshfileIndex !== -1 && bunoshfileIndex + 1 < process.argv.length) {
28
+ customBunoshfile = process.argv[bunoshfileIndex + 1];
29
+ // Remove the flag and its value from process.argv so it doesn't interfere with command parsing
30
+ process.argv.splice(bunoshfileIndex, 2);
31
+ }
32
+
33
+ let tasksFile;
34
+ if (customBunoshfile) {
35
+ const resolvedPath = path.isAbsolute(customBunoshfile) ? customBunoshfile : path.resolve(customBunoshfile);
36
+ // If it's a directory, append the default BUNOSHFILE
37
+ if (existsSync(resolvedPath) && statSync(resolvedPath).isDirectory()) {
38
+ tasksFile = path.join(resolvedPath, BUNOSHFILE);
39
+ // Change working directory to the bunoshfile directory
40
+ process.chdir(resolvedPath);
41
+ } else {
42
+ tasksFile = resolvedPath;
43
+ // Change working directory to the bunoshfile's directory
44
+ process.chdir(path.dirname(resolvedPath));
45
+ }
26
46
  } else {
27
- tasksFile = resolvedPath;
28
- // Change working directory to the bunoshfile's directory
29
- process.chdir(path.dirname(resolvedPath));
47
+ tasksFile = path.join(process.cwd(), BUNOSHFILE);
48
+ }
49
+
50
+ // Handle -e flag for executing JavaScript code
51
+ const eFlagIndex = process.argv.indexOf('-e');
52
+ if (eFlagIndex !== -1) {
53
+ let jsCode = '';
54
+
55
+ // Check if code is provided as argument
56
+ if (eFlagIndex + 1 < process.argv.length && !process.argv[eFlagIndex + 1].startsWith('-')) {
57
+ jsCode = process.argv[eFlagIndex + 1];
58
+ } else if (!process.stdin.isTTY) {
59
+ // Read from stdin only if it's not a TTY (i.e., it's being piped)
60
+ const chunks = [];
61
+ for await (const chunk of process.stdin) {
62
+ chunks.push(chunk);
63
+ }
64
+ jsCode = Buffer.concat(chunks).toString('utf8').trim();
65
+ }
66
+
67
+ if (!jsCode) {
68
+ console.error('No JavaScript code provided');
69
+ process.exit(1);
70
+ }
71
+
72
+ try {
73
+ // Import bunosh globals before executing JavaScript
74
+ await import('./index.js');
75
+
76
+ // Make bunosh globals available to the function by creating wrapper functions
77
+ for (const [key, value] of Object.entries(global.bunosh)) {
78
+ if (typeof value === 'function') {
79
+ // For template literal tag functions like exec and shell, create a wrapper
80
+ // that allows them to be called as regular functions with a string
81
+ if (key === 'exec' || key === 'writeToFile') {
82
+ globalThis[key] = (str) => {
83
+ // If called as a regular function with a string, convert to template literal call
84
+ if (typeof str === 'string') {
85
+ return value([str]);
86
+ }
87
+ // Otherwise call normally
88
+ return value(str, ...Array.from(arguments).slice(1));
89
+ };
90
+ } else if (key === 'shell') {
91
+ // Shell function has special handling for string arguments - it falls back to exec
92
+ globalThis[key] = value;
93
+ } else {
94
+ globalThis[key] = value;
95
+ }
96
+ }
97
+ }
98
+
99
+ // Execute the JavaScript code with bunosh globals available
100
+ const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
101
+ const func = new AsyncFunction(jsCode);
102
+ await func();
103
+ return;
104
+ } catch (error) {
105
+ console.error('Error executing JavaScript:', error.message);
106
+ process.exit(1);
107
+ }
30
108
  }
31
- } else {
32
- tasksFile = path.join(process.cwd(), BUNOSHFILE);
33
- }
34
109
 
35
- if (!existsSync(tasksFile)) {
36
- banner();
110
+ if (!existsSync(tasksFile)) {
111
+ banner();
37
112
 
38
- if (process.argv.includes('init')) {
39
- init();
40
- process.exit(0);
113
+ if (process.argv.includes('init')) {
114
+ init();
115
+ process.exit(0);
116
+ }
117
+
118
+ console.log();
119
+ console.error(`Bunoshfile not found: ${tasksFile}`);
120
+ console.log(customBunoshfile ?
121
+ `Run \`bunosh init\` in the directory or specify a valid --bunoshfile path` :
122
+ "Run `bunosh init` to create a new Bunoshfile here")
123
+ console.log();
124
+ process.exit(1);
41
125
  }
42
126
 
43
- console.log();
44
- console.error(`Bunoshfile not found: ${tasksFile}`);
45
- console.log(customBunoshfile ?
46
- `Run \`bunosh init\` in the directory or specify a valid --bunoshfile path` :
47
- "Run `bunosh init` to create a new Bunoshfile here")
48
- console.log();
49
- process.exit(1);
127
+ // Import bunosh globals for normal operation
128
+ await import('./index.js');
129
+
130
+ import(tasksFile).then(async (tasks) => {
131
+ try {
132
+ const source = readFileSync(tasksFile, "utf-8");
133
+ await program(tasks, source);
134
+ } catch (error) {
135
+ handleBunoshfileError(error, tasksFile);
136
+ }
137
+ }).catch((error) => {
138
+ handleBunoshfileError(error, tasksFile);
139
+ });
50
140
  }
51
141
 
52
- import(tasksFile).then((tasks) => {
53
- try {
54
- const source = readFileSync(tasksFile, "utf-8");
55
- program(tasks, source);
56
- } catch (error) {
57
- handleBunoshfileError(error, tasksFile);
58
- }
59
- }).catch((error) => {
60
- handleBunoshfileError(error, tasksFile);
142
+ main().catch((error) => {
143
+ console.error('Fatal error:', error.message);
144
+ process.exit(1);
61
145
  });
62
146
 
63
147
  // Handle unhandled promise rejections
@@ -84,9 +168,50 @@ process.on('uncaughtException', (error) => {
84
168
  process.exit(1);
85
169
  });
86
170
 
171
+ // Handle exit for task summary
172
+ process.on('exit', (code) => {
173
+ if (!process.env.BUNOSH_COMMAND_STARTED) return;
174
+
175
+ // Don't print summary if exit was due to stdin closing during an ask operation
176
+ // This prevents duplicate output when ask commands don't receive all required input
177
+ if (globalThis._bunoshInAskOperation && code === 0) {
178
+ return;
179
+ }
180
+
181
+ // Access global values directly
182
+ const tasksExecuted = globalThis._bunoshGlobalTasksExecuted || [];
183
+ const TaskStatus = globalThis._bunoshGlobalTaskStatus || { FAIL: 'fail', WARNING: 'warning' };
184
+
185
+ // Calculate total time from when the process started
186
+ const totalTime = Date.now() - globalThis._bunoshStartTime || 0;
187
+ const tasksFailed = tasksExecuted.filter(ti => ti.result?.status === TaskStatus.FAIL).length;
188
+ const tasksWarning = tasksExecuted.filter(ti => ti.result?.status === TaskStatus.WARNING).length;
189
+
190
+ // Check if we're in test environment
191
+ const isTestEnvironment = process.env.NODE_ENV === 'test' ||
192
+ typeof Bun?.jest !== 'undefined' ||
193
+ process.argv.some(arg => arg.includes('vitest') || arg.includes('jest') || arg.includes('--test') || arg.includes('test:'));
194
+
195
+ // Set exit code to 1 if any tasks failed AND we're not in test environment
196
+ if (tasksFailed > 0 && !isTestEnvironment) {
197
+ process.exitCode = 1;
198
+ }
199
+
200
+ const finalExitCode = (tasksFailed > 0 && !isTestEnvironment) ? 1 : code;
201
+ const success = finalExitCode === 0;
202
+
203
+ // Debug: Check if this handler has already run
204
+ if (globalThis._bunoshExitHandlerCalled) {
205
+ console.log('\n[DEBUG] Exit handler already called, skipping duplicate');
206
+ return;
207
+ }
208
+ globalThis._bunoshExitHandlerCalled = true;
209
+
210
+ console.log(`\n🍲 ${success ? '' : 'FAIL '}Exit Code: ${finalExitCode} | Tasks: ${tasksExecuted.length}${tasksFailed ? ` | Failed: ${tasksFailed}` : ''}${tasksWarning ? ` | Warnings: ${tasksWarning}` : ''} | Time: ${totalTime}ms`);
211
+ });
212
+
87
213
  function handleBunoshfileError(error, filePath) {
88
- banner();
89
- console.log();
214
+ // Don't show banner for errors - it interferes with error visibility
90
215
 
91
216
  // Check for Babel parser syntax errors
92
217
  if (error.code === 'BABEL_PARSER_SYNTAX_ERROR' ||
package/index.js CHANGED
@@ -5,9 +5,9 @@ import writeToFile from "./src/tasks/writeToFile.js";
5
5
  import copyFile from "./src/tasks/copyFile.js";
6
6
  import ai from "./src/tasks/ai.js";
7
7
  import { ask, yell, say } from "./src/io.js";
8
- import { task, tryTask, stopOnFail, ignoreFail, stopOnFailures, ignoreFailures, silence, prints, silent } from "./src/task.js";
8
+ import { task, tryTask, stopOnFail, ignoreFail, stopOnFailures, ignoreFailures, silence, prints, silent, TaskResult } from "./src/task.js";
9
9
 
10
- export { exec, shell, fetch, writeToFile, copyFile, ai, ask, yell, say, task, tryTask, stopOnFail, ignoreFail, stopOnFailures, ignoreFailures, silence, prints, silent };
10
+ export { exec, shell, fetch, writeToFile, copyFile, ai, ask, yell, say, task, tryTask, stopOnFail, ignoreFail, stopOnFailures, ignoreFailures, silence, prints, silent, TaskResult };
11
11
 
12
12
  export function buildCmd(cmd) {
13
13
  return function (args) {
@@ -28,12 +28,12 @@ global.bunosh = {
28
28
  stopOnFail,
29
29
  ignoreFail,
30
30
  task,
31
- try: tryTask,
32
31
  stopOnFailures,
33
32
  ignoreFailures,
34
33
  silence,
35
34
  prints,
36
35
  silent,
36
+ TaskResult,
37
37
  buildCmd,
38
38
  $: exec,
39
39
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunosh",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "module": "index.js",
6
6
  "bin": {
package/src/init.js CHANGED
@@ -4,9 +4,9 @@ import fs from 'fs';
4
4
 
5
5
  const template = `
6
6
  // Bunosh CLI required to execute tasks from this file
7
- // Get it here -> https://github.com/DavertMik/bunosh/releases
7
+ // Get it here => https://buno.sh
8
8
 
9
- const { exec, fetch, writeToFile, task, ai } = global.bunosh;
9
+ const { exec, shell, fetch, writeToFile, task, ai } = global.bunosh;
10
10
 
11
11
  // input/output
12
12
  const { say, ask, yell } = global.bunosh;
package/src/io.js CHANGED
@@ -7,6 +7,8 @@ export function say(...args) {
7
7
  }
8
8
 
9
9
  export async function ask(question, defaultValueOrOptions = {}, options = {}) {
10
+ // Track that we're in an ask operation to prevent duplicate exit summaries
11
+ globalThis._bunoshInAskOperation = true;
10
12
  // Smart parameter detection
11
13
  let opts = {};
12
14
 
@@ -37,38 +39,105 @@ export async function ask(question, defaultValueOrOptions = {}, options = {}) {
37
39
  return await askWithChoices(question, opts);
38
40
  }
39
41
 
40
- const answers = await inquirer.prompt({ name: question, message: question, ...opts })
41
- return Object.values(answers)[0];
42
+ try {
43
+ const answers = await inquirer.prompt({ name: question, message: question, ...opts })
44
+ return Object.values(answers)[0];
45
+ } finally {
46
+ // Reset ask operation flag
47
+ globalThis._bunoshInAskOperation = false;
48
+ }
42
49
  }
43
50
 
44
51
  async function askWithEditor(question, opts = {}) {
45
- const answers = await inquirer.prompt({
46
- name: question,
47
- message: question,
48
- type: 'editor',
49
- ...opts
50
- });
51
- return Object.values(answers)[0];
52
+ globalThis._bunoshInAskOperation = true;
53
+ try {
54
+ const answers = await inquirer.prompt({
55
+ name: question,
56
+ message: question,
57
+ type: 'editor',
58
+ ...opts
59
+ });
60
+ return Object.values(answers)[0];
61
+ } finally {
62
+ globalThis._bunoshInAskOperation = false;
63
+ }
52
64
  }
53
65
 
54
66
  async function askWithChoices(question, opts = {}) {
67
+ globalThis._bunoshInAskOperation = true;
55
68
  const promptType = opts.multiple ? 'checkbox' : 'list';
56
69
 
57
- const answers = await inquirer.prompt({
58
- name: question,
59
- message: question,
60
- type: promptType,
61
- choices: opts.choices,
62
- ...opts
63
- });
64
-
65
- return Object.values(answers)[0];
70
+ try {
71
+ const answers = await inquirer.prompt({
72
+ name: question,
73
+ message: question,
74
+ type: promptType,
75
+ choices: opts.choices,
76
+ ...opts
77
+ });
78
+
79
+ return Object.values(answers)[0];
80
+ } finally {
81
+ globalThis._bunoshInAskOperation = false;
82
+ }
66
83
  }
67
84
 
68
85
  export function yell(text) {
69
- console.log();
86
+ // Always use boxed capitalized format
87
+ const upperText = text.toUpperCase();
88
+ const boxText = createBoxedText(upperText);
89
+ console.log(boxText);
90
+ }
70
91
 
71
- console.log(chalk.bold.yellow(cprint(text, { symbol: '■' })));
92
+ function createGradientAscii(asciiArt) {
93
+ const lines = asciiArt.split('\n');
94
+ const colors = [
95
+ chalk.bold.yellow,
96
+ chalk.bold.green,
97
+ chalk.bold.greenBright,
98
+ chalk.bold.cyan,
99
+ chalk.bold.blue
100
+ ];
101
+
102
+ return lines.map((line, index) => {
103
+ // Create smooth gradient by interpolating between colors
104
+ const progress = index / (lines.length - 1);
105
+ const colorIndex = progress * (colors.length - 1);
106
+ const lowerIndex = Math.floor(colorIndex);
107
+ const upperIndex = Math.min(lowerIndex + 1, colors.length - 1);
108
+ const factor = colorIndex - lowerIndex;
109
+
110
+ // For smoother transition, we'll use the closest color
111
+ const color = factor < 0.5 ? colors[lowerIndex] : colors[upperIndex];
112
+ return color(line);
113
+ }).join('\n');
114
+ }
72
115
 
73
- console.log();
116
+ function createBoxedText(text) {
117
+ const maxLength = Math.max(text.length, 10);
118
+ const totalWidth = maxLength + 6; // 6 = 3 spaces padding on each side
119
+ const leftPadding = Math.floor((totalWidth - text.length) / 2);
120
+ const rightPadding = totalWidth - text.length - leftPadding;
121
+
122
+ // Use Unicode box drawing characters for a nice look
123
+ const topLeft = '╭';
124
+ const topRight = '╮';
125
+ const bottomLeft = '╰';
126
+ const bottomRight = '╯';
127
+ const horizontal = '─';
128
+ const vertical = '│';
129
+
130
+ const horizontalBorder = topLeft + horizontal.repeat(totalWidth) + topRight;
131
+ const emptyLine = vertical + ' '.repeat(totalWidth) + vertical;
132
+ const textLine = vertical + ' '.repeat(leftPadding) + chalk.bold(text) + ' '.repeat(rightPadding) + vertical;
133
+
134
+ return [
135
+ chalk.bold.cyan(horizontalBorder),
136
+ chalk.cyan(emptyLine),
137
+ chalk.cyan(emptyLine),
138
+ chalk.cyan(textLine),
139
+ chalk.cyan(emptyLine),
140
+ chalk.cyan(emptyLine),
141
+ chalk.bold.cyan(bottomLeft + horizontal.repeat(totalWidth) + bottomRight)
142
+ ].join('\n');
74
143
  }
package/src/printer.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import { createFormatter } from './formatters/factory.js';
3
- import { getTaskPrefix } from './task.js';
3
+ import { getTaskPrefix, runningTasks } from './task.js';
4
4
 
5
5
  export class Printer {
6
6
  constructor(taskType, taskId = null) {
@@ -21,9 +21,21 @@ export class Printer {
21
21
  extra.duration = Date.now() - this.startTime;
22
22
  }
23
23
 
24
+ // Get task info to check for parent task
25
+ let displayTaskName = taskName;
26
+ if (this.taskId) {
27
+ const taskInfo = runningTasks.get(this.taskId);
28
+ if (taskInfo && taskInfo.parentId) {
29
+ const parentTask = runningTasks.get(taskInfo.parentId);
30
+ if (parentTask) {
31
+ displayTaskName = `${parentTask.name} > ${taskName}`;
32
+ }
33
+ }
34
+ }
35
+
24
36
  // Add task prefix for parallel tasks
25
37
  const prefix = this.taskId ? getTaskPrefix(this.taskId) : '';
26
- const prefixedTaskName = prefix ? `${prefix} ${taskName}` : taskName;
38
+ const prefixedTaskName = prefix ? `${prefix} ${displayTaskName}` : displayTaskName;
27
39
 
28
40
  const output = this.formatter.format(prefixedTaskName, status, this.taskType, extra);
29
41
  if (output) {