builderman 1.4.0 → 1.5.1

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.
@@ -1,56 +1,45 @@
1
1
  import * as path from "node:path";
2
2
  import * as fs from "node:fs";
3
- import { $TASK_INTERNAL } from "../constants.js";
4
- import { createTaskGraph } from "../graph.js";
5
- import { PipelineError } from "../pipeline-error.js";
3
+ import { $TASK_INTERNAL, $PIPELINE_INTERNAL } from "./constants.js";
4
+ import { PipelineError } from "../errors.js";
6
5
  /**
7
6
  * Executes a task (either a regular task or a nested pipeline).
8
7
  */
9
- export function executeTask(task, executorConfig) {
8
+ export function executeTask(task, context, callbacks) {
10
9
  // Check if signal is aborted before starting new tasks
11
- if (executorConfig.signal?.aborted) {
12
- executorConfig.failPipeline(new PipelineError("Aborted", PipelineError.InvalidSignal));
10
+ if (context.signal?.aborted) {
11
+ callbacks.onTaskFailed(task[$TASK_INTERNAL].id, new PipelineError("Aborted", PipelineError.Aborted));
13
12
  return;
14
13
  }
15
- const { name: taskName, [$TASK_INTERNAL]: { id: taskId, pipeline: nestedPipeline }, } = task;
16
- if (executorConfig.runningTasks.has(taskId))
17
- return;
14
+ const { name: taskName, [$TASK_INTERNAL]: { id, pipeline }, } = task;
18
15
  // Handle pipeline tasks
19
- if (nestedPipeline) {
20
- executeNestedPipeline(task, taskId, taskName, nestedPipeline, executorConfig);
16
+ if (pipeline) {
17
+ executeNestedPipeline(task, id, taskName, pipeline, context, callbacks);
21
18
  return;
22
19
  }
23
20
  // Regular task execution
24
- executeRegularTask(task, taskId, taskName, executorConfig);
21
+ executeRegularTask(task, id, taskName, context, callbacks);
25
22
  }
26
- function executeNestedPipeline(task, taskId, taskName, nestedPipeline, executorConfig) {
27
- const { config, runningPipelines, pipelineTasksCache, failPipeline, advanceScheduler, updateTaskStatus, } = executorConfig;
23
+ function executeNestedPipeline(task, taskId, taskName, nestedPipeline, context, callbacks) {
24
+ const { config } = context;
28
25
  // Track nested pipeline state for skip behavior
29
26
  let nestedSkippedCount = 0;
30
27
  let nestedCompletedCount = 0;
31
28
  // Get total tasks from nested pipeline
32
- const nestedTasks = pipelineTasksCache.get(nestedPipeline);
33
- const nestedTotalTasks = nestedTasks
34
- ? createTaskGraph(nestedTasks).nodes.size
35
- : 0;
36
- const commandName = config?.command ?? process.env.NODE_ENV === "production" ? "build" : "dev";
37
- // Mark as ready immediately (pipeline entry nodes will handle their own ready state)
29
+ const nestedTotalTasks = nestedPipeline[$PIPELINE_INTERNAL].graph.nodes.size;
30
+ const commandName = config?.command ?? (process.env.NODE_ENV === "production" ? "build" : "dev");
31
+ // Mark as running - nested pipeline tasks handle their own ready state
32
+ // We don't call onTaskReady here because dependent tasks should wait for
33
+ // the nested pipeline to complete, not just start
38
34
  const startedAt = Date.now();
39
- updateTaskStatus(taskId, {
35
+ context.updateTaskStatus(taskId, {
40
36
  status: "running",
41
37
  command: commandName,
42
38
  startedAt,
43
39
  });
44
- advanceScheduler({ type: "ready", taskId });
45
40
  config?.onTaskBegin?.(taskName);
46
41
  // Create an abort controller to stop the nested pipeline if needed
47
42
  let pipelineStopped = false;
48
- const stopPipeline = () => {
49
- pipelineStopped = true;
50
- // The nested pipeline will continue running, but we've marked it as stopped
51
- // In a more sophisticated implementation, we could propagate stop signals
52
- };
53
- runningPipelines.set(taskId, { stop: stopPipeline });
54
43
  // Merge environment variables: pipeline.env -> task.env (from pipeline.toTask config)
55
44
  const taskEnv = task[$TASK_INTERNAL].env;
56
45
  const pipelineEnv = config?.env ?? {};
@@ -59,12 +48,14 @@ function executeNestedPipeline(task, taskId, taskName, nestedPipeline, executorC
59
48
  ...taskEnv,
60
49
  };
61
50
  // Run the nested pipeline with signal propagation
51
+ // Pass the command from parent pipeline to nested pipeline
52
+ // If undefined, nested pipeline will use its own default (build/dev based on NODE_ENV)
62
53
  nestedPipeline
63
54
  .run({
64
- spawn: executorConfig.spawn,
65
- command: config?.command,
55
+ spawn: context.spawn,
56
+ command: config?.command, // Pass command to nested pipeline
66
57
  strict: config?.strict,
67
- signal: executorConfig.signal, // Pass signal to nested pipeline
58
+ signal: context.signal, // Pass signal to nested pipeline
68
59
  env: mergedEnv, // Pass merged env to nested pipeline
69
60
  onTaskBegin: (nestedTaskName) => {
70
61
  if (pipelineStopped)
@@ -87,17 +78,16 @@ function executeNestedPipeline(task, taskId, taskName, nestedPipeline, executorC
87
78
  .then((result) => {
88
79
  if (pipelineStopped)
89
80
  return;
90
- runningPipelines.delete(taskId);
91
81
  if (!result.ok) {
92
82
  // Nested pipeline failed
93
83
  const finishedAt = Date.now();
94
- updateTaskStatus(taskId, {
84
+ context.updateTaskStatus(taskId, {
95
85
  status: "failed",
96
86
  finishedAt,
97
87
  durationMs: finishedAt - startedAt,
98
88
  error: result.error,
99
89
  });
100
- failPipeline(result.error);
90
+ callbacks.onTaskFailed(taskId, result.error);
101
91
  return;
102
92
  }
103
93
  // Determine nested pipeline result based on skip behavior:
@@ -107,33 +97,34 @@ function executeNestedPipeline(task, taskId, taskName, nestedPipeline, executorC
107
97
  if (nestedSkippedCount === nestedTotalTasks && nestedTotalTasks > 0) {
108
98
  // All tasks were skipped
109
99
  const finishedAt = Date.now();
110
- updateTaskStatus(taskId, {
100
+ context.updateTaskStatus(taskId, {
111
101
  status: "skipped",
112
102
  finishedAt,
113
103
  durationMs: finishedAt - startedAt,
114
104
  });
115
105
  config?.onTaskSkipped?.(taskName, commandName);
116
106
  setImmediate(() => {
117
- advanceScheduler({ type: "skip", taskId });
107
+ callbacks.onTaskSkipped(taskId);
118
108
  });
119
109
  }
120
110
  else {
121
111
  // Some tasks ran (and completed successfully)
122
112
  const finishedAt = Date.now();
123
- updateTaskStatus(taskId, {
113
+ context.updateTaskStatus(taskId, {
124
114
  status: "completed",
125
115
  finishedAt,
126
116
  durationMs: finishedAt - startedAt,
127
117
  });
128
118
  config?.onTaskComplete?.(taskName);
129
- advanceScheduler({ type: "complete", taskId });
119
+ // Mark as complete - this will update dependent tasks and allow them to start
120
+ callbacks.onTaskComplete(taskId);
130
121
  }
131
122
  });
132
123
  }
133
- function executeRegularTask(task, taskId, taskName, executorConfig) {
134
- const { spawn: spawnFn, signal, config, runningTasks, teardownManager, failPipeline, advanceScheduler, updateTaskStatus, } = executorConfig;
124
+ function executeRegularTask(task, taskId, taskName, context, callbacks) {
125
+ const { config, spawn: spawnFn, signal, teardownManager, timeoutManager, } = context;
135
126
  const { allowSkip, commands, cwd, env: taskEnv } = task[$TASK_INTERNAL];
136
- const commandName = config?.command ?? process.env.NODE_ENV === "production" ? "build" : "dev";
127
+ const commandName = config?.command ?? (process.env.NODE_ENV === "production" ? "build" : "dev");
137
128
  const commandConfig = commands[commandName];
138
129
  // Check if command exists
139
130
  if (commandConfig === undefined) {
@@ -142,18 +133,18 @@ function executeRegularTask(task, taskId, taskName, executorConfig) {
142
133
  if (strict && !allowSkip) {
143
134
  const error = new PipelineError(`[${taskName}] No command for "${commandName}" and strict mode is enabled`, PipelineError.TaskFailed, taskName);
144
135
  const finishedAt = Date.now();
145
- updateTaskStatus(taskId, {
136
+ context.updateTaskStatus(taskId, {
146
137
  status: "failed",
147
138
  command: commandName,
148
139
  finishedAt,
149
140
  error,
150
141
  });
151
- failPipeline(error);
142
+ callbacks.onTaskFailed(taskId, error);
152
143
  return;
153
144
  }
154
145
  // Skip the task
155
146
  const finishedAt = Date.now();
156
- updateTaskStatus(taskId, {
147
+ context.updateTaskStatus(taskId, {
157
148
  status: "skipped",
158
149
  command: commandName,
159
150
  finishedAt,
@@ -163,13 +154,14 @@ function executeRegularTask(task, taskId, taskName, executorConfig) {
163
154
  // Mark as skipped - this satisfies dependencies and unblocks dependents
164
155
  // Use setImmediate to ensure scheduler is at idle yield before receiving skip
165
156
  setImmediate(() => {
166
- advanceScheduler({ type: "skip", taskId });
157
+ callbacks.onTaskSkipped(taskId);
167
158
  });
168
159
  return;
169
160
  }
170
161
  let command;
171
162
  let readyWhen;
172
163
  let readyTimeout = Infinity;
164
+ let completedTimeout = Infinity;
173
165
  let teardown;
174
166
  let commandEnv = {};
175
167
  if (typeof commandConfig === "string") {
@@ -179,6 +171,7 @@ function executeRegularTask(task, taskId, taskName, executorConfig) {
179
171
  command = commandConfig.run;
180
172
  readyWhen = commandConfig.readyWhen;
181
173
  readyTimeout = commandConfig.readyTimeout ?? Infinity;
174
+ completedTimeout = commandConfig.completedTimeout ?? Infinity;
182
175
  teardown = commandConfig.teardown;
183
176
  commandEnv = commandConfig.env ?? {};
184
177
  }
@@ -186,14 +179,14 @@ function executeRegularTask(task, taskId, taskName, executorConfig) {
186
179
  if (!fs.existsSync(taskCwd)) {
187
180
  const finishedAt = Date.now();
188
181
  const pipelineError = new PipelineError(`[${taskName}] Working directory does not exist: ${taskCwd}`, PipelineError.InvalidTask, taskName);
189
- updateTaskStatus(taskId, {
182
+ context.updateTaskStatus(taskId, {
190
183
  status: "failed",
191
184
  command: commandName,
192
185
  finishedAt,
193
186
  durationMs: 0,
194
187
  error: pipelineError,
195
188
  });
196
- failPipeline(pipelineError);
189
+ callbacks.onTaskFailed(taskId, pipelineError);
197
190
  return;
198
191
  }
199
192
  const accumulatedPath = [
@@ -218,9 +211,13 @@ function executeRegularTask(task, taskId, taskName, executorConfig) {
218
211
  shell: true,
219
212
  env: accumulatedEnv,
220
213
  });
221
- runningTasks.set(taskId, child);
214
+ // Update the running task execution with the process
215
+ const runningTasks = context.queueManager.getRunningTasks();
216
+ const execution = runningTasks.get(taskId);
217
+ execution.process = child;
218
+ context.queueManager.markTaskRunning(taskId, execution);
222
219
  const startedAt = Date.now();
223
- updateTaskStatus(taskId, {
220
+ context.updateTaskStatus(taskId, {
224
221
  status: "running",
225
222
  command: commandName,
226
223
  startedAt,
@@ -235,26 +232,53 @@ function executeRegularTask(task, taskId, taskName, executorConfig) {
235
232
  }
236
233
  config?.onTaskBegin?.(taskName);
237
234
  let didMarkReady = false;
238
- let readyTimeoutId = null;
239
- if (!readyWhen) {
240
- advanceScheduler({ type: "ready", taskId });
241
- didMarkReady = true;
242
- }
243
- else if (readyTimeout !== Infinity) {
244
- // Set up timeout for readyWhen condition
245
- readyTimeoutId = setTimeout(() => {
235
+ // For tasks without readyWhen, we wait for the process to exit successfully
236
+ // before allowing dependent tasks to start. This ensures dependencies are
237
+ // fully completed before dependents begin execution.
238
+ if (readyWhen && readyTimeout !== Infinity) {
239
+ // Set up timeout for readyWhen condition using TimeoutManager
240
+ timeoutManager.setReadyTimeout(taskId, readyTimeout, () => {
246
241
  if (!didMarkReady) {
247
242
  const finishedAt = Date.now();
248
- const pipelineError = new PipelineError(`[${taskName}] Task did not become ready within ${readyTimeout}ms`, PipelineError.TaskFailed, taskName);
249
- updateTaskStatus(taskId, {
243
+ const pipelineError = new PipelineError(`[${taskName}] Task did not become ready within ${readyTimeout}ms`, PipelineError.TaskReadyTimeout, taskName);
244
+ // Clear completion timeout if it exists
245
+ timeoutManager.clearCompletionTimeout(taskId);
246
+ // Kill the process since it didn't become ready in time
247
+ try {
248
+ child.kill("SIGTERM");
249
+ }
250
+ catch { }
251
+ context.updateTaskStatus(taskId, {
250
252
  status: "failed",
251
253
  finishedAt,
252
254
  durationMs: finishedAt - startedAt,
253
255
  error: pipelineError,
254
256
  });
255
- failPipeline(pipelineError);
257
+ callbacks.onTaskFailed(taskId, pipelineError);
258
+ }
259
+ });
260
+ }
261
+ // Set up timeout for task completion using TimeoutManager
262
+ if (completedTimeout !== Infinity) {
263
+ timeoutManager.setCompletionTimeout(taskId, completedTimeout, () => {
264
+ // Task didn't complete within the timeout
265
+ const finishedAt = Date.now();
266
+ const pipelineError = new PipelineError(`[${taskName}] Task did not complete within ${completedTimeout}ms`, PipelineError.TaskCompletedTimeout, taskName);
267
+ // Clear ready timeout if it exists (to prevent it from firing after we've already failed)
268
+ timeoutManager.clearReadyTimeout(taskId);
269
+ // Kill the process since it didn't complete in time
270
+ try {
271
+ child.kill("SIGTERM");
256
272
  }
257
- }, readyTimeout);
273
+ catch { }
274
+ context.updateTaskStatus(taskId, {
275
+ status: "failed",
276
+ finishedAt,
277
+ durationMs: finishedAt - startedAt,
278
+ error: pipelineError,
279
+ });
280
+ callbacks.onTaskFailed(taskId, pipelineError);
281
+ });
258
282
  }
259
283
  let output = "";
260
284
  child.stdout?.on("data", (buf) => {
@@ -266,11 +290,8 @@ function executeRegularTask(task, taskId, taskName, executorConfig) {
266
290
  output += chunk;
267
291
  process.stdout.write(chunk);
268
292
  if (!didMarkReady && readyWhen && readyWhen(output)) {
269
- if (readyTimeoutId) {
270
- clearTimeout(readyTimeoutId);
271
- readyTimeoutId = null;
272
- }
273
- advanceScheduler({ type: "ready", taskId });
293
+ timeoutManager.clearReadyTimeout(taskId);
294
+ callbacks.onTaskReady(taskId);
274
295
  didMarkReady = true;
275
296
  }
276
297
  });
@@ -281,27 +302,28 @@ function executeRegularTask(task, taskId, taskName, executorConfig) {
281
302
  // Task failed before entering running state, so don't execute teardown
282
303
  // Remove teardown from map since it was never actually running
283
304
  teardownManager.unregister(taskId);
284
- // Clear ready timeout if it exists
285
- if (readyTimeoutId) {
286
- clearTimeout(readyTimeoutId);
287
- readyTimeoutId = null;
288
- }
305
+ // Clear all timeouts for this task
306
+ timeoutManager.clearTaskTimeouts(taskId);
289
307
  const finishedAt = Date.now();
290
308
  const pipelineError = new PipelineError(`[${taskName}] Failed to start: ${error.message}`, PipelineError.TaskFailed, taskName);
291
- updateTaskStatus(taskId, {
309
+ context.updateTaskStatus(taskId, {
292
310
  status: "failed",
293
311
  finishedAt,
294
312
  durationMs: finishedAt - startedAt,
295
313
  error: pipelineError,
296
314
  });
297
- failPipeline(pipelineError);
315
+ callbacks.onTaskFailed(taskId, pipelineError);
298
316
  });
299
317
  child.on("exit", (code, signal) => {
300
- runningTasks.delete(taskId);
301
- // Clear ready timeout if it exists
302
- if (readyTimeoutId) {
303
- clearTimeout(readyTimeoutId);
304
- readyTimeoutId = null;
318
+ // Clear all timeouts for this task
319
+ timeoutManager.clearTaskTimeouts(taskId);
320
+ // Check if task was already marked as failed (e.g., by timeout handlers)
321
+ // If so, don't process the exit event to avoid conflicts
322
+ const currentStats = context.taskStats.get(taskId);
323
+ if (currentStats?.status === "failed" && currentStats?.error) {
324
+ // Task was already failed, likely by a timeout handler
325
+ // Just return to avoid duplicate error handling
326
+ return;
305
327
  }
306
328
  const finishedAt = Date.now();
307
329
  const durationMs = finishedAt - startedAt;
@@ -309,7 +331,7 @@ function executeRegularTask(task, taskId, taskName, executorConfig) {
309
331
  // when the pipeline completes or fails
310
332
  if (code !== 0 || signal) {
311
333
  const pipelineError = new PipelineError(`[${taskName}] Task failed with non-zero exit code: ${code ?? signal}`, PipelineError.TaskFailed, taskName);
312
- updateTaskStatus(taskId, {
334
+ context.updateTaskStatus(taskId, {
313
335
  status: "failed",
314
336
  finishedAt,
315
337
  durationMs,
@@ -317,10 +339,10 @@ function executeRegularTask(task, taskId, taskName, executorConfig) {
317
339
  signal: signal ?? undefined,
318
340
  error: pipelineError,
319
341
  });
320
- failPipeline(pipelineError);
342
+ callbacks.onTaskFailed(taskId, pipelineError);
321
343
  return;
322
344
  }
323
- updateTaskStatus(taskId, {
345
+ context.updateTaskStatus(taskId, {
324
346
  status: "completed",
325
347
  finishedAt,
326
348
  durationMs,
@@ -328,6 +350,6 @@ function executeRegularTask(task, taskId, taskName, executorConfig) {
328
350
  });
329
351
  config?.onTaskComplete?.(taskName);
330
352
  // 🔑 Notify scheduler and drain newly runnable tasks
331
- advanceScheduler({ type: "complete", taskId });
353
+ callbacks.onTaskComplete(taskId);
332
354
  });
333
355
  }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Centralized timeout management for task execution
3
+ * Handles readyWhen and completion timeouts with proper cleanup
4
+ */
5
+ export interface TimeoutManager {
6
+ /**
7
+ * Set a timeout for a task's readyWhen condition
8
+ */
9
+ setReadyTimeout(taskId: string, timeout: number, callback: () => void): void;
10
+ /**
11
+ * Set a timeout for a task's completion
12
+ */
13
+ setCompletionTimeout(taskId: string, timeout: number, callback: () => void): void;
14
+ /**
15
+ * Clear the ready timeout for a specific task
16
+ */
17
+ clearReadyTimeout(taskId: string): void;
18
+ /**
19
+ * Clear the completion timeout for a specific task
20
+ */
21
+ clearCompletionTimeout(taskId: string): void;
22
+ /**
23
+ * Clear all timeouts for a specific task
24
+ */
25
+ clearTaskTimeouts(taskId: string): void;
26
+ /**
27
+ * Clear all timeouts (used during cancellation)
28
+ */
29
+ clearAllTimeouts(): void;
30
+ }
31
+ /**
32
+ * Creates a timeout manager for task execution
33
+ */
34
+ export declare function createTimeoutManager(): TimeoutManager;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Creates a timeout manager for task execution
3
+ */
4
+ export function createTimeoutManager() {
5
+ const readyTimeouts = new Map();
6
+ const completionTimeouts = new Map();
7
+ return {
8
+ /**
9
+ * Set a timeout for a task's readyWhen condition
10
+ */
11
+ setReadyTimeout(taskId, timeout, callback) {
12
+ // Clear any existing ready timeout for this task
13
+ const timeoutId = readyTimeouts.get(taskId);
14
+ if (timeoutId) {
15
+ clearTimeout(timeoutId);
16
+ readyTimeouts.delete(taskId);
17
+ }
18
+ const newTimeoutId = setTimeout(callback, timeout);
19
+ readyTimeouts.set(taskId, newTimeoutId);
20
+ },
21
+ /**
22
+ * Set a timeout for a task's completion
23
+ */
24
+ setCompletionTimeout(taskId, timeout, callback) {
25
+ // Clear any existing completion timeout for this task
26
+ const timeoutId = completionTimeouts.get(taskId);
27
+ if (timeoutId) {
28
+ clearTimeout(timeoutId);
29
+ completionTimeouts.delete(taskId);
30
+ }
31
+ const newTimeoutId = setTimeout(callback, timeout);
32
+ completionTimeouts.set(taskId, newTimeoutId);
33
+ },
34
+ /**
35
+ * Clear the ready timeout for a specific task
36
+ */
37
+ clearReadyTimeout(taskId) {
38
+ const timeoutId = readyTimeouts.get(taskId);
39
+ if (timeoutId) {
40
+ clearTimeout(timeoutId);
41
+ readyTimeouts.delete(taskId);
42
+ }
43
+ },
44
+ /**
45
+ * Clear the completion timeout for a specific task
46
+ */
47
+ clearCompletionTimeout(taskId) {
48
+ const timeoutId = completionTimeouts.get(taskId);
49
+ if (timeoutId) {
50
+ clearTimeout(timeoutId);
51
+ completionTimeouts.delete(taskId);
52
+ }
53
+ },
54
+ /**
55
+ * Clear all timeouts for a specific task
56
+ */
57
+ clearTaskTimeouts(taskId) {
58
+ const readyTimeoutId = readyTimeouts.get(taskId);
59
+ if (readyTimeoutId) {
60
+ clearTimeout(readyTimeoutId);
61
+ readyTimeouts.delete(taskId);
62
+ }
63
+ const completionTimeoutId = completionTimeouts.get(taskId);
64
+ if (completionTimeoutId) {
65
+ clearTimeout(completionTimeoutId);
66
+ completionTimeouts.delete(taskId);
67
+ }
68
+ },
69
+ /**
70
+ * Clear all timeouts (used during cancellation)
71
+ */
72
+ clearAllTimeouts() {
73
+ // Clear all ready timeouts
74
+ for (const timeoutId of readyTimeouts.values()) {
75
+ clearTimeout(timeoutId);
76
+ }
77
+ readyTimeouts.clear();
78
+ // Clear all completion timeouts
79
+ for (const timeoutId of completionTimeouts.values()) {
80
+ clearTimeout(timeoutId);
81
+ }
82
+ completionTimeouts.clear();
83
+ },
84
+ };
85
+ }
@@ -1,2 +1,2 @@
1
- import type { Task } from "./types.js";
1
+ import type { Task } from "../types.js";
2
2
  export declare function validateTasks(tasks?: Task[]): void;