Skip to content

feat: server crud commands - #27

Merged
loadinglucian merged 3 commits into
mainfrom
feat/server-crud-commands
Oct 4, 2025
Merged

feat: server crud commands#27
loadinglucian merged 3 commits into
mainfrom
feat/server-crud-commands

Conversation

@loadinglucian

@loadinglucian loadinglucian commented Oct 4, 2025

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Added CLI commands: server:add, server:list, and server:delete.
    • Interactive prompts with host/port validation, optional SSH connectivity test, and clear confirmation flow.
    • Enhanced server info display with defaults and helpful usage hints.
  • Documentation
    • Clarified that static analysis applies to production code only; tests are excluded.
  • Tests
    • Added comprehensive integration tests for add/list/delete commands.
    • Added unit tests for server info display and validation helpers.

@coderabbitai

coderabbitai Bot commented Oct 4, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds three new server CLI commands (add, delete, list), introduces shared server helper/validation traits, injects SSHService into BaseCommand, adjusts SSHService exception handling, and registers commands in SymfonyApp. Updates docs to exclude tests from PHPStan. Expands tests with new integration and unit coverage and updates fixtures/helpers for SSH wiring.

Changes

Cohort / File(s) Summary of changes
Docs: Static analysis rules
.cursor/rules/01-architecture.mdc, .cursor/rules/02-tests.mdc
Clarify PHPStan applies only to production code; tests excluded.
Server CLI Commands
app/Console/Server/*
Add ServerAddCommand (options/prompts, validate host/port, optional SSH test, save), ServerDeleteCommand (select/confirm/delete), ServerListCommand (list inventory).
Base command wiring
app/Contracts/BaseCommand.php
Inject protected readonly SSHService via constructor; add import.
SSH service behavior
app/Services/SSHService.php
Remove outer constructor try-catch; preserve login try-catch; simplify exception messages.
App command registration
app/SymfonyApp.php
Register new server commands.
Reusable traits
app/Traits/ServerHelpersTrait.php, app/Traits/ServerValidationTrait.php
Add displayServerInfo helper; add validateHost/validatePort methods.
Test fixtures/helpers
tests/Fixtures/TestConsoleCommand.php, tests/TestHelpers.php
Wire SSHService into fixtures; add mock SSHService helpers; expose displayServerInfo test path.
Integration tests
tests/Integration/Console/Server/*
Add end-to-end tests for add, delete, list commands covering success/edge cases and output.
Unit tests
tests/Unit/Contracts/BaseCommandTest.php, tests/Unit/Traits/ServerHelpersTraitTest.php, tests/Unit/Traits/ServerValidationTraitTest.php
Update BaseCommand tests for SSH injection; add tests for helper output and validation logic.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant CLI as Symfony Console
  participant Cmd as ServerAddCommand
  participant Repo as ServerRepository
  participant SSH as SSHService

  User->>CLI: server:add [--name --host --port --user --key --skip]
  CLI->>Cmd: execute()
  Cmd->>Cmd: validateHost/validatePort
  alt skip SSH test
    Note over Cmd: SSH check skipped
  else test SSH
    Cmd->>SSH: connect(host, port, user, key)
    SSH-->>Cmd: success/failure
    alt failure
      Cmd-->>User: show troubleshooting + hint
      Cmd-->>CLI: return FAILURE
      end
  end
  Cmd->>User: prompt confirm save
  alt confirmed
    Cmd->>Repo: save(ServerDTO)
    Repo-->>Cmd: ok
    Cmd-->>User: success + usage hint
  else declined
    Cmd-->>User: cancelled
  end
  Cmd-->>CLI: return SUCCESS
Loading
sequenceDiagram
  autonumber
  actor User
  participant CLI as Symfony Console
  participant Cmd as ServerDeleteCommand
  participant Repo as ServerRepository

  User->>CLI: server:delete [--name] [-y]
  CLI->>Cmd: execute()
  Cmd->>Repo: all()
  alt none found
    Cmd-->>User: no servers guidance
    Cmd-->>CLI: SUCCESS
  else servers exist
    alt name not provided
      Cmd->>User: select server
    end
    Cmd->>Repo: find by name
    alt not found
      Cmd-->>User: error
      Cmd-->>CLI: FAILURE
    else found
      Cmd-->>User: show server info
      alt -y provided
      else prompt confirm
      end
      alt confirmed
        Cmd->>Repo: delete(name)
        Cmd-->>User: deleted + hint
        Cmd-->>CLI: SUCCESS
      else declined
        Cmd-->>User: cancelled
        Cmd-->>CLI: SUCCESS
      end
    end
  end
Loading
sequenceDiagram
  autonumber
  actor User
  participant CLI as Symfony Console
  participant Cmd as ServerListCommand
  participant Repo as ServerRepository

  User->>CLI: server:list
  CLI->>Cmd: execute()
  Cmd->>Repo: all()
  alt empty
    Cmd-->>User: warning + add hint
  else non-empty
    loop each server
      Cmd-->>User: displayServerInfo
    end
  end
  Cmd-->>CLI: SUCCESS
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • bigpixelrocket/deployer-php#22 — Both modify SSHService; constructor/exception handling and filesystem integration are closely related.
  • bigpixelrocket/deployer-php#19 — Both change app/Contracts/BaseCommand.php constructor/dependencies affecting command wiring.
  • bigpixelrocket/deployer-php#26 — Both update test fixtures/helpers and command I/O scaffolding; overlapping console/test wiring changes.

Poem

A rabbit taps the terminal key,
Adds, lists, deletes—so swiftly, whee! 🐇
Hosts validated, ports aligned,
SSH whispers, “All is fine.”
Inventory neat, carrots in rows—
Onward the deployer burrow goes! 🌱✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.84% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ 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 concisely captures the primary addition of server CRUD commands, directly reflecting the main functionality introduced by this pull request without extraneous detail.
✨ 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 feat/server-crud-commands

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (3)
.cursor/rules/01-architecture.mdc (1)

144-145: Consider removing extra space in Markdown formatting.

There's an extra space before the closing ** in "Important: **". For consistency with Markdown conventions, consider removing it.

Apply this diff:

-**Important: ** Don't run PHPStan on test files; tests are excluded from static analysis.
+**Important:** Don't run PHPStan on test files; tests are excluded from static analysis.
app/Traits/ServerHelpersTrait.php (1)

7-33: Drop the unused use imports

ServerRepository and SSHService are never referenced in this trait. Please remove the unused imports so the file stays PSR‑12 compliant.

-use Bigpixelrocket\DeployerPHP\Repositories\ServerRepository;
-use Bigpixelrocket\DeployerPHP\Services\SSHService;
tests/Integration/Console/Server/ServerAddCommandTest.php (1)

48-57: Inconsistent output buffering and repetitive pattern.

Output buffering with ob_start()/ob_end_clean() is repeated across multiple tests to suppress Laravel Prompts output. However:

  1. The test at line 306-314 ("displays complete server information before saving") omits this pattern despite also using --yes.
  2. The repetition creates maintenance overhead.

Consider extracting this pattern:

describe('ServerAddCommand', function () {
    beforeEach(function () {
        ob_start();
    });

    afterEach(function () {
        ob_end_clean();
    });

    it('adds server with all options provided non-interactively', function () {
        // ... test without explicit ob_start/ob_end_clean
    });
});

Alternatively, create a helper function:

function executeWithOutputSuppressed(CommandTester $tester, array $input): int
{
    ob_start();
    try {
        return $tester->execute($input);
    } finally {
        ob_end_clean();
    }
}

Then update line 306 to add output buffering for consistency:

     it('displays complete server information before saving', function () {
         // ARRANGE
         $sshService = mockSSHServiceWithBehavior(true);
         $tester = createServerAddCommandTester($sshService);

         // ACT
+        ob_start();
         $tester->execute([
             '--name' => 'display-test',
             '--host' => 'example.com',
             '--port' => '22',
             '--username' => 'deployer',
             '--private-key-path' => '~/.ssh/key',
             '--skip' => true,
             '--yes' => true,
         ]);
+        ob_end_clean();

Also applies to: 76-82, 100-107, 123-130, 186-192, 217-224, 243-250, 278-287, 336-343

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between de15750 and de66807.

📒 Files selected for processing (18)
  • .cursor/rules/01-architecture.mdc (1 hunks)
  • .cursor/rules/02-tests.mdc (1 hunks)
  • app/Console/Server/ServerAddCommand.php (1 hunks)
  • app/Console/Server/ServerDeleteCommand.php (1 hunks)
  • app/Console/Server/ServerListCommand.php (1 hunks)
  • app/Contracts/BaseCommand.php (2 hunks)
  • app/Services/SSHService.php (1 hunks)
  • app/SymfonyApp.php (2 hunks)
  • app/Traits/ServerHelpersTrait.php (1 hunks)
  • app/Traits/ServerValidationTrait.php (1 hunks)
  • tests/Fixtures/TestConsoleCommand.php (4 hunks)
  • tests/Integration/Console/Server/ServerAddCommandTest.php (1 hunks)
  • tests/Integration/Console/Server/ServerDeleteCommandTest.php (1 hunks)
  • tests/Integration/Console/Server/ServerListCommandTest.php (1 hunks)
  • tests/TestHelpers.php (3 hunks)
  • tests/Unit/Contracts/BaseCommandTest.php (4 hunks)
  • tests/Unit/Traits/ServerHelpersTraitTest.php (1 hunks)
  • tests/Unit/Traits/ServerValidationTraitTest.php (1 hunks)
🧰 Additional context used
📓 Path-based instructions (8)
**/*.php

