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 +28 -1
- package/index.js +3 -1
- package/package.json +1 -1
- package/src/task.js +89 -49
- package/src/tasks/assert.js +30 -0
- package/types.d.ts +2 -1
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
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
140
|
-
|
|
184
|
+
const endTime = Date.now();
|
|
185
|
+
const duration = endTime - taskInfo.startTime;
|
|
141
186
|
|
|
142
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
226
|
-
|
|
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
|
}
|