mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-11 16:36:32 +02:00
fix(desktop): make packaged app usable for fresh accounts (#1074)
* feat(desktop): add macOS app icon Replace the default electron-vite scaffold icon with the Multica asterisk icon. Adds build/icon.icns so electron-builder picks it up automatically via the `buildResources: build` config — no YAML change needed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(desktop): run electron-vite build inside package script The package wrapper only ran bundle-cli.mjs and electron-builder, so electron-builder silently packaged whatever was already in out/. On a fresh checkout (or after a partial build) this shipped an app with a missing renderer bundle, which white-screens on launch. Add an explicit `electron-vite build` step between bundle-cli and electron-builder so `pnpm package` is self-contained. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(desktop): restore shell PATH in main process for GUI launches macOS/Linux GUI launches inherit a minimal PATH from launchd that omits ~/.zshrc, Homebrew, nvm, ~/.local/bin, and other shell config. Child processes spawned from the main process — including the bundled multica CLI used by daemon-manager — inherit the same stripped PATH, so the CLI fails to locate agent binaries like claude, codex, opencode, etc. with "no agent CLI found: … ensure it is on PATH". Use `fix-path` to recover the real shell PATH at startup, then prepend common install locations (/opt/homebrew/bin, /usr/local/bin, ~/.local/bin) as a fallback for broken shell rc or non-interactive $SHELL. Runs before setupDaemonManager so every subsequent spawn sees the corrected PATH. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(desktop): show onboarding wizard when authed user has no workspace Desktop is a single-shell architecture — every route, including /onboarding, lives inside DashboardGuard. The guard returns its loading fallback whenever workspace is null, so a fresh account that logs in with no workspaces ends up stuck on the spinner forever: the `replace(onboardingPath)` redirect navigates the tab router, but DashboardGuard still blocks its children because workspace is still null. Handle the empty-workspace case in DesktopShell itself: render OnboardingWizard as a full-screen takeover, bypassing DashboardGuard. A ref-based flag freezes the "needs onboarding" decision at first mount so creating a workspace mid-wizard (step 0) doesn't unmount the wizard and dump the user into the main shell before steps 1-3 (runtime, agent, get started) finish. Also add a local `bootstrapping` flag in AppContent so DesktopShell doesn't mount until the deep-link login chain (loginWithToken → syncToken → listWorkspaces → hydrateWorkspace) fully resolves. Without it, the shell would briefly see `!workspace` before hydration lands, causing users with existing workspaces to flash the wizard (or, with the ref freeze, get stuck in it permanently). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(desktop): extract OnboardingGate with test coverage Pull the "render onboarding wizard when authed user has no workspace" logic out of DesktopShell into a dedicated OnboardingGate component. Replaces the ref-based freeze with a lazy useState initializer (`useState(() => !hasWorkspace)`), which is React's idiomatic pattern for "capture a value once at mount". The freeze semantics are unchanged: creating a workspace in step 0 of the wizard must not unmount it, because steps 1-3 still need to run; only `onComplete` flips the gate back to the main shell. Also de-duplicates the wrapping DesktopNavigationProvider — both branches of the shell now share a single provider instead of re-mounting one per branch. Wire up jsdom + @testing-library/react in the desktop vitest config (mirroring packages/views) and add three deterministic tests covering: 1. children render when hasWorkspace is true at mount 2. wizard stays mounted when hasWorkspace flips to true mid-flow 3. onComplete transitions the gate to children Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(desktop): drop redundant syncToken call in deep-link login daemonAPI.syncToken was called twice on a deep-link login: once inside the deep-link handler's bootstrapping chain, and again in the useEffect([user]) that reacts to the user state change. Both calls spawn a multica CLI subprocess over IPC, wasting ~1-2s of startup time on the critical login path. Keep the [user] effect (it covers the session-restore path too) and drop the explicit call from the deep-link handler. Net effect: login latency shrinks, behavior is unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
BIN
apps/desktop/build/icon.icns
Normal file
BIN
apps/desktop/build/icon.icns
Normal file
Binary file not shown.
@@ -29,6 +29,7 @@
|
||||
"@multica/ui": "workspace:*",
|
||||
"@multica/views": "workspace:*",
|
||||
"electron-updater": "^6.8.3",
|
||||
"fix-path": "^5.0.0",
|
||||
"react-router-dom": "^7.6.0",
|
||||
"shadcn": "^4.1.0",
|
||||
"sonner": "^2.0.7",
|
||||
@@ -38,6 +39,8 @@
|
||||
"@electron-toolkit/tsconfig": "^2.0.0",
|
||||
"@multica/tsconfig": "workspace:*",
|
||||
"@tailwindcss/vite": "^4",
|
||||
"@testing-library/jest-dom": "catalog:",
|
||||
"@testing-library/react": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
@@ -45,6 +48,7 @@
|
||||
"electron": "^39.2.6",
|
||||
"electron-builder": "^26.0.12",
|
||||
"electron-vite": "^5.0.0",
|
||||
"jsdom": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"tailwindcss": "^4",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 735 KiB |
@@ -5,13 +5,21 @@
|
||||
// binary via the `main.version` ldflag — so a single `vX.Y.Z` tag push
|
||||
// produces matching CLI and Desktop versions.
|
||||
//
|
||||
// Runs the existing bundle-cli.mjs first (so the Go binary is compiled
|
||||
// and copied into resources/bin/), then invokes electron-builder with
|
||||
// `-c.extraMetadata.version=<derived>` so the override applies at build
|
||||
// time without mutating the tracked package.json.
|
||||
// Runs bundle-cli.mjs first (so the Go binary is compiled and copied
|
||||
// into resources/bin/), then `electron-vite build` to produce the
|
||||
// main/preload/renderer bundles under out/, then invokes electron-builder
|
||||
// with `-c.extraMetadata.version=<derived>` so the override applies at
|
||||
// build time without mutating the tracked package.json.
|
||||
//
|
||||
// The electron-vite step is important: electron-builder only packages
|
||||
// whatever is already in out/, so skipping it (or relying on stale
|
||||
// artifacts from a prior partial build) ships an app with missing
|
||||
// renderer code and white-screens on launch.
|
||||
//
|
||||
// Extra CLI args after `pnpm package --` are forwarded to electron-builder
|
||||
// unchanged (e.g. `--mac --arm64`).
|
||||
// unchanged (e.g. `--mac --arm64`). For an unsigned local smoke-test
|
||||
// build, set `CSC_IDENTITY_AUTO_DISCOVERY=false` so electron-builder falls
|
||||
// back to an ad-hoc signature instead of requiring a Developer ID cert.
|
||||
//
|
||||
// The `normalizeGitVersion` helper is exported so tests can cover the
|
||||
// version-derivation logic without shelling out.
|
||||
@@ -64,7 +72,26 @@ function main() {
|
||||
cwd: desktopRoot,
|
||||
});
|
||||
|
||||
// Step 2: derive the version that should be written into the app.
|
||||
// Step 2: build the Electron main/preload/renderer bundles. Without
|
||||
// this step electron-builder silently packages whatever is already in
|
||||
// out/, which on a fresh checkout (or after a partial build) ships an
|
||||
// app that white-screens because the renderer bundle is missing.
|
||||
const viteResult = spawnSync("electron-vite", ["build"], {
|
||||
stdio: "inherit",
|
||||
cwd: desktopRoot,
|
||||
});
|
||||
if (viteResult.error) {
|
||||
console.error(
|
||||
"[package] failed to spawn electron-vite:",
|
||||
viteResult.error.message,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (viteResult.status !== 0) {
|
||||
process.exit(viteResult.status ?? 1);
|
||||
}
|
||||
|
||||
// Step 3: derive the version that should be written into the app.
|
||||
const version = deriveVersion();
|
||||
if (version) {
|
||||
console.log(`[package] Desktop version → ${version} (from git describe)`);
|
||||
@@ -74,12 +101,12 @@ function main() {
|
||||
);
|
||||
}
|
||||
|
||||
// Step 3: assemble electron-builder args.
|
||||
// Step 4: assemble electron-builder args.
|
||||
const passthrough = process.argv.slice(2);
|
||||
const builderArgs = [];
|
||||
if (version) builderArgs.push(`-c.extraMetadata.version=${version}`);
|
||||
|
||||
// Step 4: gracefully degrade for local dev builds. electron-builder.yml
|
||||
// Step 5: gracefully degrade for local dev builds. electron-builder.yml
|
||||
// sets `notarize: true` so real releases notarize in-build (keeping the
|
||||
// stapled .app consistent with latest-mac.yml's SHA512). But a mac dev
|
||||
// who just wants to smoke-test a local package doesn't have Apple
|
||||
@@ -95,7 +122,7 @@ function main() {
|
||||
|
||||
builderArgs.push(...passthrough);
|
||||
|
||||
// Step 5: invoke electron-builder. pnpm puts node_modules/.bin on PATH
|
||||
// Step 6: invoke electron-builder. pnpm puts node_modules/.bin on PATH
|
||||
// for the script run, so spawnSync finds the binary without needing a
|
||||
// shell wrapper (avoids any risk of argv interpolation).
|
||||
const result = spawnSync("electron-builder", builderArgs, {
|
||||
|
||||
@@ -1,9 +1,31 @@
|
||||
import { app, shell, BrowserWindow, ipcMain } from "electron";
|
||||
import { homedir } from "os";
|
||||
import { join } from "path";
|
||||
import { electronApp, optimizer, is } from "@electron-toolkit/utils";
|
||||
import fixPath from "fix-path";
|
||||
import { setupAutoUpdater } from "./updater";
|
||||
import { setupDaemonManager } from "./daemon-manager";
|
||||
|
||||
// macOS/Linux GUI launches inherit a minimal PATH from launchd that omits
|
||||
// the user's shell config (~/.zshrc, Homebrew, nvm, ~/.local/bin, etc.).
|
||||
// Run the user's login shell once to recover the real PATH so the bundled
|
||||
// multica CLI can find agent binaries like claude/codex/opencode. Must run
|
||||
// before any child_process.spawn / execFile call in the main process —
|
||||
// ES module imports are hoisted, so this block executes before createWindow
|
||||
// or any daemon-manager spawn.
|
||||
if (process.platform !== "win32") {
|
||||
fixPath();
|
||||
// Fallback: prepend common install locations in case fix-path came up
|
||||
// short (broken shell rc, non-interactive $SHELL, missing entries). Safe
|
||||
// to duplicate — PATH lookups short-circuit on first match.
|
||||
const fallbackPaths = [
|
||||
"/opt/homebrew/bin",
|
||||
"/usr/local/bin",
|
||||
join(homedir(), ".local/bin"),
|
||||
];
|
||||
process.env.PATH = `${fallbackPaths.join(":")}:${process.env.PATH ?? ""}`;
|
||||
}
|
||||
|
||||
const PROTOCOL = "multica";
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { CoreProvider } from "@multica/core/platform";
|
||||
import { useAuthStore } from "@multica/core/auth";
|
||||
import { useWorkspaceStore } from "@multica/core/workspace";
|
||||
@@ -13,6 +13,14 @@ import { UpdateNotification } from "./components/update-notification";
|
||||
function AppContent() {
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const isLoading = useAuthStore((s) => s.isLoading);
|
||||
// Deep-link login runs loginWithToken → syncToken → listWorkspaces →
|
||||
// hydrateWorkspace sequentially. loginWithToken sets user+isLoading=false
|
||||
// as soon as getMe resolves, which would cause DesktopShell to mount
|
||||
// before the workspace list is hydrated and briefly see `!workspace`.
|
||||
// This local flag keeps the loading screen up until the whole chain
|
||||
// finishes, so the shell's "needs onboarding?" check gets a definitive
|
||||
// workspace state on first render.
|
||||
const [bootstrapping, setBootstrapping] = useState(false);
|
||||
|
||||
// Tell the main process which backend URL we talk to, so daemon-manager
|
||||
// can pick the matching CLI profile (server_url from ~/.multica config).
|
||||
@@ -20,17 +28,21 @@ function AppContent() {
|
||||
window.daemonAPI.setTargetApiUrl(DAEMON_TARGET_API_URL);
|
||||
}, []);
|
||||
|
||||
// Listen for auth token delivered via deep link (multica://auth/callback?token=...)
|
||||
// Listen for auth token delivered via deep link (multica://auth/callback?token=...).
|
||||
// daemonAPI.syncToken is handled separately by the [user] effect below, which
|
||||
// fires whenever a user logs in (deep link, session restore, account switch).
|
||||
useEffect(() => {
|
||||
return window.desktopAPI.onAuthToken(async (token) => {
|
||||
setBootstrapping(true);
|
||||
try {
|
||||
const loggedIn = await useAuthStore.getState().loginWithToken(token);
|
||||
await window.daemonAPI.syncToken(token, loggedIn.id);
|
||||
await useAuthStore.getState().loginWithToken(token);
|
||||
const wsList = await api.listWorkspaces();
|
||||
const lastWsId = localStorage.getItem("multica_workspace_id");
|
||||
useWorkspaceStore.getState().hydrateWorkspace(wsList, lastWsId);
|
||||
} catch {
|
||||
// Token invalid or expired — user stays on login page
|
||||
} finally {
|
||||
setBootstrapping(false);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
@@ -51,7 +63,7 @@ function AppContent() {
|
||||
})();
|
||||
}, [user]);
|
||||
|
||||
if (isLoading) {
|
||||
if (isLoading || bootstrapping) {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<MulticaIcon className="size-6 animate-pulse" />
|
||||
|
||||
@@ -12,8 +12,11 @@ import { ModalRegistry } from "@multica/views/modals/registry";
|
||||
import { AppSidebar, DashboardGuard } from "@multica/views/layout";
|
||||
import { SearchCommand, SearchTrigger } from "@multica/views/search";
|
||||
import { ChatFab, ChatWindow } from "@multica/views/chat";
|
||||
import { OnboardingWizard } from "@multica/views/onboarding";
|
||||
import { useWorkspaceStore } from "@multica/core/workspace";
|
||||
import { DesktopNavigationProvider } from "@/platform/navigation";
|
||||
import { MulticaIcon } from "@multica/ui/components/common/multica-icon";
|
||||
import { OnboardingGate } from "./onboarding-gate";
|
||||
import { TabBar } from "./tab-bar";
|
||||
import { TabContent } from "./tab-content";
|
||||
|
||||
@@ -86,34 +89,45 @@ export function DesktopShell() {
|
||||
useInternalLinkHandler();
|
||||
useActiveTitleSync();
|
||||
|
||||
const workspace = useWorkspaceStore((s) => s.workspace);
|
||||
|
||||
return (
|
||||
<DesktopNavigationProvider>
|
||||
<DashboardGuard
|
||||
loginPath="/login"
|
||||
loadingFallback={
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<MulticaIcon className="size-6 animate-pulse" />
|
||||
<OnboardingGate
|
||||
hasWorkspace={!!workspace}
|
||||
onboarding={(onComplete) => (
|
||||
<div className="h-screen overflow-auto">
|
||||
<OnboardingWizard onComplete={onComplete} />
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="flex h-screen">
|
||||
<SidebarProvider className="flex-1">
|
||||
<AppSidebar topSlot={<SidebarTopBar />} searchSlot={<SearchTrigger />} />
|
||||
{/* Right side: header + content container */}
|
||||
<div className="flex flex-1 min-w-0 flex-col">
|
||||
<MainTopBar />
|
||||
{/* Content area with inset styling — relative so ChatWindow/ChatFab are constrained here */}
|
||||
<div className="relative flex flex-1 min-h-0 flex-col overflow-hidden mr-2 mb-2 ml-0.5 rounded-xl shadow-sm bg-background">
|
||||
<TabContent />
|
||||
<ChatWindow />
|
||||
<ChatFab />
|
||||
</div>
|
||||
<DashboardGuard
|
||||
loginPath="/login"
|
||||
loadingFallback={
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<MulticaIcon className="size-6 animate-pulse" />
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</div>
|
||||
<ModalRegistry />
|
||||
<SearchCommand />
|
||||
</DashboardGuard>
|
||||
}
|
||||
>
|
||||
<div className="flex h-screen">
|
||||
<SidebarProvider className="flex-1">
|
||||
<AppSidebar topSlot={<SidebarTopBar />} searchSlot={<SearchTrigger />} />
|
||||
{/* Right side: header + content container */}
|
||||
<div className="flex flex-1 min-w-0 flex-col">
|
||||
<MainTopBar />
|
||||
{/* Content area with inset styling — relative so ChatWindow/ChatFab are constrained here */}
|
||||
<div className="relative flex flex-1 min-h-0 flex-col overflow-hidden mr-2 mb-2 ml-0.5 rounded-xl shadow-sm bg-background">
|
||||
<TabContent />
|
||||
<ChatWindow />
|
||||
<ChatFab />
|
||||
</div>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</div>
|
||||
<ModalRegistry />
|
||||
<SearchCommand />
|
||||
</DashboardGuard>
|
||||
</OnboardingGate>
|
||||
</DesktopNavigationProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { render, screen, act } from "@testing-library/react";
|
||||
import { useState } from "react";
|
||||
import { OnboardingGate } from "./onboarding-gate";
|
||||
|
||||
describe("OnboardingGate", () => {
|
||||
it("renders children when a workspace exists at mount", () => {
|
||||
render(
|
||||
<OnboardingGate
|
||||
hasWorkspace={true}
|
||||
onboarding={() => <div data-testid="wizard">wizard</div>}
|
||||
>
|
||||
<div data-testid="main">main shell</div>
|
||||
</OnboardingGate>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("main")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("wizard")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps the wizard mounted even after hasWorkspace flips to true mid-flow", () => {
|
||||
// Controls the hasWorkspace prop from outside so we can simulate
|
||||
// step 0 of the wizard creating a workspace while steps 1-3 still need
|
||||
// to render. The gate should ignore the prop change and hold the wizard.
|
||||
function Harness() {
|
||||
const [hasWorkspace, setHasWorkspace] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="grant-workspace"
|
||||
onClick={() => setHasWorkspace(true)}
|
||||
>
|
||||
grant
|
||||
</button>
|
||||
<OnboardingGate
|
||||
hasWorkspace={hasWorkspace}
|
||||
onboarding={() => <div data-testid="wizard">wizard</div>}
|
||||
>
|
||||
<div data-testid="main">main shell</div>
|
||||
</OnboardingGate>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render(<Harness />);
|
||||
expect(screen.getByTestId("wizard")).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
screen.getByTestId("grant-workspace").click();
|
||||
});
|
||||
|
||||
// Prop change alone does not dismiss the wizard — only onComplete does.
|
||||
expect(screen.getByTestId("wizard")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("main")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("transitions to children after the wizard calls onComplete", () => {
|
||||
render(
|
||||
<OnboardingGate
|
||||
hasWorkspace={false}
|
||||
onboarding={(onComplete) => (
|
||||
<button type="button" data-testid="finish" onClick={onComplete}>
|
||||
finish
|
||||
</button>
|
||||
)}
|
||||
>
|
||||
<div data-testid="main">main shell</div>
|
||||
</OnboardingGate>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("finish")).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
screen.getByTestId("finish").click();
|
||||
});
|
||||
|
||||
expect(screen.getByTestId("main")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("finish")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
33
apps/desktop/src/renderer/src/components/onboarding-gate.tsx
Normal file
33
apps/desktop/src/renderer/src/components/onboarding-gate.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useState, type ReactNode } from "react";
|
||||
|
||||
/**
|
||||
* Renders `onboarding` as a full-screen takeover when the user logs in
|
||||
* without a workspace, otherwise renders `children`.
|
||||
*
|
||||
* The "needs onboarding" decision is frozen at first mount via the lazy
|
||||
* useState initializer — creating a workspace mid-wizard (step 0) must not
|
||||
* unmount the wizard, because steps 1-3 (runtime, agent, get started) still
|
||||
* need to run. Calling the `onComplete` passed to `onboarding` flips the
|
||||
* gate to `children`.
|
||||
*
|
||||
* Assumes `hasWorkspace` is definitive at first render: desktop only mounts
|
||||
* DesktopShell after AppContent's bootstrapping flag resolves, so the first
|
||||
* render of this component reflects the actual server state.
|
||||
*/
|
||||
export function OnboardingGate({
|
||||
hasWorkspace,
|
||||
onboarding,
|
||||
children,
|
||||
}: {
|
||||
hasWorkspace: boolean;
|
||||
onboarding: (onComplete: () => void) => ReactNode;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [initialNeedsOnboarding] = useState(() => !hasWorkspace);
|
||||
const [onboardingDone, setOnboardingDone] = useState(false);
|
||||
|
||||
if (initialNeedsOnboarding && !onboardingDone) {
|
||||
return <>{onboarding(() => setOnboardingDone(true))}</>;
|
||||
}
|
||||
return <>{children}</>;
|
||||
}
|
||||
1
apps/desktop/test/setup.ts
Normal file
1
apps/desktop/test/setup.ts
Normal file
@@ -0,0 +1 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
@@ -4,7 +4,8 @@
|
||||
"src/renderer/src/env.d.ts",
|
||||
"src/renderer/src/**/*",
|
||||
"src/renderer/src/**/*.tsx",
|
||||
"src/preload/*.d.ts"
|
||||
"src/preload/*.d.ts",
|
||||
"test/setup.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
globals: true,
|
||||
include: ["src/**/*.test.ts", "scripts/**/*.test.mjs"],
|
||||
environment: "node",
|
||||
include: ["src/**/*.test.{ts,tsx}", "scripts/**/*.test.mjs"],
|
||||
environment: "jsdom",
|
||||
setupFiles: ["./test/setup.ts"],
|
||||
passWithNoTests: true,
|
||||
},
|
||||
});
|
||||
|
||||
45
pnpm-lock.yaml
generated
45
pnpm-lock.yaml
generated
@@ -126,6 +126,9 @@ importers:
|
||||
electron-updater:
|
||||
specifier: ^6.8.3
|
||||
version: 6.8.3
|
||||
fix-path:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
react-router-dom:
|
||||
specifier: ^7.6.0
|
||||
version: 7.14.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
@@ -148,6 +151,12 @@ importers:
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4
|
||||
version: 4.2.2(vite@8.0.1(@types/node@25.5.0)(jiti@2.6.1))
|
||||
'@testing-library/jest-dom':
|
||||
specifier: 'catalog:'
|
||||
version: 6.9.1
|
||||
'@testing-library/react':
|
||||
specifier: 'catalog:'
|
||||
version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@types/node':
|
||||
specifier: 'catalog:'
|
||||
version: 25.5.0
|
||||
@@ -169,6 +178,9 @@ importers:
|
||||
electron-vite:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0(vite@8.0.1(@types/node@25.5.0)(jiti@2.6.1))
|
||||
jsdom:
|
||||
specifier: 'catalog:'
|
||||
version: 29.0.1(@noble/hashes@1.8.0)
|
||||
react:
|
||||
specifier: 'catalog:'
|
||||
version: 19.2.3
|
||||
@@ -3533,6 +3545,10 @@ packages:
|
||||
resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
default-shell@2.2.0:
|
||||
resolution: {integrity: sha512-sPpMZcVhRQ0nEMDtuMJ+RtCxt7iHPAMBU+I4tAlo5dU1sjRpNax0crj6nR3qKpvVnckaQ9U38enXcwW9nZJeCw==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
defaults@1.0.4:
|
||||
resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==}
|
||||
|
||||
@@ -4000,6 +4016,10 @@ packages:
|
||||
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
fix-path@5.0.0:
|
||||
resolution: {integrity: sha512-erEWGGCN7RIu1bXTCfNVpVBdm0f5mwcbeja+4QXiEZzIQukP401sbpu8gd3Ny1vS34YNeswyMO0TdT2tP5OlHA==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
flat-cache@4.0.1:
|
||||
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
|
||||
engines: {node: '>=16'}
|
||||
@@ -6113,6 +6133,14 @@ packages:
|
||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
shell-env@4.0.3:
|
||||
resolution: {integrity: sha512-Ioe5h+hCDZ7pKL5+JGzbtPvZ5ESMHePZ8nLxohlDL+twmlcmutttMhRkrQOed8DeLT8mkYBgbwZfohe8pqaA3g==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
shell-path@3.1.0:
|
||||
resolution: {integrity: sha512-s/9q9PEtcRmDTz69+cJ3yYBAe9yGrL7e46gm2bU4pQ9N48ecPK9QrGFnLwYgb4smOHskx4PL7wCNMktW2AoD+g==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
shiki@3.23.0:
|
||||
resolution: {integrity: sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==}
|
||||
|
||||
@@ -9673,6 +9701,8 @@ snapshots:
|
||||
bundle-name: 4.1.0
|
||||
default-browser-id: 5.0.1
|
||||
|
||||
default-shell@2.2.0: {}
|
||||
|
||||
defaults@1.0.4:
|
||||
dependencies:
|
||||
clone: 1.0.4
|
||||
@@ -10376,6 +10406,11 @@ snapshots:
|
||||
locate-path: 6.0.0
|
||||
path-exists: 4.0.0
|
||||
|
||||
fix-path@5.0.0:
|
||||
dependencies:
|
||||
shell-path: 3.1.0
|
||||
strip-ansi: 7.2.0
|
||||
|
||||
flat-cache@4.0.1:
|
||||
dependencies:
|
||||
flatted: 3.4.2
|
||||
@@ -13081,6 +13116,16 @@ snapshots:
|
||||
|
||||
shebang-regex@3.0.0: {}
|
||||
|
||||
shell-env@4.0.3:
|
||||
dependencies:
|
||||
default-shell: 2.2.0
|
||||
execa: 5.1.1
|
||||
strip-ansi: 7.2.0
|
||||
|
||||
shell-path@3.1.0:
|
||||
dependencies:
|
||||
shell-env: 4.0.3
|
||||
|
||||
shiki@3.23.0:
|
||||
dependencies:
|
||||
'@shikijs/core': 3.23.0
|
||||
|
||||
Reference in New Issue
Block a user