@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 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 artifact/cache counts)
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
- | `list` (alias `ls`) | List workspaces |
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 artifact
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 { Workspace } from '../engine/workspace.js';
8
- import { DockerCliExecutor } from '../engine/docker-executor.js';
9
- import { PipelineLoader } from './pipeline-loader.js';
10
- import { PipelineRunner } from './pipeline-runner.js';
11
- import { ConsoleReporter, InteractiveReporter } from './reporter.js';
12
- function getGlobalOptions(cmd) {
13
- return cmd.optsWithGlobals();
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
- .command('run')
25
- .description('Execute a pipeline')
26
- .argument('<pipeline>', 'Pipeline file to execute (JSON or YAML)')
27
- .option('-w, --workspace <name>', 'Workspace name (for caching)')
28
- .option('-f, --force [steps]', 'Skip cache for all steps, or a comma-separated list (e.g. --force step1,step2)')
29
- .action(async (pipelineFile, options, cmd) => {
30
- const { workdir, json } = getGlobalOptions(cmd);
31
- const workdirRoot = resolve(workdir);
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 {