Tasker/NOTES.md
Funky Waddle 3ac2894fd8
Some checks are pending
CI / tasker (8.2) (push) Waiting to run
CI / tasker (8.3) (push) Waiting to run
feat: complete Tasker project implementation, documentation, and examples
2026-02-22 01:14:22 -06:00

12 KiB

`# Tasker: CLI Tool Management System

Tasker is a CLI Tool Management system built originally for use in the Phred Framework, but opened to the public to install in any codebase. It is designed to be the central nervous system for all Phred CLI activities while maintaining extreme portability for standalone library use.

Core Vision

  • Centralized Management: A single entry point for all Phred-related CLI tasks.
  • Decoupled Architecture: Individual libraries (Pairity, Atlas, etc.) should NOT depend on Tasker. They should define their own "Actions" or "Services," and provide a thin "Command" layer that Tasker can consume.
  • DI Friendly: Built to work seamlessly with PSR-11 containers.
  • Bridge-Ready: Easily integrable into existing CLI ecosystems like symfony/console.

Architectural Decisions

1. The Bridge Pattern

To solve the conflict between "Phred Integration" and "Standalone Portability," Tasker will advocate for a split between Logic and Presentation:

  • Core Logic: Remains in the library's service layer (e.g., Pairity\Migrations\Migrator).
  • Command Wrapper: A thin class that implements a simple, common interface.
  • Bridges:
    • PhredBridge: The primary runner used by Tasker.
    • Bridges Package: A dedicated, lightweight getphred/tasker-bridges package will provide PhredBridge, SymfonyBridge, and LaminasBridge. This avoids copy-pasting code across libraries while keeping the core library decoupled from the main Tasker runner.

2. "Internal Standard" Interfaces (Proposed Implementation)

Since there is no psr/cli, Tasker will define a minimal, framework-agnostic set of interfaces. To ensure true decoupling, these interfaces should either:

  • Reside in a lightweight Contracts package (e.g., getphred/console-contracts) that both the library and Tasker depend on.
  • Be duplicated/re-implemented as a simple contract within the library if a shared package is not desired.

Proposed Command API (Hybrid Approach):

CommandInterface (In ConsoleContracts)

namespace GetPhred\ConsoleContracts;

interface CommandInterface
{
    public function getName(): string;
    public function getDescription(): string;
    
    /**
     * @return array<string, string> Name => Description
     */
    public function getArguments(): array;
    
    /**
     * @return array<string, string> Name => Description
     */
    public function getOptions(): array;

    public function execute(InputInterface $input, OutputInterface $output): int;
}

Base Command Implementation (In Tasker or a Trait)

namespace Phred\Tasker;

use GetPhred\ConsoleContracts\CommandInterface;

abstract class Command implements CommandInterface
{
    protected string $name = '';
    protected string $description = '';
    protected array $arguments = [];
    protected array $options = [];

    public function getName(): string { return $this->name; }
    public function getDescription(): string { return $this->description; }
    public function getArguments(): array { return $this->arguments; }
    public function getOptions(): array { return $this->options; }
}

User Implementation Example

class MigrateCommand extends Command
{
    protected string $name = 'db:migrate';
    protected string $description = 'Run database migrations';
    
    public function execute(InputInterface $input, OutputInterface $output): int
    {
        // Logic here
        return ExitCode::OK;
    }
}

InputInterface

namespace GetPhred\ConsoleContracts;

interface InputInterface
{
    public function getArgument(string $name, mixed $default = null): mixed;
    public function getOption(string $name, mixed $default = null): mixed;
    public function hasOption(string $name): bool;
}

OutputInterface

namespace GetPhred\ConsoleContracts;

interface OutputInterface
{
    public function write(string $message): void;
    public function writeln(string $message): void;
    public function success(string $message): void;
    public function error(string $message): void;
    public function warning(string $message): void;
}

3. Command Discovery & Injection (Proposed Implementation)

Tasker is designed to work with any PSR-11 container.

Discovery Ideas:

  1. Explicit Registration (DI Container): User adds command classes to the DI container. This takes precedence.
  2. Auto-Discovery (Composer): Tasker scans for a phred-tasker.json file in the root of installed packages or a extra.phred-tasker key in composer.json.
    "extra": {
        "phred-tasker": {
            "commands": [
                "Pairity\\Console\\Commands\\MigrateCommand",
                "Pairity\\Console\\Commands\\SeedCommand"
            ]
        }
    }
    

Injection & Translation:

Commands should use Constructor Injection for their dependencies. Tasker will use the PSR-11 container to instantiate them.

Translation is the responsibility of the Command implementation. Commands should inject a translator service (e.g., from Langer) via the container and handle translation before calling output methods. This keeps the OutputInterface lean and framework-agnostic.

namespace Pairity\Console\Commands;

use GetPhred\ConsoleContracts\CommandInterface;
use Pairity\ORM\Manager;

class MigrateCommand implements CommandInterface
{
    public function __construct(
        private readonly Manager $manager
    ) {}
    
    // ... getName, execute, etc.
}

Proposed Flow:

  1. Identify MigrateCommand as the handler.
  2. Request MigrateCommand from the PSR-11 container.
  3. The container injects the Manager (already configured in the app).
  4. Tasker calls execute().

4. Exit Code Standards

Tasker follows the sysexits.h standard for exit codes to ensure interoperability with system tools and shell scripts. To simplify development, Tasker provides an ExitCode utility with semantic constants.

Constant Code Meaning
ExitCode::OK 0 Successful termination.
ExitCode::USAGE 64 Command line usage error (invalid arguments/options).
ExitCode::DATAERR 65 Data format error.
ExitCode::NOINPUT 66 Cannot open input (file not found/unreadable).
ExitCode::UNAVAILABLE 69 Service unavailable.
ExitCode::SOFTWARE 70 Internal software error (exceptions/logic failures).
ExitCode::IOERR 74 Input/output error.
ExitCode::PERM 77 Permission denied.
ExitCode::CONFIG 78 Configuration error.

5. Interactivity & Portability

To maintain extreme portability, the OutputInterface will remain a simple pipe for strings. Interactive features (e.g., asking questions, confirmation prompts) will NOT be part of the core ConsoleContracts\OutputInterface.

Instead, Tasker will define an optional InteractionInterface (or similar helper) that can be injected into Commands via the DI container. This allows:

  • Standalone Library Portability: Commands only need to know how to write output. If they need user input, they depend on an interface that can be satisfied by any runner (Tasker, Symfony, etc.).
  • Bridge Support: TaskerBridges will provide implementations of this interface for symfony/console (wrapping QuestionHelper) and Tasker (using STDIN/STDOUT).
  • Clean Transitions: If a developer switches from Tasker to native Symfony Console, they only need to swap the DI implementation for the interaction helper; the command logic remains untouched.

6. Metadata Configuration (Hybrid Approach)

To balance strict interface enforcement with modern Developer Experience (DX), Tasker adopts a hybrid approach for command metadata (name, description, etc.):

  • Interface-Driven: CommandInterface remains method-based (getName(), etc.) to ensure that any runner can reliably retrieve metadata.
  • Property-Based DX: A base Command class (or trait) provides default implementations of these methods that read from protected class properties. This mimics the concise configuration style of Symfony Console while maintaining the architectural purity of interfaces.
  • Lazy Loading Readiness: This structure allows the runner to potentially use Reflection or static analysis to read metadata without full instantiation if needed in the future (similar to Symfony's static properties or attributes).

Phase 1: The Standard & Bridges

  • Contracts: Define the core interfaces in a very lightweight package (e.g., getphred/console-contracts).
  • Bridges Package: Create getphred/tasker-bridges to house PhredBridge, SymfonyBridge, and LaminasBridge.
  • Ensure all components use PHP 8.2+ strict typing.

Phase 2: Migration of Existing Libraries

  • Pairity: Refactor Pairity\Console\Application logic into Tasker-compatible commands.
  • Atlas: Formalize Atlas\Commands\ListRoutesCommand into the Tasker standard.
  • Scape: Identify any CLI needs (e.g., cache clearing) and implement them.

Phase 3: The Framework Glue

  • The Framework package will include Tasker by default and pre-configure it to aggregate commands from all installed Phred libraries.

Implementation Roadmap & Checklist

To ensure Tasker is an efficient, user-friendly, and enterprise-ready package, the following checklist must be completed.

1. Product Definition & Scope

  • Define MVP: Confirm if the MVP is "Run registered commands with DI, explicit discovery via Composer extra, standard input/output, and --help/list UX."
  • Finalize SPECS.md:
    • Document v1 contracts (Input/Output/Command).
    • Define the JSON schema for composer.json discovery.
    • Define runner behavior (exit codes, verbosity flags, non-interactive mode).
  • Populate MILESTONES.md: Map deliverables to specific, verifiable milestones.

2. Contracts (ConsoleContracts)

  • Finalize v1 Interfaces: Ensure the proposed CommandInterface, InputInterface, and OutputInterface are finalized in getphred/console-contracts.
  • Versioning Policy: Establish a strict Semantic Versioning (SemVer) policy for contracts to ensure stability for library authors.

3. Core Runner (Tasker)

  • Clean up composer.json:
    • Add PSR-4 autoloading.
    • Remove psr/http-message (not needed for CLI).
    • Add dev-dependencies for testing and static analysis.
  • Bootstrap & Routing:
    • Implement a runner that accepts a PSR-11 container.
    • Implement discovery logic (scanning vendor for extra.phred-tasker).
    • Implement a basic argv parser (unless delegated).
  • Standard UX:
    • Implement list and help commands.
    • Support -v/-vv/-vvv for verbosity and --no-interaction for CI/CD safety.

4. Bridges (TaskerBridges)

  • PhredBridge: The primary adapter for the Tasker runner.
  • SymfonyBridge: Adapter for symfony/console integration.
  • LaminasBridge: (Verify demand first before implementing).

5. Quality Assurance & Enterprise Readiness

  • Testing Suite:
    • Unit tests for input parsing, discovery, and DI handoff.
    • Integration tests running a mock command through Tasker and Symfony bridges.
  • CI Pipeline: GitHub Actions for PHP 8.2/8.3 with zero tolerance for warnings/failures.
  • Documentation:
    • Comprehensive README with Quick Start and DI examples.
    • Security policy (SECURITY.md) and contribution guidelines.
    • Machine-readable output (e.g., --format=json for list).

Open Questions & Ambiguity Resolution

(All current open questions have been resolved and moved to Architectural Decisions.)