CLI Plugin Architecture

Understanding MediaProc's universal CLI architecture and how plugins integrate.

Universal CLI Design

MediaProc's CLI is built on a universal command pattern that provides consistency across all media types:

mediaproc <plugin> <command> <input> [options]

This pattern makes the CLI predictable and easy to learn.


Command Pattern

Structure

Every command follows the same structure:

mediaproc [global-options] <plugin> <command> <input...> [command-options]

Components:

  • mediaproc - The CLI binary
  • [global-options] - Options affecting the entire CLI (--verbose, --config)
  • <plugin> - The plugin namespace (image, video, audio, etc.)
  • <command> - The specific command to run (resize, convert, trim, etc.)
  • <input...> - One or more input files or patterns
  • [command-options] - Command-specific flags and options

Examples Across Plugins

Image Plugin:

mediaproc image resize photo.jpg --width 1920 --height 1080

Video Plugin:

mediaproc video transcode movie.mp4 --codec h264 --preset medium

Audio Plugin:

mediaproc audio normalize song.mp3 --loudness -16

Pattern is identical:

mediaproc <plugin> <command> <file> --<option> <value>

Global Options

Options available for all commands:

--help, -h

Display help information.

# CLI help
mediaproc --help

# Plugin help
mediaproc image --help

# Command help
mediaproc image resize --help

--version, -V

Show version information.

mediaproc --version
# @mediaproc/cli v1.0.0

--verbose, -v

Enable detailed output.

mediaproc image resize photo.jpg --width 1920 --verbose
# Shows: FFmpeg/Sharp output, file paths, timing

--dry-run

Preview the command without executing.

mediaproc video transcode video.mp4 --codec h264 --dry-run
# Would run: ffmpeg -i video.mp4 -c:v h264 ...

--config

Specify custom configuration file.

mediaproc --config ./my-config.json image resize photo.jpg --width 1920

--output, -o

Specify output location (available on most commands).

# Single file
mediaproc image resize photo.jpg --output resized.jpg

# Directory for batch processing
mediaproc image resize *.jpg --output ./resized/

Plugin CLI Integration

Auto-Generated CLI

Each plugin automatically generates its own CLI binary:

# Via main CLI
mediaproc image resize photo.jpg --width 1920

# Via plugin CLI
mediaproc-image resize photo.jpg --width 1920

Package Structure:

{
  "name": "@mediaproc/plugin-image",
  "bin": {
    "mediaproc-image": "./bin/cli.js"
  }
}

Plugin Registration

Plugins register with the main CLI:

// plugins/image/src/register.ts

import { Plugin } from "@mediaproc/core";
import { resizeCommand } from "./commands/resize";
import { convertCommand } from "./commands/convert";

export const plugin: Plugin = {
  name: "image",
  version: "1.0.0",
  description: "Image processing plugin powered by Sharp",

  commands: [
    resizeCommand,
    convertCommand,
    // ... more commands
  ],

  register(api) {
    api.registerPlugin(this);

    // Register all commands
    this.commands.forEach((cmd) => {
      api.registerCommand(cmd);
    });
  },
};

Command Handler Interface

All command handlers follow the same interface:

interface Command {
  name: string;
  description: string;
  options: CommandOption[];
  examples?: string[];

  handler(context: CommandContext): Promise<CommandResult>;
}

interface CommandContext {
  // Input files
  input: string[];

  // Parsed options
  options: Record<string, any>;

  // Plugin API access
  api: PluginAPI;

  // Logger
  logger: Logger;

  // Configuration
  config: Config;
}

interface CommandResult {
  success: boolean;
  processed?: number;
  failed?: number;
  message?: string;
}

Input Handling

File Patterns

The CLI supports multiple input patterns:

Single File:

mediaproc image resize photo.jpg --width 1920

Multiple Files:

mediaproc image resize photo1.jpg photo2.jpg photo3.jpg --width 1920

Glob Patterns:

# All JPGs in current directory
mediaproc image resize *.jpg --width 1920

