bunosh 0.4.1 → 0.4.7
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 +202 -61
- package/bunosh.js +246 -82
- 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 +89 -11
- 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
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
globalThis._bunoshStartTime = Date.now();
|
|
5
5
|
globalThis._bunoshCommandCompleted = false;
|
|
6
6
|
globalThis._bunoshGlobalTasksExecuted = [];
|
|
7
|
+
globalThis._bunoshIgnoreFailuresMode = false;
|
|
7
8
|
globalThis._bunoshGlobalTaskStatus = {
|
|
8
9
|
RUNNING: 'running',
|
|
9
10
|
FAIL: 'fail',
|
|
@@ -13,98 +14,248 @@ globalThis._bunoshGlobalTaskStatus = {
|
|
|
13
14
|
|
|
14
15
|
// Now import modules
|
|
15
16
|
|
|
16
|
-
import
|
|
17
|
+
import bunosh, { BUNOSHFILE, banner } from "./src/program.js";
|
|
17
18
|
import { existsSync, readFileSync, statSync } from "fs";
|
|
18
19
|
import init from "./src/init.js";
|
|
19
20
|
import path from "path";
|
|
21
|
+
import { config } from "dotenv";
|
|
20
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Load environment variables from .env files following Bun's loading order
|
|
25
|
+
* @param {string} bunoshfileDir - Directory containing the Bunoshfile
|
|
26
|
+
* @param {string} customEnvFile - Optional custom env file path from --env-file option
|
|
27
|
+
*/
|
|
28
|
+
function loadEnvFiles(bunoshfileDir, customEnvFile = null) {
|
|
29
|
+
if (customEnvFile) {
|
|
30
|
+
// Load the specified env file
|
|
31
|
+
const customEnvPath = path.isAbsolute(customEnvFile)
|
|
32
|
+
? customEnvFile
|
|
33
|
+
: path.resolve(bunoshfileDir, customEnvFile);
|
|
34
|
+
|
|
35
|
+
if (existsSync(customEnvPath)) {
|
|
36
|
+
config({ path: customEnvPath });
|
|
37
|
+
} else {
|
|
38
|
+
console.warn(`Warning: Specified env file not found: ${customEnvPath}`);
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Follow Bun's automatic loading order
|
|
44
|
+
const envFiles = [
|
|
45
|
+
'.env',
|
|
46
|
+
'.env.production',
|
|
47
|
+
'.env.development',
|
|
48
|
+
'.env.test',
|
|
49
|
+
'.env.local'
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
envFiles.forEach(envFile => {
|
|
53
|
+
const envPath = path.join(bunoshfileDir, envFile);
|
|
54
|
+
if (existsSync(envPath)) {
|
|
55
|
+
config({ path: envPath });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function loadBunoshfiles(tasksFile) {
|
|
61
|
+
const path = await import('path');
|
|
62
|
+
const fs = await import('fs');
|
|
63
|
+
|
|
64
|
+
// Get the directory and base name of the tasks file
|
|
65
|
+
const dir = path.dirname(tasksFile);
|
|
66
|
+
const baseName = path.basename(tasksFile, '.js');
|
|
67
|
+
|
|
68
|
+
// Find all matching Bunoshfile variants
|
|
69
|
+
const files = fs.readdirSync(dir);
|
|
70
|
+
const bunoshFiles = files
|
|
71
|
+
.filter(file => {
|
|
72
|
+
// Match Bunoshfile.js, Bunoshfile.*.js
|
|
73
|
+
const regex = new RegExp(`^${baseName}(\\.\\w+)?\\.js$`);
|
|
74
|
+
return regex.test(file);
|
|
75
|
+
})
|
|
76
|
+
.sort(); // Ensure consistent order
|
|
77
|
+
|
|
78
|
+
const allTasks = {};
|
|
79
|
+
const allSources = {};
|
|
80
|
+
|
|
81
|
+
for (const file of bunoshFiles) {
|
|
82
|
+
const filePath = path.join(dir, file);
|
|
83
|
+
try {
|
|
84
|
+
// Extract namespace from filename
|
|
85
|
+
let namespace = '';
|
|
86
|
+
if (file !== `${baseName}.js`) {
|
|
87
|
+
// Remove baseName and .js, then remove the leading dot
|
|
88
|
+
namespace = file.slice(baseName.length, -3).substring(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const tasks = await import(filePath);
|
|
92
|
+
const source = fs.readFileSync(filePath, 'utf-8');
|
|
93
|
+
|
|
94
|
+
// Add namespace prefix to tasks if namespace exists
|
|
95
|
+
if (namespace) {
|
|
96
|
+
Object.keys(tasks).forEach(key => {
|
|
97
|
+
if (typeof tasks[key] === 'function') {
|
|
98
|
+
allTasks[`${namespace}:${key}`] = tasks[key];
|
|
99
|
+
allSources[`${namespace}:${key}`] = { source, namespace, originalFnName: key };
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
} else {
|
|
103
|
+
// No namespace for the main Bunoshfile
|
|
104
|
+
Object.keys(tasks).forEach(key => {
|
|
105
|
+
if (typeof tasks[key] === 'function') {
|
|
106
|
+
allTasks[key] = tasks[key];
|
|
107
|
+
allSources[key] = { source, namespace: '', originalFnName: key };
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.warn(`Warning: Could not load ${file}:`, error.message);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { tasks: allTasks, sources: allSources };
|
|
117
|
+
}
|
|
21
118
|
|
|
22
119
|
async function main() {
|
|
23
120
|
|
|
24
|
-
// Parse --bunoshfile flag before importing tasks
|
|
121
|
+
// Parse --bunoshfile flag or BUNOSHFILE env var before importing tasks
|
|
25
122
|
const bunoshfileIndex = process.argv.indexOf('--bunoshfile');
|
|
26
123
|
let customBunoshfile = null;
|
|
124
|
+
|
|
27
125
|
if (bunoshfileIndex !== -1 && bunoshfileIndex + 1 < process.argv.length) {
|
|
28
126
|
customBunoshfile = process.argv[bunoshfileIndex + 1];
|
|
29
127
|
// Remove the flag and its value from process.argv so it doesn't interfere with command parsing
|
|
30
128
|
process.argv.splice(bunoshfileIndex, 2);
|
|
129
|
+
} else if (process.env.BUNOSHFILE) {
|
|
130
|
+
customBunoshfile = process.env.BUNOSHFILE;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Parse --env-file flag
|
|
134
|
+
const envFileIndex = process.argv.findIndex(arg => arg.startsWith('--env-file='));
|
|
135
|
+
let customEnvFile = null;
|
|
136
|
+
|
|
137
|
+
if (envFileIndex !== -1) {
|
|
138
|
+
customEnvFile = process.argv[envFileIndex].split('=')[1];
|
|
139
|
+
// Remove the flag from process.argv so it doesn't interfere with command parsing
|
|
140
|
+
process.argv.splice(envFileIndex, 1);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check for -mcp flag first
|
|
144
|
+
const mcpFlagIndex = process.argv.indexOf('-mcp');
|
|
145
|
+
if (mcpFlagIndex !== -1) {
|
|
146
|
+
// Remove the flag from process.argv
|
|
147
|
+
process.argv.splice(mcpFlagIndex, 1);
|
|
148
|
+
|
|
149
|
+
// Set environment variable to indicate MCP mode
|
|
150
|
+
process.env.BUNOSH_MCP_MODE = 'true';
|
|
151
|
+
|
|
152
|
+
// Import MCP server and start it
|
|
153
|
+
const { createMcpServer, startMcpServer } = await import('./src/mcp-server.js');
|
|
154
|
+
|
|
155
|
+
let tasksFile;
|
|
156
|
+
let bunoshfileDir;
|
|
157
|
+
if (customBunoshfile) {
|
|
158
|
+
const resolvedPath = path.isAbsolute(customBunoshfile) ? customBunoshfile : path.resolve(customBunoshfile);
|
|
159
|
+
// If it's a directory, append the default BUNOSHFILE
|
|
160
|
+
if (existsSync(resolvedPath) && statSync(resolvedPath).isDirectory()) {
|
|
161
|
+
tasksFile = path.join(resolvedPath, BUNOSHFILE);
|
|
162
|
+
bunoshfileDir = resolvedPath;
|
|
163
|
+
} else {
|
|
164
|
+
tasksFile = resolvedPath;
|
|
165
|
+
bunoshfileDir = path.dirname(resolvedPath);
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
tasksFile = path.join(process.cwd(), BUNOSHFILE);
|
|
169
|
+
bunoshfileDir = process.cwd();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!existsSync(tasksFile)) {
|
|
173
|
+
console.error('Bunoshfile not found for MCP mode');
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Load environment files from the Bunoshfile directory
|
|
178
|
+
loadEnvFiles(bunoshfileDir, customEnvFile);
|
|
179
|
+
|
|
180
|
+
// Load tasks and sources
|
|
181
|
+
const { tasks, sources } = await loadBunoshfiles(tasksFile);
|
|
182
|
+
|
|
183
|
+
// Create and start MCP server
|
|
184
|
+
const server = createMcpServer(tasks, sources);
|
|
185
|
+
await startMcpServer(server);
|
|
186
|
+
|
|
187
|
+
return; // Exit early for MCP mode
|
|
31
188
|
}
|
|
32
189
|
|
|
33
190
|
let tasksFile;
|
|
191
|
+
let bunoshfileDir;
|
|
34
192
|
if (customBunoshfile) {
|
|
35
193
|
const resolvedPath = path.isAbsolute(customBunoshfile) ? customBunoshfile : path.resolve(customBunoshfile);
|
|
36
194
|
// If it's a directory, append the default BUNOSHFILE
|
|
37
195
|
if (existsSync(resolvedPath) && statSync(resolvedPath).isDirectory()) {
|
|
38
196
|
tasksFile = path.join(resolvedPath, BUNOSHFILE);
|
|
197
|
+
bunoshfileDir = resolvedPath;
|
|
39
198
|
// Change working directory to the bunoshfile directory
|
|
40
199
|
process.chdir(resolvedPath);
|
|
41
200
|
} else {
|
|
42
201
|
tasksFile = resolvedPath;
|
|
202
|
+
bunoshfileDir = path.dirname(resolvedPath);
|
|
43
203
|
// Change working directory to the bunoshfile's directory
|
|
44
204
|
process.chdir(path.dirname(resolvedPath));
|
|
45
205
|
}
|
|
46
206
|
} else {
|
|
47
207
|
tasksFile = path.join(process.cwd(), BUNOSHFILE);
|
|
208
|
+
bunoshfileDir = process.cwd();
|
|
48
209
|
}
|
|
49
210
|
|
|
211
|
+
// Load environment files from the Bunoshfile directory
|
|
212
|
+
loadEnvFiles(bunoshfileDir, customEnvFile);
|
|
213
|
+
|
|
50
214
|
// Handle -e flag for executing JavaScript code
|
|
51
215
|
const eFlagIndex = process.argv.indexOf('-e');
|
|
52
216
|
if (eFlagIndex !== -1) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
217
|
+
(async () => {
|
|
218
|
+
let jsCode = '';
|
|
219
|
+
|
|
220
|
+
// Check if code is provided as argument
|
|
221
|
+
if (eFlagIndex + 1 < process.argv.length && !process.argv[eFlagIndex + 1].startsWith('-')) {
|
|
222
|
+
jsCode = process.argv[eFlagIndex + 1];
|
|
223
|
+
} else if (!process.stdin.isTTY) {
|
|
224
|
+
// Read from stdin only if it's not a TTY (i.e., it's being piped)
|
|
225
|
+
const chunks = [];
|
|
226
|
+
for await (const chunk of process.stdin) {
|
|
227
|
+
chunks.push(chunk);
|
|
228
|
+
}
|
|
229
|
+
jsCode = Buffer.concat(chunks).toString('utf8').trim();
|
|
63
230
|
}
|
|
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
231
|
|
|
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 {
|
|
232
|
+
if (!jsCode) {
|
|
233
|
+
console.error('No JavaScript code provided');
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
// Import bunosh globals before executing JavaScript
|
|
239
|
+
await import('./index.js');
|
|
240
|
+
|
|
241
|
+
// Make bunosh globals available to the function
|
|
242
|
+
for (const [key, value] of Object.entries(global.bunosh)) {
|
|
243
|
+
if (typeof value === 'function') {
|
|
94
244
|
globalThis[key] = value;
|
|
95
245
|
}
|
|
96
246
|
}
|
|
247
|
+
|
|
248
|
+
// Execute the JavaScript code with bunosh globals available
|
|
249
|
+
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
|
|
250
|
+
const func = new AsyncFunction(jsCode);
|
|
251
|
+
await func();
|
|
252
|
+
process.exit(0);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
console.error('Error executing JavaScript:', error.message);
|
|
255
|
+
process.exit(1);
|
|
97
256
|
}
|
|
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
|
-
}
|
|
257
|
+
})();
|
|
258
|
+
return;
|
|
108
259
|
}
|
|
109
260
|
|
|
110
261
|
if (!existsSync(tasksFile)) {
|
|
@@ -124,19 +275,11 @@ async function main() {
|
|
|
124
275
|
process.exit(1);
|
|
125
276
|
}
|
|
126
277
|
|
|
127
|
-
// Import bunosh globals for normal operation
|
|
128
278
|
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
|
-
});
|
|
279
|
+
|
|
280
|
+
// Load all Bunoshfile variants
|
|
281
|
+
const { tasks, sources } = await loadBunoshfiles(tasksFile);
|
|
282
|
+
await bunosh(tasks, sources);
|
|
140
283
|
}
|
|
141
284
|
|
|
142
285
|
main().catch((error) => {
|
|
@@ -162,45 +305,66 @@ process.on('uncaughtException', (error) => {
|
|
|
162
305
|
|
|
163
306
|
console.error('\n❌ Uncaught Exception:');
|
|
164
307
|
console.error(error.message);
|
|
165
|
-
if (error.stack
|
|
308
|
+
if (error.stack) {
|
|
166
309
|
console.error(error.stack);
|
|
167
310
|
}
|
|
168
311
|
process.exit(1);
|
|
169
312
|
});
|
|
170
313
|
|
|
171
|
-
// Handle exit for task summary
|
|
172
314
|
process.on('exit', (code) => {
|
|
173
315
|
if (!process.env.BUNOSH_COMMAND_STARTED) return;
|
|
174
|
-
|
|
175
|
-
// Don't print summary if exit was due to stdin closing during an ask operation
|
|
176
|
-
// This prevents duplicate output when ask commands don't receive all required input
|
|
316
|
+
|
|
177
317
|
if (globalThis._bunoshInAskOperation && code === 0) {
|
|
178
318
|
return;
|
|
179
319
|
}
|
|
180
|
-
|
|
181
|
-
// Access global values directly
|
|
320
|
+
|
|
182
321
|
const tasksExecuted = globalThis._bunoshGlobalTasksExecuted || [];
|
|
183
322
|
const TaskStatus = globalThis._bunoshGlobalTaskStatus || { FAIL: 'fail', WARNING: 'warning' };
|
|
184
|
-
|
|
185
|
-
// Calculate total time from when the process started
|
|
323
|
+
|
|
186
324
|
const totalTime = Date.now() - globalThis._bunoshStartTime || 0;
|
|
187
325
|
const tasksFailed = tasksExecuted.filter(ti => ti.result?.status === TaskStatus.FAIL).length;
|
|
188
326
|
const tasksWarning = tasksExecuted.filter(ti => ti.result?.status === TaskStatus.WARNING).length;
|
|
189
|
-
|
|
190
|
-
|
|
327
|
+
|
|
328
|
+
const commandArgs = process.argv.slice(2);
|
|
191
329
|
const isTestEnvironment = process.env.NODE_ENV === 'test' ||
|
|
192
|
-
typeof Bun?.jest !== 'undefined' ||
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
330
|
+
(typeof Bun !== 'undefined' && typeof Bun?.jest !== 'undefined') ||
|
|
331
|
+
commandArgs.some(arg => {
|
|
332
|
+
const lowerArg = arg.toLowerCase();
|
|
333
|
+
return lowerArg.includes('vitest') ||
|
|
334
|
+
lowerArg.includes('jest') ||
|
|
335
|
+
lowerArg === '--test' ||
|
|
336
|
+
lowerArg.startsWith('test:');
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const ignoreFailuresMode = globalThis._bunoshIgnoreFailuresMode || false;
|
|
340
|
+
|
|
341
|
+
if (process.env.BUNOSH_DEBUG) {
|
|
342
|
+
console.log('\n[DEBUG] Exit handler:');
|
|
343
|
+
console.log(' tasksFailed:', tasksFailed);
|
|
344
|
+
console.log(' isTestEnvironment:', isTestEnvironment);
|
|
345
|
+
console.log(' ignoreFailuresMode:', ignoreFailuresMode);
|
|
346
|
+
console.log(' NODE_ENV:', process.env.NODE_ENV);
|
|
347
|
+
console.log(' commandArgs:', commandArgs);
|
|
348
|
+
console.log(' full process.argv:', process.argv);
|
|
349
|
+
if (isTestEnvironment) {
|
|
350
|
+
const matchingArg = commandArgs.find(arg => {
|
|
351
|
+
const lowerArg = arg.toLowerCase();
|
|
352
|
+
return lowerArg.includes('vitest') ||
|
|
353
|
+
lowerArg.includes('jest') ||
|
|
354
|
+
lowerArg === '--test' ||
|
|
355
|
+
lowerArg.startsWith('test:');
|
|
356
|
+
});
|
|
357
|
+
console.log(' Matched command arg:', matchingArg);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (tasksFailed > 0 && !isTestEnvironment && !ignoreFailuresMode) {
|
|
197
362
|
process.exitCode = 1;
|
|
198
363
|
}
|
|
199
|
-
|
|
200
|
-
const finalExitCode = (tasksFailed > 0 && !isTestEnvironment) ? 1 : code;
|
|
364
|
+
|
|
365
|
+
const finalExitCode = (tasksFailed > 0 && !isTestEnvironment && !ignoreFailuresMode) ? 1 : code;
|
|
201
366
|
const success = finalExitCode === 0;
|
|
202
367
|
|
|
203
|
-
// Debug: Check if this handler has already run
|
|
204
368
|
if (globalThis._bunoshExitHandlerCalled) {
|
|
205
369
|
console.log('\n[DEBUG] Exit handler already called, skipping duplicate');
|
|
206
370
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bunosh",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
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
|