bunosh 0.3.0 → 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/README.md +639 -634
- package/bunosh.js +24 -0
- package/index.js +17 -8
- package/package.json +1 -2
- package/src/formatters/console.js +1 -0
- package/src/formatters/github-actions.js +4 -0
- package/src/io.js +55 -1
- package/src/open-editor.js +95 -0
- package/src/printer.js +11 -0
- package/src/program.js +11 -7
- package/src/task.js +131 -8
- package/src/tasks/shell.js +119 -0
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
|
@@ -1,32 +1,41 @@
|
|
|
1
1
|
import exec from "./src/tasks/exec.js";
|
|
2
|
+
import shell from "./src/tasks/shell.js";
|
|
2
3
|
import fetch from "./src/tasks/fetch.js";
|
|
3
4
|
import writeToFile from "./src/tasks/writeToFile.js";
|
|
4
5
|
import copyFile from "./src/tasks/copyFile.js";
|
|
5
6
|
import ai from "./src/tasks/ai.js";
|
|
6
7
|
import { ask, yell, say } from "./src/io.js";
|
|
7
|
-
import { task, stopOnFail, ignoreFail } from "./src/task.js";
|
|
8
|
-
|
|
9
|
-
export { exec, fetch, writeToFile, copyFile, ai, ask, yell, say, task, stopOnFail, ignoreFail };
|
|
8
|
+
import { task, tryTask, stopOnFail, ignoreFail, stopOnFailures, ignoreFailures, silence, prints, silent } from "./src/task.js";
|
|
10
9
|
|
|
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
|
-
return function(args) {
|
|
14
|
-
return exec`${cmd} ${args}
|
|
15
|
-
}
|
|
13
|
+
return function (args) {
|
|
14
|
+
return exec`${cmd} ${args}`;
|
|
15
|
+
};
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
global.bunosh = {
|
|
19
|
-
ask,
|
|
19
|
+
ask,
|
|
20
|
+
yell,
|
|
21
|
+
say,
|
|
20
22
|
fetch,
|
|
21
23
|
exec,
|
|
24
|
+
shell,
|
|
22
25
|
writeToFile,
|
|
23
26
|
copyFile,
|
|
24
27
|
ai,
|
|
25
28
|
stopOnFail,
|
|
26
29
|
ignoreFail,
|
|
27
30
|
task,
|
|
31
|
+
try: tryTask,
|
|
32
|
+
stopOnFailures,
|
|
33
|
+
ignoreFailures,
|
|
34
|
+
silence,
|
|
35
|
+
prints,
|
|
36
|
+
silent,
|
|
28
37
|
buildCmd,
|
|
29
38
|
$: exec,
|
|
30
|
-
}
|
|
39
|
+
};
|
|
31
40
|
|
|
32
41
|
export default global.bunosh;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bunosh",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"module": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
"debug": "^4.4.1",
|
|
26
26
|
"fs-extra": "^11.3.0",
|
|
27
27
|
"inquirer": "^12.6.3",
|
|
28
|
-
"open-editor": "^5.1.0",
|
|
29
28
|
"timer-node": "^5.0.9",
|
|
30
29
|
"zod": "^4.1.5"
|
|
31
30
|
},
|
|
@@ -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/io.js
CHANGED
|
@@ -6,11 +6,65 @@ export function say(...args) {
|
|
|
6
6
|
console.log('!', ...args);
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
export async function ask(question,
|
|
9
|
+
export async function ask(question, defaultValueOrOptions = {}, options = {}) {
|
|
10
|
+
// Smart parameter detection
|
|
11
|
+
let opts = {};
|
|
12
|
+
|
|
13
|
+
// If second parameter is not an object, it's a default value
|
|
14
|
+
if (defaultValueOrOptions !== null && typeof defaultValueOrOptions !== 'object') {
|
|
15
|
+
opts.default = defaultValueOrOptions;
|
|
16
|
+
opts = { ...opts, ...options }; // Merge with third parameter options
|
|
17
|
+
|
|
18
|
+
// Auto-detect type based on default value
|
|
19
|
+
if (typeof defaultValueOrOptions === 'boolean') {
|
|
20
|
+
opts.type = 'confirm';
|
|
21
|
+
}
|
|
22
|
+
} else if (Array.isArray(defaultValueOrOptions)) {
|
|
23
|
+
// If it's an array, treat as choices
|
|
24
|
+
opts.choices = defaultValueOrOptions;
|
|
25
|
+
opts = { ...opts, ...options }; // Merge with third parameter options
|
|
26
|
+
} else {
|
|
27
|
+
// Traditional object parameter
|
|
28
|
+
opts = { ...defaultValueOrOptions, ...options };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Route to appropriate handler based on options
|
|
32
|
+
if (opts.editor || opts.multiline) {
|
|
33
|
+
return await askWithEditor(question, opts);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (opts.choices) {
|
|
37
|
+
return await askWithChoices(question, opts);
|
|
38
|
+
}
|
|
39
|
+
|
|
10
40
|
const answers = await inquirer.prompt({ name: question, message: question, ...opts })
|
|
11
41
|
return Object.values(answers)[0];
|
|
12
42
|
}
|
|
13
43
|
|
|
44
|
+
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
|
+
}
|
|
53
|
+
|
|
54
|
+
async function askWithChoices(question, opts = {}) {
|
|
55
|
+
const promptType = opts.multiple ? 'checkbox' : 'list';
|
|
56
|
+
|
|
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];
|
|
66
|
+
}
|
|
67
|
+
|
|
14
68
|
export function yell(text) {
|
|
15
69
|
console.log();
|
|
16
70
|
|
|
@@ -0,0 +1,95 @@
|
|
|
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
|
+
}
|
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/program.js
CHANGED
|
@@ -4,7 +4,7 @@ import traverseDefault from "@babel/traverse";
|
|
|
4
4
|
const traverse = traverseDefault.default || traverseDefault;
|
|
5
5
|
import color from "chalk";
|
|
6
6
|
import fs from 'fs';
|
|
7
|
-
import openEditor from 'open-editor';
|
|
7
|
+
import openEditor from './open-editor.js';
|
|
8
8
|
import { yell } from './io.js';
|
|
9
9
|
import cprint from "./font.js";
|
|
10
10
|
import { handleCompletion, detectCurrentShell, installCompletion, getCompletionPaths } from './completion.js';
|
|
@@ -251,12 +251,16 @@ export default function bunosh(commands, source) {
|
|
|
251
251
|
|
|
252
252
|
const editCmd = program.command('edit')
|
|
253
253
|
.description('Open the bunosh file in your editor. $EDITOR or \'code\' is used.')
|
|
254
|
-
.action(() => {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
})
|
|
254
|
+
.action(async () => {
|
|
255
|
+
try {
|
|
256
|
+
await openEditor([{
|
|
257
|
+
file: BUNOSHFILE,
|
|
258
|
+
}]);
|
|
259
|
+
} catch (error) {
|
|
260
|
+
console.error(error.message);
|
|
261
|
+
console.error('Set $EDITOR environment variable to use a different editor');
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
260
264
|
});
|
|
261
265
|
|
|
262
266
|
internalCommands.push(editCmd);
|
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: ${
|
|
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
|
|
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
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { TaskResult, createTaskInfo, finishTaskInfo } from "../task.js";
|
|
2
|
+
import Printer from "../printer.js";
|
|
3
|
+
|
|
4
|
+
const isBun = typeof Bun !== 'undefined' && typeof Bun.spawn === 'function';
|
|
5
|
+
|
|
6
|
+
export default function shell(strings, ...values) {
|
|
7
|
+
const cmd = strings.reduce((accumulator, str, i) => {
|
|
8
|
+
return accumulator + str + (values[i] || "");
|
|
9
|
+
}, "");
|
|
10
|
+
|
|
11
|
+
let envs = null;
|
|
12
|
+
let cwd = null;
|
|
13
|
+
|
|
14
|
+
const cmdPromise = new Promise(async (resolve, reject) => {
|
|
15
|
+
const extraInfo = {};
|
|
16
|
+
if (cwd) extraInfo.cwd = cwd;
|
|
17
|
+
if (envs) extraInfo.env = envs;
|
|
18
|
+
|
|
19
|
+
if (!isBun) {
|
|
20
|
+
const { default: exec } = await import("./exec.js");
|
|
21
|
+
let execPromise = exec(strings, ...values);
|
|
22
|
+
if (envs) execPromise = execPromise.env(envs);
|
|
23
|
+
if (cwd) execPromise = execPromise.cwd(cwd);
|
|
24
|
+
const result = await execPromise;
|
|
25
|
+
resolve(result);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const taskInfo = createTaskInfo(cmd);
|
|
30
|
+
const printer = new Printer("shell", taskInfo.id);
|
|
31
|
+
printer.start(cmd, extraInfo);
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const { $ } = await import("bun");
|
|
35
|
+
|
|
36
|
+
let shell = $;
|
|
37
|
+
|
|
38
|
+
if (cwd) {
|
|
39
|
+
shell = shell.cwd(cwd);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (envs) {
|
|
43
|
+
shell = shell.env(envs);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let result;
|
|
47
|
+
try {
|
|
48
|
+
result = await shell(strings, ...values);
|
|
49
|
+
|
|
50
|
+
const output = await result.text();
|
|
51
|
+
|
|
52
|
+
printer.finish(cmd);
|
|
53
|
+
finishTaskInfo(taskInfo, true, null, output.trim());
|
|
54
|
+
resolve(TaskResult.success(output.trim()));
|
|
55
|
+
return;
|
|
56
|
+
|
|
57
|
+
} catch (shellError) {
|
|
58
|
+
const isCommandNotFound = shellError.stderr &&
|
|
59
|
+
(shellError.stderr.includes('command not found') ||
|
|
60
|
+
shellError.stderr.includes('bun: command not found'));
|
|
61
|
+
|
|
62
|
+
if (isCommandNotFound) {
|
|
63
|
+
printer.finish(cmd);
|
|
64
|
+
finishTaskInfo(taskInfo, true, null, "fallback to exec");
|
|
65
|
+
|
|
66
|
+
const { default: exec } = await import("./exec.js");
|
|
67
|
+
let execPromise = exec`${cmd}`;
|
|
68
|
+
if (envs) execPromise = execPromise.env(envs);
|
|
69
|
+
if (cwd) execPromise = execPromise.cwd(cwd);
|
|
70
|
+
const result = await execPromise;
|
|
71
|
+
resolve(result);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (shellError.exitCode !== undefined) {
|
|
76
|
+
const stderr = shellError.stderr ? Buffer.isBuffer(shellError.stderr) ? shellError.stderr.toString() : shellError.stderr : "";
|
|
77
|
+
const stdout = shellError.stdout ? Buffer.isBuffer(shellError.stdout) ? shellError.stdout.toString() : shellError.stdout : "";
|
|
78
|
+
const errorOutput = (stderr + stdout).trim() || `Command failed with exit code ${shellError.exitCode}`;
|
|
79
|
+
|
|
80
|
+
if (errorOutput) {
|
|
81
|
+
const lines = errorOutput.split('\n');
|
|
82
|
+
for (const line of lines) {
|
|
83
|
+
if (line.trim()) {
|
|
84
|
+
printer.output(line, true);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const error = new Error(`Exit code: ${shellError.exitCode}`);
|
|
90
|
+
printer.error(cmd, null, { exitCode: shellError.exitCode });
|
|
91
|
+
finishTaskInfo(taskInfo, false, error, errorOutput);
|
|
92
|
+
resolve(TaskResult.fail(errorOutput));
|
|
93
|
+
return;
|
|
94
|
+
} else {
|
|
95
|
+
const errorMessage = shellError.message || shellError.toString();
|
|
96
|
+
printer.error(cmd, shellError);
|
|
97
|
+
finishTaskInfo(taskInfo, false, shellError, errorMessage);
|
|
98
|
+
resolve(TaskResult.fail(errorMessage));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch (error) {
|
|
102
|
+
printer.error(cmd, error);
|
|
103
|
+
finishTaskInfo(taskInfo, false, error, error.message);
|
|
104
|
+
resolve(TaskResult.fail(error.message));
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
cmdPromise.env = (newEnvs) => {
|
|
109
|
+
envs = newEnvs;
|
|
110
|
+
return cmdPromise;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
cmdPromise.cwd = (newCwd) => {
|
|
114
|
+
cwd = newCwd;
|
|
115
|
+
return cmdPromise;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
return cmdPromise;
|
|
119
|
+
}
|