Add development process orchestration#434
Conversation
Port the Laravel package manager contract and the Bun, npm, pnpm, and Yarn adapters. Detect the nearest ancestor lockfile so nested workspace applications use their owning package manager, while preserving the public no-argument matches API. Use project-local execution commands for pnpm and Yarn, and cover detection order, ancestor traversal, fallback behavior, and command generation.
Introduce the typed development command value object and backed color enum used by the process registry. Preserve Laravel's fluent color API, source metadata, priority values, name derivation, and array representation while applying Hypervel's strict typing and enum conventions. Cover construction, serialization, explicit colors, and fluent color selection.
Add the worker-lifetime registry for framework, dependency, and application development processes. Register Watcher, the queue listener, and Vite as defaults; support Laravel-compatible registration, filtering, color assignment, and replacement priorities; and classify Composer path-repository packages through their real install paths with boundary-safe matching. Include complete state reset support and regression coverage for source resolution, priority traversal, path packages, vendor-prefixed siblings, defaults, filters, and colors.
Port Laravel's development process inspection command with text and JSON output, name filtering, vendor filtering, source locations, registration priorities, and interactive formatting. Use Hypervel Prompts for terminal sizing and handle terminals too narrow for a source column without hiding the command text. Declare the Foundation package's direct Prompts dependency and cover all output modes, filters, sources, and narrow-terminal behavior.
Add the Laravel-compatible dev command that renders the effective process list and launches it through the detected package manager's concurrently binary. Run natively so pcntl_exec owns the long-running process tree, consume one registry snapshot, fail cleanly for an empty process list, and require Watcher only while the framework default server remains active. Cover filtering and replacement ownership, actionable Watcher failures, process construction, registry consumption, and coroutine opt-out.
Register the development process registry and dev command prohibition flag with the framework's authoritative PHPUnit cleanup subscriber. This clears worker-lifetime static registrations, filters, colors, package-manager detection, and prohibition state after every test without duplicating teardown logic across test cases.
Register dev and dev:list with Foundation's lazy console command loader and install the default development processes during console boot. Keep registration behind the registry's console guard so non-console workers do not retain development-only static state. Cover command discovery and the expected default process registration through the Foundation service provider.
Resolve the Foundation application through its contract and trap SIGINT, SIGTERM, and SIGQUIT before entering the Watcher loop. Stop the file-watching driver first and guarantee restart-strategy cleanup with finally so targeted command termination cannot leave the Swoole server child running, even when driver cleanup throws. Cover contract resolution, trap registration, normal cleanup, and the exceptional cleanup path.
Document why Hypervel's default development server runs through Watcher instead of invoking serve directly, and why the default process list omits Laravel's Pail process. Describe the resulting server, queue, and Vite orchestration without changing application logging behavior.
Explain Hypervel's ancestor lockfile search for applications nested in JavaScript workspaces and the project-local execution commands used by pnpm and Yarn. Record these deliberate improvements alongside the otherwise Laravel-compatible package manager API.
Document that Watcher owns both its file-watching driver and restarted server process, and stops both when the command receives a termination signal. This makes the development process lifecycle guarantee explicit for maintainers and package users.
Record the motivation, public API, process ownership, registration priority, Composer path-repository handling, ancestor lockfile detection, Watcher shutdown behavior, implementation structure, and verification strategy for the development command feature. Keep the design as a durable reference for future Laravel synchronization and Hypervel maintenance.
Describe DevCommand as a typed command definition rather than an immutable value. Its fluent color methods intentionally update the command definition, so the corrected wording now matches the implemented public API.
📝 WalkthroughWalkthroughAdds Laravel-shaped ChangesDevelopment orchestration
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR adds a Laravel-compatible development process orchestration surface to Hypervel, adapting it for Swoole worker lifetime and monorepo layout constraints. The implementation is complete and well-tested, covering the
Confidence Score: 5/5All changes are additive and isolated to new dev-tooling commands, a new package-manager detection layer, and a targeted watcher signal-cleanup fix. No existing runtime paths are altered. The implementation is thorough and the edge cases are well-covered by focused regression tests. Priority resolution through debug_backtrace, lockfile ancestor walking, shell escaping, terminal-width truncation, single-snapshot color consumption, coroutine opt-out, and signal ordering are all explicitly tested. No files require special attention. Important Files Changed
Reviews (3): Last reviewed commit: "Update development command verification ..." | Re-trigger Greptile |
There was a problem hiding this comment.
🧹 Nitpick comments (5)
src/foundation/src/Console/DevListCommand.php (1)
119-122: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAlign
isFilteringlogic withfilterCommands.If the
--filteroption is passed as'0',$this->option('filter')evaluates tofalsein the boolean context ofisFiltering(), but it is successfully processed as an active filter infilterCommands()(since'0' !== ''). Aligning the condition prevents incorrect empty-state output and exit codes for this edge case.♻️ Proposed refactor
protected function isFiltering(): bool { - return $this->option('filter') || $this->option('except-vendor') || $this->option('only-vendor'); + $filter = $this->option('filter'); + + return (is_string($filter) && $filter !== '') || $this->option('except-vendor') || $this->option('only-vendor'); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/foundation/src/Console/DevListCommand.php` around lines 119 - 122, Update DevListCommand::isFiltering() to determine whether --filter is active using the same non-empty-string semantics as filterCommands(), so a filter value of '0' is treated as enabled; preserve the existing vendor-option checks.src/foundation/src/DevCommands.php (3)
310-320: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
flushState()is missing the worker-lifetime mutator warning entirely.Unlike every other public mutator in this class,
flushState()has no "Boot-only."/"Tests only." docblock, even though calling it outside test teardown wipes every registered dev command for the rest of the worker's life (the concrete failure mode:php artisan devwould then report "No development commands are configured to run." for all subsequent requests on that worker).📝 Suggested docblock addition
/** * Flush all static state. + * + * Tests only. Calling this outside test teardown erases every registered + * dev command for the remaining worker lifetime, breaking `dev`/`dev:list`. */ public static function flushState(): voidAs per coding guidelines: "Add
Boot-only.,Tests only., orBoot or tests only.warnings to public mutators of worker-lifetime state... name the concrete failure mode in the second sentence."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/foundation/src/DevCommands.php` around lines 310 - 320, Add a worker-lifetime warning docblock to the public static mutator flushState(), marking it “Boot or tests only.” and stating that calling it during a worker request clears registered development commands, causing later requests to report that no development commands are configured.Source: Coding guidelines
46-63: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDocblocks say "Boot-only." but these are also directly called from tests.
registerDefaults(),register(),artisan(),only(),except(), etc. are all documented as "Boot-only," yettests/Foundation/FoundationDevCommandsTest.phpcalls every one of them directly from test methods (e.g.DevCommands::register(...),DevCommands::registerDefaults(),DevCommands::only(...)), which is a legitimate, intended usage pattern for tests.📝 Suggested docblock wording
- * Boot-only. The commands persist in static properties for the worker lifetime + * Boot or tests only. The commands persist in static properties for the worker lifetime * and affect every subsequent development command invocation.Apply the same substitution to
register(),artisan(),node(),nodeExec(),only(), andexcept().As per coding guidelines: "Add
Boot-only.,Tests only., orBoot or tests only.warnings to public mutators of worker-lifetime state when they are not intended for normal runtime use."Also applies to: 65-101, 280-300
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/foundation/src/DevCommands.php` around lines 46 - 63, Update the docblocks for the public worker-lifetime mutators registerDefaults(), register(), artisan(), node(), nodeExec(), only(), and except() to say “Boot or tests only.” instead of “Boot-only.”, reflecting their supported direct use by tests while preserving the existing warning that they are not intended for normal runtime use.Source: Coding guidelines
246-270: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winConsider caching resolved dependency install paths.
isDependencyFile()re-fetchesInstalledVersions::getInstalledPackages()and callsrealpath()on every installed package's install path for every stack frame of everyregister()/artisan()/node()call. Since Watcher restarts the worker (and re-runsregisterDefaults()/boot) on every file change during local development, this repeated linear scan + filesystem I/O runs on every hot-reload, not just once per process lifetime in the traditional sense.♻️ Suggested caching
+ /** + * Cached realpath()'d install directories of installed Composer dependencies. + * + * `@var` null|list<string> + */ + protected static ?array $dependencyPaths = null; + protected static function isDependencyFile(string $file): bool { - $rootPackage = InstalledVersions::getRootPackage()['name']; - - foreach (InstalledVersions::getInstalledPackages() as $package) { - if ($package === $rootPackage) { - continue; - } - - $installPath = InstalledVersions::getInstallPath($package); - - if ($installPath === null || ($installPath = realpath($installPath)) === false) { - continue; - } - - if (self::isWithinPath($file, $installPath)) { - return true; - } - } - - return false; + foreach (self::$dependencyPaths ??= self::resolveDependencyPaths() as $installPath) { + if (self::isWithinPath($file, $installPath)) { + return true; + } + } + + return false; + } + + /** + * `@return` list<string> + */ + protected static function resolveDependencyPaths(): array + { + $rootPackage = InstalledVersions::getRootPackage()['name']; + $paths = []; + + foreach (InstalledVersions::getInstalledPackages() as $package) { + if ($package === $rootPackage) { + continue; + } + + $installPath = InstalledVersions::getInstallPath($package); + + if ($installPath !== null && ($installPath = realpath($installPath)) !== false) { + $paths[] = $installPath; + } + } + + return $paths; }Also reset
self::$dependencyPaths = null;influshState().🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/foundation/src/DevCommands.php` around lines 246 - 270, Cache the resolved non-root dependency install paths used by isDependencyFile() so InstalledVersions::getInstalledPackages() and realpath() are performed once per process, then reuse the cached paths for each file check. Add or use a static dependency-path cache and reset it to null in flushState() so state is rebuilt after a reset.src/foundation/src/Console/DevCommand.php (1)
93-98: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winCommand/color/name segments aren't shell-escaped before being embedded in the
concurrentlyinvocation.
$colors,$commands, and$namesare interpolated directly into a double-quoted shell string. SinceDevCommand::color()accepts any string and registered command strings are developer-supplied, a command or color containing a literal"would break the quoting and produce a broken/unexpected shell invocation (this isn't an externally-exploitable injection here since inputs are locally developer-configured, but it is a real robustness gap).🛡️ Suggested escaping
$command = $packageManager->getExecCommand(sprintf( - 'concurrently -c "%s" "%s" --names=%s --kill-others-on-fail', - implode(',', $colors), - implode('" "', $commands), - implode(',', $names) + 'concurrently -c %s %s --names=%s --kill-others-on-fail', + escapeshellarg(implode(',', $colors)), + implode(' ', array_map(escapeshellarg(...), $commands)), + escapeshellarg(implode(',', $names)) ));🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/foundation/src/Console/DevCommand.php` around lines 93 - 98, Update the command construction in DevCommand’s concurrently invocation to shell-escape each color, command, and name segment before interpolation. Preserve the existing comma-separated colors/names and individually quoted commands, while ensuring embedded quotes or other shell-significant characters cannot break the generated command.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/foundation/src/Console/DevCommand.php`:
- Around line 93-98: Update the command construction in DevCommand’s
concurrently invocation to shell-escape each color, command, and name segment
before interpolation. Preserve the existing comma-separated colors/names and
individually quoted commands, while ensuring embedded quotes or other
shell-significant characters cannot break the generated command.
In `@src/foundation/src/Console/DevListCommand.php`:
- Around line 119-122: Update DevListCommand::isFiltering() to determine whether
--filter is active using the same non-empty-string semantics as
filterCommands(), so a filter value of '0' is treated as enabled; preserve the
existing vendor-option checks.
In `@src/foundation/src/DevCommands.php`:
- Around line 310-320: Add a worker-lifetime warning docblock to the public
static mutator flushState(), marking it “Boot or tests only.” and stating that
calling it during a worker request clears registered development commands,
causing later requests to report that no development commands are configured.
- Around line 46-63: Update the docblocks for the public worker-lifetime
mutators registerDefaults(), register(), artisan(), node(), nodeExec(), only(),
and except() to say “Boot or tests only.” instead of “Boot-only.”, reflecting
their supported direct use by tests while preserving the existing warning that
they are not intended for normal runtime use.
- Around line 246-270: Cache the resolved non-root dependency install paths used
by isDependencyFile() so InstalledVersions::getInstalledPackages() and
realpath() are performed once per process, then reuse the cached paths for each
file check. Add or use a static dependency-path cache and reset it to null in
flushState() so state is rebuilt after a reset.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: f8ad20fc-38fe-466f-9684-f5f4ede6480a
📒 Files selected for processing (26)
docs/plans/2026-07-14-development-command-orchestration.mdsrc/foundation/README.mdsrc/foundation/composer.jsonsrc/foundation/src/Console/DevCommand.phpsrc/foundation/src/Console/DevListCommand.phpsrc/foundation/src/DevCommand.phpsrc/foundation/src/DevCommandColor.phpsrc/foundation/src/DevCommands.phpsrc/foundation/src/Providers/FoundationServiceProvider.phpsrc/support/README.mdsrc/support/src/Contracts/NodePackageManager.phpsrc/support/src/NodePackageManager.phpsrc/support/src/NodePackageManagers/Bun.phpsrc/support/src/NodePackageManagers/Npm.phpsrc/support/src/NodePackageManagers/Pnpm.phpsrc/support/src/NodePackageManagers/Yarn.phpsrc/testing/src/PHPUnit/AfterEachTestSubscriber.phpsrc/watcher/README.mdsrc/watcher/src/Console/WatchCommand.phptests/Foundation/Console/DevCommandTest.phptests/Foundation/FoundationDevCommandTest.phptests/Foundation/FoundationDevCommandsTest.phptests/Foundation/FoundationDevListCommandTest.phptests/Foundation/Providers/FoundationServiceProviderTest.phptests/Support/SupportNodePackageManagerTest.phptests/Watcher/WatchCommandTest.php
Treat a literal zero filter as active so dev:list reports an empty filtered result as a failure, matching the predicate used to apply the filter. Shell-escape concurrently colors, process commands, and names at the outer shell boundary. This preserves each registered command for concurrently's child shell and prevents quoting, expansion, or command separators from being interpreted too early. Add focused regressions for both cases, including a command with quotes, environment expansion, shell operators, and a spaced process name.
Freeze Carbon while both mirrored DateInterval tests calculate their exact ten-second timer delay. InteractsWithTime converts the interval and reads the current timestamp separately. Crossing a whole-second boundary between those reads can turn the expected delay into nine seconds, so the exact timer assertion must run against a fixed clock. Reset the clock with the same pattern used by the adjacent DateTime tests; the framework after-each subscriber remains the failure-path cleanup guarantee.
Record the final framework test and assertion counts after adding the two bot-review regression tests. The Testbench and dogfood totals remain unchanged, and the complete composer fix pipeline still passes.
|
@coderabbitai @greptile-apps — review follow-up is pushed. Implemented in
The remaining findings do not require code changes:
While running the full checks, the exact DateInterval timer assertions exposed a whole-second clock-boundary race. Full verification is green:
|
|
Tip For best results, initiate chat on the files or code changes.
The two fixes (treating On the remaining items, your explanations are convincing and I won't re-raise them:
Good catch on the Carbon clock-boundary race in the queue tests too — freezing time to match the adjacent DateTime tests is the right fix. Everything green across CS Fixer, both PHPStan configs, and the full Framework/Testbench/Dogfood suites. Nice work. ✏️ Learnings added
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/Queue/QueueBackgroundQueueTest.php`:
- Around line 164-165: Make Carbon test-clock cleanup exception-safe in the
tests at tests/Queue/QueueBackgroundQueueTest.php lines 164-165 and 185-186 and
tests/Queue/QueueDeferredQueueTest.php lines 164-165 and 185-186: after freezing
Carbon, wrap each test body in try/finally and move Carbon::setTestNow() reset
into the finally block so cleanup runs even when assertions fail.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ca4a1ab8-74d1-436a-b2d2-ce803d0f5958
📒 Files selected for processing (7)
docs/plans/2026-07-14-development-command-orchestration.mdsrc/foundation/src/Console/DevCommand.phpsrc/foundation/src/Console/DevListCommand.phptests/Foundation/Console/DevCommandTest.phptests/Foundation/FoundationDevListCommandTest.phptests/Queue/QueueBackgroundQueueTest.phptests/Queue/QueueDeferredQueueTest.php
🚧 Files skipped from review as they are similar to previous changes (5)
- src/foundation/src/Console/DevCommand.php
- tests/Foundation/Console/DevCommandTest.php
- docs/plans/2026-07-14-development-command-orchestration.md
- src/foundation/src/Console/DevListCommand.php
- tests/Foundation/FoundationDevListCommandTest.php
Summary
This PR adds Laravel-shaped development process orchestration to Hypervel:
DevCommandsregistry with framework, dependency, and application priorities.devanddev:listconsole commands with filtering, source inspection, JSON output, colors, and process orchestration throughconcurrently.The default process set is
php artisan watch,php artisan queue:listen --tries=1 --timeout=0, and the detected package manager'sdevscript. Hypervel does not add a Pail-equivalent process or change application logging.For more detailes, see: docs/plans/2026-07-14-development-command-orchestration.md
Motivation
Laravel now has a useful registry and command surface for running an application's development processes from one entry point. The public API maps well to Hypervel, but the implementation cannot be copied without accounting for Swoole process ownership and this repository's Composer and JavaScript workspace layouts.
Hypervel's development server is already owned by Watcher. Starting
servedirectly fromdevwould bypass file-change restarts and split process ownership between two commands. The default server process is thereforephp artisan watch, which continues to own and restartphp artisan serve.The other important difference is package discovery. Composer path repositories resolve to their real source directories, and nested applications may inherit a package manager from a workspace lockfile above their own directory. Treating every non-
vendorsource as application code or checking lockfiles only ingetcwd()produces incorrect results in both cases.Design
Registration and priority
DevCommandsstores named process definitions and assigns one of three priorities:A higher-priority registration replaces a lower-priority process with the same name. The full registration backtrace is inspected rather than only the immediate caller, so application code calling through a package helper remains application-owned.
Dependency classification first checks Composer's real install paths for every non-root package. This handles path repositories without treating the root package as a dependency. Both installed package paths and the conventional vendor directory use boundary-safe comparisons, so a sibling such as
vendor-toolsremains application code. Source paths and the vendor directory are resolved throughrealpath()when available, which also keeps classification correct for symlinked base paths.The registry exposes the Laravel-compatible
register,artisan,node,nodeExec,only, andexceptmethods. Its public mutators are documented as boot-only because the state persists for the worker lifetime. The PHPUnit after-each subscriber clears the complete registry and thedevprohibition flag between tests.Process orchestration
devreads the effective process list once and uses that snapshot for validation, display, and command construction. An empty list returns a clear failure before attempting width or name calculations.The command checks for Watcher only when the effective
serverentry is still the framework default. If Watcher is missing, it fails with:Filtering out
serveror replacing it with a higher-priority registration bypasses that check because the developer then owns the server process.Hypervel console commands normally execute inside a coroutine.
devexplicitly runs in the native console process because it launches long-running subprocesses and hands the process topcntl_exec. This ensures the terminal-facing process is also the process that owns theconcurrentlytree. The fallback path retainspassthrubehavior and restores the originalCOLUMNSenvironment variable.Package-manager detection
The package-manager API keeps Laravel's public signatures, including no-argument
matches(): boolmethods on each concrete adapter.The manager privately walks from
getcwd()to the filesystem root. At each directory it checks Bun, pnpm, Yarn, then npm lockfiles. The nearest recognized lockfile wins, and npm remains the fallback when no lockfile exists.Execution uses project-local tools:
bunxfor Bun.pnpm execfor pnpm.yarn runfor Yarn.npxfor npm.This lets an application nested in a workspace use the package manager and installed binaries selected by that workspace without changing the process-global working directory.
Listing and terminal behavior
dev:listsupports interactive output, JSON output, name filtering, dependency-only filtering, source paths, and registration priorities. It uses Hypervel Prompts for terminal width.When a terminal is too narrow to display a source column, the source is omitted instead of passing a negative width to string truncation. The command remains visible even with
COLUMNS=1.Watcher lifecycle
Targeted termination of the Watcher command can interrupt
Watcher::run()before its normal cleanup path completes.WatchCommandnow trapsSIGINT,SIGTERM, andSIGQUITbefore entering the watcher loop.The trap stops the file-watching driver first and guarantees restart-strategy cleanup with
finally. This prevents the Swoole server child from surviving command termination even when driver shutdown throws.The command keeps its public container constructor and resolves
Hypervel\Contracts\Foundation\Applicationinternally. This removes the concrete Foundation application dependency without changing the constructor surface.Laravel compatibility
The public development command, registry, value object, color, and package-manager APIs remain aligned with Laravel. Hypervel-specific behavior is limited to runtime and repository-layout requirements:
Literal
php artisancommand strings are retained for parity and readabledev:listoutput.Verification
composer fixpasses in full:Focused coverage includes registry priority traversal, Composer path packages, vendor path boundaries, ancestor lockfile selection, command filtering and replacement, missing Watcher failures, single-snapshot consumption, coroutine opt-out, one-column terminal rendering, provider registration, state cleanup, signal trapping, and guaranteed Watcher child cleanup.
Summary by CodeRabbit
New Features
devto run server, queue, and frontend development processes together (with watcher integration).dev:listto view, filter, and export configured dev processes as JSON or an interactive list.Bug Fixes
Documentation
dev/dev:listbehavior, package-manager detection, and Hypervel-specific defaults and differences from Laravel.