bunosh 0.3.1 → 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/task.js CHANGED
@@ -1,17 +1,21 @@
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
- SUCCESS: 'success'
8
+ SUCCESS: 'success',
9
+ WARNING: 'warning'
8
10
  };
9
11
 
12
+ // Initialize local array and also keep global synced
10
13
  export const tasksExecuted = [];
11
14
  export const runningTasks = new Map();
12
15
 
13
16
  let taskCounter = 0;
14
17
  let stopFailToggle = true;
18
+ let globalSilenceMode = false;
15
19
  const asyncLocalStorage = new AsyncLocalStorage();
16
20
 
17
21
 
@@ -23,20 +27,29 @@ export function ignoreFail(enable = true) {
23
27
  stopFailToggle = !enable;
24
28
  }
25
29
 
26
- const startTime = Date.now();
30
+ // Global failure mode control
31
+ // true = stop on failures (exit with code 1), false = continue on failures
32
+ let stopOnFailuresMode = false;
27
33
 
28
- process.on('exit', (code) => {
29
- if (!process.env.BUNOSH_COMMAND_STARTED) return;
34
+ export function ignoreFailures() {
35
+ stopOnFailuresMode = false;
36
+ }
30
37
 
31
- const totalTime = Date.now() - startTime;
32
- const success = code === 0;
33
- const tasksFailed = tasksExecuted.filter(ti => ti.result?.status === TaskStatus.FAIL).length;
38
+ export function stopOnFailures() {
39
+ stopOnFailuresMode = true;
40
+ }
34
41
 
35
- console.log(`\nšŸ² ${success ? '' : 'FAIL '}Exit Code: ${code} | Tasks: ${tasksExecuted.length}${tasksFailed ? ` | Failed: ${tasksFailed}` : ''} | Time: ${totalTime}ms`);
36
- });
42
+ export function silence() {
43
+ globalSilenceMode = true;
44
+ }
45
+
46
+ export function prints() {
47
+ globalSilenceMode = false;
48
+ }
37
49
 
38
50
  export function getRunningTaskCount() {
39
- return runningTasks.size;
51
+ // Only count top-level tasks (tasks without a parent)
52
+ return Array.from(runningTasks.values()).filter(task => !task.parentId).length;
40
53
  }
41
54
 
42
55
  export function getCurrentTaskId() {
@@ -44,14 +57,32 @@ export function getCurrentTaskId() {
44
57
  }
45
58
 
46
59
  export function getTaskPrefix(taskId) {
47
- 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;
48
72
  return getRunningTaskCount() > 1 ? `ā°${taskNumber}ā±` : '';
49
73
  }
50
74
 
51
- export function createTaskInfo(name) {
52
- 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);
53
78
  runningTasks.set(taskInfo.id, taskInfo);
54
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
+
55
86
  return taskInfo;
56
87
  }
57
88
 
@@ -70,27 +101,69 @@ export function finishTaskInfo(taskInfo, success = true, error = null, output =
70
101
  }
71
102
 
