bunosh 0.4.0 → 0.4.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 +236 -60
- package/bunosh.js +207 -65
- package/package.json +7 -5
- package/src/completion.js +15 -2
- package/src/formatters/console.js +10 -5
- package/src/formatters/factory.js +30 -0
- package/src/io.js +5 -0
- package/src/mcp-server.js +575 -0
- package/src/printer.js +1 -1
- package/src/program.js +564 -486
- package/src/task.js +72 -9
- package/src/tasks/ai.js +10 -2
- package/src/tasks/exec.js +31 -9
- package/src/tasks/fetch.js +11 -3
- package/src/tasks/shell.js +18 -4
- package/src/upgrade.js +279 -199
package/bunosh.js
CHANGED
|
@@ -13,98 +13,248 @@ globalThis._bunoshGlobalTaskStatus = {
|
|
|
13
13
|
|
|
14
14
|
// Now import modules
|
|
15
15
|
|
|
16
|
-
import
|
|
16
|
+
import bunosh, { BUNOSHFILE, banner } from "./src/program.js";
|
|
17
17
|
import { existsSync, readFileSync, statSync } from "fs";
|
|
18
18
|
import init from "./src/init.js";
|
|
19
19
|
import path from "path";
|
|
20
|
+
import { config } from "dotenv";
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Load environment variables from .env files following Bun's loading order
|
|
24
|
+
* @param {string} bunoshfileDir - Directory containing the Bunoshfile
|
|
25
|
+
* @param {string} customEnvFile - Optional custom env file path from --env-file option
|
|
26
|
+
*/
|
|
27
|
+
function loadEnvFiles(bunoshfileDir, customEnvFile = null) {
|
|
28
|
+
if (customEnvFile) {
|
|
29
|
+
// Load the specified env file
|
|
30
|
+
const customEnvPath = path.isAbsolute(customEnvFile)
|
|
31
|
+
? customEnvFile
|
|
32
|
+
: path.resolve(bunoshfileDir, customEnvFile);
|
|
33
|
+
|
|
34
|
+
if (existsSync(customEnvPath)) {
|
|
35
|
+
config({ path: customEnvPath });
|
|
36
|
+
} else {
|
|
37
|
+
console.warn(`Warning: Specified env file not found: ${customEnvPath}`);
|
|
38
|
+
}
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Follow Bun's automatic loading order
|
|
43
|
+
const envFiles = [
|
|
44
|
+
'.env',
|
|
45
|
+
'.env.production',
|
|
46
|
+
'.env.development',
|
|
47
|
+
'.env.test',
|
|
48
|
+
'.env.local'
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
envFiles.forEach(envFile => {
|
|
52
|
+
const envPath = path.join(bunoshfileDir, envFile);
|
|
53
|
+
if (existsSync(envPath)) {
|
|
54
|
+
config({ path: envPath });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function loadBunoshfiles(tasksFile) {
|
|
60
|
+
const path = await import('path');
|
|
61
|
+
const fs = await import('fs');
|
|
62
|
+
|
|
63
|
+
// Get the directory and base name of the tasks file
|
|
64
|
+
const dir = path.dirname(tasksFile);
|
|
65
|
+
const baseName = path.basename(tasksFile, '.js');
|
|
66
|
+
|
|
67
|
+
// Find all matching Bunoshfile variants
|
|
68
|
+
const files = fs.readdirSync(dir);
|
|
69
|
+
const bunoshFiles = files
|
|
70
|
+
.filter(file => {
|
|
71
|
+
// Match Bunoshfile.js, Bunoshfile.*.js
|
|
72
|
+
const regex = new RegExp(`^${baseName}(\\.\\w+)?\\.js$`);
|
|
73
|
+
return regex.test(file);
|
|
74
|
+
})
|
|
75
|
+
.sort(); // Ensure consistent order
|
|
76
|
+
|
|
77
|
+
const allTasks = {};
|
|
78
|
+
const allSources = {};
|
|
79
|
+
|
|
80
|
+
for (const file of bunoshFiles) {
|
|
81
|
+
const filePath = path.join(dir, file);
|
|
82
|
+
try {
|
|
83
|
+
// Extract namespace from filename
|
|
84
|
+
let namespace = '';
|
|
85
|
+
if (file !== `${baseName}.js`) {
|
|
86
|
+
// Remove baseName and .js, then remove the leading dot
|
|
87
|
+
namespace = file.slice(baseName.length, -3).substring(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const tasks = await import(filePath);
|
|
91
|
+
const source = fs.readFileSync(filePath, 'utf-8');
|
|
92
|
+
|
|
93
|
+
// Add namespace prefix to tasks if namespace exists
|
|
94
|
+
if (namespace) {
|
|
95
|
+
Object.keys(tasks).forEach(key => {
|
|
96
|
+
if (typeof tasks[key] === 'function') {
|
|
97
|
+
allTasks[`${namespace}:${key}`] = tasks[key];
|
|
98
|
+
allSources[`${namespace}:${key}`] = { source, namespace, originalFnName: key };
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
} else {
|
|
102
|
+
// No namespace for the main Bunoshfile
|
|
103
|
+
Object.keys(tasks).forEach(key => {
|
|
104
|
+
if (typeof tasks[key] === 'function') {
|
|
105
|
+
allTasks[key] = tasks[key];
|
|
106
|
+
allSources[key] = { source, namespace: '', originalFnName: key };
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.warn(`Warning: Could not load ${file}:`, error.message);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { tasks: allTasks, sources: allSources };
|
|
116
|
+
}
|
|
21
117
|
|
|
22
118
|
async function main() {
|
|
23
119
|
|
|
24
|
-
// Parse --bunoshfile flag before importing tasks
|
|
120
|
+
// Parse --bunoshfile flag or BUNOSHFILE env var before importing tasks
|
|
25
121
|
const bunoshfileIndex = process.argv.indexOf('--bunoshfile');
|
|
26
122
|
let customBunoshfile = null;
|
|
123
|
+
|
|
27
124
|
if (bunoshfileIndex !== -1 && bunoshfileIndex + 1 < process.argv.length) {
|
|
28
125
|
customBunoshfile = process.argv[bunoshfileIndex + 1];
|
|
29
126
|
// Remove the flag and its value from process.argv so it doesn't interfere with command parsing
|
|
30
127
|
process.argv.splice(bunoshfileIndex, 2);
|
|
128
|
+
} else if (process.env.BUNOSHFILE) {
|
|
129
|
+
customBunoshfile = process.env.BUNOSHFILE;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Parse --env-file flag
|
|
133
|
+
const envFileIndex = process.argv.findIndex(arg => arg.startsWith('--env-file='));
|
|
134
|
+
let customEnvFile = null;
|
|
135
|
+
|
|
136
|
+
if (envFileIndex !== -1) {
|
|
137
|
+
customEnvFile = process.argv[envFileIndex].split('=')[1];
|
|
138
|
+
// Remove the flag from process.argv so it doesn't interfere with command parsing
|
|
139
|
+
process.argv.splice(envFileIndex, 1);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check for -mcp flag first
|
|
143
|
+
const mcpFlagIndex = process.argv.indexOf('-mcp');
|
|
144
|
+
if (mcpFlagIndex !== -1) {
|
|
145
|
+
// Remove the flag from process.argv
|
|
146
|
+
process.argv.splice(mcpFlagIndex, 1);
|
|
147
|
+
|
|
148
|
+
// Set environment variable to indicate MCP mode
|
|
149
|
+
process.env.BUNOSH_MCP_MODE = 'true';
|
|
150
|
+
|
|
151
|
+
// Import MCP server and start it
|
|
152
|
+
const { createMcpServer, startMcpServer } = await import('./src/mcp-server.js');
|
|
153
|
+
|
|
154
|
+
let tasksFile;
|
|
155
|
+
let bunoshfileDir;
|
|
156
|
+
if (customBunoshfile) {
|
|
157
|
+
const resolvedPath = path.isAbsolute(customBunoshfile) ? customBunoshfile : path.resolve(customBunoshfile);
|
|
158
|
+
// If it's a directory, append the default BUNOSHFILE
|
|
159
|
+
if (existsSync(resolvedPath) && statSync(resolvedPath).isDirectory()) {
|
|
160
|
+
tasksFile = path.join(resolvedPath, BUNOSHFILE);
|
|
161
|
+
bunoshfileDir = resolvedPath;
|
|
162
|
+
} else {
|
|
163
|
+
tasksFile = resolvedPath;
|
|
164
|
+
bunoshfileDir = path.dirname(resolvedPath);
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
tasksFile = path.join(process.cwd(), BUNOSHFILE);
|
|
168
|
+
bunoshfileDir = process.cwd();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!existsSync(tasksFile)) {
|
|
172
|
+
console.error('Bunoshfile not found for MCP mode');
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Load environment files from the Bunoshfile directory
|
|
177
|
+
loadEnvFiles(bunoshfileDir, customEnvFile);
|
|
178
|
+
|
|
179
|
+
// Load tasks and sources
|
|
180
|
+
const { tasks, sources } = await loadBunoshfiles(tasksFile);
|
|
181
|
+
|
|
182
|
+
// Create and start MCP server
|
|
183
|
+
const server = createMcpServer(tasks, sources);
|
|
184
|
+
await startMcpServer(server);
|
|
185
|
+
|
|
186
|
+
return; // Exit early for MCP mode
|
|
31
187
|
}
|
|
32
188
|
|
|
33
189
|
let tasksFile;
|
|
190
|
+
let bunoshfileDir;
|
|
34
191
|
if (customBunoshfile) {
|
|
35
192
|
const resolvedPath = path.isAbsolute(customBunoshfile) ? customBunoshfile : path.resolve(customBunoshfile);
|
|
36
193
|
// If it's a directory, append the default BUNOSHFILE
|
|
37
194
|
if (existsSync(resolvedPath) && statSync(resolvedPath).isDirectory()) {
|
|
38
195
|
tasksFile = path.join(resolvedPath, BUNOSHFILE);
|
|
196
|
+
bunoshfileDir = resolvedPath;
|
|
39
197
|
// Change working directory to the bunoshfile directory
|
|
40
198
|
process.chdir(resolvedPath);
|
|
41
199
|
} else {
|
|
42
200
|
tasksFile = resolvedPath;
|
|
201
|
+
bunoshfileDir = path.dirname(resolvedPath);
|
|
43
202
|
// Change working directory to the bunoshfile's directory
|
|
44
203
|
process.chdir(path.dirname(resolvedPath));
|
|
45
204
|
}
|
|
46
205
|
} else {
|
|
47
206
|
tasksFile = path.join(process.cwd(), BUNOSHFILE);
|
|
207
|
+
bunoshfileDir = process.cwd();
|
|
48
208
|
}
|
|
49
209
|
|
|
210
|
+
// Load environment files from the Bunoshfile directory
|
|
211
|
+
loadEnvFiles(bunoshfileDir, customEnvFile);
|
|
212
|
+
|
|
50
213
|
// Handle -e flag for executing JavaScript code
|
|
51
214
|
const eFlagIndex = process.argv.indexOf('-e');
|
|
52
215
|
if (eFlagIndex !== -1) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
216
|
+
(async () => {
|
|
217
|
+
let jsCode = '';
|
|
218
|
+
|
|
219
|
+
// Check if code is provided as argument
|
|
220
|
+
if (eFlagIndex + 1 < process.argv.length && !process.argv[eFlagIndex + 1].startsWith('-')) {
|
|
221
|
+
jsCode = process.argv[eFlagIndex + 1];
|
|
222
|
+
} else if (!process.stdin.isTTY) {
|
|
223
|
+
// Read from stdin only if it's not a TTY (i.e., it's being piped)
|
|
224
|
+
const chunks = [];
|
|
225
|
+
for await (const chunk of process.stdin) {
|
|
226
|
+
chunks.push(chunk);
|
|
227
|
+
}
|
|
228
|
+
jsCode = Buffer.concat(chunks).toString('utf8').trim();
|
|
63
229
|
}
|
|
64
|
-
jsCode = Buffer.concat(chunks).toString('utf8').trim();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (!jsCode) {
|
|
68
|
-
console.error('No JavaScript code provided');
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
// Import bunosh globals before executing JavaScript
|
|
74
|
-
await import('./index.js');
|
|
75
230
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return value(str, ...Array.from(arguments).slice(1));
|
|
89
|
-
};
|
|
90
|
-
} else if (key === 'shell') {
|
|
91
|
-
// Shell function has special handling for string arguments - it falls back to exec
|
|
92
|
-
globalThis[key] = value;
|
|
93
|
-
} else {
|
|
231
|
+
if (!jsCode) {
|
|
232
|
+
console.error('No JavaScript code provided');
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
// Import bunosh globals before executing JavaScript
|
|
238
|
+
await import('./index.js');
|
|
239
|
+
|
|
240
|
+
// Make bunosh globals available to the function
|
|
241
|
+
for (const [key, value] of Object.entries(global.bunosh)) {
|
|
242
|
+
if (typeof value === 'function') {
|
|
94
243
|
globalThis[key] = value;
|
|
95
244
|
}
|
|
96
245
|
}
|
|
246
|
+
|
|
247
|
+
// Execute the JavaScript code with bunosh globals available
|
|
248
|
+
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
|
|
249
|
+
const func = new AsyncFunction(jsCode);
|
|
250
|
+
await func();
|
|
251
|
+
process.exit(0);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.error('Error executing JavaScript:', error.message);
|
|
254
|
+
process.exit(1);
|
|
97
255
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
|
|
101
|
-
const func = new AsyncFunction(jsCode);
|
|
102
|
-
await func();
|
|
103
|
-
return;
|
|
104
|
-
} catch (error) {
|
|
105
|
-
console.error('Error executing JavaScript:', error.message);
|
|
106
|
-
process.exit(1);
|
|
107
|
-
}
|
|
256
|
+
})();
|
|
257
|
+
return;
|
|
108
258
|
}
|
|
109
259
|
|
|
110
260
|
if (!existsSync(tasksFile)) {
|
|
@@ -124,19 +274,11 @@ async function main() {
|
|
|
124
274
|
process.exit(1);
|
|
125
275
|
}
|
|
126
276
|
|
|
127
|
-
// Import bunosh globals for normal operation
|
|
128
277
|
await import('./index.js');
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
await program(tasks, source);
|
|
134
|
-
} catch (error) {
|
|
135
|
-
handleBunoshfileError(error, tasksFile);
|
|
136
|
-
}
|
|
137
|
-
}).catch((error) => {
|
|
138
|
-
handleBunoshfileError(error, tasksFile);
|
|
139
|
-
});
|
|
278
|
+
|
|
279
|
+
// Load all Bunoshfile variants
|
|
280
|
+
const { tasks, sources } = await loadBunoshfiles(tasksFile);
|
|
281
|
+
await bunosh(tasks, sources);
|
|
140
282
|
}
|
|
141
283
|
|
|
142
284
|
main().catch((error) => {
|
|
@@ -162,7 +304,7 @@ process.on('uncaughtException', (error) => {
|
|
|
162
304
|
|
|
163
305
|
console.error('\n❌ Uncaught Exception:');
|
|
164
306
|
console.error(error.message);
|
|
165
|
-
if (error.stack
|
|
307
|
+
if (error.stack) {
|
|
166
308
|
console.error(error.stack);
|
|
167
309
|
}
|
|
168
310
|
process.exit(1);
|
|
@@ -189,7 +331,7 @@ process.on('exit', (code) => {
|
|
|
189
331
|
|
|
190
332
|
// Check if we're in test environment
|
|
191
333
|
const isTestEnvironment = process.env.NODE_ENV === 'test' ||
|
|
192
|
-
typeof Bun?.jest !== 'undefined' ||
|
|
334
|
+
(typeof Bun !== 'undefined' && typeof Bun?.jest !== 'undefined') ||
|
|
193
335
|
process.argv.some(arg => arg.includes('vitest') || arg.includes('jest') || arg.includes('--test') || arg.includes('test:'));
|
|
194
336
|
|
|
195
337
|
// Set exit code to 1 if any tasks failed AND we're not in test environment
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bunosh",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"module": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -19,10 +19,12 @@
|
|
|
19
19
|
"@ai-sdk/openai": "^2.0.23",
|
|
20
20
|
"@babel/parser": "^7.27.5",
|
|
21
21
|
"@babel/traverse": "^7.27.4",
|
|
22
|
+
"@modelcontextprotocol/sdk": "^1.19.1",
|
|
22
23
|
"ai": "^5.0.29",
|
|
23
24
|
"chalk": "^5.4.1",
|
|
24
25
|
"commander": "^14.0.0",
|
|
25
26
|
"debug": "^4.4.1",
|
|
27
|
+
"dotenv": "^17.2.3",
|
|
26
28
|
"fs-extra": "^11.3.0",
|
|
27
29
|
"inquirer": "^12.6.3",
|
|
28
30
|
"timer-node": "^5.0.9",
|
|
@@ -46,11 +48,11 @@
|
|
|
46
48
|
"test:watch": "bun test test/ --watch",
|
|
47
49
|
"test:e2e": "vitest run",
|
|
48
50
|
"test:e2e:watch": "vitest",
|
|
49
|
-
"build": "bun build ./bunosh.js --compile --outfile bunosh",
|
|
51
|
+
"build": "bun build ./bunosh.js --compile --define BUNOSH_EXECUTABLE=true --outfile bunosh",
|
|
50
52
|
"build:all": "npm run build:linux && npm run build:macos && npm run build:windows",
|
|
51
|
-
"build:linux": "bun build ./bunosh.js --compile --target=bun-linux-x64 --outfile dist/bunosh-linux-x64",
|
|
52
|
-
"build:macos": "bun build ./bunosh.js --compile --target=bun-darwin-arm64 --outfile dist/bunosh-darwin-arm64",
|
|
53
|
-
"build:windows": "bun build ./bunosh.js --compile --target=bun-windows-x64 --outfile dist/bunosh-windows-x64.exe",
|
|
53
|
+
"build:linux": "bun build ./bunosh.js --compile --define BUNOSH_EXECUTABLE=true --target=bun-linux-x64 --outfile dist/bunosh-linux-x64",
|
|
54
|
+
"build:macos": "bun build ./bunosh.js --compile --define BUNOSH_EXECUTABLE=true --target=bun-darwin-arm64 --outfile dist/bunosh-darwin-arm64",
|
|
55
|
+
"build:windows": "bun build ./bunosh.js --compile --define BUNOSH_EXECUTABLE=true --target=bun-windows-x64 --outfile dist/bunosh-windows-x64.exe",
|
|
54
56
|
"test:build": "npm run build && ./bunosh --help && rm bunosh",
|
|
55
57
|
"hello:other": "bunosh hello:other",
|
|
56
58
|
"hello:world": "bunosh hello:world",
|
package/src/completion.js
CHANGED
|
@@ -188,11 +188,24 @@ export function getCompletionCommands() {
|
|
|
188
188
|
* Converts function name to command name (same logic as program.js)
|
|
189
189
|
*/
|
|
190
190
|
function prepareCommandName(name) {
|
|
191
|
-
name
|
|
191
|
+
// name is already the final command name (could be namespaced or not)
|
|
192
|
+
// For namespaced commands, only transform the function part (after the last colon)
|
|
193
|
+
const lastColonIndex = name.lastIndexOf(':');
|
|
194
|
+
if (lastColonIndex !== -1) {
|
|
195
|
+
const namespace = name.substring(0, lastColonIndex);
|
|
196
|
+
const commandPart = name.substring(lastColonIndex + 1);
|
|
197
|
+
return `${namespace}:${toKebabCase(commandPart)}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// For non-namespaced commands, just convert to kebab-case
|
|
201
|
+
return toKebabCase(name);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function toKebabCase(name) {
|
|
205
|
+
return name
|
|
192
206
|
.split(/(?=[A-Z])/)
|
|
193
207
|
.join("-")
|
|
194
208
|
.toLowerCase();
|
|
195
|
-
return name.replace("-", ":");
|
|
196
209
|
}
|
|
197
210
|
|
|
198
211
|
/**
|
|
@@ -3,7 +3,7 @@ import { BaseFormatter } from './base.js';
|
|
|
3
3
|
|
|
4
4
|
const STATUS_CONFIG = {
|
|
5
5
|
start: { icon: '▶', color: 'blue' },
|
|
6
|
-
finish: { icon: '
|
|
6
|
+
finish: { icon: '✔', color: 'green' },
|
|
7
7
|
error: { icon: '✗', color: 'red' },
|
|
8
8
|
warning: { icon: '⚠', color: 'yellow' },
|
|
9
9
|
output: { icon: ' ', color: 'white' },
|
|
@@ -11,6 +11,9 @@ const STATUS_CONFIG = {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
export class ConsoleFormatter extends BaseFormatter {
|
|
14
|
+
shouldDelayStart() {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
14
17
|
format(taskName, status, taskType, extra = {}) {
|
|
15
18
|
const config = STATUS_CONFIG[status];
|
|
16
19
|
if (!config) {
|
|
@@ -20,7 +23,7 @@ export class ConsoleFormatter extends BaseFormatter {
|
|
|
20
23
|
const icon = chalk[config.color](config.icon);
|
|
21
24
|
const taskTypeFormatted = taskType ? chalk.bold(taskType) + ' ' : '';
|
|
22
25
|
const taskNameFormatted = chalk.yellow(taskName);
|
|
23
|
-
|
|
26
|
+
|
|
24
27
|
const extraParts = [];
|
|
25
28
|
Object.entries(extra).forEach(([key, value]) => {
|
|
26
29
|
if (value !== null && value !== undefined) {
|
|
@@ -41,7 +44,7 @@ export class ConsoleFormatter extends BaseFormatter {
|
|
|
41
44
|
const terminalWidth = process.stdout.columns || 100;
|
|
42
45
|
let leftContent = `${icon} ${taskTypeFormatted}${taskNameFormatted}`;
|
|
43
46
|
let rightContent = '';
|
|
44
|
-
|
|
47
|
+
|
|
45
48
|
if (extraParts.length > 0) {
|
|
46
49
|
rightContent = `(${extraParts.join(', ')})`;
|
|
47
50
|
}
|
|
@@ -53,7 +56,9 @@ export class ConsoleFormatter extends BaseFormatter {
|
|
|
53
56
|
let line = leftContent + padding + rightContent;
|
|
54
57
|
|
|
55
58
|
if (icon.trim()) {
|
|
56
|
-
|
|
59
|
+
// Apply underline to the task name/type only, not the status in parentheses
|
|
60
|
+
const underlineContent = taskTypeFormatted + taskNameFormatted + padding;
|
|
61
|
+
line = icon + ' ' + chalk.underline(underlineContent) + rightContent;
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
let result = line;
|
|
@@ -79,4 +84,4 @@ export class ConsoleFormatter extends BaseFormatter {
|
|
|
79
84
|
static detect() {
|
|
80
85
|
return !process.env.CI;
|
|
81
86
|
}
|
|
82
|
-
}
|
|
87
|
+
}
|
|
@@ -6,7 +6,28 @@ const FORMATTERS = [
|
|
|
6
6
|
ConsoleFormatter
|
|
7
7
|
];
|
|
8
8
|
|
|
9
|
+
// Global test formatter override
|
|
10
|
+
let testFormatterOverride = null;
|
|
11
|
+
|
|
9
12
|
export function createFormatter() {
|
|
13
|
+
// Use test override if set
|
|
14
|
+
if (testFormatterOverride) {
|
|
15
|
+
return new testFormatterOverride();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Check for explicit formatter override via environment variable
|
|
19
|
+
if (process.env.BUNOSH_FORMATTER) {
|
|
20
|
+
const requested = process.env.BUNOSH_FORMATTER.toLowerCase();
|
|
21
|
+
switch (requested) {
|
|
22
|
+
case 'console':
|
|
23
|
+
return new ConsoleFormatter();
|
|
24
|
+
case 'github-actions':
|
|
25
|
+
return new GitHubActionsFormatter();
|
|
26
|
+
default:
|
|
27
|
+
console.warn(`Unknown BUNOSH_FORMATTER: ${requested}. Using auto-detection.`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
10
31
|
for (const FormatterClass of FORMATTERS) {
|
|
11
32
|
if (FormatterClass.detect && FormatterClass.detect()) {
|
|
12
33
|
return new FormatterClass();
|
|
@@ -14,4 +35,13 @@ export function createFormatter() {
|
|
|
14
35
|
}
|
|
15
36
|
|
|
16
37
|
return new ConsoleFormatter();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Test utilities
|
|
41
|
+
export function setTestFormatter(FormatterClass) {
|
|
42
|
+
testFormatterOverride = FormatterClass;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function clearTestFormatter() {
|
|
46
|
+
testFormatterOverride = null;
|
|
17
47
|
}
|
package/src/io.js
CHANGED
|
@@ -7,6 +7,11 @@ 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
|
+
|
|
10
15
|
// Track that we're in an ask operation to prevent duplicate exit summaries
|
|
11
16
|
globalThis._bunoshInAskOperation = true;
|
|
12
17
|
// Smart parameter detection
|