@livingdata/pipex 0.0.8 → 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 (90) hide show
  1. package/README.md +186 -16
  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/clean.js +22 -0
  15. package/dist/cli/commands/exec.js +89 -0
  16. package/dist/cli/commands/export.js +32 -0
  17. package/dist/cli/commands/inspect.js +58 -0
  18. package/dist/cli/commands/list.js +39 -0
  19. package/dist/cli/commands/logs.js +54 -0
  20. package/dist/cli/commands/prune.js +26 -0
  21. package/dist/cli/commands/rm-step.js +41 -0
  22. package/dist/cli/commands/rm.js +27 -0
  23. package/dist/cli/commands/run-bundle.js +59 -0
  24. package/dist/cli/commands/run.js +44 -0
  25. package/dist/cli/commands/show.js +108 -0
  26. package/dist/cli/condition.js +11 -0
  27. package/dist/cli/dag.js +143 -0
  28. package/dist/cli/index.js +24 -105
  29. package/dist/cli/interactive-reporter.js +227 -0
  30. package/dist/cli/pipeline-loader.js +10 -110
  31. package/dist/cli/pipeline-runner.js +256 -111
  32. package/dist/cli/reporter.js +2 -107
  33. package/dist/cli/state.js +30 -9
  34. package/dist/cli/step-loader.js +25 -0
  35. package/dist/cli/step-resolver.js +111 -0
  36. package/dist/cli/step-runner.js +226 -0
  37. package/dist/cli/utils.js +3 -0
  38. package/dist/core/__tests__/bundle.js +663 -0
  39. package/dist/core/__tests__/condition.js +23 -0
  40. package/dist/core/__tests__/dag.js +154 -0
  41. package/dist/core/__tests__/env-file.test.js +41 -0
  42. package/dist/core/__tests__/event-aggregator.js +244 -0
  43. package/dist/core/__tests__/pipeline-loader.js +267 -0
  44. package/dist/core/__tests__/pipeline-runner.js +257 -0
  45. package/dist/core/__tests__/state-persistence.js +80 -0
  46. package/dist/core/__tests__/state.js +58 -0
  47. package/dist/core/__tests__/step-runner.js +118 -0
  48. package/dist/core/__tests__/stream-reporter.js +142 -0
  49. package/dist/core/__tests__/transport.js +50 -0
  50. package/dist/core/__tests__/utils.js +40 -0
  51. package/dist/core/bundle.js +130 -0
  52. package/dist/core/condition.js +11 -0
  53. package/dist/core/dag.js +143 -0
  54. package/dist/core/env-file.js +6 -0
  55. package/dist/core/event-aggregator.js +114 -0
  56. package/dist/core/index.js +14 -0
  57. package/dist/core/pipeline-loader.js +81 -0
  58. package/dist/core/pipeline-runner.js +360 -0
  59. package/dist/core/reporter.js +11 -0
  60. package/dist/core/state.js +110 -0
  61. package/dist/core/step-loader.js +25 -0
  62. package/dist/core/step-resolver.js +117 -0
  63. package/dist/core/step-runner.js +225 -0
  64. package/dist/core/stream-reporter.js +41 -0
  65. package/dist/core/transport.js +9 -0
  66. package/dist/core/utils.js +56 -0
  67. package/dist/engine/__tests__/workspace.js +288 -0
  68. package/dist/engine/docker-executor.js +32 -6
  69. package/dist/engine/index.js +1 -0
  70. package/dist/engine/workspace.js +164 -66
  71. package/dist/errors.js +122 -0
  72. package/dist/index.js +3 -0
  73. package/dist/kits/__tests__/index.js +23 -0
  74. package/dist/kits/builtin/__tests__/node.js +74 -0
  75. package/dist/kits/builtin/__tests__/python.js +67 -0
  76. package/dist/kits/builtin/__tests__/shell.js +74 -0
  77. package/dist/kits/builtin/node.js +10 -5
  78. package/dist/kits/builtin/python.js +10 -5
  79. package/dist/kits/builtin/shell.js +2 -1
  80. package/dist/kits/index.js +2 -1
  81. package/package.json +6 -3
  82. package/dist/cli/types.js +0 -3
  83. package/dist/engine/docker-runtime.js +0 -65
  84. package/dist/engine/runtime.js +0 -2
  85. package/dist/kits/bash.js +0 -19
  86. package/dist/kits/builtin/bash.js +0 -19
  87. package/dist/kits/node.js +0 -56
  88. package/dist/kits/python.js +0 -51
  89. package/dist/kits/types.js +0 -1
  90. package/dist/reporter.js +0 -13
