import { List, ActionPanel, Action, Form, showToast, Toast, Icon, useNavigation, popToRoot, Clipboard, } from "@raycast/api"; import React, { useState, useEffect } from "react"; import { parseSSHConfig } from "./utils/sshConfig"; import { executeRemoteLs } from "./utils/ssh"; import { validateRemotePath, validateHostConfig } from "./utils/validation"; import { SSHHostConfig, RemoteFile } from "./types/server"; /** * Main browse 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 path to browse 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() || "~"; // Validate remote path 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; } // 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; } // Navigate to file list after successful validation push( , ); } return (
{ const remotePathValue = remotePath.trim() || "~"; const remoteValidation = validateRemotePath(remotePathValue); if (!remoteValidation.valid) { 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) { await showToast({ style: Toast.Style.Failure, title: "Invalid Host Configuration", message: hostValidation.error || "The host configuration is incomplete or invalid", }); return; } push( , ); }} /> } > { setRemotePath(value); setRemotePathError(undefined); }} error={remotePathError} info="Enter the directory path on the remote server to browse" /> {hostConfig.user && ( )} {hostConfig.port && ( )} ); } /** * Remote file list loader component * Loads files from remote server and displays them */ function RemoteFileListLoader({ hostConfig, remotePath, }: { hostConfig: SSHHostConfig; remotePath: string; }) { const [files, setFiles] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { loadRemoteFiles(); }, [remotePath]); async function loadRemoteFiles() { setIsLoading(true); setError(null); console.log("Loading remote files:", { host: hostConfig.host, remotePath, }); try { const remoteFiles = await executeRemoteLs(hostConfig, remotePath); setFiles(remoteFiles); if (remoteFiles.length === 0) { await showToast({ style: Toast.Style.Success, title: "Directory is empty", }); } } catch (err) { const errorMessage = err instanceof Error ? err.message : "Unknown error occurred"; console.error("Browse error:", err); setError(errorMessage); await showToast({ style: Toast.Style.Failure, title: "Failed to List Files", message: errorMessage, }); } finally { setIsLoading(false); } } if (error) { return ( } /> ); } return ( ); } /** * Remote file list view * Displays files and directories from remote server */ function RemoteFileList({ hostConfig, remotePath, files, isLoading, }: { hostConfig: SSHHostConfig; remotePath: string; files: RemoteFile[]; isLoading?: boolean; }) { return ( {files.map((file, index) => ( {file.isDirectory ? ( <> } /> { const pathToCopy = remotePath.endsWith("/") ? `${remotePath}${file.name}` : `${remotePath}/${file.name}`; await Clipboard.copy(pathToCopy); await showToast({ style: Toast.Style.Success, title: "Path Copied", message: "Path copied to clipboard", }); await popToRoot(); }} /> { await Clipboard.copy(file.name); await showToast({ style: Toast.Style.Success, title: "Name Copied", message: "Name copied to clipboard", }); await popToRoot(); }} /> ) : ( <> { const pathToCopy = remotePath.endsWith("/") ? `${remotePath}${file.name}` : `${remotePath}/${file.name}`; await Clipboard.copy(pathToCopy); await showToast({ style: Toast.Style.Success, title: "Path Copied", message: "Path copied to clipboard", }); await popToRoot(); }} /> { await Clipboard.copy(file.name); await showToast({ style: Toast.Style.Success, title: "Name Copied", message: "Name copied to clipboard", }); await popToRoot(); }} /> )} } /> ))} ); }