@livingdata/pipex 0.0.9 → 0.0.10

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.
Files changed (88) hide show
  1. package/README.md +154 -14
  2. package/dist/__tests__/errors.js +162 -0
  3. package/dist/__tests__/helpers.js +41 -0
  4. package/dist/__tests__/types.js +8 -0
  5. package/dist/cli/__tests__/condition.js +23 -0
  6. package/dist/cli/__tests__/dag.js +154 -0
  7. package/dist/cli/__tests__/pipeline-loader.js +267 -0
  8. package/dist/cli/__tests__/pipeline-runner.js +257 -0
  9. package/dist/cli/__tests__/state-persistence.js +80 -0
  10. package/dist/cli/__tests__/state.js +58 -0
  11. package/dist/cli/__tests__/step-runner.js +116 -0
  12. package/dist/cli/commands/bundle.js +35 -0
  13. package/dist/cli/commands/cat.js +54 -0
  14. package/dist/cli/commands/exec.js +89 -0
  15. package/dist/cli/commands/export.js +2 -2
  16. package/dist/cli/commands/inspect.js +1 -1
  17. package/dist/cli/commands/list.js +2 -1
  18. package/dist/cli/commands/logs.js +1 -1
  19. package/dist/cli/commands/prune.js +1 -1
  20. package/dist/cli/commands/rm-step.js +41 -0
  21. package/dist/cli/commands/run-bundle.js +59 -0
  22. package/dist/cli/commands/run.js +9 -4
  23. package/dist/cli/commands/show.js +42 -7
  24. package/dist/cli/condition.js +11 -0
  25. package/dist/cli/dag.js +143 -0
  26. package/dist/cli/index.js +6 -0
  27. package/dist/cli/interactive-reporter.js +227 -0
  28. package/dist/cli/pipeline-loader.js +10 -110
  29. package/dist/cli/pipeline-runner.js +164 -78
  30. package/dist/cli/reporter.js +2 -158
  31. package/dist/cli/state.js +8 -0
  32. package/dist/cli/step-loader.js +25 -0
  33. package/dist/cli/step-resolver.js +111 -0
  34. package/dist/cli/step-runner.js +226 -0
  35. package/dist/cli/utils.js +0 -46
  36. package/dist/core/__tests__/bundle.js +663 -0
  37. package/dist/core/__tests__/condition.js +23 -0
  38. package/dist/core/__tests__/dag.js +154 -0
  39. package/dist/core/__tests__/env-file.test.js +41 -0
  40. package/dist/core/__tests__/event-aggregator.js +244 -0
  41. package/dist/core/__tests__/pipeline-loader.js +267 -0
  42. package/dist/core/__tests__/pipeline-runner.js +257 -0
  43. package/dist/core/__tests__/state-persistence.js +80 -0
  44. package/dist/core/__tests__/state.js +58 -0
  45. package/dist/core/__tests__/step-runner.js +118 -0
  46. package/dist/core/__tests__/stream-reporter.js +142 -0
  47. package/dist/core/__tests__/transport.js +50 -0
  48. package/dist/core/__tests__/utils.js +40 -0
  49. package/dist/core/bundle.js +130 -0
  50. package/dist/core/condition.js +11 -0
  51. package/dist/core/dag.js +143 -0
  52. package/dist/core/env-file.js +6 -0
  53. package/dist/core/event-aggregator.js +114 -0
  54. package/dist/core/index.js +14 -0
  55. package/dist/core/pipeline-loader.js +81 -0
  56. package/dist/core/pipeline-runner.js +360 -0
  57. package/dist/core/reporter.js +11 -0
  58. package/dist/core/state.js +110 -0
  59. package/dist/core/step-loader.js +25 -0
  60. package/dist/core/step-resolver.js +117 -0
  61. package/dist/core/step-runner.js +225 -0
  62. package/dist/core/stream-reporter.js +41 -0
  63. package/dist/core/transport.js +9 -0
  64. package/dist/core/utils.js +56 -0
  65. package/dist/engine/__tests__/workspace.js +288 -0
  66. package/dist/engine/docker-executor.js +10 -2
  67. package/dist/engine/index.js +1 -0
  68. package/dist/engine/workspace.js +76 -12
  69. package/dist/errors.js +122 -0
  70. package/dist/index.js +3 -0
  71. package/dist/kits/__tests__/index.js +23 -0
  72. package/dist/kits/builtin/__tests__/node.js +74 -0
  73. package/dist/kits/builtin/__tests__/python.js +67 -0
  74. package/dist/kits/builtin/__tests__/shell.js +74 -0
  75. package/dist/kits/builtin/node.js +10 -5
  76. package/dist/kits/builtin/python.js +10 -5
  77. package/dist/kits/builtin/shell.js +2 -1
  78. package/dist/kits/index.js +2 -1
  79. package/package.json +6 -3
  80. package/dist/cli/types.js +0 -3
  81. package/dist/engine/docker-runtime.js +0 -65
  82. package/dist/engine/runtime.js +0 -2
  83. package/dist/kits/bash.js +0 -19
  84. package/dist/kits/builtin/bash.js +0 -19
  85. package/dist/kits/node.js +0 -56
  86. package/dist/kits/python.js +0 -51
  87. package/dist/kits/types.js +0 -1
  88. package/dist/reporter.js +0 -13
