bunosh 0.3.1 → 0.3.2

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
@@ -60,6 +60,30 @@ import(tasksFile).then((tasks) => {
60
60
  handleBunoshfileError(error, tasksFile);
61
61
  });
62
62
 
63
+ // Handle unhandled promise rejections
64
+ process.on('unhandledRejection', (reason, promise) => {
65
+ if (!process.env.BUNOSH_COMMAND_STARTED) return;
66
+
67
+ console.error('\n❌ Unhandled Promise Rejection:');
68
+ console.error(reason instanceof Error ? reason.message : reason);
69
+ if (reason instanceof Error && reason.stack && process.env.BUNOSH_DEBUG) {
70
+ console.error(reason.stack);
71
+ }
72
+ process.exit(1);
73
+ });
74
+
75
+ // Handle uncaught exceptions
76
+ process.on('uncaughtException', (error) => {
77
+ if (!process.env.BUNOSH_COMMAND_STARTED) return;
78
+
79
+ console.error('\n❌ Uncaught Exception:');
80
+ console.error(error.message);
81
+ if (error.stack && process.env.BUNOSH_DEBUG) {
82
+ console.error(error.stack);
83
+ }
84
+ process.exit(1);
85
+ });
86
+
63
87
  function handleBunoshfileError(error, filePath) {
64
88
  banner();
65
89
  console.log();
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, stopOnFail, ignoreFail } from "./src/task.js";
8
+ import { task, tryTask, stopOnFail, ignoreFail, stopOnFailures, ignoreFailures, silence, prints, silent } from "./src/task.js";
9
9
 
10
- export { exec, shell, fetch, writeToFile, copyFile, ai, ask, yell, say, task, stopOnFail, ignoreFail };
10
+ export { exec, shell, fetch, writeToFile, copyFile, ai, ask, yell, say, task, tryTask, stopOnFail, ignoreFail, stopOnFailures, ignoreFailures, silence, prints, silent };
11
11
 