# All JPGs in subdirectories
mediaproc image resize **/*.jpg --width 1920

# Multiple patterns
mediaproc image resize *.jpg *.png --width 1920

From stdin:

find . -name "*.jpg" | mediaproc image resize --stdin --width 1920

Input Validation

CLI validates inputs before processing:

async function validateInputs(files: string[]): Promise<void> {
  for (const file of files) {
    // Check if file exists
    if (!(await fileExists(file))) {
      throw new Error(`File not found: ${file}`);
    }

    // Check if readable
    if (!(await isReadable(file))) {
      throw new Error(`Cannot read file: ${file}`);
    }

    // Check file extension
    if (!hasValidExtension(file, allowedExtensions)) {
      throw new Error(`Invalid file type: ${file}`);
    }
  }
}

Output Handling

Output Strategies

The CLI supports multiple output strategies:

1. Automatic Naming (Default):

# input: photo.jpg → output: photo-resized.jpg
mediaproc image resize photo.jpg --width 1920

2. Explicit Output:

# Specify exact output filename
mediaproc image resize photo.jpg --width 1920 --output result.jpg

3. Directory Output:

# All outputs go to directory
mediaproc image resize *.jpg --width 1920 --output ./resized/

4. In-Place (Dangerous):

# Overwrite original file
mediaproc image resize photo.jpg --width 1920 --in-place

Naming Conventions

Default output naming patterns:

function generateOutputName(input: string, command: string): string {
  const { name, ext } = path.parse(input);

  // Pattern: <name>-<command><ext>
  return `${name}-${command}${ext}`;
}

// Examples:
// photo.jpg → photo-resized.jpg
// video.mp4 → video-transcoded.mp4
// song.mp3 → song-normalized.mp3

Option Parsing

Option Types

The CLI supports multiple option types:

Boolean Flags:

--verbose
--dry-run
--overwrite

String Options:

--format webp
--codec h264
--output result.jpg

Number Options:

--width 1920
--quality 85
--bitrate 128

Array Options:

--formats webp jpg png
--sizes 1920 1280 800

Option Validation

Options are validated before execution:

interface CommandOption {
  name: string;
  alias?: string;
  type: 'boolean' | 'string' | 'number' | 'array';
  required?: boolean;
  default?: any;
  choices?: any[];
  validate?: (value: any) => boolean;
}

// Example validation
{
  name: 'quality',
  type: 'number',
  validate: (v) => v >= 1 && v <= 100,
  default: 85
}

Error Handling

Error Types

The CLI handles different error categories:

1. User Errors (Exit code 1):

# Missing required option
mediaproc image resize photo.jpg
# Error: Either --width or --height required

# Invalid option value
mediaproc image resize photo.jpg --quality 150
# Error: Quality must be between 1-100

2. System Errors (Exit code 2):

# File not found
mediaproc image resize missing.jpg --width 1920
# Error: File not found: missing.jpg

# Permission denied
mediaproc image resize protected.jpg --width 1920
# Error: Permission denied: protected.jpg

3. Processing Errors (Exit code 3):

# Corrupted file
mediaproc image resize corrupted.jpg --width 1920
# Error: Failed to decode image: corrupted.jpg

# Unsupported format
mediaproc video transcode movie.xyz --codec h264
# Error: Unsupported format: xyz

Error Messages

Error messages are user-friendly and actionable:

class CLIError extends Error {
  constructor(
    message: string,
    public code: number,
    public suggestions?: string[],
  ) {
    super(message);
  }
}

// Example usage
throw new CLIError("File not found: photo.jpg", 1, [
  "Check if the file path is correct",
  "Ensure the file exists in the current directory",
  "Try using an absolute path",
]);

Output:

Error: File not found: photo.jpg Suggestions: • Check if the file path is correct • Ensure the file exists in the current directory • Try using an absolute path

Progress Reporting

Progress Bars

For long-running operations:

const progress = context.api.createProgressBar({
  total: files.length,
  format: "Processing [{bar}] {percentage}% | {value}/{total} files",
});

for (const file of files) {
  await processFile(file);
  progress.increment();
}

progress.stop();

Output:

Processing [====================] 100% | 25/25 files

Status Messages

Real-time status updates:

mediaproc image resize *.jpg --width 1920 --verbose

# Output:
Processing 15 files...
  [1/15] photo1.jpg → photo1-resized.jpg (1.2 MB)
  [2/15] photo2.jpg → photo2-resized.jpg (1.5 MB)
  [3/15] photo3.jpg → photo3-resized.jpg (1.1 MB)
  ...
✓ Successfully processed 15 files in 3.2s

Batch Processing

Parallel Execution

Process multiple files in parallel:

async function processBatch(
  files: string[],
  handler: ProcessHandler,
  concurrency: number = 4,
): Promise<Result[]> {
  const chunks = chunkArray(files, concurrency);
  const results: Result[] = [];

  for (const chunk of chunks) {
    const batchResults = await Promise.all(chunk.map((file) => handler(file)));
    results.push(...batchResults);
  }

  return results;
}

Batch Statistics

Summary statistics after batch processing:

mediaproc image resize *.jpg --width 1920

# Output:
Processing 100 files...

Summary:
  ✓ Processed: 97 files
  ✗ Failed: 3 files
  ⏱ Duration: 12.5s
  📦 Total size: 245 MB → 89 MB (63% reduction)

Configuration Integration

Configuration Cascade

Configuration merges from multiple sources:

1. Built-in defaults ↓ 2. Global config (~/.mediaproc/config.json) ↓ 3. Project config (./mediaproc.json) ↓ 4. Environment variables (MEDIAPROC_*) ↓ 5. Command-line options

Example Configuration

{
  "image": {
    "defaultFormat": "webp",
    "quality": 85,
    "resize": {
      "width": 1920,
      "fit": "cover"
    }
  },

  "video": {
    "codec": "h264",
    "preset": "medium",
    "bitrate": "5000k"
  },

  "output": {
    "directory": "./output",
    "overwrite": false,
    "preserveStructure": true
  }
}

Using Configuration:

# Uses configured defaults
mediaproc image resize photo.jpg
# Applies: format=webp, quality=85, width=1920

# Override via CLI
mediaproc image resize photo.jpg --quality 95
# Applies: format=webp, quality=95, width=1920

Plugin Discovery

Discovery Process

The CLI discovers plugins automatically:

async function discoverPlugins(): Promise<Plugin[]> {
  const discovered: Plugin[] = [];

  // 1. Built-in plugins (monorepo)
  const builtIn = await discoverBuiltInPlugins("./plugins");
  discovered.push(...builtIn);

  // 2. Installed npm packages
  const installed = await discoverNpmPlugins("node_modules");
  discovered.push(...installed);

  // 3. Custom plugin directories
  const custom = await discoverCustomPlugins(config.pluginDirs);
  discovered.push(...custom);

  return discovered;
}

Plugin Loading

Plugins are loaded lazily:

class PluginManager {
  private cache = new Map<string, Plugin>();

  async load(pluginName: string): Promise<Plugin> {
    // Check cache
    if (this.cache.has(pluginName)) {
      return this.cache.get(pluginName)!;
    }

    // Dynamic import
    const module = await import(`./plugins/${pluginName}`);
    const plugin = module.plugin;

    // Validate
    this.validate(plugin);

    // Cache and return
    this.cache.set(pluginName, plugin);
    return plugin;
  }
}

Extensibility

Custom Commands

Add custom commands to any plugin:

// mediaproc.config.js

module.exports = {
  plugins: {
    image: {
      commands: {
        // Add custom command
        watermark: {
          handler: "./custom/watermark.js",
          description: "Add watermark to images",
        },
      },
    },
  },
};

Command Aliases

Create command aliases:

{
  "aliases": {
    "img": "image",
    "vid": "video",
    "aud": "audio",
    "opt": "optimize"
  }
}

Usage:

# Instead of:
mediaproc image optimize photo.jpg

# Use alias:
mediaproc img opt photo.jpg

Performance Optimization

Caching

The CLI caches expensive operations:

// Plugin metadata cache
const metadataCache = new Map<string, Metadata>();

// File hash cache for duplicate detection
const hashCache = new Map<string, string>();

// Processing result cache
const resultCache = new Map<string, Result>();

Incremental Processing

Only process changed files:

# Skip already processed files
mediaproc image resize *.jpg --incremental

# Output:
Scanning 100 files...
  ✓ 75 files already processed (skipped)
  → 25 files to process

Testing

CLI Testing

Test CLI commands end-to-end:

import { runCLI } from "@mediaproc/test-utils";

test("should resize image", async () => {
  const result = await runCLI([
    "image",
    "resize",
    "test.jpg",
    "--width",
    "1920",
  ]);

  expect(result.exitCode).toBe(0);
  expect(result.stdout).toContain("Successfully processed");
});

Mock Plugins

Test plugin system:

const mockPlugin = {
  name: "test-plugin",
  commands: [
    {
      name: "test-command",
      handler: jest.fn().mockResolvedValue({ success: true }),
    },
  ],
  register: jest.fn(),
};

const cli = new CLI({ plugins: [mockPlugin] });
await cli.execute(["test-plugin", "test-command"]);

expect(mockPlugin.commands[0].handler).toHaveBeenCalled();


Learn More

Found an issue? Help us improve this page.

Edit on GitHub →