@@ -0,0 +1,226 @@
1
+ import process from 'node:process';
2
+ import { cp, writeFile } from 'node:fs/promises';
3
+ import { setTimeout } from 'node:timers/promises';
4
+ import { createWriteStream } from 'node:fs';
5
+ import { join, resolve } from 'node:path';
6
+ import { ContainerCrashError, PipexError } from '../errors.js';
7
+ import { StateManager } from './state.js';
8
+ import { dirSize } from './utils.js';
9
+ /**
10
+ * Executes a single step in a workspace.
11
+ * Adapted from PipelineRunner.executeStep() for standalone use.
12
+ */
13
+ export class StepRunner {
14
+ runtime;
15
+ reporter;
16
+ constructor(runtime, reporter) {
17
+ this.runtime = runtime;
18
+ this.reporter = reporter;
19
+ }
20
+ async run(options) {
21
+ const { workspace, state, step, inputs, pipelineRoot, force, ephemeral } = options;
22
+ const stepRef = { id: step.id, displayName: step.name ?? step.id };
23
+ const resolvedMounts = step.mounts?.map(m => ({
24
+ hostPath: resolve(pipelineRoot, m.host),
25
+ containerPath: m.container
26
+ }));
27
+ const currentFingerprint = this.computeFingerprint(step, inputs, resolvedMounts);
28
+ // Cache check (skip for ephemeral or force)
29
+ if (!force && !ephemeral) {
30
+ const cacheResult = await this.tryUseCache({ workspace, state, stepId: step.id, stepRef, fingerprint: currentFingerprint });
31
+ if (cacheResult) {
32
+ return cacheResult;
33
+ }
34
+ }
35
+ this.reporter.emit({ event: 'STEP_STARTING', workspaceId: workspace.id, step: stepRef });
36
+ return this.executeStep({ workspace, state, step, stepRef, inputs, pipelineRoot, ephemeral, currentFingerprint, resolvedMounts });
37
+ }
38
+ computeFingerprint(step, inputs, resolvedMounts) {
39
+ const inputRunIds = step.inputs
40
+ ?.map(i => inputs.get(i.step))
41
+ .filter((id) => id !== undefined);
42
+ return StateManager.fingerprint({
43
+ image: step.image,
44
+ cmd: step.cmd,
45
+ env: step.env,
46
+ inputRunIds,
47
+ mounts: resolvedMounts
48
+ });
49
+ }
50
+ async tryUseCache({ workspace, state, stepId, stepRef, fingerprint }) {
51
+ const cached = state.getStep(stepId);
52
+ if (cached?.fingerprint === fingerprint) {
53
+ const runs = await workspace.listRuns();
54
+ if (runs.includes(cached.runId)) {
55
+ await workspace.linkRun(stepId, cached.runId);
56
+ this.reporter.emit({ event: 'STEP_SKIPPED', workspaceId: workspace.id, step: stepRef, runId: cached.runId, reason: 'cached' });
57
+ return { runId: cached.runId, exitCode: 0 };
58
+ }
59
+ }
60
+ return undefined;
61
+ }
62
+ async executeStep(ctx) {
63
+ const { workspace, step, stepRef, inputs, pipelineRoot, ephemeral } = ctx;
64
+ const runId = workspace.generateRunId();
65
+ const stagingPath = await workspace.prepareRun(runId);
66
+ await this.prepareStagingInputs(workspace, step, runId, inputs);
67
+ await this.prepareCaches(workspace, step);
68
+ const { containerInputs, output, caches, mounts } = this.buildMounts(step, runId, inputs, pipelineRoot);
69
+ const stdoutLog = createWriteStream(join(stagingPath, 'stdout.log'));
70
+ const stderrLog = createWriteStream(join(stagingPath, 'stderr.log'));
71
+ try {
72
+ const result = await this.executeWithRetries({
73
+ ctx, containerInputs, output, caches, mounts, stdoutLog, stderrLog
74
+ });
75
+ this.reporter.result(workspace.id, stepRef, result);
76
+ await closeStream(stdoutLog);
77
+ await closeStream(stderrLog);
78
+ if (ephemeral) {
79
+ await workspace.discardRun(runId);
80
+ this.reporter.emit({ event: 'STEP_FINISHED', workspaceId: workspace.id, step: stepRef, ephemeral: true });
81
+ return { exitCode: result.exitCode };
82
+ }
83
+ return await this.commitOrDiscard({ ...ctx, runId, stagingPath, result });
84
+ }
85
+ catch (error) {
86
+ await closeStream(stdoutLog);
87
+ await closeStream(stderrLog);
88
+ throw error;
89
+ }
90
+ }
91
+ async prepareStagingInputs(workspace, step, runId, inputs) {
92
+ if (!step.inputs) {
93
+ return;
94
+ }
95
+ for (const input of step.inputs) {
96
+ const inputRunId = inputs.get(input.step);
97
+ if (inputRunId && input.copyToOutput) {
98
+ await cp(workspace.runArtifactsPath(inputRunId), workspace.runStagingArtifactsPath(runId), { recursive: true });
99
+ }
100
+ }
101
+ }
102
+ async prepareCaches(workspace, step) {
103
+ if (!step.caches) {
104
+ return;
105
+ }
106
+ for (const cache of step.caches) {
107
+ await workspace.prepareCache(cache.name);
108
+ }
109
+ }
110
+ buildMounts(step, runId, inputs, pipelineRoot) {
111
+ const containerInputs = [];
112
+ if (step.inputs) {
113
+ for (const input of step.inputs) {
114
+ const inputRunId = inputs.get(input.step);
115
+ if (inputRunId) {
116
+ containerInputs.push({ runId: inputRunId, containerPath: `/input/${input.step}` });
117
+ }
118
+ }
119
+ }
120
+ const output = { stagingRunId: runId, containerPath: step.outputPath ?? '/output' };
121
+ const caches = step.caches?.map(c => ({ name: c.name, containerPath: c.path }));
122
+ const mounts = step.mounts?.map(m => ({
123
+ hostPath: resolve(pipelineRoot, m.host),
124
+ containerPath: m.container
125
+ }));
126
+ return { containerInputs, output, caches, mounts };
127
+ }
128
+ async executeWithRetries({ ctx, containerInputs, output, caches, mounts, stdoutLog, stderrLog }) {
129
+ const { workspace, step, stepRef, pipelineRoot, ephemeral } = ctx;
130
+ const maxRetries = step.retries ?? 0;
131
+ const retryDelay = step.retryDelayMs ?? 5000;
132
+ let result;
133
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
134
+ try {
135
+ result = await this.runtime.run(workspace, {
136
+ name: `pipex-${workspace.id}-${step.id}-${Date.now()}`,
137
+ image: step.image,
138
+ cmd: step.cmd,
139
+ env: step.env,
140
+ inputs: containerInputs,
141
+ output,
142
+ caches,
143
+ mounts,
144
+ sources: step.sources?.map(m => ({
145
+ hostPath: resolve(pipelineRoot, m.host),
146
+ containerPath: m.container
147
+ })),
148
+ network: step.allowNetwork ? 'bridge' : 'none',
149
+ timeoutSec: step.timeoutSec
150
+ }, ({ stream, line }) => {
151
+ if (stream === 'stdout') {
152
+ stdoutLog.write(line + '\n');
153
+ }
154
+ else {
155
+ stderrLog.write(line + '\n');
156
+ }
157
+ if (ephemeral && stream === 'stdout') {
158
+ process.stdout.write(line + '\n');
159
+ }
160
+ else {
161
+ this.reporter.log(workspace.id, stepRef, stream, line);
162
+ }
163
+ });
164
+ break;
165
+ }
166
+ catch (error) {
167
+ if (error instanceof PipexError && error.transient && attempt < maxRetries) {
168
+ this.reporter.emit({ event: 'STEP_RETRYING', workspaceId: workspace.id, step: stepRef, attempt: attempt + 1, maxRetries });
169
+ await setTimeout(retryDelay);
170
+ continue;
171
+ }
172
+ throw error;
173
+ }
174
+ }
175
+ return result;
176
+ }
177
+ async commitOrDiscard({ workspace, state, step, stepRef, inputs, resolvedMounts, currentFingerprint, runId, stagingPath, result }) {
178
+ const meta = {
179
+ runId,
180
+ stepId: step.id,
181
+ stepName: step.name,
182
+ startedAt: result.startedAt.toISOString(),
183
+ finishedAt: result.finishedAt.toISOString(),
184
+ durationMs: result.finishedAt.getTime() - result.startedAt.getTime(),
185
+ exitCode: result.exitCode,
186
+ image: step.image,
187
+ cmd: step.cmd,
188
+ env: step.env,
189
+ inputs: step.inputs?.map(i => ({
190
+ step: i.step,
191
+ runId: inputs.get(i.step),
192
+ mountedAs: `/input/${i.step}`
193
+ })),
194
+ mounts: resolvedMounts,
195
+ caches: step.caches?.map(c => c.name),
196
+ allowNetwork: step.allowNetwork ?? false,
197
+ fingerprint: currentFingerprint,
198
+ status: result.exitCode === 0 ? 'success' : 'failure'
199
+ };
200
+ await writeFile(join(stagingPath, 'meta.json'), JSON.stringify(meta, null, 2), 'utf8');
201
+ if (result.exitCode === 0 || step.allowFailure) {
202
+ await workspace.commitRun(runId);
203
+ await workspace.linkRun(step.id, runId);
204
+ state.setStep(step.id, runId, currentFingerprint);
205
+ await state.save();
206
+ const durationMs = result.finishedAt.getTime() - result.startedAt.getTime();
207
+ const artifactSize = await dirSize(workspace.runArtifactsPath(runId));
208
+ this.reporter.emit({ event: 'STEP_FINISHED', workspaceId: workspace.id, step: stepRef, runId, durationMs, artifactSize });
209
+ return { runId, exitCode: result.exitCode };
210
+ }
211
+ await workspace.discardRun(runId);
212
+ this.reporter.emit({ event: 'STEP_FAILED', workspaceId: workspace.id, step: stepRef, exitCode: result.exitCode });
213
+ throw new ContainerCrashError(step.id, result.exitCode);
214
+ }
215
+ }
216
+ async function closeStream(stream) {
217
+ if (stream.destroyed) {
218
+ return;
219
+ }
220
+ return new Promise((resolve, reject) => {
221
+ stream.end(() => {
222
+ resolve();
223
+ });
224
+ stream.on('error', reject);
225
+ });
226
+ }
package/dist/cli/utils.js CHANGED
@@ -1,49 +1,3 @@
1
- import { readdir, stat } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
1
  export function getGlobalOptions(cmd) {
4
2
  return cmd.optsWithGlobals();
5
3
  }
