diff --git a/src/app/_components/ScriptDetailModal.tsx b/src/app/_components/ScriptDetailModal.tsx index 4bd36dd..bceba7a 100644 --- a/src/app/_components/ScriptDetailModal.tsx +++ b/src/app/_components/ScriptDetailModal.tsx @@ -8,6 +8,7 @@ import { DiffViewer } from "./DiffViewer"; import { TextViewer } from "./TextViewer"; import { ExecutionModeModal } from "./ExecutionModeModal"; import { ConfirmationModal } from "./ConfirmationModal"; +import { ScriptVersionModal } from "./ScriptVersionModal"; import { TypeBadge, UpdateableBadge, PrivilegedBadge, NoteBadge } from "./Badge"; import { Button } from "./ui/button"; import { useRegisterModal } from './modal/ModalStackProvider'; @@ -38,6 +39,8 @@ export function ScriptDetailModal({ const [selectedDiffFile, setSelectedDiffFile] = useState(null); const [textViewerOpen, setTextViewerOpen] = useState(false); const [executionModeOpen, setExecutionModeOpen] = useState(false); + const [versionModalOpen, setVersionModalOpen] = useState(false); + const [selectedVersionType, setSelectedVersionType] = useState(null); const [isDeleting, setIsDeleting] = useState(false); const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); @@ -133,16 +136,43 @@ export function ScriptDetailModal({ const handleInstallScript = () => { if (!script) return; + + // Check if script has multiple variants (default and alpine) + const installMethods = script.install_methods || []; + const hasMultipleVariants = installMethods.filter(method => + method.type === 'default' || method.type === 'alpine' + ).length > 1; + + if (hasMultipleVariants) { + // Show version selection modal first + setVersionModalOpen(true); + } else { + // Only one variant, proceed directly to execution mode + // Use the first available method or default to 'default' type + const defaultMethod = installMethods.find(method => method.type === 'default'); + const firstMethod = installMethods[0]; + setSelectedVersionType(defaultMethod?.type || firstMethod?.type || 'default'); + setExecutionModeOpen(true); + } + }; + + const handleVersionSelect = (versionType: string) => { + setSelectedVersionType(versionType); + setVersionModalOpen(false); setExecutionModeOpen(true); }; const handleExecuteScript = (mode: "local" | "ssh", server?: any) => { if (!script || !onInstallScript) return; - // Find the script path (CT or tools) + // Find the script path based on selected version type + const versionType = selectedVersionType || 'default'; const scriptMethod = script.install_methods?.find( + (method) => method.type === versionType && method.script, + ) || script.install_methods?.find( (method) => method.script, ); + if (scriptMethod?.script) { const scriptPath = `scripts/${scriptMethod.script}`; const scriptName = script.name; @@ -804,6 +834,16 @@ export function ScriptDetailModal({ /> )} + {/* Version Selection Modal */} + {script && ( + setVersionModalOpen(false)} + onSelectVersion={handleVersionSelect} + /> + )} + {/* Execution Mode Modal */} {script && ( void; + onSelectVersion: (versionType: string) => void; + script: Script | null; +} + +export function ScriptVersionModal({ isOpen, onClose, onSelectVersion, script }: ScriptVersionModalProps) { + useRegisterModal(isOpen, { id: 'script-version-modal', allowEscape: true, onClose }); + const [selectedVersion, setSelectedVersion] = useState(null); + + if (!isOpen || !script) return null; + + // Get available install methods + const installMethods = script.install_methods || []; + const defaultMethod = installMethods.find(method => method.type === 'default'); + const alpineMethod = installMethods.find(method => method.type === 'alpine'); + + const handleConfirm = () => { + if (selectedVersion) { + onSelectVersion(selectedVersion); + onClose(); + } + }; + + const handleVersionSelect = (versionType: string) => { + setSelectedVersion(versionType); + }; + + return ( +
+
+ {/* Header */} +
+

Select Version

+ +
+ + {/* Content */} +
+
+

+ Choose a version for "{script.name}" +

+

+ Select the version you want to install. Each version has different resource requirements. +

+
+ +
+ {/* Default Version */} + {defaultMethod && ( +
handleVersionSelect('default')} + className={`cursor-pointer rounded-lg border-2 p-4 transition-all ${ + selectedVersion === 'default' + ? 'border-primary bg-primary/10' + : 'border-border bg-card hover:border-primary/50' + }`} + > +
+
+
+
+ {selectedVersion === 'default' && ( + + + + )} +
+

+ {defaultMethod.type} +

+
+
+
+ CPU: + {defaultMethod.resources.cpu} cores +
+
+ RAM: + {defaultMethod.resources.ram} MB +
+
+ HDD: + {defaultMethod.resources.hdd} GB +
+
+ OS: + + {defaultMethod.resources.os} {defaultMethod.resources.version} + +
+
+
+
+
+ )} + + {/* Alpine Version */} + {alpineMethod && ( +
handleVersionSelect('alpine')} + className={`cursor-pointer rounded-lg border-2 p-4 transition-all ${ + selectedVersion === 'alpine' + ? 'border-primary bg-primary/10' + : 'border-border bg-card hover:border-primary/50' + }`} + > +
+
+
+
+ {selectedVersion === 'alpine' && ( + + + + )} +
+

+ {alpineMethod.type} +

+
+
+
+ CPU: + {alpineMethod.resources.cpu} cores +
+
+ RAM: + {alpineMethod.resources.ram} MB +
+
+ HDD: + {alpineMethod.resources.hdd} GB +
+
+ OS: + + {alpineMethod.resources.os} {alpineMethod.resources.version} + +
+
+
+
+
+ )} +
+ + {/* Action Buttons */} +
+ + +
+
+
+
+ ); +} + diff --git a/src/server/services/scriptDownloader.js b/src/server/services/scriptDownloader.js index ad49e58..6b6f40b 100644 --- a/src/server/services/scriptDownloader.js +++ b/src/server/services/scriptDownloader.js @@ -145,6 +145,35 @@ export class ScriptDownloaderService { } } + // Download alpine install script if alpine variant exists (only for CT scripts) + const hasAlpineCtVariant = script.install_methods?.some( + method => method.type === 'alpine' && method.script?.startsWith('ct/') + ); + console.log(`[${script.slug}] Checking for alpine variant:`, { + hasAlpineCtVariant, + installMethods: script.install_methods?.map(m => ({ type: m.type, script: m.script })) + }); + + if (hasAlpineCtVariant) { + const alpineInstallScriptName = `alpine-${script.slug}-install.sh`; + try { + console.log(`[${script.slug}] Downloading alpine install script: install/${alpineInstallScriptName}`); + const alpineInstallContent = await this.downloadFileFromGitHub(`install/${alpineInstallScriptName}`); + const localAlpineInstallPath = join(this.scriptsDirectory, 'install', alpineInstallScriptName); + await writeFile(localAlpineInstallPath, alpineInstallContent, 'utf-8'); + files.push(`install/${alpineInstallScriptName}`); + console.log(`[${script.slug}] Successfully downloaded: install/${alpineInstallScriptName}`); + } catch (error) { + // Alpine install script might not exist, that's okay + console.error(`[${script.slug}] Alpine install script not found or error: install/${alpineInstallScriptName}`, error); + if (error instanceof Error) { + console.error(`[${script.slug}] Error details:`, error.message, error.stack); + } + } + } else { + console.log(`[${script.slug}] No alpine CT variant found, skipping alpine install script download`); + } + return { success: true, message: `Successfully loaded ${files.length} script(s) for ${script.name}`, @@ -286,6 +315,23 @@ export class ScriptDownloaderService { } } + // Check alpine install script if alpine variant exists (only for CT scripts) + const hasAlpineCtVariant = script.install_methods?.some( + method => method.type === 'alpine' && method.script?.startsWith('ct/') + ); + if (hasAlpineCtVariant) { + const alpineInstallScriptName = `alpine-${script.slug}-install.sh`; + const alpineInstallPath = join(this.scriptsDirectory, 'install', alpineInstallScriptName); + + try { + await access(alpineInstallPath); + files.push(`install/${alpineInstallScriptName}`); + installExists = true; // Mark as exists if alpine install script exists + } catch { + // File doesn't exist + } + } + return { ctExists, installExists, files }; } catch (error) { console.error('Error checking script existence:', error); @@ -427,6 +473,35 @@ export class ScriptDownloaderService { ); } + // Compare alpine install script if alpine variant exists (only for CT scripts) + const hasAlpineCtVariant = script.install_methods?.some( + method => method.type === 'alpine' && method.script?.startsWith('ct/') + ); + if (hasAlpineCtVariant) { + const alpineInstallScriptName = `alpine-${script.slug}-install.sh`; + const alpineInstallScriptPath = `install/${alpineInstallScriptName}`; + const localAlpineInstallPath = join(this.scriptsDirectory, alpineInstallScriptPath); + + // Check if alpine install script exists locally + try { + await access(localAlpineInstallPath); + comparisonPromises.push( + this.compareSingleFile(alpineInstallScriptPath, alpineInstallScriptPath) + .then(result => { + if (result.hasDifferences) { + hasDifferences = true; + differences.push(result.filePath); + } + }) + .catch(() => { + // Don't add to differences if there's an error reading files + }) + ); + } catch { + // Alpine install script doesn't exist locally, skip comparison + } + } + // Wait for all comparisons to complete await Promise.all(comparisonPromises);