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
25 changes: 14 additions & 11 deletions src/Console/Commands/InstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@

use Illuminate\Console\Command;
use Illuminate\Support\Str;
use Laravel\Prompts\Elements\Element;
use Laravel\Prompts\Elements\ElementContract;
use Saucebase\Installer\Console\Commands\Concerns\DisplaysBanner;
use Saucebase\Installer\Environments\Environment;
use Saucebase\Installer\ModuleRegistry;
use Symfony\Component\Process\Process;

use function Laravel\Prompts\callout;
use function Laravel\Prompts\select;

class InstallCommand extends Command
Expand Down Expand Up @@ -515,16 +518,16 @@ protected function isCI(): bool

public function displaySuccess(array $steps = []): void
{
$this->newLine();
$this->info('Installation complete!');
$this->newLine();
if ($steps) {
$this->line('Next steps:');
foreach (array_values($steps) as $i => $step) {
$this->line(' '.($i + 1).'. '.$step);
}
$this->newLine();
}
$this->line('Learn more: <fg=cyan>https://github.com/saucebase-dev/saucebase</>');
callout(label: 'Installation complete', content: $this->successCalloutContent($steps));
}

/** @return array<int, string|ElementContract> */
protected function successCalloutContent(array $steps): array
{
return array_filter([
$steps ? 'You can start your local development using:' : null,
$steps ? Element::numberedList(array_values($steps)) : null,
'Learn more: '.Element::link('https://github.com/saucebase-dev/saucebase'),
]);
}
}
6 changes: 3 additions & 3 deletions src/Environments/DockerEnvironment.php
Original file line number Diff line number Diff line change
Expand Up @@ -301,9 +301,9 @@ protected function nextSteps(InstallCommand $command): array
$appUrl = $this->readEnvValue($command, 'APP_URL') ?? ($this->ssl ? 'https://localhost' : 'http://localhost');

return [
'Compile frontend assets: <fg=yellow>npm install && npm run dev</>',
'Open your app: <fg=yellow>'.$appUrl.'</>',
'Email testing (Mailpit): <fg=yellow>http://localhost:8025</>',
'Compile frontend assets: `npm install && npm run dev`',
'Open your app: `'.$appUrl.'`',
'Email testing (Mailpit): `http://localhost:8025`',
];
}

Expand Down
22 changes: 21 additions & 1 deletion src/Environments/Environment.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,32 @@ public function run(InstallCommand $command): int
$result = $this->boot($command);

if ($result === InstallCommand::SUCCESS) {
$command->displaySuccess($this->nextSteps($command));
$command->displaySuccess(array_merge($this->cdStep($command), $this->nextSteps($command)));
}

return $result;
}

/** @return string[] A `cd` step when the target app lives outside the current directory, empty otherwise. */
protected function cdStep(InstallCommand $command): array
{
$target = $this->normalizePath($command->targetPath());
$cwd = $this->normalizePath(getcwd());

if ($target === $cwd) {
return [];
}

return ['cd `'.$target.'`'];
}

private function normalizePath(string $path): string
{
$real = realpath($path);

return rtrim($real !== false ? $real : $path, '/');
}

/** Hook: perform driver-specific steps before the module prompt. Return an exit code to abort, null to continue. */
protected function beforePrompts(InstallCommand $command): ?int
{
Expand Down
6 changes: 3 additions & 3 deletions src/Environments/NativeEnvironment.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ protected function boot(InstallCommand $command): int
protected function nextSteps(InstallCommand $command): array
{
return [
'Install frontend dependencies: <fg=yellow>npm install</>',
'Start the dev server: <fg=yellow>composer dev</>',
'Open your app: <fg=yellow>'.($this->readEnvValue($command, 'APP_URL') ?? 'http://localhost').'</>',
'Install frontend dependencies: `npm install`',
'Start the dev server: `composer dev`',
'Open your app: `'.($this->readEnvValue($command, 'APP_URL') ?? 'http://localhost').'`',
];
}
}
105 changes: 105 additions & 0 deletions tests/Feature/Environments/NativeEnvironmentTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,109 @@ public function install(): int

$this->assertSame(Command::FAILURE, $env->run($command));
}

// -------------------------------------------------------------------------
// cdStep — inherited from Environment
// -------------------------------------------------------------------------

private string $originalCwd;