6
- export async function dirSize(dirPath) {
7
- let total = 0;
8
- try {
9
- const entries = await readdir(dirPath, { withFileTypes: true });
10
- for (const entry of entries) {
11
- const fullPath = join(dirPath, entry.name);
12
- if (entry.isDirectory()) {
13
- total += await dirSize(fullPath);
14
- }
15
- else if (entry.isFile()) {
16
- const s = await stat(fullPath);
17
- total += s.size;
18
- }
19
- }
20
- }
21
- catch {
22
- // Directory doesn't exist or isn't readable
23
- }
24
- return total;
25
- }
26
- export function formatSize(bytes) {
27
- if (bytes < 1024) {
28
- return `${bytes} B`;
29
- }
30
- if (bytes < 1024 * 1024) {
31
- return `${(bytes / 1024).toFixed(1)} KB`;
32
- }
33
- if (bytes < 1024 * 1024 * 1024) {
34
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
35
- }
36
- return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
37
- }
38
- export function formatDuration(ms) {
39
- if (ms < 1000) {
40
- return `${ms}ms`;
41
- }
42
- const seconds = ms / 1000;
43
- if (seconds < 60) {
44
- return `${seconds.toFixed(1)}s`;
45
- }
46
- const minutes = Math.floor(seconds / 60);
47
- const remainingSeconds = Math.round(seconds % 60);
48
- return `${minutes}m ${remainingSeconds}s`;
49
- }