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 .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,13 @@ SLACK_DEFAULT_CHANNEL=

# GitHub
GITHUB_TOKEN=
GITHUB_ORG=conduit-ui
GITHUB_REPO=jordanpartridge/your-repo

# Bifrost
BIFROST_URL=https://bifrost.jordanpartridge.us
BIFROST_AGENT_SECRET=
BIFROST_TOKEN=

# Qdrant — shared Odin vector store (knowledge-qdrant container)
QDRANT_URL=http://127.0.0.1:6333
Expand Down
74 changes: 74 additions & 0 deletions app/Jobs/RepoGeneratorJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

namespace App\Jobs;

use App\Data\ManifestData;
use App\Data\ModelData;
use App\Services\BifrostService;
use App\Services\GitHubService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;

class RepoGeneratorJob implements ShouldQueue
{
use Queueable;

public function __construct(public readonly ManifestData $manifest) {}

public function handle(GitHubService $github, BifrostService $bifrost): void
{
$github->createRepo($this->manifest);

$github->openIssue(
$this->manifest->name,
'Setup: initial project bootstrap',
$this->setupIssueBody(),
);

foreach ($this->manifest->models as $model) {
/** @var ModelData $model */
$github->openIssue(
$this->manifest->name,
"Model: {$model->name}",
$this->modelIssueBody($model),
);
}

$bifrost->registerSource($this->manifest);
}

private function setupIssueBody(): string
{
$stack = $this->manifest->stack;

return implode("\n", [
'## Setup Checklist',
'',
"- [ ] Framework: {$stack->framework} {$stack->php}",
"- [ ] Database: {$stack->db->value}",
"- [ ] Auth: {$stack->auth->value}",
"- [ ] Queue: {$stack->queue}",
'- [ ] Install dependencies',
'- [ ] Configure environment',
'- [ ] Run migrations',
]);
}

private function modelIssueBody(ModelData $model): string
{
$attributes = implode("\n", array_map(
fn (string $attr) => "- [ ] `{$attr}`",
$model->attributes,
));

$attributeSection = $attributes !== '' ? "\n## Attributes\n{$attributes}" : '';

$relationships = $model->relationships->toCollection()
->map(fn ($rel) => "- [ ] {$rel->type}: {$rel->model}")
->implode("\n");

$relationshipSection = $relationships !== '' ? "\n## Relationships\n{$relationships}" : '';

return "## {$model->name}{$attributeSection}{$relationshipSection}";
}
}
44 changes: 44 additions & 0 deletions app/Services/BifrostService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace App\Services;

use App\Data\ManifestData;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;

class BifrostService
{
private string $url;

private string $token;

public function __construct()
{
$this->url = rtrim((string) config('services.bifrost.url'), '/');
$this->token = (string) config('services.bifrost.token');
}

public function registerSource(ManifestData $manifest): array
{
$response = Http::withHeaders([
'Authorization' => "Bearer {$this->token}",
'Accept' => 'application/json',
])->post("{$this->url}/api/sources", [
'name' => $manifest->bifrost->source,
'signing_secret' => Str::random(40),
'is_active' => true,
]);

$this->ensureSuccess($response, 'Failed to register Bifrost source');

return $response->json();
}

private function ensureSuccess(Response $response, string $message): void
{
if ($response->failed()) {
throw new \RuntimeException("{$message}: HTTP {$response->status()}");
}
}
}
65 changes: 65 additions & 0 deletions app/Services/GitHubService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace App\Services;

use App\Data\ManifestData;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;

class GitHubService
{
private string $token;

private string $org;

public function __construct()
{
$this->token = (string) config('services.github.token');
$this->org = (string) config('services.github.org', 'conduit-ui');
}

public function createRepo(ManifestData $manifest): array
{
$response = $this->client()
->post("https://api.github.com/orgs/{$this->org}/repos", [
'name' => $manifest->name,
'description' => $manifest->description,
'private' => true,
'auto_init' => true,
]);

$this->ensureSuccess($response, 'Failed to create GitHub repository');

return $response->json();
}

public function openIssue(string $repo, string $title, string $body): array
{
$response = $this->client()
->post("https://api.github.com/repos/{$this->org}/{$repo}/issues", [
'title' => $title,
'body' => $body,
]);

$this->ensureSuccess($response, "Failed to open issue: {$title}");

return $response->json();
}

private function client(): PendingRequest
{
return Http::withHeaders([
'Authorization' => "Bearer {$this->token}",
'Accept' => 'application/vnd.github+json',
'X-GitHub-Api-Version' => '2022-11-28',
]);
}

private function ensureSuccess(Response $response, string $message): void
{
if ($response->failed()) {
throw new \RuntimeException("{$message}: HTTP {$response->status()}");
}
}
}
15 changes: 15 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

return [

'github' => [
'token' => env('GITHUB_TOKEN'),
'org' => env('GITHUB_ORG', 'conduit-ui'),
],

'bifrost' => [
'url' => env('BIFROST_URL'),
'token' => env('BIFROST_TOKEN'),
],

];
Loading
Loading