72
103
  export class TaskInfo {
73
- constructor(name, startTime, status) {
104
+ constructor(name, startTime, status, parentId = null) {
74
105
  this.id = `task-${++taskCounter}-${Math.random().toString(36).substring(7)}`;
75
106
  this.name = name;
76
107
  this.startTime = startTime;
77
108
  this.status = status;
109
+ this.parentId = parentId;
78
110
  }
79
111
  }
80
112
 
81
- export async function task(name, fn) {
113
+ export async function tryTask(name, fn, isSilent = false) {
82
114
  if (!fn) {
83
115
  fn = name;
84
116
  name = fn.toString().slice(0, 50).replace(/\s+/g, ' ').trim();
85
117
  }
86
118
 
87
- const taskInfo = new TaskInfo(name, Date.now(), TaskStatus.RUNNING);
119
+ const taskInfo = createTaskInfo(name);
88
120
 
89
- tasksExecuted.push(taskInfo);
90
- runningTasks.set(taskInfo.id, taskInfo);
121
+ const shouldPrint = !globalSilenceMode && !isSilent;
122
+ const printer = new Printer('task', taskInfo.id);
123
+ if (shouldPrint) printer.start(name);
124
+
125
+ try {
126
+ const result = await asyncLocalStorage.run(taskInfo.id, async () => {
127
+ return await Promise.resolve(fn());
128
+ });
91
129
 
130
+ const endTime = Date.now();
131
+ const duration = endTime - taskInfo.startTime;
132
+
133
+ taskInfo.status = TaskStatus.SUCCESS;
134
+ taskInfo.duration = duration;
135
+ taskInfo.result = { status: TaskStatus.SUCCESS, output: result };
136
+
137
+ if (shouldPrint) printer.finish(name);
138
+ runningTasks.delete(taskInfo.id);
139
+
140
+ return true;
141
+ } catch (err) {
142
+ const endTime = Date.now();
143
+ const duration = endTime - taskInfo.startTime;
144
+
145
+ taskInfo.status = TaskStatus.WARNING;
146
+ taskInfo.duration = duration;
147
+ taskInfo.result = { status: TaskStatus.WARNING, output: err.message };
148
+
149
+ if (shouldPrint) printer.warning(name, err);
150
+ runningTasks.delete(taskInfo.id);
151
+
152
+ return false;
153
+ }
154
+ }
155
+
156
+ export async function task(name, fn, isSilent = false) {
157
+ if (!fn) {
158
+ fn = name;
159
+ name = fn.toString().slice(0, 50).replace(/\s+/g, ' ').trim();
160
+ }
161
+
162
+ const taskInfo = createTaskInfo(name);
163
+
164
+ const shouldPrint = !globalSilenceMode && !isSilent;
92
165
  const printer = new Printer('task', taskInfo.id);
93
- printer.start(name);
166
+ if (shouldPrint) printer.start(name);
94
167
 
95
168
  try {
96
169
  const result = await asyncLocalStorage.run(taskInfo.id, async () => {
@@ -100,6 +173,11 @@ export async function task(name, fn) {
100
173
  const endTime = Date.now();
101
174
  const duration = endTime - taskInfo.startTime;
102
175
 
176
+ // Check if result is a TaskResult instance
177
+ if (result && result.constructor && result.constructor.name === 'TaskResult') {
178
+ return result;
179
+ }
180
+
103
181
  taskInfo.status = TaskStatus.SUCCESS;
104
182
  taskInfo.duration = duration;
105
183
  taskInfo.result = { status: TaskStatus.SUCCESS, output: result };
@@ -122,16 +200,51 @@ export async function task(name, fn) {
122
200
  // Don't exit during testing
123
201
  const isTestEnvironment = process.env.NODE_ENV === 'test' ||
124
202
  typeof Bun?.jest !== 'undefined' ||
125
- process.argv.some(arg => arg.includes('test'));
203
+ process.argv.some(arg => arg.includes('vitest') || arg.includes('jest') || arg.includes('--test') || arg.includes('test:'));
126
204
 
205
+ // Exit immediately if stopOnFailures mode is enabled
206
+ if (stopOnFailuresMode && !isTestEnvironment) {
207
+ process.exit(1);
208
+ }
209
+
210
+ // Also exit if stopFailToggle is enabled (legacy behavior)
127
211
  if (stopFailToggle && !isTestEnvironment) {
128
212
  process.exit(1);
129
213
  }
130
-
214
+
131
215
  throw err;
132
216
  }
133
217
  }
134
218
 
219
+ // Add try method to task function
220
+ task.try = tryTask;
221
+
222
+ export class SilentTaskWrapper {
223
+ constructor() {
224
+ this.silent = true;
225
+ }
226
+
227
+ async try(name, fn) {
228
+ if (!fn) {
229
+ fn = name;
230
+ name = fn.toString().slice(0, 50).replace(/\s+/g, ' ').trim();
231
+ }
232
+ return await tryTask(name, fn, true);
233
+ }
234
+
235
+ async task(name, fn) {
236
+ if (!fn) {
237
+ fn = name;
238
+ name = fn.toString().slice(0, 50).replace(/\s+/g, ' ').trim();
239
+ }
240
+ return await task(name, fn, true);
241
+ }
242
+ }
243
+
244
+ export function silent() {
245
+ return new SilentTaskWrapper();
246
+ }
247
+
135
248
  export class TaskResult {
136
249
  constructor({ status, output }) {
137
250
  this.status = status;
@@ -146,6 +259,10 @@ export class TaskResult {
146
259
  return this.status === TaskStatus.SUCCESS;
147
260
  }
148
261
 
262
+ get hasWarning() {
263
+ return this.status === TaskStatus.WARNING;
264
+ }
265
+
149
266
  static fail(output = null) {
150
267
  return new TaskResult({ status: TaskStatus.FAIL, output });
151
268
  }
@@ -153,4 +270,8 @@ export class TaskResult {
153
270
  static success(output = null) {
154
271
  return new TaskResult({ status: TaskStatus.SUCCESS, output });
155
272
  }
273
+
274
+ static warning(output = null) {
275
+ return new TaskResult({ status: TaskStatus.WARNING, output });
276
+ }
156
277
  }
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
  }
@@ -4,13 +4,45 @@ import Printer from "../printer.js";
4
4
  const isBun = typeof Bun !== 'undefined' && typeof Bun.spawn === 'function';
5
5
 
6
6
  export default function shell(strings, ...values) {
7
+ let envs = null;
8
+ let cwd = null;
9
+
10
+ // Check if called as regular function instead of template literal
11
+ if (!Array.isArray(strings)) {
12
+ // If first argument is a string, treat it as the command
13
+ if (typeof strings === 'string') {
14
+ // For Bun shell, we need to create a template literal-like call
15
+ // But since we can't, fall back to exec
16
+ console.log('Note: shell() with string argument falls back to exec()');
17
+ const cmdPromise = (async () => {
18
+ const { default: exec } = await import("./exec.js");
19
+ let execPromise = exec(strings);
20
+ if (envs) execPromise = execPromise.env(envs);
21
+ if (cwd) execPromise = execPromise.cwd(cwd);
22
+ return execPromise;
23
+ })();
24
+
25
+ // Add .env and .cwd methods
26
+ cmdPromise.env = (newEnvs) => {
27
+ envs = newEnvs;
28
+ return cmdPromise;
29
+ };
30
+
31
+ cmdPromise.cwd = (newCwd) => {
32
+ cwd = newCwd;
33
+ return cmdPromise;
34
+ };
35
+
36
+ return cmdPromise;
37
+ } else {
38
+ throw new Error('shell() must be called as a template literal: shell`command`');
39
+ }
40
+ }
41
+
7
42
  const cmd = strings.reduce((accumulator, str, i) => {
8
43
  return accumulator + str + (values[i] || "");
9
44
  }, "");
10
45
 
11
- let envs = null;
12
- let cwd = null;
13
-
14
46
  const cmdPromise = new Promise(async (resolve, reject) => {
15
47
  const extraInfo = {};
16
48
  if (cwd) extraInfo.cwd = cwd;
@@ -18,7 +50,7 @@ export default function shell(strings, ...values) {
18
50
 
19
51
  if (!isBun) {
20
52
  const { default: exec } = await import("./exec.js");
21
- let execPromise = exec(strings, ...values);
53
+ let execPromise = exec([cmd]);
22
54
  if (envs) execPromise = execPromise.env(envs);
23
55
  if (cwd) execPromise = execPromise.cwd(cwd);
24
56
  const result = await execPromise;
@@ -64,7 +96,7 @@ export default function shell(strings, ...values) {
64
96
  finishTaskInfo(taskInfo, true, null, "fallback to exec");
65
97
 
66
98
  const { default: exec } = await import("./exec.js");
67
- let execPromise = exec`${cmd}`;
99
+ let execPromise = exec([cmd]);
68
100
  if (envs) execPromise = execPromise.env(envs);
69
101
  if (cwd) execPromise = execPromise.cwd(cwd);
70
102
  const result = await execPromise;
@@ -1,95 +0,0 @@
1
- import { spawn } from 'child_process';
2
- import path from 'path';
3
-
4
- export default function openEditor(files, options = {}) {
5
- if (!Array.isArray(files) || files.length === 0) {
6
- throw new Error('Files array is required and cannot be empty');
7
- }
8
-
9
- const editor = options.editor || getDefaultEditor();
10
- const fileArgs = buildEditorArgs(editor, files);
11
-
12
- return new Promise((resolve, reject) => {
13
- const child = spawn(editor, fileArgs, {
14
- stdio: 'inherit',
15
- detached: true
16
- });
17
-
18
- child.on('error', (err) => {
19
- reject(new Error(`Failed to open editor '${editor}': ${err.message}`));
20
- });
21
-
22
- child.on('spawn', () => {
23
- resolve();
24
- });
25
-
26
- if (child.pid) {
27
- child.unref();
28
- }
29
- });
30
- }
31
-
32
- function getDefaultEditor() {
33
- if (process.env.EDITOR) {
34
- return process.env.EDITOR;
35
- }
36
-
37
- const editors = ['code', 'subl', 'atom', 'vim', 'nvim', 'nano', 'gedit'];
38
-
39
- for (const editor of editors) {
40
- if (isCommandAvailable(editor)) {
41
- return editor;
42
- }
43
- }
44
-
45
- return process.platform === 'win32' ? 'notepad' : 'vi';
46
- }
47
-
48
- function isCommandAvailable(command) {
49
- try {
50
- const { execSync } = require('child_process');
51
- execSync(`which ${command}`, { stdio: 'ignore' });
52
- return true;
53
- } catch {
54
- return false;
55
- }
56
- }
57
-
58
- function buildEditorArgs(editor, files) {
59
- const editorName = path.basename(editor);
60
- const args = [];
61
-
62
- for (const fileInfo of files) {
63
- const filePath = typeof fileInfo === 'string' ? fileInfo : fileInfo.file;
64
-
65
- if (!filePath) {
66
- continue;
67
- }
68
-
69
- if (fileInfo.line && typeof fileInfo === 'object') {
70
- switch (editorName) {
71
- case 'code':
72
- case 'code-insiders':
73
- args.push('--goto', `${filePath}:${fileInfo.line}:${fileInfo.column || 1}`);
74
- break;
75
- case 'subl':
76
- case 'sublime_text':
77
- args.push(`${filePath}:${fileInfo.line}:${fileInfo.column || 1}`);
78
- break;
79
- case 'vim':
80
- case 'nvim':
81
- args.push(`+${fileInfo.line}`, filePath);
82
- break;
83
- case 'nano':
84
- args.push(`+${fileInfo.line}`, filePath);
85
- break;
86
- default:
87
- args.push(filePath);
88
- }
89
- } else {
90
- args.push(filePath);
91
- }
92
- }
93
-
94
- return args;
95
- }