diff --git a/src/components/GitHubConnector.tsx b/src/components/GitHubConnector.tsx index c1ee2ad..6f0bb7e 100644 --- a/src/components/GitHubConnector.tsx +++ b/src/components/GitHubConnector.tsx @@ -117,7 +117,10 @@ export function GitHubConnector({ appId, folderName }: GitHubConnectorProps) { const [isCreatingRepo, setIsCreatingRepo] = useState(false); const [createRepoError, setCreateRepoError] = useState(null); const [createRepoSuccess, setCreateRepoSuccess] = useState(false); - + // --- Sync to GitHub State --- + const [isSyncing, setIsSyncing] = useState(false); + const [syncError, setSyncError] = useState(null); + const [syncSuccess, setSyncSuccess] = useState(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) @@ -169,6 +172,24 @@ export function GitHubConnector({ appId, folderName }: GitHubConnectorProps) { }; if (app?.githubOrg && app?.githubRepo) { + const handleSyncToGithub = async () => { + setIsSyncing(true); + setSyncError(null); + setSyncSuccess(false); + try { + const result = await IpcClient.getInstance().syncGithubRepo(appId!); + if (result.success) { + setSyncSuccess(true); + } else { + setSyncError(result.error || "Failed to sync to GitHub."); + } + } catch (err: any) { + setSyncError(err.message || "Failed to sync to GitHub."); + } finally { + setIsSyncing(false); + } + }; + return (

Connected to GitHub Repo:

@@ -185,6 +206,42 @@ export function GitHubConnector({ appId, folderName }: GitHubConnectorProps) { > {app.githubOrg}/{app.githubRepo} +
+ +
+ {syncError &&

{syncError}

} + {syncSuccess && ( +

Successfully pushed to GitHub!

+ )}
); } diff --git a/src/ipc/handlers/github_handlers.ts b/src/ipc/handlers/github_handlers.ts index 470c56f..e872824 100644 --- a/src/ipc/handlers/github_handlers.ts +++ b/src/ipc/handlers/github_handlers.ts @@ -7,6 +7,14 @@ import { import fetch from "node-fetch"; // Use node-fetch for making HTTP requests in main process import { writeSettings, readSettings } from "../../main/settings"; import { updateAppGithubRepo } from "../../db/index"; +import git from "isomorphic-git"; +import http from "isomorphic-git/http/node"; +import path from "node:path"; +import fs from "node:fs"; +import { getDyadAppPath } from "../../paths/paths"; +import { db } from "../../db"; +import { apps } from "../../db/schema"; +import { eq } from "drizzle-orm"; // --- GitHub Device Flow Constants --- // TODO: Fetch this securely, e.g., from environment variables or a config file @@ -363,10 +371,57 @@ async function handleCreateRepo( } } +// --- GitHub Push Handler --- +async function handlePushToGithub( + event: IpcMainInvokeEvent, + { appId }: { 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." }; + } + // Get app info from DB + const app = await db.query.apps.findFirst({ where: eq(apps.id, appId) }); + if (!app || !app.githubOrg || !app.githubRepo) { + return { success: false, error: "App is not linked to a GitHub repo." }; + } + const appPath = getDyadAppPath(app.path); + // Set up remote URL with token + const remoteUrl = `https://${accessToken}:x-oauth-basic@github.com/${app.githubOrg}/${app.githubRepo}.git`; + // Set or update remote URL using git config + await git.setConfig({ + fs, + dir: appPath, + path: "remote.origin.url", + value: remoteUrl, + }); + // Push to GitHub + await git.push({ + fs, + http, + dir: appPath, + remote: "origin", + ref: "main", + onAuth: () => ({ username: accessToken, password: "x-oauth-basic" }), + force: false, + }); + return { success: true }; + } catch (err: any) { + return { + success: false, + error: err.message || "Failed to push to GitHub.", + }; + } +} + // --- 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); + ipcMain.handle("github:push", handlePushToGithub); } diff --git a/src/ipc/ipc_client.ts b/src/ipc/ipc_client.ts index a499543..25ac9ed 100644 --- a/src/ipc/ipc_client.ts +++ b/src/ipc/ipc_client.ts @@ -603,6 +603,18 @@ export class IpcClient { return { success: false, error: error.message || "Unknown error" }; } } + + // Sync (push) local repo to GitHub + public async syncGithubRepo( + appId: number + ): Promise<{ success: boolean; error?: string }> { + try { + const result = await this.ipcRenderer.invoke("github:push", { 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) diff --git a/src/preload.ts b/src/preload.ts index 69f8d05..baa16d8 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -35,6 +35,7 @@ const validInvokeChannels = [ "github:start-flow", "github:is-repo-available", "github:create-repo", + "github:push", ] as const; // Add valid receive channels