private string $sandbox;

protected function setUp(): void
{
parent::setUp();
$this->originalCwd = getcwd();
$this->sandbox = sys_get_temp_dir().'/saucebase-env-test-'.uniqid();
mkdir($this->sandbox.'/sub/dir', recursive: true);
chdir($this->sandbox);
}

protected function tearDown(): void
{
chdir($this->originalCwd);
$this->removeDirectory($this->sandbox);
parent::tearDown();
}

private function envExposingCdStep(): object
{
return new class extends NativeEnvironment
{
public function exposedCdStep(InstallCommand $command): array
{
return $this->cdStep($command);
}
};
}

public function test_cd_step_is_empty_when_target_matches_cwd(): void
{
$command = new class extends InstallCommand
{
public function targetPath(): string
{
return getcwd();
}
};

$this->assertSame([], $this->envExposingCdStep()->exposedCdStep($command));
}

public function test_cd_step_resolves_nested_relative_target(): void
{
$command = new class extends InstallCommand
{
public function targetPath(): string
{
return './sub/../sub/dir';
}
};

$this->assertSame(
['cd `'.realpath($this->sandbox.'/sub/dir').'`'],
$this->envExposingCdStep()->exposedCdStep($command),
);
}

public function test_cd_step_resolves_absolute_target_outside_cwd(): void
{
$elsewhere = sys_get_temp_dir().'/saucebase-env-test-elsewhere-'.uniqid();
mkdir($elsewhere, recursive: true);

$command = new class($elsewhere) extends InstallCommand
{
public function __construct(private string $elsewhere) {}

public function targetPath(): string
{
return $this->elsewhere;
}
};

$this->assertSame(
['cd `'.realpath($elsewhere).'`'],
$this->envExposingCdStep()->exposedCdStep($command),
);

$this->removeDirectory($elsewhere);
Comment thread
roble marked this conversation as resolved.
}

private function removeDirectory(string $dir): void
{
if (! is_dir($dir)) {
return;
}

foreach (scandir($dir) as $entry) {
if ($entry === '.' || $entry === '..') {
continue;
}

$path = $dir.'/'.$entry;
is_dir($path) ? $this->removeDirectory($path) : unlink($path);
}

rmdir($dir);
}
}
39 changes: 15 additions & 24 deletions tests/Feature/InstallCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
use Laravel\Prompts\Elements\NumberedList;
use Saucebase\Installer\Console\Commands\InstallCommand;
use Saucebase\Installer\Environments\Environment;
use Saucebase\Installer\Environments\NativeEnvironment;
Expand Down Expand Up @@ -611,34 +612,19 @@ public function test_module_has_seeder_checks_vendor_path(): void

public function test_display_success_numbers_steps_sequentially(): void
{
$cmd = new class extends TestableInstallCommand
{
public array $capturedLines = [];

public function line($string, $style = null, $verbosity = null): void
{
$this->capturedLines[] = $string;
}
$cmd = new TestableInstallCommand;

public function info($string, $verbosity = null): void {}
$content = $cmd->exposedSuccessCalloutContent([5 => 'first step', 10 => 'second step']);

public function newLine($count = 1): static
{
return $this;
$list = null;
foreach ($content as $part) {
if ($part instanceof NumberedList) {
$list = $part;
}
};

$cmd->displaySuccess([5 => 'first step', 10 => 'second step']);

$stepLines = implode(' ', array_filter(
$cmd->capturedLines,
fn ($l) => (bool) preg_match('/\d+\./', $l),
));
}

$this->assertStringContainsString('1. first step', $stepLines);
$this->assertStringContainsString('2. second step', $stepLines);
$this->assertStringNotContainsString('6. first step', $stepLines);
$this->assertStringNotContainsString('11. second step', $stepLines);
$this->assertNotNull($list);
$this->assertSame(['first step', 'second step'], $list->items);
}

// -------------------------------------------------------------------------
Expand Down Expand Up @@ -751,6 +737,11 @@ public function exposedFilterModulesByFramework(array $packages, string $framewo
return $this->filterModulesByFramework($packages, $framework);
}

public function exposedSuccessCalloutContent(array $steps): array
{
return $this->successCalloutContent($steps);
}

/** @var array<string, bool|string|null> Fake option values for tests that bypass CLI input. */
public array $fakeOptions = [];

Expand Down