Skip to content
Open
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
66 changes: 66 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,72 @@ In addition, Forge can assist you in managing scheduled jobs, queue workers, SSL

Documentation for Forge CLI can be found on the [Laravel Forge website](https://forge.laravel.com/docs/1.0/cli.html).

## Project Configuration

You can create a `.forge` configuration file in your project directory to define default server and site settings. This eliminates the need to specify server/site IDs for every command.

### Quick Setup

```bash
cd /path/to/your/project
forge init
```

The `init` command will interactively guide you through setting up environments for your project.

### Configuration Format

The `.forge` file supports named environments:

```json
{
"default": "staging",
"environments": {
"production": {
"server": 123456,
"site": 789012,
"confirm": true
},
"staging": {
"server": 123456,
"site": 789013
}
}
}
```

### Deploying

With a `.forge` file configured, deployments become simple:

```bash
forge deploy # Deploy to default environment
forge deploy staging # Deploy to staging
forge deploy production # Deploy to production (prompts for confirmation)
forge deploy --force # Skip confirmation prompt
```

The `confirm` option requires user confirmation before deploying, helping prevent accidental production deployments.

### Configuration Commands

```bash
forge config # Display current configuration
forge config:set # Add or update an environment (interactive)
forge config:set prod 123 456 --confirm # Quick add with IDs
forge config:remove dev # Remove an environment
forge config:default staging # Set default environment
```

### Shell Completion

Enable tab completion for commands and environments:

```bash
forge completion --install # Add to ~/.zshrc
source ~/.zshrc
```

## Contributing

Thank you for considering contributing to Forge CLI! You can read the contribution guide [here](.github/CONTRIBUTING.md).
Expand Down
19 changes: 19 additions & 0 deletions app/Commands/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use App\Repositories\ConfigRepository;
use App\Repositories\ForgeRepository;
use App\Repositories\KeyRepository;
use App\Repositories\LocalConfigRepository;
use App\Repositories\RemoteRepository;
use App\Support\Time;
use Laravel\Forge\Forge;
Expand All @@ -16,6 +17,7 @@
abstract class Command extends BaseCommand
{
use Concerns\InteractsWithIO,
Concerns\InteractsWithEnvironments,
Concerns\InteractsWithVersions;

/**
Expand All @@ -32,6 +34,13 @@ abstract class Command extends BaseCommand
*/
protected $config;

/**
* The local configuration repository.
*
* @var \App\Repositories\LocalConfigRepository
*/
protected $localConfig;

/**
* The forge repository.
*
Expand Down Expand Up @@ -65,6 +74,7 @@ abstract class Command extends BaseCommand
*/
public function __construct(
ConfigRepository $config,
LocalConfigRepository $localConfig,
ForgeRepository $forge,
KeyRepository $keys,
RemoteRepository $remote,
Expand All @@ -73,6 +83,7 @@ public function __construct(
parent::__construct();

$this->config = $config;
$this->localConfig = $localConfig;
$this->forge = $forge;
$this->keys = $keys;
$this->time = $time;
Expand Down Expand Up @@ -120,6 +131,14 @@ protected function ensureCurrentTeamIsSet()
public function currentServer()
{
return once(function () {
// Priority 1: Environment-based config (named environments or legacy .forge)
$envServerId = $this->getEnvironmentServerId();

if ($envServerId) {
return $this->forge->server($envServerId);
}

// Priority 2: Global config
$this->ensureCurrentTeamIsSet();

return $this->forge->server(
Expand Down
104 changes: 104 additions & 0 deletions app/Commands/CompletionCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php

namespace App\Commands;

class CompletionCommand extends Command
{
/**
* The signature of the command.
*
* @var string
*/
protected $signature = 'completion
{shell=zsh : Shell type (zsh, bash)}
{--install : Add completion to your shell config}';

/**
* The description of the command.
*
* @var string
*/
protected $description = 'Generate shell completion script';

/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$shell = $this->argument('shell');

if ($shell !== 'zsh') {
$this->error("Only 'zsh' is currently supported. Bash coming soon.");
return 1;
}

$completionFile = $this->getCompletionPath();

if (! file_exists($completionFile)) {
$this->error("Completion file not found: {$completionFile}");
return 1;
}

if ($this->option('install')) {
return $this->installCompletion($completionFile);
}

// Output the completion script
$this->line(file_get_contents($completionFile));
$this->line('');
$this->comment('# Add to your ~/.zshrc:');
$this->comment("# source {$completionFile}");
$this->comment('# Or run: forge completion --install');

return 0;
}

/**
* Get the path to the completion script.
*
* @return string
*/
protected function getCompletionPath()
{
return dirname(__DIR__, 2) . '/completions/forge.zsh';
}

/**
* Install the completion to the user's shell config.
*
* @param string $completionFile
* @return int
*/
protected function installCompletion($completionFile)
{
$zshrc = ($_SERVER['HOME'] ?? $_SERVER['USERPROFILE']) . '/.zshrc';

if (! file_exists($zshrc)) {
$this->error('.zshrc not found. Please add manually:');
$this->line(" source {$completionFile}");
return 1;
}

$contents = file_get_contents($zshrc);
$sourceLine = "source {$completionFile}";

// Check if already installed
if (str_contains($contents, 'completions/forge.zsh')) {
$this->warnStep('Forge completion already installed in ~/.zshrc');
return 0;
}

// Add to zshrc
$addition = "\n# Forge CLI completion\n{$sourceLine}\n";
file_put_contents($zshrc, $contents . $addition);

$this->successfulStep('Completion installed to ~/.zshrc');
$this->line('');
$this->line(' Restart your terminal or run:');
$this->line(" <comment>source ~/.zshrc</comment>");

return 0;
}
}
149 changes: 149 additions & 0 deletions app/Commands/Concerns/InteractsWithEnvironments.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php

namespace App\Commands\Concerns;

trait InteractsWithEnvironments
{
/**
* The resolved environment for this command execution.
*
* @var array|null
*/
protected $resolvedEnvironment = null;

/**
* Get the environment name from command input.
*
* Checks for --environment/-e option first, then 'environment' argument.
*
* @return string|null
*/
protected function getEnvironmentFromInput()
{
// Check for --environment/-e option
if ($this->hasOption('environment') && $this->option('environment')) {
return $this->option('environment');
}

// Check for environment argument
if ($this->hasArgument('environment') && $this->argument('environment')) {
return $this->argument('environment');
}

return null;
}

/**
* Resolve and cache the environment configuration.
*
* @return array|null Returns ['name' => string|null, 'config' => array] or null
*/
protected function resolveEnvironmentConfig()
{
if ($this->resolvedEnvironment !== null) {
return $this->resolvedEnvironment;
}

$specified = $this->getEnvironmentFromInput();
$resolved = $this->localConfig->resolveEnvironment($specified);

// Validate that specified environment exists
if ($specified !== null && $resolved === null) {
$available = $this->localConfig->getEnvironmentNames();

if (empty($available)) {
abort(1, "Environment '{$specified}' specified but no environments are configured in .forge");
}

abort(1, "Unknown environment '{$specified}'. Available: " . implode(', ', $available));
}

$this->resolvedEnvironment = $resolved;

return $this->resolvedEnvironment;
}

/**
* Check if confirmation is required and prompt the user.
*
* @param string $action Description of the action (e.g., "deploy", "push environment")
* @return bool True if confirmed or no confirmation needed, false if user declined
*/
protected function confirmEnvironmentAction($action)
{
// Check for --force flag to bypass confirmation
if ($this->hasOption('force') && $this->option('force')) {
return true;
}

$env = $this->resolveEnvironmentConfig();

if ($env === null) {
return true; // No local config, proceed normally
}

$requiresConfirmation = $env['config']['confirm'] ?? false;

if (!$requiresConfirmation) {
return true;
}

$envName = $env['name'] ?? 'this environment';
$envDisplay = strtoupper($envName);

$this->line('');
$this->line(" <fg=red;options=bold>WARNING: You are about to {$action} to {$envDisplay}</>");
$this->line('');

return $this->confirmStep(
["Are you sure you want to {$action} to %s?", $envDisplay],
false
);
}

/**
* Get the server ID from the resolved environment.
*
* @return int|string|null
*/
protected function getEnvironmentServerId()
{
$env = $this->resolveEnvironmentConfig();

return $env['config']['server'] ?? null;
}

/**
* Get the site ID from the resolved environment.
*
* @return int|string|null
*/
protected function getEnvironmentSiteId()
{
$env = $this->resolveEnvironmentConfig();

return $env['config']['site'] ?? null;
}

/**
* Get the resolved environment name.
*
* @return string|null
*/
protected function getEnvironmentName()
{
$env = $this->resolveEnvironmentConfig();

return $env['name'] ?? null;
}

/**
* Check if we're using a local environment configuration.
*
* @return bool
*/
protected function hasEnvironmentConfig()
{
return $this->resolveEnvironmentConfig() !== null;
}
}
Loading