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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
.phpunit.result.cache
/database/database.sqlite

.env

GEMINI.md
CODEX.md
/.cursor
Expand Down
18 changes: 18 additions & 0 deletions REQUIREMENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# System Requirements

This application relies on system tools to handle SSH connections securely and interactively.

## Recommended Tools

### 1. sshpass (Primary)
Used to securely pass the password to the SSH client without user interaction.
- **macOS:** `brew install hudochenkov/sshpass/sshpass`
- **Linux:** `apt-get install sshpass`

### 2. Clipboard Support (Fallback)
If `sshpass` is not available, the application attempts to copy the password to your clipboard so you can paste it.
- **macOS:** Uses `pbcopy` (Pre-installed).
- **Linux:** Uses `xclip` or `xsel` (Not yet implemented, falls back to manual).

## Fallback Behavior
If no tools are found, the application will default to the standard `ssh` command, which will prompt you to type the password manually.
132 changes: 132 additions & 0 deletions app/Commands/ConnectCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?php

namespace App\Commands;

use App\Models\Host;
use LaravelZero\Framework\Commands\Command;

use function Laravel\Prompts\error;
use function Laravel\Prompts\info;
use function Laravel\Prompts\select;

class ConnectCommand extends Command
{
protected $signature = 'connect {alias? : The host alias} {user? : The specific user} {--u|user= : (Deprecated) Optional specific user}';
protected $description = 'Connect to a host via SSH';

public function handle(): void
{
$alias = $this->argument('alias');

if (! $alias) {
// Sort hosts by the most recent login
$aliases = Host::orderByDesc('last_login_at')
->orderBy('alias')
->pluck('alias')
->toArray();

if (empty($aliases)) {
error('No hosts found.');

return;
}
$alias = select(
label: 'Select a host',
options: $aliases,
scroll: 10
);
}

$host = Host::where('alias', $alias)->first();

if (! $host) {
error("Host '{$alias}' not found.");

return;
}

// Update host last login time
$host->update(['last_login_at' => now()]);

$username = $this->argument('user') ?: $this->option('user');
$user = null;

if ($username) {
$user = $host->users()->where('username', $username)->first();
if (! $user) {
error("User '{$username}' not found for host '{$alias}'.");

return;
}
} else {
// Sort users by last login
$users = $host->users()->orderByDesc('last_login_at')->orderBy('username')->get();

if ($users->isEmpty()) {
error("No users found for host '{$alias}'.");

return;
}

if ($users->count() === 1) {
$user = $users->first();
} else {
$selectedUsername = select(
label: 'Select a user',
options: $users->pluck('username')->toArray()
);
$user = $users->where('username', $selectedUsername)->first();
}
}

// Update user last login time
$user->update(['last_login_at' => now()]);

$password = $user->password; // Decrypted via cast
$target = "{$user->username}@{$host->hostname}";

info("Connecting to {$alias} ({$target})...");

// Strategy 1: sshpass
if (! empty(shell_exec('which sshpass'))) {
$cmd = "SSHPASS='{$password}' sshpass -e ssh {$target}";
}
// Strategy 2: Clipboard (macOS pbcopy)
elseif (! empty(shell_exec('which pbcopy'))) {
// Copy password to clipboard
if (! app()->runningUnitTests()) {
$proc = proc_open('pbcopy', [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], $pipes);
if (is_resource($proc)) {
fwrite($pipes[0], $password);
fclose($pipes[0]);
proc_close($proc);
info('Password copied to clipboard! Paste it when prompted.');
} else {
error('Failed to access clipboard.');
}
}

$cmd = "ssh {$target}";
}
// Strategy 3: Manual fallback
else {
info("Neither 'sshpass' nor 'expect' found. You will need to type the password manually.");
info("Password: {$password}");
$cmd = "ssh {$target}";
}

if (app()->runningUnitTests()) {
// For testing, we just want to see which command was generated
// We simplify the expect command output for easier assertion
if (str_contains($cmd, 'expect')) {
info('Command: expect script execution');
} else {
info("Command: {$cmd}");
}

return;
}

passthru($cmd);
}
}
82 changes: 82 additions & 0 deletions app/Commands/HostAddCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

