import { List, ActionPanel, Action, Form, showToast, Toast, popToRoot, useNavigation, } from "@raycast/api"; import React, { useState, useEffect } from "react"; import { parseSSHConfig } from "./utils/sshConfig"; import { executeRsync } from "./utils/rsync"; import { validateRemotePath, validateHostConfig } from "./utils/validation"; import { SSHHostConfig, TransferDirection, TransferOptions, RsyncOptions, } from "./types/server"; import { getRsyncPreferences } from "./utils/preferences"; /** * Main download command component * Displays list of SSH hosts from config file */ export default function Command() { const [hosts, setHosts] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { loadHosts(); }, []); async function loadHosts() { try { const parsedHosts = parseSSHConfig(); if (parsedHosts.length === 0) { const errorMsg = "No host entries found in SSH config file"; setError(errorMsg); console.warn("SSH config parsed but no hosts found"); } else { setHosts(parsedHosts); console.log(`Loaded ${parsedHosts.length} SSH host(s)`); } } catch (err) { const errorMessage = err instanceof Error ? err.message : "Failed to parse SSH config"; console.error("Error loading SSH hosts:", err); setError(errorMessage); await showToast({ style: Toast.Style.Failure, title: "Error Loading SSH Config", message: errorMessage, }); } finally { setIsLoading(false); } } if (error) { return ( ); } return ( {hosts.map((host: SSHHostConfig) => ( } /> } /> ))} ); } /** * Remote path input form * Allows user to specify source path on remote server */ function RemotePathForm({ hostConfig }: { hostConfig: SSHHostConfig }) { const [remotePath, setRemotePath] = useState(""); const [remotePathError, setRemotePathError] = useState(); const { push } = useNavigation(); async function handleSubmit(values: { remotePath: string }) { const remotePathValue = values.remotePath.trim(); const remoteValidation = validateRemotePath(remotePathValue); if (!remoteValidation.valid) { console.error("Remote path validation failed:", remoteValidation.error); setRemotePathError(remoteValidation.error); await showToast({ style: Toast.Style.Failure, title: "Invalid Remote Path", message: remoteValidation.error || "The remote path format is invalid", }); return; } const hostValidation = validateHostConfig(hostConfig); if (!hostValidation.valid) { console.error("Host config validation failed:", hostValidation.error); await showToast({ style: Toast.Style.Failure, title: "Invalid Host Configuration", message: hostValidation.error || "The host configuration is incomplete or invalid", }); return; } push( , ); } return (
} > { setRemotePath(value); setRemotePathError(undefined); }} error={remotePathError} info="Enter the path to the file or directory on the remote server" /> {hostConfig.user && ( )} {hostConfig.port && ( )} {hostConfig.identityFile && ( )} ); } /** * Local destination path form * Allows user to specify destination directory on local system */ function LocalPathForm({ hostConfig, remotePath, }: { hostConfig: SSHHostConfig; remotePath: string; }) { const [localPath, setLocalPath] = useState(""); const [localPathError, setLocalPathError] = useState(); // Initialize rsync options with global preferences const defaultRsyncOptions = getRsyncPreferences(); const [humanReadable, setHumanReadable] = useState( defaultRsyncOptions.humanReadable ?? false, ); const [progress, setProgress] = useState( defaultRsyncOptions.progress ?? false, ); const [deleteExtra, setDeleteExtra] = useState( defaultRsyncOptions.delete ?? false, ); async function handleSubmit(values: { localPath: string; humanReadable: boolean; progress: boolean; deleteExtra: boolean; }) { const localPathValue = values.localPath.trim(); if (!localPathValue) { console.error("Local path is empty"); setLocalPathError("Local path is required"); await showToast({ style: Toast.Style.Failure, title: "Invalid Local Path", message: "Please enter a destination path for the downloaded files", }); return; } // Validate remote path const remoteValidation = validateRemotePath(remotePath); if (!remoteValidation.valid) { console.error("Remote path validation failed:", remoteValidation.error); await showToast({ style: Toast.Style.Failure, title: "Invalid Remote Path", message: remoteValidation.error || "The remote path format is invalid", }); return; } // Validate host config const hostValidation = validateHostConfig(hostConfig); if (!hostValidation.valid) { console.error("Host config validation failed:", hostValidation.error); await showToast({ style: Toast.Style.Failure, title: "Invalid Host Configuration", message: hostValidation.error || "The host configuration is incomplete or invalid", }); return; } // Execute transfer using form values await executeTransfer(hostConfig, remotePath, localPathValue, { humanReadable: values.humanReadable, progress: values.progress, delete: values.deleteExtra, }); } async function executeTransfer( hostConfig: SSHHostConfig, remotePath: string, localPath: string, rsyncOptions: RsyncOptions, ) { // Show initial progress toast await showToast({ style: Toast.Style.Animated, title: "Transferring files...", message: `Downloading from ${hostConfig.host}`, }); console.log("Starting download:", { host: hostConfig.host, remotePath, localPath, }); try { const options: TransferOptions = { hostConfig, localPath, remotePath, direction: TransferDirection.DOWNLOAD, rsyncOptions, }; // Progress callback to update toast in real-time const progressCallback = async (progressMessage: string) => { await showToast({ style: Toast.Style.Animated, title: "Transferring files...", message: progressMessage, }); }; const result = await executeRsync(options, progressCallback); if (result.success) { console.log("Download completed successfully"); // Show formatted rsync output message (includes file sizes and progress if flags enabled) await showToast({ style: Toast.Style.Success, title: "Download Successful", message: result.message, }); // Log full output for debugging if (result.stdout) { console.log("Rsync output:", result.stdout); } // Close the extension after successful download await popToRoot(); } else { console.error("Download failed:", result.message); await showToast({ style: Toast.Style.Failure, title: "Download Failed", message: result.message, }); } } catch (err) { const errorMessage = err instanceof Error ? err.message : "Unknown error occurred"; console.error("Download error:", err); await showToast({ style: Toast.Style.Failure, title: "Download Failed", message: errorMessage, }); } } return (
} > { setLocalPath(value); setLocalPathError(undefined); }} error={localPathError} info="Enter the destination directory on your local system" /> ); }