feat(FN-3400): add native shell connection handoff, plugin management CLI/l

This merge adds a complete plugin management system (FN-3565) with CLI commands, a loader, runner, and dashboard routes, along with project-scoped auth storage (FN-3544), native shell connection support for mobile (FN-3400) spanning onboarding, connection manager, and remote desktop handoff, and ref

Fusion-Task-Id: FN-3400
This commit is contained in:
Fusion
2026-05-06 05:24:01 -07:00
committed by gsxdsm
parent fb0e15562d
commit 419c6f3b31
20 changed files with 266 additions and 25 deletions

View File

@@ -127,4 +127,16 @@ describe("ipc handlers", () => {
expect(onDesktopModeChange).toHaveBeenCalledWith("local");
expect(window.webContents.send).toHaveBeenCalledWith("shell:state", expect.any(Object));
});
it("shell:saveProfile rejects invalid URLs", async () => {
await registerHandlers();
const handler = mocks.ipcHandlers.get("shell:saveProfile");
await expect(handler?.({}, { name: "Prod", serverUrl: "not-a-url" })).rejects.toThrow(
"Server URL must be a valid absolute URL",
);
await expect(handler?.({}, { name: "Prod", serverUrl: "ftp://fusion.example.com" })).rejects.toThrow(
"Server URL must use http or https",
);
});
});

View File

@@ -43,6 +43,20 @@ function createProfileId(): string {
return `profile_${Math.random().toString(36).slice(2, 10)}`;
}
function normalizeServerUrl(serverUrl: string): string {
const normalized = serverUrl.trim().replace(/\/$/, "");
let parsed: URL;
try {
parsed = new URL(normalized);
} catch {
throw new Error("Server URL must be a valid absolute URL");
}
if (!parsed.protocol || !/^https?:$/.test(parsed.protocol)) {
throw new Error("Server URL must use http or https");
}
return normalized;
}
function toShellState(
settings: Awaited<ReturnType<typeof readShellSettings>>,
localServerState?: DesktopLocalServerState,
@@ -113,7 +127,7 @@ export function registerIpcHandlers(mainWindow: BrowserWindow, tray: Tray, optio
const nextProfile: ShellConnectionProfile = {
id: existing?.id ?? profile.id ?? createProfileId(),
name: profile.name.trim(),
serverUrl: profile.serverUrl.trim().replace(/\/$/, ""),
serverUrl: normalizeServerUrl(profile.serverUrl),
authToken: profile.authToken ?? null,
createdAt: existing?.createdAt ?? timestamp,
updatedAt: timestamp,