@@ -1,23 +1,28 @@
1
- import { access, mkdir, readdir, rename, rm, symlink } from 'node:fs/promises';
1
+ import { access, mkdir, readdir, readFile, rename, rm, symlink, writeFile } from 'node:fs/promises';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { join } from 'node:path';
4
+ import { WorkspaceError, StagingError } from '../errors.js';
4
5
  /**
5
6
  * Isolated execution environment for container runs.
6
7
  *
7
8
  * A workspace provides:
8
9
  * - **staging/**: Temporary write location during execution
9
- * - **artifacts/**: Committed outputs (immutable, read-only)
10
+ * - **runs/**: Committed run outputs (immutable, read-only)
10
11
  * - **caches/**: Persistent read-write caches (shared across steps)
11
12
  * - **state.json**: Managed by orchestration layer (e.g., CLI) for caching
12
13
  *
13
- * ## Artifact Lifecycle
14
+ * ## Run Lifecycle
14
15
  *
15
- * 1. `prepareArtifact()` creates `staging/{artifactId}/`
16
- * 2. Container writes to `staging/{artifactId}/` (mounted as `/output`)
17
- * 3. Success: `commitArtifact()` atomically moves to `artifacts/{artifactId}/`
18
- * OR Failure: `discardArtifact()` deletes `staging/{artifactId}/`
16
+ * Each step execution produces a **run**, a structured directory containing
17
+ * artifacts (files produced by the step), logs (stdout/stderr), and metadata.
19
18
  *
20
- * Artifacts are immutable once committed.
19
+ * 1. `prepareRun()` creates `staging/{runId}/` with `artifacts/` subdirectory
20
+ * 2. Container writes to `staging/{runId}/artifacts/` (mounted as `/output`)
21
+ * 3. Orchestration layer writes logs and metadata to `staging/{runId}/`
22
+ * 4. Success: `commitRun()` atomically moves to `runs/{runId}/`
23
+ * OR Failure: `discardRun()` deletes `staging/{runId}/`
24
+ *
25
+ * Runs are immutable once committed.
21
26
  *
22
27
  * ## Cache Lifecycle
23
28
  *
@@ -30,12 +35,12 @@ import { join } from 'node:path';
30
35
  * @example
31
36
  * ```typescript
32
37
  * const ws = await Workspace.create('/tmp/workdir', 'my-workspace')
33
- * const artifactId = ws.generateArtifactId()
34
- * await ws.prepareArtifact(artifactId)
38
+ * const runId = ws.generateRunId()
39
+ * await ws.prepareRun(runId)
35
40
  * await ws.prepareCache('pnpm-store')
36
41
  * // ... container execution ...
37
- * await ws.commitArtifact(artifactId) // On success
38
- * // OR await ws.discardArtifact(artifactId) // On failure
42
+ * await ws.commitRun(runId) // On success
43
+ * // OR await ws.discardRun(runId) // On failure
39
44
  * ```
40
45
  */