📄 CodeRabbit inference engine (.cursor/rules/00-main.mdc)

**/*.php: Eliminate single-use private methods by inlining them directly
Cache expensive computed values by initializing them in the constructor instead of recomputing
Prefer direct property access over method calls when appropriate to avoid call overhead
Organize code into comment-separated sections; prefer alphabetical ordering when it does not conflict with logical grouping

**/*.php: Follow PSR-12, declare strict types, and use PHP 8.x features (unions, match, attributes, readonly)
Use import statements (use) instead of fully qualified class names
All methods must have explicit return types; use proper generics in phpdoc (e.g., Collection<int, User>)
Use dependency injection rather than manually resolving/instantiating classes
Prefer Symfony component classes (e.g., Filesystem, Process) over native PHP functions for mockability
All object creation must use $container->build(ClassName::class) except for value objects, DTOs, and pure data structures
In production code, access the container through constructor injection (no service locator pattern)
Add DocBlock comments with minimalist descriptions, parameters, and return types for classes and functions
Use comments as visual separators for sections/subsections; avoid obvious or stale comments

Files:

  • app/SymfonyApp.php
  • tests/Unit/Contracts/BaseCommandTest.php
  • tests/Unit/Traits/ServerHelpersTraitTest.php
  • tests/TestHelpers.php
  • app/Console/Server/ServerAddCommand.php
  • tests/Unit/Traits/ServerValidationTraitTest.php
  • tests/Integration/Console/Server/ServerListCommandTest.php
  • app/Console/Server/ServerListCommand.php
  • app/Traits/ServerValidationTrait.php
  • app/Console/Server/ServerDeleteCommand.php
  • tests/Fixtures/TestConsoleCommand.php
  • app/Traits/ServerHelpersTrait.php
  • tests/Integration/Console/Server/ServerDeleteCommandTest.php
  • tests/Integration/Console/Server/ServerAddCommandTest.php
  • app/Services/SSHService.php
  • app/Contracts/BaseCommand.php
{tests/**,test/**,**/*@(Test|Spec).php}

