fix: address PR #173 security and code quality feedback

Security fixes:
- Enhanced branch name sanitization for cross-platform filesystem safety
  (handles Windows-invalid chars, reserved names, path length limits)
- Added branch name validation in pr-info.ts to prevent command injection
- Sanitized prUrl in kanban-card to only allow http/https URLs

Code quality improvements:
- Fixed placeholder issue where {owner}/{repo} was passed literally to gh api
- Replaced async forEach with Promise.all for proper async handling
- Display PR number extracted from URL in kanban cards

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Cody Seibert
2025-12-19 20:39:38 -05:00
parent 6c25680115
commit ec7c2892c2
7 changed files with 153 additions and 132 deletions

View File

@@ -699,24 +699,30 @@ export const KanbanCard = memo(function KanbanCard({
)}
{/* PR URL Display */}
{feature.prUrl && (
<div className="mb-2">
<a
href={feature.prUrl}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
className="inline-flex items-center gap-1.5 text-[11px] text-purple-500 hover:text-purple-400 transition-colors"
title={feature.prUrl}
data-testid={`pr-url-${feature.id}`}
>
<GitPullRequest className="w-3 h-3 shrink-0" />
<span className="truncate max-w-[150px]">Pull Request</span>
<ExternalLink className="w-2.5 h-2.5 shrink-0" />
</a>
</div>
)}
{typeof feature.prUrl === "string" &&
/^https?:\/\//i.test(feature.prUrl) && (() => {
const prNumber = feature.prUrl.split('/').pop();
return (
<div className="mb-2">
<a
href={feature.prUrl}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
className="inline-flex items-center gap-1.5 text-[11px] text-purple-500 hover:text-purple-400 transition-colors"
title={feature.prUrl}
data-testid={`pr-url-${feature.id}`}
>
<GitPullRequest className="w-3 h-3 shrink-0" />
<span className="truncate max-w-[150px]">
{prNumber ? `Pull Request #${prNumber}` : 'Pull Request'}
</span>
<ExternalLink className="w-2.5 h-2.5 shrink-0" />
</a>
</div>
);
})()}
{/* Steps Preview */}
{showSteps && feature.steps && feature.steps.length > 0 && (

View File

@@ -29,12 +29,8 @@ interface CreatePRDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
worktree: WorktreeInfo | null;
<<<<<<< Updated upstream
onCreated: (prUrl?: string) => void;
=======
projectPath: string | null;
onCreated: () => void;
>>>>>>> Stashed changes
onCreated: (prUrl?: string) => void;
}
export function CreatePRDialog({

View File

@@ -1,7 +1,6 @@
import { Button } from "@/components/ui/button";
<<<<<<< Updated upstream
import { RefreshCw, Globe, Loader2, CircleDot } from "lucide-react";
import { RefreshCw, Globe, Loader2, CircleDot, GitPullRequest } from "lucide-react";
import { cn } from "@/lib/utils";
import {
Tooltip,
@@ -9,12 +8,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import type { WorktreeInfo, BranchInfo, DevServerInfo } from "../types";
=======
import { RefreshCw, Globe, Loader2, GitPullRequest } from "lucide-react";
import { cn } from "@/lib/utils";
import type { WorktreeInfo, BranchInfo, DevServerInfo, PRInfo } from "../types";
>>>>>>> Stashed changes
import { BranchSwitchDropdown } from "./branch-switch-dropdown";
import { WorktreeActionsDropdown } from "./worktree-actions-dropdown";
@@ -95,7 +89,6 @@ export function WorktreeTab({
onStopDevServer,
onOpenDevServerUrl,
}: WorktreeTabProps) {
<<<<<<< Updated upstream
// Determine border color based on state:
// - Running features: cyan border (high visibility, indicates active work)
// - Uncommitted changes: amber border (warning state, needs attention)
@@ -111,7 +104,7 @@ export function WorktreeTab({
};
const borderClasses = getBorderClasses();
=======
let prBadge: JSX.Element | null = null;
if (worktree.pr) {
const prState = worktree.pr.state?.toLowerCase() ?? "open";
@@ -197,7 +190,6 @@ export function WorktreeTab({
</button>
);
}
>>>>>>> Stashed changes
return (
<div className={cn("flex items-center rounded-md", borderClasses)}>
@@ -225,7 +217,6 @@ export function WorktreeTab({
{cardCount}
</span>
)}
<<<<<<< Updated upstream
{hasChanges && (
<TooltipProvider>
<Tooltip>
@@ -246,9 +237,7 @@ export function WorktreeTab({
</Tooltip>
</TooltipProvider>
)}
=======
{prBadge}
>>>>>>> Stashed changes
</Button>
<BranchSwitchDropdown
worktree={worktree}
@@ -292,7 +281,6 @@ export function WorktreeTab({
{cardCount}
</span>
)}
<<<<<<< Updated upstream
{hasChanges && (
<TooltipProvider>
<Tooltip>
@@ -313,9 +301,7 @@ export function WorktreeTab({
</Tooltip>
</TooltipProvider>
)}
=======
{prBadge}
>>>>>>> Stashed changes
</Button>
)}