mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
fix: address CodeRabbitAI security and UX review comments
Security improvements in open-in-editor.ts: - Use execFile with argument arrays instead of shell interpolation in commandExists() to prevent command injection - Replace shell `test -d` commands with Node.js fs/promises access() in findMacApp() for safer file system checks - Add cache TTL (5 minutes) for editor detection to prevent stale data UX improvements in worktree-actions-dropdown.tsx: - Add error handling for clipboard copy operation - Show success toast when path is copied - Show error toast if clipboard write fails Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,25 +4,34 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import { exec, execFile } from 'child_process';
|
import { execFile } from 'child_process';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import { homedir } from 'os';
|
import { homedir } from 'os';
|
||||||
import { isAbsolute } from 'path';
|
import { isAbsolute, join } from 'path';
|
||||||
|
import { access } from 'fs/promises';
|
||||||
import type { EditorInfo } from '@automaker/types';
|
import type { EditorInfo } from '@automaker/types';
|
||||||
import { getErrorMessage, logError } from '../common.js';
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
|
// Cache with TTL for editor detection
|
||||||
let cachedEditor: EditorInfo | null = null;
|
let cachedEditor: EditorInfo | null = null;
|
||||||
let cachedEditors: EditorInfo[] | null = null;
|
let cachedEditors: EditorInfo[] | null = null;
|
||||||
|
let cacheTimestamp: number = 0;
|
||||||
|
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
||||||
|
|
||||||
|
function isCacheValid(): boolean {
|
||||||
|
return Date.now() - cacheTimestamp < CACHE_TTL_MS;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a CLI command exists in PATH
|
* Check if a CLI command exists in PATH
|
||||||
|
* Uses execFile to avoid shell injection
|
||||||
*/
|
*/
|
||||||
async function commandExists(cmd: string): Promise<boolean> {
|
async function commandExists(cmd: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
await execAsync(process.platform === 'win32' ? `where ${cmd}` : `which ${cmd}`);
|
const whichCmd = process.platform === 'win32' ? 'where' : 'which';
|
||||||
|
await execFileAsync(whichCmd, [cmd]);
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
@@ -31,24 +40,25 @@ async function commandExists(cmd: string): Promise<boolean> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a macOS app bundle exists and return the path if found
|
* Check if a macOS app bundle exists and return the path if found
|
||||||
* Checks both /Applications and ~/Applications
|
* Uses Node fs methods instead of shell commands for safety
|
||||||
*/
|
*/
|
||||||
async function findMacApp(appName: string): Promise<string | null> {
|
async function findMacApp(appName: string): Promise<string | null> {
|
||||||
if (process.platform !== 'darwin') return null;
|
if (process.platform !== 'darwin') return null;
|
||||||
|
|
||||||
// Check /Applications first
|
// Check /Applications first
|
||||||
|
const systemAppPath = join('/Applications', `${appName}.app`);
|
||||||
try {
|
try {
|
||||||
await execAsync(`test -d "/Applications/${appName}.app"`);
|
await access(systemAppPath);
|
||||||
return `/Applications/${appName}.app`;
|
return systemAppPath;
|
||||||
} catch {
|
} catch {
|
||||||
// Not in /Applications
|
// Not in /Applications
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check ~/Applications (used by JetBrains Toolbox and others)
|
// Check ~/Applications (used by JetBrains Toolbox and others)
|
||||||
|
const userAppPath = join(homedir(), 'Applications', `${appName}.app`);
|
||||||
try {
|
try {
|
||||||
const homeDir = homedir();
|
await access(userAppPath);
|
||||||
await execAsync(`test -d "${homeDir}/Applications/${appName}.app"`);
|
return userAppPath;
|
||||||
return `${homeDir}/Applications/${appName}.app`;
|
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -81,8 +91,8 @@ async function findEditor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function detectAllEditors(): Promise<EditorInfo[]> {
|
async function detectAllEditors(): Promise<EditorInfo[]> {
|
||||||
// Return cached result if available
|
// Return cached result if still valid
|
||||||
if (cachedEditors) {
|
if (cachedEditors && isCacheValid()) {
|
||||||
return cachedEditors;
|
return cachedEditors;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +131,7 @@ async function detectAllEditors(): Promise<EditorInfo[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cachedEditors = editors;
|
cachedEditors = editors;
|
||||||
|
cacheTimestamp = Date.now();
|
||||||
return editors;
|
return editors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
AlertCircle,
|
AlertCircle,
|
||||||
Copy,
|
Copy,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { WorktreeInfo, DevServerInfo, PRInfo, GitRepoStatus } from '../types';
|
import type { WorktreeInfo, DevServerInfo, PRInfo, GitRepoStatus } from '../types';
|
||||||
import { TooltipWrapper } from './tooltip-wrapper';
|
import { TooltipWrapper } from './tooltip-wrapper';
|
||||||
@@ -249,7 +250,14 @@ export function WorktreeActionsDropdown({
|
|||||||
})}
|
})}
|
||||||
{otherEditors.length > 0 && <DropdownMenuSeparator />}
|
{otherEditors.length > 0 && <DropdownMenuSeparator />}
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => navigator.clipboard.writeText(worktree.path)}
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(worktree.path);
|
||||||
|
toast.success('Path copied to clipboard');
|
||||||
|
} catch {
|
||||||
|
toast.error('Failed to copy path to clipboard');
|
||||||
|
}
|
||||||
|
}}
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
>
|
>
|
||||||
<Copy className="w-3.5 h-3.5 mr-2" />
|
<Copy className="w-3.5 h-3.5 mr-2" />
|
||||||
|
|||||||
Reference in New Issue
Block a user