bunosh 0.5.0 → 0.5.8

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 CHANGED
@@ -187,7 +187,7 @@ Bunoshfile.api.js # bunosh api:deploy, bunosh api:test
187
187
  Built-in tasks are available via `global.bunosh`:
188
188
 
189
189
  ```javascript
190
- const { exec, shell, fetch, writeToFile, copyFile, task } = global.bunosh;
190
+ const { exec, shell, fetch, writeToFile, copyFile, task, assert } = global.bunosh;
191
191
  ```
192
192
 
193
193
  > Global variables are used instead of imports so bunosh works with the single-executable on any platform.
@@ -195,6 +195,7 @@ const { exec, shell, fetch, writeToFile, copyFile, task } = global.bunosh;
195
195
  * Async tasks: `exec`, `shell`, `fetch`
196
196
  * Sync tasks: `writeToFile`, `copyFile`
197
197
  * Task wrapper: `task`
198
+ * Precondition guard: `assert`
198
199
 
199
200
  Each task returns a `TaskResult` object:
200
201
 
@@ -273,6 +274,30 @@ For details see the [Bun shell](https://bun.sh/docs/runtime/shell) reference.
273
274
  | `exec` | Single command execution | spawn process | Node.js + Bun, platform dependent |
274
275
  | `shell` | Cross-platform shell commands | Bun shell | Bun only, cross-platform |
275
276
 
277
+ ### `assert`
278
+
279
+ Guard a command on a precondition. If the condition is falsy, `assert` prints a red failure line and records a failed task — the run continues, and the process exits with code 1 at the end:
280
+
281
+ ```javascript
282
+ export async function deploy() {
283
+ assert(process.env.TOKEN, 'TOKEN must be set');
284
+ assert(await Bun.file('dist/bundle.js').exists(), 'bundle not built');
285
+ await shell`./scripts/deploy.sh`;
286
+ }
287
+ ```
288
+
289
+ With `task.stopOnFailures()` enabled, a failed `assert` exits immediately at that line:
290
+
291
+ ```javascript
292
+ export async function strict() {
293
+ task.stopOnFailures();
294
+ assert(process.env.TOKEN, 'TOKEN must be set');
295
+ await shell`./scripts/deploy.sh`;
296
+ }
297
+ ```
298
+
299
+ `assert` does not throw a JavaScript exception in default mode — it records the failure and execution continues. Use `task.stopOnFailures()` (or `return` after the `assert`) when you need a hard stop.
300
+
276
301
  ### `fetch`
277
302
 
278
303
  Wraps the fetch API as a task:
@@ -427,7 +452,6 @@ export async function checkServices() {
427
452
 
428
453
  - **[Examples](docs/examples.md)** — Real-world examples and workflows
429
454
  - **[AI Integration](docs/ai.md)** — Built-in AI support
430
- - **[MCP Integration](docs/mcp.md)** — Expose commands to AI assistants (Claude, Cursor, etc.)
431
455
  - **[JavaScript Execution](docs/javascript-execution.md)** — Execute JavaScript directly via CLI
432
456
  - **[Bash Migration Guide](docs/bash-migration-guide.md)** — Convert bash scripts to Bunosh
433
457
  - **[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,17 +1,19 @@
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";
6
7
  import ai from "./src/tasks/ai.js";
8
+ import assert from "./src/tasks/assert.js";
7
9
  import { ask, yell, say } from "./src/io.js";
8
10
  import { task, tryTask, stopOnFail, ignoreFail, stopOnFailures, ignoreFailures, silence, prints, silent, TaskResult } from "./src/task.js";
9
11
 
10
- export { exec, shell, fetch, writeToFile, copyFile, ai, ask, yell, say, task, tryTask, stopOnFail, ignoreFail, stopOnFailures, ignoreFailures, silence, prints, silent, TaskResult };
12
+ export { exec, shell, fetch, writeToFile, copyFile, ai, assert, ask, yell, say, task, tryTask, stopOnFail, ignoreFail, stopOnFailures, ignoreFailures, silence, prints, silent, TaskResult };
11
13
 
12
14
  export function buildCmd(cmd) {
13
15
  return function (args) {
14
- return exec`${cmd} ${args}`;
16
+ return shell`${cmd} ${args}`;
15
17
  };
16
18
  }
17
19
 
@@ -25,6 +27,7 @@ global.bunosh = {
25
27
  writeToFile,
26
28
  copyFile,
27
29
  ai,
30
+ assert,
28
31
  stopOnFail,
29
32
  ignoreFail,
30
33
  task,
@@ -35,7 +38,7 @@ global.bunosh = {
35
38
  silent,
36
39
  TaskResult,
37
40
  buildCmd,
38
- $: exec,
41
+ $: shell,
39
42
  };
40
43
 
41
44
  export default global.bunosh;
package/package.json CHANGED
@@ -1,8 +1,25 @@
1
1
  {
2
2
  "name": "bunosh",
3
- "version": "0.5.0",
3
+ "version": "0.5.8",
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 false;
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
- finish(taskName, extra = {}) {
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
- if (this.startTimeout) {
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
- if (this.startTimeout) {
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 optionsObj = commanderArgs[commanderArgs.length - 1];
269
- if (optionsObj && typeof optionsObj === 'object') {
270
- Object.keys(opts).forEach((optName) => {
271
- const dasherizedOpt = optName.replace(/([A-Z])/g, '-$1').toLowerCase();
272
- if (optionsObj[dasherizedOpt] !== undefined) {
273
- transformedArgs.push(optionsObj[dasherizedOpt]);
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
- transformedArgs.push(opts[optName]);
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
- helpText += color.dim(`Special Commands:
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
@@ -47,6 +47,22 @@ export function getIgnoreFailuresMode() {
47
47
  return ignoreFailuresMode;
48
48
  }
49
49
 
50
+ export function isStopOnFailuresMode() {
51
+ return stopOnFailuresMode;
52
+ }
53
+
54
+ export function isTestEnv() {
55
+ const commandArgs = process.argv.slice(2);
56
+ return process.env.NODE_ENV === 'test' ||
57
+ commandArgs.some(arg => {
58
+ const lower = arg.toLowerCase();
59
+ return lower.includes('vitest') ||
60
+ lower.includes('jest') ||
61
+ lower === '--test' ||
62
+ lower.startsWith('test:');
63
+ });
64
+ }
65
+
50
66
  export function silence() {
51
67
  globalSilenceMode = true;
52
68
  }
@@ -198,6 +214,8 @@ export async function task(name, fn, isSilent = false) {
198
214
 
199
215
  // Check if result is a TaskResult instance
200
216
  if (result && result.constructor && result.constructor.name === 'TaskResult') {
217
+ printer.cancel();
218
+ runningTasks.delete(taskInfo.id);
201
219
  return result;
202
220
  }
203
221
 
@@ -220,24 +238,12 @@ export async function task(name, fn, isSilent = false) {
220
238
  printer.error(name, err);
221
239
  runningTasks.delete(taskInfo.id);
222
240
 
223
- // Don't exit during testing
224
- const commandArgs = process.argv.slice(2);
225
- const isTestEnvironment = process.env.NODE_ENV === 'test' ||
226
- typeof Bun?.jest !== 'undefined' ||
227
- commandArgs.some(arg => {
228
- const lowerArg = arg.toLowerCase();
229
- return lowerArg.includes('vitest') ||
230
- lowerArg.includes('jest') ||
231
- lowerArg === '--test' ||
232
- lowerArg.startsWith('test:');
233
- });
234
-
235
- // Exit immediately if stopOnFailures mode is enabled
241
+ const isTestEnvironment = isTestEnv();
242
+
236
243
  if (stopOnFailuresMode && !isTestEnvironment) {
237
244
  process.exit(1);
238
245
  }
239
-
240
- // Also exit if stopFailToggle is enabled (legacy behavior)
246
+
241
247
  if (stopFailToggle && !isTestEnvironment) {
242
248
  process.exit(1);
243
249
  }
@@ -246,8 +252,13 @@ export async function task(name, fn, isSilent = false) {
246
252
  }
247
253
  }
248
254
 
249
- // Add try method to task function
255
+ // Add methods to task function
250
256
  task.try = tryTask;
257
+ task.stopOnFailures = stopOnFailures;
258
+ task.ignoreFailures = ignoreFailures;
259
+ task.silence = silence;
260
+ task.prints = prints;
261
+ task.silent = silent;
251
262
 
252
263
 
253
264
  export class SilentTaskWrapper {
@@ -0,0 +1,23 @@
1
+ import { createTaskInfo, finishTaskInfo, getCurrentTaskId, runningTasks, isStopOnFailuresMode, isTestEnv } from '../task.js';
2
+ import Printer from '../printer.js';
3
+
4
+ export default function assert(condition, message = 'Assertion failed') {
5
+ const currentTaskId = getCurrentTaskId();
6
+ const parent = currentTaskId ? runningTasks.get(currentTaskId) : null;
7
+ const isParentSilent = parent?.isSilent || false;
8
+
9
+ const taskInfo = createTaskInfo(message, currentTaskId, isParentSilent);
10
+ const printer = new Printer('assert', taskInfo.id);
11
+
12
+ if (condition) {
13
+ printer.finish(message);
14
+ finishTaskInfo(taskInfo, true, null, message);
15
+ return;
16
+ }
17
+
18
+ const error = new Error(message);
19
+ printer.error(message, error);
20
+ finishTaskInfo(taskInfo, false, error, message);
21
+
22
+ if (isStopOnFailuresMode() && !isTestEnv()) process.exit(1);
23
+ }