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/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 program, { BUNOSHFILE, banner } from "./src/program.js";
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
- let jsCode = '';
54
-
55
- // Check if code is provided as argument
56
- if (eFlagIndex + 1 < process.argv.length && !process.argv[eFlagIndex + 1].startsWith('-')) {
57
- jsCode = process.argv[eFlagIndex + 1];
58
- } else if (!process.stdin.isTTY) {
59
- // Read from stdin only if it's not a TTY (i.e., it's being piped)
60
- const chunks = [];
61
- for await (const chunk of process.stdin) {
62
- chunks.push(chunk);
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
- // Make bunosh globals available to the function by creating wrapper functions
77
- for (const [key, value] of Object.entries(global.bunosh)) {
78
- if (typeof value === 'function') {
79
- // For template literal tag functions like exec and shell, create a wrapper
80
- // that allows them to be called as regular functions with a string
81
- if (key === 'exec' || key === 'writeToFile') {
82
- globalThis[key] = (str) => {
83
- // If called as a regular function with a string, convert to template literal call
84
- if (typeof str === 'string') {
85
- return value([str]);
86
- }
87
- // Otherwise call normally
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
- // Execute the JavaScript code with bunosh globals available
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
- import(tasksFile).then(async (tasks) => {
131
- try {
132
- const source = readFileSync(tasksFile, "utf-8");
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 && process.env.BUNOSH_DEBUG) {
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
- // Check if we're in test environment
327
+
328
+ const commandArgs = process.argv.slice(2);
191
329
  const isTestEnvironment = process.env.NODE_ENV === 'test' ||
192
- typeof Bun?.jest !== 'undefined' ||
193
- process.argv.some(arg => arg.includes('vitest') || arg.includes('jest') || arg.includes('--test') || arg.includes('test:'));
194
-
195
- // Set exit code to 1 if any tasks failed AND we're not in test environment
196
- if (tasksFailed > 0 && !isTestEnvironment) {
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.1",
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 = 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: '', color: 'green' },
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
- line = chalk.bgGray(line);
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