bunosh 0.5.6 → 0.5.9

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 CHANGED
@@ -187,7 +187,7 @@ Bunoshfile.api.js # bunosh api:deploy, bunosh api:test
187
187
  Built-in tasks are available via `global.bunosh`:
188
188
 
189
189
  ```javascript
190
- const { exec, shell, fetch, writeToFile, copyFile, task } = global.bunosh;
190
+ const { exec, shell, fetch, writeToFile, copyFile, task, assert } = global.bunosh;
191
191
  ```
192
192
 
193
193
  > Global variables are used instead of imports so bunosh works with the single-executable on any platform.
@@ -195,6 +195,7 @@ const { exec, shell, fetch, writeToFile, copyFile, task } = global.bunosh;
195
195
  * Async tasks: `exec`, `shell`, `fetch`
196
196
  * Sync tasks: `writeToFile`, `copyFile`
197
197
  * Task wrapper: `task`
198
+ * Precondition guard: `assert`
198
199
 
199
200
  Each task returns a `TaskResult` object:
200
201
 
@@ -273,6 +274,30 @@ For details see the [Bun shell](https://bun.sh/docs/runtime/shell) reference.
273
274
  | `exec` | Single command execution | spawn process | Node.js + Bun, platform dependent |
274
275
  | `shell` | Cross-platform shell commands | Bun shell | Bun only, cross-platform |
275
276
 
277
+ ### `assert`
278
+
279
+ Guard a command on a precondition. If the condition is falsy, `assert` prints a red failure line and records a failed task — the run continues, and the process exits with code 1 at the end:
280
+
281
+ ```javascript
282
+ export async function deploy() {
283
+ assert(process.env.TOKEN, 'TOKEN must be set');
284
+ assert(await Bun.file('dist/bundle.js').exists(), 'bundle not built');
285
+ await shell`./scripts/deploy.sh`;
286
+ }
287
+ ```
288
+
289
+ With `task.stopOnFailures()` enabled, a failed `assert` exits immediately at that line:
290
+
291
+ ```javascript
292
+ export async function strict() {
293
+ task.stopOnFailures();
294
+ assert(process.env.TOKEN, 'TOKEN must be set');
295
+ await shell`./scripts/deploy.sh`;
296
+ }
297
+ ```
298
+
299
+ `assert` does not throw a JavaScript exception in default mode — it records the failure and execution continues. Use `task.stopOnFailures()` (or `return` after the `assert`) when you need a hard stop.
300
+
276
301
  ### `fetch`
277
302
 
278
303
  Wraps the fetch API as a task:
@@ -423,6 +448,8 @@ export async function checkServices() {
423
448
  }
424
449
  ```
425
450
 
451
+ `task.try` fully isolates failures from the exit code. Any `shell`, `fetch`, `task`, or `assert` that fails inside the callback is recorded as a warning (yellow), never as a failed task — so the run still exits with code `0` if the rest succeeded. `task.stopOnFailures()` is also suppressed inside `task.try`: an inner failure will never call `process.exit(1)`. The return value (`true`/`false`) is the only signal you act on.
452
+
426
453
  ## Documentation
427
454
 
428
455
  - **[Examples](docs/examples.md)** — Real-world examples and workflows
package/index.js CHANGED
@@ -5,10 +5,11 @@ import fetch from "./src/tasks/fetch.js";
5
5
  import writeToFile from "./src/tasks/writeToFile.js";
6
6
  import copyFile from "./src/tasks/copyFile.js";
7
7
  import ai from "./src/tasks/ai.js";
8
+ import assert from "./src/tasks/assert.js";
8
9
  import { ask, yell, say } from "./src/io.js";
9
10
  import { task, tryTask, stopOnFail, ignoreFail, stopOnFailures, ignoreFailures, silence, prints, silent, TaskResult } from "./src/task.js";
10
11
 
11
- export { exec, shell, fetch, writeToFile, copyFile, ai, ask, yell, say, task, tryTask, stopOnFail, ignoreFail, stopOnFailures, ignoreFailures, silence, prints, silent, TaskResult };
12
+ export { exec, shell, fetch, writeToFile, copyFile, ai, assert, ask, yell, say, task, tryTask, stopOnFail, ignoreFail, stopOnFailures, ignoreFailures, silence, prints, silent, TaskResult };
12
13
 
13
14
  export function buildCmd(cmd) {
14
15
  return function (args) {
@@ -26,6 +27,7 @@ global.bunosh = {
26
27
  writeToFile,
27
28
  copyFile,
28
29
  ai,
30
+ assert,
29
31
  stopOnFail,
30
32
  ignoreFail,
31
33
  task,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunosh",
3
- "version": "0.5.6",
3
+ "version": "0.5.9",
4
4
  "description": "Task runner that turns JavaScript functions into CLI commands. Runs on Bun and Node.js.",
5
5
  "type": "module",
6
6
  "module": "index.js",
package/src/task.js CHANGED
@@ -47,6 +47,22 @@ export function getIgnoreFailuresMode() {
47
47
  return ignoreFailuresMode;
48
48
  }
49
49
 
50
+ export function isStopOnFailuresMode() {
51
+ return stopOnFailuresMode;
52
+ }
53
+
54
+ export function isTestEnv() {
55
+ const commandArgs = process.argv.slice(2);
56
+ return process.env.NODE_ENV === 'test' ||
57
+ commandArgs.some(arg => {
58
+ const lower = arg.toLowerCase();
59
+ return lower.includes('vitest') ||
60
+ lower.includes('jest') ||
61
+ lower === '--test' ||
62
+ lower.startsWith('test:');
63
+ });
64
+ }
65
+
50
66
  export function silence() {
51
67
  globalSilenceMode = true;
52
68
  }
@@ -83,10 +99,21 @@ export function getTaskPrefix(taskId) {
83
99
 
84
100
  export function createTaskInfo(name, parentId = null, isSilent = false) {
85
101
  const taskInfo = new TaskInfo(name, Date.now(), TaskStatus.RUNNING, parentId, isSilent);
102
+
103
+ let p = parentId;
104
+ while (p) {
105
+ const parent = runningTasks.get(p);
106
+ if (!parent) break;
107
+ if (parent.isTry || parent.isInsideTry) {
108
+ taskInfo.isInsideTry = true;
109
+ break;
110
+ }
111
+ p = parent.parentId;
112
+ }
113
+
86
114
  runningTasks.set(taskInfo.id, taskInfo);
87
115
  tasksExecuted.push(taskInfo);
88
116
 
89
- // Also add to global array for exit handler
90
117
  if (globalThis._bunoshGlobalTasksExecuted) {
91
118
  globalThis._bunoshGlobalTasksExecuted.push(taskInfo);
92
119
  }
@@ -98,10 +125,19 @@ export function finishTaskInfo(taskInfo, success = true, error = null, output =
98
125
  const endTime = Date.now();
99
126
  const duration = endTime - taskInfo.startTime;
100
127
 
101
- taskInfo.status = success ? TaskStatus.SUCCESS : TaskStatus.FAIL;
128
+ let status;
129
+ if (success) {
130
+ status = TaskStatus.SUCCESS;
131
+ } else if (taskInfo.isInsideTry) {
132
+ status = TaskStatus.WARNING;
133
+ } else {
134
+ status = TaskStatus.FAIL;
135
+ }
136
+
137
+ taskInfo.status = status;
102
138
  taskInfo.duration = duration;
103
139
  taskInfo.result = {
104
- status: success ? TaskStatus.SUCCESS : TaskStatus.FAIL,
140
+ status,
105
141
  output: error?.message || output || null
106
142
  };
107
143
 
@@ -116,6 +152,8 @@ export class TaskInfo {
116
152
  this.status = status;
117
153
  this.parentId = parentId;
118
154
  this.isSilent = isSilent;
155
+ this.isTry = false;
156
+ this.isInsideTry = false;
119
157
  }
120
158
  }
121
159
 
@@ -125,55 +163,60 @@ export async function tryTask(name, fn, isSilent = true) {
125
163
  name = fn.toString().slice(0, 50).replace(/\s+/g, ' ').trim();
126
164
  }
127
165
 
128
- const taskInfo = createTaskInfo(name, null, isSilent);
166
+ const taskInfo = createTaskInfo(name, getCurrentTaskId() || null, isSilent);
167
+ taskInfo.isTry = true;
168
+ const startIndex = tasksExecuted.length;
129
169
 
130
170
  const shouldPrint = !globalSilenceMode && !isSilent;
131
171
  const printer = new Printer('task', taskInfo.id);
132
172
  if (shouldPrint) printer.start(name);
133
173
 
174
+ let result;
175
+ let caughtError = null;
134
176
  try {
135
- const result = await asyncLocalStorage.run(taskInfo.id, async () => {
177
+ result = await asyncLocalStorage.run(taskInfo.id, async () => {
136
178
  return await Promise.resolve(fn());
137
179
  });
180
+ } catch (err) {
181
+ caughtError = err;
182
+ }
138
183
 
139
- const endTime = Date.now();
140
- const duration = endTime - taskInfo.startTime;
184
+ const endTime = Date.now();
185
+ const duration = endTime - taskInfo.startTime;
141
186
 
142
- // Check if result is a TaskResult and if it has failed
143
- if (result && typeof result === 'object' && result.constructor && result.constructor.name === 'TaskResult') {
144
- if (result.hasFailed || result.hasWarning) {
145
- taskInfo.status = TaskStatus.WARNING;
146
- taskInfo.duration = duration;
147
- taskInfo.result = { status: TaskStatus.WARNING, output: result.output };
187
+ let failed = caughtError !== null;
148
188
 
149
- if (shouldPrint) printer.warning(name);
150
- runningTasks.delete(taskInfo.id);
189
+ if (!failed && result && typeof result === 'object' && result.constructor && result.constructor.name === 'TaskResult') {
190
+ if (result.hasFailed || result.hasWarning) failed = true;
191
+ }
151
192
 
152
- return false;
193
+ if (!failed) {
194
+ for (let i = startIndex; i < tasksExecuted.length; i++) {
195
+ const t = tasksExecuted[i];
196
+ if (t === taskInfo) continue;
197
+ const s = t.result?.status;
198
+ if (s === TaskStatus.FAIL || s === TaskStatus.WARNING) {
199
+ failed = true;
200
+ break;
153
201
  }
154
202
  }
203
+ }
155
204
 
156
- taskInfo.status = TaskStatus.SUCCESS;
157
- taskInfo.duration = duration;
158
- taskInfo.result = { status: TaskStatus.SUCCESS, output: result };
159
-
160
- if (shouldPrint) printer.finish(name);
161
- runningTasks.delete(taskInfo.id);
162
-
163
- return true;
164
- } catch (err) {
165
- const endTime = Date.now();
166
- const duration = endTime - taskInfo.startTime;
167
-
205
+ if (failed) {
168
206
  taskInfo.status = TaskStatus.WARNING;
169
207
  taskInfo.duration = duration;
170
- taskInfo.result = { status: TaskStatus.WARNING, output: err.message };
171
-
172
- if (shouldPrint) printer.warning(name, err);
208
+ taskInfo.result = { status: TaskStatus.WARNING, output: caughtError?.message || result?.output || null };
209
+ if (shouldPrint) printer.warning(name, caughtError);
173
210
  runningTasks.delete(taskInfo.id);
174
-
175
211
  return false;
176
212
  }
213
+
214
+ taskInfo.status = TaskStatus.SUCCESS;
215
+ taskInfo.duration = duration;
216
+ taskInfo.result = { status: TaskStatus.SUCCESS, output: result };
217
+ if (shouldPrint) printer.finish(name);
218
+ runningTasks.delete(taskInfo.id);
219
+ return true;
177
220
  }
178
221
 
179
222
  export async function task(name, fn, isSilent = false) {
@@ -182,7 +225,7 @@ export async function task(name, fn, isSilent = false) {
182
225
  name = fn.toString().slice(0, 50).replace(/\s+/g, ' ').trim();
183
226
  }
184
227
 
185
- const taskInfo = createTaskInfo(name, null, isSilent);
228
+ const taskInfo = createTaskInfo(name, getCurrentTaskId() || null, isSilent);
186
229
 
187
230
  const shouldPrint = !globalSilenceMode && !isSilent;
188
231
  const printer = new Printer('task', taskInfo.id);
@@ -215,6 +258,15 @@ export async function task(name, fn, isSilent = false) {
215
258
  const endTime = Date.now();
216
259
  const duration = endTime - taskInfo.startTime;
217
260
 
261
+ if (taskInfo.isInsideTry) {
262
+ taskInfo.status = TaskStatus.WARNING;
263
+ taskInfo.duration = duration;
264
+ taskInfo.result = { status: TaskStatus.WARNING, output: err.message };
265
+ printer.warning(name, err);
266
+ runningTasks.delete(taskInfo.id);
267
+ return TaskResult.fail(err.message, { taskType: 'task', error: err });
268
+ }
269
+
218
270
  taskInfo.status = TaskStatus.FAIL;
219
271
  taskInfo.duration = duration;
220
272
  taskInfo.result = { status: TaskStatus.FAIL, output: err.message };
@@ -222,28 +274,16 @@ export async function task(name, fn, isSilent = false) {
222
274
  printer.error(name, err);
223
275
  runningTasks.delete(taskInfo.id);
224
276
 
225
- // Don't exit during testing
226
- const commandArgs = process.argv.slice(2);
227
- const isTestEnvironment = process.env.NODE_ENV === 'test' ||
228
- typeof Bun?.jest !== 'undefined' ||
229
- commandArgs.some(arg => {
230
- const lowerArg = arg.toLowerCase();
231
- return lowerArg.includes('vitest') ||
232
- lowerArg.includes('jest') ||
233
- lowerArg === '--test' ||
234
- lowerArg.startsWith('test:');
235
- });
236
-
237
- // Exit immediately if stopOnFailures mode is enabled
277
+ const isTestEnvironment = isTestEnv();
278
+
238
279
  if (stopOnFailuresMode && !isTestEnvironment) {
239
280
  process.exit(1);
240
281
  }
241
-
242
- // Also exit if stopFailToggle is enabled (legacy behavior)
282
+
243
283
  if (stopFailToggle && !isTestEnvironment) {
244
284
  process.exit(1);
245
285
  }
246
-
286
+
247
287
  return TaskResult.fail(err.message, { taskType: 'task', error: err });
248
288
  }
249
289
  }
@@ -0,0 +1,30 @@
1
+ import { createTaskInfo, finishTaskInfo, getCurrentTaskId, runningTasks, isStopOnFailuresMode, isTestEnv } from '../task.js';
2
+ import Printer from '../printer.js';
3
+
4
+ export default function assert(condition, message = 'Assertion failed') {
5
+ const currentTaskId = getCurrentTaskId();
6
+ const parent = currentTaskId ? runningTasks.get(currentTaskId) : null;
7
+ const isParentSilent = parent?.isSilent || false;
8
+
9
+ const taskInfo = createTaskInfo(message, currentTaskId, isParentSilent);
10
+ const printer = new Printer('assert', taskInfo.id);
11
+
12
+ if (condition) {
13
+ printer.finish(message);
14
+ finishTaskInfo(taskInfo, true, null, message);
15
+ return;
16
+ }
17
+
18
+ const error = new Error(message);
19
+
20
+ if (taskInfo.isInsideTry) {
21
+ printer.warning(message, error);
22
+ finishTaskInfo(taskInfo, false, error, message);
23
+ return;
24
+ }
25
+
26
+ printer.error(message, error);
27
+ finishTaskInfo(taskInfo, false, error, message);
28
+
29
+ if (isStopOnFailuresMode() && !isTestEnv()) process.exit(1);
30
+ }
package/types.d.ts CHANGED
@@ -34,8 +34,9 @@ declare global {
34
34
  copyFile(src: string, dst: string): void;
35
35
  stopOnFail(enable?: boolean): void;
36
36
  ignoreFail(enable?: boolean): void;
37
- buildCmd(cmd: string): (args: string) => Promise<any>;
37
+ buildCmd(cmd: string): (args: string) => Promise<any>;
38
38
  task(name: string | Function, fn?: Function): Promise<any>;
39
+ assert(condition: any, message?: string): asserts condition;
39
40
  };
40
41
  }
41
42
  }