namespace App\Commands;

use App\Models\Host;
use Illuminate\Support\Facades\DB;
use LaravelZero\Framework\Commands\Command;

use function Laravel\Prompts\error;
use function Laravel\Prompts\info;
use function Laravel\Prompts\password;
use function Laravel\Prompts\text;

class HostAddCommand extends Command
{
/**
* The signature of the command.
*
* @var string
*/
protected $signature = 'host:add {alias? : The host alias} {hostname? : The hostname or IP} {user? : The username}';

/**
* The description of the command.
*
* @var string
*/
protected $description = 'Add a new SSH host and user';

/**
* Execute the console command.
*/
public function handle(): void
{
$alias = $this->argument('alias') ?? text(
label: 'Host Alias',
placeholder: 'e.g. production',
required: true,
validate: fn (string $value) => match (true) {
Host::where('alias', $value)->exists() => 'This alias is already taken.',
default => null,
}
);

// If alias was provided as argument, we still need to validate it
if ($this->argument('alias') && Host::where('alias', $alias)->exists()) {
error('This alias is already taken.');

return;
}

$hostname = $this->argument('hostname') ?? text(
label: 'Hostname / IP',
placeholder: 'e.g. 192.168.1.1 or example.com',
required: true
);

$username = $this->argument('user') ?? text(
label: 'Username',
required: true
);

$password = password(
label: 'Password',
required: true
);

DB::transaction(function () use ($alias, $hostname, $username, $password) {
$host = Host::create([
'alias' => $alias,
'hostname' => $hostname,
]);

$host->users()->create([
'username' => $username,
'password' => $password,
]);
});

info("Host '{$alias}' added successfully.");
}
}
98 changes: 98 additions & 0 deletions app/Commands/HostDeleteCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

namespace App\Commands;

use App\Models\Host;
use LaravelZero\Framework\Commands\Command;

use function Laravel\Prompts\confirm;
use function Laravel\Prompts\error;
use function Laravel\Prompts\info;
use function Laravel\Prompts\select;

class HostDeleteCommand extends Command
{
/**
* The signature of the command.
*
* @var string
*/
protected $signature = 'host:delete {alias? : The host alias} {user? : The user to delete}';

/**
* The description of the command.
*
* @var string
*/
protected $description = 'Delete a host or a specific user';

/**
* Execute the console command.
*/
public function handle(): void
{
$alias = $this->argument('alias');
$targetUser = $this->argument('user');

if (! $alias) {
$aliases = Host::pluck('alias')->toArray();
if (empty($aliases)) {
error('No hosts found.');

return;
}
$alias = select(
label: 'Select a host to delete',
options: $aliases,
scroll: 10
);
}

$host = Host::where('alias', $alias)->first();

if (! $host) {
error("Host '{$alias}' not found.");

return;
}

if ($targetUser) {
$user = $host->users()->where('username', $targetUser)->first();
if (! $user) {
error("User '{$targetUser}' not found for host '{$alias}'.");

return;
}

$confirmed = confirm(
label: "Are you sure you want to delete user '{$targetUser}' from host '{$alias}'?",
default: false,
yes: 'Yes, delete it',
no: 'No, cancel'
);

if ($confirmed) {
$user->delete();
info("User '{$targetUser}' deleted successfully.");
} else {
info('Deletion cancelled.');
}

return;
}

$confirmed = confirm(
label: "Are you sure you want to delete host '{$alias}' and all its users?",
default: false,
yes: 'Yes, delete it',
no: 'No, cancel'
);

if ($confirmed) {
$host->delete();
info("Host '{$alias}' deleted successfully.");
} else {
info('Deletion cancelled.');
}
}
}
Loading
Loading