Skip to content

Comments

Moved several methods out of the installer command class.#2308

Merged
AlexSkrypnyk merged 5 commits intomainfrom
feature/improve-installer-feb26
Feb 19, 2026
Merged

Moved several methods out of the installer command class.#2308
AlexSkrypnyk merged 5 commits intomainfrom
feature/improve-installer-feb26

Conversation

@AlexSkrypnyk
Copy link
Member

@AlexSkrypnyk AlexSkrypnyk commented Feb 19, 2026

Summary by CodeRabbit

  • New Features

    • New interactive installer UI with branded headers, contextual welcome boxes, and richer post-install summaries
    • Built-in agent help output for detailed installer guidance
    • Automated file/destination handling and demo database download support
  • Improvements

    • Centralized option resolution and preflight requirement checks for more robust configuration
    • Clearer outcome-specific footers with actionable next steps and troubleshooting
  • Tests

    • Extensive unit and functional tests covering UI, options, file handling, and agent help rendering

@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 2026

Walkthrough

InstallCommand was refactored to delegate responsibilities to new components: OptionsResolver for option/requirement resolution, FileManager for filesystem/demo tasks, and InstallerPresenter for UI/footers; AgentHelp supplies static help text. Build/result constants moved into InstallerPresenter.

Changes

Cohort / File(s) Summary
Command Refactoring
.vortex/installer/src/Command/InstallCommand.php
Replaced inline initialization, header/footer, and file/build steps with dependency-injected OptionsResolver, InstallerPresenter, and FileManager. Removed local BUILD_RESULT_* constants and switched to presenter calls.
UI & Presentation
.vortex/installer/src/Prompts/InstallerPresenter.php
New presenter class providing header/footer rendering, post-build summaries (succeeded/skipped/failed), required-tool checks, and integration point for PromptManager; houses BUILD_RESULT_* constants.
File Operations
.vortex/installer/src/Utils/FileManager.php
New FileManager handling destination creation/git init, copying/cleanup of files, .env.local handling, and demo DB download via injected Downloader. Throws on critical failures.
Configuration & Resolution
.vortex/installer/src/Utils/OptionsResolver.php
New static resolver: checkRequirements() validates executables; resolve() merges CLI, config file, and env into Config/Artifact, determines destination, demo flags, and other runtime flags.
Static Help
.vortex/installer/src/Schema/AgentHelp.php
New static AgentHelp::render() returns a large HEREDOC with agent instructions, schema guidance, commands, and tips.
Tests
.vortex/installer/tests/...
Added/updated unit and functional tests for InstallerPresenter, FileManager, OptionsResolver, and AgentHelp to cover new behaviors and edge cases (many new test files).

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant InstallCommand
  participant OptionsResolver
  participant InstallerPresenter
  participant FileManager
  participant Downloader
  participant PromptManager

  CLI->>InstallCommand: run install command (options)
  InstallCommand->>OptionsResolver: checkRequirements()
  OptionsResolver-->>InstallCommand: ok / throw
  InstallCommand->>OptionsResolver: resolve(options)
  OptionsResolver-->>InstallCommand: Config + Artifact
  InstallCommand->>InstallerPresenter: header(Artifact, version)
  InstallerPresenter-->>InstallCommand: rendered header
  InstallCommand->>FileManager: prepareDestination()
  FileManager-->>InstallCommand: destination messages
  InstallCommand->>FileManager: copyFiles()
  FileManager-->>InstallCommand: copy result
  InstallCommand->>FileManager: prepareDemo(Downloader)
  FileManager->>Downloader: download demo DB (if needed)
  Downloader-->>FileManager: download result
  FileManager-->>InstallCommand: demo messages
  InstallCommand->>PromptManager: run post-build hooks (via presenter)
  PromptManager-->>InstallerPresenter: handler output
  InstallCommand->>InstallerPresenter: footerBuild*(result)
  InstallerPresenter-->>CLI: rendered footer and next steps
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

DO NOT MERGE

Poem

