@livingdata/pipex 0.0.4 → 0.0.5

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 CHANGED
@@ -14,14 +14,14 @@ Runs containers with explicit volume mounts and manages artifacts through a stag
14
14
  Run directly without installing:
15
15
 
16
16
  ```bash
17
- npx @livingdata/pipex run pipeline.yaml --workspace my-build
17
+ npx @livingdata/pipex run pipeline.yaml
18
18
  ```
19
19
 
20
20
  Or install globally:
21
21
 
22
22
  ```bash
23
23
  npm install -g @livingdata/pipex
24
- pipex run pipeline.yaml --workspace my-build
24
+ pipex run pipeline.yaml
25
25
  ```
26
26
 
27
27
  ## Usage
@@ -30,9 +30,6 @@ pipex run pipeline.yaml --workspace my-build
30
30
  # Interactive mode (default)
31
31
  pipex run pipeline.yaml
32
32
 
33
- # With workspace name (enables caching)
34
- pipex run pipeline.yaml --workspace my-build
35
-
36
33
  # JSON mode (for CI/CD)
37
34
  pipex run pipeline.yaml --json
38
35
 
@@ -126,6 +123,10 @@ steps:
126
123
  - id: analyze
127
124
  uses: python
128
125
  with: { script: analyze.py, src: scripts }
126
+ - id: extract
127
+ uses: shell
128
+ with: { packages: [unzip], run: "unzip /input/build/archive.zip -d /output/" }
129
+ inputs: [{ step: build }]
129
130
  ```
130
131
 
131
132
  `uses` and `image`/`cmd` are mutually exclusive. All other step fields (`env`, `inputs`, `mounts`, `caches`, `timeoutSec`, `allowFailure`, `allowNetwork`) remain available and merge with kit defaults (user values take priority). The `src` parameter in `with` generates a read-only mount at `/app` in the container.
@@ -154,13 +155,32 @@ steps:
154
155
  | `install` | `true` | Run dependency install before script |
155
156
  | `variant` | `"slim"` | Image variant |
156
157
 
157
- **`bash`** -- Run a shell command in a lightweight container.
158
+ **`shell`** -- Run a shell command in a container, with optional apt package installation.
158
159
 
159
160
  | Parameter | Default | Description |
160
161
  |-----------|---------|-------------|
161
162
  | `run` | *(required)* | Shell command to execute |
163
+ | `packages` | -- | Apt packages to install before running |
162
164
  | `src` | -- | Host directory to mount at `/app` |
163
- | `image` | `"alpine:3.20"` | Docker image |
165
+ | `image` | `"alpine:3.20"` | Docker image (defaults to `"debian:bookworm-slim"` when `packages` is set) |
166
+
167
+ When `packages` is provided, the kit automatically switches to a Debian image, enables network access, and provides an `apt-cache` cache. Without packages, it runs on a minimal Alpine image with no network.
168
+
169
+ ```yaml
170
+ # Simple command (alpine, no network)
171
+ - id: list-files
172
+ uses: shell
173
+ with:
174
+ run: ls -lhR /input/data/
175
+
176
+ # With system packages (debian, network + apt cache)
177
+ - id: extract
178
+ uses: shell
179
+ with:
180
+ packages: [unzip, jq]
181
+ run: unzip /input/download/data.zip -d /output/
182
+ inputs: [{ step: download }]
183
+ ```
164
184
 
165
185
  ### Raw Steps
166
186
 
@@ -253,12 +273,29 @@ Common use cases:
253
273
 
254
274
  **Note**: Caches are workspace-scoped (not global). Different workspaces have isolated caches.
255
275
 
256
- ## Example
276
+ ## Examples
277
+
278
+ ### Geodata Processing
279
+
280
+ The `examples/geodata/` pipeline downloads a shapefile archive, extracts it, and produces a CSV inventory — using the `debian` and `bash` kits:
281
+
282
+ ```
283
+ examples/geodata/
284
+ └── pipeline.yaml
285
+ ```
286
+
287
+ Steps: `download` → `extract` → `list-files` / `build-csv`
288
+
289
+ ```bash
290
+ pipex run examples/geodata/pipeline.yaml
291
+ ```
292
+
293
+ ### Multi-Language
257
294
 
258
- The `example/` directory contains a multi-language pipeline that chains Node.js and Python steps using kits:
295
+ The `examples/multi-language/` pipeline chains Node.js and Python steps using kits:
259
296
 
260
297
  ```
261
- example/
298
+ examples/multi-language/
262
299
  ├── pipeline.yaml
263
300
  └── scripts/
264
301
  ├── nodejs/ # lodash-based data analysis
@@ -272,10 +309,10 @@ example/
272
309
  └── transform.py
273
310
  ```
274
311
 
275
- The pipeline uses the `node` and `python` kits to run 4 steps: `node-analyze` → `node-transform` → `python-analyze` → `python-transform`. Each step passes artifacts to the next via `/input`.
312
+ Steps: `node-analyze` → `node-transform` → `python-analyze` → `python-transform`
276
313
 
277
314
  ```bash
278
- pipex run example/pipeline.yaml --workspace example-test
315
+ pipex run examples/multi-language/pipeline.yaml
279
316
  ```
280
317
 
281
318
  ## Caching & Workspaces
@@ -334,7 +371,7 @@ cp .env.example .env
334
371
  Run the CLI without building (via tsx):
335
372
 
336
373
  ```bash
