@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/README.md
CHANGED
|
@@ -37,13 +37,36 @@ pipex run pipeline.yaml --json
|
|
|
37
37
|
pipex run pipeline.yaml --workdir /tmp/builds
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
### Inspecting runs
|
|
41
|
+
|
|
42
|
+
Each step execution produces a **run** containing artifacts, logs (stdout/stderr), and metadata:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Show all steps and their last run (status, duration, size, date)
|
|
46
|
+
pipex show my-pipeline
|
|
47
|
+
|
|
48
|
+
# Show logs from the last run of a step
|
|
49
|
+
pipex logs my-pipeline download
|
|
50
|
+
pipex logs my-pipeline download --stream stderr
|
|
51
|
+
|
|
52
|
+
# Show execution metadata (image, cmd, duration, exit code, fingerprint…)
|
|
53
|
+
pipex inspect my-pipeline download
|
|
54
|
+
pipex inspect my-pipeline download --json
|
|
55
|
+
|
|
56
|
+
# Export artifacts from a step to the host filesystem
|
|
57
|
+
pipex export my-pipeline download ./output-dir
|
|
58
|
+
```
|
|
59
|
+
|
|
40
60
|
### Managing workspaces
|
|
41
61
|
|
|
42
62
|
```bash
|
|
43
|
-
# List workspaces (with
|
|
63
|
+
# List workspaces (with run/cache counts and disk size)
|
|
44
64
|
pipex list
|
|
45
65
|
pipex ls --json
|
|
46
66
|
|
|
67
|
+
# Remove old runs (keeps only current ones)
|
|
68
|
+
pipex prune my-pipeline
|
|
69
|
+
|
|
47
70
|
# Remove specific workspaces
|
|
48
71
|
pipex rm my-build other-build
|
|
49
72
|
|
|
@@ -56,7 +79,12 @@ pipex clean
|
|
|
56
79
|
| Command | Description |
|
|
57
80
|
|---------|-------------|
|
|
58
81
|
| `run <pipeline>` | Execute a pipeline |
|
|
59
|
-
| `
|
|
82
|
+
| `show <workspace>` | Show steps and runs in a workspace (with artifact sizes) |
|
|
83
|
+
| `logs <workspace> <step>` | Show stdout/stderr from last run |
|
|
84
|
+
| `inspect <workspace> <step>` | Show run metadata (meta.json) |
|
|
85
|
+
| `export <workspace> <step> <dest>` | Extract artifacts from a step run to the host filesystem |
|
|
86
|
+
| `prune <workspace>` | Remove old runs not referenced by current state |
|
|
87
|
+
| `list` (alias `ls`) | List workspaces (with disk sizes) |
|
|
60
88
|
| `rm <workspace...>` | Remove one or more workspaces |
|
|
61
89
|
| `clean` | Remove all workspaces |
|
|
62
90
|
|
|
@@ -73,6 +101,8 @@ pipex clean
|
|
|
73
101
|
|--------|-------|-------------|
|
|
74
102
|
| `--workspace <name>` | `-w` | Workspace name for caching |
|
|
75
103
|
| `--force [steps]` | `-f` | Skip cache for all steps, or a comma-separated list |
|
|
104
|
+
| `--dry-run` | | Validate pipeline, compute fingerprints, show what would run without executing |
|
|
105
|
+
| `--verbose` | | Stream container logs in real-time (interactive mode) |
|
|
76
106
|
|
|
77
107
|
## Pipeline Format
|
|
78
108
|
|
|
@@ -372,7 +402,7 @@ pipex rm old-workspace-id
|
|
|
372
402
|
pipex clean
|
|
373
403
|
```
|
|
374
404
|
|
|
375
|
-
### Cached step with missing
|
|
405
|
+
### Cached step with missing run
|
|
376
406
|
|
|
377
407
|
Force re-execution:
|
|
378
408
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { Workspace } from '../../engine/workspace.js';
|
|
4
|
+
import { getGlobalOptions } from '../utils.js';
|
|
5
|
+
export function registerCleanCommand(program) {
|
|
6
|
+
program
|
|
7
|
+
.command('clean')
|
|
8
|
+
.description('Remove all workspaces')
|
|
9
|
+
.action(async (_options, cmd) => {
|
|
10
|
+
const { workdir } = getGlobalOptions(cmd);
|
|
11
|
+
const workdirRoot = resolve(workdir);
|
|
12
|
+
const names = await Workspace.list(workdirRoot);
|
|
13
|
+
if (names.length === 0) {
|
|
14
|
+
console.log(chalk.gray('No workspaces to clean.'));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
for (const name of names) {
|
|
18
|
+
await Workspace.remove(workdirRoot, name);
|
|
19
|
+
}
|
|
20
|
+
console.log(chalk.green(`Removed ${names.length} workspace${names.length > 1 ? 's' : ''}.`));
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { cp } from 'node:fs/promises';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { Workspace } from '../../engine/workspace.js';
|
|
6
|
+
import { StateManager } from '../state.js';
|
|
7
|
+
import { getGlobalOptions } from '../utils.js';
|
|
8
|
+
export function registerExportCommand(program) {
|
|
9
|
+
program
|
|
10
|
+
.command('export')
|
|
11
|
+
.description('Extract artifacts from a step run to the host filesystem')
|
|
12
|
+
.argument('<workspace>', 'Workspace name')
|
|
13
|
+
.argument('<step>', 'Step ID')
|
|
14
|
+
.argument('<dest>', 'Destination directory')
|
|
15
|
+
.action(async (workspaceName, stepId, dest, cmd) => {
|
|
16
|
+
const { workdir } = getGlobalOptions(cmd);
|
|
17
|
+
const workdirRoot = resolve(workdir);
|
|
18
|
+
const workspace = await Workspace.open(workdirRoot, workspaceName);
|
|
19
|
+
const state = new StateManager(workspace.root);
|
|
20
|
+
await state.load();
|
|
21
|
+
const stepState = state.getStep(stepId);
|
|
22
|
+
if (!stepState) {
|
|
23
|
+
console.error(chalk.red(`No run found for step: ${stepId}`));
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const artifactsPath = workspace.runArtifactsPath(stepState.runId);
|
|
28
|
+
const destPath = resolve(dest);
|
|
29
|
+
await cp(artifactsPath, destPath, { recursive: true });
|
|
30
|
+
console.log(chalk.green(`Exported artifacts from ${stepId} to ${destPath}`));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { Workspace } from '../../engine/workspace.js';
|
|
6
|
+
import { StateManager } from '../state.js';
|
|
7
|
+
import { getGlobalOptions } from '../utils.js';
|
|
8
|
+
export function registerInspectCommand(program) {
|
|
9
|
+
program
|
|
10
|
+
.command('inspect')
|
|
11
|
+
.description('Show metadata from the last run of a step')
|
|
12
|
+
.argument('<workspace>', 'Workspace name')
|
|
13
|
+
.argument('<step>', 'Step ID')
|
|
14
|
+
.action(async (workspaceName, stepId, _options, cmd) => {
|
|
15
|
+
const { workdir, json } = getGlobalOptions(cmd);
|
|
16
|
+
const workdirRoot = resolve(workdir);
|
|
17
|
+
const workspace = await Workspace.open(workdirRoot, workspaceName);
|
|
18
|
+
const state = new StateManager(workspace.root);
|
|
19
|
+
await state.load();
|
|
20
|
+
const stepState = state.getStep(stepId);
|
|
21
|
+
if (!stepState) {
|
|
22
|
+
console.error(chalk.red(`No run found for step: ${stepId}`));
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const metaPath = join(workspace.runPath(stepState.runId), 'meta.json');
|
|
27
|
+
try {
|
|
28
|
+
const content = await readFile(metaPath, 'utf8');
|
|
29
|
+
if (json) {
|
|
30
|
+
console.log(content);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
const meta = JSON.parse(content);
|
|
34
|
+
console.log(chalk.bold(`\nRun: ${chalk.cyan(meta.runId)}`));
|
|
35
|
+
console.log(` Step: ${meta.stepId}${meta.stepName ? ` (${meta.stepName})` : ''}`);
|
|
36
|
+
console.log(` Status: ${meta.status === 'success' ? chalk.green('success') : chalk.red('failure')}`);
|
|
37
|
+
console.log(` Image: ${meta.image}`);
|
|
38
|
+
console.log(` Command: ${meta.cmd.join(' ')}`);
|
|
39
|
+
console.log(` Duration: ${meta.durationMs}ms`);
|
|
40
|
+
console.log(` Started: ${meta.startedAt}`);
|
|
41
|
+
console.log(` Finished: ${meta.finishedAt}`);
|
|
42
|
+
console.log(` Exit code: ${meta.exitCode}`);
|
|
43
|
+
console.log(` Fingerprint: ${meta.fingerprint}`);
|
|
44
|
+
if (meta.env && Object.keys(meta.env).length > 0) {
|
|
45
|
+
console.log(` Env: ${JSON.stringify(meta.env)}`);
|
|
46
|
+
}
|
|
47
|
+
if (meta.inputs && meta.inputs.length > 0) {
|
|
48
|
+
console.log(` Inputs: ${JSON.stringify(meta.inputs)}`);
|
|
49
|
+
}
|
|
50
|
+
console.log();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
console.error(chalk.red(`No metadata found for run: ${stepState.runId}`));
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { Workspace } from '../../engine/workspace.js';
|
|
4
|
+
import { dirSize, formatSize, getGlobalOptions } from '../utils.js';
|
|
5
|
+
export function registerListCommand(program) {
|
|
6
|
+
program
|
|
7
|
+
.command('list')
|
|
8
|
+
.alias('ls')
|
|
9
|
+
.description('List workspaces')
|
|
10
|
+
.action(async (_options, cmd) => {
|
|
11
|
+
const { workdir, json } = getGlobalOptions(cmd);
|
|
12
|
+
const workdirRoot = resolve(workdir);
|
|
13
|
+
const names = await Workspace.list(workdirRoot);
|
|
14
|
+
if (json) {
|
|
15
|
+
console.log(JSON.stringify(names));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (names.length === 0) {
|
|
19
|
+
console.log(chalk.gray('No workspaces found.'));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const rows = [];
|
|
23
|
+
for (const name of names) {
|
|
24
|
+
const ws = await Workspace.open(workdirRoot, name);
|
|
25
|
+
const runs = await ws.listRuns();
|
|
26
|
+
const caches = await ws.listCaches();
|
|
27
|
+
const wsSize = await dirSize(ws.root);
|
|
28
|
+
rows.push({ name, runs: runs.length, caches: caches.length, size: formatSize(wsSize) });
|
|
29
|
+
}
|
|
30
|
+
const nameWidth = Math.max('WORKSPACE'.length, ...rows.map(r => r.name.length));
|
|
31
|
+
const sizeWidth = Math.max('SIZE'.length, ...rows.map(r => r.size.length));
|
|
32
|
+
const header = `${'WORKSPACE'.padEnd(nameWidth)} RUNS CACHES ${'SIZE'.padStart(sizeWidth)}`;
|
|
33
|
+
console.log(chalk.bold(header));
|
|
34
|
+
for (const row of rows) {
|
|
35
|
+
console.log(`${row.name.padEnd(nameWidth)} ${String(row.runs).padStart(4)} ${String(row.caches).padStart(6)} ${row.size.padStart(sizeWidth)}`);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { Workspace } from '../../engine/workspace.js';
|
|
6
|
+
import { StateManager } from '../state.js';
|
|
7
|
+
import { getGlobalOptions } from '../utils.js';
|
|
8
|
+
export function registerLogsCommand(program) {
|
|
9
|
+
program
|
|
10
|
+
.command('logs')
|
|
11
|
+
.description('Show logs from the last run of a step')
|
|
12
|
+
.argument('<workspace>', 'Workspace name')
|
|
13
|
+
.argument('<step>', 'Step ID')
|
|
14
|
+
.option('-s, --stream <stream>', 'Show only stdout or stderr', 'both')
|
|
15
|
+
.action(async (workspaceName, stepId, options, cmd) => {
|
|
16
|
+
const { workdir } = getGlobalOptions(cmd);
|
|
17
|
+
const workdirRoot = resolve(workdir);
|
|
18
|
+
const workspace = await Workspace.open(workdirRoot, workspaceName);
|
|
19
|
+
const state = new StateManager(workspace.root);
|
|
20
|
+
await state.load();
|
|
21
|
+
const stepState = state.getStep(stepId);
|
|
22
|
+
if (!stepState) {
|
|
23
|
+
console.error(chalk.red(`No run found for step: ${stepId}`));
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const runDir = workspace.runPath(stepState.runId);
|
|
28
|
+
if (options.stream === 'both' || options.stream === 'stdout') {
|
|
29
|
+
try {
|
|
30
|
+
const stdout = await readFile(join(runDir, 'stdout.log'), 'utf8');
|
|
31
|
+
if (stdout) {
|
|
32
|
+
process.stdout.write(stdout);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// No stdout log
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (options.stream === 'both' || options.stream === 'stderr') {
|
|
40
|
+
try {
|
|
41
|
+
const stderr = await readFile(join(runDir, 'stderr.log'), 'utf8');
|
|
42
|
+
if (stderr) {
|
|
43
|
+
if (options.stream === 'both') {
|
|
44
|
+
console.error(chalk.red('── stderr ──'));
|
|
45
|
+
}
|
|
46
|
+
process.stderr.write(stderr);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// No stderr log
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { Workspace } from '../../engine/workspace.js';
|
|
4
|
+
import { StateManager } from '../state.js';
|
|
5
|
+
import { getGlobalOptions } from '../utils.js';
|
|
6
|
+
export function registerPruneCommand(program) {
|
|
7
|
+
program
|
|
8
|
+
.command('prune')
|
|
9
|
+
.description('Remove old runs not referenced by current state')
|
|
10
|
+
.argument('<workspace>', 'Workspace name')
|
|
11
|
+
.action(async (workspaceName, _options, cmd) => {
|
|
12
|
+
const { workdir } = getGlobalOptions(cmd);
|
|
13
|
+
const workdirRoot = resolve(workdir);
|
|
14
|
+
const workspace = await Workspace.open(workdirRoot, workspaceName);
|
|
15
|
+
const state = new StateManager(workspace.root);
|
|
16
|
+
await state.load();
|
|
17
|
+
const activeIds = state.activeRunIds();
|
|
18
|
+
const removed = await workspace.pruneRuns(activeIds);
|
|
19
|
+
if (removed === 0) {
|
|
20
|
+
console.log(chalk.gray('No old runs to remove.'));
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
console.log(chalk.green(`Removed ${removed} old run${removed > 1 ? 's' : ''}.`));
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { Workspace } from '../../engine/workspace.js';
|
|
5
|
+
import { getGlobalOptions } from '../utils.js';
|
|
6
|
+
export function registerRmCommand(program) {
|
|
7
|
+
program
|
|
8
|
+
.command('rm')
|
|
9
|
+
.description('Remove one or more workspaces')
|
|
10
|
+
.argument('<workspace...>', 'Workspace names to remove')
|
|
11
|
+
.action(async (workspaces, _options, cmd) => {
|
|
12
|
+
const { workdir } = getGlobalOptions(cmd);
|
|
13
|
+
const workdirRoot = resolve(workdir);
|
|
14
|
+
const existing = await Workspace.list(workdirRoot);
|
|
15
|
+
for (const name of workspaces) {
|
|
16
|
+
if (!existing.includes(name)) {
|
|
17
|
+
console.error(chalk.red(`Workspace not found: ${name}`));
|
|
18
|
+
process.exitCode = 1;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
for (const name of workspaces) {
|
|
23
|
+
await Workspace.remove(workdirRoot, name);
|
|
24
|
+
console.log(chalk.green(`Removed ${name}`));
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { DockerCliExecutor } from '../../engine/docker-executor.js';
|
|
3
|
+
import { PipelineLoader } from '../pipeline-loader.js';
|
|
4
|
+
import { PipelineRunner } from '../pipeline-runner.js';
|
|
5
|
+
import { ConsoleReporter, InteractiveReporter } from '../reporter.js';
|
|
6
|
+
import { getGlobalOptions } from '../utils.js';
|
|
7
|
+
export function registerRunCommand(program) {
|
|
8
|
+
program
|
|
9
|
+
.command('run')
|
|
10
|
+
.description('Execute a pipeline')
|
|
11
|
+
.argument('<pipeline>', 'Pipeline file to execute (JSON or YAML)')
|
|
12
|
+
.option('-w, --workspace <name>', 'Workspace name (for caching)')
|
|
13
|
+
.option('-f, --force [steps]', 'Skip cache for all steps, or a comma-separated list (e.g. --force step1,step2)')
|
|
14
|
+
.option('--dry-run', 'Validate pipeline and show what would run without executing')
|
|
15
|
+
.option('--verbose', 'Stream container logs in real-time (interactive mode)')
|
|
16
|
+
.action(async (pipelineFile, options, cmd) => {
|
|
17
|
+
const { workdir, json } = getGlobalOptions(cmd);
|
|
18
|
+
const workdirRoot = resolve(workdir);
|
|
19
|
+
const loader = new PipelineLoader();
|
|
20
|
+
const runtime = new DockerCliExecutor();
|
|
21
|
+
const reporter = json ? new ConsoleReporter() : new InteractiveReporter({ verbose: options.verbose });
|
|
22
|
+
const runner = new PipelineRunner(loader, runtime, reporter, workdirRoot);
|
|
23
|
+
try {
|
|
24
|
+
const force = options.force === true
|
|
25
|
+
? true
|
|
26
|
+
: (typeof options.force === 'string' ? options.force.split(',') : undefined);
|
|
27
|
+
await runner.run(pipelineFile, { workspace: options.workspace, force, dryRun: options.dryRun });
|
|
28
|
+
if (json) {
|
|
29
|
+
console.log('Pipeline completed');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (json) {
|
|
34
|
+
console.error('Pipeline failed:', error instanceof Error ? error.message : error);
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { Workspace } from '../../engine/workspace.js';
|
|
5
|
+
import { StateManager } from '../state.js';
|
|
6
|
+
import { dirSize, formatSize, getGlobalOptions } from '../utils.js';
|
|
7
|
+
export function registerShowCommand(program) {
|
|
8
|
+
program
|
|
9
|
+
.command('show')
|
|
10
|
+
.description('Show steps and runs in a workspace')
|
|
11
|
+
.argument('<workspace>', 'Workspace name')
|
|
12
|
+
.action(async (workspaceName, _options, cmd) => {
|
|
13
|
+
const { workdir, json } = getGlobalOptions(cmd);
|
|
14
|
+
const workdirRoot = resolve(workdir);
|
|
15
|
+
const workspace = await Workspace.open(workdirRoot, workspaceName);
|
|
16
|
+
const state = new StateManager(workspace.root);
|
|
17
|
+
await state.load();
|
|
18
|
+
const steps = state.listSteps();
|
|
19
|
+
if (steps.length === 0) {
|
|
20
|
+
console.log(chalk.gray('No runs found in this workspace.'));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const rows = [];
|
|
24
|
+
let totalSize = 0;
|
|
25
|
+
for (const { stepId, runId } of steps) {
|
|
26
|
+
const metaPath = join(workspace.runPath(runId), 'meta.json');
|
|
27
|
+
try {
|
|
28
|
+
const content = await readFile(metaPath, 'utf8');
|
|
29
|
+
const meta = JSON.parse(content);
|
|
30
|
+
const artifactBytes = await dirSize(workspace.runArtifactsPath(runId));
|
|
31
|
+
totalSize += artifactBytes;
|
|
32
|
+
rows.push({
|
|
33
|
+
stepId,
|
|
34
|
+
stepName: meta.stepName,
|
|
35
|
+
status: meta.status,
|
|
36
|
+
duration: `${meta.durationMs}ms`,
|
|
37
|
+
size: formatSize(artifactBytes),
|
|
38
|
+
date: meta.finishedAt.replace('T', ' ').replace(/\.\d+Z$/, ''),
|
|
39
|
+
runId
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
rows.push({ stepId, status: 'unknown', duration: '-', size: '-', date: '-', runId });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (json) {
|
|
47
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const stepWidth = Math.max('STEP'.length, ...rows.map(r => (r.stepName ? `${r.stepId} (${r.stepName})` : r.stepId).length));
|
|
51
|
+
const statusWidth = Math.max('STATUS'.length, ...rows.map(r => r.status.length));
|
|
52
|
+
const durationWidth = Math.max('DURATION'.length, ...rows.map(r => r.duration.length));
|
|
53
|
+
const sizeWidth = Math.max('SIZE'.length, ...rows.map(r => r.size.length));
|
|
54
|
+
const dateWidth = Math.max('FINISHED'.length, ...rows.map(r => r.date.length));
|
|
55
|
+
console.log(chalk.bold(`${'STEP'.padEnd(stepWidth)} ${'STATUS'.padEnd(statusWidth)} ${'DURATION'.padStart(durationWidth)} ${'SIZE'.padStart(sizeWidth)} ${'FINISHED'.padEnd(dateWidth)}`));
|
|
56
|
+
for (const row of rows) {
|
|
57
|
+
const stepLabel = row.stepName ? `${row.stepId} (${row.stepName})` : row.stepId;
|
|
58
|
+
const statusText = row.status === 'success' ? chalk.green(row.status) : chalk.red(row.status);
|
|
59
|
+
const statusPad = statusWidth + (statusText.length - row.status.length);
|
|
60
|
+
const cols = [
|
|
61
|
+
stepLabel.padEnd(stepWidth),
|
|
62
|
+
statusText.padEnd(statusPad),
|
|
63
|
+
row.duration.padStart(durationWidth),
|
|
64
|
+
row.size.padStart(sizeWidth),
|
|
65
|
+
row.date.padEnd(dateWidth)
|
|
66
|
+
];
|
|
67
|
+
console.log(cols.join(' '));
|
|
68
|
+
}
|
|
69
|
+
if (rows.length > 1) {
|
|
70
|
+
console.log(chalk.gray(`\n Total: ${formatSize(totalSize)}`));
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import 'dotenv/config';
|
|
3
3
|
import process from 'node:process';
|
|
4
|
-
import { resolve } from 'node:path';
|
|
5
|
-
import chalk from 'chalk';
|
|
6
4
|
import { Command } from 'commander';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
5
|
+
import { registerRunCommand } from './commands/run.js';
|
|
6
|
+
import { registerLogsCommand } from './commands/logs.js';
|
|
7
|
+
import { registerInspectCommand } from './commands/inspect.js';
|
|
8
|
+
import { registerExportCommand } from './commands/export.js';
|
|
9
|
+
import { registerShowCommand } from './commands/show.js';
|
|
10
|
+
import { registerPruneCommand } from './commands/prune.js';
|
|
11
|
+
import { registerListCommand } from './commands/list.js';
|
|
12
|
+
import { registerRmCommand } from './commands/rm.js';
|
|
13
|
+
import { registerCleanCommand } from './commands/clean.js';
|
|
15
14
|
async function main() {
|
|
16
15
|
const program = new Command();
|
|
17
16
|
program
|
|
@@ -20,101 +19,15 @@ async function main() {
|
|
|
20
19
|
.version('0.1.0')
|
|
21
20
|
.option('--workdir <path>', 'Workspaces root directory', process.env.PIPEX_WORKDIR ?? './workdir')
|
|
22
21
|
.option('--json', 'Output structured JSON logs');
|
|
23
|
-
program
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const loader = new PipelineLoader();
|
|
33
|
-
const runtime = new DockerCliExecutor();
|
|
34
|
-
const reporter = json ? new ConsoleReporter() : new InteractiveReporter();
|
|
35
|
-
const runner = new PipelineRunner(loader, runtime, reporter, workdirRoot);
|
|
36
|
-
try {
|
|
37
|
-
const force = options.force === true
|
|
38
|
-
? true
|
|
39
|
-
: (typeof options.force === 'string' ? options.force.split(',') : undefined);
|
|
40
|
-
await runner.run(pipelineFile, { workspace: options.workspace, force });
|
|
41
|
-
if (json) {
|
|
42
|
-
console.log('Pipeline completed');
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
catch (error) {
|
|
46
|
-
if (json) {
|
|
47
|
-
console.error('Pipeline failed:', error instanceof Error ? error.message : error);
|
|
48
|
-
}
|
|
49
|
-
throw error;
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
program
|
|
53
|
-
.command('list')
|
|
54
|
-
.alias('ls')
|
|
55
|
-
.description('List workspaces')
|
|
56
|
-
.action(async (_options, cmd) => {
|
|
57
|
-
const { workdir, json } = getGlobalOptions(cmd);
|
|
58
|
-
const workdirRoot = resolve(workdir);
|
|
59
|
-
const names = await Workspace.list(workdirRoot);
|
|
60
|
-
if (json) {
|
|
61
|
-
console.log(JSON.stringify(names));
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
if (names.length === 0) {
|
|
65
|
-
console.log(chalk.gray('No workspaces found.'));
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
const rows = [];
|
|
69
|
-
for (const name of names) {
|
|
70
|
-
const ws = await Workspace.open(workdirRoot, name);
|
|
71
|
-
const artifacts = await ws.listArtifacts();
|
|
72
|
-
const caches = await ws.listCaches();
|
|
73
|
-
rows.push({ name, artifacts: artifacts.length, caches: caches.length });
|
|
74
|
-
}
|
|
75
|
-
const nameWidth = Math.max('WORKSPACE'.length, ...rows.map(r => r.name.length));
|
|
76
|
-
const header = `${'WORKSPACE'.padEnd(nameWidth)} ARTIFACTS CACHES`;
|
|
77
|
-
console.log(chalk.bold(header));
|
|
78
|
-
for (const row of rows) {
|
|
79
|
-
console.log(`${row.name.padEnd(nameWidth)} ${String(row.artifacts).padStart(9)} ${String(row.caches).padStart(6)}`);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
program
|
|
83
|
-
.command('rm')
|
|
84
|
-
.description('Remove one or more workspaces')
|
|
85
|
-
.argument('<workspace...>', 'Workspace names to remove')
|
|
86
|
-
.action(async (workspaces, _options, cmd) => {
|
|
87
|
-
const { workdir } = getGlobalOptions(cmd);
|
|
88
|
-
const workdirRoot = resolve(workdir);
|
|
89
|
-
const existing = await Workspace.list(workdirRoot);
|
|
90
|
-
for (const name of workspaces) {
|
|
91
|
-
if (!existing.includes(name)) {
|
|
92
|
-
console.error(chalk.red(`Workspace not found: ${name}`));
|
|
93
|
-
process.exitCode = 1;
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
for (const name of workspaces) {
|
|
98
|
-
await Workspace.remove(workdirRoot, name);
|
|
99
|
-
console.log(chalk.green(`Removed ${name}`));
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
program
|
|
103
|
-
.command('clean')
|
|
104
|
-
.description('Remove all workspaces')
|
|
105
|
-
.action(async (_options, cmd) => {
|
|
106
|
-
const { workdir } = getGlobalOptions(cmd);
|
|
107
|
-
const workdirRoot = resolve(workdir);
|
|
108
|
-
const names = await Workspace.list(workdirRoot);
|
|
109
|
-
if (names.length === 0) {
|
|
110
|
-
console.log(chalk.gray('No workspaces to clean.'));
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
for (const name of names) {
|
|
114
|
-
await Workspace.remove(workdirRoot, name);
|
|
115
|
-
}
|
|
116
|
-
console.log(chalk.green(`Removed ${names.length} workspace${names.length > 1 ? 's' : ''}.`));
|
|
117
|
-
});
|
|
22
|
+
registerRunCommand(program);
|
|
23
|
+
registerLogsCommand(program);
|
|
24
|
+
registerInspectCommand(program);
|
|
25
|
+
registerExportCommand(program);
|
|
26
|
+
registerShowCommand(program);
|
|
27
|
+
registerPruneCommand(program);
|
|
28
|
+
registerListCommand(program);
|
|
29
|
+
registerRmCommand(program);
|
|
30
|
+
registerCleanCommand(program);
|
|
118
31
|
await program.parseAsync();
|
|
119
32
|
}
|
|
120
33
|
try {
|