Skip to content

Commit 0898258

Browse files
committed
feat: migrate to Bifrost API and add related commands
1 parent e84ffe2 commit 0898258

File tree

11 files changed

+527
-484
lines changed

11 files changed

+527
-484
lines changed

config/nativephp-internal.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@
3131
'api_url' => env('NATIVEPHP_API_URL', 'http://localhost:4000/api/'),
3232

3333
/**
34-
* Configuration for the Zephpyr API.
34+
* Configuration for the Bifrost API.
3535
*/
36-
'zephpyr' => [
37-
'host' => env('ZEPHPYR_HOST', 'https://zephpyr.com'),
38-
'token' => env('ZEPHPYR_TOKEN'),
39-
'key' => env('ZEPHPYR_KEY'),
36+
'bifrost' => [
37+
'host' => env('BIFROST_HOST', 'https://bifrost.nativephp.com'),
38+
'token' => env('BIFROST_TOKEN'),
39+
'project' => env('BIFROST_PROJECT'),
4040
],
4141

4242
/**

config/nativephp.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
'GITHUB_*',
6565
'DO_SPACES_*',
6666
'*_SECRET',
67-
'ZEPHPYR_*',
67+
'BIFROST_*',
6868
'NATIVEPHP_UPDATER_PATH',
6969
'NATIVEPHP_APPLE_ID',
7070
'NATIVEPHP_APPLE_ID_PASS',
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace Native\Electron\Commands\Bifrost;
4+
5+
use Illuminate\Console\Command;
6+
use Symfony\Component\Console\Attribute\AsCommand;
7+
8+
use function Laravel\Prompts\intro;
9+
10+
#[AsCommand(
11+
name: 'bifrost:clear-bundle',
12+
description: 'Remove the downloaded bundle from the build directory.',
13+
)]
14+
class ClearBundleCommand extends Command
15+
{
16+
protected $signature = 'bifrost:clear-bundle';
17+
18+
public function handle(): int
19+
{
20+
intro('Clearing downloaded bundle...');
21+
22+
$bundlePath = base_path('build/__nativephp_app_bundle');
23+
24+
if (! file_exists($bundlePath)) {
25+
$this->warn('No bundle found to clear.');
26+
27+
return static::SUCCESS;
28+
}
29+
30+
if (unlink($bundlePath)) {
31+
$this->info('Bundle cleared successfully!');
32+
$this->line('Note: Building in this state would be unsecure without a valid bundle.');
33+
} else {
34+
$this->error('Failed to remove bundle file.');
35+
36+
return static::FAILURE;
37+
}
38+
39+
return static::SUCCESS;
40+
}
41+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<?php
2+
3+
namespace Native\Electron\Commands\Bifrost;
4+
5+
use Carbon\CarbonInterface;
6+
use Illuminate\Console\Command;
7+
use Illuminate\Support\Facades\Http;
8+
use Native\Electron\Traits\HandlesBifrost;
9+
use Symfony\Component\Console\Attribute\AsCommand;
10+
11+
use function Laravel\Prompts\intro;
12+
use function Laravel\Prompts\progress;
13+
14+
#[AsCommand(
15+
name: 'bifrost:download-bundle',
16+
description: 'Download the latest desktop bundle from Bifrost.',
17+
)]
18+
class DownloadBundleCommand extends Command
19+
{
20+
use HandlesBifrost;
21+
22+
protected $signature = 'bifrost:download-bundle';
23+
24+
public function handle(): int
25+
{
26+
if (! $this->checkForBifrostToken()) {
27+
return static::FAILURE;
28+
}
29+
30+
if (! $this->checkForBifrostProject()) {
31+
return static::FAILURE;
32+
}
33+
34+
if (! $this->checkAuthenticated()) {
35+
$this->error('Invalid API token. Please login again.');
36+
$this->line('Run: php artisan bifrost:login');
37+
38+
return static::FAILURE;
39+
}
40+
41+
intro('Fetching latest desktop bundle...');
42+
43+
$projectId = config('nativephp-internal.bifrost.project');
44+
$response = Http::acceptJson()
45+
->withToken(config('nativephp-internal.bifrost.token'))
46+
->get($this->baseUrl()."api/v1/projects/{$projectId}/builds/latest-desktop-bundle");
47+
48+
if ($response->failed()) {
49+
$this->handleApiError($response);
50+
51+
return static::FAILURE;
52+
}
53+
54+
$buildData = $response->json();
55+
$downloadUrl = $buildData['download_url'];
56+
57+
$this->line('');
58+
$this->info('Bundle Details:');
59+
$this->line('Version: '.$buildData['version']);
60+
$this->line('Git Commit: '.substr($buildData['git_commit'], 0, 8));
61+
$this->line('Git Branch: '.$buildData['git_branch']);
62+
$this->line('Created: '.$buildData['created_at']);
63+
64+
// Create build directory if it doesn't exist
65+
$buildDir = base_path('build');
66+
if (! is_dir($buildDir)) {
67+
mkdir($buildDir, 0755, true);
68+
}
69+
70+
$bundlePath = base_path('build/__nativephp_app_bundle');
71+
72+
// Download the bundle with progress bar
73+
$this->line('');
74+
$this->info('Downloading bundle...');
75+
76+
$downloadResponse = Http::withOptions([
77+
'sink' => $bundlePath,
78+
'progress' => function ($downloadTotal, $downloadedBytes) {
79+
if ($downloadTotal > 0) {
80+
$progress = ($downloadedBytes / $downloadTotal) * 100;
81+
$this->output->write("\r".sprintf('Progress: %.1f%%', $progress));
82+
}
83+
},
84+
])->get($downloadUrl);
85+
86+
if ($downloadResponse->failed()) {
87+
$this->line('');
88+
$this->error('Failed to download bundle.');
89+
90+
if (file_exists($bundlePath)) {
91+
unlink($bundlePath);
92+
}
93+
94+
return static::FAILURE;
95+
}
96+
97+
$this->line('');
98+
$this->line('');
99+
$this->info('Bundle downloaded successfully!');
100+
$this->line('Location: '.$bundlePath);
101+
$this->line('Size: '.number_format(filesize($bundlePath) / 1024 / 1024, 2).' MB');
102+
103+
return static::SUCCESS;
104+
}
105+
106+
private function handleApiError($response): void
107+
{
108+
$status = $response->status();
109+
$data = $response->json();
110+
111+
switch ($status) {
112+
case 404:
113+
$this->line('');
114+
$this->error('No desktop builds found for this project.');
115+
$this->line('');
116+
$this->info('Create a build at: '.$this->baseUrl().'{team}/desktop/projects/{project}');
117+
break;
118+
119+
case 503:
120+
$retryAfter = intval($response->header('Retry-After'));
121+
$diff = now()->addSeconds($retryAfter);
122+
$diffMessage = $retryAfter <= 60 ? 'a minute' : $diff->diffForHumans(syntax: CarbonInterface::DIFF_ABSOLUTE);
123+
$this->line('');
124+
$this->warn('Build is still in progress.');
125+
$this->line('Please try again in '.$diffMessage.'.');
126+
break;
127+
128+
case 500:
129+
$this->line('');
130+
$this->error('Latest build has failed or was cancelled.');
131+
if (isset($data['build_id'])) {
132+
$this->line('Build ID: '.$data['build_id']);
133+
$this->line('Status: '.$data['status']);
134+
}
135+
break;
136+
137+
default:
138+
$this->line('');
139+
$this->error('Failed to fetch bundle: '.($data['message'] ?? 'Unknown error'));
140+
}
141+
}
142+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<?php
2+
3+
namespace Native\Electron\Commands\Bifrost;
4+
5+
use Illuminate\Console\Command;
6+
use Illuminate\Support\Facades\Http;
7+
use Native\Electron\Traits\HandlesBifrost;
8+
use Symfony\Component\Console\Attribute\AsCommand;
9+
10+
use function Laravel\Prompts\intro;
11+
use function Laravel\Prompts\select;
12+
13+
#[AsCommand(
14+
name: 'bifrost:init',
15+
description: 'Select a desktop project for Bifrost operations.',
16+
)]
17+
class InitCommand extends Command
18+
{
19+
use HandlesBifrost;
20+
21+
protected $signature = 'bifrost:init';
22+
23+
public function handle(): int
24+
{
25+
if (! $this->checkForBifrostToken()) {
26+
return static::FAILURE;
27+
}
28+
29+
if (! $this->checkAuthenticated()) {
30+
$this->error('Invalid API token. Please login again.');
31+
$this->line('Run: php artisan bifrost:login');
32+
33+
return static::FAILURE;
34+
}
35+
36+
intro('Fetching your desktop projects...');
37+
38+
$response = Http::acceptJson()
39+
->withToken(config('nativephp-internal.bifrost.token'))
40+
->get($this->baseUrl().'api/v1/projects');
41+
42+
if ($response->failed()) {
43+
$this->handleApiError($response);
44+
45+
return static::FAILURE;
46+
}
47+
48+
$projects = collect($response->json('data'))
49+
->filter(fn ($project) => $project['type'] === 'desktop')
50+
->values()
51+
->toArray();
52+
53+
if (empty($projects)) {
54+
$this->line('');
55+
$this->warn('No desktop projects found.');
56+
$this->line('');
57+
$this->info('Create a desktop project at: '.$this->baseUrl().'{team}/onboarding/project/desktop');
58+
59+
return static::FAILURE;
60+
}
61+
62+
$choices = [];
63+
foreach ($projects as $project) {
64+
$choices[$project['id']] = $project['name'].' - '.$project['repo'];
65+
}
66+
67+
$selectedProjectId = select(
68+
label: 'Select a desktop project',
69+
options: $choices,
70+
required: true
71+
);
72+
73+
$selectedProject = collect($projects)->firstWhere('id', $selectedProjectId);
74+
75+
// Store project in .env file
76+
$envPath = base_path('.env');
77+
$envContent = file_get_contents($envPath);
78+
79+
if (str_contains($envContent, 'BIFROST_PROJECT=')) {
80+
$envContent = preg_replace('/BIFROST_PROJECT=.*/', "BIFROST_PROJECT={$selectedProjectId}", $envContent);
81+
} else {
82+
$envContent .= "\nBIFROST_PROJECT={$selectedProjectId}";
83+
}
84+
85+
file_put_contents($envPath, $envContent);
86+
87+
$this->line('');
88+
$this->info('Project selected successfully!');
89+
$this->line('Project: '.$selectedProject['name']);
90+
$this->line('Repository: '.$selectedProject['repo']);
91+
$this->line('');
92+
$this->line('You can now run "php artisan bifrost:download-bundle" to download the latest bundle.');
93+
94+
return static::SUCCESS;
95+
}
96+
97+
private function handleApiError($response): void
98+
{
99+
$status = $response->status();
100+
$baseUrl = rtrim($this->baseUrl(), '/');
101+
102+
switch ($status) {
103+
case 403:
104+
$this->line('');
105+
$this->error('No teams found. Please create a team first.');
106+
$this->line('');
107+
$this->info('Create a team at: '.$baseUrl.'/onboarding/team');
108+
break;
109+
110+
case 422:
111+
$this->line('');
112+
$this->error('Team setup incomplete or subscription required.');
113+
$this->line('');
114+
$this->info('Complete setup at: '.$baseUrl.'/dashboard');
115+
break;
116+
117+
default:
118+
$this->line('');
119+
$this->error('Failed to fetch projects: '.$response->json('message', 'Unknown error'));
120+
$this->line('');
121+
$this->info('Visit the dashboard: '.$baseUrl.'/dashboard');
122+
}
123+
}
124+
}

0 commit comments

Comments
 (0)