import { useState, useEffect, useCallback } from "react"; import { X, Plus, Play, Trash2, Terminal, Check, AlertCircle } from "lucide-react"; import { fetchScripts, addScript, removeScript, } from "../api"; import type { ToastType } from "../hooks/useToast"; interface ScriptsModalProps { isOpen: boolean; onClose: () => void; addToast: (message: string, type?: ToastType) => void; onRunScript?: (name: string, command: string) => void; } interface ScriptFormData { name: string; command: string; } const EMPTY_FORM: ScriptFormData = { name: "", command: "", }; export function ScriptsModal({ isOpen, onClose, addToast, onRunScript }: ScriptsModalProps) { const [scripts, setScripts] = useState>({}); const [loading, setLoading] = useState(true); const [isCreating, setIsCreating] = useState(false); const [editingName, setEditingName] = useState(null); const [form, setForm] = useState(EMPTY_FORM); const [saving, setSaving] = useState(false); const [deleteConfirmName, setDeleteConfirmName] = useState(null); const [validationError, setValidationError] = useState(null); const loadScripts = useCallback(async () => { if (!isOpen) return; try { setLoading(true); const data = await fetchScripts(); setScripts(data); } catch (err: any) { addToast(err.message || "Failed to load scripts", "error"); } finally { setLoading(false); } }, [isOpen, addToast]); useEffect(() => { if (isOpen) { loadScripts(); } }, [isOpen, loadScripts]); const validateScriptName = (name: string): string | null => { if (!name.trim()) { return "Script name is required"; } if (!/^[a-zA-Z0-9_-]+$/.test(name.trim())) { return "Name must be alphanumeric with hyphens and underscores only (no spaces)"; } // Check for reserved names const reservedNames = ["run", "list", "add", "remove", "delete", "help"]; if (reservedNames.includes(name.trim().toLowerCase())) { return `Script name '${name.trim()}' is reserved`; } return null; }; const handleCreate = useCallback(() => { setIsCreating(true); setEditingName(null); setForm(EMPTY_FORM); setValidationError(null); }, []); const handleEdit = useCallback((name: string, command: string) => { setIsCreating(false); setEditingName(name); setForm({ name, command }); setValidationError(null); }, []); const handleCancel = useCallback(() => { setIsCreating(false); setEditingName(null); setForm(EMPTY_FORM); setValidationError(null); }, []); const handleSave = useCallback(async () => { const trimmedName = form.name.trim(); const trimmedCommand = form.command.trim(); // Validate name const nameError = validateScriptName(trimmedName); if (nameError) { setValidationError(nameError); return; } if (!trimmedCommand) { setValidationError("Command is required"); return; } setSaving(true); setValidationError(null); try { await addScript(trimmedName, trimmedCommand); addToast( isCreating ? `Script '${trimmedName}' created` : `Script '${trimmedName}' updated`, "success" ); setIsCreating(false); setEditingName(null); setForm(EMPTY_FORM); await loadScripts(); } catch (err: any) { const message = err.message || "Failed to save script"; setValidationError(message); addToast(message, "error"); } finally { setSaving(false); } }, [form, isCreating, addToast, loadScripts]); const handleDelete = useCallback( async (name: string) => { try { await removeScript(name); addToast(`Script '${name}' deleted`, "success"); setDeleteConfirmName(null); if (editingName === name) { setEditingName(null); setForm(EMPTY_FORM); } await loadScripts(); } catch (err: any) { addToast(err.message || "Failed to delete script", "error"); } }, [editingName, addToast, loadScripts] ); const handleRunScript = useCallback( (name: string, command: string) => { if (onRunScript) { onRunScript(name, command); } else { addToast("Terminal not available", "error"); } }, [onRunScript, addToast] ); if (!isOpen) return null; const isEditing = isCreating || editingName !== null; const scriptEntries = Object.entries(scripts).sort(([a], [b]) => a.localeCompare(b)); return (
e.stopPropagation()} role="dialog" aria-label="Scripts Manager" > {/* Header */}

Scripts

{loading ? (
Loading...
) : ( <> {/* Script List */} {!isEditing && ( <> {scriptEntries.length === 0 && (
No scripts defined yet. Add your first script to run quick commands.
)} {scriptEntries.length > 0 && (
{scriptEntries.map(([name, command]) => (
{name}
{command.length > 60 ? `${command.slice(0, 60)}...` : command}
{deleteConfirmName === name ? (
) : ( )}
))}
)} )} {/* Edit / Create form */} {isEditing && (

{isCreating ? "New Script" : "Edit Script"}

{/* Validation error */} {validationError && (
{validationError}
)}
{/* Name */}
setForm((prev) => ({ ...prev, name: e.target.value })) } placeholder="e.g. build, test, deploy" disabled={!isCreating} style={{ width: "100%", padding: "8px 12px", borderRadius: "6px", border: "1px solid var(--border-primary)", background: isCreating ? "var(--bg-primary)" : "var(--bg-tertiary)", color: "var(--text-primary)", fontSize: "13px", }} data-testid="script-name-input" />
Alphanumeric with hyphens and underscores only. No spaces.
{/* Command */}