📄 CodeRabbit inference engine (.cursor/rules/00-main.mdc)

Do not run or edit tests unless explicitly instructed

Files:

  • tests/Unit/Contracts/BaseCommandTest.php
  • tests/Unit/Traits/ServerHelpersTraitTest.php
  • tests/TestHelpers.php
  • tests/Unit/Traits/ServerValidationTraitTest.php
  • tests/Integration/Console/Server/ServerListCommandTest.php
  • tests/Fixtures/TestConsoleCommand.php
  • tests/Integration/Console/Server/ServerDeleteCommandTest.php
  • tests/Integration/Console/Server/ServerAddCommandTest.php
tests/**/*.php

📄 CodeRabbit inference engine (.cursor/rules/02-tests.mdc)

tests/**/*.php: Use Pest exclusively with it() syntax for tests
In unit tests, instantiate classes manually with explicit mocks; use the container only for integration tests
Keep individual test files under ~1.8x the size of the source they cover
Test core business logic; avoid testing the framework itself
Prefer dataset-driven testing with ->with([...]) for multiple scenarios
Eliminate overlapping tests; avoid two tests covering the same functionality
Consolidate assertions with expect(...)->and(...) when appropriate
Mock external dependencies only; do not mock internal behavior
Avoid performance tests unless performance is the primary concern
Do not sacrifice readability to hit size/ratio targets
Follow the AAA pattern (Arrange, Act, Assert) in tests; use ACT & ASSERT for exception tests when act triggers assertion
Organize tests with describe() blocks, beforeEach() setup, and extract helpers/traits for DRY
Forbid meaningless assertions (e.g., toBeTrue, not->toBeNull, type-only checks, sleep(...)); use time mocking instead of sleep
Prefer meaningful assertions on behavior and results, and proper mocks (e.g., expect domain outputs; mock->shouldReceive(...))
Unit tests: mock all external dependencies; test single units in isolation; execute in milliseconds
Integration tests: perform real file operations/external processes; cover CLI commands and full workflows
Ignore PHPStan issues in tests; avoid excessive PHPDoc added only to appease types

Files:

  • tests/Unit/Contracts/BaseCommandTest.php
  • tests/Unit/Traits/ServerHelpersTraitTest.php
  • tests/TestHelpers.php
  • tests/Unit/Traits/ServerValidationTraitTest.php
  • tests/Integration/Console/Server/ServerListCommandTest.php
  • tests/Fixtures/TestConsoleCommand.php
  • tests/Integration/Console/Server/ServerDeleteCommandTest.php
  • tests/Integration/Console/Server/ServerAddCommandTest.php
{test,tests}/**/*.php

📄 CodeRabbit inference engine (.cursor/rules/01-architecture.mdc)

In tests, you may instantiate the container directly and build services via $container->build()

Files:

  • tests/Unit/Contracts/BaseCommandTest.php
  • tests/Unit/Traits/ServerHelpersTraitTest.php
  • tests/TestHelpers.php
  • tests/Unit/Traits/ServerValidationTraitTest.php
  • tests/Integration/Console/Server/ServerListCommandTest.php
  • tests/Fixtures/TestConsoleCommand.php
  • tests/Integration/Console/Server/ServerDeleteCommandTest.php
  • tests/Integration/Console/Server/ServerAddCommandTest.php
**/*Command.php

📄 CodeRabbit inference engine (.cursor/rules/01-architecture.mdc)

**/*Command.php: Commands handle user interaction (input/output) and orchestrate Services
Commands must not contain business logic—delegate to Services
Commands must not duplicate orchestration logic—extract to shared Services
Commands are responsible for console styling, error formatting, and user prompts (use SymfonyStyle)
Commands should not invoke other commands (no proxy commands)
Use SymfonyStyle consistently for all user-facing console output

