@livingdata/pipex 0.0.8 → 0.0.9

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.
@@ -6,18 +6,22 @@ import { join } from 'node:path';
6
6
  *
7
7
  * A workspace provides:
8
8
  * - **staging/**: Temporary write location during execution
9
- * - **artifacts/**: Committed outputs (immutable, read-only)
9
+ * - **runs/**: Committed run outputs (immutable, read-only)
10
10
  * - **caches/**: Persistent read-write caches (shared across steps)
11
11
  * - **state.json**: Managed by orchestration layer (e.g., CLI) for caching
12
12
  *
13
- * ## Artifact Lifecycle
13
+ * ## Run Lifecycle
14
14
  *
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}/`
15
+ * Each step execution produces a **run**, a structured directory containing
16
+ * artifacts (files produced by the step), logs (stdout/stderr), and metadata.
19
17
  *
20
- * Artifacts are immutable once committed.
18
+ * 1. `prepareRun()` creates `staging/{runId}/` with `artifacts/` subdirectory
19
+ * 2. Container writes to `staging/{runId}/artifacts/` (mounted as `/output`)
20
+ * 3. Orchestration layer writes logs and metadata to `staging/{runId}/`
21
+ * 4. Success: `commitRun()` atomically moves to `runs/{runId}/`
22
+ * OR Failure: `discardRun()` deletes `staging/{runId}/`
23
+ *
24
+ * Runs are immutable once committed.
21
25
  *
22
26
  * ## Cache Lifecycle
23
27
  *
@@ -30,12 +34,12 @@ import { join } from 'node:path';
30
34
  * @example
31
35
  * ```typescript
32
36
  * const ws = await Workspace.create('/tmp/workdir', 'my-workspace')
33
- * const artifactId = ws.generateArtifactId()
34
- * await ws.prepareArtifact(artifactId)
37
+ * const runId = ws.generateRunId()
38
+ * await ws.prepareRun(runId)
35
39
  * await ws.prepareCache('pnpm-store')
36
40
  * // ... container execution ...
37
- * await ws.commitArtifact(artifactId) // On success
38
- * // OR await ws.discardArtifact(artifactId) // On failure
41
+ * await ws.commitRun(runId) // On success
42
+ * // OR await ws.discardRun(runId) // On failure
39
43
  * ```
40
44
  */
