@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.
- package/README.md +33 -3
- package/dist/cli/commands/clean.js +22 -0
- package/dist/cli/commands/export.js +32 -0
- package/dist/cli/commands/inspect.js +58 -0
- package/dist/cli/commands/list.js +38 -0
- package/dist/cli/commands/logs.js +54 -0
- package/dist/cli/commands/prune.js +26 -0
- package/dist/cli/commands/rm.js +27 -0
- package/dist/cli/commands/run.js +39 -0
- package/dist/cli/commands/show.js +73 -0
- package/dist/cli/index.js +18 -105
- package/dist/cli/pipeline-runner.js +123 -64
- package/dist/cli/reporter.js +58 -7
- package/dist/cli/state.js +22 -9
- package/dist/cli/utils.js +49 -0
- package/dist/engine/docker-executor.js +23 -5
- package/dist/engine/workspace.js +94 -60
- package/package.json +1 -1
package/dist/engine/workspace.js
CHANGED
|
@@ -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
|
-
* - **
|
|
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
|
-
* ##
|
|
13
|
+
* ## Run Lifecycle
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
-
*
|
|
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
|
|
34
|
-
* await ws.
|
|
37
|
+
* const runId = ws.generateRunId()
|
|
38
|
+
* await ws.prepareRun(runId)
|
|
35
39
|
* await ws.prepareCache('pnpm-store')
|
|
36
40
|
* // ... container execution ...
|
|
37
|
-
* await ws.
|
|
38
|
-
* // OR await ws.
|
|
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,
|
|
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, '
|
|
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
|
|
117
|
-
* @returns
|
|
120
|
+
* Generates a unique run identifier.
|
|
121
|
+
* @returns Run ID in format: `{timestamp}-{uuid-prefix}`
|
|
118
122
|
*/
|
|
119
|
-
|
|
123
|
+
generateRunId() {
|
|
120
124
|
return Workspace.generateId();
|
|
121
125
|
}
|
|
122
126
|
/**
|
|
123
|
-
* Returns the staging directory path for
|
|
124
|
-
*
|
|
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
|
-
|
|
130
|
-
this.
|
|
131
|
-
return join(this.root, 'staging',
|
|
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
|
|
135
|
-
*
|
|
136
|
-
* @
|
|
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
|
-
|
|
141
|
-
this.
|
|
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
|
|
146
|
-
*
|
|
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
|
|
150
|
-
const path = this.
|
|
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
|
|
173
|
+
* Commits a staging run to the runs directory.
|
|
156
174
|
* Uses atomic rename operation for consistency.
|
|
157
|
-
* @param
|
|
175
|
+
* @param runId - Run identifier
|
|
158
176
|
*/
|
|
159
|
-
async
|
|
160
|
-
await rename(this.
|
|
177
|
+
async commitRun(runId) {
|
|
178
|
+
await rename(this.runStagingPath(runId), this.runPath(runId));
|
|
161
179
|
}
|
|
162
180
|
/**
|
|
163
|
-
* Creates a symlink from `step-
|
|
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
|
|
184
|
+
* @param runId - Committed run identifier
|
|
167
185
|
*/
|
|
168
|
-
async
|
|
169
|
-
const dir = join(this.root, 'step-
|
|
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('..', '
|
|
191
|
+
await symlink(join('..', 'runs', runId), linkPath);
|
|
174
192
|
}
|
|
175
193
|
/**
|
|
176
|
-
* Discards a staging
|
|
177
|
-
* @param
|
|
194
|
+
* Discards a staging run (on execution failure).
|
|
195
|
+
* @param runId - Run identifier
|
|
178
196
|
*/
|
|
179
|
-
async
|
|
180
|
-
await rm(this.
|
|
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
|
|
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
|
|
202
|
-
* @returns Array of
|
|
219
|
+
* Lists all committed run IDs in this workspace.
|
|
220
|
+
* @returns Array of run IDs (directory names in runs/)
|
|
203
221
|
*/
|
|
204
|
-
async
|
|
222
|
+
async listRuns() {
|
|
205
223
|
try {
|
|
206
|
-
const entries = await readdir(join(this.root, '
|
|
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
|
|
250
|
-
* @param id -
|
|
251
|
-
* @throws If the
|
|
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
|
-
|
|
288
|
+
validateRunId(id) {
|
|
255
289
|
if (!/^[\w-]+$/.test(id)) {
|
|
256
|
-
throw new Error(`Invalid
|
|
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
|
|
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
|
|
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
|