Merge branch 'feature/worktrees' of github.com:AutoMaker-Org/automaker into feature/worktrees

This commit is contained in:
Cody Seibert
2025-12-16 17:43:24 -05:00
2 changed files with 123 additions and 30 deletions

View File

@@ -53,6 +53,16 @@ export function CreatePRDialog({
// Reset state when dialog opens or worktree changes // Reset state when dialog opens or worktree changes
useEffect(() => { useEffect(() => {
if (open) { if (open) {
// Only reset form fields, not the result states (prUrl, browserUrl, showBrowserFallback)
// These are set by the API response and should persist until dialog closes
setTitle("");
setBody("");
setCommitMessage("");
setBaseBranch("main");
setIsDraft(false);
setError(null);
} else {
// Reset everything when dialog closes
setTitle(""); setTitle("");
setBody(""); setBody("");
setCommitMessage(""); setCommitMessage("");
@@ -98,21 +108,22 @@ export function CreatePRDialog({
onCreated(); onCreated();
} else { } else {
// Branch was pushed successfully // Branch was pushed successfully
toast.success("Branch pushed", { const prError = result.result.prError;
description: result.result.committed const hasBrowserUrl = !!result.result.browserUrl;
? `Commit ${result.result.commitHash} pushed to ${result.result.branch}`
: `Branch ${result.result.branch} pushed`,
});
// Check if we should show browser fallback // Check if we should show browser fallback
if (!result.result.prCreated && result.result.browserUrl) { if (!result.result.prCreated && hasBrowserUrl) {
const prError = result.result.prError;
// If gh CLI is not available, show browser fallback UI // If gh CLI is not available, show browser fallback UI
if (prError === "gh_cli_not_available" || !result.result.ghCliAvailable) { if (prError === "gh_cli_not_available" || !result.result.ghCliAvailable) {
setBrowserUrl(result.result.browserUrl); setBrowserUrl(result.result.browserUrl);
setShowBrowserFallback(true); setShowBrowserFallback(true);
onCreated(); toast.success("Branch pushed", {
description: result.result.committed
? `Commit ${result.result.commitHash} pushed to ${result.result.branch}`
: `Branch ${result.result.branch} pushed`,
});
// Don't call onCreated() here - we want to keep the dialog open to show the browser URL
setIsLoading(false);
return; // Don't close dialog, show browser fallback UI return; // Don't close dialog, show browser fallback UI
} }
@@ -135,16 +146,27 @@ export function CreatePRDialog({
description: errorMessage, description: errorMessage,
duration: 8000, duration: 8000,
}); });
onCreated(); // Don't call onCreated() here - we want to keep the dialog open to show the browser URL
setIsLoading(false);
return; return;
} }
} }
// Show success toast for push
toast.success("Branch pushed", {
description: result.result.committed
? `Commit ${result.result.commitHash} pushed to ${result.result.branch}`
: `Branch ${result.result.branch} pushed`,
});
// No browser URL available, just close // No browser URL available, just close
if (!result.result.prCreated) { if (!result.result.prCreated) {
toast.info("PR not created", { if (!hasBrowserUrl) {
description: "GitHub CLI (gh) may not be installed or authenticated", toast.info("PR not created", {
}); description: "Could not determine repository URL. GitHub CLI (gh) may not be installed or authenticated.",
duration: 8000,
});
}
} }
onCreated(); onCreated();
onOpenChange(false); onOpenChange(false);
@@ -177,6 +199,8 @@ export function CreatePRDialog({
if (!worktree) return null; if (!worktree) return null;
const shouldShowBrowserFallback = showBrowserFallback && browserUrl;
return ( return (
<Dialog open={open} onOpenChange={handleClose}> <Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-[550px]"> <DialogContent className="sm:max-w-[550px]">
@@ -212,7 +236,7 @@ export function CreatePRDialog({
View Pull Request View Pull Request
</Button> </Button>
</div> </div>
) : showBrowserFallback && browserUrl ? ( ) : shouldShowBrowserFallback ? (
<div className="py-6 text-center space-y-4"> <div className="py-6 text-center space-y-4">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-blue-500/10"> <div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-blue-500/10">
<GitPullRequest className="w-8 h-8 text-blue-500" /> <GitPullRequest className="w-8 h-8 text-blue-500" />
@@ -225,17 +249,30 @@ export function CreatePRDialog({
Click below to create a pull request in your browser. Click below to create a pull request in your browser.
</p> </p>
</div> </div>
<div className="space-y-2"> <div className="space-y-3">
<Button <Button
onClick={() => window.open(browserUrl, "_blank")} onClick={() => {
if (browserUrl) {
window.open(browserUrl, "_blank");
}
}}
className="gap-2 w-full" className="gap-2 w-full"
size="lg"
> >
<ExternalLink className="w-4 h-4" /> <ExternalLink className="w-4 h-4" />
Create PR in Browser Create PR in Browser
</Button> </Button>
<div className="p-2 bg-muted rounded text-xs break-all font-mono">
{browserUrl}
</div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Tip: Install the GitHub CLI (<code className="bg-muted px-1 rounded">gh</code>) to create PRs directly from the app Tip: Install the GitHub CLI (<code className="bg-muted px-1 rounded">gh</code>) to create PRs directly from the app
</p> </p>
<DialogFooter className="mt-4">
<Button variant="outline" onClick={handleClose}>
Close
</Button>
</DialogFooter>
</div> </div>
</div> </div>
) : ( ) : (

View File

@@ -11,13 +11,34 @@ const execAsync = promisify(exec);
// Extended PATH to include common tool installation locations // Extended PATH to include common tool installation locations
// This is needed because Electron apps don't inherit the user's shell PATH // This is needed because Electron apps don't inherit the user's shell PATH
const pathSeparator = process.platform === "win32" ? ";" : ":";
const additionalPaths: string[] = [];
if (process.platform === "win32") {
// Windows paths
if (process.env.LOCALAPPDATA) {
additionalPaths.push(`${process.env.LOCALAPPDATA}\\Programs\\Git\\cmd`);
}
if (process.env.PROGRAMFILES) {
additionalPaths.push(`${process.env.PROGRAMFILES}\\Git\\cmd`);
}
if (process.env["ProgramFiles(x86)"]) {
additionalPaths.push(`${process.env["ProgramFiles(x86)"]}\\Git\\cmd`);
}
} else {
// Unix/Mac paths
additionalPaths.push(
"/opt/homebrew/bin", // Homebrew on Apple Silicon
"/usr/local/bin", // Homebrew on Intel Mac, common Linux location
"/home/linuxbrew/.linuxbrew/bin", // Linuxbrew
`${process.env.HOME}/.local/bin`, // pipx, other user installs
);
}
const extendedPath = [ const extendedPath = [
process.env.PATH, process.env.PATH,
"/opt/homebrew/bin", // Homebrew on Apple Silicon ...additionalPaths.filter(Boolean),
"/usr/local/bin", // Homebrew on Intel Mac, common Linux location ].filter(Boolean).join(pathSeparator);
"/home/linuxbrew/.linuxbrew/bin", // Linuxbrew
`${process.env.HOME}/.local/bin`, // pipx, other user installs
].filter(Boolean).join(":");
const execEnv = { const execEnv = {
...process.env, ...process.env,
@@ -122,9 +143,12 @@ export function createCreatePRHandler() {
let browserUrl: string | null = null; let browserUrl: string | null = null;
let ghCliAvailable = false; let ghCliAvailable = false;
// Check if gh CLI is available // Check if gh CLI is available (cross-platform)
try { try {
await execAsync("command -v gh", { env: execEnv }); const checkCommand = process.platform === "win32"
? "where gh"
: "command -v gh";
await execAsync(checkCommand, { env: execEnv });
ghCliAvailable = true; ghCliAvailable = true;
} catch { } catch {
ghCliAvailable = false; ghCliAvailable = false;
@@ -141,9 +165,22 @@ export function createCreatePRHandler() {
}); });
// Parse remotes to detect fork workflow and get repo URL // Parse remotes to detect fork workflow and get repo URL
const lines = remotes.split("\n"); const lines = remotes.split(/\r?\n/); // Handle both Unix and Windows line endings
for (const line of lines) { for (const line of lines) {
const match = line.match(/^(\w+)\s+.*[:/]([^/]+)\/([^/\s]+?)(?:\.git)?\s+\(fetch\)/); // Try multiple patterns to match different remote URL formats
// Pattern 1: git@github.com:owner/repo.git (fetch)
// Pattern 2: https://github.com/owner/repo.git (fetch)
// Pattern 3: https://github.com/owner/repo (fetch)
let match = line.match(/^(\w+)\s+.*[:/]([^/]+)\/([^/\s]+?)(?:\.git)?\s+\(fetch\)/);
if (!match) {
// Try SSH format: git@github.com:owner/repo.git
match = line.match(/^(\w+)\s+git@[^:]+:([^/]+)\/([^\s]+?)(?:\.git)?\s+\(fetch\)/);
}
if (!match) {
// Try HTTPS format: https://github.com/owner/repo.git
match = line.match(/^(\w+)\s+https?:\/\/[^/]+\/([^/]+)\/([^\s]+?)(?:\.git)?\s+\(fetch\)/);
}
if (match) { if (match) {
const [, remoteName, owner, repo] = match; const [, remoteName, owner, repo] = match;
if (remoteName === "upstream") { if (remoteName === "upstream") {
@@ -157,8 +194,30 @@ export function createCreatePRHandler() {
} }
} }
} }
} catch { } catch (error) {
// Couldn't parse remotes // Couldn't parse remotes - will try fallback
}
// Fallback: Try to get repo URL from git config if remote parsing failed
if (!repoUrl) {
try {
const { stdout: originUrl } = await execAsync("git config --get remote.origin.url", {
cwd: worktreePath,
env: execEnv,
});
const url = originUrl.trim();
// Parse URL to extract owner/repo
// Handle both SSH (git@github.com:owner/repo.git) and HTTPS (https://github.com/owner/repo.git)
let match = url.match(/[:/]([^/]+)\/([^/\s]+?)(?:\.git)?$/);
if (match) {
const [, owner, repo] = match;
originOwner = owner;
repoUrl = `https://github.com/${owner}/${repo}`;
}
} catch (error) {
// Failed to get repo URL from config
}
} }
// Construct browser URL for PR creation // Construct browser URL for PR creation
@@ -192,7 +251,6 @@ export function createCreatePRHandler() {
prCmd += ` --title "${title.replace(/"/g, '\\"')}" --body "${body.replace(/"/g, '\\"')}" ${draftFlag}`; prCmd += ` --title "${title.replace(/"/g, '\\"')}" --body "${body.replace(/"/g, '\\"')}" ${draftFlag}`;
prCmd = prCmd.trim(); prCmd = prCmd.trim();
console.log("[CreatePR] Running:", prCmd);
const { stdout: prOutput } = await execAsync(prCmd, { const { stdout: prOutput } = await execAsync(prCmd, {
cwd: worktreePath, cwd: worktreePath,
env: execEnv, env: execEnv,
@@ -202,11 +260,9 @@ export function createCreatePRHandler() {
// gh CLI failed // gh CLI failed
const err = ghError as { stderr?: string; message?: string }; const err = ghError as { stderr?: string; message?: string };
prError = err.stderr || err.message || "PR creation failed"; prError = err.stderr || err.message || "PR creation failed";
console.warn("[CreatePR] gh CLI error:", prError);
} }
} else { } else {
prError = "gh_cli_not_available"; prError = "gh_cli_not_available";
console.log("[CreatePR] gh CLI not available, returning browser URL");
} }
// Return result with browser fallback URL // Return result with browser fallback URL