41
45
  export class Workspace {
@@ -49,7 +53,7 @@ export class Workspace {
49
53
  return Workspace.generateId();
50
54
  }
51
55
  /**
52
- * Creates a new workspace with staging, artifacts, and caches directories.
56
+ * Creates a new workspace with staging, runs, and caches directories.
53
57
  * @param workdirRoot - Root directory for all workspaces
54
58
  * @param id - Optional workspace ID (auto-generated if omitted)
55
59
  * @returns Newly created workspace
@@ -58,7 +62,7 @@ export class Workspace {
58
62
  const workspaceId = id ?? Workspace.generateWorkspaceId();
59
63
  const root = join(workdirRoot, workspaceId);
60
64
  await mkdir(join(root, 'staging'), { recursive: true });
61
- await mkdir(join(root, 'artifacts'), { recursive: true });
65
+ await mkdir(join(root, 'runs'), { recursive: true });
62
66
  await mkdir(join(root, 'caches'), { recursive: true });
63
67
  return new Workspace(workspaceId, root);
64
68
  }
@@ -113,75 +117,89 @@ export class Workspace {
113
117
  this.root = root;
114
118
  }
115
119
  /**
116
- * Generates a unique artifact identifier.
117
- * @returns Artifact ID in format: `{timestamp}-{uuid-prefix}`
120
+ * Generates a unique run identifier.
121
+ * @returns Run ID in format: `{timestamp}-{uuid-prefix}`
118
122
  */
119
- generateArtifactId() {
123
+ generateRunId() {
120
124
  return Workspace.generateId();
121
125
  }
122
126
  /**
123
- * Returns the staging directory path for an artifact.
124
- * Staging is used for temporary writes during execution.
125
- * @param artifactId - Artifact identifier
127
+ * Returns the staging directory path for a run.
128
+ * @param runId - Run identifier
126
129
  * @returns Absolute path to staging directory
127
- * @throws If artifact ID is invalid
128
130
  */
129
- stagingPath(artifactId) {
130
- this.validateArtifactId(artifactId);
131
- return join(this.root, 'staging', artifactId);
131
+ runStagingPath(runId) {
132
+ this.validateRunId(runId);
133
+ return join(this.root, 'staging', runId);
134
+ }
135
+ /**
136
+ * Returns the staging artifacts directory path for a run.
137
+ * @param runId - Run identifier
138
+ * @returns Absolute path to staging artifacts directory
139
+ */
140
+ runStagingArtifactsPath(runId) {
141
+ return join(this.runStagingPath(runId), 'artifacts');
142
+ }
143
+ /**
144
+ * Returns the committed run directory path.
145
+ * @param runId - Run identifier
146
+ * @returns Absolute path to run directory
147
+ */
148
+ runPath(runId) {
149
+ this.validateRunId(runId);
150
+ return join(this.root, 'runs', runId);
132
151
  }
133
152
  /**
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
153
+ * Returns the artifacts directory path within a committed run.
154
+ * @param runId - Run identifier
155
+ * @returns Absolute path to run artifacts directory
139
156
  */
140
- artifactPath(artifactId) {
141
- this.validateArtifactId(artifactId);
142
- return join(this.root, 'artifacts', artifactId);
157
+ runArtifactsPath(runId) {
158
+ return join(this.runPath(runId), 'artifacts');
143
159
  }
144
160
  /**
145
- * Prepares a staging directory for a new artifact.
146
- * @param artifactId - Artifact identifier
161
+ * Prepares a staging directory for a new run.
162
+ * Creates both the run directory and its artifacts subdirectory.
163
+ * @param runId - Run identifier
147
164
  * @returns Absolute path to the created staging directory
148
165
  */
149
- async prepareArtifact(artifactId) {
150
- const path = this.stagingPath(artifactId);
166
+ async prepareRun(runId) {
167
+ const path = this.runStagingPath(runId);
151
168
  await mkdir(path, { recursive: true });
169
+ await mkdir(join(path, 'artifacts'), { recursive: true });
152
170
  return path;
153
171
  }
154
172
  /**
155
- * Commits a staging artifact to the artifacts directory.
173
+ * Commits a staging run to the runs directory.
156
174
  * Uses atomic rename operation for consistency.
157
- * @param artifactId - Artifact identifier
175
+ * @param runId - Run identifier
158
176
  */
159
- async commitArtifact(artifactId) {
160
- await rename(this.stagingPath(artifactId), this.artifactPath(artifactId));
177
+ async commitRun(runId) {
178
+ await rename(this.runStagingPath(runId), this.runPath(runId));
161
179
  }
162
180
  /**
163
- * Creates a symlink from `step-artifacts/{stepId}` to the committed artifact.
181
+ * Creates a symlink from `step-runs/{stepId}` to the committed run.
164
182
  * Replaces any existing symlink for the same step.
165
183
  * @param stepId - Step identifier
166
- * @param artifactId - Committed artifact identifier
184
+ * @param runId - Committed run identifier
167
185
  */
168
- async linkArtifact(stepId, artifactId) {
169
- const dir = join(this.root, 'step-artifacts');
186
+ async linkRun(stepId, runId) {
187
+ const dir = join(this.root, 'step-runs');
170
188
  await mkdir(dir, { recursive: true });
171
189
  const linkPath = join(dir, stepId);
172
190
  await rm(linkPath, { force: true });
173
- await symlink(join('..', 'artifacts', artifactId), linkPath);
191
+ await symlink(join('..', 'runs', runId), linkPath);
174
192
  }
175
193
  /**
176
- * Discards a staging artifact (on execution failure).
177
- * @param artifactId - Artifact identifier
194
+ * Discards a staging run (on execution failure).
195
+ * @param runId - Run identifier
178
196
  */
179
- async discardArtifact(artifactId) {
180
- await rm(this.stagingPath(artifactId), { recursive: true, force: true });
197
+ async discardRun(runId) {
198
+ await rm(this.runStagingPath(runId), { recursive: true, force: true });
181
199
  }
182
200
  /**
183
201
  * Removes all staging directories.
184
- * Should be called on workspace initialization to clean up incomplete artifacts.
202
+ * Should be called on workspace initialization to clean up incomplete runs.
185
203
  */
186
204
  async cleanupStaging() {
187
205
  const stagingDir = join(this.root, 'staging');
@@ -198,18 +216,34 @@ export class Workspace {
198
216
  }
199
217
  }
200
218
  /**
201
- * Lists all committed artifact IDs in this workspace.
202
- * @returns Array of artifact IDs (directory names in artifacts/)
219
+ * Lists all committed run IDs in this workspace.
220
+ * @returns Array of run IDs (directory names in runs/)
203
221
  */
204
- async listArtifacts() {
222
+ async listRuns() {
205
223
  try {
206
- const entries = await readdir(join(this.root, 'artifacts'), { withFileTypes: true });
224
+ const entries = await readdir(join(this.root, 'runs'), { withFileTypes: true });
207
225
  return entries.filter(e => e.isDirectory()).map(e => e.name);
208
226
  }
209
227
  catch {
210
228
  return [];
211
229
  }
212
230
  }
231
+ /**
232
+ * Removes runs not in the given set of active run IDs.
233
+ * @param activeRunIds - Set of run IDs to keep
234
+ * @returns Number of runs removed
235
+ */
236
+ async pruneRuns(activeRunIds) {
237
+ const allRuns = await this.listRuns();
238
+ let removed = 0;
239
+ for (const runId of allRuns) {
240
+ if (!activeRunIds.has(runId)) {
241
+ await rm(join(this.root, 'runs', runId), { recursive: true, force: true });
242
+ removed++;
243
+ }
244
+ }
245
+ return removed;
246
+ }
213
247
  /**
214
248
  * Returns the cache directory path.
215
249
  * Caches are persistent read-write directories shared across steps.
@@ -246,22 +280,22 @@ export class Workspace {
246
280
  }
247
281
  }
248
282
  /**
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
283
+ * Validates a run ID to prevent path traversal attacks.
284
+ * @param id - Run identifier to validate
285
+ * @throws If the run ID contains invalid characters or path traversal attempts
252
286
  * @internal
253
287
  */
254
- validateArtifactId(id) {
288
+ validateRunId(id) {
255
289
  if (!/^[\w-]+$/.test(id)) {
256
- throw new Error(`Invalid artifact ID: ${id}. Must contain only alphanumeric characters, dashes, and underscores.`);
290
+ throw new Error(`Invalid run ID: ${id}. Must contain only alphanumeric characters, dashes, and underscores.`);
257
291
  }
258
292
  if (id.includes('..')) {
259
- throw new Error(`Invalid artifact ID: ${id}. Path traversal is not allowed.`);
293
+ throw new Error(`Invalid run ID: ${id}. Path traversal is not allowed.`);
260
294
  }
261
295
  }
262
296
  /**
263
297
  * Validates a cache name to prevent path traversal attacks.
264
- * Same rules as artifact IDs: alphanumeric, dashes, underscores only.
298
+ * Same rules as run IDs: alphanumeric, dashes, underscores only.
265
299
  * @param name - Cache name to validate
266
300
  * @throws If the cache name contains invalid characters or path traversal attempts
267
301
  * @internal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livingdata/pipex",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "Execution engine for containerized pipeline steps",
5
5
  "author": "Jérôme Desboeufs <jerome@livingdata.co>",
6
6
  "type": "module",