41
46
  export class Workspace {
@@ -49,7 +54,7 @@ export class Workspace {
49
54
  return Workspace.generateId();
50
55
  }
51
56
  /**
52
- * Creates a new workspace with staging, artifacts, and caches directories.
57
+ * Creates a new workspace with staging, runs, and caches directories.
53
58
  * @param workdirRoot - Root directory for all workspaces
54
59
  * @param id - Optional workspace ID (auto-generated if omitted)
55
60
  * @returns Newly created workspace
@@ -58,7 +63,7 @@ export class Workspace {
58
63
  const workspaceId = id ?? Workspace.generateWorkspaceId();
59
64
  const root = join(workdirRoot, workspaceId);
60
65
  await mkdir(join(root, 'staging'), { recursive: true });
61
- await mkdir(join(root, 'artifacts'), { recursive: true });
66
+ await mkdir(join(root, 'runs'), { recursive: true });
62
67
  await mkdir(join(root, 'caches'), { recursive: true });
63
68
  return new Workspace(workspaceId, root);
64
69
  }
@@ -96,7 +101,7 @@ export class Workspace {
96
101
  */
97
102
  static async remove(workdirRoot, id) {
98
103
  if (id.includes('..') || id.includes('/')) {
99
- throw new Error(`Invalid workspace ID: ${id}`);
104
+ throw new WorkspaceError('INVALID_WORKSPACE_ID', `Invalid workspace ID: ${id}`);
100
105
  }
101
106
  await rm(join(workdirRoot, id), { recursive: true, force: true });
102
107
  }
@@ -113,75 +118,104 @@ export class Workspace {
113
118
  this.root = root;
114
119
  }
115
120
  /**
116
- * Generates a unique artifact identifier.
117
- * @returns Artifact ID in format: `{timestamp}-{uuid-prefix}`
121
+ * Generates a unique run identifier.
122
+ * @returns Run ID in format: `{timestamp}-{uuid-prefix}`
118
123
  */
119
- generateArtifactId() {
124
+ generateRunId() {
120
125
  return Workspace.generateId();
121
126
  }
122
127
  /**
123
- * Returns the staging directory path for an artifact.
124
- * Staging is used for temporary writes during execution.
125
- * @param artifactId - Artifact identifier
128
+ * Returns the staging directory path for a run.
129
+ * @param runId - Run identifier
126
130
  * @returns Absolute path to staging directory
127
- * @throws If artifact ID is invalid
128
131
  */
129
- stagingPath(artifactId) {
130
- this.validateArtifactId(artifactId);
131
- return join(this.root, 'staging', artifactId);
132
+ runStagingPath(runId) {
133
+ this.validateRunId(runId);
134
+ return join(this.root, 'staging', runId);
135
+ }
136
+ /**
137
+ * Returns the staging artifacts directory path for a run.
138
+ * @param runId - Run identifier
139
+ * @returns Absolute path to staging artifacts directory
140
+ */
141
+ runStagingArtifactsPath(runId) {
142
+ return join(this.runStagingPath(runId), 'artifacts');
143
+ }
144
+ /**
145
+ * Returns the committed run directory path.
146
+ * @param runId - Run identifier
147
+ * @returns Absolute path to run directory
148
+ */
149
+ runPath(runId) {
150
+ this.validateRunId(runId);
151
+ return join(this.root, 'runs', runId);
132
152
  }
133
153
  /**
134
- * Returns the committed artifact directory path.
135
- * Artifacts are immutable once committed.
136
- * @param artifactId - Artifact identifier
137
- * @returns Absolute path to artifact directory
138
- * @throws If artifact ID is invalid
154
+ * Returns the artifacts directory path within a committed run.
155
+ * @param runId - Run identifier
156
+ * @returns Absolute path to run artifacts directory
139
157
  */
140
- artifactPath(artifactId) {
141
- this.validateArtifactId(artifactId);
142
- return join(this.root, 'artifacts', artifactId);
158
+ runArtifactsPath(runId) {
159
+ return join(this.runPath(runId), 'artifacts');
143
160
  }
144
161
  /**
145
- * Prepares a staging directory for a new artifact.
146
- * @param artifactId - Artifact identifier
162
+ * Prepares a staging directory for a new run.
163
+ * Creates both the run directory and its artifacts subdirectory.
164
+ * @param runId - Run identifier
147
165
  * @returns Absolute path to the created staging directory
148
166
  */
149
- async prepareArtifact(artifactId) {
150
- const path = this.stagingPath(artifactId);
151
- await mkdir(path, { recursive: true });
152
- return path;
167
+ async prepareRun(runId) {
168
+ try {
169
+ const path = this.runStagingPath(runId);
170
+ await mkdir(path, { recursive: true });
171
+ await mkdir(join(path, 'artifacts'), { recursive: true });
172
+ return path;
173
+ }
174
+ catch (error) {
175
+ throw new StagingError(`Failed to prepare run ${runId}`, { cause: error });
176
+ }
153
177
  }
154
178
  /**
155
- * Commits a staging artifact to the artifacts directory.
179
+ * Commits a staging run to the runs directory.
156
180
  * Uses atomic rename operation for consistency.
157
- * @param artifactId - Artifact identifier
181
+ * @param runId - Run identifier
158
182
  */
159
- async commitArtifact(artifactId) {
160
- await rename(this.stagingPath(artifactId), this.artifactPath(artifactId));
183
+ async commitRun(runId) {
184
+ try {
185
+ await rename(this.runStagingPath(runId), this.runPath(runId));
186
+ }
187
+ catch (error) {
188
+ throw new StagingError(`Failed to commit run ${runId}`, { cause: error });
189
+ }
161
190
  }
162
191
  /**
163
- * Creates a symlink from `step-artifacts/{stepId}` to the committed artifact.
192
+ * Creates a symlink from `step-runs/{stepId}` to the committed run.
164
193
  * Replaces any existing symlink for the same step.
165
194
  * @param stepId - Step identifier
166
- * @param artifactId - Committed artifact identifier
195
+ * @param runId - Committed run identifier
167
196
  */
168
- async linkArtifact(stepId, artifactId) {
169
- const dir = join(this.root, 'step-artifacts');
197
+ async linkRun(stepId, runId) {
198
+ const dir = join(this.root, 'step-runs');
170
199
  await mkdir(dir, { recursive: true });
171
200
  const linkPath = join(dir, stepId);
172
201
  await rm(linkPath, { force: true });
173
- await symlink(join('..', 'artifacts', artifactId), linkPath);
202
+ await symlink(join('..', 'runs', runId), linkPath);
174
203
  }
175
204
  /**
176
- * Discards a staging artifact (on execution failure).
177
- * @param artifactId - Artifact identifier
205
+ * Discards a staging run (on execution failure).
206
+ * @param runId - Run identifier
178
207
  */
179
- async discardArtifact(artifactId) {
180
- await rm(this.stagingPath(artifactId), { recursive: true, force: true });
208
+ async discardRun(runId) {
209
+ try {
210
+ await rm(this.runStagingPath(runId), { recursive: true, force: true });
211
+ }
212
+ catch (error) {
213
+ throw new StagingError(`Failed to discard run ${runId}`, { cause: error });
214
+ }
181
215
  }
182
216
  /**
183
217
  * Removes all staging directories.
184
- * Should be called on workspace initialization to clean up incomplete artifacts.
218
+ * Should be called on workspace initialization to clean up incomplete runs.
185
219
  */
186
220
  async cleanupStaging() {
187
221
  const stagingDir = join(this.root, 'staging');
@@ -198,18 +232,34 @@ export class Workspace {
198
232
  }
199
233
  }
200
234
  /**
201
- * Lists all committed artifact IDs in this workspace.
202
- * @returns Array of artifact IDs (directory names in artifacts/)
235
+ * Lists all committed run IDs in this workspace.
236
+ * @returns Array of run IDs (directory names in runs/)
203
237
  */
204
- async listArtifacts() {
238
+ async listRuns() {
205
239
  try {
206
- const entries = await readdir(join(this.root, 'artifacts'), { withFileTypes: true });
240
+ const entries = await readdir(join(this.root, 'runs'), { withFileTypes: true });
207
241
  return entries.filter(e => e.isDirectory()).map(e => e.name);
208
242
  }
209
243
  catch {
210
244
  return [];
211
245
  }
212
246
  }
247
+ /**
248
+ * Removes runs not in the given set of active run IDs.
249
+ * @param activeRunIds - Set of run IDs to keep
250
+ * @returns Number of runs removed
251
+ */
252
+ async pruneRuns(activeRunIds) {
253
+ const allRuns = await this.listRuns();
254
+ let removed = 0;
255
+ for (const runId of allRuns) {
256
+ if (!activeRunIds.has(runId)) {
257
+ await rm(join(this.root, 'runs', runId), { recursive: true, force: true });
258
+ removed++;
259
+ }
260
+ }
261
+ return removed;
262
+ }
213
263
  /**
214
264
  * Returns the cache directory path.
215
265
  * Caches are persistent read-write directories shared across steps.
@@ -246,32 +296,80 @@ export class Workspace {
246
296
  }
247
297
  }
248
298
  /**
249
- * Validates an artifact ID to prevent path traversal attacks.
250
- * @param id - Artifact identifier to validate
251
- * @throws If the artifact ID contains invalid characters or path traversal attempts
299
+ * Marks a step as currently running by writing a marker file.
300
+ * @param stepId - Step identifier
301
+ * @param meta - Metadata about the running step
302
+ */
303
+ async markStepRunning(stepId, meta) {
304
+ const dir = join(this.root, 'running');
305
+ await mkdir(dir, { recursive: true });
306
+ await writeFile(join(dir, stepId), JSON.stringify(meta), 'utf8');
307
+ }
308
+ /**
309
+ * Removes the running marker for a step.
310
+ * @param stepId - Step identifier
311
+ */
312
+ async markStepDone(stepId) {
313
+ await rm(join(this.root, 'running', stepId), { force: true });
314
+ }
315
+ /**
316
+ * Lists all steps currently marked as running.
317
+ * @returns Array of running step metadata
318
+ */
319
+ async listRunningSteps() {
320
+ const dir = join(this.root, 'running');
321
+ try {
322
+ const entries = await readdir(dir);
323
+ const results = [];
324
+ for (const entry of entries) {
325
+ try {
326
+ const content = await readFile(join(dir, entry), 'utf8');
327
+ const meta = JSON.parse(content);
328
+ results.push({ stepId: entry, ...meta });
329
+ }
330
+ catch {
331
+ // Ignore malformed entries
332
+ }
333
+ }
334
+ return results;
335
+ }
336
+ catch {
337
+ return [];
338
+ }
339
+ }
340
+ /**
341
+ * Removes all running markers.
342
+ */
343
+ async cleanupRunning() {
344
+ await rm(join(this.root, 'running'), { recursive: true, force: true });
345
+ }
346
+ /**
347
+ * Validates a run ID to prevent path traversal attacks.
348
+ * @param id - Run identifier to validate
349
+ * @throws If the run ID contains invalid characters or path traversal attempts
252
350
  * @internal
253
351
  */
254
- validateArtifactId(id) {
352
+ validateRunId(id) {
255
353
  if (!/^[\w-]+$/.test(id)) {
256
- throw new Error(`Invalid artifact ID: ${id}. Must contain only alphanumeric characters, dashes, and underscores.`);
354
+ throw new WorkspaceError('INVALID_RUN_ID', `Invalid run ID: ${id}. Must contain only alphanumeric characters, dashes, and underscores.`);
257
355
  }
258
356
  if (id.includes('..')) {
259
- throw new Error(`Invalid artifact ID: ${id}. Path traversal is not allowed.`);
357
+ throw new WorkspaceError('INVALID_RUN_ID', `Invalid run ID: ${id}. Path traversal is not allowed.`);
260
358
  }
261
359
  }
262
360
  /**
263
361
  * Validates a cache name to prevent path traversal attacks.
264
- * Same rules as artifact IDs: alphanumeric, dashes, underscores only.
362
+ * Same rules as run IDs: alphanumeric, dashes, underscores only.
265
363
  * @param name - Cache name to validate
266
364
  * @throws If the cache name contains invalid characters or path traversal attempts
267
365
  * @internal
268
366
  */
269
367
  validateCacheName(name) {
270
368
  if (!/^[\w-]+$/.test(name)) {
271
- throw new Error(`Invalid cache name: ${name}. Must contain only alphanumeric characters, dashes, and underscores.`);
369
+ throw new WorkspaceError('INVALID_CACHE_NAME', `Invalid cache name: ${name}. Must contain only alphanumeric characters, dashes, and underscores.`);
272
370
  }
273
371
  if (name.includes('..')) {
274
- throw new Error(`Invalid cache name: ${name}. Path traversal is not allowed.`);
372
+ throw new WorkspaceError('INVALID_CACHE_NAME', `Invalid cache name: ${name}. Path traversal is not allowed.`);
275
373
  }
276
374
  }
277
375
  }
package/dist/errors.js ADDED
@@ -0,0 +1,122 @@
1
+ export class PipexError extends Error {
2
+ code;
3
+ constructor(code, message, options) {
4
+ super(message, options);
5
+ this.code = code;
6
+ this.name = 'PipexError';
7
+ }
8
+ get transient() {
9
+ return false;
10
+ }
11
+ }
12
+ // -- Docker errors -----------------------------------------------------------
13
+ export class DockerError extends PipexError {
14
+ constructor(code, message, options) {
15
+ super(code, message, options);
16
+ this.name = 'DockerError';
17
+ }
18
+ }
19
+ export class DockerNotAvailableError extends DockerError {
20
+ constructor(options) {
21
+ super('DOCKER_NOT_AVAILABLE', 'Docker CLI not found. Please install Docker.', options);
22
+ this.name = 'DockerNotAvailableError';
23
+ }
24
+ get transient() {
25
+ return true;
26
+ }
27
+ }
28
+ export class ImagePullError extends DockerError {
29
+ constructor(image, options) {
30
+ super('IMAGE_PULL_FAILED', `Failed to pull image "${image}"`, options);
31
+ this.name = 'ImagePullError';
32
+ }
33
+ get transient() {
34
+ return true;
35
+ }
36
+ }
37
+ export class ContainerTimeoutError extends DockerError {
38
+ constructor(timeoutSec, options) {
39
+ super('CONTAINER_TIMEOUT', `Container exceeded timeout of ${timeoutSec}s`, options);
40
+ this.name = 'ContainerTimeoutError';
41
+ }
42
+ }
43
+ export class ContainerCrashError extends DockerError {
44
+ stepId;
45
+ exitCode;
46
+ constructor(stepId, exitCode, options) {
47
+ super('CONTAINER_CRASH', `Step ${stepId} failed with exit code ${exitCode}`, options);
48
+ this.stepId = stepId;
49
+ this.exitCode = exitCode;
50
+ this.name = 'ContainerCrashError';
51
+ }
52
+ }
53
+ export class ContainerCleanupError extends DockerError {
54
+ constructor(options) {
55
+ super('CONTAINER_CLEANUP_FAILED', 'Failed to clean up container', options);
56
+ this.name = 'ContainerCleanupError';
57
+ }
58
+ }
59
+ // -- Workspace errors --------------------------------------------------------
60
+ export class WorkspaceError extends PipexError {
61
+ constructor(code, message, options) {
62
+ super(code, message, options);
63
+ this.name = 'WorkspaceError';
64
+ }
65
+ }
66
+ export class ArtifactNotFoundError extends WorkspaceError {
67
+ constructor(message, options) {
68
+ super('ARTIFACT_NOT_FOUND', message, options);
69
+ this.name = 'ArtifactNotFoundError';
70
+ }
71
+ }
72
+ export class StagingError extends WorkspaceError {
73
+ constructor(message, options) {
74
+ super('STAGING_FAILED', message, options);
75
+ this.name = 'StagingError';
76
+ }
77
+ }
78
+ // -- Pipeline errors ---------------------------------------------------------
79
+ export class PipelineError extends PipexError {
80
+ constructor(code, message, options) {
81
+ super(code, message, options);
82
+ this.name = 'PipelineError';
83
+ }
84
+ }
85
+ export class ValidationError extends PipelineError {
86
+ constructor(message, options) {
87
+ super('VALIDATION_ERROR', message, options);
88
+ this.name = 'ValidationError';
89
+ }
90
+ }
91
+ export class CyclicDependencyError extends PipelineError {
92
+ constructor(message, options) {
93
+ super('CYCLIC_DEPENDENCY', message, options);
94
+ this.name = 'CyclicDependencyError';
95
+ }
96
+ }
97
+ export class StepNotFoundError extends PipelineError {
98
+ constructor(stepId, referencedStep, options) {
99
+ super('STEP_NOT_FOUND', `Step ${stepId}: input step '${referencedStep}' not found or not yet executed`, options);
100
+ this.name = 'StepNotFoundError';
101
+ }
102
+ }
103
+ // -- Bundle errors -----------------------------------------------------------
104
+ export class BundleError extends PipexError {
105
+ constructor(message, options) {
106
+ super('BUNDLE_ERROR', message, options);
107
+ this.name = 'BundleError';
108
+ }
109
+ }
110
+ // -- Kit errors --------------------------------------------------------------
111
+ export class KitError extends PipexError {
112
+ constructor(code, message, options) {
113
+ super(code, message, options);
114
+ this.name = 'KitError';
115
+ }
116
+ }
117
+ export class MissingParameterError extends KitError {
118
+ constructor(kitName, paramName, options) {
119
+ super('MISSING_PARAMETER', `Kit "${kitName}": "${paramName}" parameter is required`, options);
120
+ this.name = 'MissingParameterError';
121
+ }
122
+ }
package/dist/index.js CHANGED
@@ -38,3 +38,6 @@
38
38
  */
39
39
  // Export engine layer for programmatic use
40
40
  export { Workspace, ContainerExecutor, DockerCliExecutor } from './engine/index.js';
41
+ // Export core layer for pipeline orchestration
42
+ export { PipelineRunner, StepRunner, PipelineLoader, StateManager, ConsoleReporter, StreamReporter, CompositeReporter, InMemoryTransport, EventAggregator, buildGraph, topologicalLevels, subgraph, leafNodes, evaluateCondition, dirSize, formatSize, formatDuration } from './core/index.js';
43
+ export { PipexError, DockerError, DockerNotAvailableError, ImagePullError, ContainerTimeoutError, ContainerCrashError, ContainerCleanupError, WorkspaceError, ArtifactNotFoundError, StagingError, PipelineError, ValidationError, CyclicDependencyError, StepNotFoundError, KitError, MissingParameterError } from './errors.js';
@@ -0,0 +1,23 @@
1
+ import test from 'ava';
2
+ import { KitError } from '../../errors.js';
3
+ import { getKit } from '../index.js';
4
+ test('getKit returns node kit', t => {
5
+ const kit = getKit('node');
6
+ t.is(kit.name, 'node');
7
+ });
8
+ test('getKit returns python kit', t => {
9
+ const kit = getKit('python');
10
+ t.is(kit.name, 'python');
11
+ });
12
+ test('getKit returns shell kit', t => {
13
+ const kit = getKit('shell');
14
+ t.is(kit.name, 'shell');
15
+ });
16
+ test('getKit throws KitError on unknown kit with available list', t => {
17
+ const error = t.throws(() => getKit('unknown'));
18
+ t.true(error instanceof KitError);
19
+ t.truthy(error?.message.includes('Unknown kit'));
20
+ t.truthy(error?.message.includes('node'));
21
+ t.truthy(error?.message.includes('python'));
22
+ t.truthy(error?.message.includes('shell'));
23
+ });
@@ -0,0 +1,74 @@
1
+ import test from 'ava';
2
+ import { KitError, MissingParameterError } from '../../../errors.js';
3
+ import { nodeKit } from '../node.js';
4
+ test('resolve with minimal params (script only)', t => {
5
+ const result = nodeKit.resolve({ script: 'index.js' });
6
+ t.is(result.image, 'node:24-alpine');
7
+ t.truthy(result.cmd[2].includes('node /app/index.js'));
8
+ t.is(result.sources, undefined);
9
+ });
10
+ test('resolve uses default version and variant', t => {
11
+ const result = nodeKit.resolve({ script: 'app.js' });
12
+ t.is(result.image, 'node:24-alpine');
13
+ });
14
+ test('resolve with npm package manager', t => {
15
+ const result = nodeKit.resolve({ script: 'app.js', packageManager: 'npm' });
16
+ t.truthy(result.cmd[2].includes('cd /app && npm install'));
17
+ t.deepEqual(result.caches, [{ name: 'npm-cache', path: '/root/.npm' }]);
18
+ });
19
+ test('resolve with pnpm package manager', t => {
20
+ const result = nodeKit.resolve({ script: 'app.js', packageManager: 'pnpm' });
21
+ t.truthy(result.cmd[2].includes('pnpm install'));
22
+ t.deepEqual(result.caches, [{ name: 'pnpm-store', path: '/root/.local/share/pnpm/store' }]);
23
+ });
24
+ test('resolve with yarn package manager', t => {
25
+ const result = nodeKit.resolve({ script: 'app.js', packageManager: 'yarn' });
26
+ t.truthy(result.cmd[2].includes('yarn install'));
27
+ t.deepEqual(result.caches, [{ name: 'yarn-cache', path: '/usr/local/share/.cache/yarn' }]);
28
+ });
29
+ test('resolve with install=false skips install command', t => {
30
+ const result = nodeKit.resolve({ script: 'app.js', install: false });
31
+ t.falsy(result.cmd[2].includes('npm install'));
32
+ t.truthy(result.cmd[2].includes('node /app/app.js'));
33
+ });
34
+ test('resolve with src adds source', t => {
35
+ const result = nodeKit.resolve({ script: 'app.js', src: 'myapp' });
36
+ t.deepEqual(result.sources, [{ host: 'myapp', container: '/app' }]);
37
+ t.is(result.mounts, undefined);
38
+ });
39
+ test('resolve sets allowNetwork to true', t => {
40
+ const result = nodeKit.resolve({ script: 'app.js' });
41
+ t.true(result.allowNetwork);
42
+ });
43
+ test('resolve throws KitError on unsupported packageManager', t => {
44
+ const error = t.throws(() => nodeKit.resolve({ script: 'app.js', packageManager: 'bun' }), {
45
+ message: /unsupported packageManager/
46
+ });
47
+ t.true(error instanceof KitError);
48
+ });
49
+ test('resolve throws MissingParameterError without script or run', t => {
50
+ const error = t.throws(() => nodeKit.resolve({}), { message: /required/i });
51
+ t.true(error instanceof MissingParameterError);
52
+ });
53
+ test('resolve throws KitError when both script and run are provided', t => {
54
+ const error = t.throws(() => nodeKit.resolve({ script: 'app.js', run: 'npm run build' }), {
55
+ message: /mutually exclusive/
56
+ });
57
+ t.true(error instanceof KitError);
58
+ });
59
+ // -- run parameter ------------------------------------------------------------
60
+ test('resolve with run uses the command directly', t => {
61
+ const result = nodeKit.resolve({ run: 'npm run build --prefix /app' });
62
+ t.truthy(result.cmd[2].includes('npm run build --prefix /app'));
63
+ t.falsy(result.cmd[2].includes('node /app/'));
64
+ });
65
+ test('resolve with run still runs install by default', t => {
66
+ const result = nodeKit.resolve({ run: 'npx eslint /app/src' });
67
+ t.truthy(result.cmd[2].includes('npm install'));
68
+ t.truthy(result.cmd[2].includes('npx eslint /app/src'));
69
+ });
70
+ test('resolve with run and install=false skips install', t => {
71
+ const result = nodeKit.resolve({ run: 'node --version', install: false });
72
+ t.falsy(result.cmd[2].includes('npm install'));
73
+ t.is(result.cmd[2], 'node --version');
74
+ });
@@ -0,0 +1,67 @@
1
+ import test from 'ava';
2
+ import { KitError, MissingParameterError } from '../../../errors.js';
3
+ import { pythonKit } from '../python.js';
4
+ test('resolve with minimal params (script only)', t => {
5
+ const result = pythonKit.resolve({ script: 'main.py' });
6
+ t.is(result.image, 'python:3.12-slim');
7
+ t.truthy(result.cmd[2].includes('python /app/main.py'));
8
+ });
9
+ test('resolve uses default version and variant', t => {
10
+ const result = pythonKit.resolve({ script: 'app.py' });
11
+ t.is(result.image, 'python:3.12-slim');
12
+ });
13
+ test('resolve with pip package manager', t => {
14
+ const result = pythonKit.resolve({ script: 'app.py', packageManager: 'pip' });
15
+ t.truthy(result.cmd[2].includes('pip install'));
16
+ t.deepEqual(result.caches, [{ name: 'pip-cache', path: '/root/.cache/pip' }]);
17
+ });
18
+ test('resolve with uv package manager', t => {
19
+ const result = pythonKit.resolve({ script: 'app.py', packageManager: 'uv' });
20
+ t.truthy(result.cmd[2].includes('uv pip install'));
21
+ t.deepEqual(result.caches, [{ name: 'uv-cache', path: '/root/.cache/uv' }]);
22
+ });
23
+ test('resolve with install=false skips install command', t => {
24
+ const result = pythonKit.resolve({ script: 'app.py', install: false });
25
+ t.falsy(result.cmd[2].includes('pip install'));
26
+ t.truthy(result.cmd[2].includes('python /app/app.py'));
27
+ });
28
+ test('resolve with src adds mount', t => {
29
+ const result = pythonKit.resolve({ script: 'app.py', src: 'myproject' });
30
+ t.deepEqual(result.mounts, [{ host: 'myproject', container: '/app' }]);
31
+ });
32
+ test('resolve sets allowNetwork to true', t => {
33
+ const result = pythonKit.resolve({ script: 'app.py' });
34
+ t.true(result.allowNetwork);
35
+ });
36
+ test('resolve throws KitError on unsupported packageManager', t => {
37
+ const error = t.throws(() => pythonKit.resolve({ script: 'app.py', packageManager: 'conda' }), {
38
+ message: /unsupported packageManager/
39
+ });
40
+ t.true(error instanceof KitError);
41
+ });
42
+ test('resolve throws MissingParameterError without script or run', t => {
43
+ const error = t.throws(() => pythonKit.resolve({}), { message: /required/i });
44
+ t.true(error instanceof MissingParameterError);
45
+ });
46
+ test('resolve throws KitError when both script and run are provided', t => {
47
+ const error = t.throws(() => pythonKit.resolve({ script: 'app.py', run: 'pytest /app/tests' }), {
48
+ message: /mutually exclusive/
49
+ });
50
+ t.true(error instanceof KitError);
51
+ });
52
+ // -- run parameter ------------------------------------------------------------
53
+ test('resolve with run uses the command directly', t => {
54
+ const result = pythonKit.resolve({ run: 'pytest /app/tests -v' });
55
+ t.truthy(result.cmd[2].includes('pytest /app/tests -v'));
56
+ t.falsy(result.cmd[2].includes('python /app/'));
57
+ });
58
+ test('resolve with run still runs install by default', t => {
59
+ const result = pythonKit.resolve({ run: 'pytest /app/tests' });
60
+ t.truthy(result.cmd[2].includes('pip install'));
61
+ t.truthy(result.cmd[2].includes('pytest /app/tests'));
62
+ });
63
+ test('resolve with run and install=false skips install', t => {
64
+ const result = pythonKit.resolve({ run: 'python --version', install: false });
65
+ t.falsy(result.cmd[2].includes('pip install'));
66
+ t.is(result.cmd[2], 'python --version');
67
+ });