Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions .cursor/rules/01-architecture.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -124,14 +124,18 @@ $command = $container->build(ServerAddCommand::class); // Gets mock
**Comment structure:**

```
// ----
// {h1}
// ----

//
// {Section Header}
// -------------------------------------------------------------------------------
// {h2}
// ----

//
// {Section Subheader}
// {h3}

// {Paragraph}
// {p}
```

Separate sections visually. One newline between headers/subheaders/paragraphs. No obvious comments. Remove comments when removing code.
Expand Down
31 changes: 18 additions & 13 deletions .cursor/rules/04-exceptions.mdc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
alwaysApply: true
---

## Exception Handling & Error Display

All rules MANDATORY.
Expand Down Expand Up @@ -36,6 +37,7 @@ throw new \RuntimeException("does not exist");
```

**Rules:**

- Messages must be user-facing and complete (not fragments)
- Include relevant context (paths, names, IDs, hosts)
- Use `previous: $e` to preserve exception chains for debugging
Expand Down Expand Up @@ -94,6 +96,7 @@ protected function validateGitRepo(string $repo): void
```

**Naming Convention:**

- `validate*Input()` - Returns `?string` (for prompts)
- `validate*()` - Throws exceptions (for I/O)

Expand All @@ -108,12 +111,12 @@ protected function getServerInfo(ServerDTO $server): array|int
try {
$result = $this->executePlaybook($server, 'server-info', 'Gathering...');
} catch (\RuntimeException $e) {
$this->io->error($e->getMessage()); // No "Failed to..." prefix
$this->nay($e->getMessage()); // No "Failed to..." prefix
return Command::FAILURE;
}

if ($result['exit_code'] !== 0) {
$this->io->error('Failed to gather server information');
$this->nay('Failed to gather server information');
return Command::FAILURE;
}

Expand All @@ -122,20 +125,21 @@ protected function getServerInfo(ServerDTO $server): array|int

