Unverified 提交 444397ea authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Create Publish panel to easy GitHub and Vercel push (#655)

上级 cb60a056
ALTER TABLE `apps` ADD `vercel_project_id` text;--> statement-breakpoint
ALTER TABLE `apps` ADD `vercel_project_name` text;--> statement-breakpoint
ALTER TABLE `apps` ADD `vercel_team_id` text;--> statement-breakpoint
ALTER TABLE `apps` ADD `vercel_deployment_url` text;
\ No newline at end of file
差异被折叠。
...@@ -57,6 +57,13 @@ ...@@ -57,6 +57,13 @@
"when": 1750186036000, "when": 1750186036000,
"tag": "0007_dapper_overlord", "tag": "0007_dapper_overlord",
"breakpoints": true "breakpoints": true
},
{
"idx": 8,
"version": "6",
"when": 1752625491756,
"tag": "0008_medical_vulcan",
"breakpoints": true
} }
] ]
} }
\ No newline at end of file
- button "Set up your GitHub repo": - button "Set up your GitHub repo"
- img
- button "Create new repo" - button "Create new repo"
- button "Connect to existing repo" - button "Connect to existing repo"
- text: Repository Name - text: Repository Name
......
{ {
"name": "dyad", "name": "dyad",
"version": "0.11.1", "version": "0.13.0-beta.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "dyad", "name": "dyad",
"version": "0.11.1", "version": "0.13.0-beta.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "^1.2.8", "@ai-sdk/anthropic": "^1.2.8",
...@@ -38,6 +38,7 @@ ...@@ -38,6 +38,7 @@
"@tanstack/react-query": "^5.75.5", "@tanstack/react-query": "^5.75.5",
"@tanstack/react-router": "^1.114.34", "@tanstack/react-router": "^1.114.34",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@vercel/sdk": "^1.10.0",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"ai": "^4.3.4", "ai": "^4.3.4",
"better-sqlite3": "^11.9.1", "better-sqlite3": "^11.9.1",
...@@ -6588,6 +6589,23 @@ ...@@ -6588,6 +6589,23 @@
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/@vercel/sdk": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@vercel/sdk/-/sdk-1.10.0.tgz",
"integrity": "sha512-Z3bTFhDkQoEt2wviWxbvmkrkTPxVCYZaRlkV2Y3O/oRwVRnYiZ1tAK7NkjnSNtc19vARRkEAma/DfiaqVMlPzQ==",
"bin": {
"mcp": "bin/mcp-server.js"
},
"peerDependencies": {
"@modelcontextprotocol/sdk": ">=1.5.0 <1.10.0",
"zod": "^3"
},
"peerDependenciesMeta": {
"@modelcontextprotocol/sdk": {
"optional": true
}
}
},
"node_modules/@vitejs/plugin-react": { "node_modules/@vitejs/plugin-react": {
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz",
......
...@@ -111,6 +111,7 @@ ...@@ -111,6 +111,7 @@
"@tanstack/react-query": "^5.75.5", "@tanstack/react-query": "^5.75.5",
"@tanstack/react-router": "^1.114.34", "@tanstack/react-router": "^1.114.34",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@vercel/sdk": "^1.10.0",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"ai": "^4.3.4", "ai": "^4.3.4",
"better-sqlite3": "^11.9.1", "better-sqlite3": "^11.9.1",
......
...@@ -131,7 +131,7 @@ function WindowsControls() { ...@@ -131,7 +131,7 @@ function WindowsControls() {
return ( return (
<div className="ml-auto flex no-app-region-drag"> <div className="ml-auto flex no-app-region-drag">
<button <button
className="w-12 h-11 flex items-center justify-center hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" className="w-10 h-10 flex items-center justify-center hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
onClick={minimizeWindow} onClick={minimizeWindow}
aria-label="Minimize" aria-label="Minimize"
> >
...@@ -150,7 +150,7 @@ function WindowsControls() { ...@@ -150,7 +150,7 @@ function WindowsControls() {
</svg> </svg>
</button> </button>
<button <button
className="w-12 h-11 flex items-center justify-center hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" className="w-10 h-10 flex items-center justify-center hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
onClick={maximizeWindow} onClick={maximizeWindow}
aria-label="Maximize" aria-label="Maximize"
> >
...@@ -171,7 +171,7 @@ function WindowsControls() { ...@@ -171,7 +171,7 @@ function WindowsControls() {
</svg> </svg>
</button> </button>
<button <button
className="w-12 h-11 flex items-center justify-center hover:bg-red-500 transition-colors" className="w-10 h-10 flex items-center justify-center hover:bg-red-500 transition-colors"
onClick={closeWindow} onClick={closeWindow}
aria-label="Close" aria-label="Close"
> >
......
...@@ -8,7 +8,7 @@ export const appsListAtom = atom<App[]>([]); ...@@ -8,7 +8,7 @@ export const appsListAtom = atom<App[]>([]);
export const appBasePathAtom = atom<string>(""); export const appBasePathAtom = atom<string>("");
export const versionsListAtom = atom<Version[]>([]); export const versionsListAtom = atom<Version[]>([]);
export const previewModeAtom = atom< export const previewModeAtom = atom<
"preview" | "code" | "problems" | "configure" "preview" | "code" | "problems" | "configure" | "publish"
>("preview"); >("preview");
export const selectedVersionIdAtom = atom<string | null>(null); export const selectedVersionIdAtom = atom<string | null>(null);
export const appOutputAtom = atom<AppOutput[]>([]); export const appOutputAtom = atom<AppOutput[]>([]);
......
...@@ -5,7 +5,6 @@ import { ...@@ -5,7 +5,6 @@ import {
Clipboard, Clipboard,
Check, Check,
AlertTriangle, AlertTriangle,
ChevronDown,
ChevronRight, ChevronRight,
} from "lucide-react"; } from "lucide-react";
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
...@@ -32,6 +31,7 @@ import { Label } from "@/components/ui/label"; ...@@ -32,6 +31,7 @@ import { Label } from "@/components/ui/label";
interface GitHubConnectorProps { interface GitHubConnectorProps {
appId: number | null; appId: number | null;
folderName: string; folderName: string;
expanded?: boolean;
} }
interface GitHubRepo { interface GitHubRepo {
...@@ -57,6 +57,7 @@ interface UnconnectedGitHubConnectorProps { ...@@ -57,6 +57,7 @@ interface UnconnectedGitHubConnectorProps {
settings: any; settings: any;
refreshSettings: () => void; refreshSettings: () => void;
refreshApp: () => void; refreshApp: () => void;
expanded?: boolean;
} }
function ConnectedGitHubConnector({ function ConnectedGitHubConnector({
...@@ -112,10 +113,7 @@ function ConnectedGitHubConnector({ ...@@ -112,10 +113,7 @@ function ConnectedGitHubConnector({
}; };
return ( return (
<div <div className="w-full" data-testid="github-connected-repo">
className="mt-4 w-full border border-gray-200 rounded-md p-4"
data-testid="github-connected-repo"
>
<p>Connected to GitHub Repo:</p> <p>Connected to GitHub Repo:</p>
<a <a
onClick={(e) => { onClick={(e) => {
...@@ -271,9 +269,10 @@ function UnconnectedGitHubConnector({ ...@@ -271,9 +269,10 @@ function UnconnectedGitHubConnector({
settings, settings,
refreshSettings, refreshSettings,
refreshApp, refreshApp,
expanded,
}: UnconnectedGitHubConnectorProps) { }: UnconnectedGitHubConnectorProps) {
// --- Collapsible State --- // --- Collapsible State ---
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(expanded || false);
// --- GitHub Device Flow State --- // --- GitHub Device Flow State ---
const [githubUserCode, setGithubUserCode] = useState<string | null>(null); const [githubUserCode, setGithubUserCode] = useState<string | null>(null);
...@@ -636,22 +635,19 @@ function UnconnectedGitHubConnector({ ...@@ -636,22 +635,19 @@ function UnconnectedGitHubConnector({
} }
return ( return (
<div <div className="w-full" data-testid="github-setup-repo">
className="mt-4 w-full border border-gray-200 rounded-md"
data-testid="github-setup-repo"
>
{/* Collapsible Header */} {/* Collapsible Header */}
<button <button
type="button" type="button"
onClick={() => setIsExpanded(!isExpanded)} onClick={!isExpanded ? () => setIsExpanded(true) : undefined}
className={`cursor-pointer w-full p-4 text-left transition-colors rounded-md flex items-center justify-between ${ className={`w-full p-4 text-left transition-colors rounded-md flex items-center justify-between ${
!isExpanded ? "hover:bg-gray-50 dark:hover:bg-gray-800/50" : "" !isExpanded
? "cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50"
: ""
}`} }`}
> >
<span className="font-medium">Set up your GitHub repo</span> <span className="font-medium">Set up your GitHub repo</span>
{isExpanded ? ( {isExpanded ? undefined : (
<ChevronDown className="h-4 w-4 text-gray-500" />
) : (
<ChevronRight className="h-4 w-4 text-gray-500" /> <ChevronRight className="h-4 w-4 text-gray-500" />
)} )}
</button> </button>
...@@ -879,7 +875,11 @@ function UnconnectedGitHubConnector({ ...@@ -879,7 +875,11 @@ function UnconnectedGitHubConnector({
); );
} }
export function GitHubConnector({ appId, folderName }: GitHubConnectorProps) { export function GitHubConnector({
appId,
folderName,
expanded,
}: GitHubConnectorProps) {
const { app, refreshApp } = useLoadApp(appId); const { app, refreshApp } = useLoadApp(appId);
const { settings, refreshSettings } = useSettings(); const { settings, refreshSettings } = useSettings();
...@@ -899,6 +899,7 @@ export function GitHubConnector({ appId, folderName }: GitHubConnectorProps) { ...@@ -899,6 +899,7 @@ export function GitHubConnector({ appId, folderName }: GitHubConnectorProps) {
settings={settings} settings={settings}
refreshSettings={refreshSettings} refreshSettings={refreshSettings}
refreshApp={refreshApp} refreshApp={refreshApp}
expanded={expanded}
/> />
); );
} }
......
差异被折叠。
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { useSettings } from "@/hooks/useSettings";
import { showSuccess, showError } from "@/lib/toast";
export function VercelIntegration() {
const { settings, updateSettings } = useSettings();
const [isDisconnecting, setIsDisconnecting] = useState(false);
const handleDisconnectFromVercel = async () => {
setIsDisconnecting(true);
try {
const result = await updateSettings({
vercelAccessToken: undefined,
});
if (result) {
showSuccess("Successfully disconnected from Vercel");
} else {
showError("Failed to disconnect from Vercel");
}
} catch (err: any) {
showError(
err.message || "An error occurred while disconnecting from Vercel",
);
} finally {
setIsDisconnecting(false);
}
};
const isConnected = !!settings?.vercelAccessToken;
if (!isConnected) {
return null;
}
return (
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">
Vercel Integration
</h3>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Your account is connected to Vercel.
</p>
</div>
<Button
onClick={handleDisconnectFromVercel}
variant="destructive"
size="sm"
disabled={isDisconnecting}
className="flex items-center gap-2"
>
{isDisconnecting ? "Disconnecting..." : "Disconnect from Vercel"}
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M24 22.525H0l12-21.05 12 21.05z" />
</svg>
</Button>
</div>
);
}
...@@ -10,6 +10,7 @@ import { ...@@ -10,6 +10,7 @@ import {
Trash2, Trash2,
AlertTriangle, AlertTriangle,
Wrench, Wrench,
Globe,
} from "lucide-react"; } from "lucide-react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useEffect, useRef, useState, useCallback } from "react"; import { useEffect, useRef, useState, useCallback } from "react";
...@@ -32,7 +33,12 @@ import { useMutation } from "@tanstack/react-query"; ...@@ -32,7 +33,12 @@ import { useMutation } from "@tanstack/react-query";
import { useCheckProblems } from "@/hooks/useCheckProblems"; import { useCheckProblems } from "@/hooks/useCheckProblems";
import { isPreviewOpenAtom } from "@/atoms/viewAtoms"; import { isPreviewOpenAtom } from "@/atoms/viewAtoms";
export type PreviewMode = "preview" | "code" | "problems" | "configure"; export type PreviewMode =
| "preview"
| "code"
| "problems"
| "configure"
| "publish";
const BUTTON_CLASS_NAME = const BUTTON_CLASS_NAME =
"no-app-region-drag cursor-pointer relative flex items-center gap-1 px-2 py-1 rounded-md text-[13px] font-medium z-10 hover:bg-[var(--background)]"; "no-app-region-drag cursor-pointer relative flex items-center gap-1 px-2 py-1 rounded-md text-[13px] font-medium z-10 hover:bg-[var(--background)]";
...@@ -46,12 +52,13 @@ export const PreviewHeader = () => { ...@@ -46,12 +52,13 @@ export const PreviewHeader = () => {
const codeRef = useRef<HTMLButtonElement>(null); const codeRef = useRef<HTMLButtonElement>(null);
const problemsRef = useRef<HTMLButtonElement>(null); const problemsRef = useRef<HTMLButtonElement>(null);
const configureRef = useRef<HTMLButtonElement>(null); const configureRef = useRef<HTMLButtonElement>(null);
const publishRef = useRef<HTMLButtonElement>(null);
const [indicatorStyle, setIndicatorStyle] = useState({ left: 0, width: 0 }); const [indicatorStyle, setIndicatorStyle] = useState({ left: 0, width: 0 });
const [windowWidth, setWindowWidth] = useState(window.innerWidth); const [windowWidth, setWindowWidth] = useState(window.innerWidth);
const { problemReport } = useCheckProblems(selectedAppId); const { problemReport } = useCheckProblems(selectedAppId);
const { restartApp, refreshAppIframe } = useRunApp(); const { restartApp, refreshAppIframe } = useRunApp();
const isCompact = windowWidth < 840; const isCompact = windowWidth < 860;
// Track window width // Track window width
useEffect(() => { useEffect(() => {
...@@ -128,6 +135,9 @@ export const PreviewHeader = () => { ...@@ -128,6 +135,9 @@ export const PreviewHeader = () => {
case "configure": case "configure":
targetRef = configureRef; targetRef = configureRef;
break; break;
case "publish":
targetRef = publishRef;
break;
default: default:
return; return;
} }
...@@ -239,6 +249,13 @@ export const PreviewHeader = () => { ...@@ -239,6 +249,13 @@ export const PreviewHeader = () => {
"Configure", "Configure",
"configure-mode-button", "configure-mode-button",
)} )}
{renderButton(
"publish",
publishRef,
<Globe size={14} />,
"Publish",
"publish-mode-button",
)}
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
<DropdownMenu> <DropdownMenu>
......
...@@ -15,6 +15,7 @@ import { useEffect, useRef, useState } from "react"; ...@@ -15,6 +15,7 @@ import { useEffect, useRef, useState } from "react";
import { PanelGroup, Panel, PanelResizeHandle } from "react-resizable-panels"; import { PanelGroup, Panel, PanelResizeHandle } from "react-resizable-panels";
import { Console } from "./Console"; import { Console } from "./Console";
import { useRunApp } from "@/hooks/useRunApp"; import { useRunApp } from "@/hooks/useRunApp";
import { PublishPanel } from "./PublishPanel";
interface ConsoleHeaderProps { interface ConsoleHeaderProps {
isOpen: boolean; isOpen: boolean;
...@@ -116,6 +117,8 @@ export function PreviewPanel() { ...@@ -116,6 +117,8 @@ export function PreviewPanel() {
<CodeView loading={loading} app={app} /> <CodeView loading={loading} app={app} />
) : previewMode === "configure" ? ( ) : previewMode === "configure" ? (
<ConfigurePanel /> <ConfigurePanel />
) : previewMode === "publish" ? (
<PublishPanel />
) : ( ) : (
<Problems /> <Problems />
)} )}
......
import { useAtomValue } from "jotai";
import { selectedAppIdAtom } from "@/atoms/appAtoms";
import { useLoadApp } from "@/hooks/useLoadApp";
import { GitHubConnector } from "@/components/GitHubConnector";
import { VercelConnector } from "@/components/VercelConnector";
import { IpcClient } from "@/ipc/ipc_client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export const PublishPanel = () => {
const selectedAppId = useAtomValue(selectedAppIdAtom);
const { app, loading } = useLoadApp(selectedAppId);
if (loading) {
return (
<div className="flex flex-col items-center justify-center h-full p-8 text-center">
<div className="w-12 h-12 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center">
<svg
className="w-6 h-6 text-blue-600 dark:text-blue-400 animate-spin"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="m4 12a8 8 0 0 1 8-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 0 1 4 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
</div>
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
Loading...
</h2>
</div>
);
}
if (!selectedAppId || !app) {
return (
<div className="flex flex-col items-center justify-center h-full p-8 text-center">
<div className="w-12 h-12 rounded-full bg-gray-100 dark:bg-gray-900/30 flex items-center justify-center">
<svg
className="w-6 h-6 text-gray-600 dark:text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
/>
</svg>
</div>
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
No App Selected
</h2>
<p className="text-gray-600 dark:text-gray-400 max-w-md">
Select an app to view publishing options.
</p>
</div>
);
}
return (
<div className="flex flex-col h-full overflow-y-auto">
<div className="p-4 space-y-4">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100 mb-2">
Publish App
</h1>
</div>
{/* GitHub Section */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z"
clipRule="evenodd"
/>
</svg>
GitHub
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-gray-600 dark:text-gray-400">
Sync your code to GitHub for collaboration.
</p>
<GitHubConnector
appId={selectedAppId}
folderName={app.name}
expanded={true}
/>
</CardContent>
</Card>
{/* Vercel Section */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2">
<button
onClick={() => {
const ipcClient = IpcClient.getInstance();
ipcClient.openExternalUrl("https://vercel.com/dashboard");
}}
className="flex items-center gap-2 hover:text-blue-600 dark:hover:text-blue-400 transition-colors cursor-pointer bg-transparent border-none p-0"
>
<svg
className="w-5 h-5"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M24 22.525H0l12-21.05 12 21.05z" />
</svg>
Vercel
</button>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-gray-600 dark:text-gray-400">
Publish your app by deploying it to Vercel.
</p>
{!app?.githubOrg || !app?.githubRepo ? (
<div className="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-4">
<div className="flex items-start gap-3">
<svg
className="w-5 h-5 text-amber-600 dark:text-amber-400 mt-0.5 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
<div>
<h3 className="text-sm font-medium text-amber-800 dark:text-amber-200">
GitHub Required for Vercel Deployment
</h3>
<p className="text-sm text-amber-700 dark:text-amber-300 mt-1">
Deploying to Vercel requires connecting to GitHub first.
Please set up your GitHub repository above.
</p>
</div>
</div>
</div>
) : (
<VercelConnector appId={selectedAppId} folderName={app.name} />
)}
</CardContent>
</Card>
</div>
</div>
);
};
...@@ -16,6 +16,10 @@ export const apps = sqliteTable("apps", { ...@@ -16,6 +16,10 @@ export const apps = sqliteTable("apps", {
githubRepo: text("github_repo"), githubRepo: text("github_repo"),
githubBranch: text("github_branch"), githubBranch: text("github_branch"),
supabaseProjectId: text("supabase_project_id"), supabaseProjectId: text("supabase_project_id"),
vercelProjectId: text("vercel_project_id"),
vercelProjectName: text("vercel_project_name"),
vercelTeamId: text("vercel_team_id"),
vercelDeploymentUrl: text("vercel_deployment_url"),
chatContext: text("chat_context", { mode: "json" }), chatContext: text("chat_context", { mode: "json" }),
}); });
......
...@@ -46,6 +46,7 @@ import { gitCommit } from "../utils/git_utils"; ...@@ -46,6 +46,7 @@ import { gitCommit } from "../utils/git_utils";
import { safeSend } from "../utils/safe_sender"; import { safeSend } from "../utils/safe_sender";
import { normalizePath } from "../../../shared/normalizePath"; import { normalizePath } from "../../../shared/normalizePath";
import { isServerFunction } from "@/supabase_admin/supabase_utils"; import { isServerFunction } from "@/supabase_admin/supabase_utils";
import { getVercelTeamSlug } from "../utils/vercel_utils";
async function copyDir( async function copyDir(
source: string, source: string,
...@@ -370,10 +371,16 @@ export function registerAppHandlers() { ...@@ -370,10 +371,16 @@ export function registerAppHandlers() {
supabaseProjectName = await getSupabaseProjectName(app.supabaseProjectId); supabaseProjectName = await getSupabaseProjectName(app.supabaseProjectId);
} }
let vercelTeamSlug: string | null = null;
if (app.vercelTeamId) {
vercelTeamSlug = await getVercelTeamSlug(app.vercelTeamId);
}
return { return {
...app, ...app,
files, files,
supabaseProjectName, supabaseProjectName,
vercelTeamSlug,
}; };
}); });
......
差异被折叠。
...@@ -40,6 +40,15 @@ import type { ...@@ -40,6 +40,15 @@ import type {
EditAppFileReturnType, EditAppFileReturnType,
GetAppEnvVarsParams, GetAppEnvVarsParams,
SetAppEnvVarsParams, SetAppEnvVarsParams,
ConnectToExistingVercelProjectParams,
IsVercelProjectAvailableResponse,
CreateVercelProjectParams,
VercelDeployment,
GetVercelDeploymentsParams,
DisconnectVercelProjectParams,
IsVercelProjectAvailableParams,
SaveVercelAccessTokenParams,
VercelProject,
} from "./ipc_types"; } from "./ipc_types";
import type { AppChatContext, ProposalResult } from "@/lib/schemas"; import type { AppChatContext, ProposalResult } from "@/lib/schemas";
import { showError } from "@/lib/toast"; import { showError } from "@/lib/toast";
...@@ -646,6 +655,51 @@ export class IpcClient { ...@@ -646,6 +655,51 @@ export class IpcClient {
} }
// --- End GitHub Repo Management --- // --- End GitHub Repo Management ---
// --- Vercel Token Management ---
public async saveVercelAccessToken(
params: SaveVercelAccessTokenParams,
): Promise<void> {
await this.ipcRenderer.invoke("vercel:save-token", params);
}
// --- End Vercel Token Management ---
// --- Vercel Project Management ---
public async listVercelProjects(): Promise<VercelProject[]> {
return this.ipcRenderer.invoke("vercel:list-projects", undefined);
}
public async connectToExistingVercelProject(
params: ConnectToExistingVercelProjectParams,
): Promise<void> {
await this.ipcRenderer.invoke("vercel:connect-existing-project", params);
}
public async isVercelProjectAvailable(
params: IsVercelProjectAvailableParams,
): Promise<IsVercelProjectAvailableResponse> {
return this.ipcRenderer.invoke("vercel:is-project-available", params);
}
public async createVercelProject(
params: CreateVercelProjectParams,
): Promise<void> {
await this.ipcRenderer.invoke("vercel:create-project", params);
}
// Get Vercel Deployments
public async getVercelDeployments(
params: GetVercelDeploymentsParams,
): Promise<VercelDeployment[]> {
return this.ipcRenderer.invoke("vercel:get-deployments", params);
}
public async disconnectVercelProject(
params: DisconnectVercelProjectParams,
): Promise<void> {
await this.ipcRenderer.invoke("vercel:disconnect", params);
}
// --- End Vercel Project Management ---
// Get the main app version // Get the main app version
public async getAppVersion(): Promise<string> { public async getAppVersion(): Promise<string> {
const result = await this.ipcRenderer.invoke("get-app-version"); const result = await this.ipcRenderer.invoke("get-app-version");
......
...@@ -5,6 +5,7 @@ import { registerSettingsHandlers } from "./handlers/settings_handlers"; ...@@ -5,6 +5,7 @@ import { registerSettingsHandlers } from "./handlers/settings_handlers";
import { registerShellHandlers } from "./handlers/shell_handler"; import { registerShellHandlers } from "./handlers/shell_handler";
import { registerDependencyHandlers } from "./handlers/dependency_handlers"; import { registerDependencyHandlers } from "./handlers/dependency_handlers";
import { registerGithubHandlers } from "./handlers/github_handlers"; import { registerGithubHandlers } from "./handlers/github_handlers";
import { registerVercelHandlers } from "./handlers/vercel_handlers";
import { registerNodeHandlers } from "./handlers/node_handlers"; import { registerNodeHandlers } from "./handlers/node_handlers";
import { registerProposalHandlers } from "./handlers/proposal_handlers"; import { registerProposalHandlers } from "./handlers/proposal_handlers";
import { registerDebugHandlers } from "./handlers/debug_handlers"; import { registerDebugHandlers } from "./handlers/debug_handlers";
...@@ -34,6 +35,7 @@ export function registerIpcHandlers() { ...@@ -34,6 +35,7 @@ export function registerIpcHandlers() {
registerShellHandlers(); registerShellHandlers();
registerDependencyHandlers(); registerDependencyHandlers();
registerGithubHandlers(); registerGithubHandlers();
registerVercelHandlers();
registerNodeHandlers(); registerNodeHandlers();
registerProblemsHandlers(); registerProblemsHandlers();
registerProposalHandlers(); registerProposalHandlers();
......
...@@ -81,6 +81,10 @@ export interface App { ...@@ -81,6 +81,10 @@ export interface App {
githubBranch: string | null; githubBranch: string | null;
supabaseProjectId: string | null; supabaseProjectId: string | null;
supabaseProjectName: string | null; supabaseProjectName: string | null;
vercelProjectId: string | null;
vercelProjectName: string | null;
vercelTeamSlug: string | null;
vercelDeploymentUrl: string | null;
} }
export interface Version { export interface Version {
...@@ -266,3 +270,49 @@ export interface SetAppEnvVarsParams { ...@@ -266,3 +270,49 @@ export interface SetAppEnvVarsParams {
export interface GetAppEnvVarsParams { export interface GetAppEnvVarsParams {
appId: number; appId: number;
} }
export interface VercelDeployment {
uid: string;
url: string;
state: string;
createdAt: number;
target: string;
readyState: string;
}
export interface ConnectToExistingVercelProjectParams {
projectId: string;
appId: number;
}
export interface IsVercelProjectAvailableResponse {
available: boolean;
error?: string;
}
export interface CreateVercelProjectParams {
name: string;
appId: number;
}
export interface GetVercelDeploymentsParams {
appId: number;
}
export interface DisconnectVercelProjectParams {
appId: number;
}
export interface IsVercelProjectAvailableParams {
name: string;
}
export interface SaveVercelAccessTokenParams {
token: string;
}
export interface VercelProject {
id: string;
name: string;
framework: string | null;
}
import { readSettings } from "../../main/settings";
import log from "electron-log";
import { IS_TEST_BUILD } from "./test_utils";
const logger = log.scope("vercel_utils");
// Use test server URLs when in test mode
const TEST_SERVER_BASE = "http://localhost:3500";
const VERCEL_API_BASE = IS_TEST_BUILD
? `${TEST_SERVER_BASE}/vercel/api`
: "https://api.vercel.com";
export async function getVercelTeamSlug(
teamId: string,
): Promise<string | null> {
try {
const settings = readSettings();
const accessToken = settings.vercelAccessToken?.value;
if (!accessToken) {
logger.warn("No Vercel access token found when trying to get team slug");
return null;
}
const response = await fetch(`${VERCEL_API_BASE}/v2/teams/${teamId}`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
logger.error(
`Failed to fetch team details: ${response.status} ${response.statusText}`,
);
return null;
}
const data = await response.json();
// Return the team slug if available
return data.slug || null;
} catch (error) {
logger.error("Error getting Vercel team slug:", error);
return null;
}
}
...@@ -136,6 +136,7 @@ export const UserSettingsSchema = z.object({ ...@@ -136,6 +136,7 @@ export const UserSettingsSchema = z.object({
providerSettings: z.record(z.string(), ProviderSettingSchema), providerSettings: z.record(z.string(), ProviderSettingSchema),
githubUser: GithubUserSchema.optional(), githubUser: GithubUserSchema.optional(),
githubAccessToken: SecretSchema.optional(), githubAccessToken: SecretSchema.optional(),
vercelAccessToken: SecretSchema.optional(),
supabase: SupabaseSchema.optional(), supabase: SupabaseSchema.optional(),
autoApproveChanges: z.boolean().optional(), autoApproveChanges: z.boolean().optional(),
telemetryConsent: z.enum(["opted_in", "opted_out", "unset"]).optional(), telemetryConsent: z.enum(["opted_in", "opted_out", "unset"]).optional(),
......
...@@ -74,6 +74,13 @@ export function readSettings(): UserSettings { ...@@ -74,6 +74,13 @@ export function readSettings(): UserSettings {
encryptionType, encryptionType,
}; };
} }
if (combinedSettings.vercelAccessToken) {
const encryptionType = combinedSettings.vercelAccessToken.encryptionType;
combinedSettings.vercelAccessToken = {
value: decrypt(combinedSettings.vercelAccessToken),
encryptionType,
};
}
for (const provider in combinedSettings.providerSettings) { for (const provider in combinedSettings.providerSettings) {
if (combinedSettings.providerSettings[provider].apiKey) { if (combinedSettings.providerSettings[provider].apiKey) {
const encryptionType = const encryptionType =
...@@ -105,6 +112,11 @@ export function writeSettings(settings: Partial<UserSettings>): void { ...@@ -105,6 +112,11 @@ export function writeSettings(settings: Partial<UserSettings>): void {
newSettings.githubAccessToken.value, newSettings.githubAccessToken.value,
); );
} }
if (newSettings.vercelAccessToken) {
newSettings.vercelAccessToken = encrypt(
newSettings.vercelAccessToken.value,
);
}
if (newSettings.supabase) { if (newSettings.supabase) {
if (newSettings.supabase.accessToken) { if (newSettings.supabase.accessToken) {
newSettings.supabase.accessToken = encrypt( newSettings.supabase.accessToken = encrypt(
......
...@@ -342,7 +342,9 @@ export default function AppDetailsPage() { ...@@ -342,7 +342,9 @@ export default function AppDetailsPage() {
Open in Chat Open in Chat
<MessageCircle className="h-4 w-4" /> <MessageCircle className="h-4 w-4" />
</Button> </Button>
<GitHubConnector appId={appId} folderName={selectedApp.path} /> <div className="border border-gray-200 rounded-md p-4">
<GitHubConnector appId={appId} folderName={selectedApp.path} />
</div>
{appId && <SupabaseConnector appId={appId} />} {appId && <SupabaseConnector appId={appId} />}
{appId && <CapacitorControls appId={appId} />} {appId && <CapacitorControls appId={appId} />}
<AppUpgrades appId={appId} /> <AppUpgrades appId={appId} />
......
...@@ -14,6 +14,7 @@ import { Button } from "@/components/ui/button"; ...@@ -14,6 +14,7 @@ import { Button } from "@/components/ui/button";
import { ArrowLeft } from "lucide-react"; import { ArrowLeft } from "lucide-react";
import { useRouter } from "@tanstack/react-router"; import { useRouter } from "@tanstack/react-router";
import { GitHubIntegration } from "@/components/GitHubIntegration"; import { GitHubIntegration } from "@/components/GitHubIntegration";
import { VercelIntegration } from "@/components/VercelIntegration";
import { SupabaseIntegration } from "@/components/SupabaseIntegration"; import { SupabaseIntegration } from "@/components/SupabaseIntegration";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
...@@ -109,6 +110,7 @@ export default function SettingsPage() { ...@@ -109,6 +110,7 @@ export default function SettingsPage() {
</h2> </h2>
<div className="space-y-4"> <div className="space-y-4">
<GitHubIntegration /> <GitHubIntegration />
<VercelIntegration />
<SupabaseIntegration /> <SupabaseIntegration />
</div> </div>
</div> </div>
......
...@@ -55,6 +55,13 @@ const validInvokeChannels = [ ...@@ -55,6 +55,13 @@ const validInvokeChannels = [
"github:connect-existing-repo", "github:connect-existing-repo",
"github:push", "github:push",
"github:disconnect", "github:disconnect",
"vercel:save-token",
"vercel:list-projects",
"vercel:is-project-available",
"vercel:create-project",
"vercel:connect-existing-project",
"vercel:get-deployments",
"vercel:disconnect",
"get-app-version", "get-app-version",
"reload-env-path", "reload-env-path",
"get-proposal", "get-proposal",
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论