diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..214c29d1 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +registry=https://registry.npmjs.org/ diff --git a/App.tsx b/App.tsx index 97d713db..b76d6903 100755 --- a/App.tsx +++ b/App.tsx @@ -3,6 +3,7 @@ import { activeTabStore, useIsVaultActive } from './application/state/activeTabS import { useSessionState } from './application/state/useSessionState'; import { useSettingsState } from './application/state/useSettingsState'; import { useVaultState } from './application/state/useVaultState'; +import { useWindowControls } from './application/state/useWindowControls'; import { matchesKeyBinding } from './domain/models'; import ProtocolSelectDialog from './components/ProtocolSelectDialog'; import { QuickSwitcher } from './components/QuickSwitcher'; @@ -405,14 +406,15 @@ function App() { setIsQuickSwitcherOpen(true); }, []); + const { openSettingsWindow } = useWindowControls(); + const handleOpenSettings = useCallback(() => { // Try to open in a separate window, fallback to modal dialog - if (window.netcatty?.openSettingsWindow) { - window.netcatty.openSettingsWindow(); - } else { - setIsSettingsOpen(true); - } - }, []); + void (async () => { + const opened = await openSettingsWindow(); + if (!opened) setIsSettingsOpen(true); + })(); + }, [openSettingsWindow]); const handleEndSessionDrag = useCallback(() => { setDraggingSessionId(null); diff --git a/application/state/useKeychainBackend.ts b/application/state/useKeychainBackend.ts new file mode 100644 index 00000000..1308909f --- /dev/null +++ b/application/state/useKeychainBackend.ts @@ -0,0 +1,26 @@ +import { useCallback } from "react"; +import { netcattyBridge } from "../../infrastructure/services/netcattyBridge"; + +export const useKeychainBackend = () => { + const generateKeyPair = useCallback(async (options: { type: "RSA" | "ECDSA" | "ED25519"; bits?: number; comment?: string }) => { + const bridge = netcattyBridge.get(); + return bridge?.generateKeyPair?.(options); + }, []); + + const execCommand = useCallback(async (options: { + hostname: string; + username: string; + port?: number; + password?: string; + privateKey?: string; + command: string; + timeout?: number; + }) => { + const bridge = netcattyBridge.get(); + if (!bridge?.execCommand) throw new Error("execCommand unavailable"); + return bridge.execCommand(options); + }, []); + + return { generateKeyPair, execCommand }; +}; + diff --git a/application/state/useKnownHostsBackend.ts b/application/state/useKnownHostsBackend.ts new file mode 100644 index 00000000..4616f3db --- /dev/null +++ b/application/state/useKnownHostsBackend.ts @@ -0,0 +1,12 @@ +import { useCallback } from "react"; +import { netcattyBridge } from "../../infrastructure/services/netcattyBridge"; + +export const useKnownHostsBackend = () => { + const readKnownHosts = useCallback(async () => { + const bridge = netcattyBridge.get(); + return bridge?.readKnownHosts?.(); + }, []); + + return { readKnownHosts }; +}; + diff --git a/application/state/usePortForwardingState.ts b/application/state/usePortForwardingState.ts index 9803abb7..63fb48a7 100644 --- a/application/state/usePortForwardingState.ts +++ b/application/state/usePortForwardingState.ts @@ -1,10 +1,15 @@ -import { useCallback, useEffect, useState } from "react"; -import { PortForwardingRule } from "../../domain/models"; -import { STORAGE_KEY_PORT_FORWARDING } from "../../infrastructure/config/storageKeys"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Host, PortForwardingRule } from "../../domain/models"; +import { + STORAGE_KEY_PF_PREFER_FORM_MODE, + STORAGE_KEY_PORT_FORWARDING, +} from "../../infrastructure/config/storageKeys"; import { localStorageAdapter } from "../../infrastructure/persistence/localStorageAdapter"; import { getActiveConnection, getActiveRuleIds, + startPortForward, + stopPortForward, } from "../../infrastructure/services/portForwardingService"; export type ViewMode = "grid" | "list"; @@ -16,11 +21,13 @@ export interface UsePortForwardingStateResult { viewMode: ViewMode; sortMode: SortMode; search: string; + preferFormMode: boolean; setSelectedRuleId: (id: string | null) => void; setViewMode: (mode: ViewMode) => void; setSortMode: (mode: SortMode) => void; setSearch: (query: string) => void; + setPreferFormMode: (prefer: boolean) => void; addRule: ( rule: Omit, @@ -35,6 +42,17 @@ export interface UsePortForwardingStateResult { error?: string, ) => void; + startTunnel: ( + rule: PortForwardingRule, + host: Host, + keys: { id: string; privateKey: string }[], + onStatusChange?: (status: PortForwardingRule["status"], error?: string) => void, + ) => Promise<{ success: boolean; error?: string }>; + stopTunnel: ( + ruleId: string, + onStatusChange?: (status: PortForwardingRule["status"]) => void, + ) => Promise<{ success: boolean; error?: string }>; + filteredRules: PortForwardingRule[]; selectedRule: PortForwardingRule | undefined; } @@ -45,6 +63,14 @@ export const usePortForwardingState = (): UsePortForwardingStateResult => { const [viewMode, setViewMode] = useState("grid"); const [sortMode, setSortMode] = useState("newest"); const [search, setSearch] = useState(""); + const [preferFormMode, setPreferFormModeState] = useState(() => { + return localStorageAdapter.readBoolean(STORAGE_KEY_PF_PREFER_FORM_MODE) ?? false; + }); + + const setPreferFormMode = useCallback((prefer: boolean) => { + setPreferFormModeState(prefer); + localStorageAdapter.writeBoolean(STORAGE_KEY_PF_PREFER_FORM_MODE, prefer); + }, []); // Load rules from storage on mount useEffect(() => { @@ -163,8 +189,39 @@ export const usePortForwardingState = (): UsePortForwardingStateResult => { [persistRules], ); + const startTunnel = useCallback( + async ( + rule: PortForwardingRule, + host: Host, + keys: { id: string; privateKey: string }[], + onStatusChange?: ( + status: PortForwardingRule["status"], + error?: string, + ) => void, + ) => { + return startPortForward(rule, host, keys, (status, error) => { + setRuleStatus(rule.id, status, error); + onStatusChange?.(status, error ?? undefined); + }); + }, + [setRuleStatus], + ); + + const stopTunnel = useCallback( + async ( + ruleId: string, + onStatusChange?: (status: PortForwardingRule["status"]) => void, + ) => { + return stopPortForward(ruleId, (status) => { + setRuleStatus(ruleId, status); + onStatusChange?.(status); + }); + }, + [setRuleStatus], + ); + // Filter and sort rules - const filteredRules = useCallback(() => { + const filteredRules = useMemo(() => { let result = [...rules]; // Filter by search @@ -197,7 +254,7 @@ export const usePortForwardingState = (): UsePortForwardingStateResult => { } return result; - }, [rules, search, sortMode])(); + }, [rules, search, sortMode]); const selectedRule = rules.find((r) => r.id === selectedRuleId); @@ -207,11 +264,13 @@ export const usePortForwardingState = (): UsePortForwardingStateResult => { viewMode, sortMode, search, + preferFormMode, setSelectedRuleId, setViewMode, setSortMode, setSearch, + setPreferFormMode, addRule, updateRule, @@ -219,6 +278,8 @@ export const usePortForwardingState = (): UsePortForwardingStateResult => { duplicateRule, setRuleStatus, + startTunnel, + stopTunnel, filteredRules, selectedRule, diff --git a/application/state/useSessionState.ts b/application/state/useSessionState.ts index 19458a9f..ee34f470 100644 --- a/application/state/useSessionState.ts +++ b/application/state/useSessionState.ts @@ -36,10 +36,9 @@ export const useSessionState = () => { username: 'local', status: 'connecting', }; - setSessions(prev => [...prev, newSession]); - setActiveTabId(sessionId); - // eslint-disable-next-line react-hooks/exhaustive-deps -- setActiveTabId is a stable store method reference - }, []); + setSessions(prev => [...prev, newSession]); + setActiveTabId(sessionId); + }, [setActiveTabId]); const connectToHost = useCallback((host: Host) => { const newSession: TerminalSession = { @@ -54,10 +53,9 @@ export const useSessionState = () => { port: host.port, moshEnabled: host.moshEnabled, }; - setSessions(prev => [...prev, newSession]); - setActiveTabId(newSession.id); - // eslint-disable-next-line react-hooks/exhaustive-deps -- setActiveTabId is a stable store method reference - }, []); + setSessions(prev => [...prev, newSession]); + setActiveTabId(newSession.id); + }, [setActiveTabId]); const updateSessionStatus = useCallback((sessionId: string, status: TerminalSession['status']) => { setSessions(prev => prev.map(s => s.id === sessionId ? { ...s, status } : s)); @@ -140,11 +138,10 @@ export const useSessionState = () => { } } } - - return prevSessions.filter(s => s.id !== sessionId); - }); - // eslint-disable-next-line react-hooks/exhaustive-deps -- setActiveTabId is a stable store method reference - }, [workspaces]); + + return prevSessions.filter(s => s.id !== sessionId); + }); + }, [workspaces, setActiveTabId]); const closeWorkspace = useCallback((workspaceId: string) => { setWorkspaces(prevWorkspaces => { @@ -161,10 +158,9 @@ export const useSessionState = () => { } } - return remainingWorkspaces; - }); - // eslint-disable-next-line react-hooks/exhaustive-deps -- setActiveTabId is a stable store method reference - }, []); + return remainingWorkspaces; + }); + }, [setActiveTabId]); const startWorkspaceRename = useCallback((workspaceId: string) => { setWorkspaces(prevWorkspaces => { @@ -204,7 +200,7 @@ export const useSessionState = () => { ) => { if (!hint || baseSessionId === joiningSessionId) return; - setSessions(prevSessions => { + setSessions(prevSessions => { const base = prevSessions.find(s => s.id === baseSessionId); const joining = prevSessions.find(s => s.id === joiningSessionId); if (!base || !joining || base.workspaceId || joining.workspaceId) return prevSessions; @@ -219,9 +215,8 @@ export const useSessionState = () => { } return s; }); - }); - // eslint-disable-next-line react-hooks/exhaustive-deps -- setActiveTabId is a stable store method reference - }, []); + }); + }, [setActiveTabId]); const addSessionToWorkspace = useCallback(( workspaceId: string, @@ -230,7 +225,7 @@ export const useSessionState = () => { ) => { if (!hint) return; - setSessions(prevSessions => { + setSessions(prevSessions => { const session = prevSessions.find(s => s.id === sessionId); if (!session || session.workspaceId) return prevSessions; @@ -246,9 +241,8 @@ export const useSessionState = () => { setActiveTabId(workspaceId); return prevSessions.map(s => s.id === sessionId ? { ...s, workspaceId } : s); - }); - // eslint-disable-next-line react-hooks/exhaustive-deps -- setActiveTabId is a stable store method reference - }, []); + }); + }, [setActiveTabId]); const updateSplitSizes = useCallback((workspaceId: string, splitId: string, sizes: number[]) => { setWorkspaces(prev => prev.map(ws => { @@ -263,7 +257,7 @@ export const useSessionState = () => { sessionId: string, direction: SplitDirection ) => { - setSessions(prevSessions => { + setSessions(prevSessions => { const session = prevSessions.find(s => s.id === sessionId); if (!session) return prevSessions; @@ -328,9 +322,8 @@ export const useSessionState = () => { } return s; }).concat({ ...newSession, workspaceId: newWorkspace.id }); - }); - // eslint-disable-next-line react-hooks/exhaustive-deps -- setActiveTabId is a stable store method reference - }, []); + }); + }, [setActiveTabId]); // Toggle workspace view mode between split and focus const toggleWorkspaceViewMode = useCallback((workspaceId: string) => { @@ -358,31 +351,23 @@ export const useSessionState = () => { // Move focus between panes in a workspace const moveFocusInWorkspace = useCallback((workspaceId: string, direction: FocusDirection): boolean => { - console.log('[moveFocusInWorkspace] Called with:', { workspaceId, direction }); - const workspace = workspaces.find(w => w.id === workspaceId); if (!workspace) { - console.log('[moveFocusInWorkspace] Workspace not found'); return false; } // Get current focused session, or first session if none focused const sessionIds = collectSessionIds(workspace.root); - console.log('[moveFocusInWorkspace] Session IDs:', sessionIds); const currentFocused = workspace.focusedSessionId || sessionIds[0]; if (!currentFocused) { - console.log('[moveFocusInWorkspace] No current focused session'); return false; } - console.log('[moveFocusInWorkspace] Current focused:', currentFocused); // Find the next session in the given direction const nextSessionId = getNextFocusSessionId(workspace.root, currentFocused, direction); - console.log('[moveFocusInWorkspace] Next session:', nextSessionId); if (!nextSessionId) { - console.log('[moveFocusInWorkspace] No next session found'); return false; } @@ -392,7 +377,6 @@ export const useSessionState = () => { return { ...ws, focusedSessionId: nextSessionId }; })); - console.log('[moveFocusInWorkspace] Focus updated to:', nextSessionId); return true; }, [workspaces]); @@ -428,11 +412,10 @@ export const useSessionState = () => { startupCommand: snippet.command, })); - setSessions(prev => [...prev, ...sessionsWithWorkspace]); - setWorkspaces(prev => [...prev, workspace]); - setActiveTabId(workspace.id); - // eslint-disable-next-line react-hooks/exhaustive-deps -- setActiveTabId is a stable store method reference - }, []); + setSessions(prev => [...prev, ...sessionsWithWorkspace]); + setWorkspaces(prev => [...prev, workspace]); + setActiveTabId(workspace.id); + }, [setActiveTabId]); const orphanSessions = useMemo(() => sessions.filter(s => !s.workspaceId), [sessions]); diff --git a/application/state/useSettingsState.ts b/application/state/useSettingsState.ts index c25198b5..c250b87e 100644 --- a/application/state/useSettingsState.ts +++ b/application/state/useSettingsState.ts @@ -1,4 +1,4 @@ -import { useCallback,useEffect,useMemo,useState } from 'react'; +import { useCallback,useEffect,useMemo,useState } from 'react'; import { SyncConfig, TerminalSettings, DEFAULT_TERMINAL_SETTINGS, HotkeyScheme, CustomKeyBindings, DEFAULT_KEY_BINDINGS, KeyBinding } from '../../domain/models'; import { STORAGE_KEY_COLOR, @@ -15,6 +15,7 @@ STORAGE_KEY_CUSTOM_CSS, import { TERMINAL_THEMES } from '../../infrastructure/config/terminalThemes'; import { TERMINAL_FONTS, DEFAULT_FONT_SIZE } from '../../infrastructure/config/fonts'; import { localStorageAdapter } from '../../infrastructure/persistence/localStorageAdapter'; +import { netcattyBridge } from '../../infrastructure/services/netcattyBridge'; const DEFAULT_COLOR = '221.2 83.2% 53.3%'; const DEFAULT_THEME: 'light' | 'dark' = 'light'; @@ -40,7 +41,7 @@ const applyThemeTokens = (theme: 'light' | 'dark', primaryColor: string) => { root.style.setProperty('--accent-foreground', accentForeground); // Sync with native window title bar (Electron) - window.netcatty?.setTheme?.(theme); + netcattyBridge.get()?.setTheme?.(theme); }; export const useSettingsState = () => { @@ -109,14 +110,14 @@ export const useSettingsState = () => { } } // Sync terminal settings from other windows - if (e.key === STORAGE_KEY_TERM_SETTINGS && e.newValue) { - try { - const newSettings = JSON.parse(e.newValue) as TerminalSettings; - setTerminalSettings(prev => ({ ...DEFAULT_TERMINAL_SETTINGS, ...newSettings })); - } catch { - // ignore parse errors - } - } + if (e.key === STORAGE_KEY_TERM_SETTINGS && e.newValue) { + try { + const newSettings = JSON.parse(e.newValue) as TerminalSettings; + setTerminalSettings(_prev => ({ ...DEFAULT_TERMINAL_SETTINGS, ...newSettings })); + } catch { + // ignore parse errors + } + } // Sync terminal theme from other windows if (e.key === STORAGE_KEY_TERM_THEME && e.newValue) { if (e.newValue !== terminalThemeId) { diff --git a/application/state/useSftpBackend.ts b/application/state/useSftpBackend.ts new file mode 100644 index 00000000..fd9de5f4 --- /dev/null +++ b/application/state/useSftpBackend.ts @@ -0,0 +1,206 @@ +import { useCallback } from "react"; +import { netcattyBridge } from "../../infrastructure/services/netcattyBridge"; +import type { RemoteFile } from "../../types"; + +export const useSftpBackend = () => { + const openSftp = useCallback(async (options: NetcattySSHOptions) => { + const bridge = netcattyBridge.get(); + if (!bridge?.openSftp) throw new Error("SFTP bridge unavailable"); + return bridge.openSftp(options); + }, []); + + const closeSftp = useCallback(async (sftpId: string) => { + const bridge = netcattyBridge.get(); + if (!bridge?.closeSftp) throw new Error("SFTP bridge unavailable"); + return bridge.closeSftp(sftpId); + }, []); + + const listSftp = useCallback(async (sftpId: string, path: string) => { + const bridge = netcattyBridge.get(); + if (!bridge?.listSftp) throw new Error("SFTP bridge unavailable"); + return bridge.listSftp(sftpId, path); + }, []); + + const readSftp = useCallback(async (sftpId: string, path: string) => { + const bridge = netcattyBridge.get(); + if (!bridge?.readSftp) throw new Error("SFTP bridge unavailable"); + return bridge.readSftp(sftpId, path); + }, []); + + const readSftpBinary = useCallback(async (sftpId: string, path: string) => { + const bridge = netcattyBridge.get(); + if (!bridge?.readSftpBinary) throw new Error("readSftpBinary unavailable"); + return bridge.readSftpBinary(sftpId, path); + }, []); + + const writeSftp = useCallback(async (sftpId: string, path: string, content: string) => { + const bridge = netcattyBridge.get(); + if (!bridge?.writeSftp) throw new Error("SFTP bridge unavailable"); + return bridge.writeSftp(sftpId, path, content); + }, []); + + const writeSftpBinary = useCallback(async (sftpId: string, path: string, content: ArrayBuffer) => { + const bridge = netcattyBridge.get(); + if (!bridge?.writeSftpBinary) throw new Error("writeSftpBinary unavailable"); + return bridge.writeSftpBinary(sftpId, path, content); + }, []); + + const writeSftpBinaryWithProgress = useCallback( + async ( + sftpId: string, + path: string, + content: ArrayBuffer, + transferId: string, + onProgress?: (transferred: number, total: number, speed: number) => void, + onComplete?: () => void, + onError?: (error: string) => void, + ) => { + const bridge = netcattyBridge.get(); + if (!bridge?.writeSftpBinaryWithProgress) return undefined; + return bridge.writeSftpBinaryWithProgress( + sftpId, + path, + content, + transferId, + onProgress, + onComplete, + onError, + ); + }, + [], + ); + + const mkdirSftp = useCallback(async (sftpId: string, path: string) => { + const bridge = netcattyBridge.get(); + if (!bridge?.mkdirSftp) throw new Error("mkdirSftp unavailable"); + return bridge.mkdirSftp(sftpId, path); + }, []); + + const deleteSftp = useCallback(async (sftpId: string, path: string) => { + const bridge = netcattyBridge.get(); + if (!bridge?.deleteSftp) throw new Error("deleteSftp unavailable"); + return bridge.deleteSftp(sftpId, path); + }, []); + + const renameSftp = useCallback(async (sftpId: string, oldPath: string, newPath: string) => { + const bridge = netcattyBridge.get(); + if (!bridge?.renameSftp) throw new Error("renameSftp unavailable"); + return bridge.renameSftp(sftpId, oldPath, newPath); + }, []); + + const statSftp = useCallback(async (sftpId: string, path: string) => { + const bridge = netcattyBridge.get(); + if (!bridge?.statSftp) throw new Error("statSftp unavailable"); + return bridge.statSftp(sftpId, path); + }, []); + + const chmodSftp = useCallback(async (sftpId: string, path: string, mode: string) => { + const bridge = netcattyBridge.get(); + if (!bridge?.chmodSftp) throw new Error("chmodSftp unavailable"); + return bridge.chmodSftp(sftpId, path, mode); + }, []); + + const listLocalDir = useCallback(async (path: string): Promise => { + const bridge = netcattyBridge.get(); + if (!bridge?.listLocalDir) throw new Error("listLocalDir unavailable"); + return bridge.listLocalDir(path); + }, []); + + const readLocalFile = useCallback(async (path: string): Promise => { + const bridge = netcattyBridge.get(); + if (!bridge?.readLocalFile) throw new Error("readLocalFile unavailable"); + return bridge.readLocalFile(path); + }, []); + + const writeLocalFile = useCallback(async (path: string, content: ArrayBuffer) => { + const bridge = netcattyBridge.get(); + if (!bridge?.writeLocalFile) throw new Error("writeLocalFile unavailable"); + return bridge.writeLocalFile(path, content); + }, []); + + const deleteLocalFile = useCallback(async (path: string) => { + const bridge = netcattyBridge.get(); + if (!bridge?.deleteLocalFile) throw new Error("deleteLocalFile unavailable"); + return bridge.deleteLocalFile(path); + }, []); + + const renameLocalFile = useCallback(async (oldPath: string, newPath: string) => { + const bridge = netcattyBridge.get(); + if (!bridge?.renameLocalFile) throw new Error("renameLocalFile unavailable"); + return bridge.renameLocalFile(oldPath, newPath); + }, []); + + const mkdirLocal = useCallback(async (path: string) => { + const bridge = netcattyBridge.get(); + if (!bridge?.mkdirLocal) throw new Error("mkdirLocal unavailable"); + return bridge.mkdirLocal(path); + }, []); + + const statLocal = useCallback(async (path: string) => { + const bridge = netcattyBridge.get(); + if (!bridge?.statLocal) throw new Error("statLocal unavailable"); + return bridge.statLocal(path); + }, []); + + const getHomeDir = useCallback(async () => { + const bridge = netcattyBridge.get(); + if (!bridge?.getHomeDir) return undefined; + return bridge.getHomeDir(); + }, []); + + const startStreamTransfer = useCallback( + async ( + options: Parameters>[0], + onProgress?: (transferred: number, total: number, speed: number) => void, + onComplete?: () => void, + onError?: (error: string) => void, + ) => { + const bridge = netcattyBridge.get(); + if (!bridge?.startStreamTransfer) return undefined; + return bridge.startStreamTransfer(options, onProgress, onComplete, onError); + }, + [], + ); + + const cancelTransfer = useCallback(async (transferId: string) => { + const bridge = netcattyBridge.get(); + if (!bridge?.cancelTransfer) return undefined; + return bridge.cancelTransfer(transferId); + }, []); + + const onTransferProgress = useCallback((transferId: string, cb: Parameters>[1]) => { + const bridge = netcattyBridge.get(); + if (!bridge?.onTransferProgress) return undefined; + return bridge.onTransferProgress(transferId, cb); + }, []); + + return { + openSftp, + closeSftp, + listSftp, + readSftp, + readSftpBinary, + writeSftp, + writeSftpBinary, + writeSftpBinaryWithProgress, + mkdirSftp, + deleteSftp, + renameSftp, + statSftp, + chmodSftp, + + listLocalDir, + readLocalFile, + writeLocalFile, + deleteLocalFile, + renameLocalFile, + mkdirLocal, + statLocal, + getHomeDir, + + startStreamTransfer, + cancelTransfer, + onTransferProgress, + }; +}; + diff --git a/application/state/useSftpState.ts b/application/state/useSftpState.ts index c5d4ecda..4b360d10 100644 --- a/application/state/useSftpState.ts +++ b/application/state/useSftpState.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { FileConflict, Host, @@ -9,6 +9,8 @@ import { TransferStatus, TransferTask, } from "../../domain/models"; +import { logger } from "../../lib/logger"; +import { netcattyBridge } from "../../infrastructure/services/netcattyBridge"; // Helper functions const formatFileSize = (bytes: number): string => { @@ -265,14 +267,14 @@ export const useSftpState = (hosts: Host[], keys: SSHKey[]) => { const intervalsRef = progressIntervalsRef.current; return () => { - // Clear all SFTP sessions - sessionsRef.forEach(async (sftpId) => { - try { - await window.netcatty?.closeSftp(sftpId); - } catch { - // Ignore errors when closing SFTP sessions during cleanup - } - }); + // Clear all SFTP sessions + sessionsRef.forEach(async (sftpId) => { + try { + await netcattyBridge.get()?.closeSftp(sftpId); + } catch { + // Ignore errors when closing SFTP sessions during cleanup + } + }); // Clear all progress simulation intervals intervalsRef.forEach((interval) => { clearInterval(interval); @@ -301,6 +303,47 @@ export const useSftpState = (hosts: Host[], keys: SSHKey[]) => { [keys], ); + const getMockLocalFiles = useCallback((path: string): SftpFileEntry[] => { + return buildMockLocalFiles(path); + }, []); + + const listLocalFiles = useCallback( + async (path: string): Promise => { + const rawFiles = await netcattyBridge.get()?.listLocalDir?.(path); + if (!rawFiles) { + // Fallback mock for development + return getMockLocalFiles(path); + } + + return rawFiles.map((f) => ({ + name: f.name, + type: f.type as "file" | "directory" | "symlink", + size: parseInt(f.size) || 0, + sizeFormatted: f.size, + lastModified: new Date(f.lastModified).getTime(), + lastModifiedFormatted: f.lastModified, + })); + }, + [getMockLocalFiles], + ); + + const listRemoteFiles = useCallback( + async (sftpId: string, path: string): Promise => { + const rawFiles = await netcattyBridge.get()?.listSftp(sftpId, path); + if (!rawFiles) return []; + + return rawFiles.map((f) => ({ + name: f.name, + type: f.type as "file" | "directory" | "symlink", + size: parseInt(f.size) || 0, + sizeFormatted: f.size, + lastModified: new Date(f.lastModified).getTime(), + lastModifiedFormatted: f.lastModified, + })); + }, + [], + ); + // Connect to a host const connect = useCallback( async (side: "left" | "right", host: Host | "local") => { @@ -323,25 +366,25 @@ export const useSftpState = (hosts: Host[], keys: SSHKey[]) => { const oldSftpId = sftpSessionsRef.current.get( currentPane.connection.id, ); - if (oldSftpId) { - try { - await window.netcatty?.closeSftp(oldSftpId); - } catch { - // Ignore errors when closing stale SFTP sessions - } - sftpSessionsRef.current.delete(currentPane.connection.id); - } - } + if (oldSftpId) { + try { + await netcattyBridge.get()?.closeSftp(oldSftpId); + } catch { + // Ignore errors when closing stale SFTP sessions + } + sftpSessionsRef.current.delete(currentPane.connection.id); + } + } if (host === "local") { - // Local filesystem connection - // Try to get home directory from backend, fallback to platform-specific default - let homeDir = await window.netcatty?.getHomeDir?.(); - if (!homeDir) { - // Detect platform and use appropriate default - const isWindows = navigator.platform.toLowerCase().includes("win"); - homeDir = isWindows ? "C:\\Users\\damao" : "/Users/damao"; - } + // Local filesystem connection + // Try to get home directory from backend, fallback to platform-specific default + let homeDir = await netcattyBridge.get()?.getHomeDir?.(); + if (!homeDir) { + // Detect platform and use appropriate default + const isWindows = navigator.platform.toLowerCase().includes("win"); + homeDir = isWindows ? "C:\\Users\\damao" : "/Users/damao"; + } const connection: SftpConnection = { id: connectionId, @@ -407,21 +450,21 @@ export const useSftpState = (hosts: Host[], keys: SSHKey[]) => { files: prev.reconnecting ? prev.files : [], // Keep files if reconnecting })); - try { - const credentials = getHostCredentials(host); - const sftpId = await window.netcatty?.openSftp({ - sessionId: `sftp-${connectionId}`, - ...credentials, - }); + try { + const credentials = getHostCredentials(host); + const sftpId = await netcattyBridge.get()?.openSftp({ + sessionId: `sftp-${connectionId}`, + ...credentials, + }); if (!sftpId) throw new Error("Failed to open SFTP session"); sftpSessionsRef.current.set(connectionId, sftpId); - // Try to get home directory, default to "/" - let startPath = "/"; - const statSftp = window.netcatty?.statSftp; - if (statSftp) { + // Try to get home directory, default to "/" + let startPath = "/"; + const statSftp = netcattyBridge.get()?.statSftp; + if (statSftp) { const candidates: string[] = []; if (credentials.username) { candidates.push(`/home/${credentials.username}`); @@ -438,26 +481,26 @@ export const useSftpState = (hosts: Host[], keys: SSHKey[]) => { // Ignore missing/permission errors } } - } else { - if (credentials.username) { - try { - const homeFiles = await window.netcatty?.listSftp( - sftpId, - `/home/${credentials.username}`, - ); - if (homeFiles) startPath = `/home/${credentials.username}`; - } catch { + } else { + if (credentials.username) { + try { + const homeFiles = await netcattyBridge.get()?.listSftp( + sftpId, + `/home/${credentials.username}`, + ); + if (homeFiles) startPath = `/home/${credentials.username}`; + } catch { // Fall through to /root check } } - if (startPath === "/") { - try { - const rootFiles = await window.netcatty?.listSftp( - sftpId, - "/root", - ); - if (rootFiles) startPath = "/root"; - } catch { + if (startPath === "/") { + try { + const rootFiles = await netcattyBridge.get()?.listSftp( + sftpId, + "/root", + ); + if (rootFiles) startPath = "/root"; + } catch { // Fallback path not available, use default } } @@ -507,8 +550,15 @@ export const useSftpState = (hosts: Host[], keys: SSHKey[]) => { } } }, - // eslint-disable-next-line react-hooks/exhaustive-deps -- listLocalFiles is intentionally omitted to prevent infinite loops - [getHostCredentials, leftPane, rightPane, clearCacheForConnection], + [ + getHostCredentials, + leftPane, + rightPane, + clearCacheForConnection, + makeCacheKey, + listLocalFiles, + listRemoteFiles, + ], ); // Auto-connect left pane to local filesystem on first mount @@ -561,16 +611,16 @@ export const useSftpState = (hosts: Host[], keys: SSHKey[]) => { lastConnectedHostRef.current[side] = null; if (pane.connection && !pane.connection.isLocal) { - const sftpId = sftpSessionsRef.current.get(pane.connection.id); - if (sftpId) { - try { - await window.netcatty?.closeSftp(sftpId); - } catch { - // Ignore errors when closing SFTP session during disconnect - } - sftpSessionsRef.current.delete(pane.connection.id); - } - } + const sftpId = sftpSessionsRef.current.get(pane.connection.id); + if (sftpId) { + try { + await netcattyBridge.get()?.closeSftp(sftpId); + } catch { + // Ignore errors when closing SFTP session during disconnect + } + sftpSessionsRef.current.delete(pane.connection.id); + } + } setPane({ connection: null, @@ -586,7 +636,7 @@ export const useSftpState = (hosts: Host[], keys: SSHKey[]) => { ); // Mock local file data for development (when backend is not available) - const getMockLocalFiles = (path: string): SftpFileEntry[] => { + function buildMockLocalFiles(path: string): SftpFileEntry[] { // Normalize path for matching (handle both Windows and Unix paths) const normPath = path.replace(/\\/g, "/").replace(/\/$/, "") || "/"; @@ -1035,43 +1085,7 @@ export const useSftpState = (hosts: Host[], keys: SSHKey[]) => { ], }; return mockData[normPath] || []; - }; - - // List local files - const listLocalFiles = async (path: string): Promise => { - const rawFiles = await window.netcatty?.listLocalDir?.(path); - if (!rawFiles) { - // Fallback mock for development - return getMockLocalFiles(path); - } - - return rawFiles.map((f) => ({ - name: f.name, - type: f.type as "file" | "directory" | "symlink", - size: parseInt(f.size) || 0, - sizeFormatted: f.size, - lastModified: new Date(f.lastModified).getTime(), - lastModifiedFormatted: f.lastModified, - })); - }; - - // List remote files - const listRemoteFiles = async ( - sftpId: string, - path: string, - ): Promise => { - const rawFiles = await window.netcatty?.listSftp(sftpId, path); - if (!rawFiles) return []; - - return rawFiles.map((f) => ({ - name: f.name, - type: f.type as "file" | "directory" | "symlink", - size: parseInt(f.size) || 0, - sizeFormatted: f.size, - lastModified: new Date(f.lastModified).getTime(), - lastModifiedFormatted: f.lastModified, - })); - }; + } // Navigate to path const navigateTo = useCallback( @@ -1181,8 +1195,14 @@ export const useSftpState = (hosts: Host[], keys: SSHKey[]) => { })); } }, - // eslint-disable-next-line react-hooks/exhaustive-deps -- listLocalFiles is intentionally omitted to prevent infinite loops - [leftPane, rightPane, makeCacheKey, clearCacheForConnection], + [ + leftPane, + rightPane, + makeCacheKey, + clearCacheForConnection, + listLocalFiles, + listRemoteFiles, + ], ); // Refresh current directory @@ -1305,18 +1325,18 @@ export const useSftpState = (hosts: Host[], keys: SSHKey[]) => { const fullPath = joinPath(pane.connection.currentPath, name); - try { - if (pane.connection.isLocal) { - await window.netcatty?.mkdirLocal?.(fullPath); - } else { - const sftpId = sftpSessionsRef.current.get(pane.connection.id); - if (!sftpId) { - handleSessionError(side, new Error("SFTP session not found")); - return; - } - await window.netcatty?.mkdirSftp(sftpId, fullPath); - } - await refresh(side); + try { + if (pane.connection.isLocal) { + await netcattyBridge.get()?.mkdirLocal?.(fullPath); + } else { + const sftpId = sftpSessionsRef.current.get(pane.connection.id); + if (!sftpId) { + handleSessionError(side, new Error("SFTP session not found")); + return; + } + await netcattyBridge.get()?.mkdirSftp(sftpId, fullPath); + } + await refresh(side); } catch (err) { if (isSessionError(err)) { handleSessionError(side, err as Error); @@ -1334,22 +1354,22 @@ export const useSftpState = (hosts: Host[], keys: SSHKey[]) => { const pane = side === "left" ? leftPane : rightPane; if (!pane.connection) return; - try { - for (const name of fileNames) { - const fullPath = joinPath(pane.connection.currentPath, name); + try { + for (const name of fileNames) { + const fullPath = joinPath(pane.connection.currentPath, name); - if (pane.connection.isLocal) { - await window.netcatty?.deleteLocalFile?.(fullPath); - } else { - const sftpId = sftpSessionsRef.current.get(pane.connection.id); - if (!sftpId) { - handleSessionError(side, new Error("SFTP session not found")); - return; - } - await window.netcatty?.deleteSftp?.(sftpId, fullPath); - } - } - await refresh(side); + if (pane.connection.isLocal) { + await netcattyBridge.get()?.deleteLocalFile?.(fullPath); + } else { + const sftpId = sftpSessionsRef.current.get(pane.connection.id); + if (!sftpId) { + handleSessionError(side, new Error("SFTP session not found")); + return; + } + await netcattyBridge.get()?.deleteSftp?.(sftpId, fullPath); + } + } + await refresh(side); } catch (err) { if (isSessionError(err)) { handleSessionError(side, err as Error); @@ -1370,18 +1390,18 @@ export const useSftpState = (hosts: Host[], keys: SSHKey[]) => { const oldPath = joinPath(pane.connection.currentPath, oldName); const newPath = joinPath(pane.connection.currentPath, newName); - try { - if (pane.connection.isLocal) { - await window.netcatty?.renameLocalFile?.(oldPath, newPath); - } else { - const sftpId = sftpSessionsRef.current.get(pane.connection.id); - if (!sftpId) { - handleSessionError(side, new Error("SFTP session not found")); - return; - } - await window.netcatty?.renameSftp?.(sftpId, oldPath, newPath); - } - await refresh(side); + try { + if (pane.connection.isLocal) { + await netcattyBridge.get()?.renameLocalFile?.(oldPath, newPath); + } else { + const sftpId = sftpSessionsRef.current.get(pane.connection.id); + if (!sftpId) { + handleSessionError(side, new Error("SFTP session not found")); + return; + } + await netcattyBridge.get()?.renameSftp?.(sftpId, oldPath, newPath); + } + await refresh(side); } catch (err) { if (isSessionError(err)) { handleSessionError(side, err as Error); @@ -1428,17 +1448,17 @@ export const useSftpState = (hosts: Host[], keys: SSHKey[]) => { let fileSize = 0; if (!file.isDirectory) { try { - const fullPath = joinPath(sourcePath, file.name); - if (sourcePane.connection!.isLocal) { - const stat = await window.netcatty?.statLocal?.(fullPath); - if (stat) fileSize = stat.size; - } else if (sourceSftpId) { - const stat = await window.netcatty?.statSftp?.( - sourceSftpId, - fullPath, - ); - if (stat) fileSize = stat.size; - } + const fullPath = joinPath(sourcePath, file.name); + if (sourcePane.connection!.isLocal) { + const stat = await netcattyBridge.get()?.statLocal?.(fullPath); + if (stat) fileSize = stat.size; + } else if (sourceSftpId) { + const stat = await netcattyBridge.get()?.statSftp?.( + sourceSftpId, + fullPath, + ); + if (stat) fileSize = stat.size; + } } catch { // If stat fails, we'll use estimate later } @@ -1488,35 +1508,35 @@ export const useSftpState = (hosts: Host[], keys: SSHKey[]) => { let actualFileSize = task.totalBytes; if (!task.isDirectory && actualFileSize === 0) { try { - const sourceSftpId = sourcePane.connection?.isLocal - ? null - : sftpSessionsRef.current.get(sourcePane.connection!.id); + const sourceSftpId = sourcePane.connection?.isLocal + ? null + : sftpSessionsRef.current.get(sourcePane.connection!.id); - if (sourcePane.connection?.isLocal) { - const stat = await window.netcatty?.statLocal?.(task.sourcePath); - if (stat) actualFileSize = stat.size; - } else if (sourceSftpId) { - const stat = await window.netcatty?.statSftp?.( - sourceSftpId, - task.sourcePath, - ); - if (stat) actualFileSize = stat.size; - } + if (sourcePane.connection?.isLocal) { + const stat = await netcattyBridge.get()?.statLocal?.(task.sourcePath); + if (stat) actualFileSize = stat.size; + } else if (sourceSftpId) { + const stat = await netcattyBridge.get()?.statSftp?.( + sourceSftpId, + task.sourcePath, + ); + if (stat) actualFileSize = stat.size; + } } catch { // Ignore stat errors, use estimate } } // Estimate file size for progress simulation (use a reasonable default if unknown) - const estimatedSize = + const estimatedSize = actualFileSize > 0 ? actualFileSize : task.isDirectory ? 1024 * 1024 // 1MB estimate for directories : 256 * 1024; // 256KB default for files - // Check if streaming transfer is available (will provide real progress) - const hasStreamingTransfer = !!window.netcatty?.startStreamTransfer; + // Check if streaming transfer is available (will provide real progress) + const hasStreamingTransfer = !!netcattyBridge.get()?.startStreamTransfer; updateTask({ status: "transferring", @@ -1547,22 +1567,22 @@ export const useSftpState = (hosts: Host[], keys: SSHKey[]) => { let sourceStat: { size: number; mtime: number } | null = null; // Get source file stat for accurate size and mtime - try { - if (sourcePane.connection?.isLocal) { - const stat = await window.netcatty?.statLocal?.(task.sourcePath); - if (stat) { - sourceStat = { - size: stat.size, - mtime: stat.lastModified || Date.now(), - }; - } - } else if (sourceSftpId && window.netcatty?.statSftp) { - const stat = await window.netcatty.statSftp( - sourceSftpId, - task.sourcePath, - ); - if (stat) { - sourceStat = { + try { + if (sourcePane.connection?.isLocal) { + const stat = await netcattyBridge.get()?.statLocal?.(task.sourcePath); + if (stat) { + sourceStat = { + size: stat.size, + mtime: stat.lastModified || Date.now(), + }; + } + } else if (sourceSftpId && netcattyBridge.get()?.statSftp) { + const stat = await netcattyBridge.get()!.statSftp!( + sourceSftpId, + task.sourcePath, + ); + if (stat) { + sourceStat = { size: stat.size, mtime: stat.lastModified || Date.now(), }; @@ -1573,24 +1593,24 @@ export const useSftpState = (hosts: Host[], keys: SSHKey[]) => { } // Get target file stat to check for conflict - try { - if (targetPane.connection?.isLocal) { - const stat = await window.netcatty?.statLocal?.(task.targetPath); - if (stat) { - targetExists = true; - existingStat = { - size: stat.size, - mtime: stat.lastModified || Date.now(), - }; - } - } else if (targetSftpId && window.netcatty?.statSftp) { - const stat = await window.netcatty.statSftp( - targetSftpId, - task.targetPath, - ); - if (stat) { - targetExists = true; - existingStat = { + try { + if (targetPane.connection?.isLocal) { + const stat = await netcattyBridge.get()?.statLocal?.(task.targetPath); + if (stat) { + targetExists = true; + existingStat = { + size: stat.size, + mtime: stat.lastModified || Date.now(), + }; + } + } else if (targetSftpId && netcattyBridge.get()?.statSftp) { + const stat = await netcattyBridge.get()!.statSftp!( + targetSftpId, + task.targetPath, + ); + if (stat) { + targetExists = true; + existingStat = { size: stat.size, mtime: stat.lastModified || Date.now(), }; @@ -1687,12 +1707,12 @@ export const useSftpState = (hosts: Host[], keys: SSHKey[]) => { targetSftpId: string | null, sourceIsLocal: boolean, targetIsLocal: boolean, - ): Promise => { - // Try to use streaming transfer if available - if (window.netcatty?.startStreamTransfer) { - return new Promise((resolve, reject) => { - const options = { - transferId: task.id, + ): Promise => { + // Try to use streaming transfer if available + if (netcattyBridge.get()?.startStreamTransfer) { + return new Promise((resolve, reject) => { + const options = { + transferId: task.id, sourcePath: task.sourcePath, targetPath: task.targetPath, sourceType: sourceIsLocal ? ("local" as const) : ("sftp" as const), @@ -1726,70 +1746,70 @@ export const useSftpState = (hosts: Host[], keys: SSHKey[]) => { resolve(); }; - const onError = (error: string) => { - reject(new Error(error)); - }; + const onError = (error: string) => { + reject(new Error(error)); + }; - window.netcatty!.startStreamTransfer!( - options, - onProgress, - onComplete, - onError, - ).catch(reject); - }); - } + netcattyBridge.require().startStreamTransfer!( + options, + onProgress, + onComplete, + onError, + ).catch(reject); + }); + } // Fallback to legacy transfer (read all then write all) let content: ArrayBuffer | string; - // Read from source - if (sourceIsLocal) { - content = - (await window.netcatty?.readLocalFile?.(task.sourcePath)) || - new ArrayBuffer(0); - } else if (sourceSftpId) { - if (window.netcatty?.readSftpBinary) { - content = await window.netcatty.readSftpBinary( - sourceSftpId, - task.sourcePath, - ); - } else { - content = - (await window.netcatty?.readSftp(sourceSftpId, task.sourcePath)) || ""; - } - } else { - throw new Error("No source connection"); - } + // Read from source + if (sourceIsLocal) { + content = + (await netcattyBridge.get()?.readLocalFile?.(task.sourcePath)) || + new ArrayBuffer(0); + } else if (sourceSftpId) { + if (netcattyBridge.get()?.readSftpBinary) { + content = await netcattyBridge.get()!.readSftpBinary!( + sourceSftpId, + task.sourcePath, + ); + } else { + content = + (await netcattyBridge.get()?.readSftp(sourceSftpId, task.sourcePath)) || ""; + } + } else { + throw new Error("No source connection"); + } - // Write to target - if (targetIsLocal) { - if (content instanceof ArrayBuffer) { - await window.netcatty?.writeLocalFile?.(task.targetPath, content); - } else { - const encoder = new TextEncoder(); - await window.netcatty?.writeLocalFile?.( - task.targetPath, - encoder.encode(content).buffer, - ); - } - } else if (targetSftpId) { - if (content instanceof ArrayBuffer && window.netcatty?.writeSftpBinary) { - await window.netcatty.writeSftpBinary( - targetSftpId, - task.targetPath, - content, - ); - } else { - const text = - content instanceof ArrayBuffer - ? new TextDecoder().decode(content) - : content; - await window.netcatty?.writeSftp(targetSftpId, task.targetPath, text); - } - } else { - throw new Error("No target connection"); - } - }; + // Write to target + if (targetIsLocal) { + if (content instanceof ArrayBuffer) { + await netcattyBridge.get()?.writeLocalFile?.(task.targetPath, content); + } else { + const encoder = new TextEncoder(); + await netcattyBridge.get()?.writeLocalFile?.( + task.targetPath, + encoder.encode(content).buffer, + ); + } + } else if (targetSftpId) { + if (content instanceof ArrayBuffer && netcattyBridge.get()?.writeSftpBinary) { + await netcattyBridge.get()!.writeSftpBinary!( + targetSftpId, + task.targetPath, + content, + ); + } else { + const text = + content instanceof ArrayBuffer + ? new TextDecoder().decode(content) + : content; + await netcattyBridge.get()?.writeSftp(targetSftpId, task.targetPath, text); + } + } else { + throw new Error("No target connection"); + } + }; // Transfer a directory const transferDirectory = async ( @@ -1799,12 +1819,12 @@ export const useSftpState = (hosts: Host[], keys: SSHKey[]) => { sourceIsLocal: boolean, targetIsLocal: boolean, ) => { - // Create target directory - if (targetIsLocal) { - await window.netcatty?.mkdirLocal?.(task.targetPath); - } else if (targetSftpId) { - await window.netcatty?.mkdirSftp(targetSftpId, task.targetPath); - } + // Create target directory + if (targetIsLocal) { + await netcattyBridge.get()?.mkdirLocal?.(task.targetPath); + } else if (targetSftpId) { + await netcattyBridge.get()?.mkdirSftp(targetSftpId, task.targetPath); + } // List source directory let files: SftpFileEntry[]; @@ -1873,14 +1893,14 @@ export const useSftpState = (hosts: Host[], keys: SSHKey[]) => { // Remove from conflicts if present setConflicts((prev) => prev.filter((c) => c.transferId !== transferId)); - // Cancel at backend level if streaming transfer is in progress - if (window.netcatty?.cancelTransfer) { - try { - await window.netcatty.cancelTransfer(transferId); - } catch (err) { - console.warn("Failed to cancel transfer at backend:", err); - } - } + // Cancel at backend level if streaming transfer is in progress + if (netcattyBridge.get()?.cancelTransfer) { + try { + await netcattyBridge.get()!.cancelTransfer!(transferId); + } catch (err) { + logger.warn("Failed to cancel transfer at backend:", err); + } + } }, [stopProgressSimulation], ); @@ -2028,25 +2048,25 @@ export const useSftpState = (hosts: Host[], keys: SSHKey[]) => { ) => { const pane = side === "left" ? leftPane : rightPane; if (!pane.connection || pane.connection.isLocal) { - console.warn("Cannot change permissions on local files"); + logger.warn("Cannot change permissions on local files"); return; - } - - const sftpId = sftpSessionsRef.current.get(pane.connection.id); - if (!sftpId || !window.netcatty?.chmodSftp) { - handleSessionError(side, new Error("SFTP session not found")); - return; - } - - try { - await window.netcatty.chmodSftp(sftpId, filePath, mode); - await refresh(side); - } catch (err) { + } + + const sftpId = sftpSessionsRef.current.get(pane.connection.id); + if (!sftpId || !netcattyBridge.get()?.chmodSftp) { + handleSessionError(side, new Error("SFTP session not found")); + return; + } + + try { + await netcattyBridge.get()!.chmodSftp!(sftpId, filePath, mode); + await refresh(side); + } catch (err) { if (isSessionError(err)) { handleSessionError(side, err as Error); return; } - console.error("Failed to change permissions:", err); + logger.error("Failed to change permissions:", err); } }, [leftPane, rightPane, refresh, handleSessionError], diff --git a/application/state/useSyncState.ts b/application/state/useSyncState.ts new file mode 100644 index 00000000..771ffcd9 --- /dev/null +++ b/application/state/useSyncState.ts @@ -0,0 +1,65 @@ +import { useCallback, useState } from "react"; +import { loadFromGist, syncToGist } from "../../infrastructure/services/syncService"; + +export type SyncStatus = "idle" | "success" | "error"; + +export const useSyncState = () => { + const [isSyncing, setIsSyncing] = useState(false); + const [syncStatus, setSyncStatus] = useState("idle"); + + const resetSyncStatus = useCallback(() => { + setSyncStatus("idle"); + }, []); + + const verify = useCallback(async (token: string, gistId?: string) => { + setIsSyncing(true); + setSyncStatus("idle"); + try { + if (gistId) { + await loadFromGist(token, gistId); + } + setSyncStatus("success"); + } catch (err) { + setSyncStatus("error"); + throw err; + } finally { + setIsSyncing(false); + } + }, []); + + const upload = useCallback( + async (token: string, gistId: string | undefined, data: T) => { + setIsSyncing(true); + setSyncStatus("idle"); + try { + const newGistId = await syncToGist(token, gistId, data); + setSyncStatus("success"); + return newGistId; + } catch (err) { + setSyncStatus("error"); + throw err; + } finally { + setIsSyncing(false); + } + }, + [], + ); + + const download = useCallback(async (token: string, gistId: string) => { + setIsSyncing(true); + setSyncStatus("idle"); + try { + const data = await loadFromGist(token, gistId); + setSyncStatus("success"); + return data; + } catch (err) { + setSyncStatus("error"); + throw err; + } finally { + setIsSyncing(false); + } + }, []); + + return { isSyncing, syncStatus, resetSyncStatus, verify, upload, download }; +}; + diff --git a/application/state/useTerminalBackend.ts b/application/state/useTerminalBackend.ts new file mode 100644 index 00000000..9c901887 --- /dev/null +++ b/application/state/useTerminalBackend.ts @@ -0,0 +1,122 @@ +import { useCallback } from "react"; +import { netcattyBridge } from "../../infrastructure/services/netcattyBridge"; + +export const useTerminalBackend = () => { + const telnetAvailable = useCallback(() => { + const bridge = netcattyBridge.get(); + return !!bridge?.startTelnetSession; + }, []); + + const moshAvailable = useCallback(() => { + const bridge = netcattyBridge.get(); + return !!bridge?.startMoshSession; + }, []); + + const localAvailable = useCallback(() => { + const bridge = netcattyBridge.get(); + return !!bridge?.startLocalSession; + }, []); + + const execAvailable = useCallback(() => { + const bridge = netcattyBridge.get(); + return !!bridge?.execCommand; + }, []); + + const startSSHSession = useCallback(async (options: NetcattySSHOptions) => { + const bridge = netcattyBridge.get(); + if (!bridge?.startSSHSession) throw new Error("startSSHSession unavailable"); + return bridge.startSSHSession(options); + }, []); + + const startTelnetSession = useCallback(async (options: Parameters>[0]) => { + const bridge = netcattyBridge.get(); + if (!bridge?.startTelnetSession) throw new Error("startTelnetSession unavailable"); + return bridge.startTelnetSession(options); + }, []); + + const startMoshSession = useCallback(async (options: Parameters>[0]) => { + const bridge = netcattyBridge.get(); + if (!bridge?.startMoshSession) throw new Error("startMoshSession unavailable"); + return bridge.startMoshSession(options); + }, []); + + const startLocalSession = useCallback(async (options: Parameters>[0]) => { + const bridge = netcattyBridge.get(); + if (!bridge?.startLocalSession) throw new Error("startLocalSession unavailable"); + return bridge.startLocalSession(options); + }, []); + + const execCommand = useCallback(async (options: Parameters[0]) => { + const bridge = netcattyBridge.get(); + if (!bridge?.execCommand) throw new Error("execCommand unavailable"); + return bridge.execCommand(options); + }, []); + + const writeToSession = useCallback((sessionId: string, data: string) => { + const bridge = netcattyBridge.get(); + bridge?.writeToSession?.(sessionId, data); + }, []); + + const resizeSession = useCallback((sessionId: string, cols: number, rows: number) => { + const bridge = netcattyBridge.get(); + bridge?.resizeSession?.(sessionId, cols, rows); + }, []); + + const closeSession = useCallback((sessionId: string) => { + const bridge = netcattyBridge.get(); + bridge?.closeSession?.(sessionId); + }, []); + + const onSessionData = useCallback((sessionId: string, cb: (data: string) => void) => { + const bridge = netcattyBridge.get(); + if (!bridge?.onSessionData) throw new Error("onSessionData unavailable"); + return bridge.onSessionData(sessionId, cb); + }, []); + + const onSessionExit = useCallback((sessionId: string, cb: (evt: { exitCode?: number; signal?: number }) => void) => { + const bridge = netcattyBridge.get(); + if (!bridge?.onSessionExit) throw new Error("onSessionExit unavailable"); + return bridge.onSessionExit(sessionId, cb); + }, []); + + const onChainProgress = useCallback((cb: (hop: number, total: number, label: string, status: string) => void) => { + const bridge = netcattyBridge.get(); + return bridge?.onChainProgress?.(cb); + }, []); + + const openExternal = useCallback(async (url: string) => { + const bridge = netcattyBridge.get(); + await bridge?.openExternal?.(url); + }, []); + + const openExternalAvailable = useCallback(() => { + const bridge = netcattyBridge.get(); + return !!bridge?.openExternal; + }, []); + + const backendAvailable = useCallback(() => { + const bridge = netcattyBridge.get(); + return !!bridge?.startSSHSession; + }, []); + + return { + backendAvailable, + telnetAvailable, + moshAvailable, + localAvailable, + execAvailable, + openExternalAvailable, + startSSHSession, + startTelnetSession, + startMoshSession, + startLocalSession, + execCommand, + writeToSession, + resizeSession, + closeSession, + onSessionData, + onSessionExit, + onChainProgress, + openExternal, + }; +}; diff --git a/application/state/useWindowControls.ts b/application/state/useWindowControls.ts new file mode 100644 index 00000000..9cc890bb --- /dev/null +++ b/application/state/useWindowControls.ts @@ -0,0 +1,44 @@ +import { useCallback } from "react"; +import { netcattyBridge } from "../../infrastructure/services/netcattyBridge"; + +export const useWindowControls = () => { + const closeSettingsWindow = useCallback(async () => { + const bridge = netcattyBridge.get(); + await bridge?.closeSettingsWindow?.(); + }, []); + + const openSettingsWindow = useCallback(async () => { + const bridge = netcattyBridge.get(); + return bridge?.openSettingsWindow?.(); + }, []); + + const minimize = useCallback(async () => { + const bridge = netcattyBridge.get(); + await bridge?.windowMinimize?.(); + }, []); + + const maximize = useCallback(async () => { + const bridge = netcattyBridge.get(); + return bridge?.windowMaximize?.(); + }, []); + + const close = useCallback(async () => { + const bridge = netcattyBridge.get(); + await bridge?.windowClose?.(); + }, []); + + const isMaximized = useCallback(async () => { + const bridge = netcattyBridge.get(); + return bridge?.windowIsMaximized?.(); + }, []); + + return { + closeSettingsWindow, + openSettingsWindow, + minimize, + maximize, + close, + isMaximized, + }; +}; + diff --git a/components/KeychainManager.tsx b/components/KeychainManager.tsx index 1966f0a0..4748a77c 100644 --- a/components/KeychainManager.tsx +++ b/components/KeychainManager.tsx @@ -1,4 +1,4 @@ -import { +import { BadgeCheck, ChevronDown, ChevronRight, @@ -15,8 +15,10 @@ UserPlus, } from "lucide-react"; import React, { useCallback, useMemo, useState } from "react"; +import { logger } from "../lib/logger"; import { cn } from "../lib/utils"; import { Host, Identity, KeyType, SSHKey } from "../types"; +import { useKeychainBackend } from "../application/state/useKeychainBackend"; import SelectHostPanel from "./SelectHostPanel"; import { AsidePanel, AsidePanelContent } from "./ui/aside-panel"; import { Button } from "./ui/button"; @@ -77,6 +79,7 @@ const KeychainManager: React.FC = ({ onSaveHost, onCreateGroup, }) => { + const { generateKeyPair, execCommand } = useKeychainBackend(); const [activeFilter, setActiveFilter] = useState("key"); const [search, setSearch] = useState(""); const [viewMode, setViewMode] = useState<"grid" | "list">("grid"); @@ -244,7 +247,7 @@ echo $3 >> "$FILE"`); await navigator.clipboard.writeText(key.publicKey); // Could add toast notification here } catch (err) { - console.error("Failed to copy public key:", err); + logger.error("Failed to copy public key:", err); } } }, []); @@ -328,16 +331,19 @@ echo $3 >> "$FILE"`); const keySize = draftKey.keySize; // Use real key generation via Electron backend - if (window.netcatty?.generateKeyPair) { - const result = await window.netcatty.generateKeyPair({ - type: keyType, - bits: keySize, - comment: `${draftKey.label.trim()}@netcatty`, - }); - - if (!result.success || !result.privateKey || !result.publicKey) { - throw new Error(result.error || "Failed to generate key pair"); - } + const result = await generateKeyPair({ + type: keyType, + bits: keySize, + comment: `${draftKey.label.trim()}@netcatty`, + }); + if (!result) { + throw new Error( + "Key generation not available - please ensure the app is running in Electron", + ); + } + if (!result.success || !result.privateKey || !result.publicKey) { + throw new Error(result.error || "Failed to generate key pair"); + } const newKey: SSHKey = { id: crypto.randomUUID(), @@ -355,17 +361,12 @@ echo $3 >> "$FILE"`); onSave(newKey); closePanel(); - } else { - throw new Error( - "Key generation not available - please ensure the app is running in Electron", - ); - } } catch (err) { setError(err instanceof Error ? err.message : "Failed to generate key"); } finally { setIsGenerating(false); } - }, [draftKey, onSave, closePanel]); + }, [draftKey, onSave, closePanel, generateKeyPair]); // Handle biometric key generation (Windows Hello) const handleGenerateBiometric = useCallback(async () => { @@ -1235,7 +1236,7 @@ echo $3 >> "$FILE"`); const command = scriptWithVars; // Execute via SSH - const result = await window.netcatty?.execCommand({ + const result = await execCommand({ hostname: exportHost.hostname, username: exportHost.username, port: exportHost.port || 22, diff --git a/components/KnownHostsManager.tsx b/components/KnownHostsManager.tsx index d24915ab..57c300d6 100644 --- a/components/KnownHostsManager.tsx +++ b/components/KnownHostsManager.tsx @@ -1,4 +1,4 @@ -import { +import { ArrowRight, ChevronDown, Clock, @@ -21,6 +21,8 @@ import React, { useMemo, useState, } from "react"; +import { useKnownHostsBackend } from "../application/state/useKnownHostsBackend"; +import { logger } from "../lib/logger"; import { cn } from "../lib/utils"; import { Host, KnownHost } from "../types"; import { Button } from "./ui/button"; @@ -104,7 +106,7 @@ const parseKnownHostsFile = (content: string): KnownHost[] => { discoveredAt: Date.now(), }); } catch { - console.warn("Failed to parse known_hosts line:", line); + logger.warn("Failed to parse known_hosts line:", line); } } @@ -259,13 +261,7 @@ const KnownHostsManager: React.FC = ({ onImportFromFile, onRefresh, }) => { - // Debug: track renders - const renderCountRef = React.useRef(0); - renderCountRef.current++; - console.log( - `[KnownHostsManager] render #${renderCountRef.current} - knownHosts: ${knownHosts.length}, hosts: ${hosts.length}`, - ); - + const { readKnownHosts } = useKnownHostsBackend(); const [search, setSearch] = useState(""); const deferredSearch = useDeferredValue(search); const [isScanning, setIsScanning] = useState(false); @@ -278,31 +274,28 @@ const KnownHostsManager: React.FC = ({ // Define handleScanSystem before useEffect that depends on it const handleScanSystem = useCallback(async () => { setIsScanning(true); - // Try to read from common known_hosts locations via Electron - if (window.netcatty?.readKnownHosts) { - try { - const content = await window.netcatty.readKnownHosts(); - if (content) { - const parsed = parseKnownHostsFile(content); - const existingHostnames = new Set( - knownHosts.map((h) => `${h.hostname}:${h.port}`), - ); - const newHosts = parsed.filter( - (h) => !existingHostnames.has(`${h.hostname}:${h.port}`), - ); + try { + const content = await readKnownHosts(); + if (content) { + const parsed = parseKnownHostsFile(content); + const existingHostnames = new Set( + knownHosts.map((h) => `${h.hostname}:${h.port}`), + ); + const newHosts = parsed.filter( + (h) => !existingHostnames.has(`${h.hostname}:${h.port}`), + ); - // Directly import new hosts without dialog - if (newHosts.length > 0) { - onImportFromFile(newHosts); - } + // Directly import new hosts without dialog + if (newHosts.length > 0) { + onImportFromFile(newHosts); } - } catch (err) { - console.error("Failed to scan system known_hosts:", err); } + } catch (err) { + logger.error("Failed to scan system known_hosts:", err); } onRefresh(); setIsScanning(false); - }, [knownHosts, onRefresh, onImportFromFile]); + }, [knownHosts, onRefresh, onImportFromFile, readKnownHosts]); // Auto-scan on first mount useEffect(() => { @@ -423,10 +416,6 @@ const KnownHostsManager: React.FC = ({ // Memoize the rendered list to prevent re-renders const renderedItems = useMemo(() => { - console.log( - "[KnownHostsManager] renderedItems useMemo recalculated - displayedHosts:", - displayedHosts.length, - ); return displayedHosts.map((knownHost) => ( = ({ handleConvertToHost, ]); - console.log("[KnownHostsManager] about to return JSX"); - return (
{/* Header */} diff --git a/components/PortForwardingNew.tsx b/components/PortForwardingNew.tsx index 9ab32b03..25ffa294 100644 --- a/components/PortForwardingNew.tsx +++ b/components/PortForwardingNew.tsx @@ -17,10 +17,6 @@ import { PortForwardingType, SSHKey, } from "../domain/models"; -import { - startPortForward, - stopPortForward, -} from "../infrastructure/services/portForwardingService"; import { cn } from "../lib/utils"; import SelectHostPanel from "./SelectHostPanel"; import { @@ -83,8 +79,12 @@ const PortForwarding: React.FC = ({ deleteRule, duplicateRule, setRuleStatus, + startTunnel, + stopTunnel, filteredRules, selectedRule: _selectedRule, + preferFormMode, + setPreferFormMode, } = usePortForwardingState(); // Track connecting/stopping states @@ -106,12 +106,11 @@ const PortForwarding: React.FC = ({ let errorShown = false; try { - const result = await startPortForward( + const result = await startTunnel( rule, _host, keys.map((k) => ({ id: k.id, privateKey: k.privateKey })), (status, error) => { - setRuleStatus(rule.id, status, error); // Show toast on error (only once) if (status === "error" && error && !errorShown) { errorShown = true; @@ -132,7 +131,7 @@ const PortForwarding: React.FC = ({ }); } }, - [hosts, keys, setRuleStatus], + [hosts, keys, setRuleStatus, startTunnel], ); // Stop a port forwarding tunnel @@ -141,9 +140,7 @@ const PortForwarding: React.FC = ({ setPendingOperations((prev) => new Set([...prev, rule.id])); try { - await stopPortForward(rule.id, (status) => { - setRuleStatus(rule.id, status); - }); + await stopTunnel(rule.id); } finally { setPendingOperations((prev) => { const next = new Set(prev); @@ -152,7 +149,7 @@ const PortForwarding: React.FC = ({ }); } }, - [setRuleStatus], + [stopTunnel], ); // Wizard state @@ -191,15 +188,6 @@ const PortForwarding: React.FC = ({ hostId: undefined, }, ); - // User preference: prefer wizard (false) or form (true) - const [preferFormMode, setPreferFormMode] = useState(() => { - try { - // Default to wizard mode (false) if not set - return localStorage.getItem("pf-prefer-form-mode") === "true"; - } catch { - return false; - } - }); // New forwarding menu const [showNewMenu, setShowNewMenu] = useState(false); @@ -258,11 +246,6 @@ const PortForwarding: React.FC = ({ const skipWizardToForm = () => { // Save preference setPreferFormMode(true); - try { - localStorage.setItem("pf-prefer-form-mode", "true"); - } catch { - // Ignore localStorage errors (e.g., private browsing mode) - } // Transfer current draft to form setNewFormDraft({ @@ -277,11 +260,6 @@ const PortForwarding: React.FC = ({ const openWizardFromForm = () => { // User opens wizard - prefer wizard mode next time setPreferFormMode(false); - try { - localStorage.setItem("pf-prefer-form-mode", "false"); - } catch { - // Ignore localStorage errors (e.g., private browsing mode) - } // Transfer current form draft to wizard setWizardType(newFormDraft.type || "local"); diff --git a/components/SFTPModal.tsx b/components/SFTPModal.tsx index f93e5387..8245fa52 100644 --- a/components/SFTPModal.tsx +++ b/components/SFTPModal.tsx @@ -1,4 +1,4 @@ -import { +import { ArrowUp, ChevronRight, Database, @@ -33,6 +33,8 @@ import React, { useRef, useState, } from "react"; +import { useSftpBackend } from "../application/state/useSftpBackend"; +import { logger } from "../lib/logger"; import { cn } from "../lib/utils"; import { Host, RemoteFile } from "../types"; import { DistroAvatar } from "./DistroAvatar"; @@ -196,7 +198,7 @@ const getFileIcon = (fileName: string, isDirectory: boolean) => { // Default return ; -}; + }; // Format bytes with appropriate unit (B, KB, MB, GB) const formatBytes = (bytes: number | string): string => { @@ -254,6 +256,17 @@ const SFTPModal: React.FC = ({ open, onClose, }) => { + const { + openSftp, + closeSftp: closeSftpBackend, + listSftp, + readSftp, + writeSftpBinaryWithProgress, + writeSftpBinary, + writeSftp, + deleteSftp, + mkdirSftp, + } = useSftpBackend(); const [currentPath, setCurrentPath] = useState("/"); const [files, setFiles] = useState([]); const [loading, setLoading] = useState(false); @@ -299,8 +312,7 @@ const SFTPModal: React.FC = ({ const ensureSftp = useCallback(async () => { if (sftpIdRef.current) return sftpIdRef.current; - if (!window.netcatty?.openSftp) throw new Error("SFTP bridge unavailable"); - const sftpId = await window.netcatty.openSftp({ + const sftpId = await openSftp({ sessionId: `sftp-modal-${host.id}`, hostname: credentials.hostname, username: credentials.username || "root", @@ -317,6 +329,7 @@ const SFTPModal: React.FC = ({ credentials.port, credentials.password, credentials.privateKey, + openSftp, ]); const loadFiles = useCallback( @@ -343,7 +356,7 @@ const SFTPModal: React.FC = ({ setError(null); setLoading(true); const sftpId = await ensureSftp(); - const list = await window.netcatty.listSftp(sftpId, path); + const list = await listSftp(sftpId, path); if (loadSeqRef.current !== requestId) return; dirCacheRef.current.set(cacheKey, { files: list, @@ -353,7 +366,7 @@ const SFTPModal: React.FC = ({ setSelectedFiles(new Set()); } catch (e) { if (loadSeqRef.current !== requestId) return; - console.error("Failed to load files", e); + logger.error("Failed to load files", e); setError(e instanceof Error ? e.message : "Failed to load directory"); setFiles([]); } finally { @@ -362,26 +375,26 @@ const SFTPModal: React.FC = ({ } } }, - [ensureSftp, host.id], + [ensureSftp, host.id, listSftp], ); - const closeSftp = async () => { - if (sftpIdRef.current && window.netcatty?.closeSftp) { + const closeSftpSession = useCallback(async () => { + if (sftpIdRef.current) { try { - await window.netcatty.closeSftp(sftpIdRef.current); + await closeSftpBackend(sftpIdRef.current); } catch { // Silently ignore close errors - connection may already be closed } } sftpIdRef.current = null; - }; + }, [closeSftpBackend]); useEffect(() => { return () => { // Cleanup on unmount - closeSftp(); + void closeSftpSession(); }; - }, []); + }, [closeSftpSession]); useEffect(() => { if (open) { @@ -393,10 +406,10 @@ const SFTPModal: React.FC = ({ } else { // Invalidate any in-flight directory load loadSeqRef.current += 1; - closeSftp(); + void closeSftpSession(); initializedRef.current = false; } - }, [open, currentPath, loadFiles]); + }, [open, currentPath, loadFiles, closeSftpSession]); const handleNavigate = useCallback((path: string) => { // Prevent double navigation (e.g., from double-click race condition) @@ -415,28 +428,31 @@ const SFTPModal: React.FC = ({ setCurrentPath(parent); }; - const handleDownload = async (file: RemoteFile) => { - try { - const sftpId = await ensureSftp(); - const fullPath = - currentPath === "/" ? `/${file.name}` : `${currentPath}/${file.name}`; - setLoading(true); - const content = await window.netcatty.readSftp(sftpId, fullPath); - const blob = new Blob([content], { type: "application/octet-stream" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = file.name; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - } catch (e) { - setError(e instanceof Error ? e.message : "Download failed"); - } finally { - setLoading(false); - } - }; + const handleDownload = useCallback( + async (file: RemoteFile) => { + try { + const sftpId = await ensureSftp(); + const fullPath = + currentPath === "/" ? `/${file.name}` : `${currentPath}/${file.name}`; + setLoading(true); + const content = await readSftp(sftpId, fullPath); + const blob = new Blob([content], { type: "application/octet-stream" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = file.name; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } catch (e) { + setError(e instanceof Error ? e.message : "Download failed"); + } finally { + setLoading(false); + } + }, + [currentPath, ensureSftp, readSftp], + ); const handleUploadFile = async ( file: File, @@ -466,70 +482,69 @@ const SFTPModal: React.FC = ({ currentPath === "/" ? `/${file.name}` : `${currentPath}/${file.name}`; // Use real-time progress API if available - if (window.netcatty.writeSftpBinaryWithProgress) { - await window.netcatty.writeSftpBinaryWithProgress( - sftpId, - fullPath, - arrayBuffer, - taskId, - // Real-time progress callback - (transferred: number, total: number, speed: number) => { - const progress = - total > 0 ? Math.round((transferred / total) * 100) : 0; - setUploadTasks((prev) => - prev.map((t) => - t.id === taskId && t.status === "uploading" - ? { - ...t, - transferredBytes: transferred, - progress, - speed, - } - : t, - ), - ); - }, - // Complete callback - () => { - const totalTime = (Date.now() - startTime) / 1000; - const finalSpeed = totalTime > 0 ? file.size / totalTime : 0; - setUploadTasks((prev) => - prev.map((t) => - t.id === taskId - ? { - ...t, - status: "completed" as const, - progress: 100, - transferredBytes: file.size, - speed: finalSpeed, - } - : t, - ), - ); - }, - // Error callback - (error: string) => { - setUploadTasks((prev) => - prev.map((t) => - t.id === taskId - ? { - ...t, - status: "failed" as const, - error, - } - : t, - ), - ); - }, - ); - return true; - } else if (window.netcatty.writeSftpBinary) { + const progressResult = await writeSftpBinaryWithProgress( + sftpId, + fullPath, + arrayBuffer, + taskId, + // Real-time progress callback + (transferred: number, total: number, speed: number) => { + const progress = total > 0 ? Math.round((transferred / total) * 100) : 0; + setUploadTasks((prev) => + prev.map((t) => + t.id === taskId && t.status === "uploading" + ? { + ...t, + transferredBytes: transferred, + progress, + speed, + } + : t, + ), + ); + }, + // Complete callback + () => { + const totalTime = (Date.now() - startTime) / 1000; + const finalSpeed = totalTime > 0 ? file.size / totalTime : 0; + setUploadTasks((prev) => + prev.map((t) => + t.id === taskId + ? { + ...t, + status: "completed" as const, + progress: 100, + transferredBytes: file.size, + speed: finalSpeed, + } + : t, + ), + ); + }, + // Error callback + (error: string) => { + setUploadTasks((prev) => + prev.map((t) => + t.id === taskId + ? { + ...t, + status: "failed" as const, + error, + } + : t, + ), + ); + }, + ); + if (progressResult) return true; + + try { // Fallback to non-progress API - await window.netcatty.writeSftpBinary(sftpId, fullPath, arrayBuffer); - } else { + await writeSftpBinary(sftpId, fullPath, arrayBuffer); + } catch { // Fallback: read as text (works for text files) const text = await file.text(); - await window.netcatty.writeSftp(sftpId, fullPath, text); + await writeSftp(sftpId, fullPath, text); } // Calculate final speed (for fallback methods) @@ -607,9 +622,7 @@ const SFTPModal: React.FC = ({ const fullPath = currentPath === "/" ? `/${file.name}` : `${currentPath}/${file.name}`; // Use deleteSftp which handles both files and directories - if (window.netcatty.deleteSftp) { - await window.netcatty.deleteSftp(sftpId, fullPath); - } + await deleteSftp(sftpId, fullPath); await loadFiles(currentPath, { force: true }); } catch (e) { setError(e instanceof Error ? e.message : "Delete failed"); @@ -623,7 +636,7 @@ const SFTPModal: React.FC = ({ const sftpId = await ensureSftp(); const fullPath = currentPath === "/" ? `/${folderName}` : `${currentPath}/${folderName}`; - await window.netcatty.mkdirSftp(sftpId, fullPath); + await mkdirSftp(sftpId, fullPath); await loadFiles(currentPath, { force: true }); } catch (e) { setError(e instanceof Error ? e.message : "Failed to create folder"); @@ -659,7 +672,7 @@ const SFTPModal: React.FC = ({ }; const handleClose = async () => { - await closeSftp(); + await closeSftpSession(); setIsEditingPath(false); onClose(); }; @@ -852,9 +865,7 @@ const SFTPModal: React.FC = ({ for (const fileName of fileNames) { const fullPath = currentPath === "/" ? `/${fileName}` : `${currentPath}/${fileName}`; - if (window.netcatty.deleteSftp) { - await window.netcatty.deleteSftp(sftpId, fullPath); - } + await deleteSftp(sftpId, fullPath); } await loadFiles(currentPath, { force: true }); setSelectedFiles(new Set()); diff --git a/components/SettingsDialog.tsx b/components/SettingsDialog.tsx index c433f700..1b36a3b0 100644 --- a/components/SettingsDialog.tsx +++ b/components/SettingsDialog.tsx @@ -14,16 +14,15 @@ import { X, } from "lucide-react"; import React, { useCallback, useEffect, useRef, useState } from "react"; +import { useSyncState } from "../application/state/useSyncState"; import { Host, SSHKey, Snippet, TerminalSettings, CursorShape, RightClickBehavior, LinkModifier, DEFAULT_TERMINAL_SETTINGS, HotkeyScheme, KeyBinding, keyEventToString } from "../domain/models"; import { TERMINAL_THEMES } from "../infrastructure/config/terminalThemes"; import { TERMINAL_FONTS, MIN_FONT_SIZE, MAX_FONT_SIZE } from "../infrastructure/config/fonts"; -import { - loadFromGist, - syncToGist, -} from "../infrastructure/services/syncService"; +import { logger } from "../lib/logger"; import { cn } from "../lib/utils"; import { SyncConfig } from "../types"; import { Button } from "./ui/button"; +import { toast } from "./ui/toast"; import { Dialog, DialogContent, @@ -268,13 +267,15 @@ const SettingsDialog: React.FC = ({ // Sync State const [githubToken, setGithubToken] = useState(syncConfig?.githubToken || ""); const [gistId, setGistId] = useState(syncConfig?.gistId || ""); - const [isSyncing, setIsSyncing] = useState(false); - const [syncStatus, setSyncStatus] = useState<"idle" | "success" | "error">( - "idle", - ); + const { isSyncing, syncStatus, resetSyncStatus, verify, upload, download } = + useSyncState(); const isMac = hotkeyScheme === 'mac'; + useEffect(() => { + if (isOpen) resetSyncStatus(); + }, [isOpen, resetSyncStatus]); + // Handle key recording for shortcut editing const handleKeyDown = useCallback((e: KeyboardEvent) => { if (!editingBindingId) return; @@ -368,36 +369,26 @@ const SettingsDialog: React.FC = ({ try { JSON.parse(importText); onImport(importText); - alert("Configuration imported successfully!"); + toast.success("Configuration imported successfully!"); setImportText(""); } catch { - alert("Invalid JSON format."); + toast.error("Invalid JSON format."); } }; const handleSaveSyncConfig = async () => { if (!githubToken) return; - - setIsSyncing(true); - setSyncStatus("idle"); try { - if (gistId) { - await loadFromGist(githubToken, gistId); - } + await verify(githubToken, gistId || undefined); onSyncConfigChange({ githubToken, gistId }); - setSyncStatus("success"); } catch (e) { - console.error(e); - setSyncStatus("error"); - alert("Failed to verify Gist or Token."); - } finally { - setIsSyncing(false); + logger.error(e); + toast.error("Failed to verify Gist or Token."); } }; const performSyncUpload = async () => { if (!githubToken) return; - setIsSyncing(true); try { const data = exportData() as { keys: SSHKey[]; @@ -405,11 +396,7 @@ const SettingsDialog: React.FC = ({ snippets: Snippet[]; customGroups: string[]; }; - const newGistId = await syncToGist( - githubToken, - gistId || undefined, - data, - ); + const newGistId = await upload(githubToken, gistId || undefined, data); if (!gistId) { setGistId(newGistId); onSyncConfigChange({ @@ -420,26 +407,21 @@ const SettingsDialog: React.FC = ({ } else { onSyncConfigChange({ ...syncConfig!, lastSync: Date.now() }); } - alert("Backup uploaded to Gist successfully!"); + toast.success("Backup uploaded to Gist successfully!"); } catch (e) { - alert("Upload failed: " + e); - } finally { - setIsSyncing(false); + toast.error(String(e), "Upload failed"); } }; const performSyncDownload = async () => { if (!githubToken || !gistId) return; - setIsSyncing(true); try { - const data = await loadFromGist(githubToken, gistId); + const data = await download(githubToken, gistId); onImport(JSON.stringify(data)); onSyncConfigChange({ ...syncConfig!, lastSync: Date.now() }); - alert("Configuration restored from Gist!"); + toast.success("Configuration restored from Gist!"); } catch (e) { - alert("Download failed: " + e); - } finally { - setIsSyncing(false); + toast.error(String(e), "Download failed"); } }; diff --git a/components/SettingsPage.tsx b/components/SettingsPage.tsx index 71615357..1a534d4a 100644 --- a/components/SettingsPage.tsx +++ b/components/SettingsPage.tsx @@ -19,6 +19,7 @@ import { X, } from "lucide-react"; import React, { useCallback, useEffect, useState } from "react"; +import { useSyncState } from "../application/state/useSyncState"; import { CursorShape, RightClickBehavior, @@ -30,10 +31,6 @@ import { } from "../domain/models"; import { TERMINAL_THEMES } from "../infrastructure/config/terminalThemes"; import { TERMINAL_FONTS, MIN_FONT_SIZE, MAX_FONT_SIZE } from "../infrastructure/config/fonts"; -import { - loadFromGist, - syncToGist, -} from "../infrastructure/services/syncService"; import { cn } from "../lib/utils"; import { Button } from "./ui/button"; import { Input } from "./ui/input"; @@ -41,8 +38,10 @@ import { Label } from "./ui/label"; import { ScrollArea } from "./ui/scroll-area"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"; import { Textarea } from "./ui/textarea"; +import { toast } from "./ui/toast"; import { useSettingsState } from "../application/state/useSettingsState"; import { useVaultState } from "../application/state/useVaultState"; +import { useWindowControls } from "../application/state/useWindowControls"; // More comprehensive color palette const COLORS = [ @@ -265,9 +264,10 @@ export default function SettingsPage() { exportData, importDataFromString, } = useVaultState(); + const { closeSettingsWindow } = useWindowControls(); // Local state - const [isSyncing, setIsSyncing] = useState(false); + const { isSyncing, upload, download } = useSyncState(); const [gistToken, setGistToken] = useState(syncConfig?.githubToken || ""); const [gistId, setGistId] = useState(syncConfig?.gistId || ""); const [importText, setImportText] = useState(""); @@ -276,8 +276,8 @@ export default function SettingsPage() { // Close window handler const handleClose = useCallback(() => { - window.netcatty?.closeSettingsWindow?.(); - }, []); + closeSettingsWindow(); + }, [closeSettingsWindow]); // Helper functions const getHslStyle = (hsl: string) => ({ backgroundColor: `hsl(${hsl})` }); @@ -370,41 +370,36 @@ export default function SettingsPage() { // Sync handlers const handleSaveGist = async () => { - if (!gistToken) return alert("Please enter a GitHub token"); + if (!gistToken) return toast.error("Please enter a GitHub token"); updateSyncConfig({ githubToken: gistToken, gistId: gistId || undefined }); - setIsSyncing(true); try { - const newId = await syncToGist( - gistToken, - gistId || undefined, - { hosts, keys, snippets, customGroups: [] } - ); + const newId = await upload(gistToken, gistId || undefined, { + hosts, + keys, + snippets, + customGroups: [], + }); if (newId && newId !== gistId) { setGistId(newId); updateSyncConfig({ githubToken: gistToken, gistId: newId }); - alert("Synced! Gist ID saved."); + toast.success("Synced! Gist ID saved."); } else { - alert("Synced successfully."); + toast.success("Synced successfully."); } } catch (e) { - alert("Sync failed: " + e); - } finally { - setIsSyncing(false); + toast.error(String(e), "Sync failed"); } }; const handleLoadGist = async () => { - if (!gistToken || !gistId) return alert("Token and Gist ID required"); - setIsSyncing(true); + if (!gistToken || !gistId) return toast.error("Token and Gist ID required"); try { - const data = await loadFromGist(gistToken, gistId); + const data = await download(gistToken, gistId); if (!data) throw new Error("No data found in Gist"); importDataFromString(JSON.stringify(data)); - alert("Loaded successfully!"); + toast.success("Loaded successfully!"); } catch (e) { - alert("Download failed: " + e); - } finally { - setIsSyncing(false); + toast.error(String(e), "Download failed"); } }; @@ -1224,9 +1219,9 @@ export default function SettingsPage() { try { importDataFromString(importText); setImportText(""); - alert("Import successful!"); + toast.success("Import successful!"); } catch (e) { - alert("Import failed: " + e); + toast.error(String(e), "Import failed"); } }} disabled={!importText.trim()} diff --git a/components/Terminal.tsx b/components/Terminal.tsx index 659dfd3f..933fc333 100644 --- a/components/Terminal.tsx +++ b/components/Terminal.tsx @@ -7,6 +7,7 @@ import { WebLinksAddon } from "@xterm/addon-web-links"; import "@xterm/xterm/css/xterm.css"; import { Maximize2 } from "lucide-react"; import React, { memo, useEffect, useMemo, useRef, useState, useCallback } from "react"; +import { logger } from "../lib/logger"; import { cn } from "../lib/utils"; import { Host, @@ -19,6 +20,7 @@ import { KeyBinding, } from "../types"; import { checkAppShortcut, getAppLevelActions, getTerminalPassthroughActions } from "../application/state/useGlobalHotkeys"; +import { useTerminalBackend } from "../application/state/useTerminalBackend"; import KnownHostConfirmDialog, { HostKeyInfo } from "./KnownHostConfirmDialog"; import SFTPModal from "./SFTPModal"; import { Button } from "./ui/button"; @@ -147,6 +149,9 @@ const TerminalComponent: React.FC = ({ keyBindingsRef.current = keyBindings; onHotkeyActionRef.current = onHotkeyAction; + const terminalBackend = useTerminalBackend(); + const { resizeSession } = terminalBackend; + const [isScriptsOpen, setIsScriptsOpen] = useState(false); const [status, setStatus] = useState("connecting"); const [error, setError] = useState(null); @@ -221,11 +226,11 @@ const TerminalComponent: React.FC = ({ disposeExitRef.current?.(); disposeExitRef.current = null; - if (sessionRef.current && window.netcatty?.closeSession) { + if (sessionRef.current) { try { - window.netcatty.closeSession(sessionRef.current); + terminalBackend.closeSession(sessionRef.current); } catch (err) { - console.warn("Failed to close SSH session", err); + logger.warn("Failed to close SSH session", err); } } sessionRef.current = null; @@ -244,9 +249,9 @@ const TerminalComponent: React.FC = ({ }; const runDistroDetection = async (key?: SSHKey) => { - if (!window.netcatty?.execCommand) return; + if (!terminalBackend.execAvailable()) return; try { - const res = await window.netcatty.execCommand({ + const res = await terminalBackend.execCommand({ hostname: host.hostname, username: host.username || "root", port: host.port || 22, @@ -261,10 +266,10 @@ const TerminalComponent: React.FC = ({ ? idMatch[1].replace(/"/g, "") : (data.split(/\\s+/)[0] || "").toLowerCase(); if (distro) onOsDetected?.(host.id, distro); - } catch (err) { - console.warn("OS probe failed", err); - } - }; + } catch (err) { + logger.warn("OS probe failed", err); + } + }; useEffect(() => { let disposed = false; @@ -390,7 +395,7 @@ const TerminalComponent: React.FC = ({ ? "canvas" : rendererName : "unknown"; - console.info(`[XTerm] renderer=${normalized}`); + logger.info(`[XTerm] renderer=${normalized}`); const scopedWindow = window as Window & { __xtermRenderer?: string }; scopedWindow.__xtermRenderer = normalized; if (normalized === "unknown" && attempt < 3) { @@ -440,20 +445,22 @@ const TerminalComponent: React.FC = ({ } })(); webglAddon.onContextLoss(() => { - console.warn("[XTerm] WebGL context loss detected, disposing addon"); + logger.warn("[XTerm] WebGL context loss detected, disposing addon"); webglAddon.dispose(); }); term.loadAddon(webglAddon); webglLoaded = true; } catch (webglErr) { - console.warn( + logger.warn( "[XTerm] WebGL addon failed, using canvas renderer. Error:", webglErr instanceof Error ? webglErr.message : webglErr, ); // Canvas renderer will be used as fallback - it's actually faster on some Macs } } else { - console.info("[XTerm] Skipping WebGL addon (canvas preferred for macOS profile or low-memory devices)"); + logger.info( + "[XTerm] Skipping WebGL addon (canvas preferred for macOS profile or low-memory devices)", + ); } // Store whether WebGL was successfully loaded for diagnostics @@ -481,8 +488,8 @@ const TerminalComponent: React.FC = ({ } if (shouldOpen) { // Open URL in default browser - if (window.netcatty?.openExternal) { - window.netcatty.openExternal(uri); + if (terminalBackend.openExternalAvailable()) { + void terminalBackend.openExternal(uri); } else { window.open(uri, '_blank'); } @@ -518,28 +525,14 @@ const TerminalComponent: React.FC = ({ const isMac = currentScheme === 'mac'; - // Debug: log arrow key combinations - if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key) && (e.ctrlKey || e.metaKey)) { - console.log('[Terminal] Arrow key combo detected:', { - key: e.key, - ctrlKey: e.ctrlKey, - altKey: e.altKey, - metaKey: e.metaKey, - scheme: currentScheme, - bindingsCount: currentBindings.length, - }); - } - // Check if this matches any of our shortcuts const matched = checkAppShortcut(e, currentBindings, isMac); if (!matched) return true; // Let xterm handle it - console.log('[Terminal] Matched hotkey:', matched.action); const { action } = matched; // App-level actions: call the callback directly and prevent xterm from handling if (appLevelActions.has(action)) { - console.log('[Terminal] Executing app-level action:', action); e.preventDefault(); if (hotkeyCallback) { hotkeyCallback(action, e); @@ -561,9 +554,7 @@ const TerminalComponent: React.FC = ({ case 'paste': { navigator.clipboard.readText().then((text) => { const id = sessionRef.current; - if (id && window.netcatty?.writeToSession) { - window.netcatty.writeToSession(id, text); - } + if (id) terminalBackend.writeToSession(id, text); }); break; } @@ -593,16 +584,16 @@ const TerminalComponent: React.FC = ({ const handleMiddleClick = async (e: MouseEvent) => { if (e.button === 1) { // Middle mouse button e.preventDefault(); - try { - const text = await navigator.clipboard.readText(); - if (text && sessionRef.current && window.netcatty?.writeToSession) { - window.netcatty.writeToSession(sessionRef.current, text); - } - } catch (err) { - console.warn('[Terminal] Failed to paste from clipboard:', err); - } - } - }; + try { + const text = await navigator.clipboard.readText(); + if (text && sessionRef.current) { + terminalBackend.writeToSession(sessionRef.current, text); + } + } catch (err) { + logger.warn('[Terminal] Failed to paste from clipboard:', err); + } + } + }; containerRef.current.addEventListener('auxclick', handleMiddleClick); // Store cleanup function const container = containerRef.current; @@ -617,15 +608,15 @@ const TerminalComponent: React.FC = ({ fitAddon.fit(); term.focus(); - } catch (openErr) { - console.error("[XTerm] Failed to open terminal:", openErr); - throw openErr; - } + } catch (openErr) { + logger.error("[XTerm] Failed to open terminal:", openErr); + throw openErr; + } - term.onData((data) => { - const id = sessionRef.current; - if (id && window.netcatty?.writeToSession) { - window.netcatty.writeToSession(id, data); + term.onData((data) => { + const id = sessionRef.current; + if (id) { + terminalBackend.writeToSession(id, data); // Track command input for shell history if (status === "connected" && onCommandExecuted) { @@ -663,19 +654,19 @@ const TerminalComponent: React.FC = ({ // Add debouncing for resize events to prevent excessive calls on macOS let resizeTimeout: NodeJS.Timeout | null = null; const resizeDebounceMs = XTERM_PERFORMANCE_CONFIG.resize.debounceMs; - term.onResize(({ cols, rows }) => { - const id = sessionRef.current; - if (id && window.netcatty?.resizeSession) { - // Debounce resize to prevent rapid successive calls - if (resizeTimeout) { - clearTimeout(resizeTimeout); - } - resizeTimeout = setTimeout(() => { - window.netcatty.resizeSession(id, cols, rows); - resizeTimeout = null; - }, resizeDebounceMs); // Debounce for smooth resizing on macOS - } - }); + term.onResize(({ cols, rows }) => { + const id = sessionRef.current; + if (id) { + // Debounce resize to prevent rapid successive calls + if (resizeTimeout) { + clearTimeout(resizeTimeout); + } + resizeTimeout = setTimeout(() => { + terminalBackend.resizeSession(id, cols, rows); + resizeTimeout = null; + }, resizeDebounceMs); // Debounce for smooth resizing on macOS + } + }); if (host.protocol === "local" || host.hostname === "localhost") { setStatus("connecting"); @@ -709,7 +700,7 @@ const TerminalComponent: React.FC = ({ await startSSH(term); } } catch (err) { - console.error("Failed to initialize terminal", err); + logger.error("Failed to initialize terminal", err); setError(err instanceof Error ? err.message : String(err)); updateStatus("disconnected"); } @@ -784,7 +775,7 @@ const TerminalComponent: React.FC = ({ try { fitAddon.fit(); } catch (err) { - console.warn("Fit failed", err); + logger.warn("Fit failed", err); } }; @@ -889,33 +880,33 @@ const TerminalComponent: React.FC = ({ try { term?.renderer?.remeasureFont?.(); } catch (err) { - console.warn("Font remeasure failed", err); + logger.warn("Font remeasure failed", err); } - try { - fitAddon?.fit(); - } catch (err) { - console.warn("Fit after fonts ready failed", err); - } + try { + fitAddon?.fit(); + } catch (err) { + logger.warn("Fit after fonts ready failed", err); + } - const id = sessionRef.current; - if (id && term && window.netcatty?.resizeSession) { - try { - window.netcatty.resizeSession(id, term.cols, term.rows); - } catch (err) { - console.warn("Resize session after fonts ready failed", err); - } - } - } catch (err) { - console.warn("Waiting for fonts failed", err); - } - }; + const id = sessionRef.current; + if (id && term) { + try { + resizeSession(id, term.cols, term.rows); + } catch (err) { + logger.warn("Resize session after fonts ready failed", err); + } + } + } catch (err) { + logger.warn("Waiting for fonts failed", err); + } + }; waitForFonts(); - return () => { - cancelled = true; - }; - }, [host.id, sessionId]); + return () => { + cancelled = true; + }; + }, [host.id, sessionId, resizeSession]); // Debounced fit for resize operations - only fit when not actively resizing useEffect(() => { @@ -987,7 +978,6 @@ const TerminalComponent: React.FC = ({ // Small delay to ensure state updates complete const timer = setTimeout(() => { termRef.current?.focus(); - console.log('[Terminal] Focus triggered via isFocused prop, sessionId:', sessionId.slice(0, 8)); }, 10); return () => clearTimeout(timer); } @@ -1006,7 +996,7 @@ const TerminalComponent: React.FC = ({ // Copy on select if enabled if (hasText && terminalSettings?.copyOnSelect) { navigator.clipboard.writeText(selection).catch(err => { - console.warn('Copy on select failed:', err); + logger.warn('Copy on select failed:', err); }); } }; @@ -1040,10 +1030,10 @@ const TerminalComponent: React.FC = ({ try { term.clear?.(); } catch (err) { - console.warn("Failed to clear terminal before connect", err); + logger.warn("Failed to clear terminal before connect", err); } - if (!window.netcatty?.startSSHSession) { + if (!terminalBackend.backendAvailable()) { setError("Native SSH bridge unavailable. Launch via Electron app."); term.writeln( "\r\n[netcatty SSH bridge unavailable. Please run the desktop build to connect.]", @@ -1106,8 +1096,8 @@ const TerminalComponent: React.FC = ({ ]); // Subscribe to chain progress events from IPC - if (window.netcatty?.onChainProgress) { - unsubscribeChainProgress = window.netcatty.onChainProgress( + { + const unsub = terminalBackend.onChainProgress( (hop, total, label, status) => { setChainProgress({ currentHop: hop, @@ -1123,6 +1113,7 @@ const TerminalComponent: React.FC = ({ setProgressValue(Math.min(95, hopProgress)); }, ); + if (unsub) unsubscribeChainProgress = unsub; } } @@ -1139,7 +1130,7 @@ const TerminalComponent: React.FC = ({ } } - const id = await window.netcatty.startSSHSession({ + const id = await terminalBackend.startSSHSession({ sessionId, hostname: host.hostname, username: effectiveUsername, @@ -1165,7 +1156,7 @@ const TerminalComponent: React.FC = ({ sessionRef.current = id; - disposeDataRef.current = window.netcatty.onSessionData(id, (chunk) => { + disposeDataRef.current = terminalBackend.onSessionData(id, (chunk) => { // Apply keyword highlighting before writing to terminal term.write(highlightProcessorRef.current(chunk)); if (!hasConnectedRef.current) { @@ -1177,22 +1168,18 @@ const TerminalComponent: React.FC = ({ try { fitAddonRef.current.fit(); // Send updated size to remote - if (sessionRef.current && window.netcatty?.resizeSession) { - window.netcatty.resizeSession( - sessionRef.current, - term.cols, - term.rows, - ); + if (sessionRef.current) { + terminalBackend.resizeSession(sessionRef.current, term.cols, term.rows); } } catch (err) { - console.warn("Post-connect fit failed", err); + logger.warn("Post-connect fit failed", err); } } }, 100); } }); - disposeExitRef.current = window.netcatty.onSessionExit(id, (evt) => { + disposeExitRef.current = terminalBackend.onSessionExit(id, (evt) => { updateStatus("disconnected"); setChainProgress(null); // Clear chain progress on disconnect term.writeln( @@ -1207,10 +1194,7 @@ const TerminalComponent: React.FC = ({ hasRunStartupCommandRef.current = true; setTimeout(() => { if (sessionRef.current) { - window.netcatty?.writeToSession( - sessionRef.current, - `${commandToRun}\r`, - ); + terminalBackend.writeToSession(sessionRef.current, `${commandToRun}\r`); // Track startup command execution in shell history if (onCommandExecuted) { onCommandExecuted(commandToRun, host.id, host.label, sessionId); @@ -1263,11 +1247,10 @@ const TerminalComponent: React.FC = ({ try { term.clear?.(); } catch (err) { - console.warn("Failed to clear terminal before connect", err); + logger.warn("Failed to clear terminal before connect", err); } - const startTelnetSession = window.netcatty?.startTelnetSession; - if (!startTelnetSession) { + if (!terminalBackend.telnetAvailable()) { setError("Telnet bridge unavailable. Please run the desktop build."); term.writeln( "\r\n[Telnet bridge unavailable. Please run the desktop build.]", @@ -1289,7 +1272,7 @@ const TerminalComponent: React.FC = ({ } } - const id = await startTelnetSession({ + const id = await terminalBackend.startTelnetSession({ sessionId, hostname: host.hostname, port: host.telnetPort || host.port || 23, @@ -1301,7 +1284,7 @@ const TerminalComponent: React.FC = ({ sessionRef.current = id; - disposeDataRef.current = window.netcatty?.onSessionData(id, (chunk) => { + disposeDataRef.current = terminalBackend.onSessionData(id, (chunk) => { // Apply keyword highlighting before writing to terminal term.write(highlightProcessorRef.current(chunk)); if (!hasConnectedRef.current) { @@ -1310,22 +1293,18 @@ const TerminalComponent: React.FC = ({ if (fitAddonRef.current) { try { fitAddonRef.current.fit(); - if (sessionRef.current && window.netcatty?.resizeSession) { - window.netcatty.resizeSession( - sessionRef.current, - term.cols, - term.rows, - ); + if (sessionRef.current) { + terminalBackend.resizeSession(sessionRef.current, term.cols, term.rows); } } catch (err) { - console.warn("Post-connect fit failed", err); + logger.warn("Post-connect fit failed", err); } } }, 100); } }); - disposeExitRef.current = window.netcatty?.onSessionExit(id, (evt) => { + disposeExitRef.current = terminalBackend.onSessionExit(id, (evt) => { updateStatus("disconnected"); term.writeln( `\r\n[Telnet session closed${evt?.exitCode !== undefined ? ` (code ${evt.exitCode})` : ""}]`, @@ -1344,11 +1323,10 @@ const TerminalComponent: React.FC = ({ try { term.clear?.(); } catch (err) { - console.warn("Failed to clear terminal before connect", err); + logger.warn("Failed to clear terminal before connect", err); } - const startMoshSession = window.netcatty?.startMoshSession; - if (!startMoshSession) { + if (!terminalBackend.moshAvailable()) { setError("Mosh bridge unavailable. Please run the desktop build."); term.writeln( "\r\n[Mosh bridge unavailable. Please run the desktop build.]", @@ -1370,7 +1348,7 @@ const TerminalComponent: React.FC = ({ } } - const id = await startMoshSession({ + const id = await terminalBackend.startMoshSession({ sessionId, hostname: host.hostname, username: host.username || "root", @@ -1385,7 +1363,7 @@ const TerminalComponent: React.FC = ({ sessionRef.current = id; - disposeDataRef.current = window.netcatty?.onSessionData(id, (chunk) => { + disposeDataRef.current = terminalBackend.onSessionData(id, (chunk) => { // Apply keyword highlighting before writing to terminal term.write(highlightProcessorRef.current(chunk)); if (!hasConnectedRef.current) { @@ -1394,22 +1372,18 @@ const TerminalComponent: React.FC = ({ if (fitAddonRef.current) { try { fitAddonRef.current.fit(); - if (sessionRef.current && window.netcatty?.resizeSession) { - window.netcatty.resizeSession( - sessionRef.current, - term.cols, - term.rows, - ); + if (sessionRef.current) { + terminalBackend.resizeSession(sessionRef.current, term.cols, term.rows); } } catch (err) { - console.warn("Post-connect fit failed", err); + logger.warn("Post-connect fit failed", err); } } }, 100); } }); - disposeExitRef.current = window.netcatty?.onSessionExit(id, (evt) => { + disposeExitRef.current = terminalBackend.onSessionExit(id, (evt) => { updateStatus("disconnected"); term.writeln( `\r\n[Mosh session closed${evt?.exitCode !== undefined ? ` (code ${evt.exitCode})` : ""}]`, @@ -1423,10 +1397,7 @@ const TerminalComponent: React.FC = ({ hasRunStartupCommandRef.current = true; setTimeout(() => { if (sessionRef.current) { - window.netcatty?.writeToSession( - sessionRef.current, - `${commandToRun}\r`, - ); + terminalBackend.writeToSession(sessionRef.current, `${commandToRun}\r`); if (onCommandExecuted) { onCommandExecuted(commandToRun, host.id, host.label, sessionId); } @@ -1445,11 +1416,10 @@ const TerminalComponent: React.FC = ({ try { term.clear?.(); } catch (err) { - console.warn("Failed to clear terminal before connect", err); + logger.warn("Failed to clear terminal before connect", err); } - const startLocalSession = window.netcatty?.startLocalSession; - if (!startLocalSession) { + if (!terminalBackend.localAvailable()) { setError("Local shell bridge unavailable. Please run the desktop build."); term.writeln( "\r\n[Local shell bridge unavailable. Please run the desktop build to spawn a local terminal.]", @@ -1459,7 +1429,7 @@ const TerminalComponent: React.FC = ({ } try { - const id = await startLocalSession({ + const id = await terminalBackend.startLocalSession({ sessionId, cols: term.cols, rows: term.rows, @@ -1468,7 +1438,7 @@ const TerminalComponent: React.FC = ({ }, }); sessionRef.current = id; - disposeDataRef.current = window.netcatty?.onSessionData(id, (chunk) => { + disposeDataRef.current = terminalBackend.onSessionData(id, (chunk) => { // Apply keyword highlighting before writing to terminal term.write(highlightProcessorRef.current(chunk)); if (!hasConnectedRef.current) { @@ -1479,21 +1449,17 @@ const TerminalComponent: React.FC = ({ try { fitAddonRef.current.fit(); // Send updated size to remote - if (sessionRef.current && window.netcatty?.resizeSession) { - window.netcatty.resizeSession( - sessionRef.current, - term.cols, - term.rows, - ); + if (sessionRef.current) { + terminalBackend.resizeSession(sessionRef.current, term.cols, term.rows); } } catch (err) { - console.warn("Post-connect fit failed", err); + logger.warn("Post-connect fit failed", err); } } }, 100); } }); - disposeExitRef.current = window.netcatty?.onSessionExit(id, (evt) => { + disposeExitRef.current = terminalBackend.onSessionExit(id, (evt) => { updateStatus("disconnected"); term.writeln( `\r\n[session closed${evt?.exitCode !== undefined ? ` (code ${evt.exitCode})` : ""}]`, @@ -1509,8 +1475,8 @@ const TerminalComponent: React.FC = ({ }; const handleSnippetClick = (cmd: string) => { - if (sessionRef.current && window.netcatty?.writeToSession) { - window.netcatty.writeToSession(sessionRef.current, `${cmd}\r`); + if (sessionRef.current) { + terminalBackend.writeToSession(sessionRef.current, `${cmd}\r`); setIsScriptsOpen(false); termRef.current?.focus(); return; @@ -1631,7 +1597,7 @@ const TerminalComponent: React.FC = ({ try { termRef.current.clear?.(); } catch (err) { - console.warn("Failed to clear terminal", err); + logger.warn("Failed to clear terminal", err); } startSSH(termRef.current); } @@ -1647,18 +1613,16 @@ const TerminalComponent: React.FC = ({ } }; - const handleContextPaste = async () => { - const term = termRef.current; - if (!term) return; - try { - const text = await navigator.clipboard.readText(); - if (text && sessionRef.current && window.netcatty?.writeToSession) { - window.netcatty.writeToSession(sessionRef.current, text); - } - } catch (err) { - console.warn("Failed to paste from clipboard", err); - } - }; + const handleContextPaste = async () => { + const term = termRef.current; + if (!term) return; + try { + const text = await navigator.clipboard.readText(); + if (text && sessionRef.current) terminalBackend.writeToSession(sessionRef.current, text); + } catch (err) { + logger.warn("Failed to paste from clipboard", err); + } + }; const handleContextSelectAll = () => { const term = termRef.current; diff --git a/components/TerminalLayer.tsx b/components/TerminalLayer.tsx index cf16c9af..136343c7 100644 --- a/components/TerminalLayer.tsx +++ b/components/TerminalLayer.tsx @@ -387,9 +387,6 @@ const TerminalLayerInner: React.FC = ({ const isFocusMode = activeWorkspace?.viewMode === 'focus'; const focusedSessionId = activeWorkspace?.focusedSessionId; - // Debug log for focus tracking - console.log('[TerminalLayer] focusedSessionId:', focusedSessionId?.slice(0, 8), 'isFocusMode:', isFocusMode); - // Track previous focusedSessionId to detect changes const prevFocusedSessionIdRef = useRef(undefined); @@ -421,7 +418,6 @@ const TerminalLayerInner: React.FC = ({ const textarea = targetPane.querySelector('textarea.xterm-helper-textarea') as HTMLTextAreaElement | null; if (textarea) { textarea.focus(); - console.log('[TerminalLayer] Direct DOM focus on session:', focusedSessionId.slice(0, 8)); } } }; diff --git a/components/TopTabs.tsx b/components/TopTabs.tsx index 3d2a61d2..395c13e0 100644 --- a/components/TopTabs.tsx +++ b/components/TopTabs.tsx @@ -1,6 +1,7 @@ -import { Bell, Copy, Folder, LayoutGrid, Minus, Moon, MoreHorizontal, Plus, Shield, Square, Sun, TerminalSquare, User, X } from 'lucide-react'; +import { Bell, Copy, Folder, LayoutGrid, Minus, Moon, MoreHorizontal, Plus, Shield, Square, Sun, TerminalSquare, User, X } from 'lucide-react'; import React, { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { activeTabStore, useActiveTabId } from '../application/state/activeTabStore'; +import { useWindowControls } from '../application/state/useWindowControls'; import { cn } from '../lib/utils'; import { TerminalSession, Workspace } from '../types'; import { Button } from './ui/button'; @@ -39,31 +40,32 @@ const sessionStatusDot = (status: TerminalSession['status']) => { // Custom window controls for Windows/Linux (frameless window) const WindowControls: React.FC = memo(() => { + const { minimize, maximize, close, isMaximized: fetchIsMaximized } = useWindowControls(); const [isMaximized, setIsMaximized] = useState(false); useEffect(() => { // Check initial maximized state - window.netcatty?.windowIsMaximized?.().then(setIsMaximized); + fetchIsMaximized().then(v => setIsMaximized(!!v)); // Listen for window resize to update maximized state const handleResize = () => { - window.netcatty?.windowIsMaximized?.().then(setIsMaximized); + fetchIsMaximized().then(v => setIsMaximized(!!v)); }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); - }, []); + }, [fetchIsMaximized]); const handleMinimize = () => { - window.netcatty?.windowMinimize?.(); + minimize(); }; const handleMaximize = async () => { - const result = await window.netcatty?.windowMaximize?.(); - setIsMaximized(result ?? false); + const result = await maximize(); + setIsMaximized(!!result); }; const handleClose = () => { - window.netcatty?.windowClose?.(); + close(); }; return ( @@ -118,6 +120,7 @@ const TopTabsInner: React.FC = ({ onReorderTabs, }) => { // Subscribe to activeTabId from external store + const { maximize } = useWindowControls(); const activeTabId = useActiveTabId(); const isVaultActive = activeTabId === 'vault'; const isSftpActive = activeTabId === 'sftp'; @@ -436,9 +439,9 @@ const TopTabsInner: React.FC = ({ // Only handle double-click on the drag region itself, not on buttons/tabs if ((e.target as HTMLElement).closest('.app-no-drag')) return; if (!isMacClient) { - window.netcatty?.windowMaximize?.(); + maximize(); } - }, [isMacClient]); + }, [isMacClient, maximize]); return (
= ({ exportHost, _setExportHost, // Host selection handled by onShowHostSelector callback onShowHostSelector, - onSaveHost, - onClose, + onSaveHost, + onClose, }) => { - const [exportLocation, setExportLocation] = useState('.ssh'); - const [exportFilename, setExportFilename] = useState('authorized_keys'); - const [exportAdvancedOpen, setExportAdvancedOpen] = useState(false); - const [exportScript, setExportScript] = useState(DEFAULT_EXPORT_SCRIPT); - const [isExporting, setIsExporting] = useState(false); + const { execCommand } = useKeychainBackend(); + const [exportLocation, setExportLocation] = useState('.ssh'); + const [exportFilename, setExportFilename] = useState('authorized_keys'); + const [exportAdvancedOpen, setExportAdvancedOpen] = useState(false); + const [exportScript, setExportScript] = useState(DEFAULT_EXPORT_SCRIPT); + const [isExporting, setIsExporting] = useState(false); const isMac = isMacOS(); @@ -80,14 +82,14 @@ export const ExportKeyPanel: React.FC = ({ .replace(/\$2/g, exportFilename) .replace(/\$3/g, `'${escapedPublicKey}'`); - const command = scriptWithVars; + const command = scriptWithVars; - // Execute via SSH - const result = await window.netcatty?.execCommand({ - hostname: exportHost.hostname, - username: exportHost.username, - port: exportHost.port || 22, - password: exportHost.password, + // Execute via SSH + const result = await execCommand({ + hostname: exportHost.hostname, + username: exportHost.username, + port: exportHost.port || 22, + password: exportHost.password, privateKey: hostPrivateKey, command, timeout: 30000, diff --git a/components/keychain/utils.ts b/components/keychain/utils.ts index 1b6d7995..15a95035 100644 --- a/components/keychain/utils.ts +++ b/components/keychain/utils.ts @@ -4,6 +4,7 @@ import { BadgeCheck,Fingerprint,Key,Shield } from 'lucide-react'; import React from 'react'; +import { logger } from '../../lib/logger'; import { KeyType,SSHKey } from '../../types'; /** @@ -109,7 +110,7 @@ export const createFido2Credential = async (label: string): Promise<{ rpId, }; } catch (error) { - console.error('FIDO2 credential creation failed:', error); + logger.error('FIDO2 credential creation failed:', error); throw error; } }; @@ -200,7 +201,7 @@ export const createBiometricCredential = async (label: string): Promise<{ rpId, }; } catch (error) { - console.error('WebAuthn credential creation failed:', error); + logger.error('WebAuthn credential creation failed:', error); throw error; } }; @@ -242,7 +243,7 @@ export const copyToClipboard = async (text: string): Promise => { await navigator.clipboard.writeText(text); return true; } catch (err) { - console.error('Failed to copy to clipboard:', err); + logger.error('Failed to copy to clipboard:', err); return false; } }; diff --git a/domain/workspace.ts b/domain/workspace.ts index 99e05975..428856ac 100644 --- a/domain/workspace.ts +++ b/domain/workspace.ts @@ -188,12 +188,12 @@ export const collectSessionIds = (node: WorkspaceNode): string[] => { /** * Find a pane node by session ID in the workspace tree. */ -const findPaneBySessionId = (node: WorkspaceNode, sessionId: string): WorkspaceNode | null => { +const _findPaneBySessionId = (node: WorkspaceNode, sessionId: string): WorkspaceNode | null => { if (node.type === 'pane') { return node.sessionId === sessionId ? node : null; } for (const child of node.children) { - const found = findPaneBySessionId(child, sessionId); + const found = _findPaneBySessionId(child, sessionId); if (found) return found; } return null; @@ -203,12 +203,12 @@ const findPaneBySessionId = (node: WorkspaceNode, sessionId: string): WorkspaceN * Get the path to a session in the workspace tree. * Returns an array of indices representing the path from root to the pane. */ -const getPathToSession = (node: WorkspaceNode, sessionId: string, path: number[] = []): number[] | null => { +const _getPathToSession = (node: WorkspaceNode, sessionId: string, path: number[] = []): number[] | null => { if (node.type === 'pane') { return node.sessionId === sessionId ? path : null; } for (let i = 0; i < node.children.length; i++) { - const result = getPathToSession(node.children[i], sessionId, [...path, i]); + const result = _getPathToSession(node.children[i], sessionId, [...path, i]); if (result) return result; } return null; @@ -286,17 +286,11 @@ export const getNextFocusSessionId = ( direction: FocusDirection ): string | null => { const positions = collectPanePositions(root); - console.log('[getNextFocusSessionId] All positions:'); - positions.forEach((p, i) => { - console.log(` [${i}] sessionId: ${p.sessionId.slice(0, 8)}..., x: ${p.x}, y: ${p.y}, w: ${p.width}, h: ${p.height}`); - }); const current = positions.find(p => p.sessionId === currentSessionId); if (!current) { - console.log('[getNextFocusSessionId] Current session not found in positions'); return null; } - console.log('[getNextFocusSessionId] Current pane:', current.sessionId.slice(0, 8), 'at x:', current.x, 'y:', current.y, 'w:', current.width, 'h:', current.height); // Filter candidates based on direction let candidates: PanePosition[] = []; @@ -349,11 +343,6 @@ export const getNextFocusSessionId = ( break; } - console.log('[getNextFocusSessionId] Direction:', direction, 'Candidates count:', candidates.length, '(wraparound enabled)'); - candidates.forEach((c, i) => { - console.log(` Candidate[${i}]: ${c.sessionId.slice(0, 8)}... at x: ${c.x}, y: ${c.y}`); - }); - if (candidates.length === 0) return null; // Calculate center point of current pane for scoring @@ -400,6 +389,5 @@ export const getNextFocusSessionId = ( } } - console.log('[getNextFocusSessionId] Best candidate:', best?.sessionId?.slice(0, 8)); return best?.sessionId || null; }; diff --git a/electron/bridges/localFsBridge.cjs b/electron/bridges/localFsBridge.cjs index fa2dc6e1..a02429cf 100644 --- a/electron/bridges/localFsBridge.cjs +++ b/electron/bridges/localFsBridge.cjs @@ -1,4 +1,4 @@ -/** +/** * Local Filesystem Bridge - Handles local file operations * Extracted from main.cjs for single responsibility */ diff --git a/electron/bridges/portForwardingBridge.cjs b/electron/bridges/portForwardingBridge.cjs index d44233c0..b766e396 100644 --- a/electron/bridges/portForwardingBridge.cjs +++ b/electron/bridges/portForwardingBridge.cjs @@ -1,4 +1,4 @@ -/** +/** * Port Forwarding Bridge - Handles SSH port forwarding tunnels * Extracted from main.cjs for single responsibility */ diff --git a/electron/bridges/sftpBridge.cjs b/electron/bridges/sftpBridge.cjs index 1c26e314..224dc203 100644 --- a/electron/bridges/sftpBridge.cjs +++ b/electron/bridges/sftpBridge.cjs @@ -1,4 +1,4 @@ -/** +/** * SFTP Bridge - Handles SFTP connections and file operations * Extracted from main.cjs for single responsibility */ diff --git a/electron/bridges/sshBridge.cjs b/electron/bridges/sshBridge.cjs index 043c9c4e..03a880c5 100644 --- a/electron/bridges/sshBridge.cjs +++ b/electron/bridges/sshBridge.cjs @@ -1,4 +1,4 @@ -/** +/** * SSH Bridge - Handles SSH connections, sessions, and related operations * Extracted from main.cjs for single responsibility */ diff --git a/electron/bridges/terminalBridge.cjs b/electron/bridges/terminalBridge.cjs index 2654ea59..9fbe4695 100644 --- a/electron/bridges/terminalBridge.cjs +++ b/electron/bridges/terminalBridge.cjs @@ -1,4 +1,4 @@ -/** +/** * Terminal Bridge - Handles local shell and telnet/mosh sessions * Extracted from main.cjs for single responsibility */ diff --git a/electron/bridges/transferBridge.cjs b/electron/bridges/transferBridge.cjs index 3e08359d..779133ae 100644 --- a/electron/bridges/transferBridge.cjs +++ b/electron/bridges/transferBridge.cjs @@ -1,4 +1,4 @@ -/** +/** * Transfer Bridge - Handles file transfers with progress and cancellation * Extracted from main.cjs for single responsibility */ diff --git a/electron/bridges/windowManager.cjs b/electron/bridges/windowManager.cjs index cb75c00b..308995d7 100644 --- a/electron/bridges/windowManager.cjs +++ b/electron/bridges/windowManager.cjs @@ -1,4 +1,4 @@ -/** +/** * Window Manager - Handles Electron window creation and management * Extracted from main.cjs for single responsibility */ diff --git a/electron/preload.cjs b/electron/preload.cjs index 7eeb3aa2..0011923b 100644 --- a/electron/preload.cjs +++ b/electron/preload.cjs @@ -1,4 +1,4 @@ -const { ipcRenderer, contextBridge } = require("electron"); +const { ipcRenderer, contextBridge } = require("electron"); const dataListeners = new Map(); const exitListeners = new Map(); diff --git a/eslint.config.js b/eslint.config.js index ef30efdb..33b9ba53 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -119,4 +119,53 @@ export default [ "no-case-declarations": "warn", }, }, + { + files: ["**/*.{ts,tsx}"], + rules: { + "no-restricted-properties": [ + "error", + { + object: "window", + property: "netcatty", + message: + "Do not access window.netcatty directly; use netcattyBridge or an application/state backend hook.", + }, + ], + "no-restricted-globals": ["error", "localStorage", "sessionStorage"], + }, + }, + { + files: ["infrastructure/services/netcattyBridge.ts"], + rules: { + "no-restricted-properties": "off", + }, + }, + { + files: ["infrastructure/persistence/localStorageAdapter.ts"], + rules: { + "no-restricted-globals": "off", + }, + }, + { + files: ["components/**/*.{ts,tsx}"], + rules: { + "no-restricted-imports": [ + "error", + { + patterns: [ + { + group: [ + "../infrastructure/persistence/*", + "../infrastructure/services/*", + "../../infrastructure/persistence/*", + "../../infrastructure/services/*", + ], + message: + "Components should not import infrastructure persistence/services; use application/state hooks instead.", + }, + ], + }, + ], + }, + }, ]; diff --git a/index.html b/index.html index 290bab54..39463576 100755 --- a/index.html +++ b/index.html @@ -104,45 +104,13 @@ -ms-overflow-style: none; scrollbar-width: none; } - - - - - + + +
- \ No newline at end of file + diff --git a/infrastructure/config/storageKeys.ts b/infrastructure/config/storageKeys.ts index 6f795387..754dc687 100644 --- a/infrastructure/config/storageKeys.ts +++ b/infrastructure/config/storageKeys.ts @@ -15,5 +15,6 @@ export const STORAGE_KEY_HOTKEY_SCHEME = 'netcatty_hotkey_scheme_v1'; export const STORAGE_KEY_CUSTOM_KEY_BINDINGS = 'netcatty_custom_key_bindings_v1'; export const STORAGE_KEY_CUSTOM_CSS = 'netcatty_custom_css_v1'; export const STORAGE_KEY_PORT_FORWARDING = 'netcatty_port_forwarding_v1'; +export const STORAGE_KEY_PF_PREFER_FORM_MODE = 'netcatty_pf_prefer_form_mode_v1'; export const STORAGE_KEY_KNOWN_HOSTS = 'netcatty_known_hosts_v1'; export const STORAGE_KEY_SHELL_HISTORY = 'netcatty_shell_history_v1'; diff --git a/infrastructure/persistence/localStorageAdapter.ts b/infrastructure/persistence/localStorageAdapter.ts index aee423a0..f5b5fe3e 100644 --- a/infrastructure/persistence/localStorageAdapter.ts +++ b/infrastructure/persistence/localStorageAdapter.ts @@ -20,6 +20,16 @@ export const localStorageAdapter = { writeString(key: string, value: string) { localStorage.setItem(key, value); }, + readBoolean(key: string): boolean | null { + const value = localStorage.getItem(key); + if (value === null) return null; + if (value === "true") return true; + if (value === "false") return false; + return null; + }, + writeBoolean(key: string, value: boolean) { + localStorage.setItem(key, value ? "true" : "false"); + }, readNumber(key: string): number | null { const value = localStorage.getItem(key); if (!value) return null; diff --git a/infrastructure/services/netcattyBridge.ts b/infrastructure/services/netcattyBridge.ts new file mode 100644 index 00000000..17d80431 --- /dev/null +++ b/infrastructure/services/netcattyBridge.ts @@ -0,0 +1,19 @@ +export class BridgeUnavailableError extends Error { + constructor(message = "Netcatty bridge unavailable") { + super(message); + this.name = "BridgeUnavailableError"; + } +} + +export const netcattyBridge = { + get(): NetcattyBridge | undefined { + return window.netcatty; + }, + + require(): NetcattyBridge { + const bridge = window.netcatty; + if (!bridge) throw new BridgeUnavailableError(); + return bridge; + }, +}; + diff --git a/infrastructure/services/portForwardingService.ts b/infrastructure/services/portForwardingService.ts index 147c2194..2f761f4f 100644 --- a/infrastructure/services/portForwardingService.ts +++ b/infrastructure/services/portForwardingService.ts @@ -1,10 +1,12 @@ -/** +/** * Port Forwarding Service * Handles communication between the frontend and the Electron backend * for establishing and managing SSH port forwarding tunnels. */ import { Host,PortForwardingRule } from '../../domain/models'; +import { logger } from '../../lib/logger'; +import { netcattyBridge } from './netcattyBridge'; export interface PortForwardingConnection { ruleId: string; @@ -42,11 +44,11 @@ export const startPortForward = async ( keys: { id: string; privateKey: string }[], onStatusChange: (status: PortForwardingRule['status'], error?: string) => void ): Promise<{ success: boolean; error?: string }> => { - const bridge = window.netcatty; + const bridge = netcattyBridge.get(); if (!bridge?.startPortForward) { // Fallback for browser/dev mode - simulate the connection - console.warn('[PortForwardingService] Backend not available, simulating connection...'); + logger.warn('[PortForwardingService] Backend not available, simulating connection...'); return simulateConnection(rule, onStatusChange); } @@ -122,7 +124,7 @@ export const stopPortForward = async ( ruleId: string, onStatusChange: (status: PortForwardingRule['status']) => void ): Promise<{ success: boolean; error?: string }> => { - const bridge = window.netcatty; + const bridge = netcattyBridge.get(); const conn = activeConnections.get(ruleId); if (!conn) { @@ -132,7 +134,7 @@ export const stopPortForward = async ( if (!bridge?.stopPortForward) { // Fallback for browser/dev mode - console.warn('[PortForwardingService] Backend not available, simulating stop...'); + logger.warn('[PortForwardingService] Backend not available, simulating stop...'); conn.unsubscribe?.(); activeConnections.delete(ruleId); onStatusChange('inactive'); @@ -169,25 +171,25 @@ export const getPortForwardStatus = async ( * Check if backend is available */ export const isBackendAvailable = (): boolean => { - return !!(window.netcatty?.startPortForward); + return !!(netcattyBridge.get()?.startPortForward); }; /** * Stop all active tunnels (cleanup on unmount) */ export const stopAllPortForwards = async (): Promise => { - const bridge = window.netcatty; + const bridge = netcattyBridge.get(); for (const [_ruleId, conn] of activeConnections) { - try { - if (bridge?.stopPortForward) { - await bridge.stopPortForward(conn.tunnelId); - } - conn.unsubscribe?.(); - } catch (err) { - console.warn(`[PortForwardingService] Failed to stop tunnel ${conn.tunnelId}:`, err); - } - } + try { + if (bridge?.stopPortForward) { + await bridge.stopPortForward(conn.tunnelId); + } + conn.unsubscribe?.(); + } catch (err) { + logger.warn(`[PortForwardingService] Failed to stop tunnel ${conn.tunnelId}:`, err); + } + } activeConnections.clear(); }; diff --git a/lib/logger.ts b/lib/logger.ts new file mode 100644 index 00000000..d5b12758 --- /dev/null +++ b/lib/logger.ts @@ -0,0 +1,24 @@ +type LogArgs = unknown[]; + +const isDev = + typeof import.meta !== "undefined" && + typeof import.meta.env !== "undefined" && + !!import.meta.env.DEV; + +export const logger = { + debug: (...args: LogArgs) => { + if (!isDev) return; + console.debug(...args); + }, + info: (...args: LogArgs) => { + if (!isDev) return; + console.info(...args); + }, + warn: (...args: LogArgs) => { + console.warn(...args); + }, + error: (...args: LogArgs) => { + console.error(...args); + }, +}; + diff --git a/package-lock.json b/package-lock.json index 99af8b41..430e61b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "clsx": "2.1.1", "ghostty-web": "^0.4.0", "lucide-react": "0.560.0", - "node-pty": "^1.1.0-beta19", + "node-pty": "1.1.0-beta19", "react": "^19.2.1", "react-dom": "^19.2.1", "ssh2-sftp-client": "^12.0.1", @@ -440,7 +440,7 @@ }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" @@ -457,7 +457,7 @@ }, "node_modules/@esbuild/android-arm": { "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" @@ -474,7 +474,7 @@ }, "node_modules/@esbuild/android-arm64": { "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" @@ -491,7 +491,7 @@ }, "node_modules/@esbuild/android-x64": { "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" @@ -525,7 +525,7 @@ }, "node_modules/@esbuild/darwin-x64": { "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" @@ -542,7 +542,7 @@ }, "node_modules/@esbuild/freebsd-arm64": { "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" @@ -559,7 +559,7 @@ }, "node_modules/@esbuild/freebsd-x64": { "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" @@ -576,7 +576,7 @@ }, "node_modules/@esbuild/linux-arm": { "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" @@ -593,7 +593,7 @@ }, "node_modules/@esbuild/linux-arm64": { "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" @@ -610,7 +610,7 @@ }, "node_modules/@esbuild/linux-ia32": { "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" @@ -627,7 +627,7 @@ }, "node_modules/@esbuild/linux-loong64": { "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" @@ -644,7 +644,7 @@ }, "node_modules/@esbuild/linux-mips64el": { "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" @@ -661,7 +661,7 @@ }, "node_modules/@esbuild/linux-ppc64": { "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" @@ -678,7 +678,7 @@ }, "node_modules/@esbuild/linux-riscv64": { "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" @@ -695,7 +695,7 @@ }, "node_modules/@esbuild/linux-s390x": { "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" @@ -712,7 +712,7 @@ }, "node_modules/@esbuild/linux-x64": { "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" @@ -729,7 +729,7 @@ }, "node_modules/@esbuild/netbsd-arm64": { "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ "arm64" @@ -746,7 +746,7 @@ }, "node_modules/@esbuild/netbsd-x64": { "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" @@ -763,7 +763,7 @@ }, "node_modules/@esbuild/openbsd-arm64": { "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "cpu": [ "arm64" @@ -780,7 +780,7 @@ }, "node_modules/@esbuild/openbsd-x64": { "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" @@ -797,7 +797,7 @@ }, "node_modules/@esbuild/openharmony-arm64": { "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", "cpu": [ "arm64" @@ -814,7 +814,7 @@ }, "node_modules/@esbuild/sunos-x64": { "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" @@ -831,7 +831,7 @@ }, "node_modules/@esbuild/win32-arm64": { "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" @@ -848,7 +848,7 @@ }, "node_modules/@esbuild/win32-ia32": { "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" @@ -865,7 +865,7 @@ }, "node_modules/@esbuild/win32-x64": { "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" @@ -2284,7 +2284,7 @@ }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", "cpu": [ "arm" @@ -2298,7 +2298,7 @@ }, "node_modules/@rollup/rollup-android-arm64": { "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", "cpu": [ "arm64" @@ -2326,7 +2326,7 @@ }, "node_modules/@rollup/rollup-darwin-x64": { "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", "cpu": [ "x64" @@ -2340,7 +2340,7 @@ }, "node_modules/@rollup/rollup-freebsd-arm64": { "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", "cpu": [ "arm64" @@ -2354,7 +2354,7 @@ }, "node_modules/@rollup/rollup-freebsd-x64": { "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", "cpu": [ "x64" @@ -2368,7 +2368,7 @@ }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", "cpu": [ "arm" @@ -2382,7 +2382,7 @@ }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", "cpu": [ "arm" @@ -2396,7 +2396,7 @@ }, "node_modules/@rollup/rollup-linux-arm64-gnu": { "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", "cpu": [ "arm64" @@ -2410,7 +2410,7 @@ }, "node_modules/@rollup/rollup-linux-arm64-musl": { "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", "cpu": [ "arm64" @@ -2424,7 +2424,7 @@ }, "node_modules/@rollup/rollup-linux-loong64-gnu": { "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", "cpu": [ "loong64" @@ -2438,7 +2438,7 @@ }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", "cpu": [ "ppc64" @@ -2452,7 +2452,7 @@ }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", "cpu": [ "riscv64" @@ -2466,7 +2466,7 @@ }, "node_modules/@rollup/rollup-linux-riscv64-musl": { "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", "cpu": [ "riscv64" @@ -2480,7 +2480,7 @@ }, "node_modules/@rollup/rollup-linux-s390x-gnu": { "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", "cpu": [ "s390x" @@ -2494,7 +2494,7 @@ }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", "cpu": [ "x64" @@ -2508,7 +2508,7 @@ }, "node_modules/@rollup/rollup-linux-x64-musl": { "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", "cpu": [ "x64" @@ -2522,7 +2522,7 @@ }, "node_modules/@rollup/rollup-openharmony-arm64": { "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", "cpu": [ "arm64" @@ -2536,7 +2536,7 @@ }, "node_modules/@rollup/rollup-win32-arm64-msvc": { "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", "cpu": [ "arm64" @@ -2550,7 +2550,7 @@ }, "node_modules/@rollup/rollup-win32-ia32-msvc": { "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", "cpu": [ "ia32" @@ -2564,7 +2564,7 @@ }, "node_modules/@rollup/rollup-win32-x64-gnu": { "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", "cpu": [ "x64" @@ -2578,7 +2578,7 @@ }, "node_modules/@rollup/rollup-win32-x64-msvc": { "version": "4.53.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", "cpu": [ "x64" @@ -2665,7 +2665,7 @@ }, "node_modules/@tailwindcss/oxide-android-arm64": { "version": "4.1.18", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", "cpu": [ "arm64" @@ -2699,7 +2699,7 @@ }, "node_modules/@tailwindcss/oxide-darwin-x64": { "version": "4.1.18", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", "cpu": [ "x64" @@ -2716,7 +2716,7 @@ }, "node_modules/@tailwindcss/oxide-freebsd-x64": { "version": "4.1.18", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", "cpu": [ "x64" @@ -2733,7 +2733,7 @@ }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { "version": "4.1.18", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", "cpu": [ "arm" @@ -2750,7 +2750,7 @@ }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { "version": "4.1.18", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", "cpu": [ "arm64" @@ -2767,7 +2767,7 @@ }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { "version": "4.1.18", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", "cpu": [ "arm64" @@ -2784,7 +2784,7 @@ }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { "version": "4.1.18", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", "cpu": [ "x64" @@ -2801,7 +2801,7 @@ }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { "version": "4.1.18", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", "cpu": [ "x64" @@ -2818,7 +2818,7 @@ }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { "version": "4.1.18", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", "bundleDependencies": [ "@napi-rs/wasm-runtime", @@ -2846,9 +2846,69 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.7.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.7.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.1.18", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", "cpu": [ "arm64" @@ -2865,7 +2925,7 @@ }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { "version": "4.1.18", - "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", "cpu": [ "x64" @@ -5815,7 +5875,7 @@ }, "node_modules/lightningcss-android-arm64": { "version": "1.30.2", - "resolved": "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", "cpu": [ "arm64" @@ -5857,7 +5917,7 @@ }, "node_modules/lightningcss-darwin-x64": { "version": "1.30.2", - "resolved": "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", "cpu": [ "x64" @@ -5878,7 +5938,7 @@ }, "node_modules/lightningcss-freebsd-x64": { "version": "1.30.2", - "resolved": "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", "cpu": [ "x64" @@ -5899,7 +5959,7 @@ }, "node_modules/lightningcss-linux-arm-gnueabihf": { "version": "1.30.2", - "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", "cpu": [ "arm" @@ -5920,7 +5980,7 @@ }, "node_modules/lightningcss-linux-arm64-gnu": { "version": "1.30.2", - "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", "cpu": [ "arm64" @@ -5941,7 +6001,7 @@ }, "node_modules/lightningcss-linux-arm64-musl": { "version": "1.30.2", - "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", "cpu": [ "arm64" @@ -5962,7 +6022,7 @@ }, "node_modules/lightningcss-linux-x64-gnu": { "version": "1.30.2", - "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", "cpu": [ "x64" @@ -5983,7 +6043,7 @@ }, "node_modules/lightningcss-linux-x64-musl": { "version": "1.30.2", - "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", "cpu": [ "x64" @@ -6004,7 +6064,7 @@ }, "node_modules/lightningcss-win32-arm64-msvc": { "version": "1.30.2", - "resolved": "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", "cpu": [ "arm64" @@ -6025,7 +6085,7 @@ }, "node_modules/lightningcss-win32-x64-msvc": { "version": "1.30.2", - "resolved": "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", "cpu": [ "x64" @@ -6615,9 +6675,9 @@ } }, "node_modules/node-pty": { - "version": "1.1.0-beta9", - "resolved": "https://registry.npmmirror.com/node-pty/-/node-pty-1.1.0-beta9.tgz", - "integrity": "sha512-/Ue38pvXJdgRZ3+me1FgfglLd301GhJN0NStiotdt61tm43N5htUyR/IXOUzOKuNaFmCwIhy6nwb77Ky41LMbw==", + "version": "1.1.0-beta19", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta19.tgz", + "integrity": "sha512-/p4Zu56EYDdXjjaLWzrIlFyrBnND11LQGP0/L6GEVGURfCNkAlHc3Twg/2I4NPxghimHXgvDlwp7Z2GtvDIh8A==", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 58cb0a97..0376ddcf 100755 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "clsx": "2.1.1", "ghostty-web": "^0.4.0", "lucide-react": "0.560.0", - "node-pty": "^1.1.0-beta19", + "node-pty": "1.1.0-beta19", "react": "^19.2.1", "react-dom": "^19.2.1", "ssh2-sftp-client": "^12.0.1", diff --git a/vite.config.ts b/vite.config.ts index 49f127e2..88e05337 100755 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,15 +1,14 @@ import tailwindcss from '@tailwindcss/vite'; import react from '@vitejs/plugin-react'; import path from 'path'; -import { defineConfig,loadEnv } from 'vite'; +import { defineConfig } from 'vite'; -export default defineConfig(({ mode }) => { - const env = loadEnv(mode, '.', ''); +export default defineConfig(() => { return { base: "./", server: { port: 5173, - host: '0.0.0.0', + host: '127.0.0.1', headers: { // Required for SharedArrayBuffer and WASM in some browsers 'Cross-Origin-Opener-Policy': 'same-origin', @@ -19,15 +18,22 @@ export default defineConfig(({ mode }) => { build: { chunkSizeWarningLimit: 1500, target: 'esnext', // Required for top-level await in WASM modules + rollupOptions: { + output: { + manualChunks(id) { + if (!id.includes('node_modules')) return; + if (id.includes('@xterm') || id.includes('xterm')) return 'xterm'; + if (id.includes('@radix-ui')) return 'radix'; + if (id.includes('react')) return 'react'; + return 'vendor'; + }, + }, + }, }, plugins: [tailwindcss(), react()], optimizeDeps: { exclude: ['ghostty-web'], // Don't pre-bundle ghostty-web to preserve WASM imports }, - define: { - 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY), - 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY) - }, resolve: { alias: { '@': path.resolve(__dirname, '.'),