// ❌ WRONG - Adding redundant prefix
} catch (\RuntimeException $e) {
$this->io->error('Failed to gather server information: ' . $e->getMessage());
$this->nay('Failed to gather server information: ' . $e->getMessage());
// Results in: "Failed to gather server information: SSH authentication failed..."
}
```

**When to add context:**

- Displaying raw output for debugging
- Adding actionable troubleshooting steps
- Exception message is too technical/generic

```php
// ✅ CORRECT - Adding helpful context, not redundant prefix
} catch (\RuntimeException $e) {
$this->io->error($e->getMessage());
$this->nay($e->getMessage());
$this->io->writeln([
'',
'<fg=yellow>Troubleshooting:</>',
Expand All @@ -156,13 +160,13 @@ Catch exceptions, display directly, return status:
try {
$this->servers->create($server);
} catch (\RuntimeException $e) {
$this->io->error($e->getMessage()); // Already complete: "Server 'web1' already exists"
$this->nay($e->getMessage()); // Already complete: "Server 'web1' already exists"
return Command::FAILURE;
}

// ❌ WRONG - Adding redundant prefix
} catch (\RuntimeException $e) {
$this->io->error('Failed to add server: ' . $e->getMessage());
$this->nay('Failed to add server: ' . $e->getMessage());
// Results in: "Failed to add server: Server 'web1' already exists"
}
```
Expand Down Expand Up @@ -197,6 +201,7 @@ public function executeCommand(string $host, ...): ?array
### Exception Message Quality

Every exception message must be:

- Complete (not: "does not exist", but: "SSH key does not exist: /path/to/key")
- User-facing (not: "PDO error 2002", but: "Cannot connect to database. Check host and port.")
- Actionable with context (paths, names, IDs, hosts)
Expand All @@ -206,10 +211,10 @@ Exception chains preserved via `previous: $e` for debugging.

### Layer Responsibility Summary

| Layer | Display Errors? | Pattern |
|-------|----------------|---------|
| Services | ❌ No | Throw complete exceptions |
| Repositories | ❌ No | Throw complete exceptions |
| Validation Traits | ❌ No | Return `?string` or throw |
| Orchestration Traits | ✅ Yes | Catch & display without prefix |
| Commands | ✅ Yes | Catch & display without prefix |
| Layer | Display Errors? | Pattern |
| -------------------- | --------------- | ------------------------------ |
| Services | ❌ No | Throw complete exceptions |
| Repositories | ❌ No | Throw complete exceptions |
| Validation Traits | ❌ No | Return `?string` or throw |
| Orchestration Traits | ✅ Yes | Catch & display without prefix |
| Commands | ✅ Yes | Catch & display without prefix |
16 changes: 13 additions & 3 deletions app/Console/Server/ServerAddCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Bigpixelrocket\DeployerPHP\Contracts\BaseCommand;
use Bigpixelrocket\DeployerPHP\DTOs\ServerDTO;
use Bigpixelrocket\DeployerPHP\Traits\KeysTrait;
use Bigpixelrocket\DeployerPHP\Traits\PlaybooksTrait;
use Bigpixelrocket\DeployerPHP\Traits\ServersTrait;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
Expand All @@ -21,6 +22,7 @@
class ServerAddCommand extends BaseCommand
{
use KeysTrait;
use PlaybooksTrait;
use ServersTrait;

// -------------------------------------------------------------------------------
Expand Down Expand Up @@ -86,15 +88,23 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$this->displayServerDeets($server);

//
// Verify SSH connection & add to inventory
// Get server info (verifies SSH connection and validates distribution)
// -------------------------------------------------------------------------------

$this->verifySSHConnection($server); // SSH failure is not a blocker
$info = $this->getServerInfo($server);

if (is_int($info)) {
return $info;
}

//
// Add to inventory
// -------------------------------------------------------------------------------

try {
$this->servers->create($server);
} catch (\RuntimeException $e) {
$this->nay('Failed to add server to inventory: ' . $e->getMessage());
$this->nay($e->getMessage());

return Command::FAILURE;
}
Expand Down
14 changes: 13 additions & 1 deletion app/Console/Server/ServerDeleteCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
// Destroy cloud provider resources
// -------------------------------------------------------------------------------

$destroyed = false;

if ($isDigitalOceanServer && $server->dropletId !== null) {
try {
$this->io->promptSpin(
Expand All @@ -158,6 +160,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
);

$this->yay('Droplet destroyed (ID: ' . $server->dropletId . ')');
$destroyed = true;
} catch (\RuntimeException $e) {
$this->nay($e->getMessage());
$this->io->writeln('');
Expand All @@ -179,7 +182,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int

$this->servers->delete($server->name);

$this->yay("Server '{$server->name}' deleted successfully");
$this->yay("Server '{$server->name}' deleted from inventory");

if (!$destroyed) {
$this->io->writeln([
'',
'<fg=yellow>Your server may still be running and incurring costs:</>',
' • Double-check with your cloud provider to ensure it is fully terminated.',
'',
]);
}

//
// Show command replay
Expand Down
72 changes: 1 addition & 71 deletions app/Console/Server/ServerInfoCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
namespace Bigpixelrocket\DeployerPHP\Console\Server;

use Bigpixelrocket\DeployerPHP\Contracts\BaseCommand;
use Bigpixelrocket\DeployerPHP\DTOs\ServerDTO;
use Bigpixelrocket\DeployerPHP\Enums\Distribution;
use Bigpixelrocket\DeployerPHP\Traits\PlaybooksTrait;
use Bigpixelrocket\DeployerPHP\Traits\ServersTrait;
use Symfony\Component\Console\Attribute\AsCommand;
Expand Down Expand Up @@ -62,7 +60,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$this->displayServerDeets($server);

//
// Get and display server information
// Get server info (verifies SSH connection and validates distribution)
// -------------------------------------------------------------------------------

$info = $this->getServerInfo($server);
Expand All @@ -71,8 +69,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int
return $info;
}

$this->displayServerInfo($info);

//
// Show command replay
// -------------------------------------------------------------------------------
Expand All @@ -84,70 +80,4 @@ protected function execute(InputInterface $input, OutputInterface $output): int
return Command::SUCCESS;
}

// -------------------------------------------------------------------------------
//
// Helpers
//
// -------------------------------------------------------------------------------

/**
* Get server information by executing server-info playbook.
*
* @param ServerDTO $server Server to get information for
* @return array<string, mixed>|int Returns parsed server info or failure code on failure
*/
protected function getServerInfo(ServerDTO $server): array|int
{
return $this->executePlaybook(
$server,
'server-info',
'Retrieving server information...',
);
}

/**
* Display formatted server information.
*
* @param array<string, mixed> $info
*/
protected function displayServerInfo(array $info): void
{
/** @var string $distroSlug */
$distroSlug = $info['distro'] ?? 'unknown';
$distribution = Distribution::tryFrom($distroSlug);
$distroName = $distribution?->displayName() ?? 'Unknown';

$permissionsText = match ($info['permissions'] ?? 'none') {
'root' => 'root',
'sudo' => 'sudo',
default => 'insufficient',
};

$deets = [
'Distro' => $distroName,
'User' => $permissionsText,
];

$this->io->displayDeets($deets);
$this->io->writeln('');

$services = [];

// Add listening ports if any
if (isset($info['ports']) && is_array($info['ports']) && count($info['ports']) > 0) {
$portsList = [];
foreach ($info['ports'] as $port => $process) {
if (is_numeric($port) && is_string($process)) {
$portsList[] = "Port {$port}: {$process}";
}
}
if (count($portsList) > 0) {
$services = $portsList;
}
}

$this->io->displayDeets(['Services' => $services]);
$this->io->writeln('');
}

}
Loading