Files:

  • app/Console/Server/ServerAddCommand.php
  • app/Console/Server/ServerListCommand.php
  • app/Console/Server/ServerDeleteCommand.php
  • tests/Fixtures/TestConsoleCommand.php
  • app/Contracts/BaseCommand.php
**/*{Command,Service}.php

📄 CodeRabbit inference engine (.cursor/rules/01-architecture.mdc)

**/*{Command,Service}.php: Declare all dependencies in constructor signatures (no hidden/global dependencies)
No circular dependencies between classes

Files:

  • app/Console/Server/ServerAddCommand.php
  • app/Console/Server/ServerListCommand.php
  • app/Console/Server/ServerDeleteCommand.php
  • tests/Fixtures/TestConsoleCommand.php
  • app/Services/SSHService.php
  • app/Contracts/BaseCommand.php
**/*Service.php

📄 CodeRabbit inference engine (.cursor/rules/01-architecture.mdc)

**/*Service.php: Services provide atomic, reusable functionality with no console I/O
Services accept and return plain PHP data types (no console I/O objects)
Services must be dependency-injected via constructor
Services implement core business logic, external API calls, and file operations (no user I/O)
Extract complex orchestration shared by multiple Commands into dedicated Services
Stateless services perform pure operations; stateful services manage config/caches
Stateful services should use lazy loading when initialization is expensive or path-dependent
State must be initialized explicitly via public methods (e.g., load(), initialize()) before use
Services should document stateful nature and initialization requirements
Only Commands perform console I/O; Services must not perform console input/output
Services return exceptions or structured data; commands handle display
Validation errors and business exceptions should bubble up to Commands for display

Files:

  • app/Services/SSHService.php
app/Contracts/BaseCommand.php

📄 CodeRabbit inference engine (.cursor/rules/03-commands.mdc)

Keep BaseCommand limited to shared initialization, configuration, and orchestration logic; do not implement individual I/O operations here

Files:

  • app/Contracts/BaseCommand.php
🧠 Learnings (3)
📚 Learning: 2025-10-04T12:08:00.471Z
Learnt from: CR
PR: bigpixelrocket/deployer-php#0
File: .cursor/rules/01-architecture.mdc:0-0
Timestamp: 2025-10-04T12:08:00.471Z
Learning: Run phpstan static analysis on changed PHP files excluding tests; never run static analysis against tests

Applied to files:

  • .cursor/rules/01-architecture.mdc
  • .cursor/rules/02-tests.mdc
📚 Learning: 2025-10-02T19:48:48.339Z
Learnt from: CR
PR: bigpixelrocket/deployer-php#0
File: .cursor/rules/02-tests.mdc:0-0
Timestamp: 2025-10-02T19:48:48.339Z
Learning: Applies to tests/**/*.php : Ignore PHPStan issues in tests; avoid excessive PHPDoc added only to appease types

Applied to files:

  • .cursor/rules/01-architecture.mdc
  • .cursor/rules/02-tests.mdc
📚 Learning: 2025-10-02T19:48:48.339Z
Learnt from: CR
PR: bigpixelrocket/deployer-php#0
File: .cursor/rules/02-tests.mdc:0-0
Timestamp: 2025-10-02T19:48:48.339Z
Learning: Applies to tests/**/*.php : Avoid performance tests unless performance is the primary concern

Applied to files:

  • .cursor/rules/02-tests.mdc
🧬 Code graph analysis (13)
tests/Unit/Contracts/BaseCommandTest.php (4)
app/Services/SSHService.php (1)
  • SSHService (40-327)
tests/Fixtures/TestConsoleCommand.php (1)
  • __construct (31-39)
tests/TestHelpers.php (3)
  • __construct (52-64)
  • __construct (279-282)
  • mockSSHService (261-267)
app/Contracts/BaseCommand.php (1)
  • __construct (35-43)
tests/Unit/Traits/ServerHelpersTraitTest.php (4)
app/DTOs/ServerDTO.php (1)
  • ServerDTO (7-17)
tests/TestHelpers.php (1)
  • mockTestConsoleCommand (345-358)
tests/Fixtures/TestConsoleCommand.php (2)
  • setTestMethod (44-48)
  • execute (59-90)
tests/Unit/Contracts/BaseCommandTest.php (1)
  • execute (43-48)
tests/TestHelpers.php (4)
app/Services/SSHService.php (6)
  • SSHService (40-327)
  • assertCanConnect (57-61)
  • executeCommand (70-87)
  • executeScript (96-121)
  • uploadFile (128-148)
  • downloadFile (155-171)
tests/Fixtures/TestConsoleCommand.php (2)
  • __construct (31-39)
  • TestConsoleCommand (24-234)
tests/Unit/Contracts/BaseCommandTest.php (1)
  • __construct (26-35)
app/Contracts/BaseCommand.php (1)
  • __construct (35-43)