🐰 I hopped through code, so neat and spry,
Split duties out beneath the sky.
Presenter paints, FileManager tends,
OptionsResolver ties loose ends—
A tidy burrow for installs, oh my! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.98% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: extracting methods from InstallCommand into new classes (InstallerPresenter, FileManager, OptionsResolver).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/improve-installer-feb26

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.vortex/installer/src/Prompts/InstallerPresenter.php:
- Line 29: The nullable property $promptManager can cause fatal errors because
footerBuildSucceeded(), footerBuildSkipped(), and footerBuildFailed() call
methods on it without guards; fix by making PromptManager required: add a
PromptManager parameter to the InstallerPresenter constructor, assign it to the
$promptManager property and change its type to non-nullable (remove ?), and
remove setPromptManager() usage, or alternatively add null checks in each of
footerBuildSucceeded(), footerBuildSkipped(), and footerBuildFailed() to bail
out or handle the missing manager before calling methods on $promptManager.

In @.vortex/installer/src/Utils/FileManager.php:
- Around line 35-42: Replace the passthru() call with an exec() (or proc_open)
variant that captures both stdout/stderr and the exit code when running the git
init command (the block using passthru(sprintf('git --work-tree="%s"
--git-dir="%s/.git" init > /dev/null', $dst, $dst)) and the subsequent
File::exists($dst . '/.git') check); after running the command, verify the exit
code first and if non-zero throw the RuntimeException including the captured
output/exit code for diagnostics, otherwise proceed to the File::exists check
and keep the existing exception path but augment its message with captured git
output when available.
- Around line 98-138: The prepareDemo method currently returns strings on early
exits and an array on success; change it to always return an array to make the
API consistent: update the function signature to return array (remove string
union) and replace each early string return (the 'Not a demo mode.' check, the
IS_DEMO_DB_DOWNLOAD_SKIP message, the missing VORTEX_DOWNLOAD_DB_URL message,
and the existing DB file found message) with a one-element array containing the
same message (or push into $messages and return $messages), and ensure the final
return remains an array of messages; locate prepareDemo in FileManager (method
name prepareDemo, references to $this->config, Env::get, $downloader->download)
to apply these changes.

In @.vortex/installer/src/Utils/OptionsResolver.php:
- Around line 58-61: When reading a config file in OptionsResolver.php you must
handle file_get_contents returning false: check the result of
file_get_contents($config_candidate) and if it === false either set $config_json
to a safe default (e.g. '{}') or throw a clear exception before calling
Config::fromString; ensure you reference the existing variables
$config_candidate and $config_json and update the branch that currently assigns
$config_json to (string) file_get_contents(...) so it validates the read and
handles the failure case explicitly.

/**
* The prompt manager.
*/
protected ?PromptManager $promptManager = NULL;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential null pointer exception when promptManager is not set.

The $promptManager property is declared as nullable (Line 29), but footerBuildSucceeded(), footerBuildSkipped(), and footerBuildFailed() call methods on it without null checks. If setPromptManager() is not called before these methods, a fatal error will occur.

Proposed fix - add null checks or make non-nullable

Option 1: Add null checks in each method

   public function footerBuildSucceeded(): void {
     $output = '';
     $output .= 'Get site info: ahoy info' . PHP_EOL;
     $output .= 'Login:         ahoy login' . PHP_EOL;
     $output .= PHP_EOL;

-    $handler_output = $this->promptManager->runPostBuild(self::BUILD_RESULT_SUCCESS);
-    if (!empty($handler_output)) {
-      $output .= $handler_output;
+    if ($this->promptManager !== NULL) {
+      $handler_output = $this->promptManager->runPostBuild(self::BUILD_RESULT_SUCCESS);
+      if (!empty($handler_output)) {
+        $output .= $handler_output;
+      }
     }

     Tui::box($output, 'Site is ready');
   }

Option 2: Require PromptManager in constructor (preferred)

   public function __construct(
     protected Config $config,
+    protected PromptManager $promptManager,
   ) {}

-  public function setPromptManager(PromptManager $prompt_manager): void {
-    $this->promptManager = $prompt_manager;
-  }

Also applies to: 161-231

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.vortex/installer/src/Prompts/InstallerPresenter.php at line 29, The
nullable property $promptManager can cause fatal errors because
footerBuildSucceeded(), footerBuildSkipped(), and footerBuildFailed() call
methods on it without guards; fix by making PromptManager required: add a
PromptManager parameter to the InstallerPresenter constructor, assign it to the
$promptManager property and change its type to non-nullable (remove ?), and
remove setPromptManager() usage, or alternatively add null checks in each of
footerBuildSucceeded(), footerBuildSkipped(), and footerBuildFailed() to bail
out or handle the missing manager before calling methods on $promptManager.

Comment on lines +35 to +42
if (!is_readable($dst . '/.git')) {
$messages[] = sprintf('Initialising a new Git repository in directory "%s".', $dst);
passthru(sprintf('git --work-tree="%s" --git-dir="%s/.git" init > /dev/null', $dst, $dst));

if (!File::exists($dst . '/.git')) {
throw new \RuntimeException(sprintf('Unable to initialise Git repository in directory "%s".', $dst));
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider capturing git init output and exit code.

Using passthru() with output redirection to /dev/null discards both success and error output. While the subsequent File::exists check catches initialization failures, actual git error messages are lost, making debugging harder. Additionally, the exit code isn't checked before the file existence verification.

Proposed fix using exec()
     if (!is_readable($dst . '/.git')) {
       $messages[] = sprintf('Initialising a new Git repository in directory "%s".', $dst);
-      passthru(sprintf('git --work-tree="%s" --git-dir="%s/.git" init > /dev/null', $dst, $dst));
+      $output = [];
+      $return_code = 0;
+      exec(sprintf('git --work-tree="%s" --git-dir="%s/.git" init 2>&1', $dst, $dst), $output, $return_code);
 
-      if (!File::exists($dst . '/.git')) {
+      if ($return_code !== 0 || !File::exists($dst . '/.git')) {
         throw new \RuntimeException(sprintf('Unable to initialise Git repository in directory "%s".', $dst));
       }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!is_readable($dst . '/.git')) {
$messages[] = sprintf('Initialising a new Git repository in directory "%s".', $dst);
passthru(sprintf('git --work-tree="%s" --git-dir="%s/.git" init > /dev/null', $dst, $dst));
if (!File::exists($dst . '/.git')) {
throw new \RuntimeException(sprintf('Unable to initialise Git repository in directory "%s".', $dst));
}
}
if (!is_readable($dst . '/.git')) {
$messages[] = sprintf('Initialising a new Git repository in directory "%s".', $dst);
$output = [];
$return_code = 0;
exec(sprintf('git --work-tree="%s" --git-dir="%s/.git" init 2>&1', $dst, $dst), $output, $return_code);
if ($return_code !== 0 || !File::exists($dst . '/.git')) {
throw new \RuntimeException(sprintf('Unable to initialise Git repository in directory "%s".', $dst));
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.vortex/installer/src/Utils/FileManager.php around lines 35 - 42, Replace
the passthru() call with an exec() (or proc_open) variant that captures both
stdout/stderr and the exit code when running the git init command (the block
using passthru(sprintf('git --work-tree="%s" --git-dir="%s/.git" init >
/dev/null', $dst, $dst)) and the subsequent File::exists($dst . '/.git') check);
after running the command, verify the exit code first and if non-zero throw the
RuntimeException including the captured output/exit code for diagnostics,
otherwise proceed to the File::exists check and keep the existing exception path
but augment its message with captured git output when available.

Comment on lines +98 to +138
* @return array|string
* Array of messages or a single message.
*/
public function prepareDemo(Downloader $downloader): array|string {
if (empty($this->config->get(Config::IS_DEMO))) {
return 'Not a demo mode.';
}

if (!empty($this->config->get(Config::IS_DEMO_DB_DOWNLOAD_SKIP))) {
return sprintf('%s is set. Skipping demo database download.', Config::IS_DEMO_DB_DOWNLOAD_SKIP);
}

// Reload variables from destination's .env.
Env::putFromDotenv($this->config->getDst() . '/.env');

$url = Env::get('VORTEX_DOWNLOAD_DB_URL');
if (empty($url)) {
return 'No database download URL provided. Skipping demo database download.';
}

$data_dir = $this->config->getDst() . DIRECTORY_SEPARATOR . Env::get('VORTEX_DB_DIR', './.data');
$db_file = Env::get('VORTEX_DB_FILE', 'db.sql');

if (file_exists($data_dir . DIRECTORY_SEPARATOR . $db_file)) {
return 'Database dump file already exists. Skipping demo database download.';
}

$messages = [];
if (!file_exists($data_dir)) {
$data_dir = File::mkdir($data_dir);
$messages[] = sprintf('Created data directory "%s".', $data_dir);
}

$destination = $data_dir . DIRECTORY_SEPARATOR . $db_file;
$downloader->download($url, $destination);

$messages[] = sprintf('No database dump file was found in "%s" directory.', $data_dir);
$messages[] = sprintf('Downloaded demo database from %s.', $url);

return $messages;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Inconsistent return type reduces predictability.

The method returns string for early-exit cases (Lines 103, 107, 115, 122) but array for the success path (Line 137). This forces callers to handle both types, adding unnecessary complexity.

Proposed fix - always return array
-  public function prepareDemo(Downloader $downloader): array|string {
+  public function prepareDemo(Downloader $downloader): array {
     if (empty($this->config->get(Config::IS_DEMO))) {
-      return 'Not a demo mode.';
+      return ['Not a demo mode.'];
     }
 
     if (!empty($this->config->get(Config::IS_DEMO_DB_DOWNLOAD_SKIP))) {
-      return sprintf('%s is set. Skipping demo database download.', Config::IS_DEMO_DB_DOWNLOAD_SKIP);
+      return [sprintf('%s is set. Skipping demo database download.', Config::IS_DEMO_DB_DOWNLOAD_SKIP)];
     }
     // ... similar changes for other early returns
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.vortex/installer/src/Utils/FileManager.php around lines 98 - 138, The
prepareDemo method currently returns strings on early exits and an array on
success; change it to always return an array to make the API consistent: update
the function signature to return array (remove string union) and replace each
early string return (the 'Not a demo mode.' check, the IS_DEMO_DB_DOWNLOAD_SKIP
message, the missing VORTEX_DOWNLOAD_DB_URL message, and the existing DB file
found message) with a one-element array containing the same message (or push
into $messages and return $messages), and ensure the final return remains an
array of messages; locate prepareDemo in FileManager (method name prepareDemo,
references to $this->config, Env::get, $downloader->download) to apply these
changes.

Comment on lines +58 to +61
if (isset($options['config']) && is_scalar($options['config'])) {
$config_candidate = strval($options['config']);
$config_json = is_file($config_candidate) ? (string) file_get_contents($config_candidate) : $config_candidate;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle file_get_contents failure.

If the file exists but cannot be read, file_get_contents returns false, which when cast to string becomes an empty string. This would pass an empty string (not '{}') to Config::fromString, potentially causing unexpected behavior or JSON parsing errors downstream.

Proposed fix
     if (isset($options['config']) && is_scalar($options['config'])) {
       $config_candidate = strval($options['config']);
-      $config_json = is_file($config_candidate) ? (string) file_get_contents($config_candidate) : $config_candidate;
+      if (is_file($config_candidate)) {
+        $contents = file_get_contents($config_candidate);
+        if ($contents === FALSE) {
+          throw new \RuntimeException(sprintf('Unable to read config file: %s', $config_candidate));
+        }
+        $config_json = $contents;
+      }
+      else {
+        $config_json = $config_candidate;
+      }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (isset($options['config']) && is_scalar($options['config'])) {
$config_candidate = strval($options['config']);
$config_json = is_file($config_candidate) ? (string) file_get_contents($config_candidate) : $config_candidate;
}
if (isset($options['config']) && is_scalar($options['config'])) {
$config_candidate = strval($options['config']);
if (is_file($config_candidate)) {
$contents = file_get_contents($config_candidate);
if ($contents === FALSE) {
throw new \RuntimeException(sprintf('Unable to read config file: %s', $config_candidate));
}
$config_json = $contents;
}
else {
$config_json = $config_candidate;
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.vortex/installer/src/Utils/OptionsResolver.php around lines 58 - 61, When
reading a config file in OptionsResolver.php you must handle file_get_contents
returning false: check the result of file_get_contents($config_candidate) and if
it === false either set $config_json to a safe default (e.g. '{}') or throw a
clear exception before calling Config::fromString; ensure you reference the
existing variables $config_candidate and $config_json and update the branch that
currently assigns $config_json to (string) file_get_contents(...) so it
validates the read and handles the failure case explicitly.

@AlexSkrypnyk
Copy link
Member Author

Code Coverage Report:
  2026-02-19 06:41:42

 Summary:
  Classes:  0.00% (0/1)
  Methods:  0.00% (0/2)
  Lines:   94.65% (177/187)

@github-actions

This comment has been minimized.

@AlexSkrypnyk
Copy link
Member Author

Code Coverage Report:
  2026-02-19 06:41:54

 Summary:
  Classes:  0.00% (0/1)
  Methods:  0.00% (0/2)
  Lines:   94.65% (177/187)

@codecov
Copy link

codecov bot commented Feb 19, 2026

Codecov Report

❌ Patch coverage is 97.43590% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.09%. Comparing base (f2e93fc) to head (e49db5a).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
.vortex/installer/src/Utils/OptionsResolver.php 91.48% 4 Missing ⚠️
.vortex/installer/src/Utils/FileManager.php 96.22% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2308      +/-   ##
==========================================
- Coverage   78.20%   78.09%   -0.11%     
==========================================
  Files         121      117       -4     
  Lines        6400     6245     -155     
  Branches       44        0      -44     
==========================================
- Hits         5005     4877     -128     
+ Misses       1395     1368      -27     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.vortex/installer/tests/Unit/Utils/OptionsResolverTest.php:
- Around line 209-216: The test testResolveSetsIsVortexProject currently asserts
that Config::IS_VORTEX_PROJECT is not null, which can pass when the value is
TRUE; update the assertion to explicitly assert false so the SUT is confirmed
not to be a Vortex project: locate the testResolveSetsIsVortexProject method and
replace the assertNotNull($config->get(Config::IS_VORTEX_PROJECT)) with an
assertion that the value is false (e.g., use assertFalse on
$config->get(Config::IS_VORTEX_PROJECT)) to match the test comment and intent
when calling OptionsResolver::resolve.

Comment on lines +209 to +216
public function testResolveSetsIsVortexProject(): void {
$options = self::defaultOptions();

[$config] = OptionsResolver::resolve($options);

// The SUT directory should not be a Vortex project.
$this->assertNotNull($config->get(Config::IS_VORTEX_PROJECT));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Strengthen the Vortex project assertion.

Line 215 only asserts non-null, which will pass even if the value is TRUE. The comment says the SUT should not be a Vortex project, so this should assert false explicitly.

✅ Proposed fix
-    // The SUT directory should not be a Vortex project.
-    $this->assertNotNull($config->get(Config::IS_VORTEX_PROJECT));
+    // The SUT directory should not be a Vortex project.
+    $this->assertFalse((bool) $config->get(Config::IS_VORTEX_PROJECT));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.vortex/installer/tests/Unit/Utils/OptionsResolverTest.php around lines 209
- 216, The test testResolveSetsIsVortexProject currently asserts that
Config::IS_VORTEX_PROJECT is not null, which can pass when the value is TRUE;
update the assertion to explicitly assert false so the SUT is confirmed not to
be a Vortex project: locate the testResolveSetsIsVortexProject method and
replace the assertNotNull($config->get(Config::IS_VORTEX_PROJECT)) with an
assertion that the value is false (e.g., use assertFalse on
$config->get(Config::IS_VORTEX_PROJECT)) to match the test comment and intent
when calling OptionsResolver::resolve.

@AlexSkrypnyk
Copy link
Member Author

Code Coverage Report:
  2026-02-19 06:54:02

 Summary:
  Classes:  0.00% (0/1)
  Methods:  0.00% (0/2)
  Lines:   94.65% (177/187)

@github-actions
Copy link

Code Coverage Report:
  2026-02-19 06:54:19

 Summary:
  Classes:  0.00% (0/1)
  Methods:  0.00% (0/2)
  Lines:   94.65% (177/187)

@AlexSkrypnyk
Copy link
Member Author

Code Coverage Report:
  2026-02-19 06:57:36

 Summary:
  Classes:  0.00% (0/1)
  Methods:  0.00% (0/2)
  Lines:   94.65% (177/187)

@AlexSkrypnyk
Copy link
Member Author

Code Coverage Report:
  2026-02-19 06:57:49

 Summary:
  Classes:  0.00% (0/1)
  Methods:  0.00% (0/2)
  Lines:   94.65% (177/187)

@AlexSkrypnyk AlexSkrypnyk merged commit 2183229 into main Feb 19, 2026
28 checks passed
@AlexSkrypnyk AlexSkrypnyk deleted the feature/improve-installer-feb26 branch February 19, 2026 22:20
@github-project-automation github-project-automation bot moved this from BACKLOG to Release queue in Vortex Feb 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Release queue

Development

Successfully merging this pull request may close these issues.

1 participant