builderman 1.2.0 → 1.3.0

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/dist/pipeline.js CHANGED
@@ -1,14 +1,13 @@
1
1
  import { spawn } from "node:child_process";
2
- import * as path from "node:path";
3
- import * as fs from "node:fs";
4
2
  import { $TASK_INTERNAL } from "./constants.js";
5
3
  import { createTaskGraph } from "./graph.js";
4
+ import { PipelineError } from "./pipeline-error.js";
6
5
  import { createScheduler } from "./scheduler.js";
7
6
  import { task } from "./task.js";
8
7
  import { validateTasks } from "./util.js";
9
- // Module-level cache for pipeline-to-task conversions
10
- // Key: Pipeline, Value: Map of name -> Task
11
- const pipelineTaskCache = new WeakMap();
8
+ import { createTeardownManager } from "./modules/teardown-manager.js";
9
+ import { createSignalHandler } from "./modules/signal-handler.js";
10
+ import { executeTask } from "./modules/task-executor.js";
12
11
  // Store tasks for each pipeline (for nested pipeline skip tracking)
13
12
  const pipelineTasksCache = new WeakMap();
14
13
  /**
@@ -21,27 +20,21 @@ const pipelineTasksCache = new WeakMap();
21
20
  * await pipeline([task1, task2]).run()
22
21
  */
23
22
  export function pipeline(tasks) {
24
- const graph = createTaskGraph(tasks);
23
+ const tasksClone = [...tasks];
24
+ validateTasks(tasksClone);
25
+ const graph = createTaskGraph(tasksClone);
25
26
  graph.validate();
26
27
  graph.simplify();
27
28
  const pipelineImpl = {
28
- toTask(config) {
29
- validateTasks(config.dependencies);
29
+ toTask({ name, dependencies }) {
30
30
  const syntheticTask = task({
31
- name: config.name,
31
+ name,
32
32
  commands: {},
33
- cwd: ".", // Dummy cwd
34
- dependencies: [...(config.dependencies || [])],
33
+ cwd: ".",
34
+ dependencies,
35
35
  });
36
- // Mark this task as a pipeline task
36
+ // Mark this task as a pipeline task so it can be detected by the executor
37
37
  syntheticTask[$TASK_INTERNAL].pipeline = pipelineImpl;
38
- // Cache this conversion
39
- let cache = pipelineTaskCache.get(pipelineImpl);
40
- if (!cache) {
41
- cache = new Map();
42
- pipelineTaskCache.set(pipelineImpl, cache);
43
- }
44
- cache.set(config.name, syntheticTask);
45
38
  return syntheticTask;
46
39
  },
47
40
  async run(config) {
@@ -49,144 +42,100 @@ export function pipeline(tasks) {
49
42
  const signal = config?.signal;
50
43
  const runningTasks = new Map();
51
44
  const runningPipelines = new Map();
52
- const teardownCommands = new Map();
53
45
  let failed = false;
46
+ const command = config?.command ??
47
+ (process.env.NODE_ENV === "production" ? "build" : "dev");
48
+ const startedAt = Date.now();
49
+ // Initialize task stats tracking
50
+ const taskStats = new Map();
51
+ for (const [taskId, node] of graph.nodes) {
52
+ const task = node.task;
53
+ taskStats.set(taskId, {
54
+ id: taskId,
55
+ name: task.name,
56
+ status: "pending",
57
+ dependencies: Array.from(node.dependencies),
58
+ dependents: Array.from(node.dependents),
59
+ });
60
+ }
61
+ // Helper to update task status
62
+ const updateTaskStatus = (taskId, updates) => {
63
+ const stats = taskStats.get(taskId);
64
+ if (stats) {
65
+ Object.assign(stats, updates);
66
+ }
67
+ };
54
68
  // Check if signal is already aborted
55
69
  if (signal?.aborted) {
56
- throw new PipelineError("Aborted", PipelineError.Aborted);
70
+ const error = new PipelineError("Aborted", PipelineError.Aborted);
71
+ return buildResult(error, startedAt, command, taskStats, "aborted");
57
72
  }
58
73
  const scheduler = createScheduler(graph);
59
74
  let completionResolver = null;
60
- let completionRejector = null;
61
- const completionPromise = new Promise((resolve, reject) => {
75
+ const completionPromise = new Promise((resolve) => {
62
76
  completionResolver = resolve;
63
- completionRejector = reject;
64
77
  });
65
- const executeTeardown = (taskId) => {
66
- const teardown = teardownCommands.get(taskId);
67
- if (!teardown)
68
- return Promise.resolve();
69
- // Remove from map so it doesn't run again
70
- teardownCommands.delete(taskId);
71
- config?.onTaskTeardown?.(teardown.taskName);
72
- return new Promise((resolve) => {
73
- try {
74
- const teardownProcess = spawnFn(teardown.command, {
75
- cwd: teardown.cwd,
76
- stdio: "inherit",
77
- shell: true,
78
- });
79
- let resolved = false;
80
- const resolveOnce = () => {
81
- if (!resolved) {
82
- resolved = true;
83
- resolve();
84
- }
85
- };
86
- teardownProcess.on("error", (error) => {
87
- const teardownError = new Error(`[${teardown.taskName}] Teardown failed: ${error.message}`);
88
- config?.onTaskTeardownError?.(teardown.taskName, teardownError);
89
- resolveOnce();
90
- });
91
- teardownProcess.on("exit", (code) => {
92
- if (code !== 0) {
93
- const teardownError = new Error(`[${teardown.taskName}] Teardown failed with exit code ${code ?? 1}`);
94
- config?.onTaskTeardownError?.(teardown.taskName, teardownError);
95
- }
96
- resolveOnce();
97
- });
78
+ // Initialize teardown manager with stats tracking
79
+ const teardownManager = createTeardownManager({
80
+ ...config,
81
+ spawn: spawnFn,
82
+ updateTaskTeardownStatus: (taskId, status, error) => {
83
+ const stats = taskStats.get(taskId);
84
+ if (stats) {
85
+ stats.teardown = { status, error };
98
86
  }
99
- catch (error) {
100
- const teardownError = new Error(`[${teardown.taskName}] Teardown failed to start: ${error.message}`);
101
- config?.onTaskTeardownError?.(teardown.taskName, teardownError);
102
- resolve();
103
- }
104
- });
105
- };
106
- const executeAllTeardowns = async () => {
107
- // Execute teardowns in reverse dependency order
108
- // Tasks with dependents should be torn down before their dependencies
109
- const taskIdsWithTeardown = Array.from(teardownCommands.keys());
110
- if (taskIdsWithTeardown.length === 0) {
87
+ },
88
+ });
89
+ const advanceScheduler = (input) => {
90
+ // Check if signal is aborted before advancing scheduler
91
+ if (signal?.aborted) {
92
+ failPipeline(new PipelineError("Aborted", PipelineError.Aborted));
111
93
  return;
112
94
  }
113
- // Calculate reverse topological order
114
- // Tasks that have dependents should be torn down first
115
- const teardownOrder = getReverseDependencyOrder(taskIdsWithTeardown, graph);
116
- // Execute teardowns sequentially in reverse dependency order
117
- // This ensures dependents are torn down before their dependencies
118
- for (const taskId of teardownOrder) {
119
- await executeTeardown(taskId);
95
+ let result = input ? scheduler.next(input) : scheduler.next();
96
+ // If we passed skip/complete input and got "idle", the generator processed it
97
+ // but returned the old yield. Call next() again to get the result after processing.
98
+ if (input &&
99
+ (input.type === "skip" || input.type === "complete") &&
100
+ result.value?.type === "idle") {
101
+ result = scheduler.next();
120
102
  }
121
- };
122
- const getReverseDependencyOrder = (taskIds, graph) => {
123
- // Create a set for quick lookup
124
- const taskIdSet = new Set(taskIds);
125
- // For reverse dependency order, we want to tear down dependents before dependencies
126
- // If api depends on db, we want: api first, then db
127
- // This is the reverse of normal execution order
128
- // Count how many dependencies each task has (within the teardown set)
129
- const dependencyCount = new Map();
130
- for (const taskId of taskIds) {
131
- const node = graph.nodes.get(taskId);
132
- if (node) {
133
- let count = 0;
134
- for (const depId of node.dependencies) {
135
- if (taskIdSet.has(depId)) {
136
- count++;
137
- }
138
- }
139
- dependencyCount.set(taskId, count);
103
+ while (true) {
104
+ // Check signal again in the loop
105
+ if (signal?.aborted) {
106
+ failPipeline(new PipelineError("Aborted", PipelineError.Aborted));
107
+ return;
140
108
  }
141
- }
142
- // Build result in reverse order
143
- // Start with tasks that have no dependencies (leaf nodes) - these go LAST
144
- const result = [];
145
- const visited = new Set();
146
- const queue = [];
147
- // Find leaf nodes (tasks with no dependencies in teardown set)
148
- for (const taskId of taskIds) {
149
- if (dependencyCount.get(taskId) === 0) {
150
- queue.push(taskId);
109
+ const event = result.value;
110
+ const isFinished = result.done && result.value.type === "done";
111
+ if (isFinished) {
112
+ completionResolver?.(null);
113
+ return;
151
114
  }
152
- }
153
- // Process in reverse topological order
154
- while (queue.length > 0) {
155
- const taskId = queue.shift();
156
- if (visited.has(taskId))
115
+ if (event.type === "run") {
116
+ startTask(graph.nodes.get(event.taskId).task);
117
+ result = scheduler.next();
157
118
  continue;
158
- visited.add(taskId);
159
- // Add to front (so we get reverse order: dependents before dependencies)
160
- result.unshift(taskId);
161
- // Find tasks that depend on this one (dependents)
162
- const node = graph.nodes.get(taskId);
163
- if (node) {
164
- for (const dependentId of node.dependents) {
165
- if (taskIdSet.has(dependentId) && !visited.has(dependentId)) {
166
- const currentCount = dependencyCount.get(dependentId) ?? 0;
167
- const newCount = currentCount - 1;
168
- dependencyCount.set(dependentId, newCount);
169
- // When a dependent has no more dependencies to wait for, add it to queue
170
- if (newCount === 0) {
171
- queue.push(dependentId);
172
- }
173
- }
174
- }
175
119
  }
176
- }
177
- // Add any remaining tasks (shouldn't happen in a valid graph, but handle it)
178
- for (const taskId of taskIds) {
179
- if (!visited.has(taskId)) {
180
- result.unshift(taskId);
120
+ if (event.type === "idle") {
121
+ return;
181
122
  }
182
123
  }
183
- return result;
184
124
  };
185
125
  const failPipeline = async (error) => {
186
126
  if (failed)
187
127
  return;
188
128
  failed = true;
189
- for (const child of runningTasks.values()) {
129
+ // Mark running tasks as aborted
130
+ for (const [taskId, child] of runningTasks.entries()) {
131
+ updateTaskStatus(taskId, {
132
+ status: "aborted",
133
+ finishedAt: Date.now(),
134
+ });
135
+ const stats = taskStats.get(taskId);
136
+ if (stats && stats.startedAt) {
137
+ stats.durationMs = stats.finishedAt - stats.startedAt;
138
+ }
190
139
  try {
191
140
  child.kill("SIGTERM");
192
141
  }
@@ -202,379 +151,113 @@ export function pipeline(tasks) {
202
151
  // Execute all teardown commands and wait for them to complete
203
152
  // Even if teardowns fail, we still want to reject the pipeline
204
153
  try {
205
- await executeAllTeardowns();
154
+ await teardownManager.executeAll(graph);
206
155
  }
207
156
  catch (teardownError) {
208
157
  // Teardown errors are already reported via onTaskTeardownError
209
158
  // We continue to reject the pipeline with the original error
210
159
  }
211
- config?.onPipelineError?.(error);
212
- completionRejector?.(error);
160
+ completionResolver?.(error);
213
161
  };
214
162
  const startTask = (task) => {
215
- // Check if signal is aborted before starting new tasks
216
- if (signal?.aborted) {
217
- failPipeline(new PipelineError("Aborted", PipelineError.InvalidSignal));
218
- return;
219
- }
220
- const { name: taskName, [$TASK_INTERNAL]: { id: taskId, pipeline: nestedPipeline }, } = task;
221
- if (runningTasks.has(taskId))
222
- return;
223
- // Handle pipeline tasks
224
- if (nestedPipeline) {
225
- // Track nested pipeline state for skip behavior
226
- let nestedSkippedCount = 0;
227
- let nestedCompletedCount = 0;
228
- // Get total tasks from nested pipeline
229
- const nestedTasks = pipelineTasksCache.get(nestedPipeline);
230
- const nestedTotalTasks = nestedTasks
231
- ? createTaskGraph(nestedTasks).nodes.size
232
- : 0;
233
- // Mark as ready immediately (pipeline entry nodes will handle their own ready state)
234
- advanceScheduler({ type: "ready", taskId });
235
- config?.onTaskBegin?.(taskName);
236
- // Create an abort controller to stop the nested pipeline if needed
237
- let pipelineStopped = false;
238
- const stopPipeline = () => {
239
- pipelineStopped = true;
240
- // The nested pipeline will continue running, but we've marked it as stopped
241
- // In a more sophisticated implementation, we could propagate stop signals
242
- };
243
- runningPipelines.set(taskId, { stop: stopPipeline });
244
- // Run the nested pipeline with signal propagation
245
- nestedPipeline
246
- .run({
247
- spawn: spawnFn,
248
- command: config?.command,
249
- strict: config?.strict,
250
- signal, // Pass signal to nested pipeline
251
- onTaskBegin: (nestedTaskName) => {
252
- if (pipelineStopped)
253
- return;
254
- config?.onTaskBegin?.(`${taskName}:${nestedTaskName}`);
255
- },
256
- onTaskComplete: (nestedTaskName) => {
257
- if (pipelineStopped)
258
- return;
259
- nestedCompletedCount++;
260
- config?.onTaskComplete?.(`${taskName}:${nestedTaskName}`);
261
- },
262
- onTaskSkipped: (nestedTaskName, mode) => {
263
- if (pipelineStopped)
264
- return;
265
- nestedSkippedCount++;
266
- config?.onTaskSkipped?.(`${taskName}:${nestedTaskName}`, mode);
267
- },
268
- onPipelineError: (error) => {
269
- if (pipelineStopped)
270
- return;
271
- runningPipelines.delete(taskId);
272
- // error is already a PipelineError
273
- failPipeline(error);
274
- },
275
- onPipelineComplete: () => {
276
- if (pipelineStopped)
277
- return;
278
- runningPipelines.delete(taskId);
279
- // Determine nested pipeline result based on skip behavior:
280
- // - If all inner tasks are skipped → outer task is skipped
281
- // - If some run, some skip → outer task is completed
282
- // - If any fail → outer task fails (handled in onPipelineError)
283
- const commandName = config?.command ?? process.env.NODE_ENV === "production"
284
- ? "build"
285
- : "dev";
286
- if (nestedSkippedCount === nestedTotalTasks &&
287
- nestedTotalTasks > 0) {
288
- // All tasks were skipped
289
- console.log(`[${taskName}] skipped (all nested tasks skipped)`);
290
- config?.onTaskSkipped?.(taskName, commandName);
291
- setImmediate(() => {
292
- advanceScheduler({ type: "skip", taskId });
293
- });
294
- }
295
- else {
296
- // Some tasks ran (and completed successfully)
297
- config?.onTaskComplete?.(taskName);
298
- advanceScheduler({ type: "complete", taskId });
299
- }
300
- },
301
- })
302
- .catch((error) => {
303
- if (pipelineStopped)
304
- return;
305
- runningPipelines.delete(taskId);
306
- failPipeline(error);
307
- });
308
- return;
309
- }
310
- // Regular task execution
311
- const commandName = config?.command ?? process.env.NODE_ENV === "production"
312
- ? "build"
313
- : "dev";
314
- const commandConfig = task[$TASK_INTERNAL].commands[commandName];
315
- // Check if command exists
316
- if (commandConfig === undefined) {
317
- const allowSkip = task[$TASK_INTERNAL].allowSkip ?? false;
318
- const strict = config?.strict ?? false;
319
- // If strict mode and not explicitly allowed to skip, fail
320
- if (strict && !allowSkip) {
321
- failPipeline(new PipelineError(`[${taskName}] No command for "${commandName}" and strict mode is enabled`, PipelineError.TaskFailed));
322
- return;
323
- }
324
- // Skip the task
325
- console.log(`[${taskName}] skipped "${commandName}"`);
326
- config?.onTaskSkipped?.(taskName, commandName);
327
- // Mark as skipped - this satisfies dependencies and unblocks dependents
328
- // Use setImmediate to ensure scheduler is at idle yield before receiving skip
329
- setImmediate(() => {
330
- advanceScheduler({ type: "skip", taskId });
331
- });
332
- return;
333
- }
334
- const command = typeof commandConfig === "string" ? commandConfig : commandConfig.run;
335
- const readyWhen = typeof commandConfig === "string"
336
- ? undefined
337
- : commandConfig.readyWhen;
338
- const readyTimeout = typeof commandConfig === "string"
339
- ? Infinity
340
- : commandConfig.readyTimeout ?? Infinity;
341
- const teardown = typeof commandConfig === "string" ? undefined : commandConfig.teardown;
342
- const { cwd } = task[$TASK_INTERNAL];
343
- const taskCwd = path.isAbsolute(cwd)
344
- ? cwd
345
- : path.resolve(process.cwd(), cwd);
346
- if (!fs.existsSync(taskCwd)) {
347
- failPipeline(new PipelineError(`[${taskName}] Working directory does not exist: ${taskCwd}`, PipelineError.InvalidTask));
348
- return;
349
- }
350
- const accumulatedPath = [
351
- path.join(taskCwd, "node_modules", ".bin"),
352
- path.join(process.cwd(), "node_modules", ".bin"),
353
- process.env.PATH,
354
- ]
355
- .filter(Boolean)
356
- .join(process.platform === "win32" ? ";" : ":");
357
- const env = {
358
- ...process.env,
359
- PATH: accumulatedPath,
360
- Path: accumulatedPath,
361
- };
362
- const child = spawnFn(command, {
363
- cwd: taskCwd,
364
- stdio: ["inherit", "pipe", "pipe"],
365
- shell: true,
366
- env,
163
+ executeTask(task, {
164
+ spawn: spawnFn,
165
+ signal,
166
+ config,
167
+ graph,
168
+ runningTasks,
169
+ runningPipelines,
170
+ teardownManager,
171
+ pipelineTasksCache,
172
+ failPipeline,
173
+ advanceScheduler,
174
+ updateTaskStatus,
175
+ taskStats,
367
176
  });
368
- runningTasks.set(taskId, child);
369
- // Store teardown command if provided
370
- if (teardown) {
371
- teardownCommands.set(taskId, {
372
- command: teardown,
373
- cwd: taskCwd,
374
- taskName,
375
- });
376
- }
377
- config?.onTaskBegin?.(taskName);
378
- let didMarkReady = false;
379
- let readyTimeoutId = null;
380
- if (!readyWhen) {
381
- advanceScheduler({ type: "ready", taskId });
382
- didMarkReady = true;
383
- }
384
- else if (readyTimeout !== Infinity) {
385
- // Set up timeout for readyWhen condition
386
- readyTimeoutId = setTimeout(() => {
387
- if (!didMarkReady) {
388
- failPipeline(new PipelineError(`[${taskName}] Task did not become ready within ${readyTimeout}ms`, PipelineError.TaskFailed));
389
- }
390
- }, readyTimeout);
391
- }
392
- let output = "";
393
- child.stdout?.on("data", (buf) => {
394
- // Check if signal is aborted before processing stdout
395
- if (signal?.aborted) {
396
- return;
397
- }
398
- const chunk = buf.toString();
399
- output += chunk;
400
- process.stdout.write(chunk);
401
- if (!didMarkReady && readyWhen && readyWhen(output)) {
402
- if (readyTimeoutId) {
403
- clearTimeout(readyTimeoutId);
404
- readyTimeoutId = null;
405
- }
406
- advanceScheduler({ type: "ready", taskId });
407
- didMarkReady = true;
408
- }
409
- });
410
- child.stderr?.on("data", (buf) => {
411
- process.stderr.write(buf);
412
- });
413
- child.on("error", (error) => {
414
- // Task failed before entering running state, so don't execute teardown
415
- // Remove teardown from map since it was never actually running
416
- teardownCommands.delete(taskId);
417
- // Clear ready timeout if it exists
418
- if (readyTimeoutId) {
419
- clearTimeout(readyTimeoutId);
420
- readyTimeoutId = null;
421
- }
422
- failPipeline(new PipelineError(`[${taskName}] Failed to start: ${error.message}`, PipelineError.TaskFailed));
423
- });
424
- child.on("exit", (code) => {
425
- runningTasks.delete(taskId);
426
- // Clear ready timeout if it exists
427
- if (readyTimeoutId) {
428
- clearTimeout(readyTimeoutId);
429
- readyTimeoutId = null;
430
- }
431
- // Don't execute teardown immediately - it will be executed in reverse dependency order
432
- // when the pipeline completes or fails
433
- if (code !== 0) {
434
- failPipeline(new PipelineError(`[${taskName}] Task failed with exit code ${code ?? 1}`, PipelineError.TaskFailed));
435
- return;
436
- }
437
- config?.onTaskComplete?.(taskName);
438
- // 🔑 Notify scheduler and drain newly runnable tasks
439
- advanceScheduler({ type: "complete", taskId });
440
- });
441
- };
442
- const advanceScheduler = (input) => {
443
- // Check if signal is aborted before advancing scheduler
444
- if (signal?.aborted) {
445
- failPipeline(new PipelineError("Aborted", PipelineError.Aborted));
446
- return;
447
- }
448
- let result = input ? scheduler.next(input) : scheduler.next();
449
- // If we passed skip/complete input and got "idle", the generator processed it
450
- // but returned the old yield. Call next() again to get the result after processing.
451
- if (input &&
452
- (input.type === "skip" || input.type === "complete") &&
453
- result.value?.type === "idle") {
454
- result = scheduler.next();
455
- }
456
- while (true) {
457
- // Check signal again in the loop
458
- if (signal?.aborted) {
459
- failPipeline(new PipelineError("Aborted", PipelineError.Aborted));
460
- return;
461
- }
462
- const event = result.value;
463
- const isFinished = result.done && result.value.type === "done";
464
- if (isFinished) {
465
- config?.onPipelineComplete?.();
466
- completionResolver?.();
467
- return;
468
- }
469
- if (event.type === "run") {
470
- startTask(graph.nodes.get(event.taskId).task);
471
- result = scheduler.next();
472
- continue;
473
- }
474
- if (event.type === "idle") {
475
- return;
476
- }
477
- }
478
177
  };
479
- // Handle termination signals
480
- const processTerminationListenerCleanups = [
481
- "SIGINT",
482
- "SIGTERM",
483
- "SIGQUIT",
484
- "SIGBREAK",
485
- ].map((sig) => {
486
- const handleSignal = () => {
487
- failPipeline(new PipelineError(`Received ${sig}`, PipelineError.ProcessTerminated));
488
- };
489
- process.once(sig, handleSignal);
490
- return () => {
491
- process.removeListener(sig, handleSignal);
492
- };
178
+ // Initialize signal handler
179
+ const signalHandler = createSignalHandler({
180
+ abortSignal: signal,
181
+ onAborted: () => failPipeline(new PipelineError("Aborted", PipelineError.Aborted)),
182
+ onProcessTerminated: (message) => new PipelineError(message, PipelineError.ProcessTerminated),
493
183
  });
494
- // Handle abort signal if provided
495
- let signalCleanup = null;
496
- if (signal) {
497
- const handleAbort = () => {
498
- failPipeline(new PipelineError("Aborted", PipelineError.Aborted));
499
- };
500
- signal.addEventListener("abort", handleAbort);
501
- signalCleanup = () => {
502
- signal.removeEventListener("abort", handleAbort);
503
- };
504
- }
505
184
  // 🚀 Kick off initial runnable tasks
506
185
  advanceScheduler();
507
- await completionPromise
508
- .then(async () => {
509
- // Pipeline completed successfully - execute any remaining teardowns
186
+ const error = await completionPromise.then(async (err) => {
187
+ // Pipeline completed - execute any remaining teardowns
510
188
  // (for tasks that completed successfully) and wait for them to complete
511
189
  // Even if teardowns fail, the pipeline still resolves successfully
512
190
  // (teardown errors are reported via onTaskTeardownError)
513
191
  try {
514
- await executeAllTeardowns();
192
+ await teardownManager.executeAll(graph);
515
193
  }
516
194
  catch (teardownError) {
517
195
  // Teardown errors are already reported via onTaskTeardownError
518
196
  // The pipeline still resolves successfully
519
197
  }
520
- })
521
- .finally(() => {
522
- processTerminationListenerCleanups.forEach((cleanup) => cleanup());
523
- signalCleanup?.();
198
+ return err;
524
199
  });
200
+ signalHandler.cleanup();
201
+ const status = error
202
+ ? error.code === PipelineError.Aborted
203
+ ? "aborted"
204
+ : "failed"
205
+ : "success";
206
+ return buildResult(error, startedAt, command, taskStats, status);
525
207
  },
526
208
  };
527
209
  // Store tasks for nested pipeline skip tracking
528
- pipelineTasksCache.set(pipelineImpl, tasks);
210
+ pipelineTasksCache.set(pipelineImpl, tasksClone);
529
211
  return pipelineImpl;
530
212
  }
531
- export class PipelineError extends Error {
532
- constructor(message, code, taskName) {
533
- super(message);
534
- Object.defineProperty(this, "code", {
535
- enumerable: true,
536
- configurable: true,
537
- writable: true,
538
- value: void 0
539
- });
540
- Object.defineProperty(this, "taskName", {
541
- enumerable: true,
542
- configurable: true,
543
- writable: true,
544
- value: void 0
545
- });
546
- this.name = "PipelineError";
547
- this.code = code;
548
- this.taskName = taskName;
213
+ function buildResult(error, startedAt, command, taskStats, status) {
214
+ const finishedAt = Date.now();
215
+ const durationMs = finishedAt - startedAt;
216
+ // Convert task stats map to record
217
+ const tasks = {};
218
+ for (const [taskId, stats] of taskStats) {
219
+ tasks[taskId] = stats;
220
+ }
221
+ // Calculate summary
222
+ let completed = 0;
223
+ let failed = 0;
224
+ let skipped = 0;
225
+ let running = 0;
226
+ for (const stats of taskStats.values()) {
227
+ switch (stats.status) {
228
+ case "completed":
229
+ completed++;
230
+ break;
231
+ case "failed":
232
+ failed++;
233
+ break;
234
+ case "skipped":
235
+ skipped++;
236
+ break;
237
+ case "running":
238
+ running++;
239
+ break;
240
+ }
241
+ }
242
+ const pipelineStats = {
243
+ command,
244
+ startedAt,
245
+ finishedAt,
246
+ durationMs,
247
+ status,
248
+ tasks,
249
+ summary: {
250
+ total: taskStats.size,
251
+ completed,
252
+ failed,
253
+ skipped,
254
+ running,
255
+ },
256
+ };
257
+ if (error) {
258
+ return { ok: false, error, stats: pipelineStats };
259
+ }
260
+ else {
261
+ return { ok: true, error: null, stats: pipelineStats };
549
262
  }
550
263
  }
551
- Object.defineProperty(PipelineError, "Aborted", {
552
- enumerable: true,
553
- configurable: true,
554
- writable: true,
555
- value: 0
556
- });
557
- Object.defineProperty(PipelineError, "ProcessTerminated", {
558
- enumerable: true,
559
- configurable: true,
560
- writable: true,
561
- value: 1
562
- });
563
- Object.defineProperty(PipelineError, "TaskFailed", {
564
- enumerable: true,
565
- configurable: true,
566
- writable: true,
567
- value: 2
568
- });
569
- Object.defineProperty(PipelineError, "InvalidSignal", {
570
- enumerable: true,
571
- configurable: true,
572
- writable: true,
573
- value: 3
574
- });
575
- Object.defineProperty(PipelineError, "InvalidTask", {
576
- enumerable: true,
577
- configurable: true,
578
- writable: true,
579
- value: 4
580
- });