bunosh 0.5.0 → 0.5.6
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 +0 -1
- package/bunosh.js +0 -39
- package/index.js +4 -3
- package/package.json +18 -2
- package/src/formatters/console.js +5 -1
- package/src/io.js +0 -5
- package/src/printer.js +29 -9
- package/src/program.js +46 -12
- package/src/task.js +8 -1
- package/src/tasks/exec.js +4 -248
- package/src/tasks/fetch.js +2 -1
- package/src/tasks/shell.js +194 -119
- package/src/upgrade.js +135 -30
- package/src/mcp-server.js +0 -575
package/README.md
CHANGED
|
@@ -427,7 +427,6 @@ export async function checkServices() {
|
|
|
427
427
|
|
|
428
428
|
- **[Examples](docs/examples.md)** — Real-world examples and workflows
|
|
429
429
|
- **[AI Integration](docs/ai.md)** — Built-in AI support
|
|
430
|
-
- **[MCP Integration](docs/mcp.md)** — Expose commands to AI assistants (Claude, Cursor, etc.)
|
|
431
430
|
- **[JavaScript Execution](docs/javascript-execution.md)** — Execute JavaScript directly via CLI
|
|
432
431
|
- **[Bash Migration Guide](docs/bash-migration-guide.md)** — Convert bash scripts to Bunosh
|
|
433
432
|
- **[Node.js Migration Guide](docs/nodejs-migration-guide.md)** — Migrate from Node.js scripts
|
package/bunosh.js
CHANGED
|
@@ -124,45 +124,6 @@ async function main() {
|
|
|
124
124
|
process.argv.splice(envFileIndex, 1);
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
const mcpFlagIndex = process.argv.indexOf('-mcp');
|
|
128
|
-
if (mcpFlagIndex !== -1) {
|
|
129
|
-
process.argv.splice(mcpFlagIndex, 1);
|
|
130
|
-
|
|
131
|
-
process.env.BUNOSH_MCP_MODE = 'true';
|
|
132
|
-
|
|
133
|
-
const { createMcpServer, startMcpServer } = await import('./src/mcp-server.js');
|
|
134
|
-
|
|
135
|
-
let tasksFile;
|
|
136
|
-
let bunoshfileDir;
|
|
137
|
-
if (customBunoshfile) {
|
|
138
|
-
const resolvedPath = path.isAbsolute(customBunoshfile) ? customBunoshfile : path.resolve(customBunoshfile);
|
|
139
|
-
if (existsSync(resolvedPath) && statSync(resolvedPath).isDirectory()) {
|
|
140
|
-
tasksFile = path.join(resolvedPath, BUNOSHFILE);
|
|
141
|
-
bunoshfileDir = resolvedPath;
|
|
142
|
-
} else {
|
|
143
|
-
tasksFile = resolvedPath;
|
|
144
|
-
bunoshfileDir = path.dirname(resolvedPath);
|
|
145
|
-
}
|
|
146
|
-
} else {
|
|
147
|
-
tasksFile = path.join(process.cwd(), BUNOSHFILE);
|
|
148
|
-
bunoshfileDir = process.cwd();
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (!existsSync(tasksFile)) {
|
|
152
|
-
console.error('Bunoshfile not found for MCP mode');
|
|
153
|
-
process.exit(1);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
loadEnvFiles(bunoshfileDir, customEnvFile);
|
|
157
|
-
|
|
158
|
-
const { tasks, sources } = await loadBunoshfiles(tasksFile);
|
|
159
|
-
|
|
160
|
-
const server = createMcpServer(tasks, sources);
|
|
161
|
-
await startMcpServer(server);
|
|
162
|
-
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
127
|
let tasksFile;
|
|
167
128
|
let bunoshfileDir;
|
|
168
129
|
if (customBunoshfile) {
|
package/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import exec from "./src/tasks/exec.js";
|
|
2
1
|
import shell from "./src/tasks/shell.js";
|
|
2
|
+
// Deprecated: `exec` is an alias for `shell`, kept for backward compatibility.
|
|
3
|
+
const exec = shell;
|
|
3
4
|
import fetch from "./src/tasks/fetch.js";
|
|
4
5
|
import writeToFile from "./src/tasks/writeToFile.js";
|
|
5
6
|
import copyFile from "./src/tasks/copyFile.js";
|
|
@@ -11,7 +12,7 @@ export { exec, shell, fetch, writeToFile, copyFile, ai, ask, yell, say, task, tr
|
|
|
11
12
|
|
|
12
13
|
export function buildCmd(cmd) {
|
|
13
14
|
return function (args) {
|
|
14
|
-
return
|
|
15
|
+
return shell`${cmd} ${args}`;
|
|
15
16
|
};
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -35,7 +36,7 @@ global.bunosh = {
|
|
|
35
36
|
silent,
|
|
36
37
|
TaskResult,
|
|
37
38
|
buildCmd,
|
|
38
|
-
$:
|
|
39
|
+
$: shell,
|
|
39
40
|
};
|
|
40
41
|
|
|
41
42
|
export default global.bunosh;
|
package/package.json
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bunosh",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
4
|
+
"description": "Task runner that turns JavaScript functions into CLI commands. Runs on Bun and Node.js.",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"module": "index.js",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/DavertMik/bunosh.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/DavertMik/bunosh#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/DavertMik/bunosh/issues"
|
|
14
|
+
},
|
|
15
|
+
"author": "davert",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"task-runner",
|
|
18
|
+
"cli",
|
|
19
|
+
"bun",
|
|
20
|
+
"automation",
|
|
21
|
+
"scripts"
|
|
22
|
+
],
|
|
6
23
|
"bin": {
|
|
7
24
|
"bunosh": "./bunosh.js"
|
|
8
25
|
},
|
|
@@ -19,7 +36,6 @@
|
|
|
19
36
|
"@ai-sdk/openai": "^2.0.23",
|
|
20
37
|
"@babel/parser": "^7.27.5",
|
|
21
38
|
"@babel/traverse": "^7.27.4",
|
|
22
|
-
"@modelcontextprotocol/sdk": "^1.19.1",
|
|
23
39
|
"ai": "^5.0.29",
|
|
24
40
|
"chalk": "^5.4.1",
|
|
25
41
|
"commander": "^14.0.0",
|
|
@@ -12,7 +12,11 @@ const STATUS_CONFIG = {
|
|
|
12
12
|
|
|
13
13
|
export class ConsoleFormatter extends BaseFormatter {
|
|
14
14
|
shouldDelayStart() {
|
|
15
|
-
return
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
getStartDelay() {
|
|
19
|
+
return 1000;
|
|
16
20
|
}
|
|
17
21
|
format(taskName, status, taskType, extra = {}) {
|
|
18
22
|
const config = STATUS_CONFIG[status];
|
package/src/io.js
CHANGED
|
@@ -7,11 +7,6 @@ export function say(...args) {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export async function ask(question, defaultValueOrOptions = {}, options = {}) {
|
|
10
|
-
// Check if we're in MCP mode and should use the interactive ask function
|
|
11
|
-
if (globalThis._mcpAskFunction) {
|
|
12
|
-
return globalThis._mcpAskFunction(question, defaultValueOrOptions, options);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
10
|
// Track that we're in an ask operation to prevent duplicate exit summaries
|
|
16
11
|
globalThis._bunoshInAskOperation = true;
|
|
17
12
|
// Smart parameter detection
|
package/src/printer.js
CHANGED
|
@@ -85,7 +85,10 @@ export class Printer {
|
|
|
85
85
|
const delay = this.formatter.getStartDelay ? this.formatter.getStartDelay() : 50;
|
|
86
86
|
|
|
87
87
|
if (this.formatter.shouldDelayStart && this.formatter.shouldDelayStart()) {
|
|
88
|
+
this.pendingStart = { taskName, extra };
|
|
88
89
|
this.startTimeout = setTimeout(() => {
|
|
90
|
+
this.startTimeout = null;
|
|
91
|
+
this.pendingStart = null;
|
|
89
92
|
this.hasStarted = true;
|
|
90
93
|
this.print(taskName, 'start', extra);
|
|
91
94
|
}, delay);
|
|
@@ -95,19 +98,37 @@ export class Printer {
|
|
|
95
98
|
}
|
|
96
99
|
}
|
|
97
100
|
|
|
98
|
-
|
|
101
|
+
_flushPendingStart() {
|
|
102
|
+
if (!this.startTimeout) return;
|
|
103
|
+
clearTimeout(this.startTimeout);
|
|
104
|
+
this.startTimeout = null;
|
|
105
|
+
const pending = this.pendingStart;
|
|
106
|
+
this.pendingStart = null;
|
|
107
|
+
if (pending) {
|
|
108
|
+
this.hasStarted = true;
|
|
109
|
+
this.print(pending.taskName, 'start', pending.extra);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
_cancelPendingStart() {
|
|
99
114
|
if (this.startTimeout) {
|
|
100
115
|
clearTimeout(this.startTimeout);
|
|
101
116
|
this.startTimeout = null;
|
|
102
117
|
}
|
|
118
|
+
this.pendingStart = null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
cancel() {
|
|
122
|
+
this._cancelPendingStart();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
finish(taskName, extra = {}) {
|
|
126
|
+
this._cancelPendingStart();
|
|
103
127
|
this.print(taskName, 'finish', extra);
|
|
104
128
|
}
|
|
105
129
|
|
|
106
130
|
error(taskName, error = null, extra = {}) {
|
|
107
|
-
|
|
108
|
-
clearTimeout(this.startTimeout);
|
|
109
|
-
this.startTimeout = null;
|
|
110
|
-
}
|
|
131
|
+
this._cancelPendingStart();
|
|
111
132
|
if (error) {
|
|
112
133
|
extra.error = typeof error === 'string' ? error : error.message;
|
|
113
134
|
}
|
|
@@ -115,10 +136,7 @@ export class Printer {
|
|
|
115
136
|
}
|
|
116
137
|
|
|
117
138
|
warning(taskName, error = null, extra = {}) {
|
|
118
|
-
|
|
119
|
-
clearTimeout(this.startTimeout);
|
|
120
|
-
this.startTimeout = null;
|
|
121
|
-
}
|
|
139
|
+
this._cancelPendingStart();
|
|
122
140
|
if (error) {
|
|
123
141
|
extra.error = typeof error === 'string' ? error : error.message;
|
|
124
142
|
}
|
|
@@ -128,6 +146,8 @@ export class Printer {
|
|
|
128
146
|
output(line, isError = false) {
|
|
129
147
|
if (!line.trim()) return;
|
|
130
148
|
|
|
149
|
+
this._flushPendingStart();
|
|
150
|
+
|
|
131
151
|
// Add task prefix for parallel tasks on output lines
|
|
132
152
|
const prefix = this.taskId ? getTaskPrefix(this.taskId) : '';
|
|
133
153
|
const prefixedLine = prefix ? `${prefix} ${line}` : line;
|
package/src/program.js
CHANGED
|
@@ -8,7 +8,7 @@ import { yell } from './io.js';
|
|
|
8
8
|
import { formatError } from './error-formatter.js';
|
|
9
9
|
import cprint from "./font.js";
|
|
10
10
|
import { handleCompletion, detectCurrentShell, installCompletion, getCompletionPaths } from './completion.js';
|
|
11
|
-
import { upgradeCommand } from './upgrade.js';
|
|
11
|
+
import { upgradeCommand, printUpgradeNoticeIfAvailable } from './upgrade.js';
|
|
12
12
|
|
|
13
13
|
export const BUNOSHFILE = `Bunoshfile.js`;
|
|
14
14
|
|
|
@@ -265,16 +265,22 @@ export default async function bunosh(commands, sources) {
|
|
|
265
265
|
}
|
|
266
266
|
});
|
|
267
267
|
|
|
268
|
-
const
|
|
269
|
-
if (
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
268
|
+
const optNames = Object.keys(opts);
|
|
269
|
+
if (optNames.length > 0) {
|
|
270
|
+
const lastArg = commanderArgs[commanderArgs.length - 1];
|
|
271
|
+
const optionsObj = (lastArg && typeof lastArg.opts === 'function') ? lastArg.opts() : lastArg;
|
|
272
|
+
const mergedOpts = {};
|
|
273
|
+
optNames.forEach((optName) => {
|
|
274
|
+
const camelName = optName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
275
|
+
if (optionsObj && optionsObj[camelName] !== undefined) {
|
|
276
|
+
mergedOpts[camelName] = optionsObj[camelName];
|
|
277
|
+
} else if (optionsObj && optionsObj[optName] !== undefined) {
|
|
278
|
+
mergedOpts[camelName] = optionsObj[optName];
|
|
274
279
|
} else {
|
|
275
|
-
|
|
280
|
+
mergedOpts[camelName] = opts[optName];
|
|
276
281
|
}
|
|
277
282
|
});
|
|
283
|
+
transformedArgs.push(mergedOpts);
|
|
278
284
|
}
|
|
279
285
|
|
|
280
286
|
try {
|
|
@@ -385,6 +391,23 @@ export default async function bunosh(commands, sources) {
|
|
|
385
391
|
|
|
386
392
|
internalCommands.push(setupCompletionCmd);
|
|
387
393
|
|
|
394
|
+
const SKILLS_REPO = 'DavertMik/bunosh-skills';
|
|
395
|
+
|
|
396
|
+
const installSkillsCmd = program.command('install-skills')
|
|
397
|
+
.description('Print the command to install Bunosh AI agent skills.')
|
|
398
|
+
.action(() => {
|
|
399
|
+
console.log();
|
|
400
|
+
console.log(`🤖 Install Bunosh AI agent skills (Claude Code, Cursor, Codex, ...):`);
|
|
401
|
+
console.log();
|
|
402
|
+
console.log(` ${color.bold(`npx skills add ${SKILLS_REPO}`)}`);
|
|
403
|
+
console.log();
|
|
404
|
+
console.log(color.dim(` Skills: bunosh-fundamentals, migrate-to-bunosh`));
|
|
405
|
+
console.log(color.dim(` ${SKILLS_REPO} · https://buno.sh`));
|
|
406
|
+
console.log();
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
internalCommands.push(installSkillsCmd);
|
|
410
|
+
|
|
388
411
|
const upgradeCmd = program.command('upgrade')
|
|
389
412
|
.description('Upgrade bunosh to the latest version')
|
|
390
413
|
.option('-f, --force', 'Force upgrade even if already on latest version')
|
|
@@ -410,7 +433,7 @@ export default async function bunosh(commands, sources) {
|
|
|
410
433
|
}
|
|
411
434
|
|
|
412
435
|
const lines = description.split('\n');
|
|
413
|
-
const firstLine = ` ${color.white.bold(paddedName)} ${lines[0]}`;
|
|
436
|
+
const firstLine = ` ${color.white.bold(paddedName)} ${color.dim(lines[0])}`;
|
|
414
437
|
const indentedLines = lines.slice(1).map(line =>
|
|
415
438
|
line.trim() ? ` ${line}` : ''
|
|
416
439
|
).filter(line => line);
|
|
@@ -445,7 +468,7 @@ ${mainCommands}
|
|
|
445
468
|
}
|
|
446
469
|
|
|
447
470
|
const lines = description.split('\n');
|
|
448
|
-
const firstLine = ` ${color.white.bold(paddedName)} ${lines[0]}`;
|
|
471
|
+
const firstLine = ` ${color.white.bold(paddedName)} ${color.dim(lines[0])}`;
|
|
449
472
|
const indentedLines = lines.slice(1).map(line =>
|
|
450
473
|
line.trim() ? ` ${line}` : ''
|
|
451
474
|
).filter(line => line);
|
|
@@ -471,7 +494,7 @@ ${devCommands}
|
|
|
471
494
|
}
|
|
472
495
|
|
|
473
496
|
const lines = description.split('\n');
|
|
474
|
-
const firstLine = ` ${color.white.bold(paddedName)} ${lines[0]}`;
|
|
497
|
+
const firstLine = ` ${color.white.bold(paddedName)} ${color.dim(lines[0])}`;
|
|
475
498
|
const indentedLines = lines.slice(1).map(line =>
|
|
476
499
|
line.trim() ? ` ${line}` : ''
|
|
477
500
|
).filter(line => line);
|
|
@@ -485,7 +508,10 @@ ${namespaceCommands}
|
|
|
485
508
|
}
|
|
486
509
|
});
|
|
487
510
|
|
|
488
|
-
|
|
511
|
+
const helpFlagRequested = process.argv.includes('--help') || process.argv.includes('-h');
|
|
512
|
+
|
|
513
|
+
if (helpFlagRequested) {
|
|
514
|
+
helpText += color.dim(`Special Commands:
|
|
489
515
|
${color.bold('bunosh edit')} 📝 Edit bunosh file with $EDITOR
|
|
490
516
|
${color.bold('bunosh export:scripts')} 📥 Export commands to package.json
|
|
491
517
|
${color.bold('bunosh upgrade')} 🦾 Upgrade bunosh
|
|
@@ -494,6 +520,13 @@ ${namespaceCommands}
|
|
|
494
520
|
${color.bold('bunosh --env-file …')} 🔧 Load custom environment file
|
|
495
521
|
`);
|
|
496
522
|
|
|
523
|
+
helpText += `
|
|
524
|
+
${color.bold('🤖 AI agent skills')} ${color.dim('(Claude Code, Cursor, Codex, ...)')}
|
|
525
|
+
${color.bold('npx skills add DavertMik/bunosh-skills')}
|
|
526
|
+
${color.dim('bunosh-fundamentals · migrate-to-bunosh — see "bunosh install-skills"')}
|
|
527
|
+
`;
|
|
528
|
+
}
|
|
529
|
+
|
|
497
530
|
program.addHelpText('after', helpText);
|
|
498
531
|
|
|
499
532
|
program.on("command:*", (cmd) => {
|
|
@@ -505,6 +538,7 @@ ${namespaceCommands}
|
|
|
505
538
|
|
|
506
539
|
if (process.argv.length === 2) {
|
|
507
540
|
program.outputHelp();
|
|
541
|
+
await printUpgradeNoticeIfAvailable();
|
|
508
542
|
return program;
|
|
509
543
|
}
|
|
510
544
|
|
package/src/task.js
CHANGED
|
@@ -198,6 +198,8 @@ export async function task(name, fn, isSilent = false) {
|
|
|
198
198
|
|
|
199
199
|
// Check if result is a TaskResult instance
|
|
200
200
|
if (result && result.constructor && result.constructor.name === 'TaskResult') {
|
|
201
|
+
printer.cancel();
|
|
202
|
+
runningTasks.delete(taskInfo.id);
|
|
201
203
|
return result;
|
|
202
204
|
}
|
|
203
205
|
|
|
@@ -246,8 +248,13 @@ export async function task(name, fn, isSilent = false) {
|
|
|
246
248
|
}
|
|
247
249
|
}
|
|
248
250
|
|
|
249
|
-
// Add
|
|
251
|
+
// Add methods to task function
|
|
250
252
|
task.try = tryTask;
|
|
253
|
+
task.stopOnFailures = stopOnFailures;
|
|
254
|
+
task.ignoreFailures = ignoreFailures;
|
|
255
|
+
task.silence = silence;
|
|
256
|
+
task.prints = prints;
|
|
257
|
+
task.silent = silent;
|
|
251
258
|
|
|
252
259
|
|
|
253
260
|
export class SilentTaskWrapper {
|
package/src/tasks/exec.js
CHANGED
|
@@ -1,251 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const isBun = typeof Bun !== 'undefined';
|
|
1
|
+
// Deprecated: `exec` is now an alias for `shell`.
|
|
2
|
+
// Use `shell` (or `$`) instead. This module is kept for backward compatibility.
|
|
3
|
+
import shell from './shell.js';
|
|
5
4
|
|
|
6
5
|
export default function exec(strings, ...values) {
|
|
7
|
-
|
|
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
|
-
|
|
18
|
-
const cmd = strings.reduce((accumulator, str, i) => {
|
|
19
|
-
return accumulator + str + (values[i] || '');
|
|
20
|
-
}, '');
|
|
21
|
-
|
|
22
|
-
let envs = null;
|
|
23
|
-
let cwd = null;
|
|
24
|
-
|
|
25
|
-
const cmdPromise = new Promise(async (resolve, reject) => {
|
|
26
|
-
// Wait for the next event loop tick to ensure .env() and .cwd() have been called
|
|
27
|
-
await new Promise(resolve => setTimeout(resolve, 0));
|
|
28
|
-
|
|
29
|
-
const currentTaskId = getCurrentTaskId();
|
|
30
|
-
|
|
31
|
-
// Check if parent task is silent
|
|
32
|
-
let isParentSilent = false;
|
|
33
|
-
if (currentTaskId) {
|
|
34
|
-
const parentTask = runningTasks.get(currentTaskId);
|
|
35
|
-
if (parentTask && parentTask.isSilent) {
|
|
36
|
-
isParentSilent = true;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const extraInfo = {};
|
|
41
|
-
if (cwd) extraInfo.cwd = cwd;
|
|
42
|
-
if (envs) extraInfo.env = envs;
|
|
43
|
-
|
|
44
|
-
const taskInfo = createTaskInfo(cmd, currentTaskId, isParentSilent);
|
|
45
|
-
const printer = new Printer('exec', taskInfo.id);
|
|
46
|
-
printer.start(cmd, extraInfo);
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
if (global.disableBunForTesting || !isBun) {
|
|
50
|
-
const result = await nodeExec(cmd, extraInfo, printer, taskInfo);
|
|
51
|
-
if (result.status === 'success') {
|
|
52
|
-
finishTaskInfo(taskInfo, true, null, result.output);
|
|
53
|
-
resolve(result);
|
|
54
|
-
} else {
|
|
55
|
-
finishTaskInfo(taskInfo, false, new Error(result.output), result.output);
|
|
56
|
-
resolve(result);
|
|
57
|
-
}
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Bun implementation with real-time streaming
|
|
62
|
-
const needsShell = cmd.includes('|') || cmd.includes('>') || cmd.includes('<') || cmd.includes('&&') || cmd.includes('||') || cmd.includes("'") || cmd.includes('"') || cmd.includes(';');
|
|
63
|
-
|
|
64
|
-
const { spawn } = Bun;
|
|
65
|
-
const proc = spawn({
|
|
66
|
-
cmd: needsShell ? ['/bin/sh', '-c', cmd] : cmd.trim().split(/\s+/),
|
|
67
|
-
cwd: cwd || process.cwd(),
|
|
68
|
-
env: {
|
|
69
|
-
...(envs ? { ...process.env, ...envs } : process.env),
|
|
70
|
-
},
|
|
71
|
-
stdout: "pipe",
|
|
72
|
-
stderr: "pipe",
|
|
73
|
-
stdin: "ignore"
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
const decoder = new TextDecoder();
|
|
77
|
-
let output = '';
|
|
78
|
-
let stdout = '';
|
|
79
|
-
let stderr = '';
|
|
80
|
-
let finished = false;
|
|
81
|
-
|
|
82
|
-
// Process stdout
|
|
83
|
-
const readStdout = async () => {
|
|
84
|
-
const reader = proc.stdout.getReader();
|
|
85
|
-
let buffer = '';
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
while (!finished) {
|
|
89
|
-
const { done, value } = await reader.read();
|
|
90
|
-
if (done) break;
|
|
91
|
-
|
|
92
|
-
const text = decoder.decode(value, { stream: true });
|
|
93
|
-
buffer += text;
|
|
94
|
-
|
|
95
|
-
const lines = buffer.split('\n');
|
|
96
|
-
buffer = lines.pop();
|
|
97
|
-
|
|
98
|
-
for (const line of lines) {
|
|
99
|
-
if (line.trim()) {
|
|
100
|
-
printer.output(line);
|
|
101
|
-
output += line + '\n';
|
|
102
|
-
stdout += line + '\n';
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (buffer.trim()) {
|
|
108
|
-
printer.output(buffer);
|
|
109
|
-
output += buffer + '\n';
|
|
110
|
-
stdout += buffer + '\n';
|
|
111
|
-
}
|
|
112
|
-
} finally {
|
|
113
|
-
reader.releaseLock();
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
// Process stderr
|
|
118
|
-
const readStderr = async () => {
|
|
119
|
-
const reader = proc.stderr.getReader();
|
|
120
|
-
let buffer = '';
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
while (!finished) {
|
|
124
|
-
const { done, value } = await reader.read();
|
|
125
|
-
if (done) break;
|
|
126
|
-
|
|
127
|
-
const text = decoder.decode(value, { stream: true });
|
|
128
|
-
buffer += text;
|
|
129
|
-
|
|
130
|
-
const lines = buffer.split('\n');
|
|
131
|
-
buffer = lines.pop();
|
|
132
|
-
|
|
133
|
-
for (const line of lines) {
|
|
134
|
-
if (line.trim()) {
|
|
135
|
-
printer.output(line, true);
|
|
136
|
-
output += line + '\n';
|
|
137
|
-
stderr += line + '\n';
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (buffer.trim()) {
|
|
143
|
-
printer.output(buffer, true);
|
|
144
|
-
output += buffer + '\n';
|
|
145
|
-
stderr += buffer + '\n';
|
|
146
|
-
}
|
|
147
|
-
} finally {
|
|
148
|
-
reader.releaseLock();
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
// Start reading both streams
|
|
153
|
-
const [, , exitResult] = await Promise.all([
|
|
154
|
-
readStdout(),
|
|
155
|
-
readStderr(),
|
|
156
|
-
proc.exited
|
|
157
|
-
]);
|
|
158
|
-
|
|
159
|
-
finished = true;
|
|
160
|
-
const exitCode = parseInt(exitResult, 10);
|
|
161
|
-
|
|
162
|
-
const metadata = {
|
|
163
|
-
taskType: 'exec',
|
|
164
|
-
exitCode,
|
|
165
|
-
stdout: stdout.trim(),
|
|
166
|
-
stderr: stderr.trim()
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
if (exitCode === 0) {
|
|
170
|
-
printer.finish(cmd);
|
|
171
|
-
finishTaskInfo(taskInfo, true, null, output.trim());
|
|
172
|
-
resolve(TaskResult.success(output.trim(), metadata));
|
|
173
|
-
} else {
|
|
174
|
-
const error = new Error(`Exit code: ${exitCode}`);
|
|
175
|
-
printer.error(cmd, null, { exitCode });
|
|
176
|
-
finishTaskInfo(taskInfo, false, error, output.trim());
|
|
177
|
-
resolve(TaskResult.fail(output.trim(), metadata));
|
|
178
|
-
}
|
|
179
|
-
} catch (error) {
|
|
180
|
-
printer.error(cmd, error);
|
|
181
|
-
finishTaskInfo(taskInfo, false, error, error.message);
|
|
182
|
-
resolve(TaskResult.fail(error.message, { taskType: 'exec' }));
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
cmdPromise.env = (newEnvs) => {
|
|
187
|
-
envs = newEnvs;
|
|
188
|
-
return cmdPromise;
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
cmdPromise.cwd = (newCwd) => {
|
|
192
|
-
cwd = newCwd;
|
|
193
|
-
return cmdPromise;
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
return cmdPromise;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
async function nodeExec(cmd, extraInfo, printer, taskInfo) {
|
|
200
|
-
// Node.js fallback - simple execution without real-time output
|
|
201
|
-
const { spawn } = await import('child_process');
|
|
202
|
-
|
|
203
|
-
return new Promise((resolve) => {
|
|
204
|
-
const proc = spawn('sh', ['-c', cmd], {
|
|
205
|
-
cwd: extraInfo.cwd || process.cwd(),
|
|
206
|
-
env: extraInfo.env ? { ...process.env, ...extraInfo.env } : process.env,
|
|
207
|
-
stdio: ['ignore', 'pipe', 'pipe']
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
let output = '';
|
|
211
|
-
let stdout = '';
|
|
212
|
-
let stderr = '';
|
|
213
|
-
|
|
214
|
-
proc.stdout.on('data', (data) => {
|
|
215
|
-
const text = data.toString();
|
|
216
|
-
printer.output(text.trim());
|
|
217
|
-
output += text;
|
|
218
|
-
stdout += text;
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
proc.stderr.on('data', (data) => {
|
|
222
|
-
const text = data.toString();
|
|
223
|
-
printer.output(text.trim(), true);
|
|
224
|
-
output += text;
|
|
225
|
-
stderr += text;
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
proc.on('close', (code) => {
|
|
229
|
-
const combinedOutput = (output).trim();
|
|
230
|
-
const metadata = {
|
|
231
|
-
taskType: 'exec',
|
|
232
|
-
exitCode: code,
|
|
233
|
-
stdout: stdout.trim(),
|
|
234
|
-
stderr: stderr.trim()
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
if (code === 0) {
|
|
238
|
-
printer.finish(cmd);
|
|
239
|
-
resolve(TaskResult.success(combinedOutput, metadata));
|
|
240
|
-
} else {
|
|
241
|
-
printer.error(cmd, new Error(`Exit code: ${code}`));
|
|
242
|
-
resolve(TaskResult.fail(combinedOutput, metadata));
|
|
243
|
-
}
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
proc.on('error', (error) => {
|
|
247
|
-
printer.error(cmd, error);
|
|
248
|
-
resolve(TaskResult.fail(error.message, { taskType: 'exec' }));
|
|
249
|
-
});
|
|
250
|
-
});
|
|
6
|
+
return shell(strings, ...values);
|
|
251
7
|
}
|
package/src/tasks/fetch.js
CHANGED
|
@@ -23,6 +23,7 @@ export default async function httpFetch() {
|
|
|
23
23
|
|
|
24
24
|
try {
|
|
25
25
|
const response = await fetch(...arguments);
|
|
26
|
+
const responseForBody = response.clone(); // Clone before streaming consumes the body
|
|
26
27
|
const textDecoder = new TextDecoder();
|
|
27
28
|
let output = '';
|
|
28
29
|
|
|
@@ -40,7 +41,7 @@ export default async function httpFetch() {
|
|
|
40
41
|
|
|
41
42
|
const metadata = {
|
|
42
43
|
taskType: 'fetch',
|
|
43
|
-
response:
|
|
44
|
+
response: responseForBody, // Unconsumed clone so json()/text() work later
|
|
44
45
|
status: response.status,
|
|
45
46
|
statusText: response.statusText,
|
|
46
47
|
headers: Object.fromEntries(response.headers.entries())
|