commit eead48771437013492ae72f6a4c6e3c25e533886 Author: Funky Waddle Date: Sun Feb 22 01:00:52 2026 -0600 chore: initialize project infrastructure and Phred IO adapters diff --git a/MILESTONES.md b/MILESTONES.md new file mode 100644 index 0000000..f8df0ba --- /dev/null +++ b/MILESTONES.md @@ -0,0 +1,27 @@ +# TaskerBridges Milestones + +This document defines the implementation milestones for the `getphred/tasker-bridges` package. + +## 1. Bridge Infrastructure +- [ ] Initialize repository with `getphred/console-contracts` dependency. +- [ ] Set up testing environment for multi-toolkit validation (mocking Symfony/Laravel). + +## 2. Symfony Bridge +- [ ] Implement `SymfonyCommandAdapter`. +- [ ] Implement `SymfonyInputAdapter` and `SymfonyOutputAdapter`. +- [ ] Implement helper adapters (Interaction, ProgressBar, Table). +- [ ] Implement fixed markup translation (mapping to Symfony Formatter). + +## 3. Laravel Bridge +- [ ] Implement `LaravelCommandAdapter` and `LaravelServiceProvider`. +- [ ] Integrate Symfony IO adapters within the Laravel context. +- [ ] Implement native Laravel interaction helper mappings. + +## 4. Phred (Native) Bridge +- [ ] Implement ANSI-based `OutputInterface` with full markup support. +- [ ] Implement lightweight interaction and progress helpers. +- [ ] Implement regex-based Markdown-to-Markup converter. + +## 5. Universal Integration +- [ ] Implement universal exit code translation logic. +- [ ] Implement global flag mapping (Verbosity, Decoration, Interactivity). diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 0000000..9b2c2ac --- /dev/null +++ b/NOTES.md @@ -0,0 +1,174 @@ +# TaskerBridges: Phred CLI Interoperability Adapters + +TaskerBridges is the adapter layer that allows commands written against `Phred\ConsoleContracts` to run inside multiple CLI runtimes with a native experience. Bridges translate Phred's runner-agnostic contracts into the APIs of specific console toolkits. + +Project Phase: Idea (NOTES.md) + +## 1. Core Vision +- Interoperability first: Write a command once, run it anywhere (Tasker native runner, Symfony Console, Laravel Artisan, etc.). +- Contracts-driven: Bridges never change command code; they only adapt `ConsoleContracts` to the target runtime. +- Minimal footprint: Optional, per-bridge dependencies and lazy wiring. No heavy transitive dependencies for users who don't need a given bridge. +- Native UX: When running under a given toolkit, output, helpers, and ergonomics should feel native to that toolkit. + +## 2. Scope & Responsibilities +Bridges provide a cohesive set of adapters: +- Command adapter: Wraps `CommandInterface` and exposes a native command entrypoint. +- IO adapters: Implement `InputInterface`/`OutputInterface` on top of the target toolkit’s input/output. +- Helper adapters: Implement optional helper contracts (`InteractionInterface`, `ProgressBarInterface`, `TableInterface`, `MarkdownConverterInterface`). +- Markup translator: Converts Phred’s fixed markup tags (``, ``, ``, ``, ``, ``, ``, ``) to the target toolkit’s styles or to raw ANSI. +- Policy alignment: Honor `Verbosity` levels, non-interactive mode, TTY/color detection, and `sysexits.h` exit code semantics. + +Non-goals: +- Defining new business logic or command semantics. +- Extending the fixed markup set (bridges must support the core set only). + +## 3. Supported Bridges (Initial Set) +- SymfonyBridge: Adapter for `symfony/console` (targeting Symfony 6/7 lines). +- PhredBridge (Native): Minimal adapters used by the Tasker core runner. +- LaravelBridge: Adapter for Laravel's Artisan console (`illuminate/console`). + +### 3.1 Future / Potential Bridges (Roadmap) +The following bridges are identified as high-value for future development or upon community request: +- **LaminasBridge**: Adapter for `laminas/laminas-cli`. +- **CakePHPBridge**: Adapter for the CakePHP Console/Shell ecosystem. +- **YiiBridge**: Adapter for Yii 2.0 console commands (Note: Yii 3.0 uses `symfony/console` and may be covered by `SymfonyBridge`). +- **AuraCliBridge**: Adapter for `Aura.Cli` for users of the Aura library suite. +- **RichOutputBridge**: A bridge leveraging a library like `CLImate` for advanced terminal UI features (colors, progress bars) when the native Phred runner is used. + +Each bridge lives under `Phred\TaskerBridges\{BridgeName}\...` and can be installed/used independently via Composer suggests. + +## 4. SymfonyBridge — Design + +### 4.1 High-Level Mapping +- Command metadata + - `CommandInterface::getName()` -> `Symfony\Component\Console\Command\Command::setName()` + - `getDescription()` -> `setDescription()` + - `getArguments()` -> loop `addArgument($name, InputArgument::OPTIONAL|REQUIRED, $description, $default)` + - `getOptions()` -> loop `addOption($name, $shortcut, InputOption::VALUE_NONE|VALUE_OPTIONAL|VALUE_REQUIRED, $description, $default)` + - Note: `ConsoleContracts` currently models arguments/options as `name => description` (DX-minimal). Bridge MAY enhance by honoring defaults/modes if/when added to contracts in future specs. + +- Execution + - The adapter class (e.g., `SymfonyCommandAdapter`) extends `Symfony\Component\Console\Command\Command` and wraps a `CommandInterface` instance. + - In `execute(InputInterface $in, OutputInterface $out)`, it: + 1) Adapts Symfony input/output to Phred `InputInterface`/`OutputInterface` via IO adapters. + 2) Invokes `$wrapped->execute($phredInput, $phredOutput)`. + 3) Returns the integer exit code from the wrapped command. + +### 4.2 IO Adapters +- `SymfonyInputAdapter` implements `GetPhred\ConsoleContracts\InputInterface` by delegating to `Symfony\Component\Console\Input\InputInterface` methods (`getArgument`, `getOption`, `hasOption`). +- `SymfonyOutputAdapter` implements `GetPhred\ConsoleContracts\OutputInterface` by delegating to `Symfony\Component\Console\Output\OutputInterface`/`Style` and applying markup translation. + +Verbosity mapping: +- Phred `Verbosity::QUIET|NORMAL|VERBOSE|VERY_VERBOSE|DEBUG` <-> Symfony `OutputInterface::VERBOSITY_*`. + +Non-interactive: +- Respect a runner-provided flag; adapters should avoid prompts and ensure helpers also respect non-interactive mode. + +### 4.3 Markup Translation (Fixed Set) +- Translate Phred tags into Symfony formatter equivalents or raw ANSI where Symfony lacks a native tag. + - `` -> Symfony style (green, bold) using `SymfonyStyle` or custom formatter style. + - `` -> Symfony `` (blue where applicable) or custom style to match Phred’s blue guideline. + - `` -> white on red background. + - `` -> black on yellow background. + - `` -> yellow. + - `` -> bold; `` -> italic; `` -> underline (use ANSI sequences if formatter lacks direct styles). +- If output is not decorated (no TTY or colors disabled), strip tags to plain text. + +### 4.4 Helper Adapters +- `InteractionInterface` -> wrap `QuestionHelper` (ask, confirm, secret, choice), ensuring non-interactive fallback behavior (return defaults, never block). +- `ProgressBarInterface` -> wrap `Symfony\Component\Console\Helper\ProgressBar`. +- `TableInterface` -> wrap `Symfony\Component\Console\Helper\Table`. +- `MarkdownConverterInterface` -> use a lightweight adapter strategy: + - Prefer Symfony-native formatting if available; otherwise convert Markdown to Phred markup (headings -> ``, emphasis -> ``/``, links -> underline/text), then pass through normal markup translation. + +### 4.5 Error & Exit Codes +- Return integer exit codes from wrapped command directly. +- Exceptions: Allow the runner to handle globally; optionally map known `ConsoleExceptionInterface` to `ExitCode::USAGE|CONFIG|UNAVAILABLE|SOFTWARE` as appropriate. + +## 5. PhredBridge (Native) — Design + +- Provide minimal in-house adapters usable by the Tasker runner. +- `PhredOutputAdapter`: translates fixed markup to ANSI escape sequences: + - `` -> green (e.g., `\e[32m`), `` -> blue (`\e[34m`), `` -> white on red (`\e[97;41m`), `` -> black on yellow (`\e[30;43m`), `` -> yellow (`\e[33m`), `` -> bold (`\e[1m`), `` -> italic (`\e[3m`), `` -> underline (`\e[4m`). Reset with `\e[0m`. +- TTY detection and `--no-interaction`/`--no-ansi` handling (strip tags when colors disabled or piping to files). +- Provide simple `InteractionInterface`, `ProgressBarInterface`, `TableInterface`, and `MarkdownConverterInterface` implementations with zero external dependencies (basic, but reliable). + +## 6. LaravelBridge — Design + +### 6.1 Registration & Discovery +- Provide a `TaskerServiceProvider` for Laravel applications. +- Leverage Laravel Package Auto-Discovery via `composer.json` to register the provider automatically. +- The provider will: + - Discover Phred commands via `extra.phred-tasker` or explicit container binding. + - Register them with the Artisan application. + - Provide configuration for non-interactive defaults and formatting. + +### 6.2 Container & Execution +- Resolve `CommandInterface` instances via the Laravel Service Container (`app()`), favoring constructor injection. +- Wrap commands in a `LaravelCommandAdapter` that extends `Illuminate\Console\Command`. +- Delegate execution and return integer exit codes. + +### 6.3 IO & Markup +- Reuse `SymfonyBridge` IO adapters internally (Artisan's IO is built on Symfony). +- Ensure Phred’s fixed markup set (``, ``, ``, ``, ``, ``, ``, ``) renders natively in Artisan. + +### 6.4 Helper Adapters +- `InteractionInterface` -> Map to Laravel's native console methods (`ask`, `confirm`, `secret`, `choice`). +- `ProgressBarInterface` / `TableInterface` -> Reuse SymfonyBridge adapters via Artisan. +- `MarkdownConverterInterface` -> Standard Phred implementation (lightweight regex/mapping). + +## 7. Dependency & Packaging Strategy + +- Composer `suggest` dependencies per bridge: + - `symfony/console` for SymfonyBridge. + - `illuminate/console` for LaravelBridge. +- Hard requirements: only `php:^8.2` and `getphred/console-contracts`. +- Conditional wiring: Bridge factory/services check for class existence before enabling a bridge. +- Namespacing: `Phred\TaskerBridges\Symfony\...`, `Phred\TaskerBridges\Phred\...`, `Phred\TaskerBridges\Laravel\...`. + +## 8. Registration & Discovery + +- Consumers may register bridges explicitly in DI (preferred for clarity). +- For Laravel, auto-discovery via `TaskerServiceProvider` is the primary path. +- For Symfony apps, provide a small integration guide to register `SymfonyCommandAdapter` instances in the application. +- For Tasker, PhredBridge is used by default; other bridges are opt-in and instantiated only if their dependencies are present. + +## 9. Middleware Compatibility + +- Bridges must compose with `ConsoleMiddlewareInterface` stacks managed by the runner. The adapter boundary is after middleware resolution so that middlewares see Phred `InputInterface`/`OutputInterface` consistently. + +## 10. Performance & Lazy Loading + +- Metadata access should be O(1) with minimal reflection at runtime. If attributes are used (`#[Cmd]`, `#[Arg]`, `#[Opt]`) and surfaced via `HasAttributes`, bridges continue to consume only the `CommandInterface` methods — no extra reflection needed in bridges. +- For toolkits that support lazy command discovery, prefer deferring heavy initialization until execution. + +### 11. Testing Strategy (for later phases) + +- Unit tests: Markup translation, verbosity mapping, non-interactive behavior, helpers integration. +- Integration tests: Execute a sample Phred command through SymfonyBridge, LaravelBridge, and PhredBridge, verify identical behavior and exit codes. + +## 12. Architectural Decisions (Resolved) + +### 12.1 Global Options & Flags +- Bridges will map toolkit-specific flags (e.g., Symfony’s `--ansi`, `--no-interaction`) to the Phred `InputInterface` and `OutputInterface` state. +- **Purity**: Commands must NOT depend on toolkit-specific options. Instead, they should rely on the standardized Phred flags: + - `-v`, `-vv`, `-vvv`, `-q` (Verbosity) + - `-n` or `--no-interaction` (Non-interactive) + - `--no-ansi` (Color/Decoration) +- **Passthrough**: Toolkit-specific global options remain at the application layer and are consumed only by the bridge to configure the IO adapters. + +### 12.2 Error Mapping Policy +- Bridges are responsible for a "Standard Translation" of common exceptions into `sysexits.h` exit codes. +- **Mapping Strategy**: + - `InvalidArgumentException` -> `ExitCode::USAGE` (64) + - `RuntimeException` -> `ExitCode::SOFTWARE` (70) + - `ConsoleExceptionInterface` (from Contracts) -> Mapped according to category (e.g., `UNAVAILABLE`, `CONFIG`). +- **Global Handler**: The primary runner (Tasker) or Bridge wrapper provides the top-level `try/catch` to ensure these codes are returned to the shell. + +### 12.3 Helper Feature Parity +- **Guaranteed Minimum API**: Bridges must implement the full `ProgressBarInterface` and `TableInterface` as defined in `ConsoleContracts`. +- **Progressive Enhancement**: Bridges MAY support advanced features (e.g., table styling, progress bar steps) if the underlying toolkit supports it, but commands should avoid depending on these for core functionality. +- **Fallback**: In non-TTY or non-supported environments, bridges must provide a "silent" or "degraded" implementation (e.g., printing table as a list, silencing progress bars) to prevent command failure. + +## Brainstorming / Open Questions +(All current open questions have been resolved and moved to Architectural Decisions.) diff --git a/SPECS.md b/SPECS.md new file mode 100644 index 0000000..7a32fd8 --- /dev/null +++ b/SPECS.md @@ -0,0 +1,89 @@ +# TaskerBridges Specification + +This document defines the technical specifications for the `getphred/tasker-bridges` package, which adapts `Phred\ConsoleContracts` for various CLI runtimes. + +## 1. Bridge Strategy + +Bridges act as adapters between the standardized Phred CLI contracts and the native APIs of target console toolkits. + +### 1.1 Universal Bridge Requirements +All bridges must: +- Adapt command metadata (name, description, arguments, options) to the native format. +- Wrap execution to translate native input/output to Phred `InputInterface` and `OutputInterface`. +- Implement fixed markup translation. +- Map native verbosity and interaction states to Phred's standardized levels/flags. +- Handle common exceptions and map them to `sysexits.h` exit codes. + +## 2. SymfonyBridge + +**Target**: `symfony/console` (v6.x and v7.x) +**Namespace**: `Phred\TaskerBridges\Symfony` + +### 2.1 SymfonyCommandAdapter +- Extends `Symfony\Component\Console\Command\Command`. +- Wraps a `Phred\ConsoleContracts\CommandInterface`. +- Maps Phred metadata to Symfony's `setName`, `setDescription`, `addArgument`, and `addOption`. + +### 2.2 Symfony IO Adapters +- `SymfonyInputAdapter`: Wraps `Symfony\Input\InputInterface` and implements `Phred\ConsoleContracts\InputInterface`. +- `SymfonyOutputAdapter`: Wraps `Symfony\Output\OutputInterface` and implements `Phred\ConsoleContracts\OutputInterface`. + +### 2.3 Symfony Helper Adapters +- `SymfonyInteractionAdapter`: Wraps `Symfony\Helper\QuestionHelper` to implement `Phred\ConsoleContracts\InteractionInterface`. +- `SymfonyProgressBarAdapter`: Wraps `Symfony\Helper\ProgressBar`. +- `SymfonyTableAdapter`: Wraps `Symfony\Helper\Table`. + +## 3. LaravelBridge + +**Target**: `illuminate/console` (v10.x and v11.x) +**Namespace**: `Phred\TaskerBridges\Laravel` + +### 3.1 LaravelCommandAdapter +- Extends `Illuminate\Console\Command`. +- Wraps a `Phred\ConsoleContracts\CommandInterface`. +- Internally uses Symfony adapters for IO and helpers where applicable (as Artisan is built on Symfony). + +### 3.2 LaravelServiceProvider +- Handles auto-discovery of Phred commands from `composer.json` (`extra.phred-tasker`). +- Registers commands within the Laravel container and Artisan application. + +## 4. PhredBridge (Native) + +**Target**: Tasker Core Runner +**Namespace**: `Phred\TaskerBridges\Phred` + +### 4.1 Native Adapters +- Provides ANSI-based implementations for `OutputInterface`. +- Provides lightweight implementations for `InteractionInterface`, `ProgressBarInterface`, and `TableInterface` with zero external dependencies. + +### 4.2 MarkdownConverterInterface (Native) +- **Implementation**: Provides a regex-based parser to translate standard Markdown (headings, emphasis, links) into the fixed Phred markup tag set (``, ``, ``, etc.). + +## 5. Markup Translation Mapping + +| Phred Tag | Phred Color/Style | Symfony/Laravel Implementation | Native Implementation (ANSI) | +|:---|:---|:---|:---| +| `` | Green | Custom `SymfonyStyle` or Formatter style | `\e[32m` | +| `` | Blue | Custom Formatter style | `\e[34m` | +| `` | White on Red | `` tag | `\e[97;41m` | +| `` | Black on Yellow | Custom Formatter style | `\e[30;43m` | +| `` | Yellow | `` tag | `\e[33m` | +| `` | Bold | `\e[1m` (if not native) | `\e[1m` | +| `` | Italic | `\e[3m` (if not native) | `\e[3m` | +| `` | Underline | `\e[4m` (if not native) | `\e[4m` | + +## 6. Exit Code Translation + +Bridges should map the following standard exceptions: +- `InvalidArgumentException` -> `ExitCode::USAGE` (64) +- `RuntimeException` -> `ExitCode::SOFTWARE` (70) +- `LogicException` -> `ExitCode::SOFTWARE` (70) +- `Phred\ConsoleContracts\ConsoleExceptionInterface` implementations -> mapped per category. + +## 7. Global Options & Flags + +Bridges are responsible for mapping toolkit-specific global options to the Phred state: +- Symfony `--ansi` / `--no-ansi` -> `OutputInterface::setDecorated()` +- Symfony `-v` / `-vv` / `-vvv` -> `OutputInterface::setVerbosity()` +- Symfony `--no-interaction` -> `InteractionInterface` behavior (non-blocking). +- Laravel specific variants of the above. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..19d91e7 --- /dev/null +++ b/composer.json @@ -0,0 +1,20 @@ +{ + "name": "getphred/tasker-bridges", + "description": "Optional bridges for integrating Phred CLI commands into various PHP console tools.", + "license": "MIT", + "type": "library", + "require": { + "php": "^8.2", + "getphred/console-contracts": "dev-master" + }, + "suggest": { + "getphred/tasker": "To use the PhredBridge with the Tasker runner", + "symfony/console": "To use the SymfonyBridge", + "laminas/laminas-console": "To use the LaminasBridge" + }, + "autoload": { + "psr-4": { + "Phred\\TaskerBridges\\": "src/" + } + } +} diff --git a/src/Phred/InputAdapter.php b/src/Phred/InputAdapter.php new file mode 100644 index 0000000..c59675a --- /dev/null +++ b/src/Phred/InputAdapter.php @@ -0,0 +1,47 @@ + $arguments + * @param array $options + */ + public function __construct( + private readonly array $arguments = [], + private readonly array $options = [] + ) { + } + + /** + * @inheritDoc + */ + public function getArgument(string $name, mixed $default = null): mixed + { + return $this->arguments[$name] ?? $default; + } + + /** + * @inheritDoc + */ + public function getOption(string $name, mixed $default = null): mixed + { + return $this->options[$name] ?? $default; + } + + /** + * @inheritDoc + */ + public function hasOption(string $name): bool + { + return array_key_exists($name, $this->options); + } +} diff --git a/src/Phred/OutputAdapter.php b/src/Phred/OutputAdapter.php new file mode 100644 index 0000000..ca8fe37 --- /dev/null +++ b/src/Phred/OutputAdapter.php @@ -0,0 +1,140 @@ + ANSI color mapping. + */ + private const TAGS = [ + 'info' => '34', // Blue + 'success' => '32', // Green + 'error' => '31', // Red + 'warning' => '33', // Yellow + 'comment' => '36', // Cyan + 'bold' => '1', + ]; + + /** + * @param bool $decorated Whether to use ANSI decoration. + */ + public function __construct(bool $decorated = true) + { + $this->decorated = $decorated; + } + + /** + * @inheritDoc + */ + public function write(string $message): void + { + echo $this->format($message); + } + + /** + * @inheritDoc + */ + public function writeln(string $message): void + { + echo $this->format($message) . PHP_EOL; + } + + /** + * @inheritDoc + */ + public function success(string $message): void + { + $this->writeln("$message"); + } + + /** + * @inheritDoc + */ + public function info(string $message): void + { + $this->writeln("$message"); + } + + /** + * @inheritDoc + */ + public function error(string $message): void + { + $this->writeln("$message"); + } + + /** + * @inheritDoc + */ + public function warning(string $message): void + { + $this->writeln("$message"); + } + + /** + * @inheritDoc + */ + public function comment(string $message): void + { + $this->writeln("$message"); + } + + /** + * @inheritDoc + */ + public function setVerbosity(int $level): void + { + $this->verbosity = $level; + } + + /** + * @inheritDoc + */ + public function getVerbosity(): int + { + return $this->verbosity; + } + + /** + * Sets whether the output should be decorated. + * + * @param bool $decorated + * @return void + */ + public function setDecorated(bool $decorated): void + { + $this->decorated = $decorated; + } + + /** + * Formats the message by replacing tags with ANSI codes or stripping them. + * + * @param string $message + * @return string + */ + private function format(string $message): string + { + return preg_replace_callback('/<([a-z]+)>(.*?)<\/\\1>/i', function (array $matches): string { + $tag = strtolower($matches[1]); + $content = $matches[2]; + + if (!$this->decorated || !isset(self::TAGS[$tag])) { + return $content; + } + + return sprintf("\033[%sm%s\033[0m", self::TAGS[$tag], $content); + }, $message) ?? $message; + } +}