app/Console/Server/ServerAddCommand.php (7)
app/Contracts/BaseCommand.php (3)
  • BaseCommand (26-133)
  • configure (52-69)
  • execute (111-132)
app/DTOs/ServerDTO.php (1)
  • ServerDTO (7-17)
app/Traits/ConsoleOutputTrait.php (3)
  • hr (85-91)
  • writeln (23-29)
  • showCommandHint (102-138)
app/Traits/ConsoleInputTrait.php (4)
  • getOptionOrPrompt (58-76)
  • promptText (106-124)
  • promptConfirm (166-182)
  • promptSpin (345-353)
app/Traits/ServerValidationTrait.php (2)
  • validateHost (17-28)
  • validatePort (35-43)
app/Traits/ServerHelpersTrait.php (1)
  • displayServerInfo (23-33)
app/Services/SSHService.php (1)
  • assertCanConnect (57-61)
tests/Unit/Traits/ServerValidationTraitTest.php (1)
app/Traits/ServerValidationTrait.php (2)
  • validateHost (17-28)
  • validatePort (35-43)
tests/Integration/Console/Server/ServerListCommandTest.php (4)
app/DTOs/ServerDTO.php (1)
  • ServerDTO (7-17)
tests/TestHelpers.php (3)
  • mockEnvService (144-152)
  • mockInventoryService (160-177)
  • mockSSHService (261-267)
app/Services/InventoryService.php (1)
  • loadInventoryFile (97-111)
app/Repositories/ServerRepository.php (2)
  • ServerRepository (15-168)
  • loadInventory (31-43)
app/Console/Server/ServerListCommand.php (4)
app/Contracts/BaseCommand.php (2)
  • BaseCommand (26-133)
  • execute (111-132)
app/Traits/ConsoleOutputTrait.php (4)
  • hr (85-91)
  • warning (54-57)
  • writeln (23-29)
  • h1 (74-80)
app/Repositories/ServerRepository.php (1)
  • all (83-93)
app/Traits/ServerHelpersTrait.php (1)
  • displayServerInfo (23-33)
app/Console/Server/ServerDeleteCommand.php (6)
app/Contracts/BaseCommand.php (3)
  • BaseCommand (26-133)
  • configure (52-69)
  • execute (111-132)
app/DTOs/ServerDTO.php (1)
  • ServerDTO (7-17)
app/Traits/ConsoleOutputTrait.php (7)
  • hr (85-91)
  • warning (54-57)
  • writeln (23-29)
  • h1 (74-80)
  • error (62-65)
  • success (46-49)
  • showCommandHint (102-138)
app/Repositories/ServerRepository.php (1)
  • all (83-93)
app/Traits/ConsoleInputTrait.php (3)
  • getOptionOrPrompt (58-76)
  • promptSelect (210-228)
  • promptConfirm (166-182)
app/Traits/ServerHelpersTrait.php (1)
  • displayServerInfo (23-33)
tests/Fixtures/TestConsoleCommand.php (2)
app/Contracts/BaseCommand.php (2)
  • BaseCommand (26-133)
  • __construct (35-43)
app/Traits/ServerHelpersTrait.php (1)
  • displayServerInfo (23-33)
app/Traits/ServerHelpersTrait.php (2)
app/DTOs/ServerDTO.php (1)
  • ServerDTO (7-17)
app/Traits/ConsoleOutputTrait.php (1)
  • writeln (23-29)
tests/Integration/Console/Server/ServerDeleteCommandTest.php (5)
app/Container.php (1)
  • Container (23-227)
app/DTOs/ServerDTO.php (1)
  • ServerDTO (7-17)
tests/TestHelpers.php (3)
  • mockEnvService (144-152)
  • mockInventoryService (160-177)
  • mockSSHService (261-267)
app/Services/InventoryService.php (1)
  • loadInventoryFile (97-111)
app/Repositories/ServerRepository.php (2)
  • ServerRepository (15-168)
  • loadInventory (31-43)
tests/Integration/Console/Server/ServerAddCommandTest.php (6)
app/Container.php (1)
  • Container (23-227)
app/Services/SSHService.php (1)
  • SSHService (40-327)
tests/TestHelpers.php (4)
  • mockEnvService (144-152)
  • mockInventoryService (160-177)
  • mockSSHService (261-267)
  • mockSSHServiceWithBehavior (276-338)
app/Services/InventoryService.php (1)
  • loadInventoryFile (97-111)
app/Repositories/ServerRepository.php (3)
  • ServerRepository (15-168)
  • loadInventory (31-43)
  • findByName (65-76)
tests/Fixtures/TestConsoleCommand.php (1)
  • execute (59-90)
app/Contracts/BaseCommand.php (1)
app/Services/SSHService.php (1)
  • SSHService (40-327)
🪛 PHPMD (2.15.0)
tests/TestHelpers.php

300-300: Avoid unused parameters such as '$command'. (undefined)

(UnusedFormalParameter)


310-310: Avoid unused parameters such as '$scriptPath'. (undefined)

(UnusedFormalParameter)


328-328: Avoid unused parameters such as '$host'. (undefined)

(UnusedFormalParameter)


329-329: Avoid unused parameters such as '$port'. (undefined)

