bunosh 0.4.14 → 0.5.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
@@ -1,6 +1,8 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env bun
2
+
3
+ process.removeAllListeners('warning');
4
+ process.on('warning', () => {});
2
5
 
3
- // Set up global variables BEFORE any imports
4
6
  globalThis._bunoshStartTime = Date.now();
5
7
  globalThis._bunoshCommandCompleted = false;
6
8
  globalThis._bunoshGlobalTasksExecuted = [];
@@ -12,35 +14,28 @@ globalThis._bunoshGlobalTaskStatus = {
12
14
  WARNING: 'warning'
13
15
  };
14
16
 
15
- // Now import modules
16
-
17
17
  import bunosh, { BUNOSHFILE, banner } from "./src/program.js";
18
18
  import { existsSync, readFileSync, statSync } from "fs";
19
19
  import init from "./src/init.js";
20
20
  import path from "path";
21
21
  import { config } from "dotenv";
22
+ import { formatError } from "./src/error-formatter.js";
23
+ import color from "chalk";
22
24
 
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
25
  function loadEnvFiles(bunoshfileDir, customEnvFile = null) {
29
26
  if (customEnvFile) {
30
- // Load the specified env file
31
27
  const customEnvPath = path.isAbsolute(customEnvFile)
32
28
  ? customEnvFile
33
29
  : path.resolve(bunoshfileDir, customEnvFile);
34
30
 
35
31
  if (existsSync(customEnvPath)) {
36
- config({ path: customEnvPath });
32
+ config({ path: customEnvPath, quiet: true });
37
33
  } else {
38
34
  console.warn(`Warning: Specified env file not found: ${customEnvPath}`);
39
35
  }
40
36
  return;
41
37
  }
42
38
 
43
- // Follow Bun's automatic loading order
44
39
  const envFiles = [
45
40
  '.env',
46
41
  '.env.production',
@@ -52,7 +47,7 @@ function loadEnvFiles(bunoshfileDir, customEnvFile = null) {
52
47
  envFiles.forEach(envFile => {
53
48
  const envPath = path.join(bunoshfileDir, envFile);
54
49
  if (existsSync(envPath)) {
55
- config({ path: envPath });
50
+ config({ path: envPath, quiet: true });
56
51
  }
57
52
  });
58
53
  }
@@ -60,38 +55,32 @@ function loadEnvFiles(bunoshfileDir, customEnvFile = null) {
60
55
  async function loadBunoshfiles(tasksFile) {
61
56
  const path = await import('path');
62
57
  const fs = await import('fs');
63
-
64
- // Get the directory and base name of the tasks file
58
+
65
59
  const dir = path.dirname(tasksFile);
66
60
  const baseName = path.basename(tasksFile, '.js');
67
-
68
- // Find all matching Bunoshfile variants
61
+
69
62
  const files = fs.readdirSync(dir);
70
63
  const bunoshFiles = files
71
64
  .filter(file => {
72
- // Match Bunoshfile.js, Bunoshfile.*.js
73
65
  const regex = new RegExp(`^${baseName}(\\.\\w+)?\\.js$`);
74
66
  return regex.test(file);
75
67
  })
76
- .sort(); // Ensure consistent order
77
-
68
+ .sort();
69
+
78
70
  const allTasks = {};
79
71
  const allSources = {};
80
-
72
+
81
73
  for (const file of bunoshFiles) {
82
74
  const filePath = path.join(dir, file);
83
75
  try {
84
- // Extract namespace from filename
85
76
  let namespace = '';
86
77
  if (file !== `${baseName}.js`) {
87
- // Remove baseName and .js, then remove the leading dot
88
78
  namespace = file.slice(baseName.length, -3).substring(1);
89
79
  }
90
-
80
+
91
81
  const tasks = await import(filePath);
92
82
  const source = fs.readFileSync(filePath, 'utf-8');
93
-
94
- // Add namespace prefix to tasks if namespace exists
83
+
95
84
  if (namespace) {
96
85
  Object.keys(tasks).forEach(key => {
97
86
  if (typeof tasks[key] === 'function') {
@@ -100,7 +89,6 @@ async function loadBunoshfiles(tasksFile) {
100
89
  }
101
90
  });
102
91
  } else {
103
- // No namespace for the main Bunoshfile
104
92
  Object.keys(tasks).forEach(key => {
105
93
  if (typeof tasks[key] === 'function') {
106
94
  allTasks[key] = tasks[key];
@@ -112,95 +100,41 @@ async function loadBunoshfiles(tasksFile) {
112
100
  console.warn(`Warning: Could not load ${file}:`, error.message);
113
101
  }
114
102
  }
115
-
103
+
116
104
  return { tasks: allTasks, sources: allSources };
117
105
  }
118
106
 
119
107
  async function main() {
120
108
 
121
- // Parse --bunoshfile flag or BUNOSHFILE env var before importing tasks
122
109
  const bunoshfileIndex = process.argv.indexOf('--bunoshfile');
123
110
  let customBunoshfile = null;
124
111
 
125
112
  if (bunoshfileIndex !== -1 && bunoshfileIndex + 1 < process.argv.length) {
126
113
  customBunoshfile = process.argv[bunoshfileIndex + 1];
127
- // Remove the flag and its value from process.argv so it doesn't interfere with command parsing
128
114
  process.argv.splice(bunoshfileIndex, 2);
129
115
  } else if (process.env.BUNOSHFILE) {
130
116
  customBunoshfile = process.env.BUNOSHFILE;
131
117
  }
132
118
 
133
- // Parse --env-file flag
134
119
  const envFileIndex = process.argv.findIndex(arg => arg.startsWith('--env-file='));
135
120
  let customEnvFile = null;
136
121
 
137
122
  if (envFileIndex !== -1) {
138
123
  customEnvFile = process.argv[envFileIndex].split('=')[1];
139
- // Remove the flag from process.argv so it doesn't interfere with command parsing
140
124
  process.argv.splice(envFileIndex, 1);
141
125
  }
142
126
 
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
188
- }
189
-
190
127
  let tasksFile;
191
128
  let bunoshfileDir;
192
129
  if (customBunoshfile) {
193
130
  const resolvedPath = path.isAbsolute(customBunoshfile) ? customBunoshfile : path.resolve(customBunoshfile);
194
- // If it's a directory, append the default BUNOSHFILE
195
131
  if (existsSync(resolvedPath) && statSync(resolvedPath).isDirectory()) {
196
132
  tasksFile = path.join(resolvedPath, BUNOSHFILE);
197
133
  bunoshfileDir = resolvedPath;
198
- // Change working directory to the bunoshfile directory
199
134
  process.chdir(resolvedPath);
200
135
  } else {
201
136
  tasksFile = resolvedPath;
202
137
  bunoshfileDir = path.dirname(resolvedPath);
203
- // Change working directory to the bunoshfile's directory
204
138
  process.chdir(path.dirname(resolvedPath));
205
139
  }
206
140
  } else {
@@ -208,44 +142,37 @@ async function main() {
208
142
  bunoshfileDir = process.cwd();
209
143
  }
210
144
 
211
- // Load environment files from the Bunoshfile directory
212
145
  loadEnvFiles(bunoshfileDir, customEnvFile);
213
146
 
214
- // Handle -e flag for executing JavaScript code
215
147
  const eFlagIndex = process.argv.indexOf('-e');
216
148
  if (eFlagIndex !== -1) {
217
149
  (async () => {
218
150
  let jsCode = '';
219
-
220
- // Check if code is provided as argument
151
+
221
152
  if (eFlagIndex + 1 < process.argv.length && !process.argv[eFlagIndex + 1].startsWith('-')) {
222
153
  jsCode = process.argv[eFlagIndex + 1];
223
154
  } else if (!process.stdin.isTTY) {
224
- // Read from stdin only if it's not a TTY (i.e., it's being piped)
225
155
  const chunks = [];
226
156
  for await (const chunk of process.stdin) {
227
157
  chunks.push(chunk);
228
158
  }
229
159
  jsCode = Buffer.concat(chunks).toString('utf8').trim();
230
160
  }
231
-
161
+
232
162
  if (!jsCode) {
233
163
  console.error('No JavaScript code provided');
234
164
  process.exit(1);
235
165
  }
236
-
166
+
237
167
  try {
238
- // Import bunosh globals before executing JavaScript
239
168
  await import('./index.js');
240
-
241
- // Make bunosh globals available to the function
169
+
242
170
  for (const [key, value] of Object.entries(global.bunosh)) {
243
171
  if (typeof value === 'function') {
244
172
  globalThis[key] = value;
245
173
  }
246
174
  }
247
-
248
- // Execute the JavaScript code with bunosh globals available
175
+
249
176
  const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
250
177
  const func = new AsyncFunction(jsCode);
251
178
  await func();
@@ -268,7 +195,7 @@ async function main() {
268
195
 
269
196
  console.log();
270
197
  console.error(`Bunoshfile not found: ${tasksFile}`);
271
- console.log(customBunoshfile ?
198
+ console.log(customBunoshfile ?
272
199
  `Run \`bunosh init\` in the directory or specify a valid --bunoshfile path` :
273
200
  "Run `bunosh init` to create a new Bunoshfile here")
274
201
  console.log();
@@ -277,9 +204,8 @@ async function main() {
277
204
 
278
205
  await import('./index.js');
279
206
 
280
- // Load all Bunoshfile variants
281
- const { tasks, sources } = await loadBunoshfiles(tasksFile);
282
- await bunosh(tasks, sources);
207
+ const { tasks, sources } = await loadBunoshfiles(tasksFile);
208
+ await bunosh(tasks, sources);
283
209
  }
284
210
 
285
211
  main().catch((error) => {
@@ -287,27 +213,22 @@ main().catch((error) => {
287
213
  process.exit(1);
288
214
  });
289
215
 
290
- // Handle unhandled promise rejections
291
216
  process.on('unhandledRejection', (reason, promise) => {
292
217
  if (!process.env.BUNOSH_COMMAND_STARTED) return;
293
-
294
- console.error('\n❌ Unhandled Promise Rejection:');
295
- console.error(reason instanceof Error ? reason.message : reason);
296
- if (reason instanceof Error && reason.stack && process.env.BUNOSH_DEBUG) {
297
- console.error(reason.stack);
218
+
219
+ if (reason instanceof Error) {
220
+ console.error('\n' + formatError(reason));
221
+ } else {
222
+ console.error('\n' + color.red.bold('Unhandled Promise Rejection:'));
223
+ console.error(reason);
298
224
  }
299
225
  process.exit(1);
300
226
  });
301
227
 
302
- // Handle uncaught exceptions
303
228
  process.on('uncaughtException', (error) => {
304
229
  if (!process.env.BUNOSH_COMMAND_STARTED) return;
305
-
306
- console.error('\n Uncaught Exception:');
307
- console.error(error.message);
308
- if (error.stack) {
309
- console.error(error.stack);
310
- }
230
+
231
+ console.error('\n' + formatError(error));
311
232
  process.exit(1);
312
233
  });
313
234
 
@@ -327,7 +248,6 @@ process.on('exit', (code) => {
327
248
 
328
249
  const commandArgs = process.argv.slice(2);
329
250
 
330
- // Test environment detection
331
251
  const isTestEnvironment = process.env.NODE_ENV === 'test' ||
332
252
  (typeof jest !== 'undefined' && jest.isRunning) ||
333
253
  (process.env.VITEST_WORKER_ID !== undefined) ||
@@ -349,90 +269,9 @@ process.on('exit', (code) => {
349
269
  const success = finalExitCode === 0;
350
270
 
351
271
  if (globalThis._bunoshExitHandlerCalled) {
352
- console.log('\n[DEBUG] Exit handler already called, skipping duplicate');
353
272
  return;
354
273
  }
355
274
  globalThis._bunoshExitHandlerCalled = true;
356
275
 
357
276
  console.log(`\n🍲 ${success ? '' : 'FAIL '}Exit Code: ${finalExitCode} | Tasks: ${tasksExecuted.length}${tasksFailed ? ` | Failed: ${tasksFailed}` : ''}${tasksWarning ? ` | Warnings: ${tasksWarning}` : ''} | Time: ${totalTime}ms`);
358
277
  });
359
-
360
- function handleBunoshfileError(error, filePath) {
361
- // Don't show banner for errors - it interferes with error visibility
362
-
363
- // Check for Babel parser syntax errors
364
- if (error.code === 'BABEL_PARSER_SYNTAX_ERROR' ||
365
- (error.reasonCode && error.loc) ||
366
- error.constructor.name === 'SyntaxError') {
367
-
368
- console.error(`❌ Syntax Error in ${path.basename(filePath)}:`);
369
- console.log();
370
-
371
- if (error.loc) {
372
- console.error(` Line ${error.loc.line}, Column ${error.loc.column}:`);
373
-
374
- // Provide specific error messages based on reasonCode
375
- if (error.reasonCode === 'VarRedeclaration') {
376
- console.error(` Variable redeclaration - '${error.message}' is already declared`);
377
- } else if (error.reasonCode && error.reasonCode.includes('Unexpected')) {
378
- console.error(` ${error.reasonCode}: ${error.message || 'Unexpected token'}`);
379
- } else {
380
- console.error(` ${error.message || error.reasonCode || 'Invalid syntax'}`);
381
- }
382
- } else {
383
- console.error(` ${error.message || 'Invalid JavaScript syntax'}`);
384
- }
385
-
386
- console.log();
387
- console.log('💡 Common issues:');
388
- console.log(' • Missing semicolons or commas');
389
- console.log(' • Unclosed brackets, parentheses, or quotes');
390
- console.log(' • Invalid variable declarations');
391
- console.log(' • Mixing import/export with require/module.exports');
392
- console.log();
393
- console.log(`📝 Edit your Bunoshfile: ${color.blue('bunosh edit')}`);
394
- console.log(`🔧 Validate syntax: ${color.blue(`bun --check ${path.basename(filePath)}`)}`);
395
-
396
- } else if (error.message && error.message.includes('SyntaxError')) {
397
- console.error(`❌ JavaScript Syntax Error in ${path.basename(filePath)}:`);
398
- console.log();
399
- console.error(` ${error.message}`);
400
- console.log();
401
- console.log(`💡 Try running: ${color.blue('bun --check Bunoshfile.js')}`);
402
- console.log(`📝 Edit your Bunoshfile: ${color.blue('bunosh edit')}`);
403
-
404
- } else if (error.code === 'MODULE_NOT_FOUND' ||
405
- error.message?.includes('Cannot resolve') ||
406
- error.message?.includes('Could not resolve')) {
407
- console.error(`❌ Module Import Error in ${path.basename(filePath)}:`);
408
- console.log();
409
- console.error(` ${error.message}`);
410
- console.log();
411
- console.log('💡 Common solutions:');
412
- console.log(` • Run: ${color.blue('bun install')}`);
413
- console.log(' • Check import paths are correct');
414
- console.log(' • Ensure dependencies are listed in package.json');
415
-
416
- } else {
417
- console.error(`❌ Error loading ${path.basename(filePath)}:`);
418
- console.log();
419
- console.error(` ${error.message || error.toString()}`);
420
-
421
- // Add stack trace for debugging if available
422
- if (process.env.BUNOSH_DEBUG) {
423
- console.log();
424
- console.log('🐛 Debug stack trace:');
425
- console.log(error.stack || 'No stack trace available');
426
- }
427
-
428
- console.log();
429
- console.log('💡 Try:');
430
- console.log(` • Check the file exists: ${color.blue(`ls -la ${path.basename(filePath)}`)}`);
431
- console.log(` • Validate syntax: ${color.blue(`bun --check ${path.basename(filePath)}`)}`);
432
- console.log(` • Edit the file: ${color.blue('bunosh edit')}`);
433
- console.log(` • Run with debug: ${color.blue('BUNOSH_DEBUG=1 bunosh')}`);
434
- }
435
-
436
- console.log();
437
- process.exit(1);
438
- }
package/index.js CHANGED
@@ -1,5 +1,6 @@
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";
@@ -11,7 +12,7 @@ export { exec, shell, fetch, writeToFile, copyFile, ai, ask, yell, say, task, tr
11
12
 
12
13
  export function buildCmd(cmd) {
13
14
  return function (args) {
14
- return exec`${cmd} ${args}`;
15
+ return shell`${cmd} ${args}`;
15
16
  };
16
17
  }
17
18
 
@@ -35,7 +36,7 @@ global.bunosh = {
35
36
  silent,
36
37
  TaskResult,
37
38
  buildCmd,
38
- $: exec,
39
+ $: shell,
39
40
  };
40
41
 
41
42
  export default global.bunosh;
package/package.json CHANGED
@@ -1,8 +1,25 @@
1
1
  {
2
2
  "name": "bunosh",
3
- "version": "0.4.14",
3
+ "version": "0.5.6",
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",
@@ -0,0 +1,80 @@
1
+ import { codeFrameColumns } from "@babel/code-frame";
2
+ import { readFileSync, existsSync } from "fs";
3
+ import path from "path";
4
+ import color from "chalk";
5
+
6
+ function parseErrorLocation(error) {
7
+ if (!error.stack) return null;
8
+
9
+ const lines = error.stack.split('\n');
10
+ for (const line of lines) {
11
+ const match = line.match(/at .+? \((.+?):(\d+):(\d+)\)/) ||
12
+ line.match(/at (.+?):(\d+):(\d+)/);
13
+ if (!match) continue;
14
+
15
+ const file = match[1];
16
+ if (file.includes('node_modules/') || file.startsWith('node:')) continue;
17
+
18
+ return {
19
+ file,
20
+ line: parseInt(match[2], 10),
21
+ column: parseInt(match[3], 10),
22
+ };
23
+ }
24
+
25
+ return null;
26
+ }
27
+
28
+ function cleanStack(error) {
29
+ if (!error.stack) return '';
30
+
31
+ return error.stack
32
+ .split('\n')
33
+ .filter(line => {
34
+ if (!line.trim().startsWith('at ')) return false;
35
+ if (line.includes('node_modules/')) return false;
36
+ if (line.includes('node:')) return false;
37
+ return true;
38
+ })
39
+ .join('\n');
40
+ }
41
+
42
+ export function formatError(error) {
43
+ const loc = parseErrorLocation(error);
44
+ const parts = [];
45
+
46
+ if (loc) {
47
+ const displayFile = path.relative(process.cwd(), loc.file) || loc.file;
48
+ parts.push(color.red.bold('Error') + ` in ${color.bold(displayFile + ':' + loc.line)}`);
49
+ } else {
50
+ parts.push(color.red.bold('Error'));
51
+ }
52
+
53
+ parts.push(` ${error.message}`);
54
+
55
+ if (loc) {
56
+ const absFile = path.isAbsolute(loc.file) ? loc.file : path.resolve(process.cwd(), loc.file);
57
+ if (existsSync(absFile)) {
58
+ try {
59
+ const source = readFileSync(absFile, 'utf-8');
60
+ const frame = codeFrameColumns(source, {
61
+ start: { line: loc.line, column: loc.column },
62
+ }, {
63
+ highlightCode: true,
64
+ linesAbove: 2,
65
+ linesBelow: 1,
66
+ });
67
+ parts.push('');
68
+ parts.push(frame);
69
+ } catch {}
70
+ }
71
+ }
72
+
73
+ const stack = cleanStack(error);
74
+ if (stack) {
75
+ parts.push('');
76
+ parts.push(stack);
77
+ }
78
+
79
+ return parts.join('\n');
80
+ }
@@ -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;