337
- npm run cli -- run pipeline.yaml --workspace my-build
374
+ npm run cli -- run pipeline.yaml
338
375
  npm run cli -- list
339
376
  ```
340
377
 
@@ -7,6 +7,9 @@ import { isKitStep } from '../types.js';
7
7
  export class PipelineLoader {
8
8
  async load(filePath) {
9
9
  const content = await readFile(filePath, 'utf8');
10
+ return this.parse(content, filePath);
11
+ }
12
+ parse(content, filePath) {
10
13
  const input = parsePipelineFile(content, filePath);
11
14
  if (!input.id && !input.name) {
12
15
  throw new Error('Invalid pipeline: at least one of "id" or "name" must be defined');
@@ -132,7 +135,7 @@ export class PipelineLoader {
132
135
  }
133
136
  }
134
137
  /** Convert a free-form name into a valid identifier. */
135
- function slugify(name) {
138
+ export function slugify(name) {
136
139
  return deburr(name)
137
140
  .toLowerCase()
138
141
  .replaceAll(/[^\w-]/g, '-')
@@ -140,20 +143,20 @@ function slugify(name) {
140
143
  .replace(/^-/, '')
141
144
  .replace(/-$/, '');
142
145
  }
143
- function parsePipelineFile(content, filePath) {
146
+ export function parsePipelineFile(content, filePath) {
144
147
  const ext = extname(filePath).toLowerCase();
145
148
  if (ext === '.yaml' || ext === '.yml') {
146
149
  return parseYaml(content);
147
150
  }
148
151
  return JSON.parse(content);
149
152
  }
150
- function mergeEnv(kitEnv, userEnv) {
153
+ export function mergeEnv(kitEnv, userEnv) {
151
154
  if (!kitEnv && !userEnv) {
152
155
  return undefined;
153
156
  }
154
157
  return { ...kitEnv, ...userEnv };
155
158
  }
156
- function mergeCaches(kitCaches, userCaches) {
159
+ export function mergeCaches(kitCaches, userCaches) {
157
160
  if (!kitCaches && !userCaches) {
158
161
  return undefined;
159
162
  }
@@ -166,7 +169,7 @@ function mergeCaches(kitCaches, userCaches) {
166
169
  }
167
170
  return [...map.values()];
168
171
  }
169
- function mergeMounts(kitMounts, userMounts) {
172
+ export function mergeMounts(kitMounts, userMounts) {
170
173
  if (!kitMounts && !userMounts) {
171
174
  return undefined;
172
175
  }
@@ -0,0 +1,31 @@
1
+ export const shellKit = {
2
+ name: 'shell',
3
+ resolve(params) {
4
+ const run = params.run;
5
+ if (!run || typeof run !== 'string') {
6
+ throw new Error('Kit "shell": "run" parameter is required');
7
+ }
8
+ const packages = params.packages;
9
+ const hasPackages = packages && packages.length > 0;
10
+ const src = params.src;
11
+ const defaultImage = hasPackages ? 'debian:bookworm-slim' : 'alpine:3.20';
12
+ const image = params.image ?? defaultImage;
13
+ const parts = [];
14
+ if (hasPackages) {
15
+ parts.push(`apt-get update && apt-get install -y --no-install-recommends ${packages.join(' ')} && rm -rf /var/lib/apt/lists/*`);
16
+ }
17
+ parts.push(run);
18
+ const output = {
19
+ image,
20
+ cmd: ['sh', '-c', parts.join(' && ')]
21
+ };
22
+ if (hasPackages) {
23
+ output.caches = [{ name: 'apt-cache', path: '/var/cache/apt' }];
24
+ output.allowNetwork = true;
25
+ }
26
+ if (src) {
27
+ output.mounts = [{ host: src, container: '/app' }];
28
+ }
29
+ return output;
30
+ }
31
+ };
@@ -1,10 +1,10 @@
1
- import { bashKit } from './builtin/bash.js';
2
1
  import { nodeKit } from './builtin/node.js';
3
2
  import { pythonKit } from './builtin/python.js';
3
+ import { shellKit } from './builtin/shell.js';
4
4
  const kits = new Map([
5
- [bashKit.name, bashKit],
6
5
  [nodeKit.name, nodeKit],
7
- [pythonKit.name, pythonKit]
6
+ [pythonKit.name, pythonKit],
7
+ [shellKit.name, shellKit]
8
8
  ]);
9
9
  export function getKit(name) {
10
10
  const kit = kits.get(name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livingdata/pipex",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "Execution engine for containerized pipeline steps",
5
5
  "author": "Jérôme Desboeufs <jerome@livingdata.co>",
6
6
  "type": "module",
@@ -21,6 +21,7 @@
21
21
  },
22
22
  "scripts": {
23
23
  "cli": "tsx src/cli/index.ts",
24
+ "test": "ava",
24
25
  "lint": "xo",
25
26
  "lint:fix": "xo --fix",
26
27
  "build": "tsc"
@@ -36,8 +37,10 @@
36
37
  "yaml": "^2.8.2"
37
38
  },
38
39
  "devDependencies": {
40
+ "@ava/typescript": "^6.0.0",
39
41
  "@types/lodash-es": "^4.17.12",
40
42
  "@types/node": "^25.2.0",
43
+ "ava": "^6.4.1",
41
44
  "tsx": "^4.21.0",
42
45
  "xo": "^1.2.3"
43
46
  }