(UnusedFormalParameter)


330-330: Avoid unused parameters such as '$username'. (undefined)

(UnusedFormalParameter)


331-331: Avoid unused parameters such as '$remotePath'. (undefined)

(UnusedFormalParameter)


332-332: Avoid unused parameters such as '$localPath'. (undefined)

(UnusedFormalParameter)


333-333: Avoid unused parameters such as '$privateKeyPath'. (undefined)

(UnusedFormalParameter)

🔇 Additional comments (12)
.cursor/rules/02-tests.mdc (1)

126-127: LGTM!

The clarification that PHPStan applies to production code, not tests, is helpful and aligns with the project's testing philosophy.

app/SymfonyApp.php (1)

8-10: LGTM!

The new server commands are properly imported and registered using the existing DI container pattern. The implementation is consistent with the established architecture.

Also applies to: 120-122

tests/Unit/Traits/ServerHelpersTraitTest.php (1)

1-84: LGTM!

The test suite provides good coverage of the ServerHelpersTrait functionality with clear scenarios for all fields, default SSH key display, and default values. The tests follow the AAA pattern and use appropriate assertions.

tests/Integration/Console/Server/ServerDeleteCommandTest.php (1)

1-229: LGTM!

The integration test suite provides comprehensive coverage of ServerDeleteCommand functionality including success scenarios, error handling, and display verification. The tests are well-structured and follow the AAA pattern consistently.

app/Console/Server/ServerListCommand.php (1)

1-56: LGTM!

The ServerListCommand implementation is clean and follows established patterns. It provides good user experience with helpful guidance when the inventory is empty and uses the shared ServerHelpersTrait for consistent display formatting.

app/Contracts/BaseCommand.php (1)

11-11: Verified: all BaseCommand subclasses updated with SSHService TestableBaseCommand and TestConsoleCommand now accept SSHService in their constructors.

tests/Integration/Console/Server/ServerAddCommandTest.php (3)

17-31: LGTM!

The helper function correctly instantiates the command with mocked dependencies. The pattern follows the coding guidelines that permit direct container instantiation in tests.


263-298: Excellent integration test for persistence.

This test properly verifies that server data persists to the inventory by checking all fields of the retrieved server. The comprehensive assertions ensure data integrity across the full stack.


144-178: Good use of dataset-driven testing.

The validation tests properly leverage Pest's ->with() syntax to cover multiple invalid inputs without duplication. This approach aligns with the coding guidelines.

app/Console/Server/ServerDeleteCommand.php (3)

1-35: LGTM!

The command class follows best practices:

  • Uses PHP 8 attributes for command metadata per Symfony 7.3 guidelines
  • Extends BaseCommand with proper dependency injection via constructor promotion
  • Uses ServerHelpersTrait for consistent display formatting
  • Options are properly configured

50-76: LGTM!

The server selection logic is well-implemented:

  • Handles empty inventory gracefully with helpful guidance
  • Properly extracts server names from DTOs for the selection prompt
  • Uses getOptionOrPrompt pattern consistently with other commands

Returning Command::SUCCESS when no servers exist (line 59) is appropriate for idempotent CLI operations.


99-132: LGTM!

The confirmation and deletion flow is well-implemented:

  • Respects user cancellation by returning Command::SUCCESS (appropriate for user choice)
  • Uses repository for deletion (proper separation of concerns)
  • Provides helpful command hint for non-interactive usage

Comment on lines +107 to +153
$privateKeyPathRaw = $this->getOptionOrPrompt(
'private-key-path',
fn (): string => $this->promptText(
label: 'SSH private key path (leave empty for default ~/.ssh/id_ed25519 or ~/.ssh/id_rsa):',
default: '',
required: false
)
);

/** @var ?string $privateKeyPath */
$privateKeyPath = $privateKeyPathRaw !== '' ? $privateKeyPathRaw : null;

//
// Create DTO and display server info

$server = new ServerDTO(
name: $name,
host: $host,
port: $port,
username: $username,
privateKeyPath: $privateKeyPath
);

$this->hr();

$this->displayServerInfo($server);

//
// Verify connectivity

/** @var bool $skipCheck */
$skipCheck = $this->getOptionOrPrompt(
'skip',
fn (): bool => !$this->promptConfirm(
label: 'Test SSH connection before saving?',
default: true
)
);