12
12
  export function buildCmd(cmd) {
13
13
  return function (args) {
@@ -28,6 +28,12 @@ global.bunosh = {
28
28
  stopOnFail,
29
29
  ignoreFail,
30
30
  task,
31
+ try: tryTask,
32
+ stopOnFailures,
33
+ ignoreFailures,
34
+ silence,
35
+ prints,
36
+ silent,
31
37
  buildCmd,
32
38
  $: exec,
33
39
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunosh",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "module": "index.js",
6
6
  "bin": {
@@ -5,6 +5,7 @@ const STATUS_CONFIG = {
5
5
  start: { icon: '▶', color: 'blue' },
6
6
  finish: { icon: '✓', color: 'green' },
7
7
  error: { icon: '✗', color: 'red' },
8
+ warning: { icon: '⚠', color: 'yellow' },
8
9
  output: { icon: ' ', color: 'white' },
9
10
  info: { icon: ' ', color: 'dim' }
10
11
  };
@@ -22,6 +22,10 @@ export class GitHubActionsFormatter extends BaseFormatter {
22
22
  const errorDetails = extra.error ? ` - ${extra.error}` : '';
23
23
  return `::endgroup::\n::error::❌ ${fullTaskName}${errorDetails}`;
24
24
 
25
+ case 'warning':
26
+ const warningDetails = extra.error ? ` - ${extra.error}` : '';
27
+ return `::endgroup::\n::warning::⚠️ ${fullTaskName}${warningDetails}`;
28
+
25
29
  case 'output':
26
30
  return taskName;
27
31
 
package/src/printer.js CHANGED
@@ -65,6 +65,17 @@ export class Printer {
65
65
  this.print(taskName, 'error', extra);
66
66
  }
67
67
 
68
+ warning(taskName, error = null, extra = {}) {
69
+ if (this.startTimeout) {
70
+ clearTimeout(this.startTimeout);
71
+ this.startTimeout = null;
72
+ }
73
+ if (error) {
74
+ extra.error = typeof error === 'string' ? error : error.message;
75
+ }
76
+ this.print(taskName, 'warning', extra);
77
+ }
78
+
68
79
  output(line, isError = false) {
69
80
  if (!line.trim()) return;
70
81
 
package/src/task.js CHANGED
@@ -4,7 +4,8 @@ import Printer from './printer.js';
4
4
  export const TaskStatus = {
5
5
  RUNNING: 'running',
6
6
  FAIL: 'fail',
7
- SUCCESS: 'success'
7
+ SUCCESS: 'success',
8
+ WARNING: 'warning'
8
9
  };
9
10
 
10
11
  export const tasksExecuted = [];
@@ -12,6 +13,7 @@ export const runningTasks = new Map();
12
13
 
13
14
  let taskCounter = 0;
14
15
  let stopFailToggle = true;
16
+ let globalSilenceMode = false;
15
17
  const asyncLocalStorage = new AsyncLocalStorage();
16
18
 
17
19
 
@@ -23,16 +25,50 @@ export function ignoreFail(enable = true) {
23
25
  stopFailToggle = !enable;
24
26
  }
25
27
 
28
+ // Global failure mode control
29
+ // true = stop on failures (exit with code 1), false = continue on failures
30
+ let stopOnFailuresMode = false;
31
+
32
+ export function ignoreFailures() {
33
+ stopOnFailuresMode = false;
34
+ }
35
+
36
+ export function stopOnFailures() {
37
+ stopOnFailuresMode = true;
38
+ }
39
+
40
+ export function silence() {
41
+ globalSilenceMode = true;
42
+ }
43
+
44
+ export function prints() {
45
+ globalSilenceMode = false;
46
+ }
47
+
26
48
  const startTime = Date.now();
27
49
 
28
50
  process.on('exit', (code) => {
29
51
  if (!process.env.BUNOSH_COMMAND_STARTED) return;
30
52
 
31
53
  const totalTime = Date.now() - startTime;
32
- const success = code === 0;
33
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;
34
70
 
35
- console.log(`\n🍲 ${success ? '' : 'FAIL '}Exit Code: ${code} | Tasks: ${tasksExecuted.length}${tasksFailed ? ` | Failed: ${tasksFailed}` : ''} | Time: ${totalTime}ms`);
71
+ console.log(`\n🍲 ${success ? '' : 'FAIL '}Exit Code: ${finalExitCode} | Tasks: ${tasksExecuted.length}${tasksFailed ? ` | Failed: ${tasksFailed}` : ''}${tasksWarning ? ` | Warnings: ${tasksWarning}` : ''} | Time: ${totalTime}ms`);
36
72
  });
37
73
 
38
74
  export function getRunningTaskCount() {
@@ -78,7 +114,53 @@ export class TaskInfo {
78
114
  }
79
115
  }
80
116
 
81
- export async function task(name, fn) {
117
+ export async function tryTask(name, fn, isSilent = false) {
118
+ if (!fn) {
119
+ fn = name;
120
+ name = fn.toString().slice(0, 50).replace(/\s+/g, ' ').trim();
121
+ }
122
+
123
+ const taskInfo = new TaskInfo(name, Date.now(), TaskStatus.RUNNING);
124
+
125
+ tasksExecuted.push(taskInfo);
126
+ runningTasks.set(taskInfo.id, taskInfo);
127
+
128
+ const shouldPrint = !globalSilenceMode && !isSilent;
129
+ const printer = new Printer('task', taskInfo.id);
130
+ if (shouldPrint) printer.start(name);
131
+
132
+ try {
133
+ const result = await asyncLocalStorage.run(taskInfo.id, async () => {
134
+ return await Promise.resolve(fn());
135
+ });
136
+
137
+ const endTime = Date.now();
138
+ const duration = endTime - taskInfo.startTime;
139
+
140
+ taskInfo.status = TaskStatus.SUCCESS;
141
+ taskInfo.duration = duration;
142
+ taskInfo.result = { status: TaskStatus.SUCCESS, output: result };
143
+
144
+ if (shouldPrint) printer.finish(name);
145
+ runningTasks.delete(taskInfo.id);
146
+
147
+ return true;
148
+ } catch (err) {
149
+ const endTime = Date.now();
150
+ const duration = endTime - taskInfo.startTime;
151
+
152
+ taskInfo.status = TaskStatus.WARNING;
153
+ taskInfo.duration = duration;
154
+ taskInfo.result = { status: TaskStatus.WARNING, output: err.message };
155
+
156
+ if (shouldPrint) printer.warning(name, err);
157
+ runningTasks.delete(taskInfo.id);
158
+
159
+ return false;
160
+ }
161
+ }
162
+
163
+ export async function task(name, fn, isSilent = false) {
82
164
  if (!fn) {
83
165
  fn = name;
84
166
  name = fn.toString().slice(0, 50).replace(/\s+/g, ' ').trim();
@@ -89,8 +171,9 @@ export async function task(name, fn) {
89
171
  tasksExecuted.push(taskInfo);
90
172
  runningTasks.set(taskInfo.id, taskInfo);
91
173
 
174
+ const shouldPrint = !globalSilenceMode && !isSilent;
92
175
  const printer = new Printer('task', taskInfo.id);
93
- printer.start(name);
176
+ if (shouldPrint) printer.start(name);
94
177
 
95
178
  try {
96
179
  const result = await asyncLocalStorage.run(taskInfo.id, async () => {
@@ -104,7 +187,7 @@ export async function task(name, fn) {
104
187
  taskInfo.duration = duration;
105
188
  taskInfo.result = { status: TaskStatus.SUCCESS, output: result };
106
189
 
107
- printer.finish(name);
190
+ if (shouldPrint) printer.finish(name);
108
191
  runningTasks.delete(taskInfo.id);
109
192
 
110
193
  return result;
@@ -116,14 +199,20 @@ export async function task(name, fn) {
116
199
  taskInfo.duration = duration;
117
200
  taskInfo.result = { status: TaskStatus.FAIL, output: err.message };
118
201
 
119
- printer.error(name, err);
202
+ if (shouldPrint) printer.error(name, err);
120
203
  runningTasks.delete(taskInfo.id);
121
204
 
122
205
  // Don't exit during testing
123
206
  const isTestEnvironment = process.env.NODE_ENV === 'test' ||
124
207
  typeof Bun?.jest !== 'undefined' ||
125
- process.argv.some(arg => arg.includes('test'));
208
+ process.argv.some(arg => arg.includes('vitest') || arg.includes('jest') || arg.includes('--test') || arg.includes('test:'));
126
209
 
210
+ // Exit immediately if stopOnFailures mode is enabled
211
+ if (stopOnFailuresMode && !isTestEnvironment) {
212
+ process.exit(1);
213
+ }
214
+
215
+ // Also exit if stopFailToggle is enabled (legacy behavior)
127
216
  if (stopFailToggle && !isTestEnvironment) {
128
217
  process.exit(1);
129
218
  }
@@ -132,6 +221,32 @@ export async function task(name, fn) {
132
221
  }
133
222
  }
134
223
 
224
+ export class SilentTaskWrapper {
225
+ constructor() {
226
+ this.silent = true;
227
+ }
228
+
229
+ async try(name, fn) {
230
+ if (!fn) {
231
+ fn = name;
232
+ name = fn.toString().slice(0, 50).replace(/\s+/g, ' ').trim();
233
+ }
234
+ return await tryTask(name, fn, true);
235
+ }
236
+
237
+ async task(name, fn) {
238
+ if (!fn) {
239
+ fn = name;
240
+ name = fn.toString().slice(0, 50).replace(/\s+/g, ' ').trim();
241
+ }
242
+ return await task(name, fn, true);
243
+ }
244
+ }
245
+
246
+ export function silent() {
247
+ return new SilentTaskWrapper();
248
+ }
249
+
135
250
  export class TaskResult {
136
251
  constructor({ status, output }) {
137
252
  this.status = status;
@@ -146,6 +261,10 @@ export class TaskResult {
146
261
  return this.status === TaskStatus.SUCCESS;
147
262
  }
148
263
 
264
+ get hasWarning() {
265
+ return this.status === TaskStatus.WARNING;
266
+ }
267
+
149
268
  static fail(output = null) {
150
269
  return new TaskResult({ status: TaskStatus.FAIL, output });
151
270
  }
@@ -153,4 +272,8 @@ export class TaskResult {
153
272
  static success(output = null) {
154
273
  return new TaskResult({ status: TaskStatus.SUCCESS, output });
155
274
  }
275
+
276
+ static warning(output = null) {
277
+ return new TaskResult({ status: TaskStatus.WARNING, output });
278
+ }
156
279
  }