Refactors to enforce backend access via application hooks
Replaces all direct usage of browser globals and infrastructure service imports in UI components with dedicated application/state backend hooks. Introduces lint rules to prevent direct access to backend bridges and localStorage from components, promoting a cleaner separation of concerns and improved maintainability. Moves user preferences (e.g., port forwarding form mode) to persistent state hooks, updates port forwarding and SFTP logic to rely on backend hooks, and centralizes logging through a logger utility. Cleans up debug code and removes obsolete scripts from HTML. Improves testability, prepares for alternative backend implementations, and enforces architectural boundaries.
This commit is contained in:
14
App.tsx
14
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);
|
||||
|
||||
26
application/state/useKeychainBackend.ts
Normal file
26
application/state/useKeychainBackend.ts
Normal file
@@ -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 };
|
||||
};
|
||||
|
||||
12
application/state/useKnownHostsBackend.ts
Normal file
12
application/state/useKnownHostsBackend.ts
Normal file
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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<PortForwardingRule, "id" | "createdAt" | "status">,
|
||||
@@ -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<ViewMode>("grid");
|
||||
const [sortMode, setSortMode] = useState<SortMode>("newest");
|
||||
const [search, setSearch] = useState("");
|
||||
const [preferFormMode, setPreferFormModeState] = useState<boolean>(() => {
|
||||
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,
|
||||
|
||||
@@ -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));
|
||||
@@ -141,10 +139,9 @@ 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]);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
206
application/state/useSftpBackend.ts
Normal file
206
application/state/useSftpBackend.ts
Normal file
@@ -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<RemoteFile[]> => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.listLocalDir) throw new Error("listLocalDir unavailable");
|
||||
return bridge.listLocalDir(path);
|
||||
}, []);
|
||||
|
||||
const readLocalFile = useCallback(async (path: string): Promise<ArrayBuffer> => {
|
||||
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<NonNullable<NetcattyBridge["startStreamTransfer"]>>[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<NonNullable<NetcattyBridge["onTransferProgress"]>>[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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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<SftpFileEntry[]> => {
|
||||
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<SftpFileEntry[]> => {
|
||||
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<SftpFileEntry[]> => {
|
||||
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<SftpFileEntry[]> => {
|
||||
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<void> => {
|
||||
// Try to use streaming transfer if available
|
||||
if (window.netcatty?.startStreamTransfer) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
transferId: task.id,
|
||||
): Promise<void> => {
|
||||
// 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;
|
||||
}
|
||||
const sftpId = sftpSessionsRef.current.get(pane.connection.id);
|
||||
if (!sftpId || !netcattyBridge.get()?.chmodSftp) {
|
||||
handleSessionError(side, new Error("SFTP session not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await window.netcatty.chmodSftp(sftpId, filePath, mode);
|
||||
await refresh(side);
|
||||
} catch (err) {
|
||||
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],
|
||||
|
||||
65
application/state/useSyncState.ts
Normal file
65
application/state/useSyncState.ts
Normal file
@@ -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<SyncStatus>("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 <T extends object>(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 };
|
||||
};
|
||||
|
||||
122
application/state/useTerminalBackend.ts
Normal file
122
application/state/useTerminalBackend.ts
Normal file
@@ -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<NonNullable<NetcattyBridge["startTelnetSession"]>>[0]) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.startTelnetSession) throw new Error("startTelnetSession unavailable");
|
||||
return bridge.startTelnetSession(options);
|
||||
}, []);
|
||||
|
||||
const startMoshSession = useCallback(async (options: Parameters<NonNullable<NetcattyBridge["startMoshSession"]>>[0]) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.startMoshSession) throw new Error("startMoshSession unavailable");
|
||||
return bridge.startMoshSession(options);
|
||||
}, []);
|
||||
|
||||
const startLocalSession = useCallback(async (options: Parameters<NonNullable<NetcattyBridge["startLocalSession"]>>[0]) => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.startLocalSession) throw new Error("startLocalSession unavailable");
|
||||
return bridge.startLocalSession(options);
|
||||
}, []);
|
||||
|
||||
const execCommand = useCallback(async (options: Parameters<NetcattyBridge["execCommand"]>[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,
|
||||
};
|
||||
};
|
||||
44
application/state/useWindowControls.ts
Normal file
44
application/state/useWindowControls.ts
Normal file
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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<KeychainManagerProps> = ({
|
||||
onSaveHost,
|
||||
onCreateGroup,
|
||||
}) => {
|
||||
const { generateKeyPair, execCommand } = useKeychainBackend();
|
||||
const [activeFilter, setActiveFilter] = useState<FilterTab>("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,
|
||||
|
||||
@@ -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<KnownHostsManagerProps> = ({
|
||||
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<KnownHostsManagerProps> = ({
|
||||
// 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<KnownHostsManagerProps> = ({
|
||||
|
||||
// Memoize the rendered list to prevent re-renders
|
||||
const renderedItems = useMemo(() => {
|
||||
console.log(
|
||||
"[KnownHostsManager] renderedItems useMemo recalculated - displayedHosts:",
|
||||
displayedHosts.length,
|
||||
);
|
||||
return displayedHosts.map((knownHost) => (
|
||||
<HostItem
|
||||
key={knownHost.id}
|
||||
@@ -445,8 +434,6 @@ const KnownHostsManager: React.FC<KnownHostsManagerProps> = ({
|
||||
handleConvertToHost,
|
||||
]);
|
||||
|
||||
console.log("[KnownHostsManager] about to return JSX");
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Header */}
|
||||
|
||||
@@ -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<PortForwardingProps> = ({
|
||||
deleteRule,
|
||||
duplicateRule,
|
||||
setRuleStatus,
|
||||
startTunnel,
|
||||
stopTunnel,
|
||||
filteredRules,
|
||||
selectedRule: _selectedRule,
|
||||
preferFormMode,
|
||||
setPreferFormMode,
|
||||
} = usePortForwardingState();
|
||||
|
||||
// Track connecting/stopping states
|
||||
@@ -106,12 +106,11 @@ const PortForwarding: React.FC<PortForwardingProps> = ({
|
||||
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<PortForwardingProps> = ({
|
||||
});
|
||||
}
|
||||
},
|
||||
[hosts, keys, setRuleStatus],
|
||||
[hosts, keys, setRuleStatus, startTunnel],
|
||||
);
|
||||
|
||||
// Stop a port forwarding tunnel
|
||||
@@ -141,9 +140,7 @@ const PortForwarding: React.FC<PortForwardingProps> = ({
|
||||
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<PortForwardingProps> = ({
|
||||
});
|
||||
}
|
||||
},
|
||||
[setRuleStatus],
|
||||
[stopTunnel],
|
||||
);
|
||||
|
||||
// Wizard state
|
||||
@@ -191,15 +188,6 @@ const PortForwarding: React.FC<PortForwardingProps> = ({
|
||||
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<PortForwardingProps> = ({
|
||||
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<PortForwardingProps> = ({
|
||||
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");
|
||||
|
||||
@@ -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 <File size={18} className={iconClass} />;
|
||||
};
|
||||
};
|
||||
|
||||
// Format bytes with appropriate unit (B, KB, MB, GB)
|
||||
const formatBytes = (bytes: number | string): string => {
|
||||
@@ -254,6 +256,17 @@ const SFTPModal: React.FC<SFTPModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
}) => {
|
||||
const {
|
||||
openSftp,
|
||||
closeSftp: closeSftpBackend,
|
||||
listSftp,
|
||||
readSftp,
|
||||
writeSftpBinaryWithProgress,
|
||||
writeSftpBinary,
|
||||
writeSftp,
|
||||
deleteSftp,
|
||||
mkdirSftp,
|
||||
} = useSftpBackend();
|
||||
const [currentPath, setCurrentPath] = useState("/");
|
||||
const [files, setFiles] = useState<RemoteFile[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -299,8 +312,7 @@ const SFTPModal: React.FC<SFTPModalProps> = ({
|
||||
|
||||
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<SFTPModalProps> = ({
|
||||
credentials.port,
|
||||
credentials.password,
|
||||
credentials.privateKey,
|
||||
openSftp,
|
||||
]);
|
||||
|
||||
const loadFiles = useCallback(
|
||||
@@ -343,7 +356,7 @@ const SFTPModal: React.FC<SFTPModalProps> = ({
|
||||
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<SFTPModalProps> = ({
|
||||
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<SFTPModalProps> = ({
|
||||
}
|
||||
}
|
||||
},
|
||||
[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<SFTPModalProps> = ({
|
||||
} 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<SFTPModalProps> = ({
|
||||
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<SFTPModalProps> = ({
|
||||
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<SFTPModalProps> = ({
|
||||
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<SFTPModalProps> = ({
|
||||
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<SFTPModalProps> = ({
|
||||
};
|
||||
|
||||
const handleClose = async () => {
|
||||
await closeSftp();
|
||||
await closeSftpSession();
|
||||
setIsEditingPath(false);
|
||||
onClose();
|
||||
};
|
||||
@@ -852,9 +865,7 @@ const SFTPModal: React.FC<SFTPModalProps> = ({
|
||||
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());
|
||||
|
||||
@@ -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<SettingsDialogProps> = ({
|
||||
// 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<SettingsDialogProps> = ({
|
||||
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<SettingsDialogProps> = ({
|
||||
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<SettingsDialogProps> = ({
|
||||
} 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");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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<TerminalProps> = ({
|
||||
keyBindingsRef.current = keyBindings;
|
||||
onHotkeyActionRef.current = onHotkeyAction;
|
||||
|
||||
const terminalBackend = useTerminalBackend();
|
||||
const { resizeSession } = terminalBackend;
|
||||
|
||||
const [isScriptsOpen, setIsScriptsOpen] = useState(false);
|
||||
const [status, setStatus] = useState<TerminalSession["status"]>("connecting");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -221,11 +226,11 @@ const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
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<TerminalProps> = ({
|
||||
};
|
||||
|
||||
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<TerminalProps> = ({
|
||||
? 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<TerminalProps> = ({
|
||||
? "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<TerminalProps> = ({
|
||||
}
|
||||
})();
|
||||
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<TerminalProps> = ({
|
||||
}
|
||||
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<TerminalProps> = ({
|
||||
|
||||
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<TerminalProps> = ({
|
||||
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<TerminalProps> = ({
|
||||
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<TerminalProps> = ({
|
||||
|
||||
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<TerminalProps> = ({
|
||||
// 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<TerminalProps> = ({
|
||||
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<TerminalProps> = ({
|
||||
try {
|
||||
fitAddon.fit();
|
||||
} catch (err) {
|
||||
console.warn("Fit failed", err);
|
||||
logger.warn("Fit failed", err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -889,33 +880,33 @@ const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
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<TerminalProps> = ({
|
||||
// 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<TerminalProps> = ({
|
||||
// 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<TerminalProps> = ({
|
||||
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<TerminalProps> = ({
|
||||
]);
|
||||
|
||||
// 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<TerminalProps> = ({
|
||||
setProgressValue(Math.min(95, hopProgress));
|
||||
},
|
||||
);
|
||||
if (unsub) unsubscribeChainProgress = unsub;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1139,7 +1130,7 @@ const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
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<TerminalProps> = ({
|
||||
|
||||
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<TerminalProps> = ({
|
||||
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<TerminalProps> = ({
|
||||
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<TerminalProps> = ({
|
||||
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<TerminalProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
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<TerminalProps> = ({
|
||||
|
||||
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<TerminalProps> = ({
|
||||
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<TerminalProps> = ({
|
||||
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<TerminalProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
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<TerminalProps> = ({
|
||||
|
||||
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<TerminalProps> = ({
|
||||
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<TerminalProps> = ({
|
||||
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<TerminalProps> = ({
|
||||
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<TerminalProps> = ({
|
||||
}
|
||||
|
||||
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<TerminalProps> = ({
|
||||
},
|
||||
});
|
||||
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<TerminalProps> = ({
|
||||
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<TerminalProps> = ({
|
||||
};
|
||||
|
||||
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<TerminalProps> = ({
|
||||
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<TerminalProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
@@ -387,9 +387,6 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
|
||||
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<string | undefined>(undefined);
|
||||
|
||||
@@ -421,7 +418,6 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
|
||||
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));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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<TopTabsProps> = ({
|
||||
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<TopTabsProps> = ({
|
||||
// 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 (
|
||||
<div
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/**
|
||||
/**
|
||||
* Export Key Panel - Export SSH key to remote host
|
||||
*/
|
||||
|
||||
import { ChevronRight, Info } from 'lucide-react';
|
||||
import React, { useState } from 'react';
|
||||
import { useKeychainBackend } from '../../application/state/useKeychainBackend';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Host, SSHKey } from '../../types';
|
||||
import { Button } from '../ui/button';
|
||||
@@ -44,14 +45,15 @@ export const ExportKeyPanel: React.FC<ExportKeyPanelProps> = ({
|
||||
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<ExportKeyPanelProps> = ({
|
||||
.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,
|
||||
|
||||
@@ -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<boolean> => {
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* Local Filesystem Bridge - Handles local file operations
|
||||
* Extracted from main.cjs for single responsibility
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* Port Forwarding Bridge - Handles SSH port forwarding tunnels
|
||||
* Extracted from main.cjs for single responsibility
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* SFTP Bridge - Handles SFTP connections and file operations
|
||||
* Extracted from main.cjs for single responsibility
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* SSH Bridge - Handles SSH connections, sessions, and related operations
|
||||
* Extracted from main.cjs for single responsibility
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* Terminal Bridge - Handles local shell and telnet/mosh sessions
|
||||
* Extracted from main.cjs for single responsibility
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* Transfer Bridge - Handles file transfers with progress and cancellation
|
||||
* Extracted from main.cjs for single responsibility
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* Window Manager - Handles Electron window creation and management
|
||||
* Extracted from main.cjs for single responsibility
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { ipcRenderer, contextBridge } = require("electron");
|
||||
const { ipcRenderer, contextBridge } = require("electron");
|
||||
|
||||
const dataListeners = new Map();
|
||||
const exitListeners = new Map();
|
||||
|
||||
@@ -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.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
38
index.html
38
index.html
@@ -104,41 +104,9 @@
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
// Polyfill process for environment variables if missing
|
||||
if (typeof process === 'undefined') {
|
||||
window.process = { env: {} };
|
||||
}
|
||||
</script>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"react": "https://esm.sh/react@19.2.1",
|
||||
"react/jsx-runtime": "https://esm.sh/react@19.2.1/jsx-runtime",
|
||||
"react-dom/client": "https://esm.sh/react-dom@19.2.1/client",
|
||||
"react-dom": "https://esm.sh/react-dom@19.2.1",
|
||||
"@google/genai": "https://esm.sh/@google/genai@1.31.0",
|
||||
"lucide-react": "https://esm.sh/lucide-react@0.556.0?external=react,react-dom",
|
||||
"clsx": "https://esm.sh/clsx@2.1.0",
|
||||
"tailwind-merge": "https://esm.sh/tailwind-merge@2.2.0",
|
||||
"xterm": "https://esm.sh/xterm@5.3.0",
|
||||
"xterm-addon-fit": "https://esm.sh/xterm-addon-fit@0.8.0?external=xterm",
|
||||
"@radix-ui/react-dialog": "https://esm.sh/@radix-ui/react-dialog@1.1.15?external=react,react-dom",
|
||||
"@radix-ui/react-tabs": "https://esm.sh/@radix-ui/react-tabs@1.1.13?external=react,react-dom",
|
||||
"@radix-ui/react-select": "https://esm.sh/@radix-ui/react-select@2.2.6?external=react,react-dom",
|
||||
"@radix-ui/react-popover": "https://esm.sh/@radix-ui/react-popover@1.1.15?external=react,react-dom",
|
||||
"@radix-ui/react-context-menu": "https://esm.sh/@radix-ui/react-context-menu@2.2.16?external=react,react-dom",
|
||||
"@radix-ui/react-collapsible": "https://esm.sh/@radix-ui/react-collapsible@1.1.12?external=react,react-dom",
|
||||
"@radix-ui/react-scroll-area": "https://esm.sh/@radix-ui/react-scroll-area@1.2.10?external=react,react-dom",
|
||||
"@radix-ui/react-slot": "https://esm.sh/@radix-ui/react-slot@1.2.4?external=react,react-dom",
|
||||
"react-dom/": "https://aistudiocdn.com/react-dom@^19.2.1/",
|
||||
"react/": "https://aistudiocdn.com/react@^19.2.1/"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<link rel="stylesheet" href="/index.css">
|
||||
</head>
|
||||
</style>
|
||||
<link rel="stylesheet" href="/index.css">
|
||||
</head>
|
||||
|
||||
<body class="bg-background text-foreground overflow-hidden antialiased h-screen w-screen">
|
||||
<div id="root" class="h-full w-full"></div>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
19
infrastructure/services/netcattyBridge.ts
Normal file
19
infrastructure/services/netcattyBridge.ts
Normal file
@@ -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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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<void> => {
|
||||
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();
|
||||
};
|
||||
|
||||
24
lib/logger.ts
Normal file
24
lib/logger.ts
Normal file
@@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
202
package-lock.json
generated
202
package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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, '.'),
|
||||
|
||||
Reference in New Issue
Block a user