github repo creation flow
This commit is contained in:
@@ -3,7 +3,9 @@ CREATE TABLE `apps` (
|
||||
`name` text NOT NULL,
|
||||
`path` text NOT NULL,
|
||||
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||
`updated_at` integer DEFAULT (unixepoch()) NOT NULL
|
||||
`updated_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||
`github_org` text,
|
||||
`github_repo` text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `chats` (
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "8551dcbc-91d8-4b82-afc4-9c4b6684de3a",
|
||||
"id": "1a0ffcb3-606d-4b03-81b7-7c585555a548",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"apps": {
|
||||
@@ -43,6 +43,20 @@
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch())"
|
||||
},
|
||||
"github_org": {
|
||||
"name": "github_org",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"github_repo": {
|
||||
"name": "github_repo",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1744233051865,
|
||||
"tag": "0000_mighty_stark_industries",
|
||||
"when": 1744692127560,
|
||||
"tag": "0000_nebulous_proemial_gods",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
||||
@@ -3,13 +3,16 @@ import { Button } from "@/components/ui/button";
|
||||
import { Github } from "lucide-react";
|
||||
import { IpcClient } from "@/ipc/ipc_client";
|
||||
import { useSettings } from "@/hooks/useSettings";
|
||||
import { useLoadApp } from "@/hooks/useLoadApp";
|
||||
|
||||
interface GitHubConnectorProps {
|
||||
appId: number | null;
|
||||
folderName: string;
|
||||
}
|
||||
|
||||
export function GitHubConnector({ appId }: GitHubConnectorProps) {
|
||||
export function GitHubConnector({ appId, folderName }: GitHubConnectorProps) {
|
||||
// --- GitHub Device Flow State ---
|
||||
const { app, refreshApp } = useLoadApp(appId);
|
||||
const { settings, refreshSettings } = useSettings();
|
||||
const [githubUserCode, setGithubUserCode] = useState<string | null>(null);
|
||||
const [githubVerificationUri, setGithubVerificationUri] = useState<
|
||||
@@ -106,10 +109,127 @@ export function GitHubConnector({ appId }: GitHubConnectorProps) {
|
||||
};
|
||||
}, [appId]); // Re-run effect if appId changes
|
||||
|
||||
// --- Create Repo State ---
|
||||
const [repoName, setRepoName] = useState(folderName);
|
||||
const [repoAvailable, setRepoAvailable] = useState<boolean | null>(null);
|
||||
const [repoCheckError, setRepoCheckError] = useState<string | null>(null);
|
||||
const [isCheckingRepo, setIsCheckingRepo] = useState(false);
|
||||
const [isCreatingRepo, setIsCreatingRepo] = useState(false);
|
||||
const [createRepoError, setCreateRepoError] = useState<string | null>(null);
|
||||
const [createRepoSuccess, setCreateRepoSuccess] = useState<boolean>(false);
|
||||
|
||||
// Assume org is the authenticated user for now (could add org input later)
|
||||
// TODO: After device flow, fetch and store the GitHub username/org in settings for use here
|
||||
const githubOrg = ""; // Use empty string for now (GitHub API will default to the authenticated user)
|
||||
|
||||
const handleRepoNameBlur = async () => {
|
||||
setRepoCheckError(null);
|
||||
setRepoAvailable(null);
|
||||
if (!repoName) return;
|
||||
setIsCheckingRepo(true);
|
||||
try {
|
||||
const result = await IpcClient.getInstance().checkGithubRepoAvailable(
|
||||
githubOrg,
|
||||
repoName
|
||||
);
|
||||
setRepoAvailable(result.available);
|
||||
if (!result.available) {
|
||||
setRepoCheckError(result.error || "Repository name is not available.");
|
||||
}
|
||||
} catch (err: any) {
|
||||
setRepoCheckError(err.message || "Failed to check repo availability.");
|
||||
} finally {
|
||||
setIsCheckingRepo(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateRepo = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setCreateRepoError(null);
|
||||
setIsCreatingRepo(true);
|
||||
setCreateRepoSuccess(false);
|
||||
try {
|
||||
const result = await IpcClient.getInstance().createGithubRepo(
|
||||
githubOrg,
|
||||
repoName,
|
||||
appId!
|
||||
);
|
||||
if (result.success) {
|
||||
setCreateRepoSuccess(true);
|
||||
setRepoCheckError(null);
|
||||
refreshApp();
|
||||
} else {
|
||||
setCreateRepoError(result.error || "Failed to create repository.");
|
||||
}
|
||||
} catch (err: any) {
|
||||
setCreateRepoError(err.message || "Failed to create repository.");
|
||||
} finally {
|
||||
setIsCreatingRepo(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (app?.githubOrg && app?.githubRepo) {
|
||||
return (
|
||||
<div className="mt-4 w-full border border-gray-200 rounded-md p-4">
|
||||
<p>Connected to GitHub Repo:</p>
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
IpcClient.getInstance().openExternalUrl(
|
||||
`https://github.com/${app.githubOrg}/${app.githubRepo}`
|
||||
);
|
||||
}}
|
||||
className="cursor-pointer text-blue-600 hover:underline dark:text-blue-400"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{app.githubOrg}/{app.githubRepo}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (settings?.githubSettings.secrets) {
|
||||
return (
|
||||
<div className="mt-4 w-full">
|
||||
<p>Connected to GitHub!</p>
|
||||
<div className="mt-4 w-full border border-gray-200 rounded-md p-4">
|
||||
<p>Set up your GitHub repo</p>
|
||||
<form className="mt-4 space-y-2" onSubmit={handleCreateRepo}>
|
||||
<label className="block text-sm font-medium">Repository Name</label>
|
||||
<input
|
||||
className="w-full border rounded px-2 py-1"
|
||||
value={repoName}
|
||||
onChange={(e) => {
|
||||
setRepoName(e.target.value);
|
||||
setRepoAvailable(null);
|
||||
setRepoCheckError(null);
|
||||
}}
|
||||
onBlur={handleRepoNameBlur}
|
||||
disabled={isCreatingRepo}
|
||||
/>
|
||||
{isCheckingRepo && (
|
||||
<p className="text-xs text-gray-500">Checking availability...</p>
|
||||
)}
|
||||
{repoAvailable === true && (
|
||||
<p className="text-xs text-green-600">
|
||||
Repository name is available!
|
||||
</p>
|
||||
)}
|
||||
{repoAvailable === false && (
|
||||
<p className="text-xs text-red-600">{repoCheckError}</p>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isCreatingRepo || repoAvailable === false || !repoName}
|
||||
>
|
||||
{isCreatingRepo ? "Creating..." : "Create Repo"}
|
||||
</Button>
|
||||
</form>
|
||||
{createRepoError && (
|
||||
<p className="text-red-600 mt-2">{createRepoError}</p>
|
||||
)}
|
||||
{createRepoSuccess && (
|
||||
<p className="text-green-600 mt-2">Repository created and linked!</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import { getDyadAppPath, getUserDataPath } from "../paths/paths";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
// Database connection factory
|
||||
let _db: ReturnType<typeof drizzle> | null = null;
|
||||
@@ -91,3 +92,14 @@ try {
|
||||
export const db = _db as any as BetterSQLite3Database<typeof schema> & {
|
||||
$client: Database.Database;
|
||||
};
|
||||
|
||||
export async function updateAppGithubRepo(
|
||||
appId: number,
|
||||
org: string,
|
||||
repo: string
|
||||
): Promise<void> {
|
||||
await db
|
||||
.update(schema.apps)
|
||||
.set({ githubOrg: org, githubRepo: repo })
|
||||
.where(eq(schema.apps.id, appId));
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ export const apps = sqliteTable("apps", {
|
||||
updatedAt: integer("updated_at", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.default(sql`(unixepoch())`),
|
||||
githubOrg: text("github_org"),
|
||||
githubRepo: text("github_repo"),
|
||||
});
|
||||
|
||||
export const chats = sqliteTable("chats", {
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
IpcMainInvokeEvent,
|
||||
} from "electron";
|
||||
import fetch from "node-fetch"; // Use node-fetch for making HTTP requests in main process
|
||||
import { writeSettings } from "../../main/settings";
|
||||
import { writeSettings, readSettings } from "../../main/settings";
|
||||
import { updateAppGithubRepo } from "../../db/index";
|
||||
|
||||
// --- GitHub Device Flow Constants ---
|
||||
// TODO: Fetch this securely, e.g., from environment variables or a config file
|
||||
@@ -275,8 +276,97 @@ function handleStartGithubFlow(
|
||||
// event.sender.send('github:flow-cancelled', { message: 'GitHub flow cancelled.' });
|
||||
// }
|
||||
|
||||
// --- GitHub Repo Availability Handler ---
|
||||
async function handleIsRepoAvailable(
|
||||
event: IpcMainInvokeEvent,
|
||||
{ org, repo }: { org: string; repo: string }
|
||||
) {
|
||||
try {
|
||||
// Get access token from settings
|
||||
const settings = readSettings();
|
||||
const accessToken = settings.githubSettings?.secrets?.accessToken;
|
||||
if (!accessToken) {
|
||||
return { available: false, error: "Not authenticated with GitHub." };
|
||||
}
|
||||
// If org is empty, use the authenticated user
|
||||
const owner =
|
||||
org ||
|
||||
(await fetch("https://api.github.com/user", {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((u) => u.login));
|
||||
// Check if repo exists
|
||||
const url = `https://api.github.com/repos/${owner}/${repo}`;
|
||||
const res = await fetch(url, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
if (res.status === 404) {
|
||||
return { available: true };
|
||||
} else if (res.ok) {
|
||||
return { available: false, error: "Repository already exists." };
|
||||
} else {
|
||||
const data = await res.json();
|
||||
return { available: false, error: data.message || "Unknown error" };
|
||||
}
|
||||
} catch (err: any) {
|
||||
return { available: false, error: err.message || "Unknown error" };
|
||||
}
|
||||
}
|
||||
|
||||
// --- GitHub Create Repo Handler ---
|
||||
async function handleCreateRepo(
|
||||
event: IpcMainInvokeEvent,
|
||||
{ org, repo, appId }: { org: string; repo: string; appId: number }
|
||||
) {
|
||||
try {
|
||||
// Get access token from settings
|
||||
const settings = readSettings();
|
||||
const accessToken = settings.githubSettings?.secrets?.accessToken;
|
||||
if (!accessToken) {
|
||||
return { success: false, error: "Not authenticated with GitHub." };
|
||||
}
|
||||
// If org is empty, create for the authenticated user
|
||||
let owner = org;
|
||||
if (!owner) {
|
||||
const userRes = await fetch("https://api.github.com/user", {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
const user = await userRes.json();
|
||||
owner = user.login;
|
||||
}
|
||||
// Create repo
|
||||
const createUrl = org
|
||||
? `https://api.github.com/orgs/${owner}/repos`
|
||||
: `https://api.github.com/user/repos`;
|
||||
const res = await fetch(createUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/vnd.github+json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: repo,
|
||||
private: true,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
return { success: false, error: data.message || "Failed to create repo" };
|
||||
}
|
||||
// Store org and repo in the app's DB row (apps table)
|
||||
await updateAppGithubRepo(appId, owner, repo);
|
||||
return { success: true };
|
||||
} catch (err: any) {
|
||||
return { success: false, error: err.message || "Unknown error" };
|
||||
}
|
||||
}
|
||||
|
||||
// --- Registration ---
|
||||
export function registerGithubHandlers() {
|
||||
ipcMain.handle("github:start-flow", handleStartGithubFlow);
|
||||
// ipcMain.on('github:cancel-flow', handleCancelGithubFlow); // Uncomment if you add cancellation
|
||||
ipcMain.handle("github:is-repo-available", handleIsRepoAvailable);
|
||||
ipcMain.handle("github:create-repo", handleCreateRepo);
|
||||
}
|
||||
|
||||
@@ -571,6 +571,40 @@ export class IpcClient {
|
||||
// }
|
||||
// --- End GitHub Device Flow ---
|
||||
|
||||
// --- GitHub Repo Management ---
|
||||
public async checkGithubRepoAvailable(
|
||||
org: string,
|
||||
repo: string
|
||||
): Promise<{ available: boolean; error?: string }> {
|
||||
try {
|
||||
const result = await this.ipcRenderer.invoke("github:is-repo-available", {
|
||||
org,
|
||||
repo,
|
||||
});
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
return { available: false, error: error.message || "Unknown error" };
|
||||
}
|
||||
}
|
||||
|
||||
public async createGithubRepo(
|
||||
org: string,
|
||||
repo: string,
|
||||
appId: number
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const result = await this.ipcRenderer.invoke("github:create-repo", {
|
||||
org,
|
||||
repo,
|
||||
appId,
|
||||
});
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message || "Unknown error" };
|
||||
}
|
||||
}
|
||||
// --- End GitHub Repo Management ---
|
||||
|
||||
// Example methods for listening to events (if needed)
|
||||
// public on(channel: string, func: (...args: any[]) => void): void {
|
||||
}
|
||||
|
||||
@@ -50,6 +50,8 @@ export interface App {
|
||||
files: string[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
githubOrg: string | null;
|
||||
githubRepo: string | null;
|
||||
}
|
||||
|
||||
export interface Version {
|
||||
|
||||
@@ -238,7 +238,7 @@ export default function AppDetailsPage() {
|
||||
Open in Chat
|
||||
<MessageCircle className="h-5 w-5" />
|
||||
</Button>
|
||||
<GitHubConnector appId={appId} />
|
||||
<GitHubConnector appId={appId} folderName={selectedApp.path} />
|
||||
</div>
|
||||
|
||||
{/* Rename Dialog */}
|
||||
|
||||
@@ -15,7 +15,7 @@ export function getUserDataPath(): string {
|
||||
const electron = getElectron();
|
||||
|
||||
// When running in Electron and app is ready
|
||||
if (process.env.NODE_ENV !== "development") {
|
||||
if (process.env.NODE_ENV !== "development" && electron) {
|
||||
return electron!.app.getPath("userData");
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ const validInvokeChannels = [
|
||||
"reset-all",
|
||||
"nodejs-status",
|
||||
"github:start-flow",
|
||||
"github:is-repo-available",
|
||||
"github:create-repo",
|
||||
] as const;
|
||||
|
||||
// Add valid receive channels
|
||||
|
||||
Reference in New Issue
Block a user