if ($skipCheck) {
$this->warning('Skipping SSH connection check');
$this->writeln('');
} else {
if (!$this->testConnection($server)) {
return Command::FAILURE;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Allow non-interactive runs to skip the key prompt

Calling getOptionOrPrompt('private-key-path', …) always triggers a prompt when the option is omitted. In non-interactive workflows (CI, automation) the command hangs/fails even with --yes/--skip, so users cannot rely on the default key lookup. Please fall back to the prompt only when the input is interactive; otherwise treat the missing option as null.

-        /** @var string $privateKeyPathRaw */
-        $privateKeyPathRaw = $this->getOptionOrPrompt(
-            'private-key-path',
-            fn (): string => $this->promptText(
-                label: 'SSH private key path (leave empty for default ~/.ssh/id_ed25519 or ~/.ssh/id_rsa):',
-                default: '',
-                required: false
-            )
-        );
-
-        /** @var ?string $privateKeyPath */
-        $privateKeyPath = $privateKeyPathRaw !== '' ? $privateKeyPathRaw : null;
+        /** @var string|null $privateKeyPathOption */
+        $privateKeyPathOption = $this->input->getOption('private-key-path');
+
+        if (is_string($privateKeyPathOption) && $privateKeyPathOption !== '') {
+            $privateKeyPath = $privateKeyPathOption;
+        } elseif ($this->input->isInteractive()) {
+            $privateKeyPathRaw = $this->promptText(
+                label: 'SSH private key path (leave empty for default ~/.ssh/id_ed25519 or ~/.ssh/id_rsa):',
+                default: '',
+                required: false
+            );
+            $privateKeyPath = $privateKeyPathRaw !== '' ? $privateKeyPathRaw : null;
+        } else {
+            $privateKeyPath = null;
+        }
📝 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
$privateKeyPathRaw = $this->getOptionOrPrompt(
'private-key-path',
fn (): string => $this->promptText(
label: 'SSH private key path (leave empty for default ~/.ssh/id_ed25519 or ~/.ssh/id_rsa):',
default: '',
required: false
)
);
/** @var ?string $privateKeyPath */
$privateKeyPath = $privateKeyPathRaw !== '' ? $privateKeyPathRaw : null;
//
// Create DTO and display server info
$server = new ServerDTO(
name: $name,
host: $host,
port: $port,
username: $username,
privateKeyPath: $privateKeyPath
);
$this->hr();
$this->displayServerInfo($server);
//
// Verify connectivity
/** @var bool $skipCheck */
$skipCheck = $this->getOptionOrPrompt(
'skip',
fn (): bool => !$this->promptConfirm(
label: 'Test SSH connection before saving?',
default: true
)
);
if ($skipCheck) {
$this->warning('Skipping SSH connection check');
$this->writeln('');
} else {
if (!$this->testConnection($server)) {
return Command::FAILURE;
}
}
/** @var string|null $privateKeyPathOption */
$privateKeyPathOption = $this->input->getOption('private-key-path');
if (is_string($privateKeyPathOption) && $privateKeyPathOption !== '') {
$privateKeyPath = $privateKeyPathOption;
} elseif ($this->input->isInteractive()) {
$privateKeyPathRaw = $this->promptText(
label: 'SSH private key path (leave empty for default ~/.ssh/id_ed25519 or ~/.ssh/id_rsa):',
default: '',
required: false
);
$privateKeyPath = $privateKeyPathRaw !== '' ? $privateKeyPathRaw : null;
} else {
$privateKeyPath = null;
}
//
// Create DTO and display server info
$server = new ServerDTO(
name: $name,
host: $host,
port: $port,
username: $username,
privateKeyPath: $privateKeyPath
);
🤖 Prompt for AI Agents
In app/Console/Server/ServerAddCommand.php around lines 107-153, the private-key
prompt is always invoked when the option is omitted, blocking non-interactive
runs; change the logic to first read the option directly and only call the
interactive prompt when the input is actually interactive: retrieve the raw
option value (null/empty if not provided), and if it's null and
$this->input->isInteractive() is true then call getOptionOrPrompt to prompt the
user; otherwise treat missing/empty option as null and proceed (preserving the
existing conversion from empty string to null and the rest of the flow).

Comment on lines +81 to +94
$server = null;
foreach ($allServers as $s) {
if ($s->name === $name) {
$server = $s;
break;
}
}

if ($server === null) {
$this->error("Server '{$name}' not found in inventory");
return Command::FAILURE;
}

$this->displayServerInfo($server);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use repository method instead of manual lookup.

Lines 82-87 manually loop through servers to find a match by name, duplicating logic that already exists in ServerRepository::findByName().

Apply this diff to use the repository method:

-    //
-    // Find server and display info
-
-    $server = null;
-    foreach ($allServers as $s) {
-        if ($s->name === $name) {
-            $server = $s;
-            break;
-        }
-    }
+    //
+    // Find server and display info
+
+    $server = $this->servers->findByName($name);

     if ($server === null) {
         $this->error("Server '{$name}' not found in inventory");
         return Command::FAILURE;
     }

This reduces duplication and improves maintainability by relying on the repository's existing method.

📝 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
$server = null;
foreach ($allServers as $s) {
if ($s->name === $name) {
$server = $s;
break;
}
}
if ($server === null) {
$this->error("Server '{$name}' not found in inventory");
return Command::FAILURE;
}
$this->displayServerInfo($server);
//
// Find server and display info
$server = $this->servers->findByName($name);
if ($server === null) {
$this->error("Server '{$name}' not found in inventory");
return Command::FAILURE;
}
$this->displayServerInfo($server);
🤖 Prompt for AI Agents
In app/Console/Server/ServerDeleteCommand.php around lines 81 to 94, replace the
manual foreach lookup for a server by name with a call to the repository's
findByName() method; call $this->serverRepository->findByName($name), assign the
result to $server, then keep the existing null check and error return and the
subsequent $this->displayServerInfo($server) so behavior remains identical but
avoids duplicating lookup logic.

Comment on lines 186 to 191
try {
$ssh = new SSH2($host, $port);
} catch (\Throwable $e) {
throw new \RuntimeException("Error initiating SSH connection to {$host}:{$port}: " . $e->getMessage());
}

try {
$loggedIn = $ssh->login($username, $key);
} catch (\Throwable $e) {
throw new \RuntimeException("Error authenticating SSH for {$username}@{$host}: " . $e->getMessage());
throw new \RuntimeException($e->getMessage());
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Consider retaining connection context in error messages.

Removing the outer try-catch around the SSH2 constructor means connection errors will no longer include which host/port failed. This could make debugging harder when working with multiple servers.

Consider retaining contextual information while avoiding double-wrapping:

 try {
     $ssh = new SSH2($host, $port);
     $loggedIn = $ssh->login($username, $key);
 } catch (\Throwable $e) {
-    throw new \RuntimeException($e->getMessage());
+    throw new \RuntimeException("Failed to connect or authenticate with {$username}@{$host}:{$port}: " . $e->getMessage(), previous: $e);
 }
📝 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
try {
$ssh = new SSH2($host, $port);
} catch (\Throwable $e) {
throw new \RuntimeException("Error initiating SSH connection to {$host}:{$port}: " . $e->getMessage());
}
try {
$loggedIn = $ssh->login($username, $key);
} catch (\Throwable $e) {
throw new \RuntimeException("Error authenticating SSH for {$username}@{$host}: " . $e->getMessage());
throw new \RuntimeException($e->getMessage());
}
try {
$ssh = new SSH2($host, $port);
$loggedIn = $ssh->login($username, $key);
} catch (\Throwable $e) {
throw new \RuntimeException(
"Failed to connect or authenticate with {$username}@{$host}:{$port}: " . $e->getMessage(),
previous: $e
);
}
🤖 Prompt for AI Agents
In app/Services/SSHService.php around lines 186 to 191, the current try/catch
strips connection context when SSH2 construction or login fails; update the
catch to include the host and port in the thrown message and preserve the
original exception as the previous exception (e.g. throw new
\RuntimeException("Failed to connect to {$host}:{$port}: ".$e->getMessage(), 0,
$e)) so debugging shows which server failed while retaining the original stack.

Comment on lines +23 to +41
throw new \InvalidArgumentException(
"Invalid host '{$host}'. Must be a valid IP address or domain name.\n".
'Examples: 192.168.1.100, example.com, server.example.com'
);
}
}

/**
* Validate port is in valid range.
*
* @throws \InvalidArgumentException When port is out of range
*/
protected function validatePort(int $port): void
{
if ($port < 1 || $port > 65535) {
throw new \InvalidArgumentException(
"Invalid port {$port}. Port must be between 1 and 65535.\n".
'Common SSH ports: 22 (default), 2222, 22000'
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Import InvalidArgumentException instead of using the FQN

The coding guidelines require use imports for classes. Please add use InvalidArgumentException; (and adjust the throw statements accordingly) so the trait follows the standard. As per coding guidelines.

+use InvalidArgumentException;
@@
-            throw new \InvalidArgumentException(
+            throw new InvalidArgumentException(
@@
-            throw new \InvalidArgumentException(
+            throw new InvalidArgumentException(
📝 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
throw new \InvalidArgumentException(
"Invalid host '{$host}'. Must be a valid IP address or domain name.\n".
'Examples: 192.168.1.100, example.com, server.example.com'
);
}
}
/**
* Validate port is in valid range.
*
* @throws \InvalidArgumentException When port is out of range
*/
protected function validatePort(int $port): void
{
if ($port < 1 || $port > 65535) {
throw new \InvalidArgumentException(
"Invalid port {$port}. Port must be between 1 and 65535.\n".
'Common SSH ports: 22 (default), 2222, 22000'
);
<?php
declare(strict_types=1);
namespace App\Traits;
use InvalidArgumentException;
trait ServerValidationTrait
{
// …
protected function validateHost(string $host): void
{
if (! $this->isValidHost($host)) {
throw new InvalidArgumentException(
"Invalid host '{$host}'. Must be a valid IP address or domain name.\n" .
'Examples: 192.168.1.100, example.com, server.example.com'
);
}
}
/**
* Validate port is in valid range.
*
* @throws \InvalidArgumentException When port is out of range
*/
protected function validatePort(int $port): void
{
if ($port < 1 || $port > 65535) {
throw new InvalidArgumentException(
"Invalid port {$port}. Port must be between 1 and 65535.\n" .
'Common SSH ports: 22 (default), 2222, 22000'
);
}
}
// …
}
🤖 Prompt for AI Agents
In app/Traits/ServerValidationTrait.php around lines 23 to 41, the trait uses
the fully-qualified exception class (\InvalidArgumentException) instead of
importing it; add a top-level import "use InvalidArgumentException;" and update
the throw statements in this block to use the short class name (throw new
InvalidArgumentException(...)) for both host and port validation so the file
follows the project's import conventions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant