github repo creation flow
This commit is contained in:
@@ -3,7 +3,9 @@ CREATE TABLE `apps` (
|
|||||||
`name` text NOT NULL,
|
`name` text NOT NULL,
|
||||||
`path` text NOT NULL,
|
`path` text NOT NULL,
|
||||||
`created_at` integer DEFAULT (unixepoch()) 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
|
--> statement-breakpoint
|
||||||
CREATE TABLE `chats` (
|
CREATE TABLE `chats` (
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"dialect": "sqlite",
|
"dialect": "sqlite",
|
||||||
"id": "8551dcbc-91d8-4b82-afc4-9c4b6684de3a",
|
"id": "1a0ffcb3-606d-4b03-81b7-7c585555a548",
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
"tables": {
|
"tables": {
|
||||||
"apps": {
|
"apps": {
|
||||||
@@ -43,6 +43,20 @@
|
|||||||
"notNull": true,
|
"notNull": true,
|
||||||
"autoincrement": false,
|
"autoincrement": false,
|
||||||
"default": "(unixepoch())"
|
"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": {},
|
"indexes": {},
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
{
|
{
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1744233051865,
|
"when": 1744692127560,
|
||||||
"tag": "0000_mighty_stark_industries",
|
"tag": "0000_nebulous_proemial_gods",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,13 +3,16 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Github } from "lucide-react";
|
import { Github } from "lucide-react";
|
||||||
import { IpcClient } from "@/ipc/ipc_client";
|
import { IpcClient } from "@/ipc/ipc_client";
|
||||||
import { useSettings } from "@/hooks/useSettings";
|
import { useSettings } from "@/hooks/useSettings";
|
||||||
|
import { useLoadApp } from "@/hooks/useLoadApp";
|
||||||
|
|
||||||
interface GitHubConnectorProps {
|
interface GitHubConnectorProps {
|
||||||
appId: number | null;
|
appId: number | null;
|
||||||
|
folderName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GitHubConnector({ appId }: GitHubConnectorProps) {
|
export function GitHubConnector({ appId, folderName }: GitHubConnectorProps) {
|
||||||
// --- GitHub Device Flow State ---
|
// --- GitHub Device Flow State ---
|
||||||
|
const { app, refreshApp } = useLoadApp(appId);
|
||||||
const { settings, refreshSettings } = useSettings();
|
const { settings, refreshSettings } = useSettings();
|
||||||
const [githubUserCode, setGithubUserCode] = useState<string | null>(null);
|
const [githubUserCode, setGithubUserCode] = useState<string | null>(null);
|
||||||
const [githubVerificationUri, setGithubVerificationUri] = useState<
|
const [githubVerificationUri, setGithubVerificationUri] = useState<
|
||||||
@@ -106,10 +109,127 @@ export function GitHubConnector({ appId }: GitHubConnectorProps) {
|
|||||||
};
|
};
|
||||||
}, [appId]); // Re-run effect if appId changes
|
}, [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) {
|
if (settings?.githubSettings.secrets) {
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 w-full">
|
<div className="mt-4 w-full border border-gray-200 rounded-md p-4">
|
||||||
<p>Connected to GitHub!</p>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import { getDyadAppPath, getUserDataPath } from "../paths/paths";
|
import { getDyadAppPath, getUserDataPath } from "../paths/paths";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
// Database connection factory
|
// Database connection factory
|
||||||
let _db: ReturnType<typeof drizzle> | null = null;
|
let _db: ReturnType<typeof drizzle> | null = null;
|
||||||
@@ -91,3 +92,14 @@ try {
|
|||||||
export const db = _db as any as BetterSQLite3Database<typeof schema> & {
|
export const db = _db as any as BetterSQLite3Database<typeof schema> & {
|
||||||
$client: Database.Database;
|
$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" })
|
updatedAt: integer("updated_at", { mode: "timestamp" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(sql`(unixepoch())`),
|
.default(sql`(unixepoch())`),
|
||||||
|
githubOrg: text("github_org"),
|
||||||
|
githubRepo: text("github_repo"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const chats = sqliteTable("chats", {
|
export const chats = sqliteTable("chats", {
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import {
|
|||||||
IpcMainInvokeEvent,
|
IpcMainInvokeEvent,
|
||||||
} from "electron";
|
} from "electron";
|
||||||
import fetch from "node-fetch"; // Use node-fetch for making HTTP requests in main process
|
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 ---
|
// --- GitHub Device Flow Constants ---
|
||||||
// TODO: Fetch this securely, e.g., from environment variables or a config file
|
// 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.' });
|
// 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 ---
|
// --- Registration ---
|
||||||
export function registerGithubHandlers() {
|
export function registerGithubHandlers() {
|
||||||
ipcMain.handle("github:start-flow", handleStartGithubFlow);
|
ipcMain.handle("github:start-flow", handleStartGithubFlow);
|
||||||
// ipcMain.on('github:cancel-flow', handleCancelGithubFlow); // Uncomment if you add cancellation
|
// 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 ---
|
// --- 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)
|
// Example methods for listening to events (if needed)
|
||||||
// public on(channel: string, func: (...args: any[]) => void): void {
|
// public on(channel: string, func: (...args: any[]) => void): void {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ export interface App {
|
|||||||
files: string[];
|
files: string[];
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
githubOrg: string | null;
|
||||||
|
githubRepo: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Version {
|
export interface Version {
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ export default function AppDetailsPage() {
|
|||||||
Open in Chat
|
Open in Chat
|
||||||
<MessageCircle className="h-5 w-5" />
|
<MessageCircle className="h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
<GitHubConnector appId={appId} />
|
<GitHubConnector appId={appId} folderName={selectedApp.path} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Rename Dialog */}
|
{/* Rename Dialog */}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function getUserDataPath(): string {
|
|||||||
const electron = getElectron();
|
const electron = getElectron();
|
||||||
|
|
||||||
// When running in Electron and app is ready
|
// 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");
|
return electron!.app.getPath("userData");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ const validInvokeChannels = [
|
|||||||
"reset-all",
|
"reset-all",
|
||||||
"nodejs-status",
|
"nodejs-status",
|
||||||
"github:start-flow",
|
"github:start-flow",
|
||||||
|
"github:is-repo-available",
|
||||||
|
"github:create-repo",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// Add valid receive channels
|
// Add valid receive channels
|
||||||
|
|||||||
Reference in New Issue
Block a user