@livingdata/pipex 0.0.7 → 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 -62
- 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 +103 -56
- package/package.json +1 -1
package/dist/engine/workspace.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { access, mkdir, readdir, rename, rm } from 'node:fs/promises';
|
|
1
|
+
import { access, mkdir, readdir, rename, rm, symlink } from 'node:fs/promises';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
/**
|
|
@@ -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,62 +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');
|
|
132
142
|
}
|
|
133
143
|
/**
|
|
134
|
-
* Returns the committed
|
|
135
|
-
*
|
|
136
|
-
* @
|
|
137
|
-
* @returns Absolute path to artifact directory
|
|
138
|
-
* @throws If artifact ID is invalid
|
|
144
|
+
* Returns the committed run directory path.
|
|
145
|
+
* @param runId - Run identifier
|
|
146
|
+
* @returns Absolute path to run directory
|
|
139
147
|
*/
|
|
140
|
-
|
|
141
|
-
this.
|
|
142
|
-
return join(this.root, '
|
|
148
|
+
runPath(runId) {
|
|
149
|
+
this.validateRunId(runId);
|
|
150
|
+
return join(this.root, 'runs', runId);
|
|
143
151
|
}
|
|
144
152
|
/**
|
|
145
|
-
*
|
|
146
|
-
* @param
|
|
153
|
+
* Returns the artifacts directory path within a committed run.
|
|
154
|
+
* @param runId - Run identifier
|
|
155
|
+
* @returns Absolute path to run artifacts directory
|
|
156
|
+
*/
|
|
157
|
+
runArtifactsPath(runId) {
|
|
158
|
+
return join(this.runPath(runId), 'artifacts');
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
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
|
-
*
|
|
164
|
-
*
|
|
181
|
+
* Creates a symlink from `step-runs/{stepId}` to the committed run.
|
|
182
|
+
* Replaces any existing symlink for the same step.
|
|
183
|
+
* @param stepId - Step identifier
|
|
184
|
+
* @param runId - Committed run identifier
|
|
165
185
|
*/
|
|
166
|
-
async
|
|
167
|
-
|
|
186
|
+
async linkRun(stepId, runId) {
|
|
187
|
+
const dir = join(this.root, 'step-runs');
|
|
188
|
+
await mkdir(dir, { recursive: true });
|
|
189
|
+
const linkPath = join(dir, stepId);
|
|
190
|
+
await rm(linkPath, { force: true });
|
|
191
|
+
await symlink(join('..', 'runs', runId), linkPath);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Discards a staging run (on execution failure).
|
|
195
|
+
* @param runId - Run identifier
|
|
196
|
+
*/
|
|
197
|
+
async discardRun(runId) {
|
|
198
|
+
await rm(this.runStagingPath(runId), { recursive: true, force: true });
|
|
168
199
|
}
|
|
169
200
|
/**
|
|
170
201
|
* Removes all staging directories.
|
|
171
|
-
* Should be called on workspace initialization to clean up incomplete
|
|
202
|
+
* Should be called on workspace initialization to clean up incomplete runs.
|
|
172
203
|
*/
|
|
173
204
|
async cleanupStaging() {
|
|
174
205
|
const stagingDir = join(this.root, 'staging');
|
|
@@ -185,18 +216,34 @@ export class Workspace {
|
|
|
185
216
|
}
|
|
186
217
|
}
|
|
187
218
|
/**
|
|
188
|
-
* Lists all committed
|
|
189
|
-
* @returns Array of
|
|
219
|
+
* Lists all committed run IDs in this workspace.
|
|
220
|
+
* @returns Array of run IDs (directory names in runs/)
|
|
190
221
|
*/
|
|
191
|
-
async
|
|
222
|
+
async listRuns() {
|
|
192
223
|
try {
|
|
193
|
-
const entries = await readdir(join(this.root, '
|
|
224
|
+
const entries = await readdir(join(this.root, 'runs'), { withFileTypes: true });
|
|
194
225
|
return entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
195
226
|
}
|
|
196
227
|
catch {
|
|
197
228
|
return [];
|
|
198
229
|
}
|
|
199
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
|
+
}
|
|
200
247
|
/**
|
|
201
248
|
* Returns the cache directory path.
|
|
202
249
|
* Caches are persistent read-write directories shared across steps.
|
|
@@ -233,22 +280,22 @@ export class Workspace {
|
|
|
233
280
|
}
|
|
234
281
|
}
|
|
235
282
|
/**
|
|
236
|
-
* Validates
|
|
237
|
-
* @param id -
|
|
238
|
-
* @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
|
|
239
286
|
* @internal
|
|
240
287
|
*/
|
|
241
|
-
|
|
288
|
+
validateRunId(id) {
|
|
242
289
|
if (!/^[\w-]+$/.test(id)) {
|
|
243
|
-
throw new Error(`Invalid
|
|
290
|
+
throw new Error(`Invalid run ID: ${id}. Must contain only alphanumeric characters, dashes, and underscores.`);
|
|
244
291
|
}
|
|
245
292
|
if (id.includes('..')) {
|
|
246
|
-
throw new Error(`Invalid
|
|
293
|
+
throw new Error(`Invalid run ID: ${id}. Path traversal is not allowed.`);
|
|
247
294
|
}
|
|
248
295
|
}
|
|
249
296
|
/**
|
|
250
297
|
* Validates a cache name to prevent path traversal attacks.
|
|
251
|
-
* Same rules as
|
|
298
|
+
* Same rules as run IDs: alphanumeric, dashes, underscores only.
|
|
252
299
|
* @param name - Cache name to validate
|
|
253
300
|
* @throws If the cache name contains invalid characters or path traversal attempts
|
|
254
301
|
* @internal
|