diff --git a/.claude/launch.json b/.claude/launch.json index 2a757ef..c7156e5 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -21,6 +21,21 @@ "5174" ], "port": 5174 + }, + { + "name": "dev-desktop", + "runtimeExecutable": "deno", + "runtimeArgs": [ + "run", + "--env-file=.env.desktop", + "--unstable-cron", + "-A", + "npm:vite", + "dev", + "--port", + "5175" + ], + "port": 5175 } ] } diff --git a/.env.desktop.example b/.env.desktop.example index b90a162..bc1b4ca 100644 --- a/.env.desktop.example +++ b/.env.desktop.example @@ -1,13 +1,16 @@ -# Desktop build (`deno task desktop`). Copy to `.env.desktop`. +# Desktop build (`deno task desktop:cef` / `deno task desktop`). Copy to `.env.desktop`. # # QUANTUM_TARGET=desktop switches vite.config.ts to the Deno adapter, whose -# `.deno-deploy/server.ts` output `deno desktop` packages. The rest is the local -# runtime config the packaged app boots with (until the first-launch mode screen -# lands — tasks 2.2/7.1). +# `.deno-deploy/server.ts` output `deno desktop` packages. +# QUANTUM_MODE=local runs the embedded server in single-user mode. +# QUANTUM_DESKTOP=1 makes the app read its chosen mode + display name from an +# app-config file at runtime and show the first-launch mode-selection screen when +# absent — instead of baking those into the binary. QUANTUM_TARGET=desktop QUANTUM_MODE=local -DB_PATH=./data/desktop-spike.db +QUANTUM_DESKTOP=1 -# Omit to hit the first-launch /welcome screen instead of seeding a name. -QUANTUM_LOCAL_DISPLAY_NAME=Graham +# Where the local database (and the quantum-desktop.json app-config beside it) +# live. Defaults to the OS app-data dir if unset. +DB_PATH=./data/desktop-spike.db diff --git a/deno.json b/deno.json index 988bf1d..363b72d 100644 --- a/deno.json +++ b/deno.json @@ -12,7 +12,9 @@ "build": "deno run -A npm:vite build", "start": "deno run --env-file --unstable-cron -A build/index.js", "desktop:build": "deno run -A --env-file=.env.desktop npm:vite build", - "desktop": "deno desktop --env-file=.env.desktop --unstable-cron --no-check -A .", + "desktop:patch": "deno run --allow-read --allow-write scripts/patch-route-config.ts", + "desktop": "deno task desktop:patch && deno desktop --backend cef --env-file=.env.desktop --unstable-cron --no-check -A .", + "desktop:webview": "deno task desktop:patch && deno desktop --env-file=.env.desktop --unstable-cron --no-check -A .", "check": "deno run -A npm:@sveltejs/kit/svelte-kit sync && deno run -A npm:svelte-check --tsconfig ./tsconfig.json", "test": "deno test -A src", "fmt": "deno fmt", diff --git a/openspec/changes/add-desktop-local-remote-modes/design.md b/openspec/changes/add-desktop-local-remote-modes/design.md index a26210e..c553858 100644 --- a/openspec/changes/add-desktop-local-remote-modes/design.md +++ b/openspec/changes/add-desktop-local-remote-modes/design.md @@ -264,6 +264,55 @@ No data migration. This change is additive at the boot and packaging layers: - The desktop artifact is a new build output; the container image build is unchanged. +## Resolved During Implementation + +Getting `deno desktop` to actually package and run this SvelteKit app took a +chain of discoveries, each committed as a fix: + +- **Detection needs `svelte.config.js`.** This project keeps its SvelteKit + config inline in `vite.config.ts`, so there was no `svelte.config.js`. + `deno desktop` detects SvelteKit by that file; without it, it treats the app + as generic Vite and tries to embed a nonexistent `dist/`. Added a + `svelte.config.js` stub (SvelteKit ignores it — warns it's ignored — but + `deno desktop` reads it both to detect the framework and to pick the adapter + output). It must stay in sync with `vite.config.ts`'s adapter selection. +- **Only the Deno adapter is consumable.** `@sveltejs/adapter-node`'s `build/` + layout isn't found by `deno desktop` — its generated entry imports + `build/server/*.js` files the adapter doesn't emit, which `--no-check` hid and + which crashed the binary at runtime. The desktop build uses + `@deno/svelte-adapter` (`.deno-deploy/…`) instead, selected by + `QUANTUM_TARGET=desktop` in vite.config.ts. The server/container stays on + adapter-node. +- **`--no-check` is required** for the desktop compile: the Deno adapter's + generated `handler.ts` has a benign type skew against `@sveltejs/kit@2.69.x` + (`RouteParam.matcher`). It hides only that; the fatal adapter-node module + errors above are avoided by not using adapter-node at all. +- **Migrations must be embedded, not read from disk.** The compiled binary has + no `migrations/` directory relative to its CWD, so `readdirSync` failed + wherever it was launched from. `scripts/generate-migrations.ts` bakes the SQL + into `migrations.generated.ts`; `runMigrations()` uses it when no dir is + passed (a drift test guards staleness). This also frees the server container + from shipping the directory. +- **`@deno/experimental-route-config@0.0.5` has a Windows static-serving bug.** + `parseConfig` resolves static destinations to absolute backslash paths, but + `parsePattern`'s regex only matches forward-slash `/:param`, so the + `/_app/immutable/:file*` catch-all is never substituted → every asset request + opens the literal `…\:file*` → os error 123 → crash on first paint. + `scripts/patch-route-config.ts` widens the regex (`\/:` → `[\\/]:`); it's + chained into the desktop build so the fix is bundled, and reported upstream. +- **The default OS-webview backend does not render here.** The window opened and + closed instantly. Isolating the app in a real browser proved the app is + healthy (renders, no errors), so the failure is `deno desktop`'s `webview` + backend (WebView2/laufey) on this machine. `--backend cef` (bundled Chromium, + ~300MB binary vs ~68MB) renders reliably. `desktop` uses CEF by default; + `desktop:webview` keeps the smaller variant for machines where it works. +- **First-launch is app-config-driven, not baked.** A shipped binary can't have + a mode or display name compiled in. `QUANTUM_DESKTOP=1` makes the app read + `quantum-desktop.json` (beside the database, outside it) at runtime; when + absent, the hook routes to a `/setup` mode-selection screen → local → the + existing `/welcome` name screen → dashboard, all persisted. Remote mode's + option is shown but deferred (see below). + ## Open Questions - **Tray/background-resident runtime (parked).** `deno desktop` exposes a diff --git a/openspec/changes/add-desktop-local-remote-modes/tasks.md b/openspec/changes/add-desktop-local-remote-modes/tasks.md index faf96d0..376825a 100644 --- a/openspec/changes/add-desktop-local-remote-modes/tasks.md +++ b/openspec/changes/add-desktop-local-remote-modes/tasks.md @@ -15,34 +15,42 @@ fixed-port `Deno.serve` works in the packaged binary, else publish the per-launch URL via `agent.json`. Still needs the binary to settle that sub-question. -- [~] 1.3 **Packaging sanity.** Confirm `deno desktop` (2.9.x) auto-detects this - SvelteKit project and produces a runnable Windows binary from the existing - build, with the webview backend, before building mode logic on top. — Bundling - now works (finding chain, all resolved in code): (1) detection needs a - `svelte.config.js` — without it `deno desktop` sees generic Vite and tries to - embed a nonexistent `dist/`; added a detection stub (SvelteKit ignores it, - uses the inline vite config, but the file makes `deno desktop` detect - SvelteKit). (2) SvelteKit support wants the Deno adapter's - `.deno-deploy/server.ts`, not adapter-node's `build/`; added - `@deno/svelte-adapter`, selected via `QUANTUM_TARGET=desktop` in - vite.config.ts (server/container stays adapter-node). (3) the Deno adapter's - generated `handler.ts` type-skews against `@sveltejs/kit@2.69.2` - (`RouteParam.matcher`), so compile needs `--no-check`. (4) the compiled binary - reads `migrations/` off disk via a relative path that isn't bundled; fixed - with `--include=./migrations`. With those, the binary **boots**: runtime - loads, `getConfig()` succeeds in local mode (env baked from `.env.desktop`), - it binds a real `127.0.0.1:` (confirming spike 1.2 empirically), - and migrations run. **BLOCKED (upstream):** serving static client assets - crashes — `@deno/experimental-route-config@0.0.5` (via - `@deno/svelte-adapter@0.2.1`, both latest) tries to `open` the literal - catch-all pattern `…/_app/immutable/:file*` instead of the resolved filename; - `:`/`*` are illegal in Windows paths (os error 123). Not our code; no released - fix. A working native window on Windows is blocked until the adapter is fixed - upstream or we bypass it with a custom `Deno.serve` entrypoint (the docs' - escape hatch). Also: `deno desktop .` compiles then exits (run the emitted - `quantum/quantum.exe`); `--hmr` runs the `dev` task, which loads `.env` - (server config) and collides with local mode — neither gives a quick live - window today. +- [x] 1.3 **Packaging sanity.** Confirm `deno desktop` (2.9.x) auto-detects this + SvelteKit project and produces a runnable Windows binary from the existing + build, with the webview backend, before building mode logic on top. — + Bundling now works (finding chain, all resolved in code): (1) detection + needs a `svelte.config.js` — without it `deno desktop` sees generic Vite + and tries to embed a nonexistent `dist/`; added a detection stub + (SvelteKit ignores it, uses the inline vite config, but the file makes + `deno desktop` detect SvelteKit). (2) SvelteKit support wants the Deno + adapter's `.deno-deploy/server.ts`, not adapter-node's `build/`; added + `@deno/svelte-adapter`, selected via `QUANTUM_TARGET=desktop` in + vite.config.ts (server/container stays adapter-node). (3) the Deno + adapter's generated `handler.ts` type-skews against `@sveltejs/kit@2.69.2` + (`RouteParam.matcher`), so compile needs `--no-check`. (4) the compiled + binary reads `migrations/` off disk via a relative path that isn't + bundled; fixed with `--include=./migrations`. With those, the binary + **boots**: runtime loads, `getConfig()` succeeds in local mode (env baked + from `.env.desktop`), it binds a real `127.0.0.1:` (confirming + spike 1.2 empirically), and migrations run. **BLOCKED (upstream):** + serving static client assets crashes — + `@deno/experimental-route-config@0.0.5` (via `@deno/svelte-adapter@0.2.1`, + both latest) tries to `open` the literal catch-all pattern + `…/_app/immutable/:file*` instead of the resolved filename; `:`/`*` are + illegal in Windows paths (os error 123). Not our code; no released fix. A + working native window on Windows is blocked until the adapter is fixed + upstream or we bypass it with a custom `Deno.serve` entrypoint (the docs' + escape hatch). Also: `deno desktop .` compiles then exits (run the emitted + `quantum/quantum.exe`); `--hmr` runs the `dev` task, which loads `.env` + (server config) and collides with local mode — neither gives a quick live + window today. + + **RESOLVED** (see design's "Resolved During Implementation"): the static + bug was fixed by patching `@deno/experimental-route-config`'s regex + (`scripts/patch-route-config.ts`, chained into the build); migrations are + embedded via codegen (not `--include`); and the OS-webview backend, which + failed to render on Windows, was replaced with `--backend cef`. The window + now opens, stays, and renders local-mode Quantum end to end. ## 2. Build and mode plumbing @@ -52,9 +60,13 @@ `dev:local` convenience task; existing tasks untouched. (Booting the packaged app end-to-end still needs the app-config-driven mode selection of tasks 2.2/7.1.) -- [ ] 2.2 Add a desktop launch config and the app-config read/write for the +- [x] 2.2 Add a desktop launch config and the app-config read/write for the persisted mode (a file in the OS app-data directory, outside the SQLite - database). Mode is one of `local` | `remote`, absent until first launch. + database). Mode is one of `local` | `remote`, absent until first launch. — + `src/lib/server/desktop-config.ts` reads/writes `quantum-desktop.json` + beside the database; `QUANTUM_DESKTOP=1` (baked into the desktop env) + makes `loadConfig` carry a `desktop` flag so the hook reads the app-config + at runtime rather than a baked mode. `dev-desktop` launch config added. - [ ] 2.3 Extend the release/build scripts to produce and version the desktop artifact alongside the container image, reusing the existing ChronVer version source. (Coordinate with the `release` skill's flow; do not fork @@ -157,27 +169,39 @@ the former tasks 6.1/6.3/6.4/6.5 are dropped. (See design decision 5.) ## 7. First-launch experience -- [ ] 7.1 Build the first-launch mode-selection screen: Local ("just me, on this +- [x] 7.1 Build the first-launch mode-selection screen: Local ("just me, on this computer") vs Remote ("I have a Quantum server"), shell-less and calm per - DESIGN.md, persisting the choice to app-config. -- [ ] 7.2 Local branch: prompt for a display name and seed the local user with - it. + DESIGN.md, persisting the choice to app-config. — `src/routes/setup`; + shell-less, Local functional (writes `{mode:'local'}` and routes on to + `/welcome`), Remote shown but marked "soon" (deferred, see 7.3). Verified + the full flow live and in the CEF window. +- [x] 7.2 Local branch: prompt for a display name and seed the local user with + it. — `src/routes/welcome` (built earlier); reached after the mode screen. - [ ] 7.3 Remote branch: collect the server address and point the webview at it - (task 6); the server's cookie login takes over from there. + (task 6); the server's cookie login takes over from there. — DEFERRED. The + option is present on `/setup` but disabled ("soon"). Remote-in-desktop is + architecturally uncertain: `deno desktop` always runs the embedded server + and points the webview at localhost, so navigating the webview to an + external origin needs its own spike. Scoped out of this pass by decision. - [ ] 7.4 Add the reset action (Settings) that clears app-config and returns to the mode-selection screen on next launch. Confirm with a plain sentence about what reset does and does not delete (local data stays on disk). ## 8. Verification -- [ ] 8.1 Run `deno task test` and `deno task check`. Confirm the server build +- [x] 8.1 Run `deno task test` and `deno task check`. Confirm the server build and container image are unchanged (server-mode tests all green, no config change required for existing deployments). -- [ ] 8.2 Local mode end to end: fresh launch → choose Local → set a display - name → paste a SimpleFIN token → sync populates → categorize a transaction - and confirm the display name in provenance → close and relaunch (starts in - Local, syncs on launch) → connect a local agent to the MCP URL from - `agent.json` and categorize via a token, confirming `agent` provenance. +- [~] 8.2 Local mode end to end: fresh launch → choose Local → set a display + name → paste a SimpleFIN token → sync populates → categorize a transaction and + confirm the display name in provenance → close and relaunch (starts in Local, + syncs on launch) → connect a local agent to the MCP URL from `agent.json` and + categorize via a token, confirming `agent` provenance. — The first-launch path + (fresh → /setup → Local → /welcome → dashboard) is verified live and confirmed + rendering in the CEF window by Graham. The SimpleFIN → sync → categorize → + agent-provenance tail still wants a real bank connection exercised in the + packaged app; the pieces are individually tested (sync, MCP, provenance) but + not yet driven as one desktop session. - [ ] 8.3 Remote mode end to end against a dev server: choose Remote → the server's cookie login runs in the webview → app authenticates as the logged-in user via cookie → no local database created. diff --git a/scripts/patch-route-config.ts b/scripts/patch-route-config.ts new file mode 100644 index 0000000..ea9a5bc --- /dev/null +++ b/scripts/patch-route-config.ts @@ -0,0 +1,48 @@ +// Durable patch for a Windows-only bug in @deno/experimental-route-config@0.0.5 +// (pulled in by @deno/svelte-adapter, used only by the `deno desktop` build). +// +// The bug: parseConfig() resolves a static route's destination to an absolute +// Windows path with backslashes (…\_app\immutable\:file*), but parsePattern()'s +// regex only recognizes forward-slash `/:param` syntax. So on Windows the +// `:file*` catch-all is never parsed as a parameter and every client asset +// request tries to open the literal path `…\:file*` → os error 123 → the +// desktop app crashes on first paint. Widening the regex to accept `\` fixes it. +// +// Runs before the desktop compile (chained in deno.json's `desktop` task) so the +// patched code is bundled into the binary. Idempotent; re-run after any +// `deno install`. Reported upstream. Remove once the package ships a fix. + +const target = new URL( + "../node_modules/.deno/@deno+experimental-route-config@0.0.5/node_modules/@deno/experimental-route-config/dist/pattern.js", + import.meta.url, +); + +const NEEDLE = "/(\\/:"; // the source text: /(\/: +const PATCHED = "/([\\\\/]:"; // produces: /([\\/]: + +let src: string; +try { + src = Deno.readTextFileSync(target); +} catch { + console.log( + "@deno/experimental-route-config not installed; nothing to patch", + ); + Deno.exit(0); +} + +if (src.includes(PATCHED)) { + console.log("route-config already patched"); + Deno.exit(0); +} +if (!src.includes(NEEDLE)) { + console.error( + "patch target not found — @deno/experimental-route-config may have changed. " + + "Re-verify the desktop static-serving fix before shipping.", + ); + Deno.exit(1); +} + +Deno.writeTextFileSync(target, src.replace(NEEDLE, PATCHED)); +console.log( + "patched @deno/experimental-route-config for the Windows static-path bug", +); diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 9264306..550f5cf 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -14,6 +14,7 @@ import { } from "$lib/server/services/sessions"; import { verifyToken } from "$lib/server/services/api-tokens"; import { getUser, upsertUser } from "$lib/server/services/users"; +import { readDesktopConfig } from "$lib/server/desktop-config"; import { runSync } from "$lib/server/services/sync"; import { handleMcpRequest } from "$lib/server/mcp/server"; import process from "node:process"; @@ -202,10 +203,25 @@ async function handleLocal( } event.locals.apiToken = null; + + // Desktop first launch: the mode (local/remote) is chosen on a setup screen + // and persisted in app-config, not baked into the build. Until local mode is + // chosen, route everything to /setup. + if (getConfig().desktop) { + const chosen = readDesktopConfig(getConfig().dbPath); + if (!chosen || chosen.mode !== "local") { + event.locals.user = null; + if (path !== "/setup" && !PUBLIC_PATHS.has(path)) { + redirect(303, "/setup"); + } + return resolve(event); + } + if (path === "/setup") redirect(303, "/"); // already chosen + } + const localUser = getUser(db, LOCAL_DID); - // Until the user has completed first-launch (no local identity yet), send - // them to the setup screen instead of a login page. + // Local mode chosen but the display name not yet set: go to /welcome. if (!localUser) { event.locals.user = null; if (path !== "/welcome" && !PUBLIC_PATHS.has(path)) { @@ -215,7 +231,9 @@ async function handleLocal( } event.locals.user = localUser; - // Login and welcome are meaningless once set up. - if (path === "/login" || path === "/welcome") redirect(303, "/"); + // Login, welcome, and setup are meaningless once fully set up. + if (path === "/login" || path === "/welcome" || path === "/setup") { + redirect(303, "/"); + } return resolve(event); } diff --git a/src/lib/server/config.test.ts b/src/lib/server/config.test.ts index a163fbc..9d1481c 100644 --- a/src/lib/server/config.test.ts +++ b/src/lib/server/config.test.ts @@ -99,6 +99,19 @@ Deno.test("local mode defaults the database path when DB_PATH is unset", () => { } }); +Deno.test("desktop flag comes from QUANTUM_DESKTOP in local mode", () => { + const base = { QUANTUM_MODE: "local", DB_PATH: "./data/x.db" }; + if (loadConfig(base).desktop !== false) { + throw new Error("desktop should default to false"); + } + if (loadConfig({ ...base, QUANTUM_DESKTOP: "1" }).desktop !== true) { + throw new Error("QUANTUM_DESKTOP=1 should set desktop true"); + } + if (loadConfig(VALID_ENV).desktop !== false) { + throw new Error("server mode is never a desktop build"); + } +}); + Deno.test("local mode rejects server auth configuration", () => { for ( const bad of [ diff --git a/src/lib/server/config.ts b/src/lib/server/config.ts index 128afed..94e76d4 100644 --- a/src/lib/server/config.ts +++ b/src/lib/server/config.ts @@ -27,6 +27,13 @@ export interface Config { * gates the app behind the setup screen. */ localDisplayName?: string; + /** + * True when running as the packaged `deno desktop` build (QUANTUM_DESKTOP=1). + * The app then reads its mode/settings from an app-config file at runtime and + * shows the first-launch mode-selection screen when that file is absent — + * rather than having a mode baked in. `dev:local` and tests leave this false. + */ + desktop: boolean; } /** The OS-conventional per-user data directory for the local-mode database. */ @@ -130,6 +137,7 @@ export function loadConfig( allowedDids, dbPath: dbPath!, oauthPrivateKeyJwk, + desktop: false, }; } @@ -162,6 +170,7 @@ function loadLocalConfig(env: Record): Config { dbPath: env.DB_PATH?.trim() || defaultLocalDbPath(env), oauthPrivateKeyJwk: {}, localDisplayName: env.QUANTUM_LOCAL_DISPLAY_NAME?.trim() || undefined, + desktop: env.QUANTUM_DESKTOP?.trim() === "1", }; } diff --git a/src/lib/server/desktop-config.ts b/src/lib/server/desktop-config.ts new file mode 100644 index 0000000..462a954 --- /dev/null +++ b/src/lib/server/desktop-config.ts @@ -0,0 +1,53 @@ +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; + +// Desktop app-config (add-desktop-local-remote-modes): the mode the user chose +// at first launch, persisted OUTSIDE the SQLite database. Local mode has a +// database; remote mode has none — so the mode itself can't live in the DB, and +// it must be known before the app decides what to do. Stored next to where the +// local database would live, in the OS app-data directory. + +export interface DesktopConfig { + mode: "local" | "remote"; + /** The remote Quantum origin, when mode is `remote`. */ + serverUrl?: string; +} + +function configPath(dbPath: string): string { + return join(dirname(dbPath), "quantum-desktop.json"); +} + +/** The chosen desktop config, or null on first launch (not yet chosen). */ +export function readDesktopConfig(dbPath: string): DesktopConfig | null { + let raw: string; + try { + raw = readFileSync(configPath(dbPath), "utf-8"); + } catch { + return null; // absent: first launch + } + try { + const parsed = JSON.parse(raw) as DesktopConfig; + if (parsed.mode === "local" || parsed.mode === "remote") return parsed; + } catch { + // corrupt: treat as unconfigured so the user can re-choose + } + return null; +} + +export function writeDesktopConfig( + dbPath: string, + config: DesktopConfig, +): void { + const path = configPath(dbPath); + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, JSON.stringify(config, null, 2)); +} + +/** Clear the chosen mode (the "reset" action), returning to first launch. */ +export function clearDesktopConfig(dbPath: string): void { + try { + writeFileSync(configPath(dbPath), "{}"); + } catch { + // nothing to clear + } +} diff --git a/src/routes/setup/+page.server.ts b/src/routes/setup/+page.server.ts new file mode 100644 index 0000000..aa7766f --- /dev/null +++ b/src/routes/setup/+page.server.ts @@ -0,0 +1,21 @@ +import { redirect } from "@sveltejs/kit"; +import { getConfig } from "$lib/server/config"; +import { writeDesktopConfig } from "$lib/server/desktop-config"; +import type { Actions, PageServerLoad } from "./$types"; + +// First-launch mode selection for the desktop build. Only meaningful there; a +// server or dev:local run never reaches it. +export const load: PageServerLoad = () => { + if (!getConfig().desktop) redirect(303, "/"); + return {}; +}; + +export const actions: Actions = { + local: () => { + const config = getConfig(); + if (!config.desktop) redirect(303, "/"); + writeDesktopConfig(config.dbPath, { mode: "local" }); + // The hook now routes to /welcome to choose a display name. + redirect(303, "/"); + }, +}; diff --git a/src/routes/setup/+page.svelte b/src/routes/setup/+page.svelte new file mode 100644 index 0000000..add38c5 --- /dev/null +++ b/src/routes/setup/+page.svelte @@ -0,0 +1,119 @@ + + +
+
+

Quantum

+

How do you want to use Quantum?

+ +
{ + choosing = true; + return async ({ update }) => { + await update(); + choosing = false; + }; + }} + > + +
+ +
+ I have a Quantum server soon + + Connecting this app to a self-hosted Quantum server is coming. For now, + open your server in a web browser. + +
+
+
+ + diff --git a/svelte.config.js b/svelte.config.js index 6e41c0f..82a03e0 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -2,14 +2,15 @@ import adapterNode from "@sveltejs/adapter-node"; import adapterDeno from "@deno/svelte-adapter"; import process from "node:process"; -// Detection stub for `deno desktop` (add-desktop-local-remote-modes). +// Detection + output selection for `deno desktop` (add-desktop-local-remote-modes). // -// SvelteKit itself IGNORES this file — the real config is passed inline to the -// `sveltekit()` plugin in vite.config.ts, and SvelteKit warns that this file is -// ignored. But `deno desktop` detects the framework as SvelteKit by this file's -// presence; without it, it falls back to generic Vite and tries to embed a -// nonexistent `dist/`. The adapter here mirrors vite.config.ts so the two never -// drift if SvelteKit ever starts reading this file. +// SvelteKit itself IGNORES this file — the real build config is passed inline to +// the `sveltekit()` plugin in vite.config.ts (SvelteKit warns it's ignored). But +// `deno desktop` reads THIS file to (a) detect the framework as SvelteKit — without +// it, it treats us as generic Vite and looks for a nonexistent `dist/` — and (b) +// decide which adapter's output to package. So the adapter here MUST match +// vite.config.ts: Deno adapter (.deno-deploy) for the desktop build, adapter-node +// otherwise. const adapter = process.env.QUANTUM_TARGET === "desktop" ? adapterDeno() : adapterNode(); diff --git a/vite.config.ts b/vite.config.ts index 6cca4d1..769ca3b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,8 +6,10 @@ import process from "node:process"; // The server/container deployment uses adapter-node (run under Deno via // `deno task start`). The `deno desktop` build sets QUANTUM_TARGET=desktop and -// uses the Deno Deploy adapter, whose `.deno-deploy/server.ts` output is what -// `deno desktop` detects and packages (add-desktop-local-remote-modes). +// uses the Deno Deploy adapter — the only adapter `deno desktop` actually +// consumes (adapter-node's output layout it can't resolve). Its generated +// static handler has a Windows bug we replace at build time; see +// scripts/build-desktop.ts (add-desktop-local-remote-modes). const adapter = process.env.QUANTUM_TARGET === "desktop" ? adapterDeno() : adapterNode();