@nonamelego/dsh-catppuccin 0.5.6 → 0.5.7-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -140,8 +140,10 @@ Installing from the repo works the same way: `dsh plugin --profile desktop add h
140
140
  > [DSH Desktop](https://github.com/anywhere-labs/deepseek-harness-desktop) does the same — both boot
141
141
  > `$DSH_HOME/profiles/desktop`, so the command above works for either. This plugin's desktop support
142
142
  > targets the **official web + desktop** builds; the community shell's `desktopProfiles` service probe
143
- > is kept, while the official shell is recognized through the `DSH_DESKTOP_NODE_EXECUTABLE` it injects
144
- > (see `src/profile-detect.ts`).
143
+ > is kept. The official shell's profile process carries **no** dedicated env marker (its
144
+ > `DSH_DESKTOP_NODE_EXECUTABLE` is injected only into its package-install children), so the official
145
+ > desktop build is currently treated as plain web for upgrade copy; the profile name and the settings
146
+ > round-trip are unaffected (see the comment in `src/profile-detect.ts`).
145
147
 
146
148
  ### Option 2: from the repository
147
149
 
package/README.md CHANGED
@@ -130,8 +130,9 @@ dsh plugin --profile desktop add @nonamelego/dsh-catppuccin
130
130
  > 仓库里的 `apps/desktop` / `apps/desktop-host`(Electron,仍在开发中)与社区的
131
131
  > [DSH Desktop](https://github.com/anywhere-labs/deepseek-harness-desktop) 都启动
132
132
  > `$DSH_HOME/profiles/desktop`,所以**上面的命令对两者都成立**。本插件的桌面支持以
133
- > **官方 web + 官方 desktop** 为维护核心;社区壳的 `desktopProfiles` 服务探测也保留,
134
- > 官方壳则靠它注入的 `DSH_DESKTOP_NODE_EXECUTABLE` 识别(见 `src/profile-detect.ts`)。
133
+ > **官方 web + 官方 desktop** 为维护核心;社区壳的 `desktopProfiles` 服务探测也保留。
134
+ > 但官方壳的 profile 进程**没有**可用的专用环境标记(它的 `DSH_DESKTOP_NODE_EXECUTABLE` 只注入给
135
+ > 包安装子进程),所以官方桌面版目前会被当成 web 来给升级提示;不影响 profile 名与设置的读写。
135
136
 
136
137
  ### 方式二:从仓库安装
137
138
 
package/lib/index.js CHANGED
@@ -4,7 +4,7 @@ import { join } from "node:path";
4
4
  import { readFileSync } from "node:fs";
5
5
  import Schema from "@deepseek-ai/schemastery";
6
6
  //#region package.json
7
- var version = "0.5.6";
7
+ var version = "0.5.7-beta.0";
8
8
  //#endregion
9
9
  //#region src/versions.ts
10
10
  /** Parse a semver string, or null when it is not `v?x.y.z[-pre][+build]`. */
@@ -133,20 +133,45 @@ function selectNewest(current, tags, preferredChannel) {
133
133
  for (const candidate of candidates.slice(1)) if (compareVersions(best.version, candidate.version) < 0) best = candidate;
134
134
  return best;
135
135
  }
136
- /** Detect the OFFICIAL desktop shell from the environment.
136
+ /** Whether this process runs inside Electron (Node mode included).
137
137
  *
138
- * 官方仓(`deepseek-ai/deepseek-harness`)的 `apps/desktop-host` 启动 `desktop`
139
- * profile 时,会把 `DSH_DESKTOP_NODE_EXECUTABLE` 注入该进程的环境(同一个 profile
140
- * 目录 `$DSH_HOME/profiles/desktop`),这是官方 Electron 桌面版留下的唯一可靠信号。
141
- * 为什么不能只靠服务探测:官方壳**不提供**第三方启动器那个 `desktopProfiles` 服务
142
- * (2026-09-22 在官方仓搜 `desktopProfiles` 命中 0),于是官方桌面版下 Host 会把自己
143
- * 当成普通 web —— 升级命令提示与重启提示都退化,而 profile 名其实是对的。
138
+ * Electron-as-node(`ELECTRON_RUN_AS_NODE=1`)**仍然**填 `process.versions.electron`
139
+ * —— 2026-09-24 本机实测:Electron 37.10.3 下为 `'37.10.3'`,纯 node 下为 `undefined`。
140
+ * 官方桌面壳的 Host 正是由 Electron 以 node 模式 spawn 的(`apps/desktop/src/host-process.ts`
141
+ * 用 Electron 可执行文件 + `ELECTRON_RUN_AS_NODE=1`),所以这是它在 profile 进程里留下的**唯一**痕迹。
142
+ * @param versions - `process.versions` to inspect (injectable so tests need no real Electron).
143
+ * @returns whether the runtime is Electron rather than a plain Node.js.
144
+ */
145
+ function isElectronRuntime(versions = process.versions) {
146
+ const electron = versions.electron;
147
+ return typeof electron === "string" && electron.trim() !== "";
148
+ }
149
+ /**
150
+ * Detect a DSH desktop shell — two independent signals, either one is enough:
151
+ *
152
+ * 1. **社区壳**(`anywhere-labs/dsh-desktop`,含其 `dsh-desktop-next` 重写版)在 profile
153
+ * 进程的环境里放 `DSH_DESKTOP_NODE_EXECUTABLE`(它的包安装/内部启动器约定)。
154
+ * 2. **官方壳**(`deepseek-ai/deepseek-harness` 的 `apps/desktop` + `apps/desktop-host`,
155
+ * `private: true`、未发 npm)**什么都不设**:⚠️ 2026-09-24 取证推翻 2026-09-22 的假设——
156
+ * 上游架构说明原话是「`DSH_DESKTOP_NODE_EXECUTABLE` **仅为包安装注入**」
157
+ * (`.agents/notes/implemented/architecture/2026-09-11-desktop-electron-node-runtime.zh.md`),
158
+ * 代码印证:`host-process.ts` 以 `desktopNodeEnvironment(this.node, undefined, …)` 起 host,
159
+ * `bin === undefined` 时不设它;`apps/desktop-host/src/index.ts` 只在
160
+ * `runProfile({ packageManager: { env } })` 里给它。于是官方壳在 profile 进程里靠**运行时**识别:
161
+ * Host 是 Electron-as-node,故 `process.versions.electron` 有值(见 `isElectronRuntime`)。
162
+ *
163
+ * 官方仓**没有** `desktopProfiles` 服务(2026-09-22 全仓搜索命中 0),所以那路服务探测只覆盖社区壳;
164
+ * 两路都不命中时回落纯 web 文案——**profile 名不受影响**(`detectProfile` 扫 `profiles/` 目录命中 `desktop`),
165
+ * 持久化也不受影响(走 `configForms` seam,与壳无关)。
166
+ * 代价权衡:在「用 Electron 壳跑 dsh web」这类少见组合下会误判成桌面,而误判只影响提示文案措辞。
144
167
  * @param env - the environment to inspect (injectable so tests need no real desktop).
145
- * @returns whether this Host runs inside the official desktop shell.
168
+ * @param versions - `process.versions` to inspect (injectable for the same reason).
169
+ * @returns whether this process looks like a DSH desktop shell.
146
170
  */
147
- function isDesktopShellEnv(env) {
171
+ function isDesktopShellEnv(env, versions = process.versions) {
148
172
  const marker = env.DSH_DESKTOP_NODE_EXECUTABLE;
149
- return typeof marker === "string" && marker.trim() !== "";
173
+ if (typeof marker === "string" && marker.trim() !== "") return true;
174
+ return isElectronRuntime(versions);
150
175
  }
151
176
  /** The DSH home the running process owns — `$DSH_HOME` when set, else
152
177
  * `~/.dsh`. Port-independent and stable across restarts, so it is the right
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["pkg.version"],"sources":["../package.json","../src/versions.ts","../src/update-check.ts","../src/profile-detect.ts","../src/update-check/host.ts","../src/state.ts","../src/legacy-state.ts","../src/migrate-legacy.ts","../src/settings-catppuccin.ts","../src/index.ts"],"sourcesContent":["","/**\n * Minimal semver comparison for update detection — dependency-free, so the\n * Host half and the unit tests share one implementation. Follows the semver\n * 2.0.0 precedence rules for `major.minor.patch[-prerelease][+build]`:\n * build metadata is ignored, prerelease identifiers compare numerically when\n * both are numeric and ASCII-wise otherwise, and a prerelease of the same\n * core sorts before its stable release.\n */\n\n/** A parsed semver triple plus its prerelease identifiers. */\nexport interface ParsedVersion {\n major: number\n minor: number\n patch: number\n /** Prerelease identifiers: numeric parts become numbers, the rest strings. */\n prerelease: readonly (string | number)[]\n}\n\n/** Parse a semver string, or null when it is not `v?x.y.z[-pre][+build]`. */\nexport function parseVersion(raw: string): ParsedVersion | null {\n const match = /^v?(\\d+)\\.(\\d+)\\.(\\d+)(?:-([0-9A-Za-z.-]+))?(?:\\+[0-9A-Za-z.-]+)?$/.exec(raw.trim())\n if (match === null) return null\n const prerelease = (match[4] ?? '').split('.').filter((part) => part !== '').map((part) =>\n /^\\d+$/.test(part) ? Number(part) : part,\n )\n return {\n major: Number(match[1]),\n minor: Number(match[2]),\n patch: Number(match[3]),\n prerelease,\n }\n}\n\n/**\n * Order two version strings per semver precedence.\n * @returns -1 when `a` < `b`, 0 when equal, 1 when `a` > `b`.\n * Unparseable inputs sort after every parseable version (equal to\n * each other), so a garbage `current` never reports an update.\n */\nexport function compareVersions(a: string, b: string): number {\n const pa = parseVersion(a)\n const pb = parseVersion(b)\n if (pa === null && pb === null) return 0\n if (pa === null) return 1\n if (pb === null) return -1\n for (const key of ['major', 'minor', 'patch'] as const) {\n if (pa[key] !== pb[key]) return pa[key] < pb[key] ? -1 : 1\n }\n if (pa.prerelease.length === 0 && pb.prerelease.length === 0) return 0\n if (pa.prerelease.length === 0) return 1\n if (pb.prerelease.length === 0) return -1\n const shared = Math.min(pa.prerelease.length, pb.prerelease.length)\n for (let i = 0; i < shared; i += 1) {\n const x = pa.prerelease[i]\n const y = pb.prerelease[i]\n if (x === y) continue\n // Numeric identifiers sort before alphanumeric ones; two numerics compare\n // numerically, two strings ASCII-wise.\n if (typeof x === 'number' && typeof y === 'number') return x < y ? -1 : 1\n if (typeof x === 'number') return -1\n if (typeof y === 'number') return 1\n return x < y ? -1 : 1\n }\n if (pa.prerelease.length !== pb.prerelease.length) {\n return pa.prerelease.length < pb.prerelease.length ? -1 : 1\n }\n return 0\n}\n\n/**\n * Whether `latest` is a strictly newer release than `current` — i.e. whether\n * an update is available. A newer installed prerelease (e.g. a git install\n * ahead of npm) correctly reports no update.\n */\nexport function isUpdateAvailable(current: string, latest: string): boolean {\n return compareVersions(current, latest) < 0\n}\n","/**\n * Shared contract of the update-check feature — consumed by both halves:\n * the Host registers an exact webServer route answering this JSON payload,\n * and the Client settings row fetches that same-origin route and renders it.\n * This module is dependency-free so both bundles inline it.\n */\nimport { compareVersions, parseVersion } from './versions.ts'\nimport type { UpdateChannel } from './state.ts'\n\nexport type { UpdateChannel } from './state.ts'\n\n/** The plugin's own npm name (the registry lookup key). */\nexport const PACKAGE_NAME = '@nonamelego/dsh-catppuccin'\n\n/**\n * npm registry abbreviated packument endpoint (the `install-v1` document).\n * The `latest` endpoint alone is not enough: this project ships prereleases\n * under the `beta` dist-tag while `latest` stays on the last stable, so the\n * check reads `dist-tags` to serve both channels.\n */\nexport const REGISTRY_PACKUMENT_URL =\n `https://registry.npmjs.org/${PACKAGE_NAME.replace('/', '%2F')}`\n\n/** Host route the Client row fetches (exact match, same origin). */\nexport const UPDATE_ROUTE_PATH = '/catppuccin/check-update'\n\n/** Network budget for the Host's registry lookup. */\nexport const UPDATE_FETCH_TIMEOUT_MS = 8000\n\n/** The `dist-tags` object of an npm packument. */\nexport interface DistTags {\n latest?: string\n beta?: string\n}\n\n/** The newest release a given install should chase, and on which channel. */\nexport interface NewestRelease {\n version: string\n channel: UpdateChannel\n}\n\n/**\n * The copyable CLI upgrade command for one channel (`dsh plugin` is a thin\n * pnpm forwarder). The profile name comes from the Host's runtime probe\n * (`src/profile-detect.ts`), never hard-coded, so the command works on any\n * deployment out of the box.\n * @param channel - the dist-tag to chase.\n * @param profile - the DSH profile name to target.\n */\nexport function updateCommandFor(channel: UpdateChannel, profile: string): string {\n return `dsh plugin --profile ${profile} add ${PACKAGE_NAME}@${channel}`\n}\n\n/**\n * Pick the newest release worth reporting for the given install:\n * - with NO channel preference: a stable install chases only `latest` (never\n * downgrades onto a beta), while a prerelease install (e.g. a\n * `beta`-tagged version) also chases `beta` — so beta users see newer\n * betas, and a stable release that outranks their prerelease promotes\n * them via `@latest`;\n * - an explicit `beta` preference (the settings row's prerelease pick)\n * chases `beta` even from a stable install — beta testers opt in without\n * changing their install; the newest of the two tags still wins, so a\n * newer stable promotes them back onto `@latest`;\n * - an explicit `latest` preference pins the check to the stable tag —\n * never a beta, even from a prerelease install.\n * @param current - the installed version.\n * @param tags - the registry's dist-tags.\n * @param preferredChannel - optional persisted channel preference (item I).\n * @returns the newest candidate and its channel, or null when nothing applies.\n */\nexport function selectNewest(current: string, tags: DistTags, preferredChannel?: UpdateChannel): NewestRelease | null {\n const candidates: NewestRelease[] = []\n if (tags.latest !== undefined) candidates.push({ version: tags.latest, channel: 'latest' })\n if (preferredChannel === 'beta') {\n // Beta chase, opt-in: follow the beta tag even from a stable install.\n if (tags.beta !== undefined) candidates.push({ version: tags.beta, channel: 'beta' })\n } else if (preferredChannel === undefined) {\n // No preference: prerelease installs chase beta on top of latest.\n const parsed = parseVersion(current)\n if (tags.beta !== undefined && parsed !== null && parsed.prerelease.length > 0) {\n candidates.push({ version: tags.beta, channel: 'beta' })\n }\n }\n // preferredChannel === 'latest': candidates hold the stable tag only.\n if (candidates.length === 0) return null\n let best = candidates[0]\n for (const candidate of candidates.slice(1)) {\n if (compareVersions(best.version, candidate.version) < 0) best = candidate\n }\n return best\n}\n\n/** Stable machine-readable outcome code (borrowed from the plugin-update\n * error-code discipline of dsh-vision-toolkit): the Client maps it to copy\n * instead of sniffing human text. `network.*` splits the old catch-all\n * `network` (item U): `network.local` means the browser→Host hop failed\n * (host unreachable / DNS — \"check your network\"), `network.upstream` means\n * the Host reached the registry but the lookup timed out (\"npm is\n * temporarily unavailable\"). */\nexport type UpdateErrorCode =\n | 'ok'\n | 'network.local' // the same-origin fetch itself failed (client side)\n | 'network.upstream' // the Host's registry fetch timed out\n | 'registry-unreachable' // npm registry could not be reached (fetch threw)\n | 'registry-http' // registry answered with a non-2xx status\n | 'no-dist-tags' // registry returned no usable dist-tags\n | 'invalid-response' // registry response could not be parsed\n\n/** How this package is installed inside the probed DSH profile. */\nexport type InstallSource = 'registry' | 'link' | 'file' | 'git' | 'unknown'\n\n/** Runtime that answered the check — drives the copy (command hint / restart). */\nexport type UpdateEnv = 'desktop' | 'web'\n\n/**\n * JSON payload of the update check. `ok: false` carries a human-readable\n * `error` plus a stable `code`; `ok: true` carries the comparison result.\n * The Host owns the semver comparison, the channel selection, and the\n * profile probe (single source of truth), so the Client never parses\n * versions or guesses the profile name itself.\n */\nexport interface UpdateCheckPayload {\n /** Whether the registry lookup succeeded. */\n ok: boolean\n /** Installed version (from this package's own manifest). */\n current?: string\n /** Newest release worth chasing (`latest`, plus `beta` for prerelease installs). */\n latest?: string\n /** `latest` is strictly newer than `current`. */\n outdated?: boolean\n /** The dist-tag the newest release came from. */\n channel?: UpdateChannel\n /** Copyable CLI upgrade command (present when outdated). */\n updateCommand?: string\n /** DSH profile name the upgrade command targets (probed, not guessed). */\n profile?: string\n /** Whether the profile probe actually found the install (false = fallback). */\n profileDetected?: boolean\n /** Runtime that answered: DSH Desktop vs standard dsh web (drives copy). */\n env?: UpdateEnv\n /** How the package is installed in that profile (drives copy). */\n installSource?: InstallSource\n /** ISO timestamp of the check (registry lookup moment). */\n checkedAt?: string\n /** Stable outcome code (see {@link UpdateErrorCode}). */\n code?: UpdateErrorCode\n /** Failure detail (only when `ok` is false). */\n error?: string\n}\n","/**\n * Runtime probe of the DSH profile this plugin is installed into.\n *\n * Borrowed from dsh-vision-toolkit's plugin-update module (MIT) and trimmed\n * to what the update check needs: instead of hard-coding a profile name into\n * the upgrade command, the Host scans the DSH home's `profiles/` directory\n * (plus the `--profile` argv hint) for the profile whose package.json lists\n * this package as a direct dependency, and classifies the install source so\n * the Client can tell \"registry install → copy the npm command\" apart from\n * \"local link/file/git install → the npm command does not apply\".\n *\n * Pure functions (`profileHint`, `isRegistrySpec`, `installSourceOf`) are\n * dependency-free and unit-tested; the IO probe (`detectProfile`) reads only\n * package.json manifests and never mutates anything.\n */\nimport { readFile, readdir } from 'node:fs/promises'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport type { InstallSource } from './update-check.ts'\nimport { PACKAGE_NAME } from './update-check.ts'\n\n/** Fallback profile name when nothing can be probed (keeps the command valid). */\nexport const FALLBACK_PROFILE = 'web'\n\n/** Detect the OFFICIAL desktop shell from the environment.\n *\n * 官方仓(`deepseek-ai/deepseek-harness`)的 `apps/desktop-host` 启动 `desktop`\n * profile 时,会把 `DSH_DESKTOP_NODE_EXECUTABLE` 注入该进程的环境(同一个 profile\n * 目录 `$DSH_HOME/profiles/desktop`),这是官方 Electron 桌面版留下的唯一可靠信号。\n * 为什么不能只靠服务探测:官方壳**不提供**第三方启动器那个 `desktopProfiles` 服务\n * (2026-09-22 在官方仓搜 `desktopProfiles` 命中 0),于是官方桌面版下 Host 会把自己\n * 当成普通 web —— 升级命令提示与重启提示都退化,而 profile 名其实是对的。\n * @param env - the environment to inspect (injectable so tests need no real desktop).\n * @returns whether this Host runs inside the official desktop shell.\n */\nexport function isDesktopShellEnv(env: Record<string, string | undefined>): boolean {\n const marker = env.DSH_DESKTOP_NODE_EXECUTABLE\n return typeof marker === 'string' && marker.trim() !== ''\n}\n\n/** The DSH home the running process owns — `$DSH_HOME` when set, else\n * `~/.dsh`. Port-independent and stable across restarts, so it is the right\n * root for the plugin's durable state file. */\nexport function defaultDshHome(): string {\n return process.env.DSH_HOME?.trim() || join(homedir(), '.dsh')\n}\n\n/** How this package's dependency spec was written in the profile manifest. */\nexport interface ProfileProbe {\n /** Profile name to target in the upgrade command. */\n name: string\n /** Whether the probe actually matched a profile (false = fallback). */\n detected: boolean\n /** Install source classification of this package in that profile. */\n installSource: InstallSource\n}\n\n/**\n * Extract the profile name a running DSH process was started with: either a\n * leading bare `web`-style argument or an explicit `--profile <name>` /\n * `--profile=<name>` flag. Mirrors vision-toolkit's `profileHint`.\n */\nexport function profileHint(argv: readonly string[]): string | undefined {\n if (argv[0] === 'web') return 'web'\n for (let index = 0; index < argv.length; index += 1) {\n const value = argv[index]\n if (value?.startsWith('--profile=')) return value.slice('--profile='.length)\n if (value === '--profile') return argv[index + 1]\n }\n return undefined\n}\n\n/**\n * Whether a dependency spec is a plain npm registry spec (a semver range),\n * not a local/workspace/git/URL source. Mirrors vision-toolkit's\n * `registryInstallSpec`.\n */\nexport function isRegistrySpec(spec: string): boolean {\n const normalized = spec.trim().toLowerCase()\n return normalized.length > 0\n && !normalized.startsWith('workspace:')\n && !/^[a-z][a-z0-9+.-]*:/u.test(normalized)\n && !normalized.includes('/')\n && !normalized.includes('\\\\')\n}\n\n/** Classify a non-registry spec into the more precise local source kind. */\nexport function installSourceOf(spec: string): InstallSource {\n const normalized = spec.trim().toLowerCase()\n if (normalized.startsWith('link:') || normalized.startsWith('workspace:')) return 'link'\n if (normalized.startsWith('file:')) return 'file'\n return 'git'\n}\n\n/** Read one profile's manifest at its manifest dir and return this package's install source. */\nasync function inspectProfileAt(manifestDir: string): Promise<InstallSource | undefined> {\n try {\n const manifest = JSON.parse(\n await readFile(join(manifestDir, 'package.json'), 'utf8'),\n ) as { dependencies?: Record<string, unknown> }\n const spec = manifest.dependencies?.[PACKAGE_NAME]\n if (typeof spec !== 'string') return undefined\n return isRegistrySpec(spec) ? 'registry' : installSourceOf(spec)\n } catch {\n return undefined\n }\n}\n\n/** Read one profile under the DSH home and return this package's install source. */\nasync function inspectProfile(dshHome: string, name: string): Promise<InstallSource | undefined> {\n return inspectProfileAt(join(dshHome, 'profiles', name))\n}\n\n/** The launcher-resolved profile under DSH Desktop (`desktopProfiles.current`). */\nexport interface DesktopProfileHint {\n /** Launcher-selected profile name. */\n name: string\n /** Absolute manifest dir; falls back to the DSH-home scan path when absent. */\n dir?: string\n}\n\n/**\n * Probe the DSH profile this package is installed into.\n * Priority: an authoritative `desktopProfile` (DSH Desktop's\n * `desktopProfiles.current` — the Desktop docs say never infer the profile\n * from argv / settings / `$DSH_HOME` when it is available), then the\n * `--profile`/bare-name argv hint, then a scan of every `profiles/` entry\n * whose manifest lists this package as a direct dependency. Falls back to\n * `FALLBACK_PROFILE` with `detected: false` when nothing matches (e.g. a dev\n * install outside any profile).\n */\nexport async function detectProfile(options: {\n /** DSH home; defaults to $DSH_HOME or `~/.dsh`. */\n dshHome?: string\n /** Process argv slice; defaults to the running process's argv. */\n argv?: readonly string[]\n /** Authoritative profile under DSH Desktop (`desktopProfiles.current`), when running there. */\n desktopProfile?: DesktopProfileHint | undefined\n} = {}): Promise<ProfileProbe> {\n const dshHome = options.dshHome ?? defaultDshHome()\n const argv = options.argv ?? process.argv.slice(2)\n\n // DSH Desktop wins over every probe: the launcher already resolved the\n // active profile, so the upgrade command targets it directly. The install\n // source still comes from reading that profile's manifest (via its explicit\n // dir) so the \"npm command does not apply\" local-install hint keeps working.\n const desktop = options.desktopProfile\n if (desktop?.name) {\n const dir = desktop.dir?.trim() || join(dshHome, 'profiles', desktop.name)\n const source = await inspectProfileAt(dir)\n return { name: desktop.name, detected: true, installSource: source ?? 'unknown' }\n }\n\n const hint = profileHint(argv)\n\n if (hint !== undefined) {\n const source = await inspectProfile(dshHome, hint)\n if (source !== undefined) return { name: hint, detected: true, installSource: source }\n }\n\n const profilesDir = join(dshHome, 'profiles')\n let names: string[] = []\n try {\n names = (await readdir(profilesDir, { withFileTypes: true }))\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name)\n } catch {\n // No profiles directory — the fallback below still produces a valid command.\n }\n if (hint !== undefined && !names.includes(hint)) names.push(hint)\n\n for (const name of names) {\n const source = await inspectProfile(dshHome, name)\n if (source !== undefined) return { name, detected: true, installSource: source }\n }\n\n return { name: hint ?? FALLBACK_PROFILE, detected: false, installSource: 'unknown' }\n}","/**\n * Host half of the update-check feature (extracted from `src/index.ts` —\n * item A). Owns the exact webServer route, the npm registry lookup, the\n * short-TTL verdict cache, the ETag conditional-request cache (item W), and\n * the channel preference parsed from the query string (item I).\n *\n * The Client settings row fetches this same-origin route and renders the\n * verdict; every move the route makes (semver comparison, channel choice,\n * profile probe) happens here so the Client never parses versions or\n * guesses profile names.\n */\nimport type { Context } from '@deepseek-ai/cordis'\nimport pkg from '../../package.json'\nimport {\n REGISTRY_PACKUMENT_URL,\n UPDATE_FETCH_TIMEOUT_MS,\n UPDATE_ROUTE_PATH,\n selectNewest,\n updateCommandFor,\n type UpdateChannel,\n type UpdateCheckPayload,\n type UpdateEnv,\n} from '../update-check.ts'\nimport { isUpdateAvailable } from '../versions.ts'\nimport { detectProfile, isDesktopShellEnv } from '../profile-detect.ts'\n\n// Minimal structural types for the parts of node:http and the webServer\n// service this plugin touches. The host bundle resolves cordis and friends\n// from the profile tree at runtime and ships no @types/node, so the route\n// contract is spelled out locally instead of imported.\ninterface HttpRequestLike {\n url?: string\n method?: string\n on(event: 'data', listener: (chunk: Uint8Array) => void): unknown\n on(event: 'end', listener: () => void): unknown\n on(event: 'error', listener: (error: Error) => void): unknown\n}\ninterface HttpResponseLike {\n writeHead(status: number, headers: Record<string, string>): void\n end(body?: string): void\n}\ninterface WebRouteLike {\n kind: 'exact' | 'prefix'\n path: string\n handler: (req: HttpRequestLike, res: HttpResponseLike) => void | Promise<void>\n}\ninterface WebServerLike {\n register(route: WebRouteLike): () => void\n}\n\n/** Minimal structural type for DSH Desktop's public `desktopProfiles` service\n * (the host bundle ships no dsh-plugin-desktop types). */\ninterface DesktopProfilesLike {\n readonly current?: {\n readonly name: string\n readonly dir?: string\n }\n}\n\n/** Query-string channel preference (item I): `?channel=beta` chases the\n * beta tag from a stable install; anything else keeps the install-derived\n * channel logic of `selectNewest`. */\nfunction parseChannel(url: string | undefined): UpdateChannel | undefined {\n if (url === undefined) return undefined\n try {\n const channel = new URL(url, 'http://dsh.localhost').searchParams.get('channel')\n return channel === 'beta' ? 'beta' : channel === 'latest' ? 'latest' : undefined\n } catch {\n return undefined\n }\n}\n\nfunction sendJson(res: HttpResponseLike, status: number, payload: unknown): void {\n res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })\n res.end(JSON.stringify(payload))\n}\n\n/** Result of one registry lookup: an OK payload, a 304 not-modified (serve\n * the cached verdict), or an error payload (never cached). */\ntype FetchResult =\n | { kind: 'ok'; payload: UpdateCheckPayload; etag?: string }\n | { kind: 'not-modified' }\n | { kind: 'error'; payload: UpdateCheckPayload }\n\n/** Query the npm registry and build the check payload. The probe result\n * feeds the upgrade command's profile name and the install-source copy, so\n * the Client never guesses either. Under DSH Desktop the target profile is\n * `desktopProfiles.current` (launcher-resolved, authoritative), so the\n * copied command targets the right profile there too.\n *\n * Failure classification (item U): a timed-out lookup (the abort fired) is\n * `network.upstream` — the npm registry is temporarily unavailable; a\n * fetch that throws outright (DNS / connection refused) stays\n * `registry-unreachable`; the 304 conditional path (item W) revalidates\n * the cached verdict without pulling the full packument again. */\nasync function fetchLatestVersion(options: {\n env: UpdateEnv\n channel?: UpdateChannel\n etag?: string\n desktopProfile?: DesktopProfilesLike['current']\n}): Promise<FetchResult> {\n const current = pkg.version\n const probe = await detectProfile(\n options.desktopProfile !== undefined ? { desktopProfile: options.desktopProfile } : {},\n )\n const base = {\n current,\n env: options.env,\n profile: probe.name,\n profileDetected: probe.detected,\n installSource: probe.installSource,\n checkedAt: new Date().toISOString(),\n }\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), UPDATE_FETCH_TIMEOUT_MS)\n let response: Response\n try {\n response = await fetch(REGISTRY_PACKUMENT_URL, {\n signal: controller.signal,\n headers: {\n accept: 'application/vnd.npm.install-v1+json',\n // Item W: conditional request — a 304 answer revalidates the cached\n // verdict without pulling the full packument again.\n ...(options.etag !== undefined ? { 'if-none-match': options.etag } : {}),\n },\n })\n } catch (error) {\n const timedOut = controller.signal.aborted\n return {\n kind: 'error',\n payload: {\n ...base,\n ok: false,\n code: timedOut ? 'network.upstream' : 'registry-unreachable',\n error: error instanceof Error ? error.message : String(error),\n },\n }\n } finally {\n clearTimeout(timer)\n }\n // 304: the registry's own cache says our cached verdict is still current.\n if (response.status === 304) return { kind: 'not-modified' }\n if (!response.ok) {\n return {\n kind: 'error',\n payload: {\n ...base,\n ok: false,\n code: 'registry-http',\n error: `npm registry responded HTTP ${response.status}`,\n },\n }\n }\n let data: { 'dist-tags'?: Record<string, string> }\n try {\n data = await response.json() as { 'dist-tags'?: Record<string, string> }\n } catch {\n return {\n kind: 'error',\n payload: { ...base, ok: false, code: 'invalid-response', error: 'npm registry returned an unparseable packument' },\n }\n }\n const newest = selectNewest(current, data['dist-tags'] ?? {}, options.channel)\n if (newest === null) {\n return {\n kind: 'error',\n payload: { ...base, ok: false, code: 'no-dist-tags', error: 'npm registry returned no usable dist-tags' },\n }\n }\n const outdated = isUpdateAvailable(current, newest.version)\n const etag = response.headers.get('etag') ?? undefined\n return {\n kind: 'ok',\n ...(etag !== undefined ? { etag } : {}),\n payload: {\n ...base,\n ok: true,\n code: 'ok',\n latest: newest.version,\n outdated,\n channel: newest.channel,\n ...(outdated ? { updateCommand: updateCommandFor(newest.channel, probe.name) } : {}),\n },\n }\n}\n\n/** Short TTL for the cached registry verdict: repeated checks (settings row\n * re-opened, user re-clicks) don't re-hit npm; failures stay uncached so the\n * next click retries for real. The ETag (item W) lets a stale-but-valid\n * cache refresh itself with a conditional request instead of a full\n * packument pull — npm traffic drops to one 304 per 5 minutes.\n *\n * The cache is keyed by the requested channel (item I): switching the row's\n * channel must produce the new channel's verdict, not a stale cached one. */\nconst UPDATE_CACHE_TTL_MS = 5 * 60 * 1000\n\ninterface UpdateCacheEntry {\n at: number\n etag?: string\n payload: UpdateCheckPayload\n}\n\n/** Channel cache buckets. No query param and an explicit channel select\n * differently for a prerelease install (`selectNewest` only chases beta\n * without a preference), so the default bucket is DISTINCT from `latest` —\n * a pinned-latest check must never serve a beta verdict from the\n * unopinionated bucket (and vice versa). */\ntype CacheBucket = 'default' | UpdateChannel\n\nconst updateCaches = new Map<CacheBucket, UpdateCacheEntry>()\n\nfunction cacheKeyOf(channel: UpdateChannel | undefined): CacheBucket {\n return channel ?? 'default'\n}\n\n/** Answer the update-check route with the JSON contract from update-check.ts.\n * Recognizes BOTH desktop shells: the third-party launcher's optional\n * `desktopProfiles` service (target profile = `desktopProfiles.current`,\n * Desktop-flavoured hints) and the official Electron shell's environment\n * marker (`DSH_DESKTOP_NODE_EXECUTABLE`, see `isDesktopShellEnv`). Otherwise\n * it is the standard dsh web/CLI route and the web copy + profile scan apply.\n *\n * The route itself is stateless about retries: item V's 30s auto-retry is\n * scheduled by the settings row (an immediate route retry would just hang\n * a manual click); once a failure has passed the 5-minute window, the next\n * request re-runs the lookup for real. */\nasync function handleUpdateCheck(ctx: Context, req: HttpRequestLike, res: HttpResponseLike): Promise<void> {\n const channel = parseChannel(req.url)\n const key = cacheKeyOf(channel)\n const entry = updateCaches.get(key)\n if (entry !== undefined && Date.now() - entry.at < UPDATE_CACHE_TTL_MS) {\n sendJson(res, 200, entry.payload)\n return\n }\n const desktopProfiles = ctx.get('desktopProfiles') as DesktopProfilesLike | undefined\n const current = desktopProfiles?.current\n // Two desktop shells to recognize (2026-09-22): the third-party launcher\n // exposes the `desktopProfiles` service, the OFFICIAL Electron shell does not\n // and marks itself through the environment instead (`apps/desktop-host`).\n // Missing the second one made official desktop builds fall back to the plain\n // web copy while the probed profile name was already correct.\n const isDesktop = isDesktopShellEnv(process.env)\n || (current?.name !== undefined && current.name !== '')\n const result = await fetchLatestVersion({\n env: isDesktop ? 'desktop' : 'web',\n channel,\n ...(entry?.etag !== undefined ? { etag: entry.etag } : {}),\n ...(isDesktop ? { desktopProfile: current } : {}),\n })\n if (result.kind === 'ok') {\n updateCaches.set(key, { at: Date.now(), payload: result.payload, ...(result.etag !== undefined ? { etag: result.etag } : {}) })\n sendJson(res, 200, result.payload)\n return\n }\n if (result.kind === 'not-modified') {\n // The registry 304'd our conditional request: refresh the cache window\n // and serve the stored verdict (item W). A cache is guaranteed here —\n // we only send If-None-Match when one exists.\n if (entry !== undefined) updateCaches.set(key, { ...entry, at: Date.now() })\n const cached = entry?.payload\n if (cached !== undefined) {\n sendJson(res, 200, cached)\n return\n }\n }\n // Registry failure (or the unreachable not-modified-without-cache\n // combination): the error payload is never cached, so the next check\n // retries for real.\n sendJson(res, 502, result.kind === 'error'\n ? result.payload\n : { ok: false, code: 'invalid-response', error: 'registry state inconsistency' } as UpdateCheckPayload)\n}\n\n/** Register the update-check route on the owning context. `webServer` is a\n * hard inject dependency of the Host plugin, so it is guaranteed live when\n * this runs — mounting can never race ahead of the service. */\nexport function registerUpdateCheckRoute(ctx: Context): void {\n const webServer = ctx.get('webServer') as WebServerLike\n ctx.effect(() => webServer.register({\n kind: 'exact',\n path: UPDATE_ROUTE_PATH,\n handler: (req, res) => void handleUpdateCheck(ctx, req, res),\n }), 'dsh-catppuccin: update-check route')\n}","/**\n * Durable Catppuccin state — the shared contract between the two halves.\n *\n * Why this exists: localStorage is scoped per origin, so it is the wrong place\n * for the ONLY copy of a preference. It is per-browser (two browsers on one\n * machine disagree), per-loopback-port (a second Desktop instance that lands\n * on the next free port gets a different origin — the official shell pins\n * `19387` and `anywhere-labs/dsh-desktop` starts at `43120` and increments on\n * a bind collision, both observed 2026-09-24), and it disappears whenever the\n * user clears site data. The durable copy lives in the official settings seam\n * under the DSH home, which is the machine-wide truth every browser and every\n * profile entry reads.\n *\n * (Historical note, corrected 2026-09-24: the 0.5.0 rationale claimed DSH\n * Desktop launched the profile with `--port 0`, \"a fresh random port every\n * launch\". That was already untrue when 0.5.0 shipped — the community shell\n * pinned a stable port on 2026-08-21, and the official shell passes an\n * explicit `--port 19387`. The design is unchanged and still justified by the\n * reasons above, but do not repeat the port-per-launch claim.)\n *\n * Since 0.5.0 the DURABLE copy lives in the official settings seam, which\n * persists under the DSH home exactly like the legacy\n * `$DSH_HOME/catppuccin-state.json` did — origin-independent, so it survives\n * the Desktop's per-launch port churn. The legacy file is kept as a\n * read-only migration source (`src/legacy-state.ts`) and rolled into the\n * settings document once (`src/index.ts`). Browser localStorage remains the\n * in-browser cache and cross-tab sync bus, and the Client's fallback when the\n * settings transport is unavailable. This module is dependency-free so both\n * bundles inline it (like `update-check.ts`).\n *\n * The seam itself is version-dependent (issue #15): DSH <= 0.1.6-alpha.2\n * serves a Client `ctx.settingsScope` bound to a namespace the plugin\n * registers itself (`CATPPUCCIN_SETTINGS_NS`), while >= 0.1.7-alpha.1 serves\n * `ctx.configForms` projecting the plugin's volatile `Config` form, keyed by\n * the PROFILE ENTRY ID (`CATPPUCCIN_ENTRY_ID`). Both are addressed from this\n * one contract (see `src/client/state-sync.ts`).\n */\nexport const STATE_VERSION = 1\n\n/** Release channels the update check knows about (a persisted preference;\n * `selectNewest` in `update-check.ts` consumes it). */\nexport type UpdateChannel = 'latest' | 'beta'\n\n/** Shiki syntax-highlighting style variants (see `src/client/shiki-tokens.ts`). */\nexport type ShikiStyle = 'default' | 'italic-comments'\n\n/** Legacy settings namespace (DSH <= 0.1.6-alpha.2): the Host registers it\n * through `settings.installSection`, the Client binds it through\n * `ctx.settingsScope` (see `src/settings-catppuccin.ts`). */\nexport const CATPPUCCIN_SETTINGS_NS = 'catppuccin'\n\n/**\n * Profile entry id carrying the plugin (DSH >= 0.1.7-alpha.1): the settings\n * service projects our volatile `Config` into a form named by the ACTIVE\n * PROFILE ENTRY's id, so this constant is the new seam's namespace.\n *\n * It is a CONTRACT with `cordis.patch.yml`: the `insert` row whose `name` is\n * `@nonamelego/dsh-catppuccin` must keep `id: dsh-catppuccin`, because the\n * Client can only address the form by that id (asserted by\n * `tests/settings-seam.spec.ts`). A user who renames the entry loses the new\n * seam and degrades to localStorage-only — the legacy seam is unaffected,\n * since that namespace is registered by the plugin itself.\n */\nexport const CATPPUCCIN_ENTRY_ID = 'dsh-catppuccin'\n\n/** State file name under the DSH home (kept next to the Desktop-managed\n * `cordis.patch.yml` so it survives the Desktop's per-launch port churn). */\nexport const STATE_FILENAME = 'catppuccin-state.json'\n\n/**\n * Whether a raw settings view carries a user layer at all.\n *\n * The two seams answer this differently and the difference is load-bearing\n * (issue #15): the NEW form's `user` is the profile patch's `override`, which\n * `configEditor.configuration()` defaults to `{}` — so \"absent\" arrives as an\n * EMPTY OBJECT, and `!== undefined` would be true forever. The LEGACY\n * document omits the field entirely when the document holds no such section,\n * so the old `!== undefined` test stays correct there. Callers must use the\n * predicate their channel defines (see `src/client/state-sync.ts`).\n * @param user - a snapshot's raw user layer.\n * @returns whether it holds at least one entry.\n */\nexport function hasUserLayerSection(user: unknown): boolean {\n return typeof user === 'object' && user !== null && !Array.isArray(user) && Object.keys(user).length > 0\n}\n\n/** The four registered Catppuccin theme ids. MUST stay in sync with the\n * `themeId` of `CATPPUCCIN_FLAVORS` in `src/client/palettes.ts` — guarded by\n * `tests/state.spec.ts` — because the persisted flavour value IS the theme id. */\nexport const CATPPUCCIN_THEME_IDS = [\n 'catppuccin-latte',\n 'catppuccin-frappe',\n 'catppuccin-macchiato',\n 'catppuccin-mocha',\n] as const\n\n/** A persisted flavour choice: one registered theme id or `off` (fall back to\n * the official theme). */\nexport type FlavorValue = (typeof CATPPUCCIN_THEME_IDS)[number] | 'off'\n\n/** Durable glass-layer settings (no derived `dark` field — that resolves at\n * runtime from the active theme). */\nexport interface GlassState {\n /** Layer enable flag. */\n enabled: boolean\n /** Rendering mode. */\n mode: 'mica' | 'compat'\n /** Glass blur radius, px (0-40). */\n blur: number\n /** Glass fill frost, 0-100. */\n frost: number\n /** Backdrop brightness, 0-100. */\n brightness: number\n}\n\n/** The whole durable state the plugin persists. */\nexport interface CatppuccinState {\n /** Schema version; anything else is treated as unknown and defaulted. */\n version: typeof STATE_VERSION\n /** Selected flavour (theme id or `off`). */\n flavor: FlavorValue\n /** Glass-layer settings (enable flag + knobs). */\n glass: GlassState\n /** Automatically re-check for updates (boot + periodic, while on). */\n autoCheck: boolean\n /** Release channel the update check chases. */\n updateChannel: UpdateChannel\n /** Per-token `--dsw-*` overrides merged LAST into the active theme. */\n overrides: Record<string, string>\n /** Shiki syntax-highlighting style variant. */\n shikiStyle: ShikiStyle\n}\n\n/** The settings-document section: `CatppuccinState` minus the synthetic\n * `version` field — the official seam resolves defaults, composition and the\n * user layer itself, so no plugin-side version is stored. */\nexport interface CatppuccinSettingsSection {\n /** Selected flavour (theme id or `off`). */\n flavor: FlavorValue\n /** Glass-layer settings (enable flag + knobs). */\n glass: GlassState\n /** Automatically re-check for updates (boot + periodic, while on). */\n autoCheck: boolean\n /** Release channel the update check chases. */\n updateChannel: UpdateChannel\n /** Per-token `--dsw-*` overrides merged LAST into the active theme. */\n overrides: Record<string, string>\n /** Shiki syntax-highlighting style variant. */\n shikiStyle: ShikiStyle\n}\n\n/** Shipped glass defaults — the single source of truth; the client glass layer\n * (`src/client/glass/glass-layer.ts`) derives its knob defaults from here. */\nexport const DEFAULT_GLASS: GlassState = {\n enabled: false,\n mode: 'mica',\n blur: 2,\n frost: 20,\n brightness: 50,\n}\n\n/** Shipped update-check preference: automatic checks on. */\nexport const DEFAULT_AUTO_CHECK = true\n/** Shipped update-check channel: chase the stable `latest` tag. */\nexport const DEFAULT_UPDATE_CHANNEL: UpdateChannel = 'latest'\n/** Shipped syntax-highlighting style. */\nexport const DEFAULT_SHIKI_STYLE: ShikiStyle = 'default'\n\n/** A fully default state: no Catppuccin flavour and no glass. */\nexport function defaultState(): CatppuccinState {\n return {\n version: STATE_VERSION,\n flavor: 'off',\n glass: { ...DEFAULT_GLASS },\n autoCheck: DEFAULT_AUTO_CHECK,\n updateChannel: DEFAULT_UPDATE_CHANNEL,\n overrides: {},\n shikiStyle: DEFAULT_SHIKI_STYLE,\n }\n}\n\n/**\n * Keep only well-formed custom token overrides: string values under a `--`\n * key (the token shape `--dsw-*` names). Everything else is dropped so a\n * hand-edited document can never smuggle junk into the theme registration.\n */\nexport function sanitizeOverrides(input: unknown): Record<string, string> {\n const out: Record<string, string> = {}\n if (typeof input !== 'object' || input === null) return out\n for (const [key, value] of Object.entries(input)) {\n if (typeof value === 'string' && key.startsWith('--')) out[key] = value\n }\n return out\n}\n\n/**\n * Whether a raw overrides map carries entries {@link sanitizeOverrides} would\n * drop — a key that is not a `--` token, or a non-string value.\n *\n * The settings-document schema types `overrides` as a dict of strings only, so\n * a hand-edited document can hold such entries. The client reads them away but\n * never wrote the cleaned map back, so every settings publish re-ran the\n * \"document wins\" adoption for a difference nothing could clear (audit F7).\n * The caller uses this to push the sanitized shape back once, so the document\n * converges instead of drifting forever.\n */\nexport function hasUnpersistableOverrides(raw: unknown): boolean {\n if (typeof raw !== 'object' || raw === null) return false\n return Object.entries(raw).some(([key, value]) => typeof value !== 'string' || !key.startsWith('--'))\n}\n\n/** Clamp a finite number into [min, max]; non-finite values fall back. */\nfunction clampFinite(value: unknown, min: number, max: number, fallback: number): number {\n return typeof value === 'number' && Number.isFinite(value)\n ? Math.min(max, Math.max(min, value))\n : fallback\n}\n\n/**\n * Normalize arbitrary input (JSON from disk or the wire) into a valid state:\n * unknown/missing fields fall back to defaults, out-of-range numbers clamp,\n * and unknown flavours/invalid enums fall back too. The Host applies this on\n * both read and write so a corrupt file or a hand-crafted payload can never\n * poison the plugin's settings.\n */\nexport function sanitizeState(input: unknown): CatppuccinState {\n const state = defaultState()\n if (typeof input !== 'object' || input === null) return state\n const raw = input as Record<string, unknown>\n\n const flavor = raw.flavor\n if (typeof flavor === 'string' && (CATPPUCCIN_THEME_IDS as readonly string[]).includes(flavor)) {\n state.flavor = flavor as FlavorValue\n } else if (flavor === 'off') {\n state.flavor = 'off'\n }\n\n const glass = typeof raw.glass === 'object' && raw.glass !== null\n ? raw.glass as Record<string, unknown>\n : {}\n state.glass.enabled = glass.enabled === true\n state.glass.mode = glass.mode === 'compat' ? 'compat' : 'mica'\n state.glass.blur = clampFinite(glass.blur, 0, 40, DEFAULT_GLASS.blur)\n state.glass.frost = clampFinite(glass.frost, 0, 100, DEFAULT_GLASS.frost)\n state.glass.brightness = clampFinite(glass.brightness, 0, 100, DEFAULT_GLASS.brightness)\n\n // Preference fields: absent values keep their shipped defaults, so a v1\n // document (predating them) rolls forward without a migration step.\n state.autoCheck = raw.autoCheck !== false\n state.updateChannel = raw.updateChannel === 'beta' ? 'beta' : 'latest'\n state.overrides = sanitizeOverrides(raw.overrides)\n state.shikiStyle = raw.shikiStyle === 'italic-comments' ? 'italic-comments' : 'default'\n\n return state\n}\n\n/**\n * Version-aware migration of an arbitrary persisted document. Version 1\n * documents roll forward through `sanitizeState` (every field added since\n * v1 has a sanitize-time default, so no structural step exists yet); future\n * versions chain their steps here, oldest first. Documents from a NEWER\n * plugin keep every field `sanitizeState` understands instead of being\n * nuked — a downgrade never destroys user state. The version-upgrade\n * convention lives in `docs/state-migrations.md` (item Y).\n */\nexport function migrate(raw: unknown): CatppuccinState {\n if (typeof raw !== 'object' || raw === null) return defaultState()\n const record = raw as Record<string, unknown>\n const version = typeof record.version === 'number' ? record.version : 1\n // Future schema steps chain here, oldest first:\n // let current: unknown = record\n // if (version < 2) current = migrateV1toV2(current)\n // if (version < 3) current = migrateV2toV3(current)\n // (the state contract of each step is spelled out in docs/state-migrations.md)\n return sanitizeState(record)\n}\n\n/** Whether a normalized state carries nothing beyond the shipped defaults —\n * i.e. the user never actually chose anything worth persisting. Used to tell\n * \"no durable state yet\" apart from \"an explicit default choice\". */\nexport function isDefaultState(state: CatppuccinState): boolean {\n const expected = defaultState()\n return state.flavor === expected.flavor\n && state.glass.enabled === expected.glass.enabled\n && state.glass.mode === expected.glass.mode\n && state.glass.blur === expected.glass.blur\n && state.glass.frost === expected.glass.frost\n && state.glass.brightness === expected.glass.brightness\n && state.autoCheck === expected.autoCheck\n && state.updateChannel === expected.updateChannel\n && Object.keys(state.overrides).length === 0\n && state.shikiStyle === expected.shikiStyle\n}\n\n/** Default settings-document section: no flavour and no glass (mirrors\n * `defaultState()` minus the synthetic `version`). */\nexport function defaultSettingsSection(): CatppuccinSettingsSection {\n return {\n flavor: 'off',\n glass: { ...DEFAULT_GLASS },\n autoCheck: DEFAULT_AUTO_CHECK,\n updateChannel: DEFAULT_UPDATE_CHANNEL,\n overrides: {},\n shikiStyle: DEFAULT_SHIKI_STYLE,\n }\n}\n\n/** Drop the synthetic `version` field: full state → settings-document section. */\nexport function settingsSectionFromState(state: CatppuccinState): CatppuccinSettingsSection {\n return {\n flavor: state.flavor,\n glass: { ...state.glass },\n autoCheck: state.autoCheck,\n updateChannel: state.updateChannel,\n overrides: { ...state.overrides },\n shikiStyle: state.shikiStyle,\n }\n}\n\n/** Lift a settings-document section back into the full state-shaped contract\n * (re-sanitized so a hand-edited document can never poison the plugin). */\nexport function stateFromSettingsSection(section: CatppuccinSettingsSection): CatppuccinState {\n return sanitizeState(section)\n}\n\n/** Whether two settings sections are field-for-field equal (used to skip\n * redundant mirror writes and to recognize the Host echoing our own write). */\nexport function settingsSectionsEqual(a: CatppuccinSettingsSection, b: CatppuccinSettingsSection): boolean {\n return a.flavor === b.flavor\n && a.glass.enabled === b.glass.enabled\n && a.glass.mode === b.glass.mode\n && a.glass.blur === b.glass.blur\n && a.glass.frost === b.glass.frost\n && a.glass.brightness === b.glass.brightness\n && a.autoCheck === b.autoCheck\n && a.updateChannel === b.updateChannel\n && a.shikiStyle === b.shikiStyle\n && Object.keys(a.overrides).every((key) => b.overrides[key] === a.overrides[key])\n && Object.keys(b.overrides).every((key) => a.overrides[key] === b.overrides[key])\n}\n","/**\n * Legacy durable-state read — the one-shot migration source for 0.5.0.\n *\n * Up to 0.4.x the plugin persisted its state in a small JSON file under the\n * DSH home (`catppuccin-state.json`), served to the Client through the\n * `/catppuccin/state` route. 0.5.0 migrates persistence to the official\n * settings seam (`ctx.settings` Host-side, and whichever Client transport the\n * host serves — see `src/client/state-sync.ts`), which is durable across\n * browsers and origins just like the file was (it lives in the DSH home). The legacy file is READ-ONLY here: 0.5.0 migrates its content into\n * the settings document once and keeps the file on disk as a rollback copy\n * (an affected user can restore their old state by hand from it).\n *\n * Host-only module (node:fs / node:path resolve at runtime in the DSH\n * process); the Client never sees it.\n */\nimport { readFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { defaultDshHome } from './profile-detect.ts'\nimport { STATE_FILENAME, sanitizeState, type CatppuccinState } from './state.ts'\n\n/** Absolute path of the legacy state file under a DSH home. */\nexport function legacyStateFilePath(home = defaultDshHome()): string {\n return join(home, STATE_FILENAME)\n}\n\n/** Read the legacy durable state; absent or unparseable input means none yet. */\nexport function readLegacyState(home = defaultDshHome()): CatppuccinState | null {\n try {\n return sanitizeState(JSON.parse(readFileSync(legacyStateFilePath(home), 'utf8')))\n } catch {\n // Missing file (fresh install or a user who never wrote state) or a\n // corrupt/partial write — nothing to migrate.\n return null\n }\n}","/**\n * One-shot migration of the pre-0.5.0 durable file into whichever settings\n * seam the running host offers (issue #15 split it out of `src/index.ts` so\n * the composition stays thin and the migration is unit-testable).\n *\n * The migration writes `$DSH_HOME/catppuccin-state.json` into the durable\n * store exactly once, and only while that store holds NO user layer yet — a\n * user who already configured through the official settings surface (or a\n * previous migration) is never overwritten. The legacy file is left on disk\n * untouched as a rollback copy; a corrupt or absent file means nothing to\n * migrate. Every failure is non-fatal.\n *\n * Two host shapes, two addressing rules:\n *\n * - LEGACY (DSH <= 0.1.6-alpha.2): the namespace is one the plugin just\n * registered itself, so `describe()` finds it immediately and the caller\n * can run {@link migrateLegacyStateOnce} synchronously. \"No user layer\"\n * arrives as an ABSENT `user` field.\n * - NEW (DSH >= 0.1.7-alpha.1): the namespace IS the profile entry, and the\n * settings service only lists an entry whose own fiber is ACTIVE. The\n * `settings` inject callback fires SYNCHRONOUSLY when the service is\n * already live — at that instant the entry is still activating, so an\n * immediate `describe()` cannot see it and the migration would be silently\n * skipped. {@link scheduleLegacyMigration} defers it and also retries on\n * the entry's own `settings/document-updated`. Here \"no user layer\"\n * arrives as an EMPTY OBJECT (`configEditor` defaults `override` to `{}`),\n * which is why the predicate is injected rather than hard-coded.\n */\nimport type { Context } from '@deepseek-ai/cordis'\nimport { readLegacyState } from './legacy-state.ts'\nimport { isDefaultState, settingsSectionFromState } from './state.ts'\n\n/** The settings-service surface this module needs — satisfied by both seams\n * (`SettingsForms` on 0.1.7+, the document provider before it). */\nexport interface SettingsLike {\n /** Read the active namespaces (keyed by namespace / profile entry id). */\n describe(options?: { redactSecrets?: boolean }): readonly SettingsLikeDescriptor[]\n /** Merge a patch into one namespace's user layer. */\n update(ns: string, patch: object, expectedRevision?: number): Promise<void>\n}\n\n/** The descriptor fields this module reads. */\nexport interface SettingsLikeDescriptor {\n ns: string\n revision?: number\n user?: unknown\n}\n\n/** Outcome of one migration attempt. `pending` means \"not yet addressable —\n * try again\", which is the only one the caller should retry. */\nexport type MigrationOutcome =\n /** The legacy state was written into the durable store. */\n | 'migrated'\n /** The legacy file is absent or already equals the shipped defaults. */\n | 'absent'\n /** The store already holds a user layer — never overwritten. */\n | 'skipped'\n /** The namespace is not addressable yet (entry still activating / service\n * has not published its first describe). */\n | 'pending'\n\n/** Options for one migration attempt. */\nexport interface MigrationOptions {\n /**\n * Send the descriptor's revision as the write fence. The NEW seam sets it\n * (its `update` validates the revision the caller read); the LEGACY seam\n * omits it, so that line's write stays exactly what 0.5.x issued and cannot\n * turn into a spurious conflict against a namespace only we write.\n */\n fence?: boolean\n}\n\n/**\n * Run the migration once.\n * @param settings - the live settings service.\n * @param ns - the namespace to write: `CATPPUCCIN_SETTINGS_NS` on the legacy\n * seam, `CATPPUCCIN_ENTRY_ID` on the new one.\n * @param hasUserLayer - the channel's own \"the store already holds a user\n * layer\" predicate (see the module header).\n * @param options - fence policy (see {@link MigrationOptions}).\n * @returns the outcome; only `pending` is worth retrying.\n */\nexport async function migrateLegacyStateOnce(\n settings: SettingsLike,\n ns: string,\n hasUserLayer: (user: unknown) => boolean,\n options: MigrationOptions = {},\n): Promise<MigrationOutcome> {\n const legacy = readLegacyState()\n if (legacy === null || isDefaultState(legacy)) return 'absent'\n const descriptor = settings.describe({ redactSecrets: true })\n .find((candidate) => candidate.ns === ns)\n if (descriptor === undefined) return 'pending'\n if (hasUserLayer(descriptor.user)) return 'skipped'\n await settings.update(\n ns,\n settingsSectionFromState(legacy),\n options.fence === true ? descriptor.revision : undefined,\n )\n return 'migrated'\n}\n\n/**\n * Run the migration on the NEW seam without racing the plugin's own\n * activation: one deferred attempt, one retry when the entry's form finally\n * lands in the describe mirror, and one last attempt a few seconds later.\n * Bounded on purpose — a permanently missing entry (a user who renamed it)\n * must not leave a timer running for the process lifetime. The whole schedule\n * is fiber-scoped, so unloading the plugin cancels it.\n * @param ctx - the Host plugin context.\n * @param settings - the live settings service.\n * @param ns - the profile entry id carrying the form.\n * @param hasUserLayer - the new seam's user-layer predicate.\n */\nexport function scheduleLegacyMigration(\n ctx: Context,\n settings: SettingsLike,\n ns: string,\n hasUserLayer: (user: unknown) => boolean,\n): void {\n let settled = false\n // One attempt in flight at a time. The deferred timer and the entry's own\n // `settings/document-updated` routinely land within the same tick, and two\n // concurrent `describe` → `update` pairs race on the namespace revision: the\n // loser is refused and reported as a FAILURE even though the migration\n // succeeded (observed on a real 0.1.7 host, 2026-09-24). `claimed` is set\n // synchronously before the first `await`, so the second caller cannot pass.\n let claimed = false\n const attempt = (): void => {\n if (settled || claimed) return\n claimed = true\n void migrateLegacyStateOnce(settings, ns, hasUserLayer, { fence: true })\n .then((outcome) => {\n if (outcome === 'pending') {\n // Not addressable yet — release the slot so a later attempt retries.\n claimed = false\n return\n }\n settled = true\n if (outcome === 'migrated') {\n console.info(`[dsh-catppuccin] migrated the legacy state file into \"${ns}\"`)\n }\n })\n .catch((error: unknown) => {\n settled = true\n console.warn(`[dsh-catppuccin] legacy state migration failed: ${String(error)}`)\n })\n }\n ctx.effect(() => {\n const deferred = setTimeout(attempt, 0)\n const late = setTimeout(attempt, 3000)\n // The entry becomes addressable exactly when the settings service emits\n // its own document-changed notification for it.\n const off = ctx.on('settings/document-updated', (documentNs: string) => {\n if (String(documentNs) === ns) attempt()\n })\n return () => {\n clearTimeout(deferred)\n clearTimeout(late)\n off()\n }\n }, 'catppuccin: legacy state migration')\n}\n","/**\n * The Catppuccin settings contract — the official-settings shape of the\n * plugin's durable state (0.5.0+, replacing the legacy\n * `catppuccin-state.json`), in BOTH forms the DSH versions in the wild need.\n *\n * Two seams, one contract (issue #15):\n *\n * - **DSH <= 0.1.6-alpha.2** (`latest` on npm) serves an abstract settings\n * document. The Host half registers a namespace on it\n * (`settings.installSection(CATPPUCCIN_SETTINGS_NS, schema, base, hooks)`)\n * and the Client binds that namespace through `ctx.settingsScope`. This\n * module owns the registered schema ({@link CatppuccinSettingsSchema}) and\n * the composition `base` ({@link CATPPUCCIN_SETTINGS_BASE}).\n * - **DSH >= 0.1.7-alpha.1** (`next`) dropped both. A plugin no longer\n * registers a namespace: it declares a `Config` schema whose fields carry\n * `.volatile()`, and the settings service projects exactly those fields\n * into a form named by the ACTIVE PROFILE ENTRY id\n * (`CATPPUCCIN_ENTRY_ID`). That projected schema is {@link Config}, the\n * module's only export the Loader consumes.\n *\n * Both forms are built from ONE field table ({@link catppuccinSchemaFields}),\n * so a field can never drift between them. They must NOT share node\n * instances: `.volatile()` mutates the node's meta in place and returns it,\n * so reusing a node would silently mark the legacy schema volatile as well.\n *\n * {@link maybeVolatile} is the compatibility hinge: `.volatile()` exists only\n * from schemastery **3.18.3** (DSH 0.1.5/0.1.6 hosts pin `3.18.2`), and this\n * module is loaded with the HOST's schemastery (it stays external in the lib\n * build). An unconditional `.volatile()` would therefore throw at module\n * evaluation time on every old host — turning \"plugin pending\" into \"plugin\n * failed to load\". On such a host the guarded call is a no-op, which is\n * exactly the pre-0.1.7 status quo (no entry-level form exists there).\n *\n * This module is Host-side because it imports `@deepseek-ai/schemastery`\n * (resolved from the DSH profile tree at runtime). The Client never imports\n * it; it consumes the form by name.\n */\nimport Schema from '@deepseek-ai/schemastery'\nimport {\n CATPPUCCIN_THEME_IDS,\n DEFAULT_AUTO_CHECK,\n DEFAULT_GLASS,\n DEFAULT_SHIKI_STYLE,\n DEFAULT_UPDATE_CHANNEL,\n defaultSettingsSection,\n type CatppuccinSettingsSection,\n} from './state.ts'\n\n/** The subset of a schemastery node this module relies on. */\ninterface VolatileCapable {\n volatile?: () => unknown\n}\n\n/**\n * Mark a schema node volatile when the running schemastery supports it.\n *\n * Guarded on purpose (see the module header): `.volatile()` landed in\n * schemastery 3.18.3, while DSH <= 0.1.6-alpha.2 hosts pin 3.18.2 and this\n * module resolves schemastery from THEIR tree. Calling it unconditionally\n * throws at import time on those hosts.\n * @param node - freshly built schema node (never reused across schemas).\n * @returns the same node, marked volatile when the capability exists.\n */\nexport function maybeVolatile<T extends object>(node: T): T {\n const capable = node as VolatileCapable\n return typeof capable.volatile === 'function' ? capable.volatile() as T : node\n}\n\n/**\n * Build FRESH schema nodes for every field of the durable contract. Every\n * field maps 1:1 onto `CatppuccinSettingsSection` in `src/state.ts`, and every\n * default is the shipped default — the Client treats \"resolves to the shipped\n * defaults\" as \"no user choice yet\", so a wrong default would write garbage\n * into a user's profile on first boot.\n *\n * Callers MUST NOT share the returned nodes between schemas (see the header).\n * @returns a new field table, one fresh node per field.\n */\nexport function catppuccinSchemaFields(): Record<string, Schema> {\n return {\n flavor: Schema.union([...CATPPUCCIN_THEME_IDS, 'off']).default('off'),\n glass: Schema.object({\n enabled: Schema.boolean().default(DEFAULT_GLASS.enabled),\n mode: Schema.union(['mica', 'compat']).default(DEFAULT_GLASS.mode),\n blur: Schema.number().min(0).max(40).default(DEFAULT_GLASS.blur),\n frost: Schema.number().min(0).max(100).default(DEFAULT_GLASS.frost),\n brightness: Schema.number().min(0).max(100).default(DEFAULT_GLASS.brightness),\n }),\n autoCheck: Schema.boolean().default(DEFAULT_AUTO_CHECK),\n updateChannel: Schema.union(['latest', 'beta']).default(DEFAULT_UPDATE_CHANNEL),\n overrides: Schema.dict(Schema.string()).default({}),\n shikiStyle: Schema.union(['default', 'italic-comments']).default(DEFAULT_SHIKI_STYLE),\n }\n}\n\n/** The same table with every leaf marked volatile where the capability exists.\n * Marking the leaves (rather than the `glass` subtree) keeps\n * `isVolatilePath` true for each nested path without putting volatile inside\n * volatile — schemastery rejects that combination. */\nfunction volatileFields(): Record<string, Schema> {\n const fields = catppuccinSchemaFields()\n const glassFields = (fields.glass as Schema & { dict?: Record<string, Schema> }).dict ?? {}\n return {\n flavor: maybeVolatile(fields.flavor),\n glass: Schema.object(Object.fromEntries(\n Object.entries(glassFields).map(([key, node]) => [key, maybeVolatile(node)]),\n )),\n autoCheck: maybeVolatile(fields.autoCheck),\n updateChannel: maybeVolatile(fields.updateChannel),\n overrides: maybeVolatile(fields.overrides),\n shikiStyle: maybeVolatile(fields.shikiStyle),\n }\n}\n\n/**\n * The namespace schema registered by the LEGACY Host seam\n * (`settings.installSection`). Deliberately plain: on a host that has\n * `installSection` there is no `.volatile()` capability, and the field set is\n * resolved by the settings document rather than by a form projection.\n */\nexport const CatppuccinSettingsSchema = Schema.object(catppuccinSchemaFields())\n\n/** The composition `base` for the legacy namespace: the shipped defaults. */\nexport const CATPPUCCIN_SETTINGS_BASE: CatppuccinSettingsSection = defaultSettingsSection()\n\n/**\n * The plugin `Config` consumed by the NEW Host seam (DSH >= 0.1.7-alpha.1).\n * Its volatile fields are what the settings service projects into the form the\n * Client addresses by `CATPPUCCIN_ENTRY_ID`; the Host half never reads the\n * values itself. On a legacy host every field stays plain, so the entry\n * exposes no form at all — the old seam keeps doing the persisting.\n */\nexport const Config = Schema.object(volatileFields())\n\n/**\n * The value shape the Loader hands `apply` for {@link Config}. Every leaf is a\n * `Volatile` reference on a 0.1.7 host (read it with `.get()`); the Host half\n * deliberately consumes none of them, so this interface exists to document the\n * contract and to keep the schema honest.\n */\nexport interface CatppuccinConfig {\n flavor: { get(): CatppuccinSettingsSection['flavor'] }\n glass: {\n enabled: { get(): boolean }\n mode: { get(): CatppuccinSettingsSection['glass']['mode'] }\n blur: { get(): number }\n frost: { get(): number }\n brightness: { get(): number }\n }\n autoCheck: { get(): boolean }\n updateChannel: { get(): CatppuccinSettingsSection['updateChannel'] }\n overrides: { get(): Record<string, string> }\n shikiStyle: { get(): CatppuccinSettingsSection['shikiStyle'] }\n}\n","/**\n * Host half of the Catppuccin theme plugin for the DeepSeek Harness web GUI.\n *\n * Pure UI plugin: the four flavour themes and the glass skin ship in the\n * browser half (exports[\"./client\"], discovered through the package.json\n * dsh.client declaration). Since 0.5.0 the flavour/glass preferences persist\n * durably through the OFFICIAL settings seam — but that seam changed shape in\n * DSH 0.1.7-alpha.1 (issue #15), so this half wires BOTH:\n *\n * - **<= 0.1.6-alpha.2** (`latest` on npm): a settings SERVICE the plugin\n * registers a namespace on (`ctx.settings.installSection`) and the Client\n * binds through `ctx.settingsScope`.\n * - **>= 0.1.7-alpha.1** (`next`): no registration at all. The plugin\n * declares a `Config` schema with `.volatile()` fields (exported below),\n * and the settings service projects them into a form named by the active\n * profile entry id; the Client reads it through `ctx.configForms`.\n *\n * Which one is live is decided by FEATURE DETECTION, never by version parsing:\n * `installSection` is present exactly on the legacy service. Both wirings live\n * inside an optional `ctx.inject(['settings'], …)` child, so a deployment\n * without a settings provider keeps the plugin working from browser\n * localStorage alone (losing only cross-restart durability). No profile is\n * ever blocked on the settings service.\n *\n * The legacy seam itself predates DSH 0.1.1-rc.2, which is why 0.4.x shipped a\n * hand-rolled `$DSH_HOME/catppuccin-state.json` + `/catppuccin/state` route\n * instead: the Host settings wire then only served an explicit allowlist of\n * namespaces (see dsh-host-apiproxy's WEB_SETTINGS_NAMESPACES), so a\n * plugin-owned settings namespace answered `settings-not-exposed` even when\n * registered. 0.1.1-rc.2 dropped the allowlist (\"注册即暴露\"), and the legacy\n * file is now only a one-shot migration source (`src/legacy-state.ts`,\n * consumed by `src/migrate-legacy.ts`; the file stays as rollback).\n *\n * The one host-side behavior besides settings is the\n * `/catppuccin/check-update` exact webServer route, owned by\n * `src/update-check/host.ts` (item A: the update-check logic used to live\n * inline here; it moved so `index.ts` stays a thin composition of the host\n * responsibilities and the route module can be tested on its own).\n * `webServer` is a hard inject dependency, so Cordis starts this plugin only\n * after the service is live — mounting can never race ahead of it and\n * silently skip the routes. (An earlier version read `ctx.get('webServer')`\n * once and bailed silently when it was not yet visible, which left a\n * mounted-but-routeless plugin and made \"check for updates\" fail forever.)\n * A headless profile has no webServer and no settings UI, so the plugin\n * simply stays waiting there. The responses carry only the plugin's own\n * settings and public package metadata, so no workspace gate is needed.\n */\nimport type { Context } from '@deepseek-ai/cordis'\n// Type-only: pulls the settings service's Context merge (ctx.settings).\nimport type {} from '@deepseek-ai/dsh-settings'\nimport { registerUpdateCheckRoute } from './update-check/host.ts'\nimport { migrateLegacyStateOnce, scheduleLegacyMigration, type SettingsLike } from './migrate-legacy.ts'\nimport { CATPPUCCIN_SETTINGS_BASE, CatppuccinSettingsSchema } from './settings-catppuccin.ts'\nimport { CATPPUCCIN_ENTRY_ID, CATPPUCCIN_SETTINGS_NS, hasUserLayerSection } from './state.ts'\n\n/** Stable cordis plugin name (matches cordis.patch.yml insert id). */\nexport const name = 'dsh-catppuccin'\n\n/**\n * Hard dependencies: the update check is a webServer route, so this Host\n * half is not activated until the webServer service is live. Declaring\n * inject (not a one-shot `ctx.get` probe) makes route registration\n * deterministic: Cordis reactivates the plugin once the service appears, no\n * matter the mount order.\n */\nexport const inject = ['webServer']\n\n/**\n * The plugin's volatile configuration form (DSH >= 0.1.7-alpha.1) — the\n * namespace the Client addresses by `CATPPUCCIN_ENTRY_ID`.\n *\n * Re-exported here because the Loader reads `Config` off the plugin module;\n * on a legacy host the guarded `.volatile()` calls inside it are no-ops, so\n * the entry exposes no form there and the `installSection` wiring below keeps\n * doing the persisting.\n */\nexport { Config } from './settings-catppuccin.ts'\n\n/**\n * The LEGACY settings service (DSH <= 0.1.6-alpha.2): it registers plugin\n * namespaces. Declared as a local structural type on purpose — 0.1.7 removed\n * the method from the published types, and this shape is all this branch\n * touches, so the cross-version code stays honest instead of reaching for\n * `any`.\n */\ninterface LegacySettings extends SettingsLike {\n installSection?: (\n owner: Context,\n ns: string,\n schema: unknown,\n base: unknown,\n hooks: { setSource: () => void, onChange: () => void },\n ) => void\n}\n\n/** The NEW settings service's extra surface (DSH >= 0.1.7-alpha.1). */\ninterface ModernSettings extends SettingsLike {\n /** Own this plugin instance's automatic-page policy (`auto` defaults true).\n * Absent on the legacy seam, hence the feature detection. */\n configure?: (presentation: { auto?: boolean }, owner?: unknown) => unknown\n}\n\n/** The legacy document describes \"no user layer\" as an ABSENT field (it maps\n * over the namespaces it has a section for), so presence is the whole test —\n * the same predicate 0.5.x used. */\nfunction legacyUserLayer(user: unknown): boolean {\n return user !== undefined\n}\n\n/** Host plugin body: register the update-check route and wire whichever\n * settings seam the running host offers (webServer is inject; settings is\n * optional wiring). */\nexport function apply(ctx: Context): void {\n registerUpdateCheckRoute(ctx)\n\n ctx.inject(['settings'], (sctx) => {\n const settings = sctx.settings as unknown as LegacySettings & ModernSettings\n if (typeof settings.installSection === 'function') {\n // Legacy seam. Registering and migrating inline reproduces 0.5.x\n // behaviour exactly: the namespace is ours, so it is addressable the\n // moment `installSection` returns, and the write stays unfenced.\n settings.installSection(\n sctx,\n CATPPUCCIN_SETTINGS_NS,\n CatppuccinSettingsSchema,\n CATPPUCCIN_SETTINGS_BASE,\n {\n // Host half never consumes the resolved value itself — the Client\n // renders and edits it through its own bound scope — so the source\n // sink and change hook are intentionally empty (the wiring still\n // needs them to keep the registration fiber-scoped).\n setSource() {},\n onChange() {},\n },\n )\n void migrateLegacyStateOnce(settings, CATPPUCCIN_SETTINGS_NS, legacyUserLayer)\n .catch((error: unknown) => {\n console.warn(`[dsh-catppuccin] legacy state migration failed: ${String(error)}`)\n })\n return\n }\n\n // New seam. Nothing to register: the exported `Config` IS the namespace,\n // and its volatile fields are what the settings service projects into the\n // form the Client reads. Suppress the schema-generated page — this plugin\n // ships its own settings rows.\n settings.configure?.({ auto: false }, ctx.fiber)\n // The entry is not addressable until its own fiber is ACTIVE, so the\n // migration is deferred and retried rather than run inline (a synchronous\n // `describe()` here cannot see us yet — see `src/migrate-legacy.ts`).\n scheduleLegacyMigration(ctx, settings, CATPPUCCIN_ENTRY_ID, hasUserLayerSection)\n })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["pkg.version"],"sources":["../package.json","../src/versions.ts","../src/update-check.ts","../src/profile-detect.ts","../src/update-check/host.ts","../src/state.ts","../src/legacy-state.ts","../src/migrate-legacy.ts","../src/settings-catppuccin.ts","../src/index.ts"],"sourcesContent":["","/**\n * Minimal semver comparison for update detection — dependency-free, so the\n * Host half and the unit tests share one implementation. Follows the semver\n * 2.0.0 precedence rules for `major.minor.patch[-prerelease][+build]`:\n * build metadata is ignored, prerelease identifiers compare numerically when\n * both are numeric and ASCII-wise otherwise, and a prerelease of the same\n * core sorts before its stable release.\n */\n\n/** A parsed semver triple plus its prerelease identifiers. */\nexport interface ParsedVersion {\n major: number\n minor: number\n patch: number\n /** Prerelease identifiers: numeric parts become numbers, the rest strings. */\n prerelease: readonly (string | number)[]\n}\n\n/** Parse a semver string, or null when it is not `v?x.y.z[-pre][+build]`. */\nexport function parseVersion(raw: string): ParsedVersion | null {\n const match = /^v?(\\d+)\\.(\\d+)\\.(\\d+)(?:-([0-9A-Za-z.-]+))?(?:\\+[0-9A-Za-z.-]+)?$/.exec(raw.trim())\n if (match === null) return null\n const prerelease = (match[4] ?? '').split('.').filter((part) => part !== '').map((part) =>\n /^\\d+$/.test(part) ? Number(part) : part,\n )\n return {\n major: Number(match[1]),\n minor: Number(match[2]),\n patch: Number(match[3]),\n prerelease,\n }\n}\n\n/**\n * Order two version strings per semver precedence.\n * @returns -1 when `a` < `b`, 0 when equal, 1 when `a` > `b`.\n * Unparseable inputs sort after every parseable version (equal to\n * each other), so a garbage `current` never reports an update.\n */\nexport function compareVersions(a: string, b: string): number {\n const pa = parseVersion(a)\n const pb = parseVersion(b)\n if (pa === null && pb === null) return 0\n if (pa === null) return 1\n if (pb === null) return -1\n for (const key of ['major', 'minor', 'patch'] as const) {\n if (pa[key] !== pb[key]) return pa[key] < pb[key] ? -1 : 1\n }\n if (pa.prerelease.length === 0 && pb.prerelease.length === 0) return 0\n if (pa.prerelease.length === 0) return 1\n if (pb.prerelease.length === 0) return -1\n const shared = Math.min(pa.prerelease.length, pb.prerelease.length)\n for (let i = 0; i < shared; i += 1) {\n const x = pa.prerelease[i]\n const y = pb.prerelease[i]\n if (x === y) continue\n // Numeric identifiers sort before alphanumeric ones; two numerics compare\n // numerically, two strings ASCII-wise.\n if (typeof x === 'number' && typeof y === 'number') return x < y ? -1 : 1\n if (typeof x === 'number') return -1\n if (typeof y === 'number') return 1\n return x < y ? -1 : 1\n }\n if (pa.prerelease.length !== pb.prerelease.length) {\n return pa.prerelease.length < pb.prerelease.length ? -1 : 1\n }\n return 0\n}\n\n/**\n * Whether `latest` is a strictly newer release than `current` — i.e. whether\n * an update is available. A newer installed prerelease (e.g. a git install\n * ahead of npm) correctly reports no update.\n */\nexport function isUpdateAvailable(current: string, latest: string): boolean {\n return compareVersions(current, latest) < 0\n}\n","/**\n * Shared contract of the update-check feature — consumed by both halves:\n * the Host registers an exact webServer route answering this JSON payload,\n * and the Client settings row fetches that same-origin route and renders it.\n * This module is dependency-free so both bundles inline it.\n */\nimport { compareVersions, parseVersion } from './versions.ts'\nimport type { UpdateChannel } from './state.ts'\n\nexport type { UpdateChannel } from './state.ts'\n\n/** The plugin's own npm name (the registry lookup key). */\nexport const PACKAGE_NAME = '@nonamelego/dsh-catppuccin'\n\n/**\n * npm registry abbreviated packument endpoint (the `install-v1` document).\n * The `latest` endpoint alone is not enough: this project ships prereleases\n * under the `beta` dist-tag while `latest` stays on the last stable, so the\n * check reads `dist-tags` to serve both channels.\n */\nexport const REGISTRY_PACKUMENT_URL =\n `https://registry.npmjs.org/${PACKAGE_NAME.replace('/', '%2F')}`\n\n/** Host route the Client row fetches (exact match, same origin). */\nexport const UPDATE_ROUTE_PATH = '/catppuccin/check-update'\n\n/** Network budget for the Host's registry lookup. */\nexport const UPDATE_FETCH_TIMEOUT_MS = 8000\n\n/** The `dist-tags` object of an npm packument. */\nexport interface DistTags {\n latest?: string\n beta?: string\n}\n\n/** The newest release a given install should chase, and on which channel. */\nexport interface NewestRelease {\n version: string\n channel: UpdateChannel\n}\n\n/**\n * The copyable CLI upgrade command for one channel (`dsh plugin` is a thin\n * pnpm forwarder). The profile name comes from the Host's runtime probe\n * (`src/profile-detect.ts`), never hard-coded, so the command works on any\n * deployment out of the box.\n * @param channel - the dist-tag to chase.\n * @param profile - the DSH profile name to target.\n */\nexport function updateCommandFor(channel: UpdateChannel, profile: string): string {\n return `dsh plugin --profile ${profile} add ${PACKAGE_NAME}@${channel}`\n}\n\n/**\n * Pick the newest release worth reporting for the given install:\n * - with NO channel preference: a stable install chases only `latest` (never\n * downgrades onto a beta), while a prerelease install (e.g. a\n * `beta`-tagged version) also chases `beta` — so beta users see newer\n * betas, and a stable release that outranks their prerelease promotes\n * them via `@latest`;\n * - an explicit `beta` preference (the settings row's prerelease pick)\n * chases `beta` even from a stable install — beta testers opt in without\n * changing their install; the newest of the two tags still wins, so a\n * newer stable promotes them back onto `@latest`;\n * - an explicit `latest` preference pins the check to the stable tag —\n * never a beta, even from a prerelease install.\n * @param current - the installed version.\n * @param tags - the registry's dist-tags.\n * @param preferredChannel - optional persisted channel preference (item I).\n * @returns the newest candidate and its channel, or null when nothing applies.\n */\nexport function selectNewest(current: string, tags: DistTags, preferredChannel?: UpdateChannel): NewestRelease | null {\n const candidates: NewestRelease[] = []\n if (tags.latest !== undefined) candidates.push({ version: tags.latest, channel: 'latest' })\n if (preferredChannel === 'beta') {\n // Beta chase, opt-in: follow the beta tag even from a stable install.\n if (tags.beta !== undefined) candidates.push({ version: tags.beta, channel: 'beta' })\n } else if (preferredChannel === undefined) {\n // No preference: prerelease installs chase beta on top of latest.\n const parsed = parseVersion(current)\n if (tags.beta !== undefined && parsed !== null && parsed.prerelease.length > 0) {\n candidates.push({ version: tags.beta, channel: 'beta' })\n }\n }\n // preferredChannel === 'latest': candidates hold the stable tag only.\n if (candidates.length === 0) return null\n let best = candidates[0]\n for (const candidate of candidates.slice(1)) {\n if (compareVersions(best.version, candidate.version) < 0) best = candidate\n }\n return best\n}\n\n/** Stable machine-readable outcome code (borrowed from the plugin-update\n * error-code discipline of dsh-vision-toolkit): the Client maps it to copy\n * instead of sniffing human text. `network.*` splits the old catch-all\n * `network` (item U): `network.local` means the browser→Host hop failed\n * (host unreachable / DNS — \"check your network\"), `network.upstream` means\n * the Host reached the registry but the lookup timed out (\"npm is\n * temporarily unavailable\"). */\nexport type UpdateErrorCode =\n | 'ok'\n | 'network.local' // the same-origin fetch itself failed (client side)\n | 'network.upstream' // the Host's registry fetch timed out\n | 'registry-unreachable' // npm registry could not be reached (fetch threw)\n | 'registry-http' // registry answered with a non-2xx status\n | 'no-dist-tags' // registry returned no usable dist-tags\n | 'invalid-response' // registry response could not be parsed\n\n/** How this package is installed inside the probed DSH profile. */\nexport type InstallSource = 'registry' | 'link' | 'file' | 'git' | 'unknown'\n\n/** Runtime that answered the check — drives the copy (command hint / restart). */\nexport type UpdateEnv = 'desktop' | 'web'\n\n/**\n * JSON payload of the update check. `ok: false` carries a human-readable\n * `error` plus a stable `code`; `ok: true` carries the comparison result.\n * The Host owns the semver comparison, the channel selection, and the\n * profile probe (single source of truth), so the Client never parses\n * versions or guesses the profile name itself.\n */\nexport interface UpdateCheckPayload {\n /** Whether the registry lookup succeeded. */\n ok: boolean\n /** Installed version (from this package's own manifest). */\n current?: string\n /** Newest release worth chasing (`latest`, plus `beta` for prerelease installs). */\n latest?: string\n /** `latest` is strictly newer than `current`. */\n outdated?: boolean\n /** The dist-tag the newest release came from. */\n channel?: UpdateChannel\n /** Copyable CLI upgrade command (present when outdated). */\n updateCommand?: string\n /** DSH profile name the upgrade command targets (probed, not guessed). */\n profile?: string\n /** Whether the profile probe actually found the install (false = fallback). */\n profileDetected?: boolean\n /** Runtime that answered: DSH Desktop vs standard dsh web (drives copy). */\n env?: UpdateEnv\n /** How the package is installed in that profile (drives copy). */\n installSource?: InstallSource\n /** ISO timestamp of the check (registry lookup moment). */\n checkedAt?: string\n /** Stable outcome code (see {@link UpdateErrorCode}). */\n code?: UpdateErrorCode\n /** Failure detail (only when `ok` is false). */\n error?: string\n}\n","/**\n * Runtime probe of the DSH profile this plugin is installed into.\n *\n * Borrowed from dsh-vision-toolkit's plugin-update module (MIT) and trimmed\n * to what the update check needs: instead of hard-coding a profile name into\n * the upgrade command, the Host scans the DSH home's `profiles/` directory\n * (plus the `--profile` argv hint) for the profile whose package.json lists\n * this package as a direct dependency, and classifies the install source so\n * the Client can tell \"registry install → copy the npm command\" apart from\n * \"local link/file/git install → the npm command does not apply\".\n *\n * Pure functions (`profileHint`, `isRegistrySpec`, `installSourceOf`) are\n * dependency-free and unit-tested; the IO probe (`detectProfile`) reads only\n * package.json manifests and never mutates anything.\n */\nimport { readFile, readdir } from 'node:fs/promises'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport type { InstallSource } from './update-check.ts'\nimport { PACKAGE_NAME } from './update-check.ts'\n\n/** Fallback profile name when nothing can be probed (keeps the command valid). */\nexport const FALLBACK_PROFILE = 'web'\n\n/** Whether this process runs inside Electron (Node mode included).\n *\n * Electron-as-node(`ELECTRON_RUN_AS_NODE=1`)**仍然**填 `process.versions.electron`\n * —— 2026-09-24 本机实测:Electron 37.10.3 下为 `'37.10.3'`,纯 node 下为 `undefined`。\n * 官方桌面壳的 Host 正是由 Electron 以 node 模式 spawn 的(`apps/desktop/src/host-process.ts`\n * 用 Electron 可执行文件 + `ELECTRON_RUN_AS_NODE=1`),所以这是它在 profile 进程里留下的**唯一**痕迹。\n * @param versions - `process.versions` to inspect (injectable so tests need no real Electron).\n * @returns whether the runtime is Electron rather than a plain Node.js.\n */\nexport function isElectronRuntime(\n versions: Record<string, string | undefined> = process.versions,\n): boolean {\n const electron = versions.electron\n return typeof electron === 'string' && electron.trim() !== ''\n}\n\n/**\n * Detect a DSH desktop shell — two independent signals, either one is enough:\n *\n * 1. **社区壳**(`anywhere-labs/dsh-desktop`,含其 `dsh-desktop-next` 重写版)在 profile\n * 进程的环境里放 `DSH_DESKTOP_NODE_EXECUTABLE`(它的包安装/内部启动器约定)。\n * 2. **官方壳**(`deepseek-ai/deepseek-harness` 的 `apps/desktop` + `apps/desktop-host`,\n * `private: true`、未发 npm)**什么都不设**:⚠️ 2026-09-24 取证推翻 2026-09-22 的假设——\n * 上游架构说明原话是「`DSH_DESKTOP_NODE_EXECUTABLE` **仅为包安装注入**」\n * (`.agents/notes/implemented/architecture/2026-09-11-desktop-electron-node-runtime.zh.md`),\n * 代码印证:`host-process.ts` 以 `desktopNodeEnvironment(this.node, undefined, …)` 起 host,\n * `bin === undefined` 时不设它;`apps/desktop-host/src/index.ts` 只在\n * `runProfile({ packageManager: { env } })` 里给它。于是官方壳在 profile 进程里靠**运行时**识别:\n * Host 是 Electron-as-node,故 `process.versions.electron` 有值(见 `isElectronRuntime`)。\n *\n * 官方仓**没有** `desktopProfiles` 服务(2026-09-22 全仓搜索命中 0),所以那路服务探测只覆盖社区壳;\n * 两路都不命中时回落纯 web 文案——**profile 名不受影响**(`detectProfile` 扫 `profiles/` 目录命中 `desktop`),\n * 持久化也不受影响(走 `configForms` seam,与壳无关)。\n * 代价权衡:在「用 Electron 壳跑 dsh web」这类少见组合下会误判成桌面,而误判只影响提示文案措辞。\n * @param env - the environment to inspect (injectable so tests need no real desktop).\n * @param versions - `process.versions` to inspect (injectable for the same reason).\n * @returns whether this process looks like a DSH desktop shell.\n */\nexport function isDesktopShellEnv(\n env: Record<string, string | undefined>,\n versions: Record<string, string | undefined> = process.versions,\n): boolean {\n const marker = env.DSH_DESKTOP_NODE_EXECUTABLE\n if (typeof marker === 'string' && marker.trim() !== '') return true\n return isElectronRuntime(versions)\n}\n\n/** The DSH home the running process owns — `$DSH_HOME` when set, else\n * `~/.dsh`. Port-independent and stable across restarts, so it is the right\n * root for the plugin's durable state file. */\nexport function defaultDshHome(): string {\n return process.env.DSH_HOME?.trim() || join(homedir(), '.dsh')\n}\n\n/** How this package's dependency spec was written in the profile manifest. */\nexport interface ProfileProbe {\n /** Profile name to target in the upgrade command. */\n name: string\n /** Whether the probe actually matched a profile (false = fallback). */\n detected: boolean\n /** Install source classification of this package in that profile. */\n installSource: InstallSource\n}\n\n/**\n * Extract the profile name a running DSH process was started with: either a\n * leading bare `web`-style argument or an explicit `--profile <name>` /\n * `--profile=<name>` flag. Mirrors vision-toolkit's `profileHint`.\n */\nexport function profileHint(argv: readonly string[]): string | undefined {\n if (argv[0] === 'web') return 'web'\n for (let index = 0; index < argv.length; index += 1) {\n const value = argv[index]\n if (value?.startsWith('--profile=')) return value.slice('--profile='.length)\n if (value === '--profile') return argv[index + 1]\n }\n return undefined\n}\n\n/**\n * Whether a dependency spec is a plain npm registry spec (a semver range),\n * not a local/workspace/git/URL source. Mirrors vision-toolkit's\n * `registryInstallSpec`.\n */\nexport function isRegistrySpec(spec: string): boolean {\n const normalized = spec.trim().toLowerCase()\n return normalized.length > 0\n && !normalized.startsWith('workspace:')\n && !/^[a-z][a-z0-9+.-]*:/u.test(normalized)\n && !normalized.includes('/')\n && !normalized.includes('\\\\')\n}\n\n/** Classify a non-registry spec into the more precise local source kind. */\nexport function installSourceOf(spec: string): InstallSource {\n const normalized = spec.trim().toLowerCase()\n if (normalized.startsWith('link:') || normalized.startsWith('workspace:')) return 'link'\n if (normalized.startsWith('file:')) return 'file'\n return 'git'\n}\n\n/** Read one profile's manifest at its manifest dir and return this package's install source. */\nasync function inspectProfileAt(manifestDir: string): Promise<InstallSource | undefined> {\n try {\n const manifest = JSON.parse(\n await readFile(join(manifestDir, 'package.json'), 'utf8'),\n ) as { dependencies?: Record<string, unknown> }\n const spec = manifest.dependencies?.[PACKAGE_NAME]\n if (typeof spec !== 'string') return undefined\n return isRegistrySpec(spec) ? 'registry' : installSourceOf(spec)\n } catch {\n return undefined\n }\n}\n\n/** Read one profile under the DSH home and return this package's install source. */\nasync function inspectProfile(dshHome: string, name: string): Promise<InstallSource | undefined> {\n return inspectProfileAt(join(dshHome, 'profiles', name))\n}\n\n/** The launcher-resolved profile under DSH Desktop (`desktopProfiles.current`). */\nexport interface DesktopProfileHint {\n /** Launcher-selected profile name. */\n name: string\n /** Absolute manifest dir; falls back to the DSH-home scan path when absent. */\n dir?: string\n}\n\n/**\n * Probe the DSH profile this package is installed into.\n * Priority: an authoritative `desktopProfile` (DSH Desktop's\n * `desktopProfiles.current` — the Desktop docs say never infer the profile\n * from argv / settings / `$DSH_HOME` when it is available), then the\n * `--profile`/bare-name argv hint, then a scan of every `profiles/` entry\n * whose manifest lists this package as a direct dependency. Falls back to\n * `FALLBACK_PROFILE` with `detected: false` when nothing matches (e.g. a dev\n * install outside any profile).\n */\nexport async function detectProfile(options: {\n /** DSH home; defaults to $DSH_HOME or `~/.dsh`. */\n dshHome?: string\n /** Process argv slice; defaults to the running process's argv. */\n argv?: readonly string[]\n /** Authoritative profile under DSH Desktop (`desktopProfiles.current`), when running there. */\n desktopProfile?: DesktopProfileHint | undefined\n} = {}): Promise<ProfileProbe> {\n const dshHome = options.dshHome ?? defaultDshHome()\n const argv = options.argv ?? process.argv.slice(2)\n\n // DSH Desktop wins over every probe: the launcher already resolved the\n // active profile, so the upgrade command targets it directly. The install\n // source still comes from reading that profile's manifest (via its explicit\n // dir) so the \"npm command does not apply\" local-install hint keeps working.\n const desktop = options.desktopProfile\n if (desktop?.name) {\n const dir = desktop.dir?.trim() || join(dshHome, 'profiles', desktop.name)\n const source = await inspectProfileAt(dir)\n return { name: desktop.name, detected: true, installSource: source ?? 'unknown' }\n }\n\n const hint = profileHint(argv)\n\n if (hint !== undefined) {\n const source = await inspectProfile(dshHome, hint)\n if (source !== undefined) return { name: hint, detected: true, installSource: source }\n }\n\n const profilesDir = join(dshHome, 'profiles')\n let names: string[] = []\n try {\n names = (await readdir(profilesDir, { withFileTypes: true }))\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name)\n } catch {\n // No profiles directory — the fallback below still produces a valid command.\n }\n if (hint !== undefined && !names.includes(hint)) names.push(hint)\n\n for (const name of names) {\n const source = await inspectProfile(dshHome, name)\n if (source !== undefined) return { name, detected: true, installSource: source }\n }\n\n return { name: hint ?? FALLBACK_PROFILE, detected: false, installSource: 'unknown' }\n}","/**\n * Host half of the update-check feature (extracted from `src/index.ts` —\n * item A). Owns the exact webServer route, the npm registry lookup, the\n * short-TTL verdict cache, the ETag conditional-request cache (item W), and\n * the channel preference parsed from the query string (item I).\n *\n * The Client settings row fetches this same-origin route and renders the\n * verdict; every move the route makes (semver comparison, channel choice,\n * profile probe) happens here so the Client never parses versions or\n * guesses profile names.\n */\nimport type { Context } from '@deepseek-ai/cordis'\nimport pkg from '../../package.json'\nimport {\n REGISTRY_PACKUMENT_URL,\n UPDATE_FETCH_TIMEOUT_MS,\n UPDATE_ROUTE_PATH,\n selectNewest,\n updateCommandFor,\n type UpdateChannel,\n type UpdateCheckPayload,\n type UpdateEnv,\n} from '../update-check.ts'\nimport { isUpdateAvailable } from '../versions.ts'\nimport { detectProfile, isDesktopShellEnv } from '../profile-detect.ts'\n\n// Minimal structural types for the parts of node:http and the webServer\n// service this plugin touches. The host bundle resolves cordis and friends\n// from the profile tree at runtime and ships no @types/node, so the route\n// contract is spelled out locally instead of imported.\ninterface HttpRequestLike {\n url?: string\n method?: string\n on(event: 'data', listener: (chunk: Uint8Array) => void): unknown\n on(event: 'end', listener: () => void): unknown\n on(event: 'error', listener: (error: Error) => void): unknown\n}\ninterface HttpResponseLike {\n writeHead(status: number, headers: Record<string, string>): void\n end(body?: string): void\n}\ninterface WebRouteLike {\n kind: 'exact' | 'prefix'\n path: string\n handler: (req: HttpRequestLike, res: HttpResponseLike) => void | Promise<void>\n}\ninterface WebServerLike {\n register(route: WebRouteLike): () => void\n}\n\n/** Minimal structural type for DSH Desktop's public `desktopProfiles` service\n * (the host bundle ships no dsh-plugin-desktop types). */\ninterface DesktopProfilesLike {\n readonly current?: {\n readonly name: string\n readonly dir?: string\n }\n}\n\n/** Query-string channel preference (item I): `?channel=beta` chases the\n * beta tag from a stable install; anything else keeps the install-derived\n * channel logic of `selectNewest`. */\nfunction parseChannel(url: string | undefined): UpdateChannel | undefined {\n if (url === undefined) return undefined\n try {\n const channel = new URL(url, 'http://dsh.localhost').searchParams.get('channel')\n return channel === 'beta' ? 'beta' : channel === 'latest' ? 'latest' : undefined\n } catch {\n return undefined\n }\n}\n\nfunction sendJson(res: HttpResponseLike, status: number, payload: unknown): void {\n res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })\n res.end(JSON.stringify(payload))\n}\n\n/** Result of one registry lookup: an OK payload, a 304 not-modified (serve\n * the cached verdict), or an error payload (never cached). */\ntype FetchResult =\n | { kind: 'ok'; payload: UpdateCheckPayload; etag?: string }\n | { kind: 'not-modified' }\n | { kind: 'error'; payload: UpdateCheckPayload }\n\n/** Query the npm registry and build the check payload. The probe result\n * feeds the upgrade command's profile name and the install-source copy, so\n * the Client never guesses either. Under DSH Desktop the target profile is\n * `desktopProfiles.current` (launcher-resolved, authoritative), so the\n * copied command targets the right profile there too.\n *\n * Failure classification (item U): a timed-out lookup (the abort fired) is\n * `network.upstream` — the npm registry is temporarily unavailable; a\n * fetch that throws outright (DNS / connection refused) stays\n * `registry-unreachable`; the 304 conditional path (item W) revalidates\n * the cached verdict without pulling the full packument again. */\nasync function fetchLatestVersion(options: {\n env: UpdateEnv\n channel?: UpdateChannel\n etag?: string\n desktopProfile?: DesktopProfilesLike['current']\n}): Promise<FetchResult> {\n const current = pkg.version\n const probe = await detectProfile(\n options.desktopProfile !== undefined ? { desktopProfile: options.desktopProfile } : {},\n )\n const base = {\n current,\n env: options.env,\n profile: probe.name,\n profileDetected: probe.detected,\n installSource: probe.installSource,\n checkedAt: new Date().toISOString(),\n }\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), UPDATE_FETCH_TIMEOUT_MS)\n let response: Response\n try {\n response = await fetch(REGISTRY_PACKUMENT_URL, {\n signal: controller.signal,\n headers: {\n accept: 'application/vnd.npm.install-v1+json',\n // Item W: conditional request — a 304 answer revalidates the cached\n // verdict without pulling the full packument again.\n ...(options.etag !== undefined ? { 'if-none-match': options.etag } : {}),\n },\n })\n } catch (error) {\n const timedOut = controller.signal.aborted\n return {\n kind: 'error',\n payload: {\n ...base,\n ok: false,\n code: timedOut ? 'network.upstream' : 'registry-unreachable',\n error: error instanceof Error ? error.message : String(error),\n },\n }\n } finally {\n clearTimeout(timer)\n }\n // 304: the registry's own cache says our cached verdict is still current.\n if (response.status === 304) return { kind: 'not-modified' }\n if (!response.ok) {\n return {\n kind: 'error',\n payload: {\n ...base,\n ok: false,\n code: 'registry-http',\n error: `npm registry responded HTTP ${response.status}`,\n },\n }\n }\n let data: { 'dist-tags'?: Record<string, string> }\n try {\n data = await response.json() as { 'dist-tags'?: Record<string, string> }\n } catch {\n return {\n kind: 'error',\n payload: { ...base, ok: false, code: 'invalid-response', error: 'npm registry returned an unparseable packument' },\n }\n }\n const newest = selectNewest(current, data['dist-tags'] ?? {}, options.channel)\n if (newest === null) {\n return {\n kind: 'error',\n payload: { ...base, ok: false, code: 'no-dist-tags', error: 'npm registry returned no usable dist-tags' },\n }\n }\n const outdated = isUpdateAvailable(current, newest.version)\n const etag = response.headers.get('etag') ?? undefined\n return {\n kind: 'ok',\n ...(etag !== undefined ? { etag } : {}),\n payload: {\n ...base,\n ok: true,\n code: 'ok',\n latest: newest.version,\n outdated,\n channel: newest.channel,\n ...(outdated ? { updateCommand: updateCommandFor(newest.channel, probe.name) } : {}),\n },\n }\n}\n\n/** Short TTL for the cached registry verdict: repeated checks (settings row\n * re-opened, user re-clicks) don't re-hit npm; failures stay uncached so the\n * next click retries for real. The ETag (item W) lets a stale-but-valid\n * cache refresh itself with a conditional request instead of a full\n * packument pull — npm traffic drops to one 304 per 5 minutes.\n *\n * The cache is keyed by the requested channel (item I): switching the row's\n * channel must produce the new channel's verdict, not a stale cached one. */\nconst UPDATE_CACHE_TTL_MS = 5 * 60 * 1000\n\ninterface UpdateCacheEntry {\n at: number\n etag?: string\n payload: UpdateCheckPayload\n}\n\n/** Channel cache buckets. No query param and an explicit channel select\n * differently for a prerelease install (`selectNewest` only chases beta\n * without a preference), so the default bucket is DISTINCT from `latest` —\n * a pinned-latest check must never serve a beta verdict from the\n * unopinionated bucket (and vice versa). */\ntype CacheBucket = 'default' | UpdateChannel\n\nconst updateCaches = new Map<CacheBucket, UpdateCacheEntry>()\n\nfunction cacheKeyOf(channel: UpdateChannel | undefined): CacheBucket {\n return channel ?? 'default'\n}\n\n/** Answer the update-check route with the JSON contract from update-check.ts.\n * Recognizes BOTH desktop shells: the third-party launcher's optional\n * `desktopProfiles` service (target profile = `desktopProfiles.current`,\n * Desktop-flavoured hints) and the official Electron shell's environment\n * marker (`DSH_DESKTOP_NODE_EXECUTABLE`, see `isDesktopShellEnv`). Otherwise\n * it is the standard dsh web/CLI route and the web copy + profile scan apply.\n *\n * The route itself is stateless about retries: item V's 30s auto-retry is\n * scheduled by the settings row (an immediate route retry would just hang\n * a manual click); once a failure has passed the 5-minute window, the next\n * request re-runs the lookup for real. */\nasync function handleUpdateCheck(ctx: Context, req: HttpRequestLike, res: HttpResponseLike): Promise<void> {\n const channel = parseChannel(req.url)\n const key = cacheKeyOf(channel)\n const entry = updateCaches.get(key)\n if (entry !== undefined && Date.now() - entry.at < UPDATE_CACHE_TTL_MS) {\n sendJson(res, 200, entry.payload)\n return\n }\n const desktopProfiles = ctx.get('desktopProfiles') as DesktopProfilesLike | undefined\n const current = desktopProfiles?.current\n // Three desktop signals, any one of them is enough (2026-09-24):\n // - the third-party launcher's `desktopProfiles` service;\n // - its `DSH_DESKTOP_NODE_EXECUTABLE` env marker (the `dsh-desktop-next`\n // rewrite sets that one too);\n // - the OFFICIAL Electron shell, which sets NEITHER — upstream injects that\n // variable into package-install children only, so its profile process is\n // recognized through the Electron runtime instead. Missing this last one\n // made official desktop builds fall back to the plain web copy while the\n // probed profile name was already correct.\n const isDesktop = isDesktopShellEnv(process.env)\n || (current?.name !== undefined && current.name !== '')\n const result = await fetchLatestVersion({\n env: isDesktop ? 'desktop' : 'web',\n channel,\n ...(entry?.etag !== undefined ? { etag: entry.etag } : {}),\n ...(isDesktop ? { desktopProfile: current } : {}),\n })\n if (result.kind === 'ok') {\n updateCaches.set(key, { at: Date.now(), payload: result.payload, ...(result.etag !== undefined ? { etag: result.etag } : {}) })\n sendJson(res, 200, result.payload)\n return\n }\n if (result.kind === 'not-modified') {\n // The registry 304'd our conditional request: refresh the cache window\n // and serve the stored verdict (item W). A cache is guaranteed here —\n // we only send If-None-Match when one exists.\n if (entry !== undefined) updateCaches.set(key, { ...entry, at: Date.now() })\n const cached = entry?.payload\n if (cached !== undefined) {\n sendJson(res, 200, cached)\n return\n }\n }\n // Registry failure (or the unreachable not-modified-without-cache\n // combination): the error payload is never cached, so the next check\n // retries for real.\n sendJson(res, 502, result.kind === 'error'\n ? result.payload\n : { ok: false, code: 'invalid-response', error: 'registry state inconsistency' } as UpdateCheckPayload)\n}\n\n/** Register the update-check route on the owning context. `webServer` is a\n * hard inject dependency of the Host plugin, so it is guaranteed live when\n * this runs — mounting can never race ahead of the service. */\nexport function registerUpdateCheckRoute(ctx: Context): void {\n const webServer = ctx.get('webServer') as WebServerLike\n ctx.effect(() => webServer.register({\n kind: 'exact',\n path: UPDATE_ROUTE_PATH,\n handler: (req, res) => void handleUpdateCheck(ctx, req, res),\n }), 'dsh-catppuccin: update-check route')\n}","/**\n * Durable Catppuccin state — the shared contract between the two halves.\n *\n * Why this exists: localStorage is scoped per origin, so it is the wrong place\n * for the ONLY copy of a preference. It is per-browser (two browsers on one\n * machine disagree), per-loopback-port (a second Desktop instance that lands\n * on the next free port gets a different origin — the official shell pins\n * `19387` and `anywhere-labs/dsh-desktop` starts at `43120` and increments on\n * a bind collision, both observed 2026-09-24), and it disappears whenever the\n * user clears site data. The durable copy lives in the official settings seam\n * under the DSH home, which is the machine-wide truth every browser and every\n * profile entry reads.\n *\n * (Historical note, corrected 2026-09-24: the 0.5.0 rationale claimed DSH\n * Desktop launched the profile with `--port 0`, \"a fresh random port every\n * launch\". That was already untrue when 0.5.0 shipped — the community shell\n * pinned a stable port on 2026-08-21, and the official shell passes an\n * explicit `--port 19387`. The design is unchanged and still justified by the\n * reasons above, but do not repeat the port-per-launch claim.)\n *\n * Since 0.5.0 the DURABLE copy lives in the official settings seam, which\n * persists under the DSH home exactly like the legacy\n * `$DSH_HOME/catppuccin-state.json` did — origin-independent, so it survives\n * the Desktop's per-launch port churn. The legacy file is kept as a\n * read-only migration source (`src/legacy-state.ts`) and rolled into the\n * settings document once (`src/index.ts`). Browser localStorage remains the\n * in-browser cache and cross-tab sync bus, and the Client's fallback when the\n * settings transport is unavailable. This module is dependency-free so both\n * bundles inline it (like `update-check.ts`).\n *\n * The seam itself is version-dependent (issue #15): DSH <= 0.1.6-alpha.2\n * serves a Client `ctx.settingsScope` bound to a namespace the plugin\n * registers itself (`CATPPUCCIN_SETTINGS_NS`), while >= 0.1.7-alpha.1 serves\n * `ctx.configForms` projecting the plugin's volatile `Config` form, keyed by\n * the PROFILE ENTRY ID (`CATPPUCCIN_ENTRY_ID`). Both are addressed from this\n * one contract (see `src/client/state-sync.ts`).\n */\nexport const STATE_VERSION = 1\n\n/** Release channels the update check knows about (a persisted preference;\n * `selectNewest` in `update-check.ts` consumes it). */\nexport type UpdateChannel = 'latest' | 'beta'\n\n/** Shiki syntax-highlighting style variants (see `src/client/shiki-tokens.ts`). */\nexport type ShikiStyle = 'default' | 'italic-comments'\n\n/** Legacy settings namespace (DSH <= 0.1.6-alpha.2): the Host registers it\n * through `settings.installSection`, the Client binds it through\n * `ctx.settingsScope` (see `src/settings-catppuccin.ts`). */\nexport const CATPPUCCIN_SETTINGS_NS = 'catppuccin'\n\n/**\n * Profile entry id carrying the plugin (DSH >= 0.1.7-alpha.1): the settings\n * service projects our volatile `Config` into a form named by the ACTIVE\n * PROFILE ENTRY's id, so this constant is the new seam's namespace.\n *\n * It is a CONTRACT with `cordis.patch.yml`: the `insert` row whose `name` is\n * `@nonamelego/dsh-catppuccin` must keep `id: dsh-catppuccin`, because the\n * Client can only address the form by that id (asserted by\n * `tests/settings-seam.spec.ts`). A user who renames the entry loses the new\n * seam and degrades to localStorage-only — the legacy seam is unaffected,\n * since that namespace is registered by the plugin itself.\n */\nexport const CATPPUCCIN_ENTRY_ID = 'dsh-catppuccin'\n\n/** State file name under the DSH home (kept next to the Desktop-managed\n * `cordis.patch.yml` so it survives the Desktop's per-launch port churn). */\nexport const STATE_FILENAME = 'catppuccin-state.json'\n\n/**\n * Whether a raw settings view carries a user layer at all.\n *\n * The two seams answer this differently and the difference is load-bearing\n * (issue #15): the NEW form's `user` is the profile patch's `override`, which\n * `configEditor.configuration()` defaults to `{}` — so \"absent\" arrives as an\n * EMPTY OBJECT, and `!== undefined` would be true forever. The LEGACY\n * document omits the field entirely when the document holds no such section,\n * so the old `!== undefined` test stays correct there. Callers must use the\n * predicate their channel defines (see `src/client/state-sync.ts`).\n * @param user - a snapshot's raw user layer.\n * @returns whether it holds at least one entry.\n */\nexport function hasUserLayerSection(user: unknown): boolean {\n return typeof user === 'object' && user !== null && !Array.isArray(user) && Object.keys(user).length > 0\n}\n\n/** The four registered Catppuccin theme ids. MUST stay in sync with the\n * `themeId` of `CATPPUCCIN_FLAVORS` in `src/client/palettes.ts` — guarded by\n * `tests/state.spec.ts` — because the persisted flavour value IS the theme id. */\nexport const CATPPUCCIN_THEME_IDS = [\n 'catppuccin-latte',\n 'catppuccin-frappe',\n 'catppuccin-macchiato',\n 'catppuccin-mocha',\n] as const\n\n/** A persisted flavour choice: one registered theme id or `off` (fall back to\n * the official theme). */\nexport type FlavorValue = (typeof CATPPUCCIN_THEME_IDS)[number] | 'off'\n\n/** Durable glass-layer settings (no derived `dark` field — that resolves at\n * runtime from the active theme). */\nexport interface GlassState {\n /** Layer enable flag. */\n enabled: boolean\n /** Rendering mode. */\n mode: 'mica' | 'compat'\n /** Glass blur radius, px (0-40). */\n blur: number\n /** Glass fill frost, 0-100. */\n frost: number\n /** Backdrop brightness, 0-100. */\n brightness: number\n}\n\n/** The whole durable state the plugin persists. */\nexport interface CatppuccinState {\n /** Schema version; anything else is treated as unknown and defaulted. */\n version: typeof STATE_VERSION\n /** Selected flavour (theme id or `off`). */\n flavor: FlavorValue\n /** Glass-layer settings (enable flag + knobs). */\n glass: GlassState\n /** Automatically re-check for updates (boot + periodic, while on). */\n autoCheck: boolean\n /** Release channel the update check chases. */\n updateChannel: UpdateChannel\n /** Per-token `--dsw-*` overrides merged LAST into the active theme. */\n overrides: Record<string, string>\n /** Shiki syntax-highlighting style variant. */\n shikiStyle: ShikiStyle\n}\n\n/** The settings-document section: `CatppuccinState` minus the synthetic\n * `version` field — the official seam resolves defaults, composition and the\n * user layer itself, so no plugin-side version is stored. */\nexport interface CatppuccinSettingsSection {\n /** Selected flavour (theme id or `off`). */\n flavor: FlavorValue\n /** Glass-layer settings (enable flag + knobs). */\n glass: GlassState\n /** Automatically re-check for updates (boot + periodic, while on). */\n autoCheck: boolean\n /** Release channel the update check chases. */\n updateChannel: UpdateChannel\n /** Per-token `--dsw-*` overrides merged LAST into the active theme. */\n overrides: Record<string, string>\n /** Shiki syntax-highlighting style variant. */\n shikiStyle: ShikiStyle\n}\n\n/** Shipped glass defaults — the single source of truth; the client glass layer\n * (`src/client/glass/glass-layer.ts`) derives its knob defaults from here. */\nexport const DEFAULT_GLASS: GlassState = {\n enabled: false,\n mode: 'mica',\n blur: 2,\n frost: 20,\n brightness: 50,\n}\n\n/** Shipped update-check preference: automatic checks on. */\nexport const DEFAULT_AUTO_CHECK = true\n/** Shipped update-check channel: chase the stable `latest` tag. */\nexport const DEFAULT_UPDATE_CHANNEL: UpdateChannel = 'latest'\n/** Shipped syntax-highlighting style. */\nexport const DEFAULT_SHIKI_STYLE: ShikiStyle = 'default'\n\n/** A fully default state: no Catppuccin flavour and no glass. */\nexport function defaultState(): CatppuccinState {\n return {\n version: STATE_VERSION,\n flavor: 'off',\n glass: { ...DEFAULT_GLASS },\n autoCheck: DEFAULT_AUTO_CHECK,\n updateChannel: DEFAULT_UPDATE_CHANNEL,\n overrides: {},\n shikiStyle: DEFAULT_SHIKI_STYLE,\n }\n}\n\n/**\n * Keep only well-formed custom token overrides: string values under a `--`\n * key (the token shape `--dsw-*` names). Everything else is dropped so a\n * hand-edited document can never smuggle junk into the theme registration.\n */\nexport function sanitizeOverrides(input: unknown): Record<string, string> {\n const out: Record<string, string> = {}\n if (typeof input !== 'object' || input === null) return out\n for (const [key, value] of Object.entries(input)) {\n if (typeof value === 'string' && key.startsWith('--')) out[key] = value\n }\n return out\n}\n\n/**\n * Whether a raw overrides map carries entries {@link sanitizeOverrides} would\n * drop — a key that is not a `--` token, or a non-string value.\n *\n * The settings-document schema types `overrides` as a dict of strings only, so\n * a hand-edited document can hold such entries. The client reads them away but\n * never wrote the cleaned map back, so every settings publish re-ran the\n * \"document wins\" adoption for a difference nothing could clear (audit F7).\n * The caller uses this to push the sanitized shape back once, so the document\n * converges instead of drifting forever.\n */\nexport function hasUnpersistableOverrides(raw: unknown): boolean {\n if (typeof raw !== 'object' || raw === null) return false\n return Object.entries(raw).some(([key, value]) => typeof value !== 'string' || !key.startsWith('--'))\n}\n\n/** Clamp a finite number into [min, max]; non-finite values fall back. */\nfunction clampFinite(value: unknown, min: number, max: number, fallback: number): number {\n return typeof value === 'number' && Number.isFinite(value)\n ? Math.min(max, Math.max(min, value))\n : fallback\n}\n\n/**\n * Normalize arbitrary input (JSON from disk or the wire) into a valid state:\n * unknown/missing fields fall back to defaults, out-of-range numbers clamp,\n * and unknown flavours/invalid enums fall back too. The Host applies this on\n * both read and write so a corrupt file or a hand-crafted payload can never\n * poison the plugin's settings.\n */\nexport function sanitizeState(input: unknown): CatppuccinState {\n const state = defaultState()\n if (typeof input !== 'object' || input === null) return state\n const raw = input as Record<string, unknown>\n\n const flavor = raw.flavor\n if (typeof flavor === 'string' && (CATPPUCCIN_THEME_IDS as readonly string[]).includes(flavor)) {\n state.flavor = flavor as FlavorValue\n } else if (flavor === 'off') {\n state.flavor = 'off'\n }\n\n const glass = typeof raw.glass === 'object' && raw.glass !== null\n ? raw.glass as Record<string, unknown>\n : {}\n state.glass.enabled = glass.enabled === true\n state.glass.mode = glass.mode === 'compat' ? 'compat' : 'mica'\n state.glass.blur = clampFinite(glass.blur, 0, 40, DEFAULT_GLASS.blur)\n state.glass.frost = clampFinite(glass.frost, 0, 100, DEFAULT_GLASS.frost)\n state.glass.brightness = clampFinite(glass.brightness, 0, 100, DEFAULT_GLASS.brightness)\n\n // Preference fields: absent values keep their shipped defaults, so a v1\n // document (predating them) rolls forward without a migration step.\n state.autoCheck = raw.autoCheck !== false\n state.updateChannel = raw.updateChannel === 'beta' ? 'beta' : 'latest'\n state.overrides = sanitizeOverrides(raw.overrides)\n state.shikiStyle = raw.shikiStyle === 'italic-comments' ? 'italic-comments' : 'default'\n\n return state\n}\n\n/**\n * Version-aware migration of an arbitrary persisted document. Version 1\n * documents roll forward through `sanitizeState` (every field added since\n * v1 has a sanitize-time default, so no structural step exists yet); future\n * versions chain their steps here, oldest first. Documents from a NEWER\n * plugin keep every field `sanitizeState` understands instead of being\n * nuked — a downgrade never destroys user state. The version-upgrade\n * convention lives in `docs/state-migrations.md` (item Y).\n */\nexport function migrate(raw: unknown): CatppuccinState {\n if (typeof raw !== 'object' || raw === null) return defaultState()\n const record = raw as Record<string, unknown>\n const version = typeof record.version === 'number' ? record.version : 1\n // Future schema steps chain here, oldest first:\n // let current: unknown = record\n // if (version < 2) current = migrateV1toV2(current)\n // if (version < 3) current = migrateV2toV3(current)\n // (the state contract of each step is spelled out in docs/state-migrations.md)\n return sanitizeState(record)\n}\n\n/** Whether a normalized state carries nothing beyond the shipped defaults —\n * i.e. the user never actually chose anything worth persisting. Used to tell\n * \"no durable state yet\" apart from \"an explicit default choice\". */\nexport function isDefaultState(state: CatppuccinState): boolean {\n const expected = defaultState()\n return state.flavor === expected.flavor\n && state.glass.enabled === expected.glass.enabled\n && state.glass.mode === expected.glass.mode\n && state.glass.blur === expected.glass.blur\n && state.glass.frost === expected.glass.frost\n && state.glass.brightness === expected.glass.brightness\n && state.autoCheck === expected.autoCheck\n && state.updateChannel === expected.updateChannel\n && Object.keys(state.overrides).length === 0\n && state.shikiStyle === expected.shikiStyle\n}\n\n/** Default settings-document section: no flavour and no glass (mirrors\n * `defaultState()` minus the synthetic `version`). */\nexport function defaultSettingsSection(): CatppuccinSettingsSection {\n return {\n flavor: 'off',\n glass: { ...DEFAULT_GLASS },\n autoCheck: DEFAULT_AUTO_CHECK,\n updateChannel: DEFAULT_UPDATE_CHANNEL,\n overrides: {},\n shikiStyle: DEFAULT_SHIKI_STYLE,\n }\n}\n\n/** Drop the synthetic `version` field: full state → settings-document section. */\nexport function settingsSectionFromState(state: CatppuccinState): CatppuccinSettingsSection {\n return {\n flavor: state.flavor,\n glass: { ...state.glass },\n autoCheck: state.autoCheck,\n updateChannel: state.updateChannel,\n overrides: { ...state.overrides },\n shikiStyle: state.shikiStyle,\n }\n}\n\n/** Lift a settings-document section back into the full state-shaped contract\n * (re-sanitized so a hand-edited document can never poison the plugin). */\nexport function stateFromSettingsSection(section: CatppuccinSettingsSection): CatppuccinState {\n return sanitizeState(section)\n}\n\n/** Whether two settings sections are field-for-field equal (used to skip\n * redundant mirror writes and to recognize the Host echoing our own write). */\nexport function settingsSectionsEqual(a: CatppuccinSettingsSection, b: CatppuccinSettingsSection): boolean {\n return a.flavor === b.flavor\n && a.glass.enabled === b.glass.enabled\n && a.glass.mode === b.glass.mode\n && a.glass.blur === b.glass.blur\n && a.glass.frost === b.glass.frost\n && a.glass.brightness === b.glass.brightness\n && a.autoCheck === b.autoCheck\n && a.updateChannel === b.updateChannel\n && a.shikiStyle === b.shikiStyle\n && Object.keys(a.overrides).every((key) => b.overrides[key] === a.overrides[key])\n && Object.keys(b.overrides).every((key) => a.overrides[key] === b.overrides[key])\n}\n","/**\n * Legacy durable-state read — the one-shot migration source for 0.5.0.\n *\n * Up to 0.4.x the plugin persisted its state in a small JSON file under the\n * DSH home (`catppuccin-state.json`), served to the Client through the\n * `/catppuccin/state` route. 0.5.0 migrates persistence to the official\n * settings seam (`ctx.settings` Host-side, and whichever Client transport the\n * host serves — see `src/client/state-sync.ts`), which is durable across\n * browsers and origins just like the file was (it lives in the DSH home). The legacy file is READ-ONLY here: 0.5.0 migrates its content into\n * the settings document once and keeps the file on disk as a rollback copy\n * (an affected user can restore their old state by hand from it).\n *\n * Host-only module (node:fs / node:path resolve at runtime in the DSH\n * process); the Client never sees it.\n */\nimport { readFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { defaultDshHome } from './profile-detect.ts'\nimport { STATE_FILENAME, sanitizeState, type CatppuccinState } from './state.ts'\n\n/** Absolute path of the legacy state file under a DSH home. */\nexport function legacyStateFilePath(home = defaultDshHome()): string {\n return join(home, STATE_FILENAME)\n}\n\n/** Read the legacy durable state; absent or unparseable input means none yet. */\nexport function readLegacyState(home = defaultDshHome()): CatppuccinState | null {\n try {\n return sanitizeState(JSON.parse(readFileSync(legacyStateFilePath(home), 'utf8')))\n } catch {\n // Missing file (fresh install or a user who never wrote state) or a\n // corrupt/partial write — nothing to migrate.\n return null\n }\n}","/**\n * One-shot migration of the pre-0.5.0 durable file into whichever settings\n * seam the running host offers (issue #15 split it out of `src/index.ts` so\n * the composition stays thin and the migration is unit-testable).\n *\n * The migration writes `$DSH_HOME/catppuccin-state.json` into the durable\n * store exactly once, and only while that store holds NO user layer yet — a\n * user who already configured through the official settings surface (or a\n * previous migration) is never overwritten. The legacy file is left on disk\n * untouched as a rollback copy; a corrupt or absent file means nothing to\n * migrate. Every failure is non-fatal.\n *\n * Two host shapes, two addressing rules:\n *\n * - LEGACY (DSH <= 0.1.6-alpha.2): the namespace is one the plugin just\n * registered itself, so `describe()` finds it immediately and the caller\n * can run {@link migrateLegacyStateOnce} synchronously. \"No user layer\"\n * arrives as an ABSENT `user` field.\n * - NEW (DSH >= 0.1.7-alpha.1): the namespace IS the profile entry, and the\n * settings service only lists an entry whose own fiber is ACTIVE. The\n * `settings` inject callback fires SYNCHRONOUSLY when the service is\n * already live — at that instant the entry is still activating, so an\n * immediate `describe()` cannot see it and the migration would be silently\n * skipped. {@link scheduleLegacyMigration} defers it and also retries on\n * the entry's own `settings/document-updated`. Here \"no user layer\"\n * arrives as an EMPTY OBJECT (`configEditor` defaults `override` to `{}`),\n * which is why the predicate is injected rather than hard-coded.\n */\nimport type { Context } from '@deepseek-ai/cordis'\nimport { readLegacyState } from './legacy-state.ts'\nimport { isDefaultState, settingsSectionFromState } from './state.ts'\n\n/** The settings-service surface this module needs — satisfied by both seams\n * (`SettingsForms` on 0.1.7+, the document provider before it). */\nexport interface SettingsLike {\n /** Read the active namespaces (keyed by namespace / profile entry id). */\n describe(options?: { redactSecrets?: boolean }): readonly SettingsLikeDescriptor[]\n /** Merge a patch into one namespace's user layer. */\n update(ns: string, patch: object, expectedRevision?: number): Promise<void>\n}\n\n/** The descriptor fields this module reads. */\nexport interface SettingsLikeDescriptor {\n ns: string\n revision?: number\n user?: unknown\n}\n\n/** Outcome of one migration attempt. `pending` means \"not yet addressable —\n * try again\", which is the only one the caller should retry. */\nexport type MigrationOutcome =\n /** The legacy state was written into the durable store. */\n | 'migrated'\n /** The legacy file is absent or already equals the shipped defaults. */\n | 'absent'\n /** The store already holds a user layer — never overwritten. */\n | 'skipped'\n /** The namespace is not addressable yet (entry still activating / service\n * has not published its first describe). */\n | 'pending'\n\n/** Options for one migration attempt. */\nexport interface MigrationOptions {\n /**\n * Send the descriptor's revision as the write fence. The NEW seam sets it\n * (its `update` validates the revision the caller read); the LEGACY seam\n * omits it, so that line's write stays exactly what 0.5.x issued and cannot\n * turn into a spurious conflict against a namespace only we write.\n */\n fence?: boolean\n}\n\n/**\n * Run the migration once.\n * @param settings - the live settings service.\n * @param ns - the namespace to write: `CATPPUCCIN_SETTINGS_NS` on the legacy\n * seam, `CATPPUCCIN_ENTRY_ID` on the new one.\n * @param hasUserLayer - the channel's own \"the store already holds a user\n * layer\" predicate (see the module header).\n * @param options - fence policy (see {@link MigrationOptions}).\n * @returns the outcome; only `pending` is worth retrying.\n */\nexport async function migrateLegacyStateOnce(\n settings: SettingsLike,\n ns: string,\n hasUserLayer: (user: unknown) => boolean,\n options: MigrationOptions = {},\n): Promise<MigrationOutcome> {\n const legacy = readLegacyState()\n if (legacy === null || isDefaultState(legacy)) return 'absent'\n const descriptor = settings.describe({ redactSecrets: true })\n .find((candidate) => candidate.ns === ns)\n if (descriptor === undefined) return 'pending'\n if (hasUserLayer(descriptor.user)) return 'skipped'\n await settings.update(\n ns,\n settingsSectionFromState(legacy),\n options.fence === true ? descriptor.revision : undefined,\n )\n return 'migrated'\n}\n\n/**\n * Run the migration on the NEW seam without racing the plugin's own\n * activation: one deferred attempt, one retry when the entry's form finally\n * lands in the describe mirror, and one last attempt a few seconds later.\n * Bounded on purpose — a permanently missing entry (a user who renamed it)\n * must not leave a timer running for the process lifetime. The whole schedule\n * is fiber-scoped, so unloading the plugin cancels it.\n * @param ctx - the Host plugin context.\n * @param settings - the live settings service.\n * @param ns - the profile entry id carrying the form.\n * @param hasUserLayer - the new seam's user-layer predicate.\n */\nexport function scheduleLegacyMigration(\n ctx: Context,\n settings: SettingsLike,\n ns: string,\n hasUserLayer: (user: unknown) => boolean,\n): void {\n let settled = false\n // One attempt in flight at a time. The deferred timer and the entry's own\n // `settings/document-updated` routinely land within the same tick, and two\n // concurrent `describe` → `update` pairs race on the namespace revision: the\n // loser is refused and reported as a FAILURE even though the migration\n // succeeded (observed on a real 0.1.7 host, 2026-09-24). `claimed` is set\n // synchronously before the first `await`, so the second caller cannot pass.\n let claimed = false\n const attempt = (): void => {\n if (settled || claimed) return\n claimed = true\n void migrateLegacyStateOnce(settings, ns, hasUserLayer, { fence: true })\n .then((outcome) => {\n if (outcome === 'pending') {\n // Not addressable yet — release the slot so a later attempt retries.\n claimed = false\n return\n }\n settled = true\n if (outcome === 'migrated') {\n console.info(`[dsh-catppuccin] migrated the legacy state file into \"${ns}\"`)\n }\n })\n .catch((error: unknown) => {\n settled = true\n console.warn(`[dsh-catppuccin] legacy state migration failed: ${String(error)}`)\n })\n }\n ctx.effect(() => {\n const deferred = setTimeout(attempt, 0)\n const late = setTimeout(attempt, 3000)\n // The entry becomes addressable exactly when the settings service emits\n // its own document-changed notification for it.\n const off = ctx.on('settings/document-updated', (documentNs: string) => {\n if (String(documentNs) === ns) attempt()\n })\n return () => {\n clearTimeout(deferred)\n clearTimeout(late)\n off()\n }\n }, 'catppuccin: legacy state migration')\n}\n","/**\n * The Catppuccin settings contract — the official-settings shape of the\n * plugin's durable state (0.5.0+, replacing the legacy\n * `catppuccin-state.json`), in BOTH forms the DSH versions in the wild need.\n *\n * Two seams, one contract (issue #15):\n *\n * - **DSH <= 0.1.6-alpha.2** (`latest` on npm) serves an abstract settings\n * document. The Host half registers a namespace on it\n * (`settings.installSection(CATPPUCCIN_SETTINGS_NS, schema, base, hooks)`)\n * and the Client binds that namespace through `ctx.settingsScope`. This\n * module owns the registered schema ({@link CatppuccinSettingsSchema}) and\n * the composition `base` ({@link CATPPUCCIN_SETTINGS_BASE}).\n * - **DSH >= 0.1.7-alpha.1** (`next`) dropped both. A plugin no longer\n * registers a namespace: it declares a `Config` schema whose fields carry\n * `.volatile()`, and the settings service projects exactly those fields\n * into a form named by the ACTIVE PROFILE ENTRY id\n * (`CATPPUCCIN_ENTRY_ID`). That projected schema is {@link Config}, the\n * module's only export the Loader consumes.\n *\n * Both forms are built from ONE field table ({@link catppuccinSchemaFields}),\n * so a field can never drift between them. They must NOT share node\n * instances: `.volatile()` mutates the node's meta in place and returns it,\n * so reusing a node would silently mark the legacy schema volatile as well.\n *\n * {@link maybeVolatile} is the compatibility hinge: `.volatile()` exists only\n * from schemastery **3.18.3** (DSH 0.1.5/0.1.6 hosts pin `3.18.2`), and this\n * module is loaded with the HOST's schemastery (it stays external in the lib\n * build). An unconditional `.volatile()` would therefore throw at module\n * evaluation time on every old host — turning \"plugin pending\" into \"plugin\n * failed to load\". On such a host the guarded call is a no-op, which is\n * exactly the pre-0.1.7 status quo (no entry-level form exists there).\n *\n * This module is Host-side because it imports `@deepseek-ai/schemastery`\n * (resolved from the DSH profile tree at runtime). The Client never imports\n * it; it consumes the form by name.\n */\nimport Schema from '@deepseek-ai/schemastery'\nimport {\n CATPPUCCIN_THEME_IDS,\n DEFAULT_AUTO_CHECK,\n DEFAULT_GLASS,\n DEFAULT_SHIKI_STYLE,\n DEFAULT_UPDATE_CHANNEL,\n defaultSettingsSection,\n type CatppuccinSettingsSection,\n} from './state.ts'\n\n/** The subset of a schemastery node this module relies on. */\ninterface VolatileCapable {\n volatile?: () => unknown\n}\n\n/**\n * Mark a schema node volatile when the running schemastery supports it.\n *\n * Guarded on purpose (see the module header): `.volatile()` landed in\n * schemastery 3.18.3, while DSH <= 0.1.6-alpha.2 hosts pin 3.18.2 and this\n * module resolves schemastery from THEIR tree. Calling it unconditionally\n * throws at import time on those hosts.\n * @param node - freshly built schema node (never reused across schemas).\n * @returns the same node, marked volatile when the capability exists.\n */\nexport function maybeVolatile<T extends object>(node: T): T {\n const capable = node as VolatileCapable\n return typeof capable.volatile === 'function' ? capable.volatile() as T : node\n}\n\n/**\n * Build FRESH schema nodes for every field of the durable contract. Every\n * field maps 1:1 onto `CatppuccinSettingsSection` in `src/state.ts`, and every\n * default is the shipped default — the Client treats \"resolves to the shipped\n * defaults\" as \"no user choice yet\", so a wrong default would write garbage\n * into a user's profile on first boot.\n *\n * Callers MUST NOT share the returned nodes between schemas (see the header).\n * @returns a new field table, one fresh node per field.\n */\nexport function catppuccinSchemaFields(): Record<string, Schema> {\n return {\n flavor: Schema.union([...CATPPUCCIN_THEME_IDS, 'off']).default('off'),\n glass: Schema.object({\n enabled: Schema.boolean().default(DEFAULT_GLASS.enabled),\n mode: Schema.union(['mica', 'compat']).default(DEFAULT_GLASS.mode),\n blur: Schema.number().min(0).max(40).default(DEFAULT_GLASS.blur),\n frost: Schema.number().min(0).max(100).default(DEFAULT_GLASS.frost),\n brightness: Schema.number().min(0).max(100).default(DEFAULT_GLASS.brightness),\n }),\n autoCheck: Schema.boolean().default(DEFAULT_AUTO_CHECK),\n updateChannel: Schema.union(['latest', 'beta']).default(DEFAULT_UPDATE_CHANNEL),\n overrides: Schema.dict(Schema.string()).default({}),\n shikiStyle: Schema.union(['default', 'italic-comments']).default(DEFAULT_SHIKI_STYLE),\n }\n}\n\n/** The same table with every leaf marked volatile where the capability exists.\n * Marking the leaves (rather than the `glass` subtree) keeps\n * `isVolatilePath` true for each nested path without putting volatile inside\n * volatile — schemastery rejects that combination. */\nfunction volatileFields(): Record<string, Schema> {\n const fields = catppuccinSchemaFields()\n const glassFields = (fields.glass as Schema & { dict?: Record<string, Schema> }).dict ?? {}\n return {\n flavor: maybeVolatile(fields.flavor),\n glass: Schema.object(Object.fromEntries(\n Object.entries(glassFields).map(([key, node]) => [key, maybeVolatile(node)]),\n )),\n autoCheck: maybeVolatile(fields.autoCheck),\n updateChannel: maybeVolatile(fields.updateChannel),\n overrides: maybeVolatile(fields.overrides),\n shikiStyle: maybeVolatile(fields.shikiStyle),\n }\n}\n\n/**\n * The namespace schema registered by the LEGACY Host seam\n * (`settings.installSection`). Deliberately plain: on a host that has\n * `installSection` there is no `.volatile()` capability, and the field set is\n * resolved by the settings document rather than by a form projection.\n */\nexport const CatppuccinSettingsSchema = Schema.object(catppuccinSchemaFields())\n\n/** The composition `base` for the legacy namespace: the shipped defaults. */\nexport const CATPPUCCIN_SETTINGS_BASE: CatppuccinSettingsSection = defaultSettingsSection()\n\n/**\n * The plugin `Config` consumed by the NEW Host seam (DSH >= 0.1.7-alpha.1).\n * Its volatile fields are what the settings service projects into the form the\n * Client addresses by `CATPPUCCIN_ENTRY_ID`; the Host half never reads the\n * values itself. On a legacy host every field stays plain, so the entry\n * exposes no form at all — the old seam keeps doing the persisting.\n */\nexport const Config = Schema.object(volatileFields())\n\n/**\n * The value shape the Loader hands `apply` for {@link Config}. Every leaf is a\n * `Volatile` reference on a 0.1.7 host (read it with `.get()`); the Host half\n * deliberately consumes none of them, so this interface exists to document the\n * contract and to keep the schema honest.\n */\nexport interface CatppuccinConfig {\n flavor: { get(): CatppuccinSettingsSection['flavor'] }\n glass: {\n enabled: { get(): boolean }\n mode: { get(): CatppuccinSettingsSection['glass']['mode'] }\n blur: { get(): number }\n frost: { get(): number }\n brightness: { get(): number }\n }\n autoCheck: { get(): boolean }\n updateChannel: { get(): CatppuccinSettingsSection['updateChannel'] }\n overrides: { get(): Record<string, string> }\n shikiStyle: { get(): CatppuccinSettingsSection['shikiStyle'] }\n}\n","/**\n * Host half of the Catppuccin theme plugin for the DeepSeek Harness web GUI.\n *\n * Pure UI plugin: the four flavour themes and the glass skin ship in the\n * browser half (exports[\"./client\"], discovered through the package.json\n * dsh.client declaration). Since 0.5.0 the flavour/glass preferences persist\n * durably through the OFFICIAL settings seam — but that seam changed shape in\n * DSH 0.1.7-alpha.1 (issue #15), so this half wires BOTH:\n *\n * - **<= 0.1.6-alpha.2** (`latest` on npm): a settings SERVICE the plugin\n * registers a namespace on (`ctx.settings.installSection`) and the Client\n * binds through `ctx.settingsScope`.\n * - **>= 0.1.7-alpha.1** (`next`): no registration at all. The plugin\n * declares a `Config` schema with `.volatile()` fields (exported below),\n * and the settings service projects them into a form named by the active\n * profile entry id; the Client reads it through `ctx.configForms`.\n *\n * Which one is live is decided by FEATURE DETECTION, never by version parsing:\n * `installSection` is present exactly on the legacy service. Both wirings live\n * inside an optional `ctx.inject(['settings'], …)` child, so a deployment\n * without a settings provider keeps the plugin working from browser\n * localStorage alone (losing only cross-restart durability). No profile is\n * ever blocked on the settings service.\n *\n * The legacy seam itself predates DSH 0.1.1-rc.2, which is why 0.4.x shipped a\n * hand-rolled `$DSH_HOME/catppuccin-state.json` + `/catppuccin/state` route\n * instead: the Host settings wire then only served an explicit allowlist of\n * namespaces (see dsh-host-apiproxy's WEB_SETTINGS_NAMESPACES), so a\n * plugin-owned settings namespace answered `settings-not-exposed` even when\n * registered. 0.1.1-rc.2 dropped the allowlist (\"注册即暴露\"), and the legacy\n * file is now only a one-shot migration source (`src/legacy-state.ts`,\n * consumed by `src/migrate-legacy.ts`; the file stays as rollback).\n *\n * The one host-side behavior besides settings is the\n * `/catppuccin/check-update` exact webServer route, owned by\n * `src/update-check/host.ts` (item A: the update-check logic used to live\n * inline here; it moved so `index.ts` stays a thin composition of the host\n * responsibilities and the route module can be tested on its own).\n * `webServer` is a hard inject dependency, so Cordis starts this plugin only\n * after the service is live — mounting can never race ahead of it and\n * silently skip the routes. (An earlier version read `ctx.get('webServer')`\n * once and bailed silently when it was not yet visible, which left a\n * mounted-but-routeless plugin and made \"check for updates\" fail forever.)\n * A headless profile has no webServer and no settings UI, so the plugin\n * simply stays waiting there. The responses carry only the plugin's own\n * settings and public package metadata, so no workspace gate is needed.\n */\nimport type { Context } from '@deepseek-ai/cordis'\n// Type-only: pulls the settings service's Context merge (ctx.settings).\nimport type {} from '@deepseek-ai/dsh-settings'\nimport { registerUpdateCheckRoute } from './update-check/host.ts'\nimport { migrateLegacyStateOnce, scheduleLegacyMigration, type SettingsLike } from './migrate-legacy.ts'\nimport { CATPPUCCIN_SETTINGS_BASE, CatppuccinSettingsSchema } from './settings-catppuccin.ts'\nimport { CATPPUCCIN_ENTRY_ID, CATPPUCCIN_SETTINGS_NS, hasUserLayerSection } from './state.ts'\n\n/** Stable cordis plugin name (matches cordis.patch.yml insert id). */\nexport const name = 'dsh-catppuccin'\n\n/**\n * Hard dependencies: the update check is a webServer route, so this Host\n * half is not activated until the webServer service is live. Declaring\n * inject (not a one-shot `ctx.get` probe) makes route registration\n * deterministic: Cordis reactivates the plugin once the service appears, no\n * matter the mount order.\n */\nexport const inject = ['webServer']\n\n/**\n * The plugin's volatile configuration form (DSH >= 0.1.7-alpha.1) — the\n * namespace the Client addresses by `CATPPUCCIN_ENTRY_ID`.\n *\n * Re-exported here because the Loader reads `Config` off the plugin module;\n * on a legacy host the guarded `.volatile()` calls inside it are no-ops, so\n * the entry exposes no form there and the `installSection` wiring below keeps\n * doing the persisting.\n */\nexport { Config } from './settings-catppuccin.ts'\n\n/**\n * The LEGACY settings service (DSH <= 0.1.6-alpha.2): it registers plugin\n * namespaces. Declared as a local structural type on purpose — 0.1.7 removed\n * the method from the published types, and this shape is all this branch\n * touches, so the cross-version code stays honest instead of reaching for\n * `any`.\n */\ninterface LegacySettings extends SettingsLike {\n installSection?: (\n owner: Context,\n ns: string,\n schema: unknown,\n base: unknown,\n hooks: { setSource: () => void, onChange: () => void },\n ) => void\n}\n\n/** The NEW settings service's extra surface (DSH >= 0.1.7-alpha.1). */\ninterface ModernSettings extends SettingsLike {\n /** Own this plugin instance's automatic-page policy (`auto` defaults true).\n * Absent on the legacy seam, hence the feature detection. */\n configure?: (presentation: { auto?: boolean }, owner?: unknown) => unknown\n}\n\n/** The legacy document describes \"no user layer\" as an ABSENT field (it maps\n * over the namespaces it has a section for), so presence is the whole test —\n * the same predicate 0.5.x used. */\nfunction legacyUserLayer(user: unknown): boolean {\n return user !== undefined\n}\n\n/** Host plugin body: register the update-check route and wire whichever\n * settings seam the running host offers (webServer is inject; settings is\n * optional wiring). */\nexport function apply(ctx: Context): void {\n registerUpdateCheckRoute(ctx)\n\n ctx.inject(['settings'], (sctx) => {\n const settings = sctx.settings as unknown as LegacySettings & ModernSettings\n if (typeof settings.installSection === 'function') {\n // Legacy seam. Registering and migrating inline reproduces 0.5.x\n // behaviour exactly: the namespace is ours, so it is addressable the\n // moment `installSection` returns, and the write stays unfenced.\n settings.installSection(\n sctx,\n CATPPUCCIN_SETTINGS_NS,\n CatppuccinSettingsSchema,\n CATPPUCCIN_SETTINGS_BASE,\n {\n // Host half never consumes the resolved value itself — the Client\n // renders and edits it through its own bound scope — so the source\n // sink and change hook are intentionally empty (the wiring still\n // needs them to keep the registration fiber-scoped).\n setSource() {},\n onChange() {},\n },\n )\n void migrateLegacyStateOnce(settings, CATPPUCCIN_SETTINGS_NS, legacyUserLayer)\n .catch((error: unknown) => {\n console.warn(`[dsh-catppuccin] legacy state migration failed: ${String(error)}`)\n })\n return\n }\n\n // New seam. Nothing to register: the exported `Config` IS the namespace,\n // and its volatile fields are what the settings service projects into the\n // form the Client reads. Suppress the schema-generated page — this plugin\n // ships its own settings rows.\n settings.configure?.({ auto: false }, ctx.fiber)\n // The entry is not addressable until its own fiber is ACTIVE, so the\n // migration is deferred and retried rather than run inline (a synchronous\n // `describe()` here cannot see us yet — see `src/migrate-legacy.ts`).\n scheduleLegacyMigration(ctx, settings, CATPPUCCIN_ENTRY_ID, hasUserLayerSection)\n })\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nonamelego/dsh-catppuccin",
3
3
  "description": "Catppuccin themes + a toggleable glassmorphism skin for the DeepSeek Harness web GUI — Latte, Frappé, Macchiato and Mocha registered into the official theme system (Appearance settings), fully remapping the --dsw-* token ladder, with adjustable frosted glass for the top bar, sidebar, composer, stats line and trajectory view.",
4
- "version": "0.5.6",
4
+ "version": "0.5.7-beta.0",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.33.3",
7
7
  "engines": {