@visulima/task-runner 1.0.0-alpha.3 → 1.0.0-alpha.5
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/CHANGELOG.md +58 -0
- package/README.md +193 -51
- package/dist/affected.d.ts +37 -3
- package/dist/cache.d.ts +8 -1
- package/dist/command-parser/expand-arguments.d.ts +11 -0
- package/dist/command-parser/expand-shortcut.d.ts +15 -0
- package/dist/command-parser/expand-wildcard.d.ts +13 -0
- package/dist/command-parser/index.d.ts +18 -0
- package/dist/command-parser/strip-quotes.d.ts +6 -0
- package/dist/concurrent-fallback.d.ts +16 -0
- package/dist/concurrent.d.ts +23 -0
- package/dist/detect-shell.d.ts +19 -0
- package/dist/flow-controllers/index.d.ts +7 -0
- package/dist/flow-controllers/input-handler.d.ts +44 -0
- package/dist/flow-controllers/log-timings.d.ts +18 -0
- package/dist/flow-controllers/restart-process.d.ts +21 -0
- package/dist/flow-controllers/teardown.d.ts +22 -0
- package/dist/index.d.ts +14 -4
- package/dist/index.js +26 -12
- package/dist/native-binding.d.ts +44 -2
- package/dist/packem_shared/{Cache-IYpTYVUC.js → Cache-iAjRMV2d.js} +5 -5
- package/dist/packem_shared/{FingerprintManager-D6Y0erg-.js → FingerprintManager-Cu-ta9ee.js} +0 -1
- package/dist/packem_shared/{IncrementalFileHasher-Ds3J6dgb.js → IncrementalFileHasher-Cm_kJY5V.js} +1 -1
- package/dist/packem_shared/{TaskOrchestrator-BvYs3ONw.js → TaskOrchestrator-lLn-PH1m.js} +2 -5
- package/dist/packem_shared/TerminalBuffer-CnPyFgPB.js +266 -0
- package/dist/packem_shared/{filterAffectedTasks-I-18zPg6.js → buildForwardDependencyMap-0BJFMMPv.js} +61 -21
- package/dist/packem_shared/{computeTaskHash-BoCnnvIJ.js → computeTaskHash-B2SVZqgp.js} +1 -2
- package/dist/packem_shared/createInputHandler-DTfePcTG.js +37 -0
- package/dist/packem_shared/{defaultTaskRunner-CrW4v5Ye.js → defaultTaskRunner-BdFTifsh.js} +6 -7
- package/dist/packem_shared/detectScriptShell-CR-xXKA4.js +53 -0
- package/dist/packem_shared/enforceProjectConstraints-C5Jp_C3u.js +111 -0
- package/dist/packem_shared/expandArguments-0AwD2BIA.js +26 -0
- package/dist/packem_shared/expandShortcut-BVG05ee4.js +23 -0
- package/dist/packem_shared/expandWildcard-B0xN_knq.js +107 -0
- package/dist/packem_shared/{findCycle-DF4_BRdO.js → findCycle-DefgNYhg.js} +1 -1
- package/dist/packem_shared/formatTimingTable-3qtCM552.js +46 -0
- package/dist/packem_shared/isNativeAvailable-BpD28A6Z.js +44 -0
- package/dist/packem_shared/parseCommands-D-IgF8Zh.js +26 -0
- package/dist/packem_shared/{TaskScheduler-CJilHDta.js → parsePartition-C4-P5RjK.js} +44 -1
- package/dist/packem_shared/{projectGraphToDot-VdTjHcVp.js → projectGraphToDot-C8uYeaPo.js} +20 -3
- package/dist/packem_shared/runConcurrentFallback-CGHz_f-Q.js +371 -0
- package/dist/packem_shared/runConcurrently-qrkWyzXW.js +67 -0
- package/dist/packem_shared/runTeardown-BAezH79J.js +49 -0
- package/dist/packem_shared/stripQuotes-Cey-zwFf.js +9 -0
- package/dist/packem_shared/withRestart-BREjRJa4.js +49 -0
- package/dist/project-constraints.d.ts +9 -0
- package/dist/task-scheduler.d.ts +23 -0
- package/dist/terminal-buffer.d.ts +29 -0
- package/dist/types.d.ts +239 -1
- package/index.js +769 -0
- package/package.json +14 -13
- package/binding.js +0 -204
- package/dist/packem_shared/isNativeAvailable-BWhnZ4ES.js +0 -19
- package/dist/packem_shared/{RemoteCache-BDqrnDEi.js → RemoteCache-BFceSe4a.js} +1 -1
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
const parsePartition = (value) => {
|
|
2
|
+
const raw = value ?? process.env.VIS_PARTITION;
|
|
3
|
+
if (!raw) {
|
|
4
|
+
return void 0;
|
|
5
|
+
}
|
|
6
|
+
const parts = raw.split("/");
|
|
7
|
+
if (parts.length !== 2) {
|
|
8
|
+
throw new Error(`Invalid partition format: "${raw}". Expected format: "index/total" (e.g., "1/4").`);
|
|
9
|
+
}
|
|
10
|
+
const index = Number(parts[0]);
|
|
11
|
+
const total = Number(parts[1]);
|
|
12
|
+
if (!Number.isInteger(index) || !Number.isInteger(total) || index < 1 || total < 1) {
|
|
13
|
+
throw new Error(`Invalid partition values: "${raw}". Both index and total must be positive integers.`);
|
|
14
|
+
}
|
|
15
|
+
if (index > total) {
|
|
16
|
+
throw new Error(`Invalid partition index: ${index} exceeds total ${total}.`);
|
|
17
|
+
}
|
|
18
|
+
return { index, total };
|
|
19
|
+
};
|
|
1
20
|
const calculateProjectDepths = (projectGraph) => {
|
|
2
21
|
const depths = /* @__PURE__ */ new Map();
|
|
3
22
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -31,6 +50,30 @@ class TaskScheduler {
|
|
|
31
50
|
#totalTasks;
|
|
32
51
|
#dependentCounts;
|
|
33
52
|
#projectDepths;
|
|
53
|
+
/**
|
|
54
|
+
* Partitions a list of tasks for distributed CI execution.
|
|
55
|
+
* Tasks are sorted by ID for deterministic distribution, then split
|
|
56
|
+
* using ceiling division so partitions differ by at most one task.
|
|
57
|
+
* @param tasks The full list of tasks to partition
|
|
58
|
+
* @param partition The partition configuration (1-based index and total)
|
|
59
|
+
* @returns The subset of tasks assigned to this partition
|
|
60
|
+
*/
|
|
61
|
+
static partitionTasks(tasks, partition) {
|
|
62
|
+
if (partition.total < 1) {
|
|
63
|
+
throw new Error(`Invalid partition total: ${partition.total}. Must be at least 1.`);
|
|
64
|
+
}
|
|
65
|
+
if (partition.index < 1 || partition.index > partition.total) {
|
|
66
|
+
throw new Error(`Invalid partition index: ${partition.index}. Must be between 1 and ${partition.total}.`);
|
|
67
|
+
}
|
|
68
|
+
if (tasks.length === 0) {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
const sorted = [...tasks].toSorted((a, b) => a.id.localeCompare(b.id));
|
|
72
|
+
const size = Math.ceil(sorted.length / partition.total);
|
|
73
|
+
const start = size * (partition.index - 1);
|
|
74
|
+
const end = partition.index === partition.total ? sorted.length : size * partition.index;
|
|
75
|
+
return sorted.slice(start, end);
|
|
76
|
+
}
|
|
34
77
|
constructor(taskGraph, projectGraph, maxParallel = 3) {
|
|
35
78
|
this.#taskGraph = taskGraph;
|
|
36
79
|
this.#maxParallel = maxParallel;
|
|
@@ -108,4 +151,4 @@ class TaskScheduler {
|
|
|
108
151
|
}
|
|
109
152
|
}
|
|
110
153
|
|
|
111
|
-
export { TaskScheduler };
|
|
154
|
+
export { TaskScheduler, parsePartition };
|
|
@@ -153,7 +153,7 @@ const toGraphHtml = (taskGraph, options = {}) => {
|
|
|
153
153
|
graphData.edges = graphData.edges.filter((e) => focused.has(e.source) && focused.has(e.target));
|
|
154
154
|
graphData.roots = graphData.roots.filter((r) => focused.has(r));
|
|
155
155
|
}
|
|
156
|
-
return String.raw(_a || (_a = __template(['<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<title>Task Graph</title>\n<style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body { font-family: system-ui, -apple-system, sans-serif; background: #1a1a2e; color: #eee; }\n svg { width: 100vw; height: 100vh; }\n .node rect { rx: 6; ry: 6; stroke: #555; stroke-width: 1.5; cursor: pointer; }\n .node text { font-size: 11px; fill: #1a1a2e; font-weight: 600; pointer-events: none; }\n .node:hover rect { stroke: #fff; stroke-width: 2; }\n .edge { stroke: #444; stroke-width: 1.5; fill: none; marker-end: url(#arrow); }\n .label { font-size: 10px; fill: #888; }\n #info { position: fixed; top: 12px; right: 12px; background: #16213e; padding: 12px 16px; border-radius: 8px; font-size: 13px; }\n #info b { color: #e94560; }\n</style>\n</head>\n<body>\n<div id="info">\n <b>', "</b> tasks · <b>", "</b> dependencies · <b>", '</b> roots\n</div>\n<svg id="graph">\n <defs>\n <marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto">\n <path d="M 0 0 L 10 5 L 0 10 z" fill="#666"/>\n </marker>\n </defs>\n</svg>\n<script>\nconst data = ', ";\nconst svg = document.getElementById('graph');\nconst W = window.innerWidth, H = window.innerHeight;\nconst statusColors = {\n success: '#2ecc71', 'local-cache': '#3498db', 'remote-cache': '#9b59b6',\n failure: '#e74c3c', running: '#f39c12', skipped: '#95a5a6', pending: '#ecf0f1'\n};\nconst projectColors = {};\nconst palette = ['#e94560','#0f3460','#533483','#16c79a','#f39c12','#2ecc71','#3498db','#e67e22','#9b59b6','#1abc9c'];\nlet ci = 0;\ndata.nodes.forEach(n => {\n if (!projectColors[n.project]) projectColors[n.project] = palette[ci++ % palette.length];\n});\n\n// Simple force-directed layout\nconst nodes = data.nodes.map((n, i) => ({\n ...n, x: W/2 + (Math.random()-0.5)*400, y: H/2 + (Math.random()-0.5)*300, vx: 0, vy: 0\n}));\nconst nodeMap = new Map(nodes.map(n => [n.id, n]));\nconst edges = data.edges.map(e => ({ source: nodeMap.get(e.source), target: nodeMap.get(e.target) }));\n\nfunction simulate() {\n for (let iter = 0; iter < 300; iter++) {\n // Repulsion\n for (let i = 0; i < nodes.length; i++) {\n for (let j = i+1; j < nodes.length; j++) {\n let dx = nodes[j].x - nodes[i].x, dy = nodes[j].y - nodes[i].y;\n let d = Math.sqrt(dx*dx + dy*dy) || 1;\n let f = 8000 / (d * d);\n nodes[i].vx -= dx/d * f; nodes[i].vy -= dy/d * f;\n nodes[j].vx += dx/d * f; nodes[j].vy += dy/d * f;\n }\n }\n // Attraction (edges)\n edges.forEach(e => {\n if (!e.source || !e.target) return;\n let dx = e.target.x - e.source.x, dy = e.target.y - e.source.y;\n let d = Math.sqrt(dx*dx + dy*dy) || 1;\n let f = (d - 150) * 0.01;\n e.source.vx += dx/d * f; e.source.vy += dy/d * f;\n e.target.vx -= dx/d * f; e.target.vy -= dy/d * f;\n });\n // Gravity\n nodes.forEach(n => {\n n.vx += (W/2 - n.x) * 0.001;\n n.vy += (H/2 - n.y) * 0.001;\n n.x += n.vx * 0.3; n.y += n.vy * 0.3;\n n.vx *= 0.8; n.vy *= 0.8;\n n.x = Math.max(60, Math.min(W-60, n.x));\n n.y = Math.max(30, Math.min(H-30, n.y));\n });\n }\n}\nsimulate();\n\n// Render\nedges.forEach(e => {\n if (!e.source || !e.target) return;\n const line = document.createElementNS('http://www.w3.org/2000/svg','line');\n line.setAttribute('x1', e.source.x); line.setAttribute('y1', e.source.y);\n line.setAttribute('x2', e.target.x); line.setAttribute('y2', e.target.y);\n line.setAttribute('class','edge');\n svg.appendChild(line);\n});\nnodes.forEach(n => {\n const g = document.createElementNS('http://www.w3.org/2000/svg','g');\n g.setAttribute('class','node');\n g.setAttribute('transform','translate('+(n.x-50)+','+(n.y-14)+')');\n const rect = document.createElementNS('http://www.w3.org/2000/svg','rect');\n rect.setAttribute('width','100'); rect.setAttribute('height','28');\n rect.setAttribute('fill', n.status ? (statusColors[n.status]||'#ecf0f1') : projectColors[n.project]);\n g.appendChild(rect);\n const text = document.createElementNS('http://www.w3.org/2000/svg','text');\n text.setAttribute('x','50'); text.setAttribute('y','18'); text.setAttribute('text-anchor','middle');\n text.textContent = n.id.length > 14 ? n.target : n.id;\n g.appendChild(text);\n g.addEventListener('click', () => {\n const deps = data.edges.filter(e => e.source === n.id).map(e => e.target);\n const rdeps = data.edges.filter(e => e.target === n.id).map(e => e.source);\n alert(n.id + '\n\nDepends on: ' + (deps.join(', ')||'none') + '\nRequired by: ' + (rdeps.join(', ')||'none'));\n });\n svg.appendChild(g);\n});\n<\/script>\n</body>\n</html>"], ['<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<title>Task Graph</title>\n<style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body { font-family: system-ui, -apple-system, sans-serif; background: #1a1a2e; color: #eee; }\n svg { width: 100vw; height: 100vh; }\n .node rect { rx: 6; ry: 6; stroke: #555; stroke-width: 1.5; cursor: pointer; }\n .node text { font-size: 11px; fill: #1a1a2e; font-weight: 600; pointer-events: none; }\n .node:hover rect { stroke: #fff; stroke-width: 2; }\n .edge { stroke: #444; stroke-width: 1.5; fill: none; marker-end: url(#arrow); }\n .label { font-size: 10px; fill: #888; }\n #info { position: fixed; top: 12px; right: 12px; background: #16213e; padding: 12px 16px; border-radius: 8px; font-size: 13px; }\n #info b { color: #e94560; }\n</style>\n</head>\n<body>\n<div id="info">\n <b>', "</b> tasks · <b>", "</b> dependencies · <b>", '</b> roots\n</div>\n<svg id="graph">\n <defs>\n <marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto">\n <path d="M 0 0 L 10 5 L 0 10 z" fill="#666"/>\n </marker>\n </defs>\n</svg>\n<script>\nconst data = ', ";\nconst svg = document.getElementById('graph');\nconst W = window.innerWidth, H = window.innerHeight;\nconst statusColors = {\n success: '#2ecc71', 'local-cache': '#3498db', 'remote-cache': '#9b59b6',\n failure: '#e74c3c', running: '#f39c12', skipped: '#95a5a6', pending: '#ecf0f1'\n};\nconst projectColors = {};\nconst palette = ['#e94560','#0f3460','#533483','#16c79a','#f39c12','#2ecc71','#3498db','#e67e22','#9b59b6','#1abc9c'];\nlet ci = 0;\ndata.nodes.forEach(n => {\n if (!projectColors[n.project]) projectColors[n.project] = palette[ci++ % palette.length];\n});\n\n// Simple force-directed layout\nconst nodes = data.nodes.map((n, i) => ({\n ...n, x: W/2 + (Math.random()-0.5)*400, y: H/2 + (Math.random()-0.5)*300, vx: 0, vy: 0\n}));\nconst nodeMap = new Map(nodes.map(n => [n.id, n]));\nconst edges = data.edges.map(e => ({ source: nodeMap.get(e.source), target: nodeMap.get(e.target) }));\n\nfunction simulate() {\n for (let iter = 0; iter < 300; iter++) {\n // Repulsion\n for (let i = 0; i < nodes.length; i++) {\n for (let j = i+1; j < nodes.length; j++) {\n let dx = nodes[j].x - nodes[i].x, dy = nodes[j].y - nodes[i].y;\n let d = Math.sqrt(dx*dx + dy*dy) || 1;\n let f = 8000 / (d * d);\n nodes[i].vx -= dx/d * f; nodes[i].vy -= dy/d * f;\n nodes[j].vx += dx/d * f; nodes[j].vy += dy/d * f;\n }\n }\n // Attraction (edges)\n edges.forEach(e => {\n if (!e.source || !e.target) return;\n let dx = e.target.x - e.source.x, dy = e.target.y - e.source.y;\n let d = Math.sqrt(dx*dx + dy*dy) || 1;\n let f = (d - 150) * 0.01;\n e.source.vx += dx/d * f; e.source.vy += dy/d * f;\n e.target.vx -= dx/d * f; e.target.vy -= dy/d * f;\n });\n // Gravity\n nodes.forEach(n => {\n n.vx += (W/2 - n.x) * 0.001;\n n.vy += (H/2 - n.y) * 0.001;\n n.x += n.vx * 0.3; n.y += n.vy * 0.3;\n n.vx *= 0.8; n.vy *= 0.8;\n n.x = Math.max(60, Math.min(W-60, n.x));\n n.y = Math.max(30, Math.min(H-30, n.y));\n });\n }\n}\nsimulate();\n\n// Render\nedges.forEach(e => {\n if (!e.source || !e.target) return;\n const line = document.createElementNS('http://www.w3.org/2000/svg','line');\n line.setAttribute('x1', e.source.x); line.setAttribute('y1', e.source.y);\n line.setAttribute('x2', e.target.x); line.setAttribute('y2', e.target.y);\n line.setAttribute('class','edge');\n svg.appendChild(line);\n});\nnodes.forEach(n => {\n const g = document.createElementNS('http://www.w3.org/2000/svg','g');\n g.setAttribute('class','node');\n g.setAttribute('transform','translate('+(n.x-50)+','+(n.y-14)+')');\n const rect = document.createElementNS('http://www.w3.org/2000/svg','rect');\n rect.setAttribute('width','100'); rect.setAttribute('height','28');\n rect.setAttribute('fill', n.status ? (statusColors[n.status]||'#ecf0f1') : projectColors[n.project]);\n g.appendChild(rect);\n const text = document.createElementNS('http://www.w3.org/2000/svg','text');\n text.setAttribute('x','50'); text.setAttribute('y','18'); text.setAttribute('text-anchor','middle');\n text.textContent = n.id.length > 14 ? n.target : n.id;\n g.appendChild(text);\n g.addEventListener('click', () => {\n const deps = data.edges.filter(e => e.source === n.id).map(e => e.target);\n const rdeps = data.edges.filter(e => e.target === n.id).map(e => e.source);\n alert(n.id + '\\n\\nDepends on: ' + (deps.join(', ')||'none') + '\\nRequired by: ' + (rdeps.join(', ')||'none'));\n });\n svg.appendChild(g);\n});\n<\/script>\n</body>\n</html>"])), graphData.nodes.length, graphData.edges.length, graphData.roots.length, JSON.stringify(graphData).replaceAll("</", String.raw`<\/`));
|
|
156
|
+
return String.raw(_a || (_a = __template(['<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<title>Task Graph</title>\n<style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body { font-family: system-ui, -apple-system, sans-serif; background: #1a1a2e; color: #eee; }\n svg { width: 100vw; height: 100vh; }\n .node rect { rx: 6; ry: 6; stroke: #555; stroke-width: 1.5; cursor: pointer; }\n .node text { font-size: 11px; fill: #1a1a2e; font-weight: 600; pointer-events: none; }\n .node:hover rect { stroke: #fff; stroke-width: 2; }\n .edge { stroke: #444; stroke-width: 1.5; fill: none; marker-end: url(#arrow); }\n .label { font-size: 10px; fill: #888; }\n #info { position: fixed; top: 12px; right: 12px; background: #16213e; padding: 12px 16px; border-radius: 8px; font-size: 13px; }\n #info b { color: #e94560; }\n</style>\n</head>\n<body>\n<div id="info">\n <b>', "</b> tasks · <b>", "</b> dependencies · <b>", '</b> roots\n</div>\n<svg id="graph">\n <defs>\n <marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto">\n <path d="M 0 0 L 10 5 L 0 10 z" fill="#666"/>\n </marker>\n </defs>\n</svg>\n<script>\nconst data = ', ";\nconst svg = document.getElementById('graph');\nconst W = window.innerWidth, H = window.innerHeight;\nconst statusColors = {\n success: '#2ecc71', 'local-cache': '#3498db', 'remote-cache': '#9b59b6',\n failure: '#e74c3c', running: '#f39c12', skipped: '#95a5a6', pending: '#ecf0f1'\n};\nconst projectColors = {};\nconst palette = ['#e94560','#0f3460','#533483','#16c79a','#f39c12','#2ecc71','#3498db','#e67e22','#9b59b6','#1abc9c'];\nlet ci = 0;\ndata.nodes.forEach(n => {\n if (!projectColors[n.project]) projectColors[n.project] = palette[ci++ % palette.length];\n});\n\n// Simple force-directed layout\nconst nodes = data.nodes.map((n, i) => ({\n ...n, x: W/2 + (Math.random()-0.5)*400, y: H/2 + (Math.random()-0.5)*300, vx: 0, vy: 0\n}));\nconst nodeMap = new Map(nodes.map(n => [n.id, n]));\nconst edges = data.edges.map(e => ({ source: nodeMap.get(e.source), target: nodeMap.get(e.target) }));\n\nfunction simulate() {\n for (let iter = 0; iter < 300; iter++) {\n // Repulsion\n for (let i = 0; i < nodes.length; i++) {\n for (let j = i+1; j < nodes.length; j++) {\n let dx = nodes[j].x - nodes[i].x, dy = nodes[j].y - nodes[i].y;\n let d = Math.sqrt(dx*dx + dy*dy) || 1;\n let f = 8000 / (d * d);\n nodes[i].vx -= dx/d * f; nodes[i].vy -= dy/d * f;\n nodes[j].vx += dx/d * f; nodes[j].vy += dy/d * f;\n }\n }\n // Attraction (edges)\n edges.forEach(e => {\n if (!e.source || !e.target) {\n return;\n }\n let dx = e.target.x - e.source.x, dy = e.target.y - e.source.y;\n let d = Math.sqrt(dx*dx + dy*dy) || 1;\n let f = (d - 150) * 0.01;\n e.source.vx += dx/d * f; e.source.vy += dy/d * f;\n e.target.vx -= dx/d * f; e.target.vy -= dy/d * f;\n });\n // Gravity\n nodes.forEach(n => {\n n.vx += (W/2 - n.x) * 0.001;\n n.vy += (H/2 - n.y) * 0.001;\n n.x += n.vx * 0.3; n.y += n.vy * 0.3;\n n.vx *= 0.8; n.vy *= 0.8;\n n.x = Math.max(60, Math.min(W-60, n.x));\n n.y = Math.max(30, Math.min(H-30, n.y));\n });\n }\n}\nsimulate();\n\n// Render\nedges.forEach(e => {\n if (!e.source || !e.target) {\n return;\n }\n const line = document.createElementNS('http://www.w3.org/2000/svg','line');\n line.setAttribute('x1', e.source.x); line.setAttribute('y1', e.source.y);\n line.setAttribute('x2', e.target.x); line.setAttribute('y2', e.target.y);\n line.setAttribute('class','edge');\n svg.appendChild(line);\n});\nnodes.forEach(n => {\n const g = document.createElementNS('http://www.w3.org/2000/svg','g');\n g.setAttribute('class','node');\n g.setAttribute('transform','translate('+(n.x-50)+','+(n.y-14)+')');\n const rect = document.createElementNS('http://www.w3.org/2000/svg','rect');\n rect.setAttribute('width','100'); rect.setAttribute('height','28');\n rect.setAttribute('fill', n.status ? (statusColors[n.status]||'#ecf0f1') : projectColors[n.project]);\n g.appendChild(rect);\n const text = document.createElementNS('http://www.w3.org/2000/svg','text');\n text.setAttribute('x','50'); text.setAttribute('y','18'); text.setAttribute('text-anchor','middle');\n text.textContent = n.id.length > 14 ? n.target : n.id;\n g.appendChild(text);\n g.addEventListener('click', () => {\n const deps = data.edges.filter(e => e.source === n.id).map(e => e.target);\n const rdeps = data.edges.filter(e => e.target === n.id).map(e => e.source);\n alert(n.id + '\n\nDepends on: ' + (deps.join(', ')||'none') + '\nRequired by: ' + (rdeps.join(', ')||'none'));\n });\n svg.appendChild(g);\n});\n<\/script>\n</body>\n</html>"], ['<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<title>Task Graph</title>\n<style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body { font-family: system-ui, -apple-system, sans-serif; background: #1a1a2e; color: #eee; }\n svg { width: 100vw; height: 100vh; }\n .node rect { rx: 6; ry: 6; stroke: #555; stroke-width: 1.5; cursor: pointer; }\n .node text { font-size: 11px; fill: #1a1a2e; font-weight: 600; pointer-events: none; }\n .node:hover rect { stroke: #fff; stroke-width: 2; }\n .edge { stroke: #444; stroke-width: 1.5; fill: none; marker-end: url(#arrow); }\n .label { font-size: 10px; fill: #888; }\n #info { position: fixed; top: 12px; right: 12px; background: #16213e; padding: 12px 16px; border-radius: 8px; font-size: 13px; }\n #info b { color: #e94560; }\n</style>\n</head>\n<body>\n<div id="info">\n <b>', "</b> tasks · <b>", "</b> dependencies · <b>", '</b> roots\n</div>\n<svg id="graph">\n <defs>\n <marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto">\n <path d="M 0 0 L 10 5 L 0 10 z" fill="#666"/>\n </marker>\n </defs>\n</svg>\n<script>\nconst data = ', ";\nconst svg = document.getElementById('graph');\nconst W = window.innerWidth, H = window.innerHeight;\nconst statusColors = {\n success: '#2ecc71', 'local-cache': '#3498db', 'remote-cache': '#9b59b6',\n failure: '#e74c3c', running: '#f39c12', skipped: '#95a5a6', pending: '#ecf0f1'\n};\nconst projectColors = {};\nconst palette = ['#e94560','#0f3460','#533483','#16c79a','#f39c12','#2ecc71','#3498db','#e67e22','#9b59b6','#1abc9c'];\nlet ci = 0;\ndata.nodes.forEach(n => {\n if (!projectColors[n.project]) projectColors[n.project] = palette[ci++ % palette.length];\n});\n\n// Simple force-directed layout\nconst nodes = data.nodes.map((n, i) => ({\n ...n, x: W/2 + (Math.random()-0.5)*400, y: H/2 + (Math.random()-0.5)*300, vx: 0, vy: 0\n}));\nconst nodeMap = new Map(nodes.map(n => [n.id, n]));\nconst edges = data.edges.map(e => ({ source: nodeMap.get(e.source), target: nodeMap.get(e.target) }));\n\nfunction simulate() {\n for (let iter = 0; iter < 300; iter++) {\n // Repulsion\n for (let i = 0; i < nodes.length; i++) {\n for (let j = i+1; j < nodes.length; j++) {\n let dx = nodes[j].x - nodes[i].x, dy = nodes[j].y - nodes[i].y;\n let d = Math.sqrt(dx*dx + dy*dy) || 1;\n let f = 8000 / (d * d);\n nodes[i].vx -= dx/d * f; nodes[i].vy -= dy/d * f;\n nodes[j].vx += dx/d * f; nodes[j].vy += dy/d * f;\n }\n }\n // Attraction (edges)\n edges.forEach(e => {\n if (!e.source || !e.target) {\n return;\n }\n let dx = e.target.x - e.source.x, dy = e.target.y - e.source.y;\n let d = Math.sqrt(dx*dx + dy*dy) || 1;\n let f = (d - 150) * 0.01;\n e.source.vx += dx/d * f; e.source.vy += dy/d * f;\n e.target.vx -= dx/d * f; e.target.vy -= dy/d * f;\n });\n // Gravity\n nodes.forEach(n => {\n n.vx += (W/2 - n.x) * 0.001;\n n.vy += (H/2 - n.y) * 0.001;\n n.x += n.vx * 0.3; n.y += n.vy * 0.3;\n n.vx *= 0.8; n.vy *= 0.8;\n n.x = Math.max(60, Math.min(W-60, n.x));\n n.y = Math.max(30, Math.min(H-30, n.y));\n });\n }\n}\nsimulate();\n\n// Render\nedges.forEach(e => {\n if (!e.source || !e.target) {\n return;\n }\n const line = document.createElementNS('http://www.w3.org/2000/svg','line');\n line.setAttribute('x1', e.source.x); line.setAttribute('y1', e.source.y);\n line.setAttribute('x2', e.target.x); line.setAttribute('y2', e.target.y);\n line.setAttribute('class','edge');\n svg.appendChild(line);\n});\nnodes.forEach(n => {\n const g = document.createElementNS('http://www.w3.org/2000/svg','g');\n g.setAttribute('class','node');\n g.setAttribute('transform','translate('+(n.x-50)+','+(n.y-14)+')');\n const rect = document.createElementNS('http://www.w3.org/2000/svg','rect');\n rect.setAttribute('width','100'); rect.setAttribute('height','28');\n rect.setAttribute('fill', n.status ? (statusColors[n.status]||'#ecf0f1') : projectColors[n.project]);\n g.appendChild(rect);\n const text = document.createElementNS('http://www.w3.org/2000/svg','text');\n text.setAttribute('x','50'); text.setAttribute('y','18'); text.setAttribute('text-anchor','middle');\n text.textContent = n.id.length > 14 ? n.target : n.id;\n g.appendChild(text);\n g.addEventListener('click', () => {\n const deps = data.edges.filter(e => e.source === n.id).map(e => e.target);\n const rdeps = data.edges.filter(e => e.target === n.id).map(e => e.source);\n alert(n.id + '\\n\\nDepends on: ' + (deps.join(', ')||'none') + '\\nRequired by: ' + (rdeps.join(', ')||'none'));\n });\n svg.appendChild(g);\n});\n<\/script>\n</body>\n</html>"])), graphData.nodes.length, graphData.edges.length, graphData.roots.length, JSON.stringify(graphData).replaceAll("</", String.raw`<\/`));
|
|
157
157
|
};
|
|
158
158
|
const toGraphAscii = (taskGraph, options = {}) => {
|
|
159
159
|
const { taskStatuses } = options;
|
|
@@ -191,8 +191,25 @@ const projectGraphToDot = (projectGraph) => {
|
|
|
191
191
|
}
|
|
192
192
|
for (const [project, deps] of Object.entries(projectGraph.dependencies)) {
|
|
193
193
|
for (const dep of deps) {
|
|
194
|
-
const
|
|
195
|
-
|
|
194
|
+
const attributes = [];
|
|
195
|
+
switch (dep.type) {
|
|
196
|
+
case "devDependency": {
|
|
197
|
+
attributes.push("style=dotted", 'color="#888888"');
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
case "implicit": {
|
|
201
|
+
attributes.push("style=dashed");
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
case "peerDependency": {
|
|
205
|
+
attributes.push("style=dashed", 'color="#CC8800"');
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
default: {
|
|
209
|
+
attributes.push("style=solid");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
lines.push(` "${project}" -> "${dep.target}" [${attributes.join(", ")}];`);
|
|
196
213
|
}
|
|
197
214
|
}
|
|
198
215
|
lines.push("}");
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { createRequire as __cjs_createRequire } from "node:module";
|
|
2
|
+
|
|
3
|
+
const __cjs_require = __cjs_createRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
const __cjs_getProcess = typeof globalThis !== "undefined" && typeof globalThis.process !== "undefined" ? globalThis.process : process;
|
|
6
|
+
|
|
7
|
+
const __cjs_getBuiltinModule = (module) => {
|
|
8
|
+
// Check if we're in Node.js and version supports getBuiltinModule
|
|
9
|
+
if (typeof __cjs_getProcess !== "undefined" && __cjs_getProcess.versions && __cjs_getProcess.versions.node) {
|
|
10
|
+
const [major, minor] = __cjs_getProcess.versions.node.split(".").map(Number);
|
|
11
|
+
// Node.js 20.16.0+ and 22.3.0+
|
|
12
|
+
if (major > 22 || (major === 22 && minor >= 3) || (major === 20 && minor >= 16)) {
|
|
13
|
+
return __cjs_getProcess.getBuiltinModule(module);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
// Fallback to createRequire
|
|
17
|
+
return __cjs_require(module);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
spawn: spawn$1
|
|
22
|
+
} = __cjs_getBuiltinModule("node:child_process");
|
|
23
|
+
import { spawn } from '@lydell/node-pty';
|
|
24
|
+
|
|
25
|
+
const evaluateSuccess = (events, condition) => {
|
|
26
|
+
if (events.length === 0) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
const normalized = condition.trim().toLowerCase();
|
|
30
|
+
if (normalized === "first") {
|
|
31
|
+
return events[0].exitCode === 0;
|
|
32
|
+
}
|
|
33
|
+
if (normalized === "last") {
|
|
34
|
+
return events.at(-1).exitCode === 0;
|
|
35
|
+
}
|
|
36
|
+
const commandMatch = /^(!?)command-(.+)$/.exec(normalized);
|
|
37
|
+
if (commandMatch) {
|
|
38
|
+
const [, negated, target] = commandMatch;
|
|
39
|
+
const isNegated = negated === "!";
|
|
40
|
+
const matching = events.filter((e) => e.name === target || String(e.index) === target);
|
|
41
|
+
if (isNegated) {
|
|
42
|
+
return events.filter((e) => !matching.includes(e)).every((e) => e.exitCode === 0);
|
|
43
|
+
}
|
|
44
|
+
return matching.length > 0 && matching.every((e) => e.exitCode === 0);
|
|
45
|
+
}
|
|
46
|
+
return events.every((e) => e.exitCode === 0);
|
|
47
|
+
};
|
|
48
|
+
const killTree = (pid, signal) => {
|
|
49
|
+
try {
|
|
50
|
+
if (process.platform === "win32") {
|
|
51
|
+
spawn$1("taskkill", ["/F", "/T", "/PID", String(pid)], { stdio: "ignore" });
|
|
52
|
+
} else {
|
|
53
|
+
process.kill(-pid, signal);
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const spawnCommand = (index, config, shellPath, onEvent, onComplete) => {
|
|
59
|
+
const startTime = process.hrtime();
|
|
60
|
+
const useShell = config.shell !== false;
|
|
61
|
+
const stdinMode = config.stdin ?? "null";
|
|
62
|
+
const stdinStdio = stdinMode === "inherit" ? "inherit" : stdinMode === "pipe" ? "pipe" : "ignore";
|
|
63
|
+
let child;
|
|
64
|
+
if (useShell) {
|
|
65
|
+
let shellProgram;
|
|
66
|
+
let shellArgs;
|
|
67
|
+
let verbatimArgs = false;
|
|
68
|
+
if (shellPath) {
|
|
69
|
+
shellProgram = shellPath;
|
|
70
|
+
shellArgs = ["-c", config.command];
|
|
71
|
+
} else if (process.platform === "win32") {
|
|
72
|
+
shellProgram = "cmd.exe";
|
|
73
|
+
shellArgs = ["/s", "/c", `"${config.command}"`];
|
|
74
|
+
verbatimArgs = true;
|
|
75
|
+
} else {
|
|
76
|
+
shellProgram = "/bin/sh";
|
|
77
|
+
shellArgs = ["-c", config.command];
|
|
78
|
+
}
|
|
79
|
+
child = spawn$1(shellProgram, shellArgs, {
|
|
80
|
+
cwd: config.cwd,
|
|
81
|
+
detached: process.platform !== "win32",
|
|
82
|
+
env: {
|
|
83
|
+
...process.env,
|
|
84
|
+
...config.env,
|
|
85
|
+
FORCE_COLOR: process.env["FORCE_COLOR"] ?? "1"
|
|
86
|
+
},
|
|
87
|
+
stdio: [stdinStdio, "pipe", "pipe"],
|
|
88
|
+
windowsVerbatimArguments: verbatimArgs
|
|
89
|
+
});
|
|
90
|
+
} else {
|
|
91
|
+
const parts = config.command.split(/\s+/);
|
|
92
|
+
const program = parts[0];
|
|
93
|
+
const args = parts.slice(1);
|
|
94
|
+
child = spawn$1(program, args, {
|
|
95
|
+
cwd: config.cwd,
|
|
96
|
+
detached: process.platform !== "win32",
|
|
97
|
+
env: {
|
|
98
|
+
...process.env,
|
|
99
|
+
...config.env,
|
|
100
|
+
FORCE_COLOR: process.env["FORCE_COLOR"] ?? "1"
|
|
101
|
+
},
|
|
102
|
+
stdio: [stdinStdio, "pipe", "pipe"]
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
onEvent({
|
|
106
|
+
index,
|
|
107
|
+
kill: child.pid ? (signal) => {
|
|
108
|
+
killTree(child.pid, signal ?? "SIGTERM");
|
|
109
|
+
} : void 0,
|
|
110
|
+
kind: "started",
|
|
111
|
+
write: child.stdin ? (data) => child.stdin.write(data) : void 0
|
|
112
|
+
});
|
|
113
|
+
let stdoutBuffer = "";
|
|
114
|
+
let stdoutFlushTimer;
|
|
115
|
+
const flushStdoutBuffer = () => {
|
|
116
|
+
if (stdoutBuffer) {
|
|
117
|
+
onEvent({ index, kind: "stdout", text: stdoutBuffer });
|
|
118
|
+
stdoutBuffer = "";
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
child.stdout?.on("data", (data) => {
|
|
122
|
+
if (stdoutFlushTimer) {
|
|
123
|
+
clearTimeout(stdoutFlushTimer);
|
|
124
|
+
stdoutFlushTimer = void 0;
|
|
125
|
+
}
|
|
126
|
+
stdoutBuffer += data.toString();
|
|
127
|
+
const lines = stdoutBuffer.split("\n");
|
|
128
|
+
stdoutBuffer = lines.pop() ?? "";
|
|
129
|
+
for (const line of lines) {
|
|
130
|
+
onEvent({ index, kind: "stdout", text: line });
|
|
131
|
+
}
|
|
132
|
+
if (stdoutBuffer) {
|
|
133
|
+
stdoutFlushTimer = setTimeout(flushStdoutBuffer, 100);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
child.stdout?.on("end", () => {
|
|
137
|
+
if (stdoutFlushTimer) {
|
|
138
|
+
clearTimeout(stdoutFlushTimer);
|
|
139
|
+
stdoutFlushTimer = void 0;
|
|
140
|
+
}
|
|
141
|
+
flushStdoutBuffer();
|
|
142
|
+
});
|
|
143
|
+
let stderrBuffer = "";
|
|
144
|
+
let stderrFlushTimer;
|
|
145
|
+
const flushStderrBuffer = () => {
|
|
146
|
+
if (stderrBuffer) {
|
|
147
|
+
onEvent({ index, kind: "stderr", text: stderrBuffer });
|
|
148
|
+
stderrBuffer = "";
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
child.stderr?.on("data", (data) => {
|
|
152
|
+
if (stderrFlushTimer) {
|
|
153
|
+
clearTimeout(stderrFlushTimer);
|
|
154
|
+
stderrFlushTimer = void 0;
|
|
155
|
+
}
|
|
156
|
+
stderrBuffer += data.toString();
|
|
157
|
+
const lines = stderrBuffer.split("\n");
|
|
158
|
+
stderrBuffer = lines.pop() ?? "";
|
|
159
|
+
for (const line of lines) {
|
|
160
|
+
onEvent({ index, kind: "stderr", text: line });
|
|
161
|
+
}
|
|
162
|
+
if (stderrBuffer) {
|
|
163
|
+
stderrFlushTimer = setTimeout(flushStderrBuffer, 100);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
child.stderr?.on("end", () => {
|
|
167
|
+
if (stderrFlushTimer) {
|
|
168
|
+
clearTimeout(stderrFlushTimer);
|
|
169
|
+
stderrFlushTimer = void 0;
|
|
170
|
+
}
|
|
171
|
+
flushStderrBuffer();
|
|
172
|
+
});
|
|
173
|
+
child.on("error", (error) => {
|
|
174
|
+
onEvent({ index, kind: "error", message: error.message });
|
|
175
|
+
});
|
|
176
|
+
child.on("close", (code, signal) => {
|
|
177
|
+
if (stdoutFlushTimer) {
|
|
178
|
+
clearTimeout(stdoutFlushTimer);
|
|
179
|
+
stdoutFlushTimer = void 0;
|
|
180
|
+
}
|
|
181
|
+
if (stderrFlushTimer) {
|
|
182
|
+
clearTimeout(stderrFlushTimer);
|
|
183
|
+
stderrFlushTimer = void 0;
|
|
184
|
+
}
|
|
185
|
+
flushStdoutBuffer();
|
|
186
|
+
flushStderrBuffer();
|
|
187
|
+
const elapsed = process.hrtime(startTime);
|
|
188
|
+
const durationMs = elapsed[0] * 1e3 + elapsed[1] / 1e6;
|
|
189
|
+
const exitCode = code ?? (signal ? 1 : -1);
|
|
190
|
+
const closeEvent = {
|
|
191
|
+
command: config.command,
|
|
192
|
+
durationMs,
|
|
193
|
+
exitCode,
|
|
194
|
+
index,
|
|
195
|
+
killed: signal !== null,
|
|
196
|
+
name: config.name
|
|
197
|
+
};
|
|
198
|
+
onEvent({
|
|
199
|
+
commandName: config.name,
|
|
200
|
+
durationMs,
|
|
201
|
+
exitCode,
|
|
202
|
+
index,
|
|
203
|
+
killed: signal !== null,
|
|
204
|
+
kind: "close"
|
|
205
|
+
});
|
|
206
|
+
onComplete(closeEvent);
|
|
207
|
+
});
|
|
208
|
+
return { child, index, startTime };
|
|
209
|
+
};
|
|
210
|
+
const spawnCommandPty = (index, config, shellPath, onEvent, onComplete) => {
|
|
211
|
+
const startTime = process.hrtime();
|
|
212
|
+
let shellProgram;
|
|
213
|
+
let shellArgs;
|
|
214
|
+
if (shellPath) {
|
|
215
|
+
shellProgram = shellPath;
|
|
216
|
+
shellArgs = ["-c", config.command];
|
|
217
|
+
} else if (process.platform === "win32") {
|
|
218
|
+
shellProgram = "cmd.exe";
|
|
219
|
+
shellArgs = ["/s", "/c", config.command];
|
|
220
|
+
} else {
|
|
221
|
+
shellProgram = "/bin/sh";
|
|
222
|
+
shellArgs = ["-c", config.command];
|
|
223
|
+
}
|
|
224
|
+
const ptyInstance = spawn(shellProgram, shellArgs, {
|
|
225
|
+
cols: config.ptySize?.cols ?? 80,
|
|
226
|
+
cwd: config.cwd ?? process.cwd(),
|
|
227
|
+
env: Object.fromEntries(
|
|
228
|
+
Object.entries({ ...process.env, ...config.env, FORCE_COLOR: process.env["FORCE_COLOR"] ?? "1", TERM: "xterm-256color" }).filter(
|
|
229
|
+
(entry) => entry[1] !== void 0
|
|
230
|
+
)
|
|
231
|
+
),
|
|
232
|
+
name: "xterm-256color",
|
|
233
|
+
rows: config.ptySize?.rows ?? 24
|
|
234
|
+
});
|
|
235
|
+
onEvent({
|
|
236
|
+
index,
|
|
237
|
+
kill: (signal) => {
|
|
238
|
+
ptyInstance.kill(signal);
|
|
239
|
+
},
|
|
240
|
+
kind: "started",
|
|
241
|
+
resize: (cols, rows) => {
|
|
242
|
+
ptyInstance.resize(cols, rows);
|
|
243
|
+
},
|
|
244
|
+
write: (data) => {
|
|
245
|
+
ptyInstance.write(data);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
ptyInstance.onData((data) => {
|
|
249
|
+
onEvent({ index, kind: "stdout", text: data });
|
|
250
|
+
});
|
|
251
|
+
ptyInstance.onExit(({ exitCode, signal }) => {
|
|
252
|
+
const elapsed = process.hrtime(startTime);
|
|
253
|
+
const durationMs = elapsed[0] * 1e3 + elapsed[1] / 1e6;
|
|
254
|
+
const code = exitCode ?? (signal ? 1 : -1);
|
|
255
|
+
const closeEvent = {
|
|
256
|
+
command: config.command,
|
|
257
|
+
durationMs,
|
|
258
|
+
exitCode: code,
|
|
259
|
+
index,
|
|
260
|
+
killed: signal !== void 0 && signal !== 0,
|
|
261
|
+
name: config.name
|
|
262
|
+
};
|
|
263
|
+
onEvent({
|
|
264
|
+
commandName: config.name,
|
|
265
|
+
durationMs,
|
|
266
|
+
exitCode: code,
|
|
267
|
+
index,
|
|
268
|
+
killed: signal !== void 0 && signal !== 0,
|
|
269
|
+
kind: "close"
|
|
270
|
+
});
|
|
271
|
+
onComplete(closeEvent);
|
|
272
|
+
});
|
|
273
|
+
return { index, pty: ptyInstance, startTime };
|
|
274
|
+
};
|
|
275
|
+
const runConcurrentFallback = (commands, options) => new Promise((resolve) => {
|
|
276
|
+
if (commands.length === 0) {
|
|
277
|
+
resolve({ closeEvents: [], success: true });
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const maxProcesses = options.maxProcesses && options.maxProcesses > 0 ? options.maxProcesses : commands.length;
|
|
281
|
+
const killSignal = options.killSignal ?? "SIGTERM";
|
|
282
|
+
const killOthers = options.killOthers ?? [];
|
|
283
|
+
const successCondition = options.successCondition ?? "all";
|
|
284
|
+
const onEvent = options.onEvent ?? (() => {
|
|
285
|
+
});
|
|
286
|
+
const active = [];
|
|
287
|
+
const closeEvents = [];
|
|
288
|
+
const pending = commands.map((_, i) => i);
|
|
289
|
+
let aborting = false;
|
|
290
|
+
let sigintAbort = false;
|
|
291
|
+
const killAll = () => {
|
|
292
|
+
for (const proc of active) {
|
|
293
|
+
if (proc.pty) {
|
|
294
|
+
proc.pty.kill(killSignal);
|
|
295
|
+
} else if (proc.child?.pid) {
|
|
296
|
+
killTree(proc.child.pid, killSignal);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
const shouldKillOthers = (event) => {
|
|
301
|
+
for (const condition of killOthers) {
|
|
302
|
+
if (condition === "failure" && event.exitCode !== 0) {
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
if (condition === "success" && event.exitCode === 0) {
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return false;
|
|
310
|
+
};
|
|
311
|
+
const handleClose = (cmdIndex, closeEvent) => {
|
|
312
|
+
const activeIndex = active.findIndex((p) => p.index === cmdIndex);
|
|
313
|
+
if (activeIndex !== -1) {
|
|
314
|
+
active.splice(activeIndex, 1);
|
|
315
|
+
}
|
|
316
|
+
if (aborting) {
|
|
317
|
+
closeEvent.killed = true;
|
|
318
|
+
if (sigintAbort) {
|
|
319
|
+
closeEvent.exitCode = 0;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
closeEvents.push(closeEvent);
|
|
323
|
+
if (!aborting && shouldKillOthers(closeEvent)) {
|
|
324
|
+
aborting = true;
|
|
325
|
+
killAll();
|
|
326
|
+
}
|
|
327
|
+
if (!aborting) {
|
|
328
|
+
maybeSpawnMore();
|
|
329
|
+
}
|
|
330
|
+
if (active.length === 0 && pending.length === 0) {
|
|
331
|
+
const success = evaluateSuccess(closeEvents, successCondition);
|
|
332
|
+
resolve({ closeEvents, success });
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
const maybeSpawnMore = () => {
|
|
336
|
+
while (active.length < maxProcesses && pending.length > 0) {
|
|
337
|
+
const cmdIndex = pending.shift();
|
|
338
|
+
const config = commands[cmdIndex];
|
|
339
|
+
const proc = config.stdin === "pty" ? spawnCommandPty(cmdIndex, config, options.shellPath, onEvent, (ce) => {
|
|
340
|
+
handleClose(cmdIndex, ce);
|
|
341
|
+
}) : spawnCommand(cmdIndex, config, options.shellPath, onEvent, (ce) => {
|
|
342
|
+
handleClose(cmdIndex, ce);
|
|
343
|
+
});
|
|
344
|
+
active.push(proc);
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
const handleSigint = () => {
|
|
348
|
+
if (!aborting) {
|
|
349
|
+
aborting = true;
|
|
350
|
+
sigintAbort = true;
|
|
351
|
+
killAll();
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
const handleSigterm = () => {
|
|
355
|
+
if (!aborting) {
|
|
356
|
+
aborting = true;
|
|
357
|
+
killAll();
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
process.on("SIGINT", handleSigint);
|
|
361
|
+
process.on("SIGTERM", handleSigterm);
|
|
362
|
+
const originalResolve = resolve;
|
|
363
|
+
resolve = ((result) => {
|
|
364
|
+
process.removeListener("SIGINT", handleSigint);
|
|
365
|
+
process.removeListener("SIGTERM", handleSigterm);
|
|
366
|
+
originalResolve(result);
|
|
367
|
+
});
|
|
368
|
+
maybeSpawnMore();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
export { runConcurrentFallback };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { runConcurrentFallback } from './runConcurrentFallback-CGHz_f-Q.js';
|
|
2
|
+
import { detectScriptShell } from './detectScriptShell-CR-xXKA4.js';
|
|
3
|
+
import { logTimings } from './formatTimingTable-3qtCM552.js';
|
|
4
|
+
import { withRestart } from './withRestart-BREjRJa4.js';
|
|
5
|
+
import { runTeardown } from './runTeardown-BAezH79J.js';
|
|
6
|
+
import { loadNativeBindings } from './isNativeAvailable-BpD28A6Z.js';
|
|
7
|
+
|
|
8
|
+
const normalizeCommands = (inputs) => inputs.map((input) => {
|
|
9
|
+
if (typeof input === "string") {
|
|
10
|
+
return { command: input };
|
|
11
|
+
}
|
|
12
|
+
return input;
|
|
13
|
+
});
|
|
14
|
+
const coreRun = async (configs, options) => {
|
|
15
|
+
const shellPath = options.shellPath ?? detectScriptShell();
|
|
16
|
+
const native = loadNativeBindings();
|
|
17
|
+
const needsJsFallback = configs.some((c) => c.stdin === "pipe" || c.stdin === "pty") || !!options.onEvent;
|
|
18
|
+
if (native && !needsJsFallback) {
|
|
19
|
+
const nativeOptions = {
|
|
20
|
+
killOthers: options.killOthers,
|
|
21
|
+
killSignal: options.killSignal,
|
|
22
|
+
killTimeout: options.killTimeout,
|
|
23
|
+
maxProcesses: options.maxProcesses,
|
|
24
|
+
shellPath,
|
|
25
|
+
successCondition: options.successCondition
|
|
26
|
+
};
|
|
27
|
+
const nativeCommands = configs.map((c) => {
|
|
28
|
+
return {
|
|
29
|
+
command: c.command,
|
|
30
|
+
cwd: c.cwd,
|
|
31
|
+
env: c.env,
|
|
32
|
+
name: c.name,
|
|
33
|
+
shell: c.shell,
|
|
34
|
+
stdin: c.stdin
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
return native.runConcurrentBatch(nativeCommands, nativeOptions);
|
|
38
|
+
}
|
|
39
|
+
return runConcurrentFallback(configs, { ...options, shellPath });
|
|
40
|
+
};
|
|
41
|
+
const runConcurrently = async (commands, options = {}) => {
|
|
42
|
+
const configs = normalizeCommands(commands);
|
|
43
|
+
if (configs.length === 0) {
|
|
44
|
+
return { closeEvents: [], success: true };
|
|
45
|
+
}
|
|
46
|
+
let result;
|
|
47
|
+
if (options.restart && options.restart.tries !== 0) {
|
|
48
|
+
result = await withRestart((cmds, options_) => coreRun(cmds, options_), configs, options, {
|
|
49
|
+
delay: options.restart.delay ?? 0,
|
|
50
|
+
tries: options.restart.tries
|
|
51
|
+
});
|
|
52
|
+
} else {
|
|
53
|
+
result = await coreRun(configs, options);
|
|
54
|
+
}
|
|
55
|
+
if (options.timings) {
|
|
56
|
+
logTimings(result.closeEvents);
|
|
57
|
+
}
|
|
58
|
+
if (options.teardown && options.teardown.length > 0) {
|
|
59
|
+
await runTeardown({
|
|
60
|
+
commands: options.teardown,
|
|
61
|
+
cwd: options.teardownCwd
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export { runConcurrently };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createRequire as __cjs_createRequire } from "node:module";
|
|
2
|
+
|
|
3
|
+
const __cjs_require = __cjs_createRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
const __cjs_getProcess = typeof globalThis !== "undefined" && typeof globalThis.process !== "undefined" ? globalThis.process : process;
|
|
6
|
+
|
|
7
|
+
const __cjs_getBuiltinModule = (module) => {
|
|
8
|
+
// Check if we're in Node.js and version supports getBuiltinModule
|
|
9
|
+
if (typeof __cjs_getProcess !== "undefined" && __cjs_getProcess.versions && __cjs_getProcess.versions.node) {
|
|
10
|
+
const [major, minor] = __cjs_getProcess.versions.node.split(".").map(Number);
|
|
11
|
+
// Node.js 20.16.0+ and 22.3.0+
|
|
12
|
+
if (major > 22 || (major === 22 && minor >= 3) || (major === 20 && minor >= 16)) {
|
|
13
|
+
return __cjs_getProcess.getBuiltinModule(module);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
// Fallback to createRequire
|
|
17
|
+
return __cjs_require(module);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
spawn
|
|
22
|
+
} = __cjs_getBuiltinModule("node:child_process");
|
|
23
|
+
|
|
24
|
+
const runTeardown = async (options) => {
|
|
25
|
+
const { commands, cwd } = options;
|
|
26
|
+
const results = [];
|
|
27
|
+
for (const command of commands) {
|
|
28
|
+
const code = await runTeardownCommand(command, cwd);
|
|
29
|
+
results.push(code);
|
|
30
|
+
}
|
|
31
|
+
return results;
|
|
32
|
+
};
|
|
33
|
+
const runTeardownCommand = (command, cwd) => new Promise((resolve) => {
|
|
34
|
+
const shellProgram = process.platform === "win32" ? "cmd.exe" : "/bin/sh";
|
|
35
|
+
const shellArgs = process.platform === "win32" ? ["/s", "/c", `"${command}"`] : ["-c", command];
|
|
36
|
+
const child = spawn(shellProgram, shellArgs, {
|
|
37
|
+
cwd,
|
|
38
|
+
stdio: "inherit",
|
|
39
|
+
windowsVerbatimArguments: process.platform === "win32"
|
|
40
|
+
});
|
|
41
|
+
child.on("close", (code) => {
|
|
42
|
+
resolve(code ?? 1);
|
|
43
|
+
});
|
|
44
|
+
child.on("error", () => {
|
|
45
|
+
resolve(1);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export { runTeardown };
|