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/bunosh.js CHANGED
@@ -13,98 +13,248 @@ globalThis._bunoshGlobalTaskStatus = {
13
13
 
14
14
  // Now import modules
15
15
 
16
- import program, { BUNOSHFILE, banner } from "./src/program.js";
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
- 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);
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
- // 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 {
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
- // 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
- }
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
- 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
- });
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 && process.env.BUNOSH_DEBUG) {
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.0",
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 = 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