From ba833fc769110cc274c62de39f2f3f949afa6f18 Mon Sep 17 00:00:00 2001 From: idoall Date: Fri, 25 Sep 2026 23:52:54 +0800 Subject: [PATCH] fix: resolve the schemastery peer explicitly so a stale copy cannot kill the host half MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The host half imported `@deepseek-ai/schemastery` by bare specifier, so Node resolved the peer from the plugin's own directory. An unmanaged dev `node_modules` left inside the install directory by a local-directory install (pnpm never removes it) pinned `3.18.2`, which has no `volatile()`, so `lib/index.js` threw `TypeError: …volatile is not a function` while it was being imported — the whole host half disappeared before any plugin code ran. The symptom is machine-specific and version-independent: another machine whose leftover copy happened to be `3.18.4` kept working on the same DSH and plugin versions. `src/host/schemastery.ts` resolves the peer itself, platform copies first (the running DSH installation, then the profile peer farm at `$DSH_HOME/profiles/node_modules`, then the profile tree) and only then Node's walk, and takes the first candidate that actually exposes `volatile()`. A resolution that cannot find it still loads: `Config` is built with ordinary fields, the Host logs the resolution, and the panel shows a new `stale-schemastery` notice (severity `notice`, so the chip never repaints) naming the version, the resolved path and the exact directory to remove. Only a total miss still throws, naming every candidate it tried. Locks the regression three ways: resolver specs driven by injected candidates, a source guard that fails if any file under `src/` takes a value import of the peer again, and `tests/guards/shadowed-peer.mjs`, which loads the built entry with a fake `3.18.2` planted beside it. The guard is wired into `pnpm run verify`, CI and both release workflows, after the build. Tests: 116 passing (up from 98). --- .github/workflows/ci.yml | 1 + .github/workflows/release-candidate.yml | 1 + .github/workflows/release.yml | 1 + CHANGELOG.md | 10 + README.md | 7 + README.zh.md | 7 + lib/client.js | 29 ++ lib/client.js.map | 2 +- lib/index.d.ts | 78 ++++- lib/index.js | 371 ++++++++++++++++++++++-- package.json | 3 +- src/client/contract.ts | 9 + src/client/i18n.ts | 10 + src/host/schemastery.ts | 329 +++++++++++++++++++++ src/host/settings.ts | 56 +++- src/host/update-status.ts | 41 ++- src/index.ts | 17 +- src/shared/types.ts | 7 + tests/client/contract.spec.ts | 56 ++++ tests/client/i18n.spec.ts | 20 ++ tests/guards/shadowed-peer.mjs | 191 ++++++++++++ tests/host/schemastery.spec.ts | 281 ++++++++++++++++++ tests/host/update-status.spec.ts | 31 ++ 23 files changed, 1508 insertions(+), 50 deletions(-) create mode 100644 src/host/schemastery.ts create mode 100644 tests/client/contract.spec.ts create mode 100644 tests/guards/shadowed-peer.mjs create mode 100644 tests/host/schemastery.spec.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89088be..34b4c51 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,4 +26,5 @@ jobs: - run: pnpm install --frozen-lockfile - run: pnpm run test - run: pnpm run build + - run: pnpm run guard:shadowed-peer - run: pnpm pack --dry-run diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 973be65..f0eb480 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -20,6 +20,7 @@ jobs: - run: pnpm install --frozen-lockfile - run: pnpm run test - run: pnpm run build + - run: pnpm run guard:shadowed-peer - name: Pack and checksum run: | package="$(pnpm pack --pack-destination release-assets | tail -n 1)" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6185f2d..bbc2463 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,6 +56,7 @@ jobs: - run: pnpm install --frozen-lockfile - run: pnpm run test - run: pnpm run build + - run: pnpm run guard:shadowed-peer - name: Pack immutable release artifact run: | mkdir -p release-assets diff --git a/CHANGELOG.md b/CHANGELOG.md index 2159595..570ac89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project are documented here. +## Unreleased + +No version bump yet: this is the fix for a load failure that made the plugin disappear on some machines, and it changes no user-facing contract beyond one new advisory warning. + +- **`@deepseek-ai/schemastery` is resolved explicitly instead of imported by bare specifier.** The host half used to `import z from '@deepseek-ai/schemastery'`, which Node resolves *from the importing file*, so any copy left inside the plugin's install directory won over the copy DSH ships. That is not theoretical: an unmanaged dev `node_modules` (copied in by a local-directory install, which pnpm never removes) pinned `3.18.2`, which has no `volatile()`, and `lib/index.js` threw `TypeError: …volatile is not a function` while it was being imported — the whole host half was gone before any plugin code ran. `src/host/schemastery.ts` now resolves the peer itself, platform copies first (the running DSH installation, then the profile peer farm at `$DSH_HOME/profiles/node_modules`, then the profile tree), and only then falls back to Node's walk. The first candidate that actually exposes `volatile()` wins, and the capability is verified rather than assumed. +- **A missing `volatile()` degrades the form instead of killing the plugin.** When only a stale copy is reachable the plugin still loads: `Config` is built with ordinary fields, the Host logs the resolution, and the panel shows a new `stale-schemastery` warning (severity `notice`, so the chip never repaints) naming the version, the resolved path and the exact directory to remove. Only a total miss — no schemastery anywhere, which means the plugin is not running inside a working DSH installation — still throws, and it names every candidate it tried. +- **The regression is locked three ways.** `tests/host/schemastery.spec.ts` drives the resolver with injected candidates (platform copy beats a shadowing copy; a stale-only environment loads and reports; unresolvable candidates are skipped; the default order is platform-first, plugin-local last) and adds a source guard that fails if any file under `src/` takes a *value* import of the peer again. `tests/guards/shadowed-peer.mjs` (wired into `pnpm run verify`, CI and both release workflows, after the build) loads the **built** entry with a fake `3.18.2` planted beside it, asserting it still loads and that, with a platform copy present, the platform copy wins. +- **Tests: 114 passing** (up from 98) across 15 files. +- **Unchanged**: the status read, the RPC channel, the client rendering path, the sidebar chip, the detail panel, the LAN fallback and the profile-patch preference model. The three preferences are still the only volatile fields. + ## 0.1.8 — 2026-09-25 Verified DeepSeek Harness: `0.1.7-rc.2` (the latest release candidate, and what npm's `next` dist-tag publishes), also `0.1.7-rc.1` and `0.1.7-alpha.2`. Full bilingual release notes: [`docs/releases/v0.1.8.md`](docs/releases/v0.1.8.md). diff --git a/README.md b/README.md index afbf8db..c750eec 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,13 @@ The Host caches one registry response, 360 minutes by default, and only a normal **Nothing reaches the npm registry.** The plugin reports the failure and still shows the locally detected running version. Registry access is HTTPS-only to `registry.npmjs.org`; a proxy or offline host produces that warning rather than a wrong version. +**The plugin disappeared after an upgrade, and the host log says `volatile is not a function`.** +Up to `0.1.5` this plugin declared `@deepseek-ai/schemastery` as a regular dependency. From `0.1.6` it is a **peer that DSH provides**, and the `volatile()` DSH adds to it is what turns the three preferences into this entry's settings form. If a copy of that package is left inside the plugin's own install directory — a dev `node_modules` copied in by a local-directory install, which pnpm never removes — Node used to resolve that stale copy, and the missing method threw while the host half was being imported, so the plugin disappeared before it could report anything at all. Newer builds resolve the platform copy explicitly and load either way, and when only a stale copy is reachable the panel shows a `stale-schemastery` notice naming the directory. Remove the leftover copy and restart DSH: + +```sh +rm -rf ~/.dsh/profiles//node_modules/dsh-update-status/node_modules +``` + ## Security boundary - Registry access is limited to HTTPS `registry.npmjs.org`; redirects are rejected. diff --git a/README.zh.md b/README.zh.md index b6cb50e..c89ab1b 100644 --- a/README.zh.md +++ b/README.zh.md @@ -223,6 +223,13 @@ Host 会缓存一次 registry 响应,默认 360 分钟,且只有普通读取 **完全连不上 npm registry。** 插件会报告失败,并仍然显示本地检测到的运行版本。registry 访问只允许 HTTPS 的 `registry.npmjs.org`;代理或离线环境会给出这条警告,而不是编造一个版本号。 +**升级之后插件整个消失了,宿主日志里是 `volatile is not a function`。** +`0.1.5` 及更早版本把 `@deepseek-ai/schemastery` 声明为普通依赖;从 `0.1.6` 起它是**由 DSH 提供的 peer**,而 DSH 给它加的 `volatile()` 正是把三个偏好变成这个条目设置表单的机制。如果该包的一份旧副本被留在插件自己的安装目录里(用本地目录安装时被一起拷进来的 dev `node_modules`,pnpm 永远不会清理),Node 就会解析到那份旧副本,缺失的方法在宿主半 import 阶段直接抛错,插件在来得及报告任何信息之前就消失了。新版本改为显式解析平台那份,两种情况都能加载;只找得到旧副本时,面板会给出 `stale-schemastery` 提示并指出该删除哪个目录。删除残留目录后重启 DSH: + +```sh +rm -rf ~/.dsh/profiles//node_modules/dsh-update-status/node_modules +``` + ## 安全边界 - registry 访问只允许 HTTPS `registry.npmjs.org`,并拒绝重定向。 diff --git a/lib/client.js b/lib/client.js index 4fcc5d0..5799a2e 100644 --- a/lib/client.js +++ b/lib/client.js @@ -277,6 +277,14 @@ window.__ModuleLoader__.load({ zh: "{channel} 是预览通道,版本 {version} 尚未验证与本插件兼容。", en: "{channel} is a preview channel; version {version} has not been verified as compatible with this plugin." }, + "warning.staleSchemastery": { + zh: "本插件解析到的 @deepseek-ai/schemastery(版本 {version})来自 {path},不是 DSH 自带的那份,偏好字段无法标记为 volatile。请删除该残留目录并重启 DSH:rm -rf {nodeModulesDir}", + en: "This plugin resolved @deepseek-ai/schemastery {version} from {path} instead of the copy DSH provides, so preference fields cannot be marked volatile. Remove the stale directory and restart DSH: rm -rf {nodeModulesDir}" + }, + "warning.staleSchemasteryNoPath": { + zh: "本插件解析到的 @deepseek-ai/schemastery(版本 {version})来自 {path},不是 DSH 自带的那份,偏好字段无法标记为 volatile。请删除该处残留的 @deepseek-ai/schemastery 目录并重启 DSH。", + en: "This plugin resolved @deepseek-ai/schemastery {version} from {path} instead of the copy DSH provides, so preference fields cannot be marked volatile. Remove the stale @deepseek-ai/schemastery directory there and restart DSH." + }, "guidance.sourceCheckout": { zh: "请更新 DSH 源码 checkout、安装依赖并重新构建;本插件无法从 GUI 原地替换。", en: "Update the DSH source checkout, install its dependencies, and rebuild it; this plugin cannot replace it in place from the GUI." @@ -383,6 +391,16 @@ window.__ModuleLoader__.load({ channel: warning.channel, version: warning.version }, language); + case "stale-schemastery": { + const params = { + version: warning.version ?? "—", + path: warning.path + }; + return warning.nodeModulesDir === null ? t("warning.staleSchemasteryNoPath", params, language) : t("warning.staleSchemastery", { + ...params, + nodeModulesDir: warning.nodeModulesDir + }, language); + } } } /** Prefer structured warnings, but keep the Host string for mixed-version clients. */ @@ -1028,6 +1046,17 @@ window.__ModuleLoader__.load({ channel: record.channel, version: record.version }; + if (record.code === "stale-schemastery") { + const version = record.version === null ? null : stringOrNull(record.version); + const nodeModulesDir = record.nodeModulesDir === null ? null : stringOrNull(record.nodeModulesDir); + const path = stringOrNull(record.path); + if (path !== null && (record.version === null || version !== null) && (record.nodeModulesDir === null || nodeModulesDir !== null)) return { + code: record.code, + version, + path, + nodeModulesDir + }; + } } /** Reject malformed RPC output before it reaches a slot component. */ function updateStatusOf(value) { diff --git a/lib/client.js.map b/lib/client.js.map index ca90691..1146b99 100644 --- a/lib/client.js.map +++ b/lib/client.js.map @@ -1 +1 @@ -{"version":3,"file":"client.js","names":["PLUGIN_ID","PLUGIN_ID","sameSnapshot"],"sources":["../src/shared/channels.ts","../src/shared/types.ts","../src/shared/semver.ts","../src/shared/preview-guidance.ts","../src/shared/visual-state.ts","../src/client/i18n.ts","../src/client/react.ts","../src/client/components.tsx","../src/client/contract.ts","../src/client/stores.ts","../src/client/settings/hostDirectScope.ts","../src/client/settings/configFormScope.ts","../src/client/settings/settingsChannel.ts","../src/client/styles.ts","../src/client/index.ts"],"sourcesContent":["import type { ChannelRelease, UpdateStatus } from './types.ts'\n\nconst CHANNEL_ORDER = { latest: 0, next: 1, alpha: 2 } as const\n\n/**\n * Reduce raw dist-tags to user-meaningful choices without losing Host facts.\n * The selected channel and stable return path are always present; every other\n * row must carry a version no earlier row already shows.\n *\n * A channel is deliberately NOT hidden just because it currently points at the\n * release that is running. Following a channel is a statement about future\n * releases, and `currentVersion` still rides the input for the caller's own\n * rendering, but filtering on it created a deadlock: while running the version\n * published on `alpha`, the `alpha` row disappeared, so the one channel a user\n * on that line would want to follow could never be selected — the only way to\n * see it was to already follow it.\n */\nexport function visibleChannelReleases(status: Pick): ChannelRelease[] {\n const ordered = [...status.channels].sort((left, right) => CHANNEL_ORDER[left.channel] - CHANNEL_ORDER[right.channel])\n const mandatory = new Set(['latest', status.channel])\n const visible = ordered.filter(release => mandatory.has(release.channel))\n const seenVersions = new Set(visible.map(release => release.version).filter((version): version is string => version !== null))\n\n for (const release of ordered) {\n if (mandatory.has(release.channel) || release.version === null) continue\n if (seenVersions.has(release.version)) continue\n visible.push(release)\n seenVersions.add(release.version)\n }\n\n return visible.sort((left, right) => CHANNEL_ORDER[left.channel] - CHANNEL_ORDER[right.channel])\n}\n","/**\n * JSON-only contract shared by the Host and Web halves.\n *\n * The static package uses the authenticated Connection RPC channel because\n * `harness.handle` / `host.call` are dynamic-Cordis-only closure APIs. The\n * endpoint vocabulary remains deliberately small and private to this plugin\n * channel.\n */\n\nexport const PLUGIN_ID = 'dsh-update-status'\nexport const PACKAGE_NAME = '@deepseek-ai/dsh'\n/** Shared Connection RPC channel. Custom prefixes 405 on the SPA fallback. */\nexport const UPDATE_STATUS_CHANNEL = '/api'\nexport const RELEASES_URL = 'https://github.com/deepseek-ai/deepseek-harness/releases'\n/**\n * DSH releases this bundle has actually been verified against. The same list is\n * declared in package.json's `dsh.compatibility.dshReleases`, and every other\n * release the registry reports stays `unverified` — the plugin never claims a\n * compatibility nobody checked.\n */\nexport const VERIFIED_DSH_VERSIONS: readonly string[] = ['0.1.7-alpha.2', '0.1.7-rc.1', '0.1.7-rc.2']\nexport const RELEASE_CHANNELS = ['latest', 'next', 'alpha'] as const\nexport const DEFAULT_CACHE_TTL_MINUTES = 360\nexport const MIN_CACHE_TTL_MINUTES = 30\nexport const MAX_CACHE_TTL_MINUTES = 1_440\n\nexport function isCacheTtlMinutes(value: unknown): value is number {\n return typeof value === 'number' && Number.isInteger(value)\n && value >= MIN_CACHE_TTL_MINUTES && value <= MAX_CACHE_TTL_MINUTES\n}\n\nexport const UPDATE_ENDPOINTS = {\n getStatus: 'dsh-update-status.get-status',\n checkUpdate: 'dsh-update-status.check-update',\n} as const\n\nexport type UpdateEndpoint = (typeof UPDATE_ENDPOINTS)[keyof typeof UPDATE_ENDPOINTS]\nexport type ReleaseChannel = (typeof RELEASE_CHANNELS)[number]\nexport type ReleaseCompatibility = 'verified' | 'unverified' | 'incompatible'\n/**\n * Severity of `warning`, so a surface can tell \"the plugin could not determine\n * the update state\" from \"here is something worth knowing\".\n *\n * - `failure`: no usable answer — a failed registry read, a channel the registry\n * does not publish, or a version SemVer cannot compare. This is what justifies\n * repainting the sidebar chip.\n * - `notice`: the answer is complete and usable; the text is advisory, e.g. a\n * preview channel whose release this bundle has not been verified against.\n * The chip must NOT repaint for this — an operator upgrading DSH ahead of the\n * plugin would otherwise see the chip turn red for a plugin-side bookkeeping\n * fact.\n *\n * Optional on purpose: a Host older than this field leaves it `undefined`, and\n * the client then falls back to treating any warning as a failure.\n */\nexport type UpdateWarningKind = 'failure' | 'notice'\nexport type InstallKind = 'npm-global' | 'pnpm-global' | 'source-checkout' | 'unknown'\n\n/** Language-neutral warning facts; the browser renders them in its own locale. */\nexport type UpdateWarning =\n | { code: 'registry-unavailable'; detail: string }\n | { code: 'channel-unavailable'; channel: ReleaseChannel }\n | { code: 'version-incomparable'; currentVersion: string; channel: ReleaseChannel; selectedVersion: string }\n | { code: 'preview-unverified'; channel: ReleaseChannel; version: string }\n\nexport function isReleaseChannel(value: unknown): value is ReleaseChannel {\n return value === 'latest' || value === 'next' || value === 'alpha'\n}\n\nexport interface ChannelRelease {\n channel: ReleaseChannel\n version: string | null\n publishedAt: string | null\n compatibility: ReleaseCompatibility\n}\n\n/** Arguments accepted by either read/check endpoint. */\nexport interface CheckUpdateRequest {\n force?: boolean\n channel?: ReleaseChannel\n /** User preference, bounded by the Host before it affects cache expiry. */\n cacheTtlMinutes?: number\n}\n\n/**\n * A lossless, JSON-serializable snapshot used by both browser surfaces.\n * Nulls are intentional: a failed first check must still render a truthful\n * current-version card instead of an empty or malformed UI.\n */\nexport interface UpdateStatus {\n currentVersion: string\n latestVersion: string | null\n hasUpdate: boolean\n cached: boolean\n checkedAt: string | null\n /** English fallback for older clients; current clients localize `warnings`. */\n warning: string | null\n /** Severity of `warning`; absent from a Host older than the field. */\n warningKind?: UpdateWarningKind | null\n /** Structured warning facts; absent from a Host older than this field. */\n warnings?: UpdateWarning[]\n installKind: InstallKind\n upgradeCommand: string\n releaseUrl: string\n changelogUrl: string\n publishedAt: string | null\n packageName: string\n /** Selected npm dist-tag used for comparison and command generation. */\n channel: ReleaseChannel\n /** All supported dist-tags returned by the same cached registry request. */\n channels: ChannelRelease[]\n /** Phase 1 is informational only; the GUI must never apply an update. */\n canApplyInPlace: false\n}\n","/**\n * Small, dependency-free SemVer 2.0 comparator for the host check path.\n * It intentionally accepts the common leading `v` used by release tags while\n * preserving prerelease precedence (`0.1.2-rc.1 < 0.1.2`).\n */\n\nexport interface ParsedSemver {\n major: number\n minor: number\n patch: number\n prerelease: readonly string[]\n}\n\nconst SEMVER = /^(?:v)?(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$/\nconst NUMERIC_IDENTIFIER = /^(0|[1-9]\\d*)$/\n\nexport function parseSemver(value: string): ParsedSemver | undefined {\n const match = SEMVER.exec(value.trim())\n if (match === null) return undefined\n const major = Number(match[1])\n const minor = Number(match[2])\n const patch = Number(match[3])\n if (!Number.isSafeInteger(major) || !Number.isSafeInteger(minor) || !Number.isSafeInteger(patch)) return undefined\n const prerelease = match[4] === undefined ? [] : match[4].split('.')\n return { major, minor, patch, prerelease }\n}\n\nfunction compareIdentifier(left: string, right: string): number {\n const leftNumeric = NUMERIC_IDENTIFIER.test(left)\n const rightNumeric = NUMERIC_IDENTIFIER.test(right)\n if (leftNumeric && rightNumeric) return Number(left) - Number(right)\n if (leftNumeric) return -1\n if (rightNumeric) return 1\n return left < right ? -1 : left > right ? 1 : 0\n}\n\n/** Returns a negative number when left is older; undefined means unparsable. */\nexport function compareSemver(leftValue: string, rightValue: string): number | undefined {\n const left = parseSemver(leftValue)\n const right = parseSemver(rightValue)\n if (left === undefined || right === undefined) return undefined\n\n for (const key of ['major', 'minor', 'patch'] as const) {\n if (left[key] !== right[key]) return left[key] - right[key]\n }\n\n const leftStable = left.prerelease.length === 0\n const rightStable = right.prerelease.length === 0\n if (leftStable && rightStable) return 0\n if (leftStable) return 1\n if (rightStable) return -1\n\n const length = Math.max(left.prerelease.length, right.prerelease.length)\n for (let index = 0; index < length; index += 1) {\n const leftPart = left.prerelease[index]\n const rightPart = right.prerelease[index]\n if (leftPart === undefined) return -1\n if (rightPart === undefined) return 1\n const comparison = compareIdentifier(leftPart, rightPart)\n if (comparison !== 0) return comparison\n }\n return 0\n}\n\n/** False rather than a guess when either version is not valid SemVer. */\nexport function hasSemverUpdate(currentVersion: string, latestVersion: string): boolean {\n const comparison = compareSemver(currentVersion, latestVersion)\n return comparison !== undefined && comparison < 0\n}\n","import type { UpdateStatus } from './types.ts'\nimport { PACKAGE_NAME } from './types.ts'\nimport { parseSemver } from './semver.ts'\n\n/** Pinned guidance only: never execute a command or mutate the followed channel. */\nexport function previewCommand(status: UpdateStatus, version: string | null): string | null {\n if (version === null || version.trim() !== version || parseSemver(version) === undefined) return null\n if (status.installKind === 'npm-global') return `npm install -g ${PACKAGE_NAME}@${version}`\n if (status.installKind === 'pnpm-global') return `pnpm add -g ${PACKAGE_NAME}@${version}`\n return null\n}\n","/**\n * The single place that decides which visual state the sidebar chip is in.\n *\n * It lives in `shared` rather than inside the component so the decision is a pure\n * function that a test can drive directly: the regression this guards against is\n * exactly a state-classification mistake, and it was invisible to every test while\n * it sat inside TSX.\n */\nimport type { UpdateStatus } from './types.ts'\n\nexport type VisualState = 'loading' | 'update' | 'current' | 'problem'\n\n/**\n * Only a read the plugin could not complete is a chip-level problem.\n *\n * An advisory notice — a preview channel this bundle has not been verified against,\n * say — leaves the chip surface alone and belongs in the panel. Repainting the brand\n * row for a plugin-side bookkeeping fact is the surprise the breathing dot replaced:\n * an operator who upgrades DSH ahead of this bundle must not watch the chip turn red.\n *\n * A Host older than `warningKind` sends no field at all; `undefined` then keeps the\n * previous behaviour (any warning is a problem) rather than silently dropping a real\n * failure on the floor.\n */\nexport function isChipProblem(status: UpdateStatus | null): boolean {\n if (status === null) return false\n if (status.warningKind === undefined) return status.warning !== null\n return status.warningKind === 'failure'\n}\n\nexport function visualState(status: UpdateStatus | null, loading: boolean, error: string | null): VisualState {\n if (loading) return 'loading'\n if (error !== null || isChipProblem(status)) return status?.hasUpdate === true ? 'update' : 'problem'\n return status?.hasUpdate === true ? 'update' : 'current'\n}\n","/** Tiny bilingual dictionary kept local to avoid changing the host locale tree. */\n\nimport type { UpdateStatus, UpdateWarning } from '../shared/types.ts'\n\nexport type Language = 'zh' | 'en'\n\ntype Entry = { zh: string; en: string }\n\nconst DICTIONARY: Record = {\n 'brand.status': { zh: 'DSH 版本状态', en: 'DSH version status' },\n 'brand.checking': { zh: '正在检查版本', en: 'Checking version' },\n 'brand.update': { zh: '有可用更新', en: 'Update available' },\n 'brand.current': { zh: '已是最新或尚未发现更新', en: 'Up to date or no update found' },\n 'brand.problem': { zh: '检查遇到问题', en: 'Check encountered a problem' },\n 'footer.status': { zh: '打开 DSH 版本状态', en: 'Open DSH version status' },\n 'panel.title': { zh: 'DSH 更新状态', en: 'DSH update status' },\n 'panel.close': { zh: '关闭', en: 'Close' },\n 'panel.cacheDuration': { zh: '缓存时长', en: 'Cache duration' },\n 'panel.minutes': { zh: '分钟', en: 'minutes' },\n 'panel.cacheHint': { zh: '到期后在下次打开或读取状态时检查;不会后台轮询。点击“检查更新”会立即检查。', en: 'After expiry, DSH checks only on the next status read; there is no background polling. “Check for updates” checks immediately.' },\n 'panel.cacheReadonly': { zh: '此连接的缓存设置为只读。', en: 'This connection’s cache setting is read-only.' },\n 'panel.current': { zh: '当前运行版本', en: 'Current running version' },\n 'panel.latest': { zh: '所选通道版本', en: 'Selected channel version' },\n 'panel.channels': { zh: '可用发布通道', en: 'Available release channels' },\n 'panel.compatVerified': { zh: '已验证兼容', en: 'Verified compatible' },\n 'panel.compatUnverified': { zh: '尚未验证兼容', en: 'Compatibility unverified' },\n 'panel.compatIncompatible': { zh: '已知不兼容', en: 'Known incompatible' },\n 'panel.selectChannel': { zh: '选择此通道', en: 'Select channel' },\n 'panel.selectedChannel': { zh: '已选择', en: 'Selected' },\n 'panel.notChecked': { zh: '尚未取得', en: 'Not available yet' },\n 'panel.available': { zh: '发现新版本', en: 'Update available' },\n 'panel.currentState': { zh: '未发现可用更新', en: 'No update found' },\n 'panel.cached': { zh: '来自 Host 缓存', en: 'From Host cache' },\n 'panel.live': { zh: '刚从 npm registry 检查', en: 'Checked npm registry now' },\n 'panel.checkedAt': { zh: '上次检查', en: 'Last checked' },\n 'panel.publishedAt': { zh: '发布时间', en: 'Published' },\n 'panel.check': { zh: '检查更新', en: 'Check for updates' },\n 'panel.checking': { zh: '检查中…', en: 'Checking…' },\n 'panel.switchingChannel': { zh: '正在切换通道…', en: 'Switching channel…' },\n 'panel.releaseNotes': { zh: '发布说明', en: 'Release notes' },\n 'panel.command': { zh: '升级命令', en: 'Upgrade command' },\n 'panel.copy': { zh: '复制命令', en: 'Copy command' },\n 'panel.copied': { zh: '已复制', en: 'Copied' },\n 'panel.copyFallback': { zh: '浏览器未允许复制;已选中文本,请长按或复制。', en: 'Clipboard unavailable; the command is selected for long-press or copy.' },\n 'panel.commandNote': { zh: '仅复制,不会执行。请在运行 DSH 的那台电脑的终端执行;完成后由你自行重启 DSH。', en: 'Copy only — nothing runs here. Execute it in a terminal on the computer running DSH, then restart DSH yourself.' },\n 'panel.readOnly': { zh: '阶段 1 仅提示:本插件不会安装、重启、回滚或替换任何文件。', en: 'Phase 1 is advisory only: this plugin never installs, restarts, rolls back, or replaces files.' },\n 'panel.error': { zh: '检查提示', en: 'Check notice' },\n 'warning.registryUnavailable': { zh: '无法检查 npm registry:{detail}', en: 'Unable to check the npm registry: {detail}' },\n 'warning.channelUnavailable': { zh: 'npm registry 未发布 {channel} 通道。', en: 'The npm registry does not publish a {channel} channel.' },\n 'warning.versionIncomparable': { zh: '无法按 SemVer 比较当前版本 {currentVersion} 与 {channel} 通道版本 {selectedVersion}。', en: 'Unable to compare the current version {currentVersion} with {channel} channel version {selectedVersion} using SemVer.' },\n 'warning.previewUnverified': { zh: '{channel} 是预览通道,版本 {version} 尚未验证与本插件兼容。', en: '{channel} is a preview channel; version {version} has not been verified as compatible with this plugin.' },\n 'guidance.sourceCheckout': { zh: '请更新 DSH 源码 checkout、安装依赖并重新构建;本插件无法从 GUI 原地替换。', en: 'Update the DSH source checkout, install its dependencies, and rebuild it; this plugin cannot replace it in place from the GUI.' },\n 'guidance.unknownInstall': { zh: '升级前请先确认 DSH 的安装方式;本插件无法代为执行升级。', en: 'Confirm how DSH was installed before upgrading; this plugin cannot perform the upgrade for you.' },\n 'panel.static': { zh: '此连接只显示静态版本;请在运行 DSH 的本机打开侧栏查看完整更新信息。', en: 'This connection shows only the static version. Open the sidebar on the computer running DSH for full update details.' },\n 'preview.open': { zh: '查看预览版升级方式', en: 'View preview upgrade instructions' },\n 'preview.close': { zh: '收起升级说明', en: 'Hide upgrade instructions' },\n 'preview.follow': { zh: '关注通道只影响检查结果,不代表已安装或已切换版本。', en: 'Following a channel only changes update checks, not the installed version.' },\n 'preview.target': { zh: '目标版本', en: 'Target version' },\n 'preview.risk': { zh: '预览版本可能不稳定,插件兼容性尚需确认。执行前请保存工作、备份配置并结束运行中的任务;安装后需手动重启 DSH。此页面不会安装或重启。', en: 'Preview builds may be unstable and plugin compatibility needs checking. Save work, back up configuration and finish active tasks before executing. Restart DSH manually afterwards. This page never installs or restarts.' },\n 'preview.unavailable': { zh: '暂无可确认的预览目标,请先检查更新。', en: 'No confirmed preview target. Check for updates first.' },\n 'preview.manual': { zh: '当前安装方式无法安全生成命令,请先确认安装来源;源码安装需按其构建说明操作。', en: 'Cannot safely generate a command for this installation. Confirm its source; source checkouts require their build instructions.' },\n 'preview.same': { zh: '此目标与当前运行版本相同,无需重复安装。', en: 'This target is already running; no reinstall is needed.' },\n 'settings.title': { zh: '版本与更新', en: 'Version & updates' },\n 'settings.sidebar': { zh: '在侧栏显示版本状态入口', en: 'Show the version-status entry in the sidebar' },\n 'settings.sidebarHint': { zh: '关闭后不再渲染品牌 Badge 和收起轨道兜底入口。不会执行或安排升级。', en: 'When off, the brand badge and collapsed-rail fallback are not rendered. No update is run or scheduled.' },\n 'settings.channel': { zh: '关注发布通道', en: 'Release channel to follow' },\n 'settings.channelLatest': { zh: '稳定版(latest,推荐)', en: 'Stable (latest, recommended)' },\n 'settings.channelNext': { zh: '候选版(next)', en: 'Release candidate (next)' },\n 'settings.channelAlpha': { zh: '预览版(alpha)', en: 'Preview (alpha)' },\n 'settings.channelHint': { zh: '预览通道可能包含未稳定接口或插件兼容性变化。这里只检查并生成命令,不会安装。', en: 'Preview channels may contain unstable APIs or plugin compatibility changes. This only checks and generates a command; it never installs.' },\n 'settings.readonly': { zh: '此连接的设置为只读;显示状态不受影响。', en: 'Settings are read-only on this connection; status display is unchanged.' },\n}\n\nexport function languageOf(): Language {\n try {\n return navigator.language.toLowerCase().startsWith('zh') ? 'zh' : 'en'\n } catch {\n return 'en'\n }\n}\n\nexport function t(key: keyof typeof DICTIONARY, params: Record = {}, language: Language = languageOf()): string {\n const entry = DICTIONARY[key]\n if (entry === undefined) return key\n const template = entry[language] ?? entry.en\n return template.replace(/\\{([A-Za-z]+)\\}/g, (match, name: string) => params[name] ?? match)\n}\n\nexport function warningText(warning: UpdateWarning, language: Language = languageOf()): string {\n switch (warning.code) {\n case 'registry-unavailable': return t('warning.registryUnavailable', { detail: warning.detail }, language)\n case 'channel-unavailable': return t('warning.channelUnavailable', { channel: warning.channel }, language)\n case 'version-incomparable': return t('warning.versionIncomparable', {\n currentVersion: warning.currentVersion,\n channel: warning.channel,\n selectedVersion: warning.selectedVersion,\n }, language)\n case 'preview-unverified': return t('warning.previewUnverified', { channel: warning.channel, version: warning.version }, language)\n }\n}\n\n/** Prefer structured warnings, but keep the Host string for mixed-version clients. */\nexport function localizedWarning(status: Pick, language: Language = languageOf()): string | null {\n const warnings = status.warnings ?? []\n return warnings.length === 0 ? status.warning : warnings.map(item => warningText(item, language)).join(' ')\n}\n\nexport function localizedUpgradeGuidance(\n status: Pick,\n language: Language = languageOf(),\n): string {\n if (status.installKind === 'source-checkout') return t('guidance.sourceCheckout', {}, language)\n if (status.installKind === 'unknown') return t('guidance.unknownInstall', {}, language)\n return status.upgradeCommand\n}\n","/** React arrives from DSH's frozen browser module table at client materialization. */\nimport type * as ReactNS from 'react'\n\n// eslint/oxlint would normally discourage require; DSH's closure factory makes\n// this the supported runtime import path for a third-party client bundle.\nexport const React: typeof ReactNS = require('react') as typeof ReactNS\nexport const h = React.createElement\n","/** Sidebar replacement, collapsed-rail fallback, overlay detail panel, and settings page. */\n\nimport type * as ReactNS from 'react'\nimport { visibleChannelReleases } from '../shared/channels.ts'\nimport { previewCommand } from '../shared/preview-guidance.ts'\nimport { isChipProblem } from '../shared/visual-state.ts'\nimport { MAX_CACHE_TTL_MINUTES, MIN_CACHE_TTL_MINUTES, isCacheTtlMinutes, type ReleaseChannel, type ReleaseCompatibility, type UpdateStatus } from '../shared/types.ts'\nimport type { PreferencesStore, PanelStore, StatusStore } from './stores.ts'\nimport { localizedUpgradeGuidance, localizedWarning, t } from './i18n.ts'\nimport { React, h } from './react.ts'\n\nfunction useObservable(store: { subscribe(listener: () => void): () => void; getSnapshot(): T }): T {\n return React.useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot)\n}\n\nfunction timeText(value: string | null): string | null {\n if (value === null) return null\n try {\n return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(new Date(value))\n } catch {\n return value\n }\n}\n\nfunction shortVersion(value: string): string {\n if (value.length <= 16) return value\n return value.slice(0, 15) + '…'\n}\n\nfunction channelLabel(channel: ReleaseChannel): string {\n if (channel === 'latest') return t('settings.channelLatest')\n if (channel === 'next') return t('settings.channelNext')\n return t('settings.channelAlpha')\n}\n\nfunction compatibilityLabel(value: ReleaseCompatibility): string {\n if (value === 'verified') return t('panel.compatVerified')\n if (value === 'incompatible') return t('panel.compatIncompatible')\n return t('panel.compatUnverified')\n}\n\n/** Persists a cache policy only; it never schedules a browser or Host timer. */\nfunction CacheTtlControl({ preferences }: { preferences: PreferencesStore }): ReactNS.ReactElement {\n const snapshot = useObservable(preferences)\n const [draft, setDraft] = React.useState(String(snapshot.cacheTtlMinutes))\n React.useEffect(() => { setDraft(String(snapshot.cacheTtlMinutes)) }, [snapshot.cacheTtlMinutes])\n const save = (): void => {\n const value = Number(draft)\n if (isCacheTtlMinutes(value)) preferences.setCacheTtlMinutes(value)\n else setDraft(String(snapshot.cacheTtlMinutes))\n }\n return (\n
\n \n

{t('panel.cacheHint')}

\n {!snapshot.writable &&

{t('panel.cacheReadonly')}

}\n
\n )\n}\n\ntype VisualState = 'loading' | 'update' | 'current' | 'problem'\n\nfunction visualState(status: UpdateStatus | null, loading: boolean, error: string | null): VisualState {\n if (loading) return 'loading'\n if (error !== null || isChipProblem(status)) return status?.hasUpdate === true ? 'update' : 'problem'\n return status?.hasUpdate === true ? 'update' : 'current'\n}\n\nfunction badgeLabel(status: UpdateStatus | null, loading: boolean, error: string | null): string {\n const state = visualState(status, loading, error)\n if (state === 'loading') return t('brand.checking')\n if (state === 'update') return t('brand.update')\n if (state === 'problem') return t('brand.problem')\n return t('brand.current')\n}\n\nexport interface SharedUi {\n status: StatusStore\n preferences: PreferencesStore\n panel: PanelStore\n}\n\n/** Occupies ONLY sidebar.brand.name; the official fish mark stays untouched. */\nexport function BrandName({ ui }: { ui: SharedUi }): ReactNS.ReactElement | null {\n const preferences = useObservable(ui.preferences)\n const snapshot = useObservable(ui.status)\n if (!preferences.sidebarEnabled) return null\n\n const state = visualState(snapshot.status, snapshot.loading, snapshot.error)\n const version = snapshot.status?.currentVersion ?? '—'\n const activate = (event: ReactNS.SyntheticEvent): void => {\n // The surrounding sidebar brand is an existing New Session \n )\n}\n\nfunction statusSummary(status: UpdateStatus | null, loading: boolean, error: string | null): { kind: 'update' | 'ok' | 'warning' | 'error'; text: string } {\n if (loading && status === null) return { kind: 'warning', text: t('brand.checking') }\n if (error !== null) return { kind: 'error', text: error }\n if (status?.hasUpdate === true) return { kind: 'update', text: t('panel.available') }\n if (status !== null) {\n const warning = localizedWarning(status)\n if (warning !== null) return { kind: 'warning', text: warning }\n }\n return { kind: 'ok', text: t('panel.currentState') }\n}\n\nfunction selectCommand(element: HTMLElement | null): void {\n if (element === null) return\n try {\n const selection = window.getSelection()\n if (selection === null) return\n const range = document.createRange()\n range.selectNodeContents(element)\n selection.removeAllRanges()\n selection.addRange(range)\n element.focus()\n } catch {\n // The command remains ordinary selectable text even when selection fails.\n }\n}\n\n/** Full detail from whichever page the operator is on (the transport is authenticated). */\nexport function UpdatePanel({ ui }: { ui: SharedUi }): ReactNS.ReactElement | null {\n const panel = useObservable(ui.panel)\n const preferences = useObservable(ui.preferences)\n const snapshot = useObservable(ui.status)\n const commandRef = React.useRef(null)\n const dialogRef = React.useRef(null)\n const [copyMessage, setCopyMessage] = React.useState(null)\n const visible = panel.open && preferences.sidebarEnabled\n\n React.useLayoutEffect(() => {\n if (!visible) return undefined\n const dialog = dialogRef.current\n if (dialog === null) return undefined\n try {\n if (!dialog.open) dialog.showModal()\n } catch {\n // Older embedded Chromium still receives a visible non-top-layer dialog.\n dialog.setAttribute('open', '')\n }\n return () => {\n if (dialog.open) dialog.close()\n }\n }, [visible])\n\n React.useEffect(() => {\n if (!panel.open) return undefined\n const onKeyDown = (event: KeyboardEvent): void => {\n if (event.key === 'Escape') ui.panel.close()\n }\n window.addEventListener('keydown', onKeyDown)\n return () => { window.removeEventListener('keydown', onKeyDown) }\n }, [panel.open, ui.panel])\n\n if (!visible) return null\n const status = snapshot.status\n const warning = status === null ? null : localizedWarning(status)\n const summary = statusSummary(status, snapshot.loading, snapshot.error)\n const switchingChannel = status !== null && status.channel !== preferences.channel\n const command = switchingChannel ? t('panel.switchingChannel') : status === null ? '—' : localizedUpgradeGuidance(status)\n\n const copy = async (): Promise => {\n setCopyMessage(null)\n try {\n if (!navigator.clipboard?.writeText) throw new Error('Clipboard API unavailable')\n await navigator.clipboard.writeText(command)\n setCopyMessage(t('panel.copied'))\n } catch {\n selectCommand(commandRef.current)\n setCopyMessage(t('panel.copyFallback'))\n }\n }\n\n return (\n {\n event.preventDefault()\n ui.panel.close()\n }}\n >\n
{ ui.panel.close() }} />\n
{ event.stopPropagation() }}>\n
\n {t('panel.title')}\n \n
\n\n \n

{summary.text}

\n\n
\n
\n {t('panel.current')}\n {status?.currentVersion ?? '…'}\n
\n
\n {t('panel.latest')} · {status === null ? 'latest' : channelLabel(status.channel)}\n {status?.latestVersion ?? t('panel.notChecked')}\n {status?.publishedAt !== null && status?.publishedAt !== undefined && {t('panel.publishedAt')}: {timeText(status.publishedAt)}}\n
\n {status?.checkedAt !== null && status?.checkedAt !== undefined &&
\n {t('panel.checkedAt')}\n {timeText(status.checkedAt)}\n {status.cached ? t('panel.cached') : t('panel.live')}\n
}\n
\n\n {status !== null &&
\n

{t('panel.channels')}

\n {visibleChannelReleases(status).map(release => {\n const selected = release.channel === preferences.channel\n return
\n {channelLabel(release.channel)}\n {release.version ?? '—'}\n {compatibilityLabel(release.compatibility)}\n { ui.preferences.setChannel(release.channel) }}\n >{selected ? t('panel.selectedChannel') : t('panel.selectChannel')}\n
\n })}\n
}\n\n {warning !== null &&

{t('panel.error')}: {warning}

}\n {snapshot.error !== null &&

{t('panel.error')}: {snapshot.error}

}\n\n
\n \n {status !== null && {t('panel.releaseNotes')}}\n \n
\n\n

{t('panel.command')}

\n {command}\n {copyMessage !== null &&

{copyMessage}

}\n

{t('panel.commandNote')}

\n

{t('panel.readOnly')}

\n
\n \n )\n}\n\n/** Separate setting page: disables only this plugin's own rendering. */\nexport function UpdateSettings({ ui }: { ui: SharedUi }): ReactNS.ReactElement {\n const preferences = useObservable(ui.preferences)\n const snapshot = useObservable(ui.status)\n const [showGuidance, setShowGuidance] = React.useState(false)\n const previews = snapshot.status?.channels.filter(release => release.channel !== 'latest') ?? []\n const channelOptions = snapshot.status === null\n ? [{ channel: preferences.channel }]\n : visibleChannelReleases(snapshot.status)\n return (\n
\n

{t('settings.title')}

\n
\n \n

{t('settings.sidebarHint')}

\n {!preferences.writable &&

{t('settings.readonly')}

}\n
\n
\n \n

{t('settings.channelHint')}

\n

{t('panel.current')}: {snapshot.status?.currentVersion ?? '…'}

\n

{t('preview.follow')}

\n \n {showGuidance &&
\n

{t('preview.risk')}

\n \n {snapshot.error !== null &&

{snapshot.error}

}\n {previews.length === 0 &&

{t('preview.unavailable')}

}\n {previews.map(release => {\n const status = snapshot.status!\n const command = previewCommand(status, release.version)\n return
\n

{channelLabel(release.channel)} · {t('preview.target')}: {release.version ?? t('panel.notChecked')}

\n

{compatibilityLabel(release.compatibility)}

\n {release.version === null ?

{t('preview.unavailable')}

: release.version === status.currentVersion ?

{t('preview.same')}

: command !== null ? <>

{t('panel.command')}

{command} :

{t('preview.manual')}

}\n
\n })}\n

{t('panel.commandNote')}

\n
}\n
\n
\n )\n}\n","/** Minimal structural client faces — runtime services stay owned by DSH. */\n\nimport { isReleaseChannel, type ChannelRelease, type UpdateStatus, type UpdateWarning } from '../shared/types.ts'\n\nexport interface Observable {\n getSnapshot(): T\n subscribe(listener: () => void): () => void\n}\n\nexport interface ConnectionRpc {\n call(channel: string, endpoint: string, payload: unknown, signal?: AbortSignal): Promise\n}\n\n/**\n * The authenticated transport face. There is deliberately NO `isLoopback` gate\n * here: DSH's settings Client treats a non-loopback page as process-local, but\n * that is a settings *persistence* policy — the Connection RPC itself stays\n * authenticated and reachable over a LAN bridge, so gating this plugin's own\n * read-only status on it only made the panel inert on exactly the page it is\n * used from. See settings/settingsChannel.ts for the one place a loopback\n * distinction still matters (choosing the Host settings channel).\n */\nexport interface ConnectionClient {\n rpc?: ConnectionRpc\n}\n\nexport interface SlotRegistration {\n name: string\n id?: string\n order?: number\n /** Single-slot arbitration: the lowest registered priority renders. */\n priority?: number\n label?: () => string\n locale?: string\n inject?: () => unknown\n}\n\nexport interface Slots {\n inject(name: string, callback: () => unknown): () => void\n register(registration: SlotRegistration, component: (props: Record) => unknown): () => void\n}\n\nexport interface ClientContext {\n slots: Slots\n get(name: string): unknown\n inject(names: string[], callback: (ctx: unknown) => void): () => void\n effect(setup: () => (() => void) | void, label?: string): () => void\n /** Optional lifecycle event seam (Cordis contexts expose it; guarded here). */\n on?(name: string, listener: () => void): unknown\n}\n\nexport function errorMessage(error: unknown): string {\n const text = error instanceof Error ? error.message : String(error)\n return text.replace(/\\s+/g, ' ').trim().slice(0, 220) || 'unknown error'\n}\n\nfunction stringOrNull(value: unknown): string | null {\n return typeof value === 'string' ? value : null\n}\n\nfunction warningOf(value: unknown): UpdateWarning | undefined {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) return undefined\n const record = value as Record\n if (record.code === 'registry-unavailable' && typeof record.detail === 'string') {\n return { code: record.code, detail: record.detail }\n }\n if (record.code === 'channel-unavailable' && isReleaseChannel(record.channel)) {\n return { code: record.code, channel: record.channel }\n }\n if (record.code === 'version-incomparable' && typeof record.currentVersion === 'string'\n && isReleaseChannel(record.channel) && typeof record.selectedVersion === 'string') {\n return {\n code: record.code,\n currentVersion: record.currentVersion,\n channel: record.channel,\n selectedVersion: record.selectedVersion,\n }\n }\n if (record.code === 'preview-unverified' && isReleaseChannel(record.channel) && typeof record.version === 'string') {\n return { code: record.code, channel: record.channel, version: record.version }\n }\n return undefined\n}\n\n/** Reject malformed RPC output before it reaches a slot component. */\nexport function updateStatusOf(value: unknown): UpdateStatus | undefined {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) return undefined\n const record = value as Record\n if (typeof record.currentVersion !== 'string' || typeof record.hasUpdate !== 'boolean'\n || typeof record.cached !== 'boolean' || typeof record.installKind !== 'string'\n || typeof record.upgradeCommand !== 'string' || typeof record.releaseUrl !== 'string'\n || typeof record.changelogUrl !== 'string' || typeof record.packageName !== 'string'\n || !isReleaseChannel(record.channel) || !Array.isArray(record.channels) || record.canApplyInPlace !== false) return undefined\n const validKind = record.installKind === 'npm-global' || record.installKind === 'pnpm-global'\n || record.installKind === 'source-checkout' || record.installKind === 'unknown'\n if (!validKind) return undefined\n const latestVersion = record.latestVersion === null ? null : stringOrNull(record.latestVersion)\n const checkedAt = record.checkedAt === null ? null : stringOrNull(record.checkedAt)\n const warning = record.warning === null ? null : stringOrNull(record.warning)\n // Absent or unrecognised means a Host older than the field: leave it undefined and\n // let the visual state fall back to \"any warning is a failure\", never to \"no warning\".\n const warningKind = record.warningKind === 'failure' || record.warningKind === 'notice' ? record.warningKind : undefined\n const publishedAt = record.publishedAt === null ? null : stringOrNull(record.publishedAt)\n if ((record.latestVersion !== null && latestVersion === null) || (record.checkedAt !== null && checkedAt === null)\n || (record.warning !== null && warning === null) || (record.publishedAt !== null && publishedAt === null)) return undefined\n const warnings: UpdateWarning[] = []\n if (record.warnings !== undefined) {\n if (!Array.isArray(record.warnings)) return undefined\n for (const raw of record.warnings) {\n const parsed = warningOf(raw)\n if (parsed === undefined) return undefined\n warnings.push(parsed)\n }\n }\n const channels: ChannelRelease[] = []\n for (const raw of record.channels) {\n if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) return undefined\n const item = raw as Record\n const version = item.version === null ? null : stringOrNull(item.version)\n const channelPublishedAt = item.publishedAt === null ? null : stringOrNull(item.publishedAt)\n if (!isReleaseChannel(item.channel) || (item.version !== null && version === null)\n || (item.publishedAt !== null && channelPublishedAt === null)\n || (item.compatibility !== 'verified' && item.compatibility !== 'unverified' && item.compatibility !== 'incompatible')) return undefined\n channels.push({ channel: item.channel, version, publishedAt: channelPublishedAt, compatibility: item.compatibility })\n }\n return {\n currentVersion: record.currentVersion,\n latestVersion,\n hasUpdate: record.hasUpdate,\n cached: record.cached,\n checkedAt,\n warning,\n warningKind,\n warnings,\n installKind: record.installKind as UpdateStatus['installKind'],\n upgradeCommand: record.upgradeCommand,\n releaseUrl: record.releaseUrl,\n changelogUrl: record.changelogUrl,\n publishedAt,\n packageName: record.packageName,\n channel: record.channel,\n channels,\n canApplyInPlace: false,\n }\n}\n","/** Small observable stores shared by the independent sidebar and overlay slots. */\n\nimport { DEFAULT_CACHE_TTL_MINUTES, isCacheTtlMinutes, isReleaseChannel, PLUGIN_ID, UPDATE_ENDPOINTS, UPDATE_STATUS_CHANNEL, type ReleaseChannel, type UpdateStatus } from '../shared/types.ts'\nimport type { ConnectionClient, Observable } from './contract.ts'\nimport { errorMessage, updateStatusOf } from './contract.ts'\nimport type { SettingsScopeLike } from './settings/scopeFaces.ts'\n\nexport interface StatusSnapshot {\n status: UpdateStatus | null\n loading: boolean\n error: string | null\n}\n\nconst INITIAL_STATUS: StatusSnapshot = { status: null, loading: true, error: null }\n\nexport class StatusStore implements Observable {\n private snapshot: StatusSnapshot = INITIAL_STATUS\n private readonly listeners = new Set<() => void>()\n private inFlight: Promise | undefined\n private stopped = false\n\n constructor(private readonly connection: ConnectionClient) {}\n\n getSnapshot = (): StatusSnapshot => this.snapshot\n\n subscribe = (listener: () => void): (() => void) => {\n this.listeners.add(listener)\n return () => { this.listeners.delete(listener) }\n }\n\n /** First mount reads the Host's cached status; it never starts a browser timer. */\n async load(cacheTtlMinutes: number = DEFAULT_CACHE_TTL_MINUTES): Promise {\n await this.request(false, this.snapshot.status?.channel ?? 'latest', cacheTtlMinutes)\n }\n\n /** User gesture only: force the Host to bypass TTL (while retaining single-flight). */\n async refresh(channel: ReleaseChannel = this.snapshot.status?.channel ?? 'latest', cacheTtlMinutes: number = DEFAULT_CACHE_TTL_MINUTES): Promise {\n await this.request(true, channel, cacheTtlMinutes)\n }\n\n /** Channel selection re-projects the Host cache; it is not a forced refresh. */\n async selectChannel(channel: ReleaseChannel, cacheTtlMinutes: number = DEFAULT_CACHE_TTL_MINUTES): Promise {\n // A persisted preference or a newer selection can arrive while another\n // channel is in flight. Keep checking after every joined request until the\n // requested projection is actually the published snapshot.\n for (let attempts = 0; attempts < 3 && !this.stopped && this.snapshot.status?.channel !== channel; attempts += 1) {\n const pending = this.inFlight\n if (pending !== undefined && !await pending) return\n if (this.stopped || this.snapshot.status?.channel === channel) return\n if (!await this.request(false, channel, cacheTtlMinutes)) return\n }\n }\n\n stop(): void {\n this.stopped = true\n this.listeners.clear()\n }\n\n private publish(next: StatusSnapshot): void {\n if (this.stopped) return\n this.snapshot = next\n for (const listener of this.listeners) listener()\n }\n\n private request(force: boolean, channel: ReleaseChannel, cacheTtlMinutes: number): Promise {\n if (this.inFlight !== undefined) return this.inFlight\n const rpc = this.connection.rpc\n if (rpc === undefined || typeof rpc.call !== 'function') {\n this.publish({ ...this.snapshot, loading: false, error: 'DSH connection RPC is unavailable' })\n return Promise.resolve(false)\n }\n\n this.publish({ ...this.snapshot, loading: true, error: null })\n const run = (async () => {\n try {\n const endpoint = force ? UPDATE_ENDPOINTS.checkUpdate : UPDATE_ENDPOINTS.getStatus\n const ttl = isCacheTtlMinutes(cacheTtlMinutes) ? cacheTtlMinutes : DEFAULT_CACHE_TTL_MINUTES\n const raw = await rpc.call(UPDATE_STATUS_CHANNEL, endpoint, force\n ? { force: true, channel, cacheTtlMinutes: ttl }\n : { channel, cacheTtlMinutes: ttl })\n if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) throw new Error('invalid update-status RPC response')\n const envelope = raw as { ok?: unknown; value?: unknown; error?: { message?: unknown } }\n if (envelope.ok !== true) throw new Error(typeof envelope.error?.message === 'string' ? envelope.error.message : 'update-status RPC failed')\n const status = updateStatusOf(envelope.value)\n if (status === undefined) throw new Error('invalid update-status payload')\n this.publish({ status, loading: false, error: null })\n return true\n } catch (error) {\n this.publish({ ...this.snapshot, loading: false, error: errorMessage(error) })\n return false\n }\n })()\n this.inFlight = run\n void run.finally(() => {\n if (this.inFlight === run) this.inFlight = undefined\n })\n return run\n }\n}\n\nexport interface PreferencesSnapshot {\n sidebarEnabled: boolean\n channel: ReleaseChannel\n cacheTtlMinutes: number\n writable: boolean\n status: 'loading' | 'ready' | 'unavailable'\n}\n\nconst INITIAL_PREFERENCES: PreferencesSnapshot = {\n sidebarEnabled: true,\n channel: 'latest',\n cacheTtlMinutes: DEFAULT_CACHE_TTL_MINUTES,\n writable: false,\n status: 'loading',\n}\n\nexport class PreferencesStore implements Observable {\n private snapshot: PreferencesSnapshot = INITIAL_PREFERENCES\n private readonly listeners = new Set<() => void>()\n private scope: SettingsScopeLike | undefined\n\n getSnapshot = (): PreferencesSnapshot => this.snapshot\n\n subscribe = (listener: () => void): (() => void) => {\n this.listeners.add(listener)\n return () => { this.listeners.delete(listener) }\n }\n\n attach(scope: SettingsScopeLike): () => void {\n this.scope = scope\n const sync = () => {\n const raw = scope.getSnapshot()\n const value = raw.value !== null && typeof raw.value === 'object' && !Array.isArray(raw.value)\n ? raw.value as Record\n : {}\n const status = raw.status === 'ready' || raw.status === 'unavailable' ? raw.status : 'loading'\n this.publish({\n sidebarEnabled: value.sidebarEnabled !== false,\n channel: isReleaseChannel(value.channel) ? value.channel : 'latest',\n cacheTtlMinutes: isCacheTtlMinutes(value.cacheTtlMinutes) ? value.cacheTtlMinutes : DEFAULT_CACHE_TTL_MINUTES,\n writable: raw.writable === true,\n status,\n })\n }\n sync()\n return scope.subscribe(sync)\n }\n\n setSidebarEnabled(enabled: boolean): void {\n const previous = this.snapshot\n this.publish({ ...previous, sidebarEnabled: enabled })\n const scope = this.scope\n if (scope === undefined || !previous.writable) return\n void scope.set('sidebarEnabled', enabled).catch(() => {\n // A rejected Host-backed write restores the authoritative scope snapshot.\n try {\n const raw = scope.getSnapshot()\n const value = raw.value !== null && typeof raw.value === 'object' && !Array.isArray(raw.value)\n ? raw.value as Record\n : {}\n this.publish({\n sidebarEnabled: value.sidebarEnabled !== false,\n channel: isReleaseChannel(value.channel) ? value.channel : 'latest',\n cacheTtlMinutes: isCacheTtlMinutes(value.cacheTtlMinutes) ? value.cacheTtlMinutes : DEFAULT_CACHE_TTL_MINUTES,\n writable: raw.writable === true,\n status: raw.status === 'ready' || raw.status === 'unavailable' ? raw.status : 'loading',\n })\n } catch {\n this.publish(previous)\n }\n })\n }\n\n setChannel(channel: ReleaseChannel): void {\n const previous = this.snapshot\n this.publish({ ...previous, channel })\n const scope = this.scope\n if (scope === undefined || !previous.writable) return\n void scope.set('channel', channel).catch(() => { this.publish(previous) })\n }\n\n setCacheTtlMinutes(cacheTtlMinutes: number): void {\n if (!isCacheTtlMinutes(cacheTtlMinutes)) return\n const previous = this.snapshot\n this.publish({ ...previous, cacheTtlMinutes })\n const scope = this.scope\n if (scope === undefined || !previous.writable) return\n void scope.set('cacheTtlMinutes', cacheTtlMinutes).catch(() => { this.publish(previous) })\n }\n\n private publish(next: PreferencesSnapshot): void {\n const previous = this.snapshot\n if (previous.sidebarEnabled === next.sidebarEnabled && previous.channel === next.channel\n && previous.cacheTtlMinutes === next.cacheTtlMinutes\n && previous.writable === next.writable && previous.status === next.status) return\n this.snapshot = next\n for (const listener of this.listeners) listener()\n }\n}\n\nexport type PanelOrigin = 'brand' | 'rail'\n\nexport interface PanelSnapshot {\n open: boolean\n origin: PanelOrigin\n}\n\n/** Open state also retains its trigger, so the desktop card sits beside it. */\nexport class PanelStore implements Observable {\n private snapshot: PanelSnapshot = { open: false, origin: 'brand' }\n private readonly listeners = new Set<() => void>()\n\n getSnapshot = (): PanelSnapshot => this.snapshot\n\n subscribe = (listener: () => void): (() => void) => {\n this.listeners.add(listener)\n return () => { this.listeners.delete(listener) }\n }\n\n toggle(origin: PanelOrigin): void {\n const open = !(this.snapshot.open && this.snapshot.origin === origin)\n this.set({ open, origin })\n }\n\n close(): void { this.set({ ...this.snapshot, open: false }) }\n\n private set(next: PanelSnapshot): void {\n if (this.snapshot.open === next.open && this.snapshot.origin === next.origin) return\n this.snapshot = next\n for (const listener of this.listeners) listener()\n }\n}\n\n/**\n * The one settings namespace, which on DSH 0.1.7 IS the Loader entry id in\n * `cordis.patch.yml`: the Host half's volatile `Config` fields are projected as\n * that entry's form, and the browser half addresses the same id through\n * `ctx.configForms.get(...)`.\n */\nexport const SETTINGS_NAMESPACE = PLUGIN_ID\n","/**\n * dsh-update-status — direct Host settings channel (non-loopback pages).\n *\n * WHY THIS EXISTS\n *\n * DSH's settings Client keeps Host persistence disabled on any page whose\n * location is not a loopback authority. `ctx.configForms.get(entryId)` — the\n * DSH 0.1.7 successor of `settingsScope.bind()` — then answers\n * `status: 'unavailable'` immediately and NEVER sends `settings.describe`\n * (`@deepseek-ai/dsh-client-ui-settings`: the persistence mode resolves from\n * `remote.$host.isLoopback` and pins a remote page to `memory`; every row it\n * backs is inert even though Connection authentication covers the API).\n *\n * The decision itself is one line in the official client:\n *\n * const persistence = ctx.remote.$host.isLoopback ? \"host\" : \"memory\"\n *\n * and `isLoopback` is true only for `localhost` / `[::1]` / `127.0.0.0/8` pages\n * (or a transport that declares `ownsHost`). A LAN page reached through a\n * forwarding plugin is therefore non-loopback even though the API answers and\n * the page is authenticated.\n *\n * This plugin's preferences — the sidebar toggle, the followed release channel\n * and the cache policy — live in the Host settings entry `dsh-update-status`, so\n * without a channel they all silently fall back to defaults on a LAN page (the\n * host's configured `channel` would be ignored, silently switching the followed\n * release line). This module speaks the SAME public Remote the official form\n * speaks (`settings.describe` / `settings.mutate`, the generated Typert face\n * mounted by `@deepseek-ai/dsh-api-remotes`) and derives the same per-namespace\n * snapshot shape, so the ONE shared Host document keeps serving every device.\n *\n * This channel is opened ONLY when the official form reports `unavailable`\n * (see `settingsChannel.ts`), so loopback pages keep the official path and pay\n * no extra wire read. Everything here is guarded: an absent Remote, a refused\n * read, or a malformed answer leaves the preferences exactly as unavailable as\n * they were before this channel existed.\n */\nimport type { SettingsPathOpLike, SettingsScopeLike, SettingsScopeSnapshotLike } from './scopeFaces.ts'\n\n/** One namespace row of a `settings.describe` answer (the fields consumed here). */\nexport interface SettingsNamespaceViewLike {\n readonly ns: string\n readonly value: unknown\n readonly base?: unknown\n readonly user?: unknown\n readonly revision: number\n}\n\n/** The `settings.describe` answer body. */\nexport interface SettingsDescribeViewLike {\n readonly writable: boolean\n readonly hasDocument?: boolean\n readonly namespaces: readonly SettingsNamespaceViewLike[]\n}\n\n/** The Remote envelope every generated client call answers with. */\nexport type SettingsRemoteOutcome =\n | { readonly ok: true; readonly value: T }\n | { readonly ok: false; readonly error?: { readonly code?: string; readonly message?: string } }\n\n/** The `remote.settings` namespace face (structural; never imported at runtime). */\nexport interface SettingsRemoteLike {\n describe(): Promise>\n mutate(\n namespace: string,\n ops: readonly SettingsPathOpLike[],\n expectedRevision?: number,\n ): Promise>\n}\n\n/** A refused namespace write; `code` keeps the Host's own diagnosis. */\nexport class SettingsWriteFailure extends Error {\n readonly code: string\n\n constructor(code: string, message?: string) {\n super(message === undefined || message === '' ? code : message)\n this.name = 'SettingsWriteFailure'\n this.code = code\n }\n}\n\nfunction isRecord(value: unknown): value is Record {\n return typeof value === 'object' && value !== null\n}\n\n/**\n * Guard: does this value look like the `remote.settings` namespace face?\n * Applied to a bare service object (`ctx.get('remote.settings')` or a payload\n * member), and it never calls anything.\n */\nexport function settingsRemoteFace(value: unknown): SettingsRemoteLike | undefined {\n if (!isRecord(value)) return undefined\n if (typeof value.describe !== 'function' || typeof value.mutate !== 'function') return undefined\n return value as unknown as SettingsRemoteLike\n}\n\n/**\n * Guard: read the `remote.settings` face off an injection payload.\n *\n * The payload a `ctx.inject(['remote.settings'], …)` callback receives exposes\n * the LITERAL service key. Reading the dotted PARENT off that injected context\n * (`payload.remote`) throws `cannot get property \"remote\" without inject` — a\n * throw that used to abort a whole wiring callback on a LAN page. So the literal\n * key is read FIRST and every read is contained; the nested\n * (`payload.remote.settings`) shape is only a fallback for a plain object\n * payload. Both absence and refusal answer `undefined`.\n */\nexport function settingsRemoteOf(raw: unknown): SettingsRemoteLike | undefined {\n if (!isRecord(raw)) return undefined\n try {\n const direct = settingsRemoteFace((raw as Record)['remote.settings'])\n if (direct !== undefined) return direct\n } catch {\n // Injected-context proxy refusal: fall through to the nested shape.\n }\n try {\n const remote = (raw as { remote?: unknown }).remote\n if (isRecord(remote)) return settingsRemoteFace(remote.settings)\n } catch {\n // Same refusal: no face is readable from this payload.\n }\n return undefined\n}\n\n/**\n * Guard: subscribe to the Host's settings-document invalidation on the Remote\n * service object (`remote.$on`). Returns undefined when the service exposes no\n * event seam — the preferences then refresh on reconnect only.\n */\nexport function settingsInvalidationsOf(service: unknown): ((listener: () => void) => () => void) | undefined {\n if (!isRecord(service)) return undefined\n const on = service.$on\n if (typeof on !== 'function') return undefined\n return (listener) => {\n const dispose = (on as (event: string, callback: () => void) => unknown)\n .call(service, 'settings/document-updated', listener)\n return typeof dispose === 'function' ? dispose as () => void : () => {}\n }\n}\n\n/** The channel consumers subscribe to, plus the refresh/dispose seams the client wires. */\nexport interface HostDirectScope extends SettingsScopeLike {\n /** Re-read the Host document; resolves after the snapshot reflects the answer. */\n load(): Promise\n /** Drop every listener and stop accepting work. */\n dispose(): void\n /** Last Host error message (diagnostics only; never rendered as plugin state). */\n lastError(): string | undefined\n}\n\nfunction cloneOps(ops: readonly SettingsPathOpLike[]): SettingsPathOpLike[] {\n try {\n return structuredClone(ops) as SettingsPathOpLike[]\n } catch {\n // structuredClone absent/refusing the shape: copy by hand (the op payload is\n // plain JSON by contract, so a shallow structural copy is equivalent).\n return ops.map(op => (op.op === 'set'\n ? { op: 'set' as const, path: [...op.path], value: op.value }\n : { op: 'unset' as const, path: [...op.path] }))\n }\n}\n\nfunction messageOf(error: unknown): string {\n return error instanceof Error ? error.message : String(error)\n}\n\nfunction sameSnapshot(a: SettingsScopeSnapshotLike, b: SettingsScopeSnapshotLike): boolean {\n return a.status === b.status\n && a.revision === b.revision\n && a.writable === b.writable\n && a.mode === b.mode\n && a.value === b.value\n && a.base === b.base\n && a.user === b.user\n}\n\nfunction readySnapshot(\n row: SettingsNamespaceViewLike,\n writable: boolean,\n): SettingsScopeSnapshotLike {\n return {\n status: 'ready',\n // The raw section crosses this boundary untouched: PreferencesStore is the\n // plugin's own strict judge and reports a hand-edited or future-schema\n // document truthfully instead of silently dropping it.\n value: row.value,\n base: row.base,\n user: row.user,\n revision: row.revision,\n writable,\n mode: 'host',\n }\n}\n\nfunction unavailableSnapshot(writable: boolean): SettingsScopeSnapshotLike {\n return {\n status: 'unavailable',\n value: undefined,\n base: undefined,\n user: undefined,\n revision: undefined,\n writable,\n mode: 'host',\n }\n}\n\n/**\n * Build the direct Host settings scope for one namespace.\n *\n * Snapshot semantics mirror the official per-namespace derivation: a namespace\n * row answers `ready` with the resolved section, its `base`/`user` layers and its\n * revision fence, and the document's `writable` decides editability. A row the\n * Host does not serve answers `unavailable`; a refused read keeps the last\n * confirmed document and reports `unavailable` only while it has never held one.\n *\n * Writes are serialized in issue order, fenced by the latest known revision and\n * REJECT with {@link SettingsWriteFailure} on a Host refusal — that rejection is\n * what makes the settings page surface a conflict instead of pretending the edit\n * landed (the official scope resolves on `!ok` after its recovery read).\n *\n * @param remote - the `remote.settings` face of the page's Remote service.\n * @param namespace - the settings namespace this scope derives from.\n * @returns the scope plus its load/dispose seams.\n */\nexport function createHostDirectScope(remote: SettingsRemoteLike, namespace: string): HostDirectScope {\n let snapshot: SettingsScopeSnapshotLike = {\n status: 'loading',\n value: undefined,\n base: undefined,\n user: undefined,\n revision: undefined,\n writable: false,\n mode: 'host',\n }\n let held: SettingsScopeSnapshotLike | undefined\n let error: string | undefined\n let disposed = false\n let tail: Promise = Promise.resolve()\n const listeners = new Set<() => void>()\n\n const publish = (next: SettingsScopeSnapshotLike): void => {\n if (disposed || sameSnapshot(next, snapshot)) return\n snapshot = next\n for (const listener of [...listeners]) {\n try {\n listener()\n } catch {\n // A broken listener must never break the channel.\n }\n }\n }\n\n const adopt = (next: SettingsScopeSnapshotLike): void => {\n if (next.status === 'ready') held = next\n publish(next)\n }\n\n async function load(): Promise {\n if (disposed) return\n let outcome: SettingsRemoteOutcome\n try {\n outcome = await remote.describe()\n } catch (caught) {\n outcome = { ok: false, error: { message: messageOf(caught) } }\n }\n if (disposed) return\n if (!outcome.ok) {\n error = outcome.error?.message ?? outcome.error?.code ?? 'settings/describe-failed'\n // A refused read never invents state: the last confirmed document stays\n // authoritative; without one the channel is unavailable.\n publish(held ?? unavailableSnapshot(snapshot.writable))\n return\n }\n error = undefined\n const view = outcome.value\n const writable = view.writable === true\n const row = Array.isArray(view.namespaces)\n ? view.namespaces.find(candidate => candidate.ns === namespace)\n : undefined\n if (row === undefined || typeof row.revision !== 'number') {\n publish(unavailableSnapshot(writable))\n return\n }\n adopt(readySnapshot(row, writable))\n }\n\n /** Fold one write answer's namespace row in without a second wire read. */\n function fold(row: SettingsNamespaceViewLike, writable: boolean): void {\n if (typeof row.revision !== 'number') return\n adopt(readySnapshot(row, writable))\n }\n\n function mutate(ops: readonly SettingsPathOpLike[], expectedRevision?: number): Promise {\n const owned = cloneOps(ops)\n const run = tail.then(async () => {\n if (disposed) throw new SettingsWriteFailure('settings/unavailable')\n const revision = expectedRevision ?? snapshot.revision\n let outcome: SettingsRemoteOutcome\n try {\n outcome = revision === undefined\n ? await remote.mutate(namespace, owned)\n : await remote.mutate(namespace, owned, revision)\n } catch (caught) {\n await load()\n throw new SettingsWriteFailure('settings/unreachable', messageOf(caught))\n }\n if (outcome.ok) {\n // A successful write proves the document accepts writes on this page.\n fold(outcome.value, true)\n return\n }\n await load()\n throw new SettingsWriteFailure(\n outcome.error?.code ?? 'settings/unknown',\n outcome.error?.message,\n )\n })\n // The queue never poisons itself: the caller sees the rejection, the next\n // queued write still runs.\n tail = run.then(() => undefined, () => undefined)\n return run\n }\n\n return {\n getSnapshot: () => snapshot,\n subscribe(listener) {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n },\n // The official scope implements `set` exactly this way; the revision fence\n // falls back to the latest known one, same as the official pendingRevision.\n set: (field, value) => mutate([{ op: 'set', path: [field], value }]),\n load,\n dispose() {\n disposed = true\n listeners.clear()\n },\n lastError: () => error,\n }\n}\n","/**\n * dsh-update-status — official settings-form adapter.\n *\n * DSH 0.1.7 removed the per-namespace `settingsScope` service this plugin's\n * client half was built on. Its successor is `ctx.configForms`\n * (`@deepseek-ai/dsh-client-ui-settings`): one shared `settings.describe`\n * mirror per browser plus a per-entry write queue, reached as\n * `ctx.configForms.get(entryId)` for the entry the plugin's Host half owns.\n * That form already answers the exact facts this plugin's own scope contract\n * needs — status, value, base, user, revision, writable, mode — so this module\n * only projects it onto {@link SettingsScopeLike} and leaves the existing\n * channel selection (`settingsChannel.ts`) in charge of the LAN fallback.\n *\n * Two deliberate translations:\n *\n * - A refused write (`set` answers `false`) becomes a REJECTION. The settings\n * page must surface a conflict instead of pretending the edit landed, which\n * is the same contract the direct Host channel already obeys\n * (`hostDirectScope.ts`). A transport fault rejects too.\n * - `mode` is what separates the two pages: DSH pins it to `memory` on a\n * non-loopback page, where the form is terminally `unavailable` and never\n * writes — exactly the state that hands the preferences to the direct Host\n * Remote so a phone still edits the one shared entry.\n *\n * The service is deliberately read structurally: the client bundle purity gate\n * forbids cross-plugin value imports, and the runtime object belongs to the\n * user's harness.\n */\nimport type { SettingsScopeLike, SettingsScopeSnapshotLike } from './scopeFaces.ts'\nimport { SettingsWriteFailure } from './hostDirectScope.ts'\n\n/** One form snapshot as `ConfigForm.getSnapshot()` answers it. */\nexport interface ConfigFormSnapshotLike {\n status: 'loading' | 'ready' | 'unavailable'\n value: T | undefined\n base: unknown\n user: unknown\n revision: number | undefined\n writable: boolean\n mode: 'host' | 'memory'\n}\n\n/** The subset of `ConfigForm` this adapter reads. */\nexport interface ConfigFormLike {\n getSnapshot(): ConfigFormSnapshotLike\n subscribe(listener: () => void): () => void\n set(field: string, value: unknown): Promise\n}\n\n/** The `ctx.configForms` binder face. */\nexport interface ConfigFormsFace {\n get(entryId: string): ConfigFormLike\n}\n\n/**\n * Guard: read the `configForms` face off an injection payload.\n *\n * The literal service key is read inside a `try`, because an injected payload\n * proxy can refuse a member read (the failure this plugin already hit on the\n * dotted `remote.settings` parent). Absence and refusal both answer\n * `undefined`, which leaves the channel on its direct Host path.\n *\n * @param raw - the payload a `ctx.inject(['configForms'], …)` callback received.\n * @returns the binder face, or undefined when this page has no settings forms.\n */\nexport function configFormsOf(raw: unknown): ConfigFormsFace | undefined {\n if (raw === null || typeof raw !== 'object') return undefined\n let face: unknown\n try {\n face = (raw as Record)['configForms']\n } catch {\n return undefined\n }\n if (face === null || typeof face !== 'object') return undefined\n if (typeof (face as { get?: unknown }).get !== 'function') return undefined\n return face as ConfigFormsFace\n}\n\n/**\n * Project one official config form onto the plugin's settings-scope contract.\n *\n * @param form - the shared form for the plugin's own Loader entry id.\n * @returns a scope the preferences store and the channel selection can use as-is.\n */\nexport function configFormScope(form: ConfigFormLike): SettingsScopeLike {\n return {\n getSnapshot(): SettingsScopeSnapshotLike {\n const snapshot = form.getSnapshot()\n return {\n status: snapshot.status,\n value: snapshot.value as SettingsScopeSnapshotLike['value'],\n base: snapshot.base,\n user: snapshot.user,\n revision: snapshot.revision,\n writable: snapshot.writable,\n mode: snapshot.mode,\n }\n },\n subscribe(listener) {\n return form.subscribe(listener)\n },\n async set(field, value) {\n let accepted: boolean\n try {\n accepted = await form.set(field, value)\n } catch (error) {\n // Transport fault: the write did not land and the caller must not\n // pretend it did (the recovery read belongs to the form itself).\n throw new SettingsWriteFailure(\n 'settings/unreachable',\n error instanceof Error ? error.message : String(error),\n )\n }\n if (!accepted) {\n // Refused or skipped (memory mode, stale revision, Host validation):\n // the form has already re-read the authoritative value.\n throw new SettingsWriteFailure('settings/conflict')\n }\n },\n }\n}\n","/**\n * dsh-update-status — settings source selection.\n *\n * The plugin's ONE storage contract is the Host settings entry\n * `dsh-update-status`. Two client channels can serve it:\n *\n * 1. the official `ctx.configForms` form (loopback pages) — the DSH 0.1.7\n * successor of the removed `settingsScope` service, and\n * 2. the direct Host channel (`hostDirectScope.ts`), which exists exactly\n * because the official form is deliberately inert on a non-loopback page —\n * the LAN page a phone uses.\n *\n * This module picks between them without ever guessing:\n *\n * - The official form wins whenever it is not `unavailable`, so a loopback page\n * keeps the official semantics (the one shared describe mirror, the official\n * write queue and revision fences) and pays NO extra wire read.\n * - Only when the official form reports `unavailable` (the documented\n * non-loopback degradation: it is pinned to `memory` mode there and never\n * crosses the wire) is the direct channel opened, lazily and once.\n * - With neither source the channel stays `loading`, i.e. exactly the state this\n * plugin had before the direct channel existed.\n */\nimport type { SettingsScopeLike, SettingsScopeSnapshotLike } from './scopeFaces.ts'\n\n/** Snapshot used while no channel has answered yet. */\nconst LOADING_SNAPSHOT: SettingsScopeSnapshotLike = Object.freeze({\n status: 'loading',\n value: undefined,\n base: undefined,\n user: undefined,\n revision: undefined,\n writable: false,\n mode: 'host',\n}) as SettingsScopeSnapshotLike\n\nexport interface SettingsChannelOptions {\n /**\n * Build the direct Host channel. Called at most once, and only when the\n * official scope cannot serve this page. Returning `undefined` (the Remote\n * service is not composed) leaves the channel on the official/unavailable\n * path; a later {@link SettingsChannel.refresh} retries.\n */\n openDirect(): SettingsScopeLike | undefined\n}\n\nexport interface SettingsChannel extends SettingsScopeLike {\n /** Point the channel at the official form once the service answers. */\n setOfficial(scope: SettingsScopeLike | undefined): void\n /** Re-evaluate the selection (e.g. the Remote service just arrived). */\n refresh(): void\n /** Ask the active source for a fresh read (no-op for the official form). */\n reload(): void\n dispose(): void\n}\n\nfunction sameSnapshot(a: SettingsScopeSnapshotLike, b: SettingsScopeSnapshotLike): boolean {\n return a.status === b.status\n && a.revision === b.revision\n && a.writable === b.writable\n && a.mode === b.mode\n && a.value === b.value\n && a.base === b.base\n && a.user === b.user\n}\n\n/** The load seam a direct channel exposes (the official scope refreshes itself). */\ntype LoadableScope = SettingsScopeLike & { load?: () => Promise }\n\n/** Ask a channel for its first/next read; a refusal surfaces as a snapshot, never a throw. */\nfunction loadSource(source: SettingsScopeLike | undefined): Promise {\n const loadable = source as LoadableScope | undefined\n if (loadable === undefined || typeof loadable.load !== 'function') return Promise.resolve()\n return Promise.resolve(loadable.load()).then(() => undefined, () => undefined)\n}\n\nexport function createSettingsChannel(options: SettingsChannelOptions): SettingsChannel {\n let official: SettingsScopeLike | undefined\n let direct: SettingsScopeLike | undefined\n let active: SettingsScopeLike | undefined\n let snapshot: SettingsScopeSnapshotLike = LOADING_SNAPSHOT\n let disposed = false\n const disposers: Array<() => void> = []\n const listeners = new Set<() => void>()\n\n const notify = (): void => {\n for (const listener of [...listeners]) {\n try {\n listener()\n } catch {\n // A broken listener must never break the channel.\n }\n }\n }\n\n const publish = (next: SettingsScopeSnapshotLike): void => {\n if (disposed || sameSnapshot(next, snapshot)) return\n snapshot = next\n notify()\n }\n\n const select = (): SettingsScopeLike | undefined => {\n // The official scope is authoritative unless it reports the documented\n // non-loopback degradation.\n if (official !== undefined && official.getSnapshot().status !== 'unavailable') return official\n if (direct === undefined) {\n try {\n direct = options.openDirect()\n } catch {\n direct = undefined\n }\n if (direct !== undefined) {\n try {\n disposers.push(direct.subscribe(() => {\n if (active === direct) publish(direct!.getSnapshot())\n }))\n } catch {\n // An unsubscribable direct channel is still readable synchronously.\n }\n // The direct channel has no owner to `ensure()` it (the official scope is\n // ensured by its own plugin), so the initial read is issued here.\n void loadSource(direct)\n }\n }\n return direct ?? official\n }\n\n const reselect = (): void => {\n if (disposed) return\n const next = select()\n if (next !== active) {\n active = next\n publish(next?.getSnapshot() ?? LOADING_SNAPSHOT)\n return\n }\n // Same source: forward its current snapshot (a change must have come from it).\n publish(next?.getSnapshot() ?? LOADING_SNAPSHOT)\n }\n\n return {\n getSnapshot: () => snapshot,\n subscribe(listener) {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n },\n set(field, value) {\n const source = active\n if (source === undefined) {\n return Promise.reject(new Error('dsh-update-status: settings channel is unavailable'))\n }\n return source.set(field, value)\n },\n setOfficial(scope) {\n if (disposed) return\n official = scope\n if (scope !== undefined) {\n try {\n disposers.push(scope.subscribe(() => {\n reselect()\n }))\n } catch {\n // Guarded read path: selection still happens synchronously below.\n }\n }\n reselect()\n },\n refresh: reselect,\n reload() {\n if (active === undefined || active === official) return\n void loadSource(active)\n },\n dispose() {\n disposed = true\n listeners.clear()\n for (const dispose of disposers.splice(0)) {\n try {\n dispose()\n } catch {\n // Contained.\n }\n }\n const disposable = active as (SettingsScopeLike & { dispose?: () => void }) | undefined\n if (disposable !== undefined && typeof disposable.dispose === 'function') {\n try {\n disposable.dispose()\n } catch {\n // Contained.\n }\n }\n },\n }\n}\n","/** Plugin-owned CSS only; no shell DOM selection or official SVG manipulation. */\n\nexport const UPDATE_STATUS_CSS = `\n.dus-brand-name{align-items:center;gap:6px;height:24px;max-width:100%;min-width:0;display:inline-flex}\n.dus-brand-deepseek{font-size:14px;font-weight:650;letter-spacing:-.015em;line-height:24px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n/* The chip is a NEUTRAL SECOND-LEVEL SURFACE, never a high-contrast pill. It used\n to be label-primary + label-primary-inverted, which is a near-white box with dark\n text on the dark shell — a white frame that swallows the amber halo and fights the\n warn colour. --dsw-alias-button-floating-hover is the one palette surface that is\n grey in BOTH themes (#f1f3f5 light / #353638 dark), matching the reference: a light\n grey chip on the light shell, a dark grey chip with white text on the dark one. The\n sibling button-floating-fill is pure white in light mode and changes nothing there.\n Text stays label-primary (#0f1115 light / #f9fafb dark), so it is never low-contrast.\n Every colour here is a theme token, so the chip follows whatever appearance DSH\n resolves — light, dark, or system-follows-the-OS — with no plugin-side detection,\n no media query and no per-theme branch to keep in sync. */\n.dus-badge{--dus-badge-surface:var(--dsw-alias-button-floating-hover,#f1f3f5);align-items:center;background:var(--dus-badge-surface);border:0;border-radius:4px;color:var(--dsw-alias-label-primary,#0f1115);cursor:pointer;display:inline-flex;flex:none;font-family:var(--ds-font-family-code,var(--dsw-font-family-mono,ui-monospace,SFMono-Regular,Menlo,monospace));font-size:10px;font-variant-numeric:tabular-nums;font-weight:600;gap:4px;height:16px;line-height:16px;max-width:140px;outline:none;padding:0 6px;touch-action:manipulation;user-select:none;white-space:nowrap}\n.dus-badge:focus-visible,.dus-footer-button:focus-visible,.dus-action:focus-visible,.dus-close:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}\n/* Hover has to read as \"raised\" in BOTH themes. brightness() only ever brightens, and on\n the light shell it turns the grey chip pure white — the chip all but disappears. Mixing\n a little of the theme's own label colour into the surface darkens it in light mode and\n lightens it in dark mode, so one declaration stays correct in both. The brightness\n filter remains the fallback: an engine without color-mix drops only the second\n declaration, never the whole hover rule. */\n.dus-badge:hover{filter:brightness(1.08)}\n@supports (background:color-mix(in srgb,red 50%,transparent)){.dus-badge:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary) 8%,var(--dus-badge-surface));filter:none}}\n/* The failure state is the one deliberate repaint, and it follows the shell's own\n badge pattern (brokenBadge): error fill with a bg-layer-3 label, which is white\n text on the dark red of the light theme and dark text on the light red of the dark\n theme — readable in both, unlike the fixed white text it replaces. */\n.dus-badge[data-error=true]{background:var(--dsw-alias-state-error-primary,#dc2626);color:var(--dsw-alias-bg-layer-3,#fff)}\n.dus-badge-version{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.dus-dot{background:currentColor;border-radius:50%;display:inline-block;flex:0 0 auto;height:5px;opacity:.9;width:5px}\n/* An available update repaints NOTHING: the chip keeps its normal fill and only\n the amber dot breathes. The halo is animated with the dot so the glow grows\n and shrinks as one light, never as a separate ripple. The update state is\n declared AFTER the loading state, so a re-check on a known update keeps the\n amber glow instead of falling back to the neutral loading pulse.\n Each box-shadow is declared twice: a literal amber first for any engine\n without color-mix, then the halo mixed from the theme's own warn colour so the\n glow always matches the dot in both light and dark themes. */\n/* The dot carries the whole status. Three states, in precedence order:\n up to date = a plain green circle (success token, never the text colour, so a\n dark theme no longer paints a black dot that reads as \"off\"); checking = the\n neutral grey pulse; a pending update = the amber dot below, which is the only\n state allowed to breathe or glow.\n The up-to-date rule is declared BEFORE loading and update: equal specificity, so\n the later, louder states win if two ever land on the same dot. */\n.dus-dot[data-state=current]{background:var(--dsw-alias-state-success-primary,#22c55e);opacity:1}\n.dus-dot[data-loading=true]{animation:dus-pulse .9s ease-in-out infinite}\n.dus-dot[data-update=true]{--dus-update-color:var(--dsw-alias-state-warn-primary,#f59e0b);animation:dus-update-pulse 1.6s ease-in-out infinite;background:var(--dus-update-color);height:6px;opacity:1;width:6px}\n@keyframes dus-pulse{0%,100%{opacity:.45;transform:scale(.82)}50%{opacity:1;transform:scale(1.15)}}\n@keyframes dus-update-pulse{0%,100%{box-shadow:0 0 2px 0 rgba(245,158,11,.4);box-shadow:0 0 2px 0 color-mix(in srgb,var(--dus-update-color) 45%,transparent);transform:scale(.8)}50%{box-shadow:0 0 7px 2px rgba(245,158,11,.72);box-shadow:0 0 7px 2px color-mix(in srgb,var(--dus-update-color) 72%,transparent);transform:scale(1.15)}}\n@media (prefers-reduced-motion:reduce){.dus-dot[data-update=true],.dus-dot[data-loading=true]{animation:none}.dus-dot[data-update=true]{box-shadow:0 0 5px 1px rgba(245,158,11,.6);box-shadow:0 0 5px 1px color-mix(in srgb,var(--dus-update-color) 62%,transparent)}}\n.dus-footer-button{align-items:center;background:transparent;border:0;border-radius:10px;color:var(--dsw-alias-label-secondary);cursor:pointer;display:flex;height:44px;justify-content:center;min-height:44px;min-width:44px;padding:0;position:relative;touch-action:manipulation;width:44px}\n.dus-footer-button:hover{background:var(--dsw-alias-button-floating-hover);color:var(--dsw-alias-label-primary)}\n.dus-footer-icon{font-size:18px;line-height:1}\n.dus-footer-dot{border:1.5px solid var(--dsw-specific-sidebar-fill);position:absolute;right:9px;top:9px}\n.dus-overlay-root{background:transparent;border:0;color:inherit;height:100dvh;inset:0;margin:0;max-height:none;max-width:none;padding:0;pointer-events:none;position:fixed;width:100vw}\n.dus-overlay-root::backdrop{background:transparent}\n.dus-backdrop{background:transparent;inset:0;pointer-events:auto;position:absolute}\n.dus-panel{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:14px;box-shadow:var(--dsw-elevation-panel);box-sizing:border-box;color:var(--dsw-alias-label-primary);max-height:min(640px,calc(100dvh - 32px - env(safe-area-inset-top) - env(safe-area-inset-bottom)));overflow:auto;padding:14px;pointer-events:auto;position:absolute;top:12px;width:min(390px,calc(100vw - 24px));z-index:1}\n.dus-panel[data-origin=brand]{left:min(292px,calc(100vw - 402px))}\n.dus-panel[data-origin=rail]{left:min(68px,calc(100vw - 402px))}\n.dus-panel-head{align-items:center;display:flex;gap:8px;justify-content:space-between;margin-bottom:12px}\n.dus-panel-title{font-size:14px;font-weight:650}\n.dus-close{align-items:center;background:transparent;border:0;border-radius:8px;color:inherit;cursor:pointer;display:inline-flex;font-size:20px;height:32px;justify-content:center;line-height:1;min-height:32px;min-width:32px;padding:0;touch-action:manipulation;width:32px}\n.dus-close:hover,.dus-action:hover{background:var(--dsw-alias-button-floating-hover)}\n.dus-cache-setting{border:1px solid var(--dsw-alias-border-l2);border-radius:9px;margin:0 0 12px;padding:9px}\n.dus-cache-field{align-items:center;display:flex;font-size:12px;font-weight:600;gap:10px;justify-content:space-between}\n.dus-cache-input-wrap{align-items:center;display:flex;gap:6px;font-size:11px;font-weight:400}\n.dus-cache-input{background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:7px;color:inherit;font:inherit;min-height:30px;padding:0 7px;text-align:right;width:72px}\n.dus-cache-input:focus{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}\n.dus-cache-input:disabled{cursor:not-allowed;opacity:.55}\n.dus-cache-hint{color:var(--dsw-alias-label-secondary);font-size:11px;line-height:1.4;margin:7px 0 0}\n.dus-state{border-radius:9px;color:var(--dsw-alias-label-primary);font-size:12px;line-height:1.45;margin:0 0 12px;padding:8px 9px}\n.dus-state[data-kind=update],.dus-state[data-kind=warning]{background:var(--dsw-alias-state-warn-tertiary)}\n.dus-state[data-kind=ok]{background:var(--dsw-alias-state-success-tertiary)}\n.dus-state[data-kind=error]{background:var(--dsw-alias-bg-layer-3)}\n.dus-metadata{display:grid;gap:9px;margin:0 0 12px}\n.dus-metadata-row{align-items:baseline;display:grid;gap:8px;grid-template-columns:minmax(0,1fr) auto}\n.dus-meta-label{color:var(--dsw-alias-label-secondary);font-size:12px}\n.dus-meta-value{font-family:var(--dsw-font-family-mono,ui-monospace,SFMono-Regular,Menlo,monospace);font-size:12px;max-width:210px;overflow-wrap:anywhere;text-align:right}\n.dus-meta-sub{color:var(--dsw-alias-label-secondary);font-size:11px;grid-column:1 / -1}\n.dus-warning{border-left:2px solid var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.45;margin:0 0 12px;padding-left:8px}\n.dus-actions{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 12px}\n.dus-action{background:var(--dsw-alias-button-floating-fill);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;color:inherit;cursor:pointer;font-size:12px;line-height:30px;min-height:32px;padding:0 10px;touch-action:manipulation}\n.dus-action:disabled{cursor:wait;opacity:.65}\n.dus-command-label{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;margin:0 0 6px}\n.dus-command{background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;display:block;font-family:var(--dsw-font-family-mono,ui-monospace,SFMono-Regular,Menlo,monospace);font-size:11px;line-height:1.5;margin:0;overflow-wrap:anywhere;padding:9px;tab-size:2;user-select:text;white-space:pre-wrap}\n.dus-command:focus{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}\n.dus-note{color:var(--dsw-alias-label-secondary);font-size:11px;line-height:1.45;margin:8px 0 0}\n.dus-copy-message{color:var(--dsw-alias-state-business-primary);font-size:11px;margin:7px 0 0}\n.dus-settings{display:grid;gap:14px;max-width:640px;padding:4px 0}\n.dus-settings-heading{font-size:16px;font-weight:650;margin:0}\n.dus-settings-card{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:12px}\n.dus-settings-toggle{align-items:flex-start;cursor:pointer;display:flex;gap:10px;font-size:13px;line-height:1.4}\n.dus-settings-toggle input{accent-color:var(--dsw-alias-state-business-primary);height:18px;margin:0;min-height:18px;min-width:18px;width:18px}\n.dus-settings-field{display:grid;font-size:13px;font-weight:600;gap:8px}\n.dus-channel-select{background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;color:inherit;font:inherit;min-height:40px;padding:0 10px;width:100%}\n.dus-channel-list{border-top:1px solid var(--dsw-alias-border-l2);margin:12px 0 0;padding-top:2px}\n.dus-channel-row{align-items:center;display:grid;font-size:11px;gap:8px;grid-template-columns:minmax(0,1fr) auto;line-height:1.4;padding:6px 0}\n.dus-channel-row[data-selected=true]{color:var(--dsw-alias-state-business-primary)}\n.dus-channel-row code{font-size:11px}\n.dus-channel-compat{color:var(--dsw-alias-label-secondary)}\n.dus-channel-action{background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;color:inherit;cursor:pointer;font-size:11px;justify-self:end;min-height:32px;padding:0 9px}\n.dus-channel-action:disabled{cursor:default;opacity:.55}\n.dus-channel-action:not(:disabled):hover{background:var(--dsw-alias-button-floating-hover)}\n.dus-channel-compat[data-compatibility=verified]{color:var(--dsw-alias-state-success-primary)}\n.dus-channel-compat[data-compatibility=incompatible]{color:var(--dsw-alias-state-error-primary)}\n.dus-settings-hint{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.45;margin:8px 0 0}\n@media (max-width:640px),(hover:none) and (pointer:coarse){.dus-panel[data-origin]{border-bottom:0;border-bottom-left-radius:0;border-bottom-right-radius:0;bottom:0;left:0;right:0;max-height:min(78dvh,calc(100dvh - env(safe-area-inset-top)));padding:16px max(16px,env(safe-area-inset-right)) max(16px,env(safe-area-inset-bottom)) max(16px,env(safe-area-inset-left));position:fixed;top:auto;width:100vw}.dus-footer-button{height:48px;min-height:48px;min-width:48px;width:48px}.dus-action{min-height:40px;line-height:38px}.dus-close{height:40px;min-height:40px;min-width:40px;width:40px}}\n`\n","/**\n * DSH Web client half. It shadows only `sidebar.brand.name` with a compact\n * name + version chip that fits the 24px brand row, leaves the official fish\n * (`sidebar.brand.mark`) untouched, and adds a collapsed-rail fallback at\n * `sidebar.footer.action`. The detail panel is an additive `shell.overlay`,\n * never a chat-area floating widget.\n *\n * The status read and the detail panel are NOT restricted to a loopback page.\n * DSH disables Host settings *persistence* on a non-loopback page, but the\n * Connection RPC stays authenticated and reachable there — see\n * settings/settingsChannel.ts for the one place the loopback distinction still\n * matters.\n *\n * Preferences live in the Host-side `dsh-update-status` settings entry — the\n * plugin's own Loader entry, whose volatile `Config` fields DSH 0.1.7 projects\n * as that entry's form. Two client channels can serve it: the official\n * `ctx.configForms.get(entryId)` form (the successor of the removed\n * `settingsScope` service) and, on the non-loopback pages where that form is\n * deliberately inert, a direct Host channel over `remote.settings`. See\n * settingsChannel.ts.\n */\n\nimport type { ClientContext, ConnectionClient } from './contract.ts'\nimport { BrandName, FooterAction, type SharedUi, UpdatePanel, UpdateSettings } from './components.tsx'\nimport { SETTINGS_NAMESPACE, PanelStore, PreferencesStore, StatusStore } from './stores.ts'\nimport { configFormScope, configFormsOf } from './settings/configFormScope.ts'\nimport {\n createHostDirectScope,\n settingsInvalidationsOf,\n settingsRemoteFace,\n settingsRemoteOf,\n type SettingsRemoteLike,\n} from './settings/hostDirectScope.ts'\nimport { createSettingsChannel } from './settings/settingsChannel.ts'\nimport { t } from './i18n.ts'\nimport { UPDATE_STATUS_CSS } from './styles.ts'\n\nconst PLUGIN_ID = 'dsh-update-status'\nconst CSS_TAG_ID = `${PLUGIN_ID}/styles`\n\nfunction installStyles(): () => void {\n if (typeof document === 'undefined') return () => {}\n const existing = document.querySelector(`style[data-plugin-css=\"${CSS_TAG_ID}\"]`)\n if (existing !== null) return () => {}\n const tag = document.createElement('style')\n tag.dataset.plugin = PLUGIN_ID\n tag.dataset.pluginCss = CSS_TAG_ID\n tag.textContent = UPDATE_STATUS_CSS\n document.head.appendChild(tag)\n return () => {\n const live = document.querySelector(`style[data-plugin-css=\"${CSS_TAG_ID}\"]`)\n if (live?.parentNode !== undefined && live?.parentNode !== null) live.parentNode.removeChild(live)\n }\n}\n\nfunction connectionOf(ctx: ClientContext): ConnectionClient {\n try {\n const candidate = ctx.get('connection')\n return candidate !== null && typeof candidate === 'object' ? candidate as ConnectionClient : {}\n } catch {\n return {}\n }\n}\n\nfunction apply(ctx: ClientContext): void {\n const connection = connectionOf(ctx)\n const status = new StatusStore(connection)\n const preferences = new PreferencesStore()\n const panel = new PanelStore()\n const ui: SharedUi = { status, preferences, panel }\n\n const disposers: Array<() => void> = []\n let stopped = false\n /** Own a subscription raised from an injection callback; release after teardown at once. */\n const own = (dispose: unknown): void => {\n if (typeof dispose !== 'function') return\n if (stopped) {\n try {\n (dispose as () => void)()\n } catch {\n // Contained.\n }\n return\n }\n disposers.push(dispose as () => void)\n }\n\n // The plugin's ONE storage contract is the Host settings entry\n // `dsh-update-status` (the plugin's own Loader entry id), but two client\n // channels can serve it: the official `configForms` form (loopback pages) and\n // — only when that form reports the documented non-loopback degradation — a\n // direct Host channel over the same public Remote. A LAN page is exactly that\n // non-loopback case; without the direct channel every preference silently\n // falls back to its default there. See settingsChannel.ts.\n let remoteSettings: SettingsRemoteLike | undefined\n /** Resolve the direct channel's Remote face; `ctx.get` covers a payload we could not read. */\n const directRemote = (): SettingsRemoteLike | undefined => {\n if (remoteSettings !== undefined) return remoteSettings\n try {\n remoteSettings = settingsRemoteFace(ctx.get('remote.settings'))\n } catch {\n // The dotted service key is not readable on this context.\n }\n return remoteSettings\n }\n const channel = createSettingsChannel({\n openDirect: () => {\n const remote = directRemote()\n return remote === undefined ? undefined : createHostDirectScope(remote, SETTINGS_NAMESPACE)\n },\n })\n\n ctx.effect(() => {\n const disposeStyles = installStyles()\n const detach = preferences.attach(channel)\n // Changing cache policy deliberately does not issue a network request. Its\n // value is sent on the next ordinary status read or manual check.\n let selected = status.getSnapshot().status?.channel ?? 'latest'\n const syncPreferences = () => {\n const next = preferences.getSnapshot()\n if (next.channel !== selected) {\n selected = next.channel\n void status.selectChannel(selected, next.cacheTtlMinutes)\n }\n }\n syncPreferences()\n const unsubscribe = preferences.subscribe(syncPreferences)\n // Every mount reads the Host's cached status, whichever page it is on; the\n // transport is authenticated and the Host route is the plugin's own.\n void status.load(preferences.getSnapshot().cacheTtlMinutes)\n return () => {\n stopped = true\n unsubscribe()\n detach()\n for (const dispose of disposers.splice(0)) {\n try {\n dispose()\n } catch {\n // Contained.\n }\n }\n channel.dispose()\n status.stop()\n disposeStyles()\n }\n }, 'dsh-update-status: styles, settings channel and first status read')\n\n // Official seam first: it stays authoritative whenever it is not `unavailable`,\n // so a loopback page keeps the official semantics (one shared describe mirror,\n // the official write queue) and pays no extra wire read. DSH 0.1.7 replaced\n // the per-namespace `settingsScope` service with these entry-addressed forms,\n // keyed by the Loader entry id the Host Config half owns (SETTINGS_NAMESPACE).\n try {\n ctx.inject(['configForms'], (raw: unknown) => {\n try {\n const forms = configFormsOf(raw)\n if (forms === undefined) return\n channel.setOfficial(configFormScope(forms.get(SETTINGS_NAMESPACE)))\n } catch {\n // Unreadable settings seam: the preferences stay on their defaults.\n }\n })\n } catch {\n // No configForms seam on this host: the direct channel is the only hope.\n }\n\n // Direct Host channel. The Remote service can arrive before or after the\n // official scope, so `refresh()` re-evaluates the selection either way; the\n // document invalidation keeps this page in step with edits made elsewhere,\n // and a reconnect retries a read that was refused.\n //\n // Every read here is contained: the injected payload intentionally refuses the\n // dotted PARENT (`payload.remote` throws \"cannot get property … without\n // inject\"), so one unreadable member must never abort the whole wiring.\n try {\n ctx.inject(['remote.settings'], (raw: unknown) => {\n try {\n remoteSettings = settingsRemoteOf(raw) ?? remoteSettings\n channel.refresh()\n const invalidations = settingsInvalidationsOf(ctx.get('remote'))\n if (invalidations !== undefined) own(invalidations(() => channel.reload()))\n } catch {\n // No usable Remote seam: the direct channel stays closed and the official\n // scope keeps its verdict.\n }\n })\n } catch {\n // No Remote seam: non-loopback pages keep the honest unavailable state.\n }\n if (typeof ctx.on === 'function') {\n try {\n own(ctx.on('connection/reset', () => channel.reload()))\n } catch {\n // No lifecycle event seam on this host.\n }\n }\n\n // Single slot: a compact name + version chip shadows the official wordmark\n // (priority 0). Lowest priority renders; never touch sidebar.brand.mark.\n ctx.slots.inject('sidebar.brand.name', () => ctx.slots.register(\n { name: 'sidebar.brand.name', priority: -10 },\n () => BrandName({ ui }),\n ))\n\n // additive fallback; returns null in wide mode to avoid two visible badges.\n ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register(\n { name: 'sidebar.footer.action', id: 'dsh-update-status', order: 40 },\n props => FooterAction({ wide: props.wide, ui }),\n ))\n\n // additive frame overlay for click/tap panel and narrow-view bottom sheet.\n ctx.slots.inject('shell.overlay', () => ctx.slots.register(\n { name: 'shell.overlay', id: 'dsh-update-status', order: 40 },\n () => UpdatePanel({ ui }),\n ))\n\n // Independent settings section provides the required way to hide our entry.\n ctx.slots.inject('settings.section', () => ctx.slots.register(\n { name: 'settings.section', id: 'dsh-update-status', order: 80, label: () => t('settings.title') },\n () => UpdateSettings({ ui }),\n ))\n}\n\nmodule.exports = {\n name: PLUGIN_ID,\n inject: ['slots', 'connection'],\n apply,\n}\n"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"client.js","names":["PLUGIN_ID","PLUGIN_ID","sameSnapshot"],"sources":["../src/shared/channels.ts","../src/shared/types.ts","../src/shared/semver.ts","../src/shared/preview-guidance.ts","../src/shared/visual-state.ts","../src/client/i18n.ts","../src/client/react.ts","../src/client/components.tsx","../src/client/contract.ts","../src/client/stores.ts","../src/client/settings/hostDirectScope.ts","../src/client/settings/configFormScope.ts","../src/client/settings/settingsChannel.ts","../src/client/styles.ts","../src/client/index.ts"],"sourcesContent":["import type { ChannelRelease, UpdateStatus } from './types.ts'\n\nconst CHANNEL_ORDER = { latest: 0, next: 1, alpha: 2 } as const\n\n/**\n * Reduce raw dist-tags to user-meaningful choices without losing Host facts.\n * The selected channel and stable return path are always present; every other\n * row must carry a version no earlier row already shows.\n *\n * A channel is deliberately NOT hidden just because it currently points at the\n * release that is running. Following a channel is a statement about future\n * releases, and `currentVersion` still rides the input for the caller's own\n * rendering, but filtering on it created a deadlock: while running the version\n * published on `alpha`, the `alpha` row disappeared, so the one channel a user\n * on that line would want to follow could never be selected — the only way to\n * see it was to already follow it.\n */\nexport function visibleChannelReleases(status: Pick): ChannelRelease[] {\n const ordered = [...status.channels].sort((left, right) => CHANNEL_ORDER[left.channel] - CHANNEL_ORDER[right.channel])\n const mandatory = new Set(['latest', status.channel])\n const visible = ordered.filter(release => mandatory.has(release.channel))\n const seenVersions = new Set(visible.map(release => release.version).filter((version): version is string => version !== null))\n\n for (const release of ordered) {\n if (mandatory.has(release.channel) || release.version === null) continue\n if (seenVersions.has(release.version)) continue\n visible.push(release)\n seenVersions.add(release.version)\n }\n\n return visible.sort((left, right) => CHANNEL_ORDER[left.channel] - CHANNEL_ORDER[right.channel])\n}\n","/**\n * JSON-only contract shared by the Host and Web halves.\n *\n * The static package uses the authenticated Connection RPC channel because\n * `harness.handle` / `host.call` are dynamic-Cordis-only closure APIs. The\n * endpoint vocabulary remains deliberately small and private to this plugin\n * channel.\n */\n\nexport const PLUGIN_ID = 'dsh-update-status'\nexport const PACKAGE_NAME = '@deepseek-ai/dsh'\n/** Shared Connection RPC channel. Custom prefixes 405 on the SPA fallback. */\nexport const UPDATE_STATUS_CHANNEL = '/api'\nexport const RELEASES_URL = 'https://github.com/deepseek-ai/deepseek-harness/releases'\n/**\n * DSH releases this bundle has actually been verified against. The same list is\n * declared in package.json's `dsh.compatibility.dshReleases`, and every other\n * release the registry reports stays `unverified` — the plugin never claims a\n * compatibility nobody checked.\n */\nexport const VERIFIED_DSH_VERSIONS: readonly string[] = ['0.1.7-alpha.2', '0.1.7-rc.1', '0.1.7-rc.2']\nexport const RELEASE_CHANNELS = ['latest', 'next', 'alpha'] as const\nexport const DEFAULT_CACHE_TTL_MINUTES = 360\nexport const MIN_CACHE_TTL_MINUTES = 30\nexport const MAX_CACHE_TTL_MINUTES = 1_440\n\nexport function isCacheTtlMinutes(value: unknown): value is number {\n return typeof value === 'number' && Number.isInteger(value)\n && value >= MIN_CACHE_TTL_MINUTES && value <= MAX_CACHE_TTL_MINUTES\n}\n\nexport const UPDATE_ENDPOINTS = {\n getStatus: 'dsh-update-status.get-status',\n checkUpdate: 'dsh-update-status.check-update',\n} as const\n\nexport type UpdateEndpoint = (typeof UPDATE_ENDPOINTS)[keyof typeof UPDATE_ENDPOINTS]\nexport type ReleaseChannel = (typeof RELEASE_CHANNELS)[number]\nexport type ReleaseCompatibility = 'verified' | 'unverified' | 'incompatible'\n/**\n * Severity of `warning`, so a surface can tell \"the plugin could not determine\n * the update state\" from \"here is something worth knowing\".\n *\n * - `failure`: no usable answer — a failed registry read, a channel the registry\n * does not publish, or a version SemVer cannot compare. This is what justifies\n * repainting the sidebar chip.\n * - `notice`: the answer is complete and usable; the text is advisory, e.g. a\n * preview channel whose release this bundle has not been verified against.\n * The chip must NOT repaint for this — an operator upgrading DSH ahead of the\n * plugin would otherwise see the chip turn red for a plugin-side bookkeeping\n * fact.\n *\n * Optional on purpose: a Host older than this field leaves it `undefined`, and\n * the client then falls back to treating any warning as a failure.\n */\nexport type UpdateWarningKind = 'failure' | 'notice'\nexport type InstallKind = 'npm-global' | 'pnpm-global' | 'source-checkout' | 'unknown'\n\n/** Language-neutral warning facts; the browser renders them in its own locale. */\nexport type UpdateWarning =\n | { code: 'registry-unavailable'; detail: string }\n | { code: 'channel-unavailable'; channel: ReleaseChannel }\n | { code: 'version-incomparable'; currentVersion: string; channel: ReleaseChannel; selectedVersion: string }\n | { code: 'preview-unverified'; channel: ReleaseChannel; version: string }\n /**\n * The Host resolved a `@deepseek-ai/schemastery` that DSH does not ship, so the\n * Loader's volatile projection is unavailable and the preference fields cannot\n * be marked volatile. Advisory: the version answer itself is complete, and the\n * text carries the exact directory to remove. See `host/schemastery.ts`.\n */\n | { code: 'stale-schemastery'; version: string | null; path: string; nodeModulesDir: string | null }\n\nexport function isReleaseChannel(value: unknown): value is ReleaseChannel {\n return value === 'latest' || value === 'next' || value === 'alpha'\n}\n\nexport interface ChannelRelease {\n channel: ReleaseChannel\n version: string | null\n publishedAt: string | null\n compatibility: ReleaseCompatibility\n}\n\n/** Arguments accepted by either read/check endpoint. */\nexport interface CheckUpdateRequest {\n force?: boolean\n channel?: ReleaseChannel\n /** User preference, bounded by the Host before it affects cache expiry. */\n cacheTtlMinutes?: number\n}\n\n/**\n * A lossless, JSON-serializable snapshot used by both browser surfaces.\n * Nulls are intentional: a failed first check must still render a truthful\n * current-version card instead of an empty or malformed UI.\n */\nexport interface UpdateStatus {\n currentVersion: string\n latestVersion: string | null\n hasUpdate: boolean\n cached: boolean\n checkedAt: string | null\n /** English fallback for older clients; current clients localize `warnings`. */\n warning: string | null\n /** Severity of `warning`; absent from a Host older than the field. */\n warningKind?: UpdateWarningKind | null\n /** Structured warning facts; absent from a Host older than this field. */\n warnings?: UpdateWarning[]\n installKind: InstallKind\n upgradeCommand: string\n releaseUrl: string\n changelogUrl: string\n publishedAt: string | null\n packageName: string\n /** Selected npm dist-tag used for comparison and command generation. */\n channel: ReleaseChannel\n /** All supported dist-tags returned by the same cached registry request. */\n channels: ChannelRelease[]\n /** Phase 1 is informational only; the GUI must never apply an update. */\n canApplyInPlace: false\n}\n","/**\n * Small, dependency-free SemVer 2.0 comparator for the host check path.\n * It intentionally accepts the common leading `v` used by release tags while\n * preserving prerelease precedence (`0.1.2-rc.1 < 0.1.2`).\n */\n\nexport interface ParsedSemver {\n major: number\n minor: number\n patch: number\n prerelease: readonly string[]\n}\n\nconst SEMVER = /^(?:v)?(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$/\nconst NUMERIC_IDENTIFIER = /^(0|[1-9]\\d*)$/\n\nexport function parseSemver(value: string): ParsedSemver | undefined {\n const match = SEMVER.exec(value.trim())\n if (match === null) return undefined\n const major = Number(match[1])\n const minor = Number(match[2])\n const patch = Number(match[3])\n if (!Number.isSafeInteger(major) || !Number.isSafeInteger(minor) || !Number.isSafeInteger(patch)) return undefined\n const prerelease = match[4] === undefined ? [] : match[4].split('.')\n return { major, minor, patch, prerelease }\n}\n\nfunction compareIdentifier(left: string, right: string): number {\n const leftNumeric = NUMERIC_IDENTIFIER.test(left)\n const rightNumeric = NUMERIC_IDENTIFIER.test(right)\n if (leftNumeric && rightNumeric) return Number(left) - Number(right)\n if (leftNumeric) return -1\n if (rightNumeric) return 1\n return left < right ? -1 : left > right ? 1 : 0\n}\n\n/** Returns a negative number when left is older; undefined means unparsable. */\nexport function compareSemver(leftValue: string, rightValue: string): number | undefined {\n const left = parseSemver(leftValue)\n const right = parseSemver(rightValue)\n if (left === undefined || right === undefined) return undefined\n\n for (const key of ['major', 'minor', 'patch'] as const) {\n if (left[key] !== right[key]) return left[key] - right[key]\n }\n\n const leftStable = left.prerelease.length === 0\n const rightStable = right.prerelease.length === 0\n if (leftStable && rightStable) return 0\n if (leftStable) return 1\n if (rightStable) return -1\n\n const length = Math.max(left.prerelease.length, right.prerelease.length)\n for (let index = 0; index < length; index += 1) {\n const leftPart = left.prerelease[index]\n const rightPart = right.prerelease[index]\n if (leftPart === undefined) return -1\n if (rightPart === undefined) return 1\n const comparison = compareIdentifier(leftPart, rightPart)\n if (comparison !== 0) return comparison\n }\n return 0\n}\n\n/** False rather than a guess when either version is not valid SemVer. */\nexport function hasSemverUpdate(currentVersion: string, latestVersion: string): boolean {\n const comparison = compareSemver(currentVersion, latestVersion)\n return comparison !== undefined && comparison < 0\n}\n","import type { UpdateStatus } from './types.ts'\nimport { PACKAGE_NAME } from './types.ts'\nimport { parseSemver } from './semver.ts'\n\n/** Pinned guidance only: never execute a command or mutate the followed channel. */\nexport function previewCommand(status: UpdateStatus, version: string | null): string | null {\n if (version === null || version.trim() !== version || parseSemver(version) === undefined) return null\n if (status.installKind === 'npm-global') return `npm install -g ${PACKAGE_NAME}@${version}`\n if (status.installKind === 'pnpm-global') return `pnpm add -g ${PACKAGE_NAME}@${version}`\n return null\n}\n","/**\n * The single place that decides which visual state the sidebar chip is in.\n *\n * It lives in `shared` rather than inside the component so the decision is a pure\n * function that a test can drive directly: the regression this guards against is\n * exactly a state-classification mistake, and it was invisible to every test while\n * it sat inside TSX.\n */\nimport type { UpdateStatus } from './types.ts'\n\nexport type VisualState = 'loading' | 'update' | 'current' | 'problem'\n\n/**\n * Only a read the plugin could not complete is a chip-level problem.\n *\n * An advisory notice — a preview channel this bundle has not been verified against,\n * say — leaves the chip surface alone and belongs in the panel. Repainting the brand\n * row for a plugin-side bookkeeping fact is the surprise the breathing dot replaced:\n * an operator who upgrades DSH ahead of this bundle must not watch the chip turn red.\n *\n * A Host older than `warningKind` sends no field at all; `undefined` then keeps the\n * previous behaviour (any warning is a problem) rather than silently dropping a real\n * failure on the floor.\n */\nexport function isChipProblem(status: UpdateStatus | null): boolean {\n if (status === null) return false\n if (status.warningKind === undefined) return status.warning !== null\n return status.warningKind === 'failure'\n}\n\nexport function visualState(status: UpdateStatus | null, loading: boolean, error: string | null): VisualState {\n if (loading) return 'loading'\n if (error !== null || isChipProblem(status)) return status?.hasUpdate === true ? 'update' : 'problem'\n return status?.hasUpdate === true ? 'update' : 'current'\n}\n","/** Tiny bilingual dictionary kept local to avoid changing the host locale tree. */\n\nimport type { UpdateStatus, UpdateWarning } from '../shared/types.ts'\n\nexport type Language = 'zh' | 'en'\n\ntype Entry = { zh: string; en: string }\n\nconst DICTIONARY: Record = {\n 'brand.status': { zh: 'DSH 版本状态', en: 'DSH version status' },\n 'brand.checking': { zh: '正在检查版本', en: 'Checking version' },\n 'brand.update': { zh: '有可用更新', en: 'Update available' },\n 'brand.current': { zh: '已是最新或尚未发现更新', en: 'Up to date or no update found' },\n 'brand.problem': { zh: '检查遇到问题', en: 'Check encountered a problem' },\n 'footer.status': { zh: '打开 DSH 版本状态', en: 'Open DSH version status' },\n 'panel.title': { zh: 'DSH 更新状态', en: 'DSH update status' },\n 'panel.close': { zh: '关闭', en: 'Close' },\n 'panel.cacheDuration': { zh: '缓存时长', en: 'Cache duration' },\n 'panel.minutes': { zh: '分钟', en: 'minutes' },\n 'panel.cacheHint': { zh: '到期后在下次打开或读取状态时检查;不会后台轮询。点击“检查更新”会立即检查。', en: 'After expiry, DSH checks only on the next status read; there is no background polling. “Check for updates” checks immediately.' },\n 'panel.cacheReadonly': { zh: '此连接的缓存设置为只读。', en: 'This connection’s cache setting is read-only.' },\n 'panel.current': { zh: '当前运行版本', en: 'Current running version' },\n 'panel.latest': { zh: '所选通道版本', en: 'Selected channel version' },\n 'panel.channels': { zh: '可用发布通道', en: 'Available release channels' },\n 'panel.compatVerified': { zh: '已验证兼容', en: 'Verified compatible' },\n 'panel.compatUnverified': { zh: '尚未验证兼容', en: 'Compatibility unverified' },\n 'panel.compatIncompatible': { zh: '已知不兼容', en: 'Known incompatible' },\n 'panel.selectChannel': { zh: '选择此通道', en: 'Select channel' },\n 'panel.selectedChannel': { zh: '已选择', en: 'Selected' },\n 'panel.notChecked': { zh: '尚未取得', en: 'Not available yet' },\n 'panel.available': { zh: '发现新版本', en: 'Update available' },\n 'panel.currentState': { zh: '未发现可用更新', en: 'No update found' },\n 'panel.cached': { zh: '来自 Host 缓存', en: 'From Host cache' },\n 'panel.live': { zh: '刚从 npm registry 检查', en: 'Checked npm registry now' },\n 'panel.checkedAt': { zh: '上次检查', en: 'Last checked' },\n 'panel.publishedAt': { zh: '发布时间', en: 'Published' },\n 'panel.check': { zh: '检查更新', en: 'Check for updates' },\n 'panel.checking': { zh: '检查中…', en: 'Checking…' },\n 'panel.switchingChannel': { zh: '正在切换通道…', en: 'Switching channel…' },\n 'panel.releaseNotes': { zh: '发布说明', en: 'Release notes' },\n 'panel.command': { zh: '升级命令', en: 'Upgrade command' },\n 'panel.copy': { zh: '复制命令', en: 'Copy command' },\n 'panel.copied': { zh: '已复制', en: 'Copied' },\n 'panel.copyFallback': { zh: '浏览器未允许复制;已选中文本,请长按或复制。', en: 'Clipboard unavailable; the command is selected for long-press or copy.' },\n 'panel.commandNote': { zh: '仅复制,不会执行。请在运行 DSH 的那台电脑的终端执行;完成后由你自行重启 DSH。', en: 'Copy only — nothing runs here. Execute it in a terminal on the computer running DSH, then restart DSH yourself.' },\n 'panel.readOnly': { zh: '阶段 1 仅提示:本插件不会安装、重启、回滚或替换任何文件。', en: 'Phase 1 is advisory only: this plugin never installs, restarts, rolls back, or replaces files.' },\n 'panel.error': { zh: '检查提示', en: 'Check notice' },\n 'warning.registryUnavailable': { zh: '无法检查 npm registry:{detail}', en: 'Unable to check the npm registry: {detail}' },\n 'warning.channelUnavailable': { zh: 'npm registry 未发布 {channel} 通道。', en: 'The npm registry does not publish a {channel} channel.' },\n 'warning.versionIncomparable': { zh: '无法按 SemVer 比较当前版本 {currentVersion} 与 {channel} 通道版本 {selectedVersion}。', en: 'Unable to compare the current version {currentVersion} with {channel} channel version {selectedVersion} using SemVer.' },\n 'warning.previewUnverified': { zh: '{channel} 是预览通道,版本 {version} 尚未验证与本插件兼容。', en: '{channel} is a preview channel; version {version} has not been verified as compatible with this plugin.' },\n 'warning.staleSchemastery': { zh: '本插件解析到的 @deepseek-ai/schemastery(版本 {version})来自 {path},不是 DSH 自带的那份,偏好字段无法标记为 volatile。请删除该残留目录并重启 DSH:rm -rf {nodeModulesDir}', en: 'This plugin resolved @deepseek-ai/schemastery {version} from {path} instead of the copy DSH provides, so preference fields cannot be marked volatile. Remove the stale directory and restart DSH: rm -rf {nodeModulesDir}' },\n 'warning.staleSchemasteryNoPath': { zh: '本插件解析到的 @deepseek-ai/schemastery(版本 {version})来自 {path},不是 DSH 自带的那份,偏好字段无法标记为 volatile。请删除该处残留的 @deepseek-ai/schemastery 目录并重启 DSH。', en: 'This plugin resolved @deepseek-ai/schemastery {version} from {path} instead of the copy DSH provides, so preference fields cannot be marked volatile. Remove the stale @deepseek-ai/schemastery directory there and restart DSH.' },\n 'guidance.sourceCheckout': { zh: '请更新 DSH 源码 checkout、安装依赖并重新构建;本插件无法从 GUI 原地替换。', en: 'Update the DSH source checkout, install its dependencies, and rebuild it; this plugin cannot replace it in place from the GUI.' },\n 'guidance.unknownInstall': { zh: '升级前请先确认 DSH 的安装方式;本插件无法代为执行升级。', en: 'Confirm how DSH was installed before upgrading; this plugin cannot perform the upgrade for you.' },\n 'panel.static': { zh: '此连接只显示静态版本;请在运行 DSH 的本机打开侧栏查看完整更新信息。', en: 'This connection shows only the static version. Open the sidebar on the computer running DSH for full update details.' },\n 'preview.open': { zh: '查看预览版升级方式', en: 'View preview upgrade instructions' },\n 'preview.close': { zh: '收起升级说明', en: 'Hide upgrade instructions' },\n 'preview.follow': { zh: '关注通道只影响检查结果,不代表已安装或已切换版本。', en: 'Following a channel only changes update checks, not the installed version.' },\n 'preview.target': { zh: '目标版本', en: 'Target version' },\n 'preview.risk': { zh: '预览版本可能不稳定,插件兼容性尚需确认。执行前请保存工作、备份配置并结束运行中的任务;安装后需手动重启 DSH。此页面不会安装或重启。', en: 'Preview builds may be unstable and plugin compatibility needs checking. Save work, back up configuration and finish active tasks before executing. Restart DSH manually afterwards. This page never installs or restarts.' },\n 'preview.unavailable': { zh: '暂无可确认的预览目标,请先检查更新。', en: 'No confirmed preview target. Check for updates first.' },\n 'preview.manual': { zh: '当前安装方式无法安全生成命令,请先确认安装来源;源码安装需按其构建说明操作。', en: 'Cannot safely generate a command for this installation. Confirm its source; source checkouts require their build instructions.' },\n 'preview.same': { zh: '此目标与当前运行版本相同,无需重复安装。', en: 'This target is already running; no reinstall is needed.' },\n 'settings.title': { zh: '版本与更新', en: 'Version & updates' },\n 'settings.sidebar': { zh: '在侧栏显示版本状态入口', en: 'Show the version-status entry in the sidebar' },\n 'settings.sidebarHint': { zh: '关闭后不再渲染品牌 Badge 和收起轨道兜底入口。不会执行或安排升级。', en: 'When off, the brand badge and collapsed-rail fallback are not rendered. No update is run or scheduled.' },\n 'settings.channel': { zh: '关注发布通道', en: 'Release channel to follow' },\n 'settings.channelLatest': { zh: '稳定版(latest,推荐)', en: 'Stable (latest, recommended)' },\n 'settings.channelNext': { zh: '候选版(next)', en: 'Release candidate (next)' },\n 'settings.channelAlpha': { zh: '预览版(alpha)', en: 'Preview (alpha)' },\n 'settings.channelHint': { zh: '预览通道可能包含未稳定接口或插件兼容性变化。这里只检查并生成命令,不会安装。', en: 'Preview channels may contain unstable APIs or plugin compatibility changes. This only checks and generates a command; it never installs.' },\n 'settings.readonly': { zh: '此连接的设置为只读;显示状态不受影响。', en: 'Settings are read-only on this connection; status display is unchanged.' },\n}\n\nexport function languageOf(): Language {\n try {\n return navigator.language.toLowerCase().startsWith('zh') ? 'zh' : 'en'\n } catch {\n return 'en'\n }\n}\n\nexport function t(key: keyof typeof DICTIONARY, params: Record = {}, language: Language = languageOf()): string {\n const entry = DICTIONARY[key]\n if (entry === undefined) return key\n const template = entry[language] ?? entry.en\n return template.replace(/\\{([A-Za-z]+)\\}/g, (match, name: string) => params[name] ?? match)\n}\n\nexport function warningText(warning: UpdateWarning, language: Language = languageOf()): string {\n switch (warning.code) {\n case 'registry-unavailable': return t('warning.registryUnavailable', { detail: warning.detail }, language)\n case 'channel-unavailable': return t('warning.channelUnavailable', { channel: warning.channel }, language)\n case 'version-incomparable': return t('warning.versionIncomparable', {\n currentVersion: warning.currentVersion,\n channel: warning.channel,\n selectedVersion: warning.selectedVersion,\n }, language)\n case 'preview-unverified': return t('warning.previewUnverified', { channel: warning.channel, version: warning.version }, language)\n case 'stale-schemastery': {\n const params = { version: warning.version ?? '—', path: warning.path }\n // Only print a removal command when the Host could name the directory to\n // remove; the module path alone would make `rm -rf` delete a single file.\n return warning.nodeModulesDir === null\n ? t('warning.staleSchemasteryNoPath', params, language)\n : t('warning.staleSchemastery', { ...params, nodeModulesDir: warning.nodeModulesDir }, language)\n }\n }\n}\n\n/** Prefer structured warnings, but keep the Host string for mixed-version clients. */\nexport function localizedWarning(status: Pick, language: Language = languageOf()): string | null {\n const warnings = status.warnings ?? []\n return warnings.length === 0 ? status.warning : warnings.map(item => warningText(item, language)).join(' ')\n}\n\nexport function localizedUpgradeGuidance(\n status: Pick,\n language: Language = languageOf(),\n): string {\n if (status.installKind === 'source-checkout') return t('guidance.sourceCheckout', {}, language)\n if (status.installKind === 'unknown') return t('guidance.unknownInstall', {}, language)\n return status.upgradeCommand\n}\n","/** React arrives from DSH's frozen browser module table at client materialization. */\nimport type * as ReactNS from 'react'\n\n// eslint/oxlint would normally discourage require; DSH's closure factory makes\n// this the supported runtime import path for a third-party client bundle.\nexport const React: typeof ReactNS = require('react') as typeof ReactNS\nexport const h = React.createElement\n","/** Sidebar replacement, collapsed-rail fallback, overlay detail panel, and settings page. */\n\nimport type * as ReactNS from 'react'\nimport { visibleChannelReleases } from '../shared/channels.ts'\nimport { previewCommand } from '../shared/preview-guidance.ts'\nimport { isChipProblem } from '../shared/visual-state.ts'\nimport { MAX_CACHE_TTL_MINUTES, MIN_CACHE_TTL_MINUTES, isCacheTtlMinutes, type ReleaseChannel, type ReleaseCompatibility, type UpdateStatus } from '../shared/types.ts'\nimport type { PreferencesStore, PanelStore, StatusStore } from './stores.ts'\nimport { localizedUpgradeGuidance, localizedWarning, t } from './i18n.ts'\nimport { React, h } from './react.ts'\n\nfunction useObservable(store: { subscribe(listener: () => void): () => void; getSnapshot(): T }): T {\n return React.useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot)\n}\n\nfunction timeText(value: string | null): string | null {\n if (value === null) return null\n try {\n return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(new Date(value))\n } catch {\n return value\n }\n}\n\nfunction shortVersion(value: string): string {\n if (value.length <= 16) return value\n return value.slice(0, 15) + '…'\n}\n\nfunction channelLabel(channel: ReleaseChannel): string {\n if (channel === 'latest') return t('settings.channelLatest')\n if (channel === 'next') return t('settings.channelNext')\n return t('settings.channelAlpha')\n}\n\nfunction compatibilityLabel(value: ReleaseCompatibility): string {\n if (value === 'verified') return t('panel.compatVerified')\n if (value === 'incompatible') return t('panel.compatIncompatible')\n return t('panel.compatUnverified')\n}\n\n/** Persists a cache policy only; it never schedules a browser or Host timer. */\nfunction CacheTtlControl({ preferences }: { preferences: PreferencesStore }): ReactNS.ReactElement {\n const snapshot = useObservable(preferences)\n const [draft, setDraft] = React.useState(String(snapshot.cacheTtlMinutes))\n React.useEffect(() => { setDraft(String(snapshot.cacheTtlMinutes)) }, [snapshot.cacheTtlMinutes])\n const save = (): void => {\n const value = Number(draft)\n if (isCacheTtlMinutes(value)) preferences.setCacheTtlMinutes(value)\n else setDraft(String(snapshot.cacheTtlMinutes))\n }\n return (\n
\n \n

{t('panel.cacheHint')}

\n {!snapshot.writable &&

{t('panel.cacheReadonly')}

}\n
\n )\n}\n\ntype VisualState = 'loading' | 'update' | 'current' | 'problem'\n\nfunction visualState(status: UpdateStatus | null, loading: boolean, error: string | null): VisualState {\n if (loading) return 'loading'\n if (error !== null || isChipProblem(status)) return status?.hasUpdate === true ? 'update' : 'problem'\n return status?.hasUpdate === true ? 'update' : 'current'\n}\n\nfunction badgeLabel(status: UpdateStatus | null, loading: boolean, error: string | null): string {\n const state = visualState(status, loading, error)\n if (state === 'loading') return t('brand.checking')\n if (state === 'update') return t('brand.update')\n if (state === 'problem') return t('brand.problem')\n return t('brand.current')\n}\n\nexport interface SharedUi {\n status: StatusStore\n preferences: PreferencesStore\n panel: PanelStore\n}\n\n/** Occupies ONLY sidebar.brand.name; the official fish mark stays untouched. */\nexport function BrandName({ ui }: { ui: SharedUi }): ReactNS.ReactElement | null {\n const preferences = useObservable(ui.preferences)\n const snapshot = useObservable(ui.status)\n if (!preferences.sidebarEnabled) return null\n\n const state = visualState(snapshot.status, snapshot.loading, snapshot.error)\n const version = snapshot.status?.currentVersion ?? '—'\n const activate = (event: ReactNS.SyntheticEvent): void => {\n // The surrounding sidebar brand is an existing New Session \n )\n}\n\nfunction statusSummary(status: UpdateStatus | null, loading: boolean, error: string | null): { kind: 'update' | 'ok' | 'warning' | 'error'; text: string } {\n if (loading && status === null) return { kind: 'warning', text: t('brand.checking') }\n if (error !== null) return { kind: 'error', text: error }\n if (status?.hasUpdate === true) return { kind: 'update', text: t('panel.available') }\n if (status !== null) {\n const warning = localizedWarning(status)\n if (warning !== null) return { kind: 'warning', text: warning }\n }\n return { kind: 'ok', text: t('panel.currentState') }\n}\n\nfunction selectCommand(element: HTMLElement | null): void {\n if (element === null) return\n try {\n const selection = window.getSelection()\n if (selection === null) return\n const range = document.createRange()\n range.selectNodeContents(element)\n selection.removeAllRanges()\n selection.addRange(range)\n element.focus()\n } catch {\n // The command remains ordinary selectable text even when selection fails.\n }\n}\n\n/** Full detail from whichever page the operator is on (the transport is authenticated). */\nexport function UpdatePanel({ ui }: { ui: SharedUi }): ReactNS.ReactElement | null {\n const panel = useObservable(ui.panel)\n const preferences = useObservable(ui.preferences)\n const snapshot = useObservable(ui.status)\n const commandRef = React.useRef(null)\n const dialogRef = React.useRef(null)\n const [copyMessage, setCopyMessage] = React.useState(null)\n const visible = panel.open && preferences.sidebarEnabled\n\n React.useLayoutEffect(() => {\n if (!visible) return undefined\n const dialog = dialogRef.current\n if (dialog === null) return undefined\n try {\n if (!dialog.open) dialog.showModal()\n } catch {\n // Older embedded Chromium still receives a visible non-top-layer dialog.\n dialog.setAttribute('open', '')\n }\n return () => {\n if (dialog.open) dialog.close()\n }\n }, [visible])\n\n React.useEffect(() => {\n if (!panel.open) return undefined\n const onKeyDown = (event: KeyboardEvent): void => {\n if (event.key === 'Escape') ui.panel.close()\n }\n window.addEventListener('keydown', onKeyDown)\n return () => { window.removeEventListener('keydown', onKeyDown) }\n }, [panel.open, ui.panel])\n\n if (!visible) return null\n const status = snapshot.status\n const warning = status === null ? null : localizedWarning(status)\n const summary = statusSummary(status, snapshot.loading, snapshot.error)\n const switchingChannel = status !== null && status.channel !== preferences.channel\n const command = switchingChannel ? t('panel.switchingChannel') : status === null ? '—' : localizedUpgradeGuidance(status)\n\n const copy = async (): Promise => {\n setCopyMessage(null)\n try {\n if (!navigator.clipboard?.writeText) throw new Error('Clipboard API unavailable')\n await navigator.clipboard.writeText(command)\n setCopyMessage(t('panel.copied'))\n } catch {\n selectCommand(commandRef.current)\n setCopyMessage(t('panel.copyFallback'))\n }\n }\n\n return (\n {\n event.preventDefault()\n ui.panel.close()\n }}\n >\n
{ ui.panel.close() }} />\n
{ event.stopPropagation() }}>\n
\n {t('panel.title')}\n \n
\n\n \n

{summary.text}

\n\n
\n
\n {t('panel.current')}\n {status?.currentVersion ?? '…'}\n
\n
\n {t('panel.latest')} · {status === null ? 'latest' : channelLabel(status.channel)}\n {status?.latestVersion ?? t('panel.notChecked')}\n {status?.publishedAt !== null && status?.publishedAt !== undefined && {t('panel.publishedAt')}: {timeText(status.publishedAt)}}\n
\n {status?.checkedAt !== null && status?.checkedAt !== undefined &&
\n {t('panel.checkedAt')}\n {timeText(status.checkedAt)}\n {status.cached ? t('panel.cached') : t('panel.live')}\n
}\n
\n\n {status !== null &&
\n

{t('panel.channels')}

\n {visibleChannelReleases(status).map(release => {\n const selected = release.channel === preferences.channel\n return
\n {channelLabel(release.channel)}\n {release.version ?? '—'}\n {compatibilityLabel(release.compatibility)}\n { ui.preferences.setChannel(release.channel) }}\n >{selected ? t('panel.selectedChannel') : t('panel.selectChannel')}\n
\n })}\n
}\n\n {warning !== null &&

{t('panel.error')}: {warning}

}\n {snapshot.error !== null &&

{t('panel.error')}: {snapshot.error}

}\n\n
\n \n {status !== null && {t('panel.releaseNotes')}}\n \n
\n\n

{t('panel.command')}

\n {command}\n {copyMessage !== null &&

{copyMessage}

}\n

{t('panel.commandNote')}

\n

{t('panel.readOnly')}

\n
\n \n )\n}\n\n/** Separate setting page: disables only this plugin's own rendering. */\nexport function UpdateSettings({ ui }: { ui: SharedUi }): ReactNS.ReactElement {\n const preferences = useObservable(ui.preferences)\n const snapshot = useObservable(ui.status)\n const [showGuidance, setShowGuidance] = React.useState(false)\n const previews = snapshot.status?.channels.filter(release => release.channel !== 'latest') ?? []\n const channelOptions = snapshot.status === null\n ? [{ channel: preferences.channel }]\n : visibleChannelReleases(snapshot.status)\n return (\n
\n

{t('settings.title')}

\n
\n \n

{t('settings.sidebarHint')}

\n {!preferences.writable &&

{t('settings.readonly')}

}\n
\n
\n \n

{t('settings.channelHint')}

\n

{t('panel.current')}: {snapshot.status?.currentVersion ?? '…'}

\n

{t('preview.follow')}

\n \n {showGuidance &&
\n

{t('preview.risk')}

\n \n {snapshot.error !== null &&

{snapshot.error}

}\n {previews.length === 0 &&

{t('preview.unavailable')}

}\n {previews.map(release => {\n const status = snapshot.status!\n const command = previewCommand(status, release.version)\n return
\n

{channelLabel(release.channel)} · {t('preview.target')}: {release.version ?? t('panel.notChecked')}

\n

{compatibilityLabel(release.compatibility)}

\n {release.version === null ?

{t('preview.unavailable')}

: release.version === status.currentVersion ?

{t('preview.same')}

: command !== null ? <>

{t('panel.command')}

{command} :

{t('preview.manual')}

}\n
\n })}\n

{t('panel.commandNote')}

\n
}\n
\n
\n )\n}\n","/** Minimal structural client faces — runtime services stay owned by DSH. */\n\nimport { isReleaseChannel, type ChannelRelease, type UpdateStatus, type UpdateWarning } from '../shared/types.ts'\n\nexport interface Observable {\n getSnapshot(): T\n subscribe(listener: () => void): () => void\n}\n\nexport interface ConnectionRpc {\n call(channel: string, endpoint: string, payload: unknown, signal?: AbortSignal): Promise\n}\n\n/**\n * The authenticated transport face. There is deliberately NO `isLoopback` gate\n * here: DSH's settings Client treats a non-loopback page as process-local, but\n * that is a settings *persistence* policy — the Connection RPC itself stays\n * authenticated and reachable over a LAN bridge, so gating this plugin's own\n * read-only status on it only made the panel inert on exactly the page it is\n * used from. See settings/settingsChannel.ts for the one place a loopback\n * distinction still matters (choosing the Host settings channel).\n */\nexport interface ConnectionClient {\n rpc?: ConnectionRpc\n}\n\nexport interface SlotRegistration {\n name: string\n id?: string\n order?: number\n /** Single-slot arbitration: the lowest registered priority renders. */\n priority?: number\n label?: () => string\n locale?: string\n inject?: () => unknown\n}\n\nexport interface Slots {\n inject(name: string, callback: () => unknown): () => void\n register(registration: SlotRegistration, component: (props: Record) => unknown): () => void\n}\n\nexport interface ClientContext {\n slots: Slots\n get(name: string): unknown\n inject(names: string[], callback: (ctx: unknown) => void): () => void\n effect(setup: () => (() => void) | void, label?: string): () => void\n /** Optional lifecycle event seam (Cordis contexts expose it; guarded here). */\n on?(name: string, listener: () => void): unknown\n}\n\nexport function errorMessage(error: unknown): string {\n const text = error instanceof Error ? error.message : String(error)\n return text.replace(/\\s+/g, ' ').trim().slice(0, 220) || 'unknown error'\n}\n\nfunction stringOrNull(value: unknown): string | null {\n return typeof value === 'string' ? value : null\n}\n\nfunction warningOf(value: unknown): UpdateWarning | undefined {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) return undefined\n const record = value as Record\n if (record.code === 'registry-unavailable' && typeof record.detail === 'string') {\n return { code: record.code, detail: record.detail }\n }\n if (record.code === 'channel-unavailable' && isReleaseChannel(record.channel)) {\n return { code: record.code, channel: record.channel }\n }\n if (record.code === 'version-incomparable' && typeof record.currentVersion === 'string'\n && isReleaseChannel(record.channel) && typeof record.selectedVersion === 'string') {\n return {\n code: record.code,\n currentVersion: record.currentVersion,\n channel: record.channel,\n selectedVersion: record.selectedVersion,\n }\n }\n if (record.code === 'preview-unverified' && isReleaseChannel(record.channel) && typeof record.version === 'string') {\n return { code: record.code, channel: record.channel, version: record.version }\n }\n if (record.code === 'stale-schemastery') {\n const version = record.version === null ? null : stringOrNull(record.version)\n const nodeModulesDir = record.nodeModulesDir === null ? null : stringOrNull(record.nodeModulesDir)\n const path = stringOrNull(record.path)\n if (path !== null && (record.version === null || version !== null)\n && (record.nodeModulesDir === null || nodeModulesDir !== null)) {\n return { code: record.code, version, path, nodeModulesDir }\n }\n }\n return undefined\n}\n\n/** Reject malformed RPC output before it reaches a slot component. */\nexport function updateStatusOf(value: unknown): UpdateStatus | undefined {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) return undefined\n const record = value as Record\n if (typeof record.currentVersion !== 'string' || typeof record.hasUpdate !== 'boolean'\n || typeof record.cached !== 'boolean' || typeof record.installKind !== 'string'\n || typeof record.upgradeCommand !== 'string' || typeof record.releaseUrl !== 'string'\n || typeof record.changelogUrl !== 'string' || typeof record.packageName !== 'string'\n || !isReleaseChannel(record.channel) || !Array.isArray(record.channels) || record.canApplyInPlace !== false) return undefined\n const validKind = record.installKind === 'npm-global' || record.installKind === 'pnpm-global'\n || record.installKind === 'source-checkout' || record.installKind === 'unknown'\n if (!validKind) return undefined\n const latestVersion = record.latestVersion === null ? null : stringOrNull(record.latestVersion)\n const checkedAt = record.checkedAt === null ? null : stringOrNull(record.checkedAt)\n const warning = record.warning === null ? null : stringOrNull(record.warning)\n // Absent or unrecognised means a Host older than the field: leave it undefined and\n // let the visual state fall back to \"any warning is a failure\", never to \"no warning\".\n const warningKind = record.warningKind === 'failure' || record.warningKind === 'notice' ? record.warningKind : undefined\n const publishedAt = record.publishedAt === null ? null : stringOrNull(record.publishedAt)\n if ((record.latestVersion !== null && latestVersion === null) || (record.checkedAt !== null && checkedAt === null)\n || (record.warning !== null && warning === null) || (record.publishedAt !== null && publishedAt === null)) return undefined\n const warnings: UpdateWarning[] = []\n if (record.warnings !== undefined) {\n if (!Array.isArray(record.warnings)) return undefined\n for (const raw of record.warnings) {\n const parsed = warningOf(raw)\n if (parsed === undefined) return undefined\n warnings.push(parsed)\n }\n }\n const channels: ChannelRelease[] = []\n for (const raw of record.channels) {\n if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) return undefined\n const item = raw as Record\n const version = item.version === null ? null : stringOrNull(item.version)\n const channelPublishedAt = item.publishedAt === null ? null : stringOrNull(item.publishedAt)\n if (!isReleaseChannel(item.channel) || (item.version !== null && version === null)\n || (item.publishedAt !== null && channelPublishedAt === null)\n || (item.compatibility !== 'verified' && item.compatibility !== 'unverified' && item.compatibility !== 'incompatible')) return undefined\n channels.push({ channel: item.channel, version, publishedAt: channelPublishedAt, compatibility: item.compatibility })\n }\n return {\n currentVersion: record.currentVersion,\n latestVersion,\n hasUpdate: record.hasUpdate,\n cached: record.cached,\n checkedAt,\n warning,\n warningKind,\n warnings,\n installKind: record.installKind as UpdateStatus['installKind'],\n upgradeCommand: record.upgradeCommand,\n releaseUrl: record.releaseUrl,\n changelogUrl: record.changelogUrl,\n publishedAt,\n packageName: record.packageName,\n channel: record.channel,\n channels,\n canApplyInPlace: false,\n }\n}\n","/** Small observable stores shared by the independent sidebar and overlay slots. */\n\nimport { DEFAULT_CACHE_TTL_MINUTES, isCacheTtlMinutes, isReleaseChannel, PLUGIN_ID, UPDATE_ENDPOINTS, UPDATE_STATUS_CHANNEL, type ReleaseChannel, type UpdateStatus } from '../shared/types.ts'\nimport type { ConnectionClient, Observable } from './contract.ts'\nimport { errorMessage, updateStatusOf } from './contract.ts'\nimport type { SettingsScopeLike } from './settings/scopeFaces.ts'\n\nexport interface StatusSnapshot {\n status: UpdateStatus | null\n loading: boolean\n error: string | null\n}\n\nconst INITIAL_STATUS: StatusSnapshot = { status: null, loading: true, error: null }\n\nexport class StatusStore implements Observable {\n private snapshot: StatusSnapshot = INITIAL_STATUS\n private readonly listeners = new Set<() => void>()\n private inFlight: Promise | undefined\n private stopped = false\n\n constructor(private readonly connection: ConnectionClient) {}\n\n getSnapshot = (): StatusSnapshot => this.snapshot\n\n subscribe = (listener: () => void): (() => void) => {\n this.listeners.add(listener)\n return () => { this.listeners.delete(listener) }\n }\n\n /** First mount reads the Host's cached status; it never starts a browser timer. */\n async load(cacheTtlMinutes: number = DEFAULT_CACHE_TTL_MINUTES): Promise {\n await this.request(false, this.snapshot.status?.channel ?? 'latest', cacheTtlMinutes)\n }\n\n /** User gesture only: force the Host to bypass TTL (while retaining single-flight). */\n async refresh(channel: ReleaseChannel = this.snapshot.status?.channel ?? 'latest', cacheTtlMinutes: number = DEFAULT_CACHE_TTL_MINUTES): Promise {\n await this.request(true, channel, cacheTtlMinutes)\n }\n\n /** Channel selection re-projects the Host cache; it is not a forced refresh. */\n async selectChannel(channel: ReleaseChannel, cacheTtlMinutes: number = DEFAULT_CACHE_TTL_MINUTES): Promise {\n // A persisted preference or a newer selection can arrive while another\n // channel is in flight. Keep checking after every joined request until the\n // requested projection is actually the published snapshot.\n for (let attempts = 0; attempts < 3 && !this.stopped && this.snapshot.status?.channel !== channel; attempts += 1) {\n const pending = this.inFlight\n if (pending !== undefined && !await pending) return\n if (this.stopped || this.snapshot.status?.channel === channel) return\n if (!await this.request(false, channel, cacheTtlMinutes)) return\n }\n }\n\n stop(): void {\n this.stopped = true\n this.listeners.clear()\n }\n\n private publish(next: StatusSnapshot): void {\n if (this.stopped) return\n this.snapshot = next\n for (const listener of this.listeners) listener()\n }\n\n private request(force: boolean, channel: ReleaseChannel, cacheTtlMinutes: number): Promise {\n if (this.inFlight !== undefined) return this.inFlight\n const rpc = this.connection.rpc\n if (rpc === undefined || typeof rpc.call !== 'function') {\n this.publish({ ...this.snapshot, loading: false, error: 'DSH connection RPC is unavailable' })\n return Promise.resolve(false)\n }\n\n this.publish({ ...this.snapshot, loading: true, error: null })\n const run = (async () => {\n try {\n const endpoint = force ? UPDATE_ENDPOINTS.checkUpdate : UPDATE_ENDPOINTS.getStatus\n const ttl = isCacheTtlMinutes(cacheTtlMinutes) ? cacheTtlMinutes : DEFAULT_CACHE_TTL_MINUTES\n const raw = await rpc.call(UPDATE_STATUS_CHANNEL, endpoint, force\n ? { force: true, channel, cacheTtlMinutes: ttl }\n : { channel, cacheTtlMinutes: ttl })\n if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) throw new Error('invalid update-status RPC response')\n const envelope = raw as { ok?: unknown; value?: unknown; error?: { message?: unknown } }\n if (envelope.ok !== true) throw new Error(typeof envelope.error?.message === 'string' ? envelope.error.message : 'update-status RPC failed')\n const status = updateStatusOf(envelope.value)\n if (status === undefined) throw new Error('invalid update-status payload')\n this.publish({ status, loading: false, error: null })\n return true\n } catch (error) {\n this.publish({ ...this.snapshot, loading: false, error: errorMessage(error) })\n return false\n }\n })()\n this.inFlight = run\n void run.finally(() => {\n if (this.inFlight === run) this.inFlight = undefined\n })\n return run\n }\n}\n\nexport interface PreferencesSnapshot {\n sidebarEnabled: boolean\n channel: ReleaseChannel\n cacheTtlMinutes: number\n writable: boolean\n status: 'loading' | 'ready' | 'unavailable'\n}\n\nconst INITIAL_PREFERENCES: PreferencesSnapshot = {\n sidebarEnabled: true,\n channel: 'latest',\n cacheTtlMinutes: DEFAULT_CACHE_TTL_MINUTES,\n writable: false,\n status: 'loading',\n}\n\nexport class PreferencesStore implements Observable {\n private snapshot: PreferencesSnapshot = INITIAL_PREFERENCES\n private readonly listeners = new Set<() => void>()\n private scope: SettingsScopeLike | undefined\n\n getSnapshot = (): PreferencesSnapshot => this.snapshot\n\n subscribe = (listener: () => void): (() => void) => {\n this.listeners.add(listener)\n return () => { this.listeners.delete(listener) }\n }\n\n attach(scope: SettingsScopeLike): () => void {\n this.scope = scope\n const sync = () => {\n const raw = scope.getSnapshot()\n const value = raw.value !== null && typeof raw.value === 'object' && !Array.isArray(raw.value)\n ? raw.value as Record\n : {}\n const status = raw.status === 'ready' || raw.status === 'unavailable' ? raw.status : 'loading'\n this.publish({\n sidebarEnabled: value.sidebarEnabled !== false,\n channel: isReleaseChannel(value.channel) ? value.channel : 'latest',\n cacheTtlMinutes: isCacheTtlMinutes(value.cacheTtlMinutes) ? value.cacheTtlMinutes : DEFAULT_CACHE_TTL_MINUTES,\n writable: raw.writable === true,\n status,\n })\n }\n sync()\n return scope.subscribe(sync)\n }\n\n setSidebarEnabled(enabled: boolean): void {\n const previous = this.snapshot\n this.publish({ ...previous, sidebarEnabled: enabled })\n const scope = this.scope\n if (scope === undefined || !previous.writable) return\n void scope.set('sidebarEnabled', enabled).catch(() => {\n // A rejected Host-backed write restores the authoritative scope snapshot.\n try {\n const raw = scope.getSnapshot()\n const value = raw.value !== null && typeof raw.value === 'object' && !Array.isArray(raw.value)\n ? raw.value as Record\n : {}\n this.publish({\n sidebarEnabled: value.sidebarEnabled !== false,\n channel: isReleaseChannel(value.channel) ? value.channel : 'latest',\n cacheTtlMinutes: isCacheTtlMinutes(value.cacheTtlMinutes) ? value.cacheTtlMinutes : DEFAULT_CACHE_TTL_MINUTES,\n writable: raw.writable === true,\n status: raw.status === 'ready' || raw.status === 'unavailable' ? raw.status : 'loading',\n })\n } catch {\n this.publish(previous)\n }\n })\n }\n\n setChannel(channel: ReleaseChannel): void {\n const previous = this.snapshot\n this.publish({ ...previous, channel })\n const scope = this.scope\n if (scope === undefined || !previous.writable) return\n void scope.set('channel', channel).catch(() => { this.publish(previous) })\n }\n\n setCacheTtlMinutes(cacheTtlMinutes: number): void {\n if (!isCacheTtlMinutes(cacheTtlMinutes)) return\n const previous = this.snapshot\n this.publish({ ...previous, cacheTtlMinutes })\n const scope = this.scope\n if (scope === undefined || !previous.writable) return\n void scope.set('cacheTtlMinutes', cacheTtlMinutes).catch(() => { this.publish(previous) })\n }\n\n private publish(next: PreferencesSnapshot): void {\n const previous = this.snapshot\n if (previous.sidebarEnabled === next.sidebarEnabled && previous.channel === next.channel\n && previous.cacheTtlMinutes === next.cacheTtlMinutes\n && previous.writable === next.writable && previous.status === next.status) return\n this.snapshot = next\n for (const listener of this.listeners) listener()\n }\n}\n\nexport type PanelOrigin = 'brand' | 'rail'\n\nexport interface PanelSnapshot {\n open: boolean\n origin: PanelOrigin\n}\n\n/** Open state also retains its trigger, so the desktop card sits beside it. */\nexport class PanelStore implements Observable {\n private snapshot: PanelSnapshot = { open: false, origin: 'brand' }\n private readonly listeners = new Set<() => void>()\n\n getSnapshot = (): PanelSnapshot => this.snapshot\n\n subscribe = (listener: () => void): (() => void) => {\n this.listeners.add(listener)\n return () => { this.listeners.delete(listener) }\n }\n\n toggle(origin: PanelOrigin): void {\n const open = !(this.snapshot.open && this.snapshot.origin === origin)\n this.set({ open, origin })\n }\n\n close(): void { this.set({ ...this.snapshot, open: false }) }\n\n private set(next: PanelSnapshot): void {\n if (this.snapshot.open === next.open && this.snapshot.origin === next.origin) return\n this.snapshot = next\n for (const listener of this.listeners) listener()\n }\n}\n\n/**\n * The one settings namespace, which on DSH 0.1.7 IS the Loader entry id in\n * `cordis.patch.yml`: the Host half's volatile `Config` fields are projected as\n * that entry's form, and the browser half addresses the same id through\n * `ctx.configForms.get(...)`.\n */\nexport const SETTINGS_NAMESPACE = PLUGIN_ID\n","/**\n * dsh-update-status — direct Host settings channel (non-loopback pages).\n *\n * WHY THIS EXISTS\n *\n * DSH's settings Client keeps Host persistence disabled on any page whose\n * location is not a loopback authority. `ctx.configForms.get(entryId)` — the\n * DSH 0.1.7 successor of `settingsScope.bind()` — then answers\n * `status: 'unavailable'` immediately and NEVER sends `settings.describe`\n * (`@deepseek-ai/dsh-client-ui-settings`: the persistence mode resolves from\n * `remote.$host.isLoopback` and pins a remote page to `memory`; every row it\n * backs is inert even though Connection authentication covers the API).\n *\n * The decision itself is one line in the official client:\n *\n * const persistence = ctx.remote.$host.isLoopback ? \"host\" : \"memory\"\n *\n * and `isLoopback` is true only for `localhost` / `[::1]` / `127.0.0.0/8` pages\n * (or a transport that declares `ownsHost`). A LAN page reached through a\n * forwarding plugin is therefore non-loopback even though the API answers and\n * the page is authenticated.\n *\n * This plugin's preferences — the sidebar toggle, the followed release channel\n * and the cache policy — live in the Host settings entry `dsh-update-status`, so\n * without a channel they all silently fall back to defaults on a LAN page (the\n * host's configured `channel` would be ignored, silently switching the followed\n * release line). This module speaks the SAME public Remote the official form\n * speaks (`settings.describe` / `settings.mutate`, the generated Typert face\n * mounted by `@deepseek-ai/dsh-api-remotes`) and derives the same per-namespace\n * snapshot shape, so the ONE shared Host document keeps serving every device.\n *\n * This channel is opened ONLY when the official form reports `unavailable`\n * (see `settingsChannel.ts`), so loopback pages keep the official path and pay\n * no extra wire read. Everything here is guarded: an absent Remote, a refused\n * read, or a malformed answer leaves the preferences exactly as unavailable as\n * they were before this channel existed.\n */\nimport type { SettingsPathOpLike, SettingsScopeLike, SettingsScopeSnapshotLike } from './scopeFaces.ts'\n\n/** One namespace row of a `settings.describe` answer (the fields consumed here). */\nexport interface SettingsNamespaceViewLike {\n readonly ns: string\n readonly value: unknown\n readonly base?: unknown\n readonly user?: unknown\n readonly revision: number\n}\n\n/** The `settings.describe` answer body. */\nexport interface SettingsDescribeViewLike {\n readonly writable: boolean\n readonly hasDocument?: boolean\n readonly namespaces: readonly SettingsNamespaceViewLike[]\n}\n\n/** The Remote envelope every generated client call answers with. */\nexport type SettingsRemoteOutcome =\n | { readonly ok: true; readonly value: T }\n | { readonly ok: false; readonly error?: { readonly code?: string; readonly message?: string } }\n\n/** The `remote.settings` namespace face (structural; never imported at runtime). */\nexport interface SettingsRemoteLike {\n describe(): Promise>\n mutate(\n namespace: string,\n ops: readonly SettingsPathOpLike[],\n expectedRevision?: number,\n ): Promise>\n}\n\n/** A refused namespace write; `code` keeps the Host's own diagnosis. */\nexport class SettingsWriteFailure extends Error {\n readonly code: string\n\n constructor(code: string, message?: string) {\n super(message === undefined || message === '' ? code : message)\n this.name = 'SettingsWriteFailure'\n this.code = code\n }\n}\n\nfunction isRecord(value: unknown): value is Record {\n return typeof value === 'object' && value !== null\n}\n\n/**\n * Guard: does this value look like the `remote.settings` namespace face?\n * Applied to a bare service object (`ctx.get('remote.settings')` or a payload\n * member), and it never calls anything.\n */\nexport function settingsRemoteFace(value: unknown): SettingsRemoteLike | undefined {\n if (!isRecord(value)) return undefined\n if (typeof value.describe !== 'function' || typeof value.mutate !== 'function') return undefined\n return value as unknown as SettingsRemoteLike\n}\n\n/**\n * Guard: read the `remote.settings` face off an injection payload.\n *\n * The payload a `ctx.inject(['remote.settings'], …)` callback receives exposes\n * the LITERAL service key. Reading the dotted PARENT off that injected context\n * (`payload.remote`) throws `cannot get property \"remote\" without inject` — a\n * throw that used to abort a whole wiring callback on a LAN page. So the literal\n * key is read FIRST and every read is contained; the nested\n * (`payload.remote.settings`) shape is only a fallback for a plain object\n * payload. Both absence and refusal answer `undefined`.\n */\nexport function settingsRemoteOf(raw: unknown): SettingsRemoteLike | undefined {\n if (!isRecord(raw)) return undefined\n try {\n const direct = settingsRemoteFace((raw as Record)['remote.settings'])\n if (direct !== undefined) return direct\n } catch {\n // Injected-context proxy refusal: fall through to the nested shape.\n }\n try {\n const remote = (raw as { remote?: unknown }).remote\n if (isRecord(remote)) return settingsRemoteFace(remote.settings)\n } catch {\n // Same refusal: no face is readable from this payload.\n }\n return undefined\n}\n\n/**\n * Guard: subscribe to the Host's settings-document invalidation on the Remote\n * service object (`remote.$on`). Returns undefined when the service exposes no\n * event seam — the preferences then refresh on reconnect only.\n */\nexport function settingsInvalidationsOf(service: unknown): ((listener: () => void) => () => void) | undefined {\n if (!isRecord(service)) return undefined\n const on = service.$on\n if (typeof on !== 'function') return undefined\n return (listener) => {\n const dispose = (on as (event: string, callback: () => void) => unknown)\n .call(service, 'settings/document-updated', listener)\n return typeof dispose === 'function' ? dispose as () => void : () => {}\n }\n}\n\n/** The channel consumers subscribe to, plus the refresh/dispose seams the client wires. */\nexport interface HostDirectScope extends SettingsScopeLike {\n /** Re-read the Host document; resolves after the snapshot reflects the answer. */\n load(): Promise\n /** Drop every listener and stop accepting work. */\n dispose(): void\n /** Last Host error message (diagnostics only; never rendered as plugin state). */\n lastError(): string | undefined\n}\n\nfunction cloneOps(ops: readonly SettingsPathOpLike[]): SettingsPathOpLike[] {\n try {\n return structuredClone(ops) as SettingsPathOpLike[]\n } catch {\n // structuredClone absent/refusing the shape: copy by hand (the op payload is\n // plain JSON by contract, so a shallow structural copy is equivalent).\n return ops.map(op => (op.op === 'set'\n ? { op: 'set' as const, path: [...op.path], value: op.value }\n : { op: 'unset' as const, path: [...op.path] }))\n }\n}\n\nfunction messageOf(error: unknown): string {\n return error instanceof Error ? error.message : String(error)\n}\n\nfunction sameSnapshot(a: SettingsScopeSnapshotLike, b: SettingsScopeSnapshotLike): boolean {\n return a.status === b.status\n && a.revision === b.revision\n && a.writable === b.writable\n && a.mode === b.mode\n && a.value === b.value\n && a.base === b.base\n && a.user === b.user\n}\n\nfunction readySnapshot(\n row: SettingsNamespaceViewLike,\n writable: boolean,\n): SettingsScopeSnapshotLike {\n return {\n status: 'ready',\n // The raw section crosses this boundary untouched: PreferencesStore is the\n // plugin's own strict judge and reports a hand-edited or future-schema\n // document truthfully instead of silently dropping it.\n value: row.value,\n base: row.base,\n user: row.user,\n revision: row.revision,\n writable,\n mode: 'host',\n }\n}\n\nfunction unavailableSnapshot(writable: boolean): SettingsScopeSnapshotLike {\n return {\n status: 'unavailable',\n value: undefined,\n base: undefined,\n user: undefined,\n revision: undefined,\n writable,\n mode: 'host',\n }\n}\n\n/**\n * Build the direct Host settings scope for one namespace.\n *\n * Snapshot semantics mirror the official per-namespace derivation: a namespace\n * row answers `ready` with the resolved section, its `base`/`user` layers and its\n * revision fence, and the document's `writable` decides editability. A row the\n * Host does not serve answers `unavailable`; a refused read keeps the last\n * confirmed document and reports `unavailable` only while it has never held one.\n *\n * Writes are serialized in issue order, fenced by the latest known revision and\n * REJECT with {@link SettingsWriteFailure} on a Host refusal — that rejection is\n * what makes the settings page surface a conflict instead of pretending the edit\n * landed (the official scope resolves on `!ok` after its recovery read).\n *\n * @param remote - the `remote.settings` face of the page's Remote service.\n * @param namespace - the settings namespace this scope derives from.\n * @returns the scope plus its load/dispose seams.\n */\nexport function createHostDirectScope(remote: SettingsRemoteLike, namespace: string): HostDirectScope {\n let snapshot: SettingsScopeSnapshotLike = {\n status: 'loading',\n value: undefined,\n base: undefined,\n user: undefined,\n revision: undefined,\n writable: false,\n mode: 'host',\n }\n let held: SettingsScopeSnapshotLike | undefined\n let error: string | undefined\n let disposed = false\n let tail: Promise = Promise.resolve()\n const listeners = new Set<() => void>()\n\n const publish = (next: SettingsScopeSnapshotLike): void => {\n if (disposed || sameSnapshot(next, snapshot)) return\n snapshot = next\n for (const listener of [...listeners]) {\n try {\n listener()\n } catch {\n // A broken listener must never break the channel.\n }\n }\n }\n\n const adopt = (next: SettingsScopeSnapshotLike): void => {\n if (next.status === 'ready') held = next\n publish(next)\n }\n\n async function load(): Promise {\n if (disposed) return\n let outcome: SettingsRemoteOutcome\n try {\n outcome = await remote.describe()\n } catch (caught) {\n outcome = { ok: false, error: { message: messageOf(caught) } }\n }\n if (disposed) return\n if (!outcome.ok) {\n error = outcome.error?.message ?? outcome.error?.code ?? 'settings/describe-failed'\n // A refused read never invents state: the last confirmed document stays\n // authoritative; without one the channel is unavailable.\n publish(held ?? unavailableSnapshot(snapshot.writable))\n return\n }\n error = undefined\n const view = outcome.value\n const writable = view.writable === true\n const row = Array.isArray(view.namespaces)\n ? view.namespaces.find(candidate => candidate.ns === namespace)\n : undefined\n if (row === undefined || typeof row.revision !== 'number') {\n publish(unavailableSnapshot(writable))\n return\n }\n adopt(readySnapshot(row, writable))\n }\n\n /** Fold one write answer's namespace row in without a second wire read. */\n function fold(row: SettingsNamespaceViewLike, writable: boolean): void {\n if (typeof row.revision !== 'number') return\n adopt(readySnapshot(row, writable))\n }\n\n function mutate(ops: readonly SettingsPathOpLike[], expectedRevision?: number): Promise {\n const owned = cloneOps(ops)\n const run = tail.then(async () => {\n if (disposed) throw new SettingsWriteFailure('settings/unavailable')\n const revision = expectedRevision ?? snapshot.revision\n let outcome: SettingsRemoteOutcome\n try {\n outcome = revision === undefined\n ? await remote.mutate(namespace, owned)\n : await remote.mutate(namespace, owned, revision)\n } catch (caught) {\n await load()\n throw new SettingsWriteFailure('settings/unreachable', messageOf(caught))\n }\n if (outcome.ok) {\n // A successful write proves the document accepts writes on this page.\n fold(outcome.value, true)\n return\n }\n await load()\n throw new SettingsWriteFailure(\n outcome.error?.code ?? 'settings/unknown',\n outcome.error?.message,\n )\n })\n // The queue never poisons itself: the caller sees the rejection, the next\n // queued write still runs.\n tail = run.then(() => undefined, () => undefined)\n return run\n }\n\n return {\n getSnapshot: () => snapshot,\n subscribe(listener) {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n },\n // The official scope implements `set` exactly this way; the revision fence\n // falls back to the latest known one, same as the official pendingRevision.\n set: (field, value) => mutate([{ op: 'set', path: [field], value }]),\n load,\n dispose() {\n disposed = true\n listeners.clear()\n },\n lastError: () => error,\n }\n}\n","/**\n * dsh-update-status — official settings-form adapter.\n *\n * DSH 0.1.7 removed the per-namespace `settingsScope` service this plugin's\n * client half was built on. Its successor is `ctx.configForms`\n * (`@deepseek-ai/dsh-client-ui-settings`): one shared `settings.describe`\n * mirror per browser plus a per-entry write queue, reached as\n * `ctx.configForms.get(entryId)` for the entry the plugin's Host half owns.\n * That form already answers the exact facts this plugin's own scope contract\n * needs — status, value, base, user, revision, writable, mode — so this module\n * only projects it onto {@link SettingsScopeLike} and leaves the existing\n * channel selection (`settingsChannel.ts`) in charge of the LAN fallback.\n *\n * Two deliberate translations:\n *\n * - A refused write (`set` answers `false`) becomes a REJECTION. The settings\n * page must surface a conflict instead of pretending the edit landed, which\n * is the same contract the direct Host channel already obeys\n * (`hostDirectScope.ts`). A transport fault rejects too.\n * - `mode` is what separates the two pages: DSH pins it to `memory` on a\n * non-loopback page, where the form is terminally `unavailable` and never\n * writes — exactly the state that hands the preferences to the direct Host\n * Remote so a phone still edits the one shared entry.\n *\n * The service is deliberately read structurally: the client bundle purity gate\n * forbids cross-plugin value imports, and the runtime object belongs to the\n * user's harness.\n */\nimport type { SettingsScopeLike, SettingsScopeSnapshotLike } from './scopeFaces.ts'\nimport { SettingsWriteFailure } from './hostDirectScope.ts'\n\n/** One form snapshot as `ConfigForm.getSnapshot()` answers it. */\nexport interface ConfigFormSnapshotLike {\n status: 'loading' | 'ready' | 'unavailable'\n value: T | undefined\n base: unknown\n user: unknown\n revision: number | undefined\n writable: boolean\n mode: 'host' | 'memory'\n}\n\n/** The subset of `ConfigForm` this adapter reads. */\nexport interface ConfigFormLike {\n getSnapshot(): ConfigFormSnapshotLike\n subscribe(listener: () => void): () => void\n set(field: string, value: unknown): Promise\n}\n\n/** The `ctx.configForms` binder face. */\nexport interface ConfigFormsFace {\n get(entryId: string): ConfigFormLike\n}\n\n/**\n * Guard: read the `configForms` face off an injection payload.\n *\n * The literal service key is read inside a `try`, because an injected payload\n * proxy can refuse a member read (the failure this plugin already hit on the\n * dotted `remote.settings` parent). Absence and refusal both answer\n * `undefined`, which leaves the channel on its direct Host path.\n *\n * @param raw - the payload a `ctx.inject(['configForms'], …)` callback received.\n * @returns the binder face, or undefined when this page has no settings forms.\n */\nexport function configFormsOf(raw: unknown): ConfigFormsFace | undefined {\n if (raw === null || typeof raw !== 'object') return undefined\n let face: unknown\n try {\n face = (raw as Record)['configForms']\n } catch {\n return undefined\n }\n if (face === null || typeof face !== 'object') return undefined\n if (typeof (face as { get?: unknown }).get !== 'function') return undefined\n return face as ConfigFormsFace\n}\n\n/**\n * Project one official config form onto the plugin's settings-scope contract.\n *\n * @param form - the shared form for the plugin's own Loader entry id.\n * @returns a scope the preferences store and the channel selection can use as-is.\n */\nexport function configFormScope(form: ConfigFormLike): SettingsScopeLike {\n return {\n getSnapshot(): SettingsScopeSnapshotLike {\n const snapshot = form.getSnapshot()\n return {\n status: snapshot.status,\n value: snapshot.value as SettingsScopeSnapshotLike['value'],\n base: snapshot.base,\n user: snapshot.user,\n revision: snapshot.revision,\n writable: snapshot.writable,\n mode: snapshot.mode,\n }\n },\n subscribe(listener) {\n return form.subscribe(listener)\n },\n async set(field, value) {\n let accepted: boolean\n try {\n accepted = await form.set(field, value)\n } catch (error) {\n // Transport fault: the write did not land and the caller must not\n // pretend it did (the recovery read belongs to the form itself).\n throw new SettingsWriteFailure(\n 'settings/unreachable',\n error instanceof Error ? error.message : String(error),\n )\n }\n if (!accepted) {\n // Refused or skipped (memory mode, stale revision, Host validation):\n // the form has already re-read the authoritative value.\n throw new SettingsWriteFailure('settings/conflict')\n }\n },\n }\n}\n","/**\n * dsh-update-status — settings source selection.\n *\n * The plugin's ONE storage contract is the Host settings entry\n * `dsh-update-status`. Two client channels can serve it:\n *\n * 1. the official `ctx.configForms` form (loopback pages) — the DSH 0.1.7\n * successor of the removed `settingsScope` service, and\n * 2. the direct Host channel (`hostDirectScope.ts`), which exists exactly\n * because the official form is deliberately inert on a non-loopback page —\n * the LAN page a phone uses.\n *\n * This module picks between them without ever guessing:\n *\n * - The official form wins whenever it is not `unavailable`, so a loopback page\n * keeps the official semantics (the one shared describe mirror, the official\n * write queue and revision fences) and pays NO extra wire read.\n * - Only when the official form reports `unavailable` (the documented\n * non-loopback degradation: it is pinned to `memory` mode there and never\n * crosses the wire) is the direct channel opened, lazily and once.\n * - With neither source the channel stays `loading`, i.e. exactly the state this\n * plugin had before the direct channel existed.\n */\nimport type { SettingsScopeLike, SettingsScopeSnapshotLike } from './scopeFaces.ts'\n\n/** Snapshot used while no channel has answered yet. */\nconst LOADING_SNAPSHOT: SettingsScopeSnapshotLike = Object.freeze({\n status: 'loading',\n value: undefined,\n base: undefined,\n user: undefined,\n revision: undefined,\n writable: false,\n mode: 'host',\n}) as SettingsScopeSnapshotLike\n\nexport interface SettingsChannelOptions {\n /**\n * Build the direct Host channel. Called at most once, and only when the\n * official scope cannot serve this page. Returning `undefined` (the Remote\n * service is not composed) leaves the channel on the official/unavailable\n * path; a later {@link SettingsChannel.refresh} retries.\n */\n openDirect(): SettingsScopeLike | undefined\n}\n\nexport interface SettingsChannel extends SettingsScopeLike {\n /** Point the channel at the official form once the service answers. */\n setOfficial(scope: SettingsScopeLike | undefined): void\n /** Re-evaluate the selection (e.g. the Remote service just arrived). */\n refresh(): void\n /** Ask the active source for a fresh read (no-op for the official form). */\n reload(): void\n dispose(): void\n}\n\nfunction sameSnapshot(a: SettingsScopeSnapshotLike, b: SettingsScopeSnapshotLike): boolean {\n return a.status === b.status\n && a.revision === b.revision\n && a.writable === b.writable\n && a.mode === b.mode\n && a.value === b.value\n && a.base === b.base\n && a.user === b.user\n}\n\n/** The load seam a direct channel exposes (the official scope refreshes itself). */\ntype LoadableScope = SettingsScopeLike & { load?: () => Promise }\n\n/** Ask a channel for its first/next read; a refusal surfaces as a snapshot, never a throw. */\nfunction loadSource(source: SettingsScopeLike | undefined): Promise {\n const loadable = source as LoadableScope | undefined\n if (loadable === undefined || typeof loadable.load !== 'function') return Promise.resolve()\n return Promise.resolve(loadable.load()).then(() => undefined, () => undefined)\n}\n\nexport function createSettingsChannel(options: SettingsChannelOptions): SettingsChannel {\n let official: SettingsScopeLike | undefined\n let direct: SettingsScopeLike | undefined\n let active: SettingsScopeLike | undefined\n let snapshot: SettingsScopeSnapshotLike = LOADING_SNAPSHOT\n let disposed = false\n const disposers: Array<() => void> = []\n const listeners = new Set<() => void>()\n\n const notify = (): void => {\n for (const listener of [...listeners]) {\n try {\n listener()\n } catch {\n // A broken listener must never break the channel.\n }\n }\n }\n\n const publish = (next: SettingsScopeSnapshotLike): void => {\n if (disposed || sameSnapshot(next, snapshot)) return\n snapshot = next\n notify()\n }\n\n const select = (): SettingsScopeLike | undefined => {\n // The official scope is authoritative unless it reports the documented\n // non-loopback degradation.\n if (official !== undefined && official.getSnapshot().status !== 'unavailable') return official\n if (direct === undefined) {\n try {\n direct = options.openDirect()\n } catch {\n direct = undefined\n }\n if (direct !== undefined) {\n try {\n disposers.push(direct.subscribe(() => {\n if (active === direct) publish(direct!.getSnapshot())\n }))\n } catch {\n // An unsubscribable direct channel is still readable synchronously.\n }\n // The direct channel has no owner to `ensure()` it (the official scope is\n // ensured by its own plugin), so the initial read is issued here.\n void loadSource(direct)\n }\n }\n return direct ?? official\n }\n\n const reselect = (): void => {\n if (disposed) return\n const next = select()\n if (next !== active) {\n active = next\n publish(next?.getSnapshot() ?? LOADING_SNAPSHOT)\n return\n }\n // Same source: forward its current snapshot (a change must have come from it).\n publish(next?.getSnapshot() ?? LOADING_SNAPSHOT)\n }\n\n return {\n getSnapshot: () => snapshot,\n subscribe(listener) {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n },\n set(field, value) {\n const source = active\n if (source === undefined) {\n return Promise.reject(new Error('dsh-update-status: settings channel is unavailable'))\n }\n return source.set(field, value)\n },\n setOfficial(scope) {\n if (disposed) return\n official = scope\n if (scope !== undefined) {\n try {\n disposers.push(scope.subscribe(() => {\n reselect()\n }))\n } catch {\n // Guarded read path: selection still happens synchronously below.\n }\n }\n reselect()\n },\n refresh: reselect,\n reload() {\n if (active === undefined || active === official) return\n void loadSource(active)\n },\n dispose() {\n disposed = true\n listeners.clear()\n for (const dispose of disposers.splice(0)) {\n try {\n dispose()\n } catch {\n // Contained.\n }\n }\n const disposable = active as (SettingsScopeLike & { dispose?: () => void }) | undefined\n if (disposable !== undefined && typeof disposable.dispose === 'function') {\n try {\n disposable.dispose()\n } catch {\n // Contained.\n }\n }\n },\n }\n}\n","/** Plugin-owned CSS only; no shell DOM selection or official SVG manipulation. */\n\nexport const UPDATE_STATUS_CSS = `\n.dus-brand-name{align-items:center;gap:6px;height:24px;max-width:100%;min-width:0;display:inline-flex}\n.dus-brand-deepseek{font-size:14px;font-weight:650;letter-spacing:-.015em;line-height:24px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n/* The chip is a NEUTRAL SECOND-LEVEL SURFACE, never a high-contrast pill. It used\n to be label-primary + label-primary-inverted, which is a near-white box with dark\n text on the dark shell — a white frame that swallows the amber halo and fights the\n warn colour. --dsw-alias-button-floating-hover is the one palette surface that is\n grey in BOTH themes (#f1f3f5 light / #353638 dark), matching the reference: a light\n grey chip on the light shell, a dark grey chip with white text on the dark one. The\n sibling button-floating-fill is pure white in light mode and changes nothing there.\n Text stays label-primary (#0f1115 light / #f9fafb dark), so it is never low-contrast.\n Every colour here is a theme token, so the chip follows whatever appearance DSH\n resolves — light, dark, or system-follows-the-OS — with no plugin-side detection,\n no media query and no per-theme branch to keep in sync. */\n.dus-badge{--dus-badge-surface:var(--dsw-alias-button-floating-hover,#f1f3f5);align-items:center;background:var(--dus-badge-surface);border:0;border-radius:4px;color:var(--dsw-alias-label-primary,#0f1115);cursor:pointer;display:inline-flex;flex:none;font-family:var(--ds-font-family-code,var(--dsw-font-family-mono,ui-monospace,SFMono-Regular,Menlo,monospace));font-size:10px;font-variant-numeric:tabular-nums;font-weight:600;gap:4px;height:16px;line-height:16px;max-width:140px;outline:none;padding:0 6px;touch-action:manipulation;user-select:none;white-space:nowrap}\n.dus-badge:focus-visible,.dus-footer-button:focus-visible,.dus-action:focus-visible,.dus-close:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}\n/* Hover has to read as \"raised\" in BOTH themes. brightness() only ever brightens, and on\n the light shell it turns the grey chip pure white — the chip all but disappears. Mixing\n a little of the theme's own label colour into the surface darkens it in light mode and\n lightens it in dark mode, so one declaration stays correct in both. The brightness\n filter remains the fallback: an engine without color-mix drops only the second\n declaration, never the whole hover rule. */\n.dus-badge:hover{filter:brightness(1.08)}\n@supports (background:color-mix(in srgb,red 50%,transparent)){.dus-badge:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary) 8%,var(--dus-badge-surface));filter:none}}\n/* The failure state is the one deliberate repaint, and it follows the shell's own\n badge pattern (brokenBadge): error fill with a bg-layer-3 label, which is white\n text on the dark red of the light theme and dark text on the light red of the dark\n theme — readable in both, unlike the fixed white text it replaces. */\n.dus-badge[data-error=true]{background:var(--dsw-alias-state-error-primary,#dc2626);color:var(--dsw-alias-bg-layer-3,#fff)}\n.dus-badge-version{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.dus-dot{background:currentColor;border-radius:50%;display:inline-block;flex:0 0 auto;height:5px;opacity:.9;width:5px}\n/* An available update repaints NOTHING: the chip keeps its normal fill and only\n the amber dot breathes. The halo is animated with the dot so the glow grows\n and shrinks as one light, never as a separate ripple. The update state is\n declared AFTER the loading state, so a re-check on a known update keeps the\n amber glow instead of falling back to the neutral loading pulse.\n Each box-shadow is declared twice: a literal amber first for any engine\n without color-mix, then the halo mixed from the theme's own warn colour so the\n glow always matches the dot in both light and dark themes. */\n/* The dot carries the whole status. Three states, in precedence order:\n up to date = a plain green circle (success token, never the text colour, so a\n dark theme no longer paints a black dot that reads as \"off\"); checking = the\n neutral grey pulse; a pending update = the amber dot below, which is the only\n state allowed to breathe or glow.\n The up-to-date rule is declared BEFORE loading and update: equal specificity, so\n the later, louder states win if two ever land on the same dot. */\n.dus-dot[data-state=current]{background:var(--dsw-alias-state-success-primary,#22c55e);opacity:1}\n.dus-dot[data-loading=true]{animation:dus-pulse .9s ease-in-out infinite}\n.dus-dot[data-update=true]{--dus-update-color:var(--dsw-alias-state-warn-primary,#f59e0b);animation:dus-update-pulse 1.6s ease-in-out infinite;background:var(--dus-update-color);height:6px;opacity:1;width:6px}\n@keyframes dus-pulse{0%,100%{opacity:.45;transform:scale(.82)}50%{opacity:1;transform:scale(1.15)}}\n@keyframes dus-update-pulse{0%,100%{box-shadow:0 0 2px 0 rgba(245,158,11,.4);box-shadow:0 0 2px 0 color-mix(in srgb,var(--dus-update-color) 45%,transparent);transform:scale(.8)}50%{box-shadow:0 0 7px 2px rgba(245,158,11,.72);box-shadow:0 0 7px 2px color-mix(in srgb,var(--dus-update-color) 72%,transparent);transform:scale(1.15)}}\n@media (prefers-reduced-motion:reduce){.dus-dot[data-update=true],.dus-dot[data-loading=true]{animation:none}.dus-dot[data-update=true]{box-shadow:0 0 5px 1px rgba(245,158,11,.6);box-shadow:0 0 5px 1px color-mix(in srgb,var(--dus-update-color) 62%,transparent)}}\n.dus-footer-button{align-items:center;background:transparent;border:0;border-radius:10px;color:var(--dsw-alias-label-secondary);cursor:pointer;display:flex;height:44px;justify-content:center;min-height:44px;min-width:44px;padding:0;position:relative;touch-action:manipulation;width:44px}\n.dus-footer-button:hover{background:var(--dsw-alias-button-floating-hover);color:var(--dsw-alias-label-primary)}\n.dus-footer-icon{font-size:18px;line-height:1}\n.dus-footer-dot{border:1.5px solid var(--dsw-specific-sidebar-fill);position:absolute;right:9px;top:9px}\n.dus-overlay-root{background:transparent;border:0;color:inherit;height:100dvh;inset:0;margin:0;max-height:none;max-width:none;padding:0;pointer-events:none;position:fixed;width:100vw}\n.dus-overlay-root::backdrop{background:transparent}\n.dus-backdrop{background:transparent;inset:0;pointer-events:auto;position:absolute}\n.dus-panel{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:14px;box-shadow:var(--dsw-elevation-panel);box-sizing:border-box;color:var(--dsw-alias-label-primary);max-height:min(640px,calc(100dvh - 32px - env(safe-area-inset-top) - env(safe-area-inset-bottom)));overflow:auto;padding:14px;pointer-events:auto;position:absolute;top:12px;width:min(390px,calc(100vw - 24px));z-index:1}\n.dus-panel[data-origin=brand]{left:min(292px,calc(100vw - 402px))}\n.dus-panel[data-origin=rail]{left:min(68px,calc(100vw - 402px))}\n.dus-panel-head{align-items:center;display:flex;gap:8px;justify-content:space-between;margin-bottom:12px}\n.dus-panel-title{font-size:14px;font-weight:650}\n.dus-close{align-items:center;background:transparent;border:0;border-radius:8px;color:inherit;cursor:pointer;display:inline-flex;font-size:20px;height:32px;justify-content:center;line-height:1;min-height:32px;min-width:32px;padding:0;touch-action:manipulation;width:32px}\n.dus-close:hover,.dus-action:hover{background:var(--dsw-alias-button-floating-hover)}\n.dus-cache-setting{border:1px solid var(--dsw-alias-border-l2);border-radius:9px;margin:0 0 12px;padding:9px}\n.dus-cache-field{align-items:center;display:flex;font-size:12px;font-weight:600;gap:10px;justify-content:space-between}\n.dus-cache-input-wrap{align-items:center;display:flex;gap:6px;font-size:11px;font-weight:400}\n.dus-cache-input{background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:7px;color:inherit;font:inherit;min-height:30px;padding:0 7px;text-align:right;width:72px}\n.dus-cache-input:focus{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}\n.dus-cache-input:disabled{cursor:not-allowed;opacity:.55}\n.dus-cache-hint{color:var(--dsw-alias-label-secondary);font-size:11px;line-height:1.4;margin:7px 0 0}\n.dus-state{border-radius:9px;color:var(--dsw-alias-label-primary);font-size:12px;line-height:1.45;margin:0 0 12px;padding:8px 9px}\n.dus-state[data-kind=update],.dus-state[data-kind=warning]{background:var(--dsw-alias-state-warn-tertiary)}\n.dus-state[data-kind=ok]{background:var(--dsw-alias-state-success-tertiary)}\n.dus-state[data-kind=error]{background:var(--dsw-alias-bg-layer-3)}\n.dus-metadata{display:grid;gap:9px;margin:0 0 12px}\n.dus-metadata-row{align-items:baseline;display:grid;gap:8px;grid-template-columns:minmax(0,1fr) auto}\n.dus-meta-label{color:var(--dsw-alias-label-secondary);font-size:12px}\n.dus-meta-value{font-family:var(--dsw-font-family-mono,ui-monospace,SFMono-Regular,Menlo,monospace);font-size:12px;max-width:210px;overflow-wrap:anywhere;text-align:right}\n.dus-meta-sub{color:var(--dsw-alias-label-secondary);font-size:11px;grid-column:1 / -1}\n.dus-warning{border-left:2px solid var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.45;margin:0 0 12px;padding-left:8px}\n.dus-actions{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 12px}\n.dus-action{background:var(--dsw-alias-button-floating-fill);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;color:inherit;cursor:pointer;font-size:12px;line-height:30px;min-height:32px;padding:0 10px;touch-action:manipulation}\n.dus-action:disabled{cursor:wait;opacity:.65}\n.dus-command-label{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;margin:0 0 6px}\n.dus-command{background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;display:block;font-family:var(--dsw-font-family-mono,ui-monospace,SFMono-Regular,Menlo,monospace);font-size:11px;line-height:1.5;margin:0;overflow-wrap:anywhere;padding:9px;tab-size:2;user-select:text;white-space:pre-wrap}\n.dus-command:focus{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}\n.dus-note{color:var(--dsw-alias-label-secondary);font-size:11px;line-height:1.45;margin:8px 0 0}\n.dus-copy-message{color:var(--dsw-alias-state-business-primary);font-size:11px;margin:7px 0 0}\n.dus-settings{display:grid;gap:14px;max-width:640px;padding:4px 0}\n.dus-settings-heading{font-size:16px;font-weight:650;margin:0}\n.dus-settings-card{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:12px}\n.dus-settings-toggle{align-items:flex-start;cursor:pointer;display:flex;gap:10px;font-size:13px;line-height:1.4}\n.dus-settings-toggle input{accent-color:var(--dsw-alias-state-business-primary);height:18px;margin:0;min-height:18px;min-width:18px;width:18px}\n.dus-settings-field{display:grid;font-size:13px;font-weight:600;gap:8px}\n.dus-channel-select{background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;color:inherit;font:inherit;min-height:40px;padding:0 10px;width:100%}\n.dus-channel-list{border-top:1px solid var(--dsw-alias-border-l2);margin:12px 0 0;padding-top:2px}\n.dus-channel-row{align-items:center;display:grid;font-size:11px;gap:8px;grid-template-columns:minmax(0,1fr) auto;line-height:1.4;padding:6px 0}\n.dus-channel-row[data-selected=true]{color:var(--dsw-alias-state-business-primary)}\n.dus-channel-row code{font-size:11px}\n.dus-channel-compat{color:var(--dsw-alias-label-secondary)}\n.dus-channel-action{background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;color:inherit;cursor:pointer;font-size:11px;justify-self:end;min-height:32px;padding:0 9px}\n.dus-channel-action:disabled{cursor:default;opacity:.55}\n.dus-channel-action:not(:disabled):hover{background:var(--dsw-alias-button-floating-hover)}\n.dus-channel-compat[data-compatibility=verified]{color:var(--dsw-alias-state-success-primary)}\n.dus-channel-compat[data-compatibility=incompatible]{color:var(--dsw-alias-state-error-primary)}\n.dus-settings-hint{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.45;margin:8px 0 0}\n@media (max-width:640px),(hover:none) and (pointer:coarse){.dus-panel[data-origin]{border-bottom:0;border-bottom-left-radius:0;border-bottom-right-radius:0;bottom:0;left:0;right:0;max-height:min(78dvh,calc(100dvh - env(safe-area-inset-top)));padding:16px max(16px,env(safe-area-inset-right)) max(16px,env(safe-area-inset-bottom)) max(16px,env(safe-area-inset-left));position:fixed;top:auto;width:100vw}.dus-footer-button{height:48px;min-height:48px;min-width:48px;width:48px}.dus-action{min-height:40px;line-height:38px}.dus-close{height:40px;min-height:40px;min-width:40px;width:40px}}\n`\n","/**\n * DSH Web client half. It shadows only `sidebar.brand.name` with a compact\n * name + version chip that fits the 24px brand row, leaves the official fish\n * (`sidebar.brand.mark`) untouched, and adds a collapsed-rail fallback at\n * `sidebar.footer.action`. The detail panel is an additive `shell.overlay`,\n * never a chat-area floating widget.\n *\n * The status read and the detail panel are NOT restricted to a loopback page.\n * DSH disables Host settings *persistence* on a non-loopback page, but the\n * Connection RPC stays authenticated and reachable there — see\n * settings/settingsChannel.ts for the one place the loopback distinction still\n * matters.\n *\n * Preferences live in the Host-side `dsh-update-status` settings entry — the\n * plugin's own Loader entry, whose volatile `Config` fields DSH 0.1.7 projects\n * as that entry's form. Two client channels can serve it: the official\n * `ctx.configForms.get(entryId)` form (the successor of the removed\n * `settingsScope` service) and, on the non-loopback pages where that form is\n * deliberately inert, a direct Host channel over `remote.settings`. See\n * settingsChannel.ts.\n */\n\nimport type { ClientContext, ConnectionClient } from './contract.ts'\nimport { BrandName, FooterAction, type SharedUi, UpdatePanel, UpdateSettings } from './components.tsx'\nimport { SETTINGS_NAMESPACE, PanelStore, PreferencesStore, StatusStore } from './stores.ts'\nimport { configFormScope, configFormsOf } from './settings/configFormScope.ts'\nimport {\n createHostDirectScope,\n settingsInvalidationsOf,\n settingsRemoteFace,\n settingsRemoteOf,\n type SettingsRemoteLike,\n} from './settings/hostDirectScope.ts'\nimport { createSettingsChannel } from './settings/settingsChannel.ts'\nimport { t } from './i18n.ts'\nimport { UPDATE_STATUS_CSS } from './styles.ts'\n\nconst PLUGIN_ID = 'dsh-update-status'\nconst CSS_TAG_ID = `${PLUGIN_ID}/styles`\n\nfunction installStyles(): () => void {\n if (typeof document === 'undefined') return () => {}\n const existing = document.querySelector(`style[data-plugin-css=\"${CSS_TAG_ID}\"]`)\n if (existing !== null) return () => {}\n const tag = document.createElement('style')\n tag.dataset.plugin = PLUGIN_ID\n tag.dataset.pluginCss = CSS_TAG_ID\n tag.textContent = UPDATE_STATUS_CSS\n document.head.appendChild(tag)\n return () => {\n const live = document.querySelector(`style[data-plugin-css=\"${CSS_TAG_ID}\"]`)\n if (live?.parentNode !== undefined && live?.parentNode !== null) live.parentNode.removeChild(live)\n }\n}\n\nfunction connectionOf(ctx: ClientContext): ConnectionClient {\n try {\n const candidate = ctx.get('connection')\n return candidate !== null && typeof candidate === 'object' ? candidate as ConnectionClient : {}\n } catch {\n return {}\n }\n}\n\nfunction apply(ctx: ClientContext): void {\n const connection = connectionOf(ctx)\n const status = new StatusStore(connection)\n const preferences = new PreferencesStore()\n const panel = new PanelStore()\n const ui: SharedUi = { status, preferences, panel }\n\n const disposers: Array<() => void> = []\n let stopped = false\n /** Own a subscription raised from an injection callback; release after teardown at once. */\n const own = (dispose: unknown): void => {\n if (typeof dispose !== 'function') return\n if (stopped) {\n try {\n (dispose as () => void)()\n } catch {\n // Contained.\n }\n return\n }\n disposers.push(dispose as () => void)\n }\n\n // The plugin's ONE storage contract is the Host settings entry\n // `dsh-update-status` (the plugin's own Loader entry id), but two client\n // channels can serve it: the official `configForms` form (loopback pages) and\n // — only when that form reports the documented non-loopback degradation — a\n // direct Host channel over the same public Remote. A LAN page is exactly that\n // non-loopback case; without the direct channel every preference silently\n // falls back to its default there. See settingsChannel.ts.\n let remoteSettings: SettingsRemoteLike | undefined\n /** Resolve the direct channel's Remote face; `ctx.get` covers a payload we could not read. */\n const directRemote = (): SettingsRemoteLike | undefined => {\n if (remoteSettings !== undefined) return remoteSettings\n try {\n remoteSettings = settingsRemoteFace(ctx.get('remote.settings'))\n } catch {\n // The dotted service key is not readable on this context.\n }\n return remoteSettings\n }\n const channel = createSettingsChannel({\n openDirect: () => {\n const remote = directRemote()\n return remote === undefined ? undefined : createHostDirectScope(remote, SETTINGS_NAMESPACE)\n },\n })\n\n ctx.effect(() => {\n const disposeStyles = installStyles()\n const detach = preferences.attach(channel)\n // Changing cache policy deliberately does not issue a network request. Its\n // value is sent on the next ordinary status read or manual check.\n let selected = status.getSnapshot().status?.channel ?? 'latest'\n const syncPreferences = () => {\n const next = preferences.getSnapshot()\n if (next.channel !== selected) {\n selected = next.channel\n void status.selectChannel(selected, next.cacheTtlMinutes)\n }\n }\n syncPreferences()\n const unsubscribe = preferences.subscribe(syncPreferences)\n // Every mount reads the Host's cached status, whichever page it is on; the\n // transport is authenticated and the Host route is the plugin's own.\n void status.load(preferences.getSnapshot().cacheTtlMinutes)\n return () => {\n stopped = true\n unsubscribe()\n detach()\n for (const dispose of disposers.splice(0)) {\n try {\n dispose()\n } catch {\n // Contained.\n }\n }\n channel.dispose()\n status.stop()\n disposeStyles()\n }\n }, 'dsh-update-status: styles, settings channel and first status read')\n\n // Official seam first: it stays authoritative whenever it is not `unavailable`,\n // so a loopback page keeps the official semantics (one shared describe mirror,\n // the official write queue) and pays no extra wire read. DSH 0.1.7 replaced\n // the per-namespace `settingsScope` service with these entry-addressed forms,\n // keyed by the Loader entry id the Host Config half owns (SETTINGS_NAMESPACE).\n try {\n ctx.inject(['configForms'], (raw: unknown) => {\n try {\n const forms = configFormsOf(raw)\n if (forms === undefined) return\n channel.setOfficial(configFormScope(forms.get(SETTINGS_NAMESPACE)))\n } catch {\n // Unreadable settings seam: the preferences stay on their defaults.\n }\n })\n } catch {\n // No configForms seam on this host: the direct channel is the only hope.\n }\n\n // Direct Host channel. The Remote service can arrive before or after the\n // official scope, so `refresh()` re-evaluates the selection either way; the\n // document invalidation keeps this page in step with edits made elsewhere,\n // and a reconnect retries a read that was refused.\n //\n // Every read here is contained: the injected payload intentionally refuses the\n // dotted PARENT (`payload.remote` throws \"cannot get property … without\n // inject\"), so one unreadable member must never abort the whole wiring.\n try {\n ctx.inject(['remote.settings'], (raw: unknown) => {\n try {\n remoteSettings = settingsRemoteOf(raw) ?? remoteSettings\n channel.refresh()\n const invalidations = settingsInvalidationsOf(ctx.get('remote'))\n if (invalidations !== undefined) own(invalidations(() => channel.reload()))\n } catch {\n // No usable Remote seam: the direct channel stays closed and the official\n // scope keeps its verdict.\n }\n })\n } catch {\n // No Remote seam: non-loopback pages keep the honest unavailable state.\n }\n if (typeof ctx.on === 'function') {\n try {\n own(ctx.on('connection/reset', () => channel.reload()))\n } catch {\n // No lifecycle event seam on this host.\n }\n }\n\n // Single slot: a compact name + version chip shadows the official wordmark\n // (priority 0). Lowest priority renders; never touch sidebar.brand.mark.\n ctx.slots.inject('sidebar.brand.name', () => ctx.slots.register(\n { name: 'sidebar.brand.name', priority: -10 },\n () => BrandName({ ui }),\n ))\n\n // additive fallback; returns null in wide mode to avoid two visible badges.\n ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register(\n { name: 'sidebar.footer.action', id: 'dsh-update-status', order: 40 },\n props => FooterAction({ wide: props.wide, ui }),\n ))\n\n // additive frame overlay for click/tap panel and narrow-view bottom sheet.\n ctx.slots.inject('shell.overlay', () => ctx.slots.register(\n { name: 'shell.overlay', id: 'dsh-update-status', order: 40 },\n () => UpdatePanel({ ui }),\n ))\n\n // Independent settings section provides the required way to hide our entry.\n ctx.slots.inject('settings.section', () => ctx.slots.register(\n { name: 'settings.section', id: 'dsh-update-status', order: 80, label: () => t('settings.title') },\n () => UpdateSettings({ ui }),\n ))\n}\n\nmodule.exports = {\n name: PLUGIN_ID,\n inject: ['slots', 'connection'],\n apply,\n}\n"],"mappings":"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"} \ No newline at end of file diff --git a/lib/index.d.ts b/lib/index.d.ts index 7f59008..06d324c 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -1,5 +1,5 @@ -import z from "@deepseek-ai/schemastery"; import { Context } from "@deepseek-ai/cordis"; +import z from "@deepseek-ai/schemastery"; //#region src/shared/types.d.ts declare const RELEASE_CHANNELS: readonly ['latest', 'next', 'alpha']; type ReleaseChannel = (typeof RELEASE_CHANNELS)[number]; @@ -38,6 +38,18 @@ type UpdateWarning = { code: 'preview-unverified'; channel: ReleaseChannel; version: string; +} | +/** + * The Host resolved a `@deepseek-ai/schemastery` that DSH does not ship, so the + * Loader's volatile projection is unavailable and the preference fields cannot + * be marked volatile. Advisory: the version answer itself is complete, and the + * text carries the exact directory to remove. See `host/schemastery.ts`. + */ +{ + code: 'stale-schemastery'; + version: string | null; + path: string; + nodeModulesDir: string | null; }; interface ChannelRelease { channel: ReleaseChannel; @@ -98,7 +110,8 @@ interface UpdateStatusConfig { * The explicit two-argument annotation is load-bearing: a `.volatile()` field's * output is a stable reference (`Volatile`) rather than the bare value the * input side takes, so the inferred schema type cannot be named by the emitted - * `.d.ts` (TS2883) without stating the input side here. + * `.d.ts` (TS2883) without stating the input side here. The builder preserves + * that contract even on the degraded path, where no field is volatile. */ export declare const Config: z>; //#endregion @@ -123,6 +136,11 @@ interface UpdateStatusServiceOptions { now?: () => number; ttlMs?: number; releaseUrl?: string; + /** + * Facts about this process's own runtime, appended to every status. The schema + * resolution is the only producer today; the registry read never sets these. + */ + runtimeWarnings?: readonly UpdateWarning[]; } export declare class UpdateStatusService { private readonly installation; @@ -130,6 +148,7 @@ export declare class UpdateStatusService { private readonly now; private readonly ttlMs; private readonly releaseUrl; + private readonly runtimeWarnings; private cache; private inFlight; constructor(options: UpdateStatusServiceOptions); @@ -142,6 +161,59 @@ export declare class UpdateStatusService { private statusWithoutRemoteRelease; } //#endregion +//#region src/host/schemastery.d.ts +/** Where a candidate copy of schemastery was looked for. */ +type SchemaCandidateSource = 'dsh-install' | 'profile-peers' | 'profile-local' | 'plugin-local'; +interface SchemaCandidate { + source: SchemaCandidateSource; + /** Absolute file path `createRequire` anchors on; the file need not exist. */ + anchor: string; +} +interface SchemaCandidateReport { + source: SchemaCandidateSource; + anchor: string; + /** Absolute path of the module that loaded, or null when it did not load. */ + path: string | null; + version: string | null; + /** The loaded module exposes `volatile()`. */ + volatile: boolean; + /** Why this candidate was skipped, or null when it loaded. */ + error: string | null; +} +interface SchemaRuntime { + /** The loaded schemastery factory — structurally the package's default export. */ + z: unknown; + volatile: boolean; + source: SchemaCandidateSource; + path: string | null; + version: string | null; + /** Every candidate that was tried, in order, for diagnostics. */ + candidates: SchemaCandidateReport[]; + /** Set when the resolved copy cannot project volatile preferences. */ + warning: UpdateWarning | null; +} +interface ResolveSchemaRuntimeOptions { + /** Override the candidate order; used by tests and by an embedding host. */ + candidates?: readonly SchemaCandidate[]; +} +/** + * Resolve schemastery once, preferring the copy that can actually do the job. + * + * Only a total miss throws, and only with the per-candidate reasons attached: + * that state means the plugin is not running inside a working DSH installation, + * so there is no schema system to build a settings form with. Every other + * outcome loads. + */ +export declare function resolveSchemaRuntime(options?: ResolveSchemaRuntimeOptions): SchemaRuntime; +/** + * The process-wide resolution, computed on first use. + * + * The settings schema is built at module scope — the Loader reads the exported + * `Config` before `apply` runs — so the resolution cannot wait for a Host + * context. Every input here comes from the process environment or the filesystem. + */ +export declare function schemaRuntime(): SchemaRuntime; +//#endregion //#region src/index.d.ts export declare const name = "dsh-update-status"; /** Connection supplies the authenticated transport; settings remains optional. */ @@ -149,4 +221,4 @@ export declare const inject: string[]; export type Config = UpdateStatusConfig; export declare function apply(ctx: Context, config?: Config): void; //#endregion -export type { InstallKind, UpdateStatus }; \ No newline at end of file +export type { InstallKind, SchemaRuntime, UpdateStatus }; \ No newline at end of file diff --git a/lib/index.js b/lib/index.js index 50f3e3f..e009c74 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,7 +1,8 @@ import { createRequire } from "node:module"; import { existsSync, readFileSync, realpathSync } from "node:fs"; -import { dirname, join, normalize, sep } from "node:path"; -import z from "@deepseek-ai/schemastery"; +import { basename, dirname, join, normalize, sep } from "node:path"; +import { fileURLToPath } from "node:url"; +import { homedir } from "node:os"; //#region src/shared/types.ts const PACKAGE_NAME = "@deepseek-ai/dsh"; /** Shared Connection RPC channel. Custom prefixes 405 on the SPA fallback. */ @@ -51,7 +52,7 @@ function manifestAt(path) { return; } } -function realPath(path) { +function realPath$1(path) { try { return realpathSync(path); } catch { @@ -60,14 +61,14 @@ function realPath(path) { } /** Ascend from an entry file until the owning DSH package manifest is found. */ function manifestFromEntry(entryPath) { - let directory = dirname(realPath(entryPath)); + let directory = dirname(realPath$1(entryPath)); for (let depth = 0; depth < 10; depth += 1) { const manifest = join(directory, "package.json"); if (existsSync(manifest)) { const found = manifestAt(manifest); if (found !== void 0) return { ...found, - root: realPath(found.root) + root: realPath$1(found.root) }; } const parent = dirname(directory); @@ -80,7 +81,7 @@ function manifestFromResolver(resolve) { const found = manifestAt(resolve(PACKAGE_NAME + "/package.json")); if (found !== void 0) return { ...found, - root: realPath(found.root) + root: realPath$1(found.root) }; } catch {} try { @@ -114,7 +115,7 @@ function ownResolver(selfUrl) { */ function classifyInstallRoot(packageRoot, nodePrefix = process.execPath) { if (packageRoot === void 0) return "unknown"; - const root = normalize(realPath(packageRoot)).split(sep).join("/"); + const root = normalize(realPath$1(packageRoot)).split(sep).join("/"); const prefix = nodePrefix === void 0 ? "" : normalize(dirname(dirname(nodePrefix))).split(sep).join("/"); if (!root.includes("/node_modules/") && /\/apps\/cli(?:\/|$)/.test(root)) return "source-checkout"; if (root.includes("/.pnpm/") || /\/pnpm\/global(?:\/|$)/.test(root)) return "pnpm-global"; @@ -257,6 +258,278 @@ function installUpdateStatusRpc(ctx, service) { }); } //#endregion +//#region src/host/schemastery.ts +/** +* dsh-update-status — shadow-proof resolution of the platform's schemastery. +* +* `@deepseek-ai/schemastery` is a peer dependency: DSH ships it, and DSH's copy +* is the one carrying `volatile()` — the Loader's volatile-field projection this +* plugin's settings form is built on. A plain +* `import z from '@deepseek-ai/schemastery'` is resolved by Node's own directory +* walk *from the importing file*, so any copy sitting next to the plugin wins +* over the platform's copy. That is not hypothetical: an unmanaged tree left +* inside the install directory — a dev `node_modules` copied in by a +* local-directory install, which pnpm never removes — made `lib/index.js` throw +* `TypeError: …volatile is not a function` while it was being imported, so the +* whole host half disappeared before one line of plugin code ran. +* +* Nothing inside the plugin can repair that state: the failure precedes every +* plugin entry point. The resolution itself is made immune instead, trying +* platform copies before anything else: +* +* 1. the DSH installation running this process, +* 2. the profile peer farm DSH maintains (`$DSH_HOME/profiles/node_modules`), +* 3. the active profile's own tree, +* 4. whatever Node's walk finds next to this file — last resort. +* +* The first candidate that actually exposes `volatile()` wins, and the +* capability is verified rather than assumed. When no candidate provides it, the +* plugin still loads on the best copy it could load and reports exactly what it +* resolved: the missing capability becomes a `stale-schemastery` warning the +* panel renders instead of a dead plugin. +*/ +const SCHEMASTERY_NAME = "@deepseek-ai/schemastery"; +/** Anchor file name; it never has to exist, only to give `createRequire` a base. */ +const PROBE_FILE = "dsh-update-status-peer-probe.cjs"; +const MAX_ASCENT = 10; +function realPath(path) { + try { + return realpathSync(path); + } catch { + return path; + } +} +function boundedMessage$1(error) { + const trimmed = (error instanceof Error ? error.message : String(error)).replace(/\s+/g, " ").trim(); + return trimmed === "" ? "unknown error" : trimmed.slice(0, 220); +} +function environmentDirectory(name) { + const value = process.env[name]; + return typeof value === "string" && value.trim() !== "" ? value : null; +} +/** The DSH home directory, from the environment first and `~/.dsh` as a fallback. */ +function dshHomeDirectory() { + const configured = environmentDirectory("DSH_HOME"); + if (configured !== null) return configured; + try { + return join(homedir(), ".dsh"); + } catch { + return null; + } +} +/** The active profile directory, from `$DSH_PROFILE_DIR` or `$DSH_HOME/profiles/`. */ +function profileDirectory() { + const configured = environmentDirectory("DSH_PROFILE_DIR"); + if (configured !== null) return configured; + const home = dshHomeDirectory(); + const profile = environmentDirectory("DSH_PROFILE"); + return home === null || profile === null ? null : join(home, "profiles", profile); +} +/** +* Anchor on the DSH package that owns the running entry point. +* +* The entry is the CLI file Node was started with, so ascending from it reaches +* the DSH installation whose `node_modules` holds the platform's schemastery. +* Reusing `manifestFromEntry` keeps this probe identical to the installation +* detection the status read already performs. +*/ +function dshInstallAnchor() { + const entry = process.argv[1]; + if (typeof entry !== "string" || entry === "") return null; + const found = manifestFromEntry(entry); + return found === void 0 || found.root === "" ? null : join(found.root, "package.json"); +} +/** Candidate order: platform copies first, the shadowable walk last. */ +function defaultSchemaCandidates() { + const candidates = []; + const install = dshInstallAnchor(); + if (install !== null) candidates.push({ + source: "dsh-install", + anchor: install + }); + const home = dshHomeDirectory(); + if (home !== null) candidates.push({ + source: "profile-peers", + anchor: join(home, "profiles", PROBE_FILE) + }); + const profile = profileDirectory(); + if (profile !== null) candidates.push({ + source: "profile-local", + anchor: join(profile, PROBE_FILE) + }); + candidates.push({ + source: "plugin-local", + anchor: fileURLToPath(import.meta.url) + }); + return candidates; +} +function manifestVersion(manifestPath) { + try { + const parsed = JSON.parse(readFileSync(manifestPath, "utf8")); + if (parsed.name !== "@deepseek-ai/schemastery" || typeof parsed.version !== "string") return null; + const version = parsed.version.trim(); + return version === "" ? null : version; + } catch { + return null; + } +} +/** Read the version of the schemastery package that owns a resolved entry path. */ +function versionOfResolved(resolvedPath) { + let directory = dirname(realPath(resolvedPath)); + for (let depth = 0; depth < MAX_ASCENT; depth += 1) { + const manifestPath = join(directory, "package.json"); + if (existsSync(manifestPath)) { + const version = manifestVersion(manifestPath); + if (version !== null) return version; + } + const parent = dirname(directory); + if (parent === directory) break; + directory = parent; + } + return null; +} +/** The nearest `node_modules` directory above a resolved package entry. */ +function nodeModulesDirectoryOf(resolvedPath) { + if (resolvedPath === null) return null; + let directory = dirname(realPath(resolvedPath)); + for (let depth = 0; depth < MAX_ASCENT; depth += 1) { + if (basename(directory) === "node_modules") return directory; + const parent = dirname(directory); + if (parent === directory) break; + directory = parent; + } + return null; +} +/** +* Does this module expose the Loader's volatile projection? +* +* The probe builds a throwaway field because the capability is a prototype +* method, not a flag: 3.18.2 has no `volatile` at all, 3.18.4 (the copy DSH +* ships) does. Never throws — an unusable module is simply not a candidate. +*/ +function supportsVolatile(factory) { + if (factory === null || typeof factory !== "object" && typeof factory !== "function") return false; + try { + const boolean = factory.boolean; + if (typeof boolean !== "function") return false; + const field = boolean.call(factory); + if (field === null || field === void 0 || typeof field.default !== "function") return false; + const defined = field.default(true); + return defined !== null && defined !== void 0 && typeof defined.volatile === "function"; + } catch { + return false; + } +} +/** Can this value build a schema? A wrapper object that cannot is never chosen. */ +function isSchemaFactory(value) { + if (value === null || typeof value !== "object" && typeof value !== "function") return false; + const record = value; + return typeof record.object === "function" && typeof record.boolean === "function" && typeof record.number === "function"; +} +/** +* Pick the factory out of the shapes a CommonJS build can expose. +* +* The package assigns `module.exports` directly, but a bundler or an interop +* wrapper can put the same factory behind `default`. A shape that cannot build a +* schema is never chosen even as a fallback, because the degraded path still has +* to produce a `Config` — a wrapper object would throw while the entry loads, +* which is the failure this module exists to prevent. +*/ +function factoryOf(loaded) { + const shapes = [loaded, (loaded !== null && (typeof loaded === "object" || typeof loaded === "function") ? loaded : void 0)?.default].filter(isSchemaFactory); + return shapes.find(supportsVolatile) ?? shapes[0]; +} +function loadCandidate(candidate) { + const report = { + source: candidate.source, + anchor: candidate.anchor, + path: null, + version: null, + volatile: false, + error: null + }; + try { + const require = createRequire(candidate.anchor); + const resolved = require.resolve(SCHEMASTERY_NAME); + const factory = factoryOf(require(resolved)); + if (factory === void 0) throw new Error("module does not export a schemastery factory"); + report.path = realPath(resolved); + report.version = versionOfResolved(resolved); + report.volatile = supportsVolatile(factory); + return { + report, + factory + }; + } catch (error) { + report.error = boundedMessage$1(error); + return { + report, + factory: void 0 + }; + } +} +/** +* Resolve schemastery once, preferring the copy that can actually do the job. +* +* Only a total miss throws, and only with the per-candidate reasons attached: +* that state means the plugin is not running inside a working DSH installation, +* so there is no schema system to build a settings form with. Every other +* outcome loads. +*/ +function resolveSchemaRuntime(options = {}) { + const candidates = options.candidates ?? defaultSchemaCandidates(); + const reports = []; + let usable; + let loaded; + for (const candidate of candidates) { + const attempt = loadCandidate(candidate); + reports.push(attempt.report); + if (attempt.factory === void 0) continue; + loaded ??= attempt; + if (attempt.report.volatile) { + usable = attempt; + break; + } + } + const chosen = usable ?? loaded; + if (chosen === void 0) { + const tried = reports.map((report) => `${report.source}: ${report.error ?? "not loadable"}`).join("; "); + throw new Error(`dsh-update-status could not resolve ${SCHEMASTERY_NAME} (${tried})`); + } + return { + z: chosen.factory, + volatile: chosen.report.volatile, + source: chosen.report.source, + path: chosen.report.path, + version: chosen.report.version, + candidates: reports, + warning: chosen.report.volatile ? null : { + code: "stale-schemastery", + version: chosen.report.version, + path: chosen.report.path ?? chosen.report.anchor, + nodeModulesDir: nodeModulesDirectoryOf(chosen.report.path) + } + }; +} +let resolved; +/** +* The process-wide resolution, computed on first use. +* +* The settings schema is built at module scope — the Loader reads the exported +* `Config` before `apply` runs — so the resolution cannot wait for a Host +* context. Every input here comes from the process environment or the filesystem. +*/ +function schemaRuntime() { + resolved ??= resolveSchemaRuntime(); + return resolved; +} +/** English, single-line summary of a resolution, for the Host log. */ +function describeSchemaRuntime(runtime) { + const where = runtime.path === null ? runtime.source : `${runtime.path} (${runtime.source})`; + const version = runtime.version === null ? "" : `@${runtime.version}`; + return `${SCHEMASTERY_NAME}${version} from ${where}; volatile preferences ${runtime.volatile ? "available" : "unavailable"}`; +} +//#endregion //#region src/shared/semver.ts const SEMVER = /^(?:v)?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/; const NUMERIC_IDENTIFIER = /^(0|[1-9]\d*)$/; @@ -317,20 +590,34 @@ function boundedMessage(error) { const trimmed = (error instanceof Error ? error.message : String(error)).replace(/\s+/g, " ").trim(); return trimmed === "" ? "unknown error" : trimmed.slice(0, 220); } -function warningText(warning) { +/** English, single-line rendering of one warning; the Host log uses it directly. */ +function describeWarning(warning) { switch (warning.code) { case "registry-unavailable": return `Unable to check the npm registry: ${warning.detail}`; case "channel-unavailable": return `The npm registry does not publish a ${warning.channel} channel.`; case "version-incomparable": return `Unable to compare the current version ${warning.currentVersion} with ${warning.channel} channel version ${warning.selectedVersion} using SemVer.`; case "preview-unverified": return `${warning.channel} is a preview channel; version ${warning.version} has not been verified as compatible with this plugin.`; + case "stale-schemastery": { + const version = warning.version === null ? "" : ` ${warning.version}`; + const remedy = warning.nodeModulesDir === null ? `Remove the stale ${SCHEMASTERY_NAME} directory there and restart DSH.` : `Remove the stale copy and restart DSH: rm -rf ${warning.nodeModulesDir}`; + return `This plugin resolved ${SCHEMASTERY_NAME}${version} from ${warning.path} instead of the copy DSH provides, so preference fields cannot be marked volatile. ${remedy}`; + } } } function warningFallback(warnings) { - return warnings.length === 0 ? null : warnings.map(warningText).join(" "); + return warnings.length === 0 ? null : warnings.map(describeWarning).join(" "); } +/** +* Advisory codes leave the chip alone; everything else is a failed read. +* +* `stale-schemastery` belongs here: the version answer is complete and usable, +* and only the settings form is degraded, so repainting the brand row would +* misreport a plugin-runtime fact as a failed update check. +*/ +const ADVISORY_CODES = ["preview-unverified", "stale-schemastery"]; function warningKindOf(warnings) { if (warnings.length === 0) return null; - return warnings.every((warning) => warning.code === "preview-unverified") ? "notice" : "failure"; + return warnings.every((warning) => ADVISORY_CODES.includes(warning.code)) ? "notice" : "failure"; } function dateOrNull(value) { if (typeof value !== "string" || value === "") return null; @@ -399,6 +686,7 @@ var UpdateStatusService = class { now; ttlMs; releaseUrl; + runtimeWarnings; cache; inFlight; constructor(options) { @@ -407,6 +695,7 @@ var UpdateStatusService = class { this.now = options.now ?? Date.now; this.ttlMs = Math.max(1, Math.floor(options.ttlMs ?? DEFAULT_TTL_MS)); this.releaseUrl = options.releaseUrl ?? "https://github.com/deepseek-ai/deepseek-harness/releases"; + this.runtimeWarnings = options.runtimeWarnings ?? []; } getStatus(channel = "latest", cacheTtlMinutes) { return this.check(false, channel, cacheTtlMinutes); @@ -470,6 +759,7 @@ var UpdateStatusService = class { channel, version: selected.version }); + warnings.push(...this.runtimeWarnings); return { currentVersion: this.installation.currentVersion, latestVersion: selected.version, @@ -491,15 +781,16 @@ var UpdateStatusService = class { }; } statusWithoutRemoteRelease(channel, warnings) { + const combined = [...warnings, ...this.runtimeWarnings]; return { currentVersion: this.installation.currentVersion, latestVersion: null, hasUpdate: false, cached: false, checkedAt: null, - warning: warningFallback(warnings), - warningKind: warningKindOf(warnings), - warnings, + warning: warningFallback(combined), + warningKind: warningKindOf(combined), + warnings: combined, installKind: this.installation.installKind, upgradeCommand: upgradeCommandFor(this.installation.installKind, this.installation.packageName || "@deepseek-ai/dsh", channel), releaseUrl: this.releaseUrl, @@ -520,25 +811,46 @@ var UpdateStatusService = class { //#endregion //#region src/host/settings.ts /** +* Build the Config schema from a resolved schemastery factory. +* +* `volatileAvailable` is the capability the resolver *verified*, not an +* assumption. On a copy without `volatile()` the three preferences stay ordinary +* fields — the plugin keeps loading and reports why through +* `schemaRuntime().warning` instead of throwing while its own entry is imported. +* +* @param factory - A schemastery factory, structurally the package's default export. +* @param volatileAvailable - Whether `factory` exposes the Loader's volatile projection. +*/ +function buildConfigSchema(factory, volatileAvailable) { + const volatile = (field) => { + if (!volatileAvailable) return field; + const method = field.volatile; + return typeof method === "function" ? method.call(field) : field; + }; + return factory.object({ + cacheTtlHours: factory.number().step(1).min(1).max(24).default(6), + timeoutMs: factory.number().step(1).min(1e3).max(3e4).default(DEFAULT_TIMEOUT_MS), + autoCheckOnMount: factory.boolean().default(true), + sidebarEnabled: volatile(factory.boolean().default(true)), + channel: volatile(factory.union([ + "latest", + "next", + "alpha" + ]).default("latest").loose()), + cacheTtlMinutes: volatile(factory.number().min(30).max(1440).default(360)) + }); +} +const runtime = schemaRuntime(); +/** * The plugin's Config schema; its volatile fields ARE the settings form. * * The explicit two-argument annotation is load-bearing: a `.volatile()` field's * output is a stable reference (`Volatile`) rather than the bare value the * input side takes, so the inferred schema type cannot be named by the emitted -* `.d.ts` (TS2883) without stating the input side here. +* `.d.ts` (TS2883) without stating the input side here. The builder preserves +* that contract even on the degraded path, where no field is volatile. */ -const Config = z.object({ - cacheTtlHours: z.number().step(1).min(1).max(24).default(6), - timeoutMs: z.number().step(1).min(1e3).max(3e4).default(DEFAULT_TIMEOUT_MS), - autoCheckOnMount: z.boolean().default(true), - sidebarEnabled: z.boolean().default(true).volatile(), - channel: z.union([ - "latest", - "next", - "alpha" - ]).default("latest").loose().volatile(), - cacheTtlMinutes: z.number().min(30).max(1440).default(360).volatile() -}); +const Config = buildConfigSchema(runtime.z, runtime.volatile); /** * Suppress the auto-generated settings page for this entry. * @@ -576,14 +888,17 @@ function boundedNumber(value, fallback, min, max) { function apply(ctx, config = {}) { const ttlHours = boundedNumber(config.cacheTtlHours, 6, 1, 24); const timeoutMs = boundedNumber(config.timeoutMs, DEFAULT_TIMEOUT_MS, 1e3, 3e4); + const schema = schemaRuntime(); + if (schema.warning !== null) console.warn(`[dsh-update-status] ${describeSchemaRuntime(schema)} — ${describeWarning(schema.warning)}`); const service = new UpdateStatusService({ installation: detectInstallation(ctx), fetchLatest: createRegistryFetcher(timeoutMs), - ttlMs: ttlHours * 60 * 60 * 1e3 + ttlMs: ttlHours * 60 * 60 * 1e3, + runtimeWarnings: schema.warning === null ? [] : [schema.warning] }); installSettings(ctx); installUpdateStatusRpc(ctx, service); if (config.autoCheckOnMount !== false) service.getStatus(); } //#endregion -export { Config, UpdateStatusService, apply, inject, name }; +export { Config, UpdateStatusService, apply, inject, name, resolveSchemaRuntime, schemaRuntime }; diff --git a/package.json b/package.json index 7c55327..f43e950 100644 --- a/package.json +++ b/package.json @@ -36,8 +36,9 @@ "test": "pnpm run typecheck && vitest run", "test:unit": "vitest run", "clean": "rm -rf lib coverage", + "guard:shadowed-peer": "node tests/guards/shadowed-peer.mjs", "prepack": "pnpm run build", - "verify": "pnpm run test && pnpm run build && pnpm pack --dry-run" + "verify": "pnpm run test && pnpm run build && pnpm run guard:shadowed-peer && pnpm pack --dry-run" }, "repository": { "type": "git", diff --git a/src/client/contract.ts b/src/client/contract.ts index 862abf2..90dfdf9 100644 --- a/src/client/contract.ts +++ b/src/client/contract.ts @@ -79,6 +79,15 @@ function warningOf(value: unknown): UpdateWarning | undefined { if (record.code === 'preview-unverified' && isReleaseChannel(record.channel) && typeof record.version === 'string') { return { code: record.code, channel: record.channel, version: record.version } } + if (record.code === 'stale-schemastery') { + const version = record.version === null ? null : stringOrNull(record.version) + const nodeModulesDir = record.nodeModulesDir === null ? null : stringOrNull(record.nodeModulesDir) + const path = stringOrNull(record.path) + if (path !== null && (record.version === null || version !== null) + && (record.nodeModulesDir === null || nodeModulesDir !== null)) { + return { code: record.code, version, path, nodeModulesDir } + } + } return undefined } diff --git a/src/client/i18n.ts b/src/client/i18n.ts index f35ffd7..ca603b9 100644 --- a/src/client/i18n.ts +++ b/src/client/i18n.ts @@ -49,6 +49,8 @@ const DICTIONARY: Record = { 'warning.channelUnavailable': { zh: 'npm registry 未发布 {channel} 通道。', en: 'The npm registry does not publish a {channel} channel.' }, 'warning.versionIncomparable': { zh: '无法按 SemVer 比较当前版本 {currentVersion} 与 {channel} 通道版本 {selectedVersion}。', en: 'Unable to compare the current version {currentVersion} with {channel} channel version {selectedVersion} using SemVer.' }, 'warning.previewUnverified': { zh: '{channel} 是预览通道,版本 {version} 尚未验证与本插件兼容。', en: '{channel} is a preview channel; version {version} has not been verified as compatible with this plugin.' }, + 'warning.staleSchemastery': { zh: '本插件解析到的 @deepseek-ai/schemastery(版本 {version})来自 {path},不是 DSH 自带的那份,偏好字段无法标记为 volatile。请删除该残留目录并重启 DSH:rm -rf {nodeModulesDir}', en: 'This plugin resolved @deepseek-ai/schemastery {version} from {path} instead of the copy DSH provides, so preference fields cannot be marked volatile. Remove the stale directory and restart DSH: rm -rf {nodeModulesDir}' }, + 'warning.staleSchemasteryNoPath': { zh: '本插件解析到的 @deepseek-ai/schemastery(版本 {version})来自 {path},不是 DSH 自带的那份,偏好字段无法标记为 volatile。请删除该处残留的 @deepseek-ai/schemastery 目录并重启 DSH。', en: 'This plugin resolved @deepseek-ai/schemastery {version} from {path} instead of the copy DSH provides, so preference fields cannot be marked volatile. Remove the stale @deepseek-ai/schemastery directory there and restart DSH.' }, 'guidance.sourceCheckout': { zh: '请更新 DSH 源码 checkout、安装依赖并重新构建;本插件无法从 GUI 原地替换。', en: 'Update the DSH source checkout, install its dependencies, and rebuild it; this plugin cannot replace it in place from the GUI.' }, 'guidance.unknownInstall': { zh: '升级前请先确认 DSH 的安装方式;本插件无法代为执行升级。', en: 'Confirm how DSH was installed before upgrading; this plugin cannot perform the upgrade for you.' }, 'panel.static': { zh: '此连接只显示静态版本;请在运行 DSH 的本机打开侧栏查看完整更新信息。', en: 'This connection shows only the static version. Open the sidebar on the computer running DSH for full update details.' }, @@ -96,6 +98,14 @@ export function warningText(warning: UpdateWarning, language: Language = languag selectedVersion: warning.selectedVersion, }, language) case 'preview-unverified': return t('warning.previewUnverified', { channel: warning.channel, version: warning.version }, language) + case 'stale-schemastery': { + const params = { version: warning.version ?? '—', path: warning.path } + // Only print a removal command when the Host could name the directory to + // remove; the module path alone would make `rm -rf` delete a single file. + return warning.nodeModulesDir === null + ? t('warning.staleSchemasteryNoPath', params, language) + : t('warning.staleSchemastery', { ...params, nodeModulesDir: warning.nodeModulesDir }, language) + } } } diff --git a/src/host/schemastery.ts b/src/host/schemastery.ts new file mode 100644 index 0000000..e9c8f1b --- /dev/null +++ b/src/host/schemastery.ts @@ -0,0 +1,329 @@ +/** + * dsh-update-status — shadow-proof resolution of the platform's schemastery. + * + * `@deepseek-ai/schemastery` is a peer dependency: DSH ships it, and DSH's copy + * is the one carrying `volatile()` — the Loader's volatile-field projection this + * plugin's settings form is built on. A plain + * `import z from '@deepseek-ai/schemastery'` is resolved by Node's own directory + * walk *from the importing file*, so any copy sitting next to the plugin wins + * over the platform's copy. That is not hypothetical: an unmanaged tree left + * inside the install directory — a dev `node_modules` copied in by a + * local-directory install, which pnpm never removes — made `lib/index.js` throw + * `TypeError: …volatile is not a function` while it was being imported, so the + * whole host half disappeared before one line of plugin code ran. + * + * Nothing inside the plugin can repair that state: the failure precedes every + * plugin entry point. The resolution itself is made immune instead, trying + * platform copies before anything else: + * + * 1. the DSH installation running this process, + * 2. the profile peer farm DSH maintains (`$DSH_HOME/profiles/node_modules`), + * 3. the active profile's own tree, + * 4. whatever Node's walk finds next to this file — last resort. + * + * The first candidate that actually exposes `volatile()` wins, and the + * capability is verified rather than assumed. When no candidate provides it, the + * plugin still loads on the best copy it could load and reports exactly what it + * resolved: the missing capability becomes a `stale-schemastery` warning the + * panel renders instead of a dead plugin. + */ + +import { existsSync, readFileSync, realpathSync } from 'node:fs' +import { createRequire } from 'node:module' +import { homedir } from 'node:os' +import { basename, dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import type { UpdateWarning } from '../shared/types.ts' +import { manifestFromEntry } from './installation.ts' + +export const SCHEMASTERY_NAME = '@deepseek-ai/schemastery' +/** Anchor file name; it never has to exist, only to give `createRequire` a base. */ +const PROBE_FILE = 'dsh-update-status-peer-probe.cjs' +const MAX_ASCENT = 10 + +/** Where a candidate copy of schemastery was looked for. */ +export type SchemaCandidateSource = 'dsh-install' | 'profile-peers' | 'profile-local' | 'plugin-local' + +export interface SchemaCandidate { + source: SchemaCandidateSource + /** Absolute file path `createRequire` anchors on; the file need not exist. */ + anchor: string +} + +export interface SchemaCandidateReport { + source: SchemaCandidateSource + anchor: string + /** Absolute path of the module that loaded, or null when it did not load. */ + path: string | null + version: string | null + /** The loaded module exposes `volatile()`. */ + volatile: boolean + /** Why this candidate was skipped, or null when it loaded. */ + error: string | null +} + +export interface SchemaRuntime { + /** The loaded schemastery factory — structurally the package's default export. */ + z: unknown + volatile: boolean + source: SchemaCandidateSource + path: string | null + version: string | null + /** Every candidate that was tried, in order, for diagnostics. */ + candidates: SchemaCandidateReport[] + /** Set when the resolved copy cannot project volatile preferences. */ + warning: UpdateWarning | null +} + +export interface ResolveSchemaRuntimeOptions { + /** Override the candidate order; used by tests and by an embedding host. */ + candidates?: readonly SchemaCandidate[] +} + +function realPath(path: string): string { + try { + return realpathSync(path) + } catch { + return path + } +} + +function boundedMessage(error: unknown): string { + const raw = error instanceof Error ? error.message : String(error) + const trimmed = raw.replace(/\s+/g, ' ').trim() + return trimmed === '' ? 'unknown error' : trimmed.slice(0, 220) +} + +function environmentDirectory(name: string): string | null { + const value = process.env[name] + return typeof value === 'string' && value.trim() !== '' ? value : null +} + +/** The DSH home directory, from the environment first and `~/.dsh` as a fallback. */ +export function dshHomeDirectory(): string | null { + const configured = environmentDirectory('DSH_HOME') + if (configured !== null) return configured + try { + return join(homedir(), '.dsh') + } catch { + return null + } +} + +/** The active profile directory, from `$DSH_PROFILE_DIR` or `$DSH_HOME/profiles/`. */ +export function profileDirectory(): string | null { + const configured = environmentDirectory('DSH_PROFILE_DIR') + if (configured !== null) return configured + const home = dshHomeDirectory() + const profile = environmentDirectory('DSH_PROFILE') + return home === null || profile === null ? null : join(home, 'profiles', profile) +} + +/** + * Anchor on the DSH package that owns the running entry point. + * + * The entry is the CLI file Node was started with, so ascending from it reaches + * the DSH installation whose `node_modules` holds the platform's schemastery. + * Reusing `manifestFromEntry` keeps this probe identical to the installation + * detection the status read already performs. + */ +function dshInstallAnchor(): string | null { + const entry = process.argv[1] + if (typeof entry !== 'string' || entry === '') return null + const found = manifestFromEntry(entry) + return found === undefined || found.root === '' ? null : join(found.root, 'package.json') +} + +/** Candidate order: platform copies first, the shadowable walk last. */ +export function defaultSchemaCandidates(): SchemaCandidate[] { + const candidates: SchemaCandidate[] = [] + const install = dshInstallAnchor() + if (install !== null) candidates.push({ source: 'dsh-install', anchor: install }) + const home = dshHomeDirectory() + if (home !== null) candidates.push({ source: 'profile-peers', anchor: join(home, 'profiles', PROBE_FILE) }) + const profile = profileDirectory() + if (profile !== null) candidates.push({ source: 'profile-local', anchor: join(profile, PROBE_FILE) }) + candidates.push({ source: 'plugin-local', anchor: fileURLToPath(import.meta.url) }) + return candidates +} + +function manifestVersion(manifestPath: string): string | null { + try { + const parsed = JSON.parse(readFileSync(manifestPath, 'utf8')) as { name?: unknown; version?: unknown } + if (parsed.name !== SCHEMASTERY_NAME || typeof parsed.version !== 'string') return null + const version = parsed.version.trim() + return version === '' ? null : version + } catch { + return null + } +} + +/** Read the version of the schemastery package that owns a resolved entry path. */ +function versionOfResolved(resolvedPath: string): string | null { + let directory = dirname(realPath(resolvedPath)) + for (let depth = 0; depth < MAX_ASCENT; depth += 1) { + const manifestPath = join(directory, 'package.json') + if (existsSync(manifestPath)) { + const version = manifestVersion(manifestPath) + if (version !== null) return version + } + const parent = dirname(directory) + if (parent === directory) break + directory = parent + } + return null +} + +/** The nearest `node_modules` directory above a resolved package entry. */ +function nodeModulesDirectoryOf(resolvedPath: string | null): string | null { + if (resolvedPath === null) return null + let directory = dirname(realPath(resolvedPath)) + for (let depth = 0; depth < MAX_ASCENT; depth += 1) { + if (basename(directory) === 'node_modules') return directory + const parent = dirname(directory) + if (parent === directory) break + directory = parent + } + return null +} + +/** + * Does this module expose the Loader's volatile projection? + * + * The probe builds a throwaway field because the capability is a prototype + * method, not a flag: 3.18.2 has no `volatile` at all, 3.18.4 (the copy DSH + * ships) does. Never throws — an unusable module is simply not a candidate. + */ +function supportsVolatile(factory: unknown): boolean { + if (factory === null || (typeof factory !== 'object' && typeof factory !== 'function')) return false + try { + const boolean = (factory as { boolean?: () => unknown }).boolean + if (typeof boolean !== 'function') return false + const field = boolean.call(factory) as { default?: (value: unknown) => unknown } | null | undefined + if (field === null || field === undefined || typeof field.default !== 'function') return false + const defined = field.default(true) as { volatile?: unknown } | null | undefined + return defined !== null && defined !== undefined && typeof defined.volatile === 'function' + } catch { + return false + } +} + +interface LoadedCandidate { + report: SchemaCandidateReport + factory: unknown +} + +/** Can this value build a schema? A wrapper object that cannot is never chosen. */ +function isSchemaFactory(value: unknown): boolean { + if (value === null || (typeof value !== 'object' && typeof value !== 'function')) return false + const record = value as { object?: unknown; boolean?: unknown; number?: unknown } + return typeof record.object === 'function' && typeof record.boolean === 'function' && typeof record.number === 'function' +} + +/** + * Pick the factory out of the shapes a CommonJS build can expose. + * + * The package assigns `module.exports` directly, but a bundler or an interop + * wrapper can put the same factory behind `default`. A shape that cannot build a + * schema is never chosen even as a fallback, because the degraded path still has + * to produce a `Config` — a wrapper object would throw while the entry loads, + * which is the failure this module exists to prevent. + */ +function factoryOf(loaded: unknown): unknown { + const wrapper = loaded !== null && (typeof loaded === 'object' || typeof loaded === 'function') + ? loaded as { default?: unknown } + : undefined + const shapes = [loaded, wrapper?.default].filter(isSchemaFactory) + return shapes.find(supportsVolatile) ?? shapes[0] +} + +function loadCandidate(candidate: SchemaCandidate): LoadedCandidate { + const report: SchemaCandidateReport = { + source: candidate.source, + anchor: candidate.anchor, + path: null, + version: null, + volatile: false, + error: null, + } + try { + const require = createRequire(candidate.anchor) + const resolved = require.resolve(SCHEMASTERY_NAME) + const factory = factoryOf(require(resolved)) + if (factory === undefined) throw new Error('module does not export a schemastery factory') + report.path = realPath(resolved) + report.version = versionOfResolved(resolved) + report.volatile = supportsVolatile(factory) + return { report, factory } + } catch (error) { + report.error = boundedMessage(error) + return { report, factory: undefined } + } +} + +/** + * Resolve schemastery once, preferring the copy that can actually do the job. + * + * Only a total miss throws, and only with the per-candidate reasons attached: + * that state means the plugin is not running inside a working DSH installation, + * so there is no schema system to build a settings form with. Every other + * outcome loads. + */ +export function resolveSchemaRuntime(options: ResolveSchemaRuntimeOptions = {}): SchemaRuntime { + const candidates = options.candidates ?? defaultSchemaCandidates() + const reports: SchemaCandidateReport[] = [] + let usable: LoadedCandidate | undefined + let loaded: LoadedCandidate | undefined + for (const candidate of candidates) { + const attempt = loadCandidate(candidate) + reports.push(attempt.report) + if (attempt.factory === undefined) continue + loaded ??= attempt + if (attempt.report.volatile) { + usable = attempt + break + } + } + const chosen = usable ?? loaded + if (chosen === undefined) { + const tried = reports.map(report => `${report.source}: ${report.error ?? 'not loadable'}`).join('; ') + throw new Error(`dsh-update-status could not resolve ${SCHEMASTERY_NAME} (${tried})`) + } + return { + z: chosen.factory, + volatile: chosen.report.volatile, + source: chosen.report.source, + path: chosen.report.path, + version: chosen.report.version, + candidates: reports, + warning: chosen.report.volatile + ? null + : { + code: 'stale-schemastery', + version: chosen.report.version, + path: chosen.report.path ?? chosen.report.anchor, + nodeModulesDir: nodeModulesDirectoryOf(chosen.report.path), + }, + } +} + +let resolved: SchemaRuntime | undefined + +/** + * The process-wide resolution, computed on first use. + * + * The settings schema is built at module scope — the Loader reads the exported + * `Config` before `apply` runs — so the resolution cannot wait for a Host + * context. Every input here comes from the process environment or the filesystem. + */ +export function schemaRuntime(): SchemaRuntime { + resolved ??= resolveSchemaRuntime() + return resolved +} + +/** English, single-line summary of a resolution, for the Host log. */ +export function describeSchemaRuntime(runtime: SchemaRuntime): string { + const where = runtime.path === null ? runtime.source : `${runtime.path} (${runtime.source})` + const version = runtime.version === null ? '' : `@${runtime.version}` + return `${SCHEMASTERY_NAME}${version} from ${where}; volatile preferences ${runtime.volatile ? 'available' : 'unavailable'}` +} diff --git a/src/host/settings.ts b/src/host/settings.ts index ffbfec4..983bd6d 100644 --- a/src/host/settings.ts +++ b/src/host/settings.ts @@ -24,13 +24,20 @@ * The preferences persist as this entry's `config` in the active profile's * `cordis.patch.yml`, which is DSH 0.1.7's official plugin-preference model — * the `~/.dsh/settings.yaml` namespace this plugin used before is gone. + * + * The schema is built from a *resolved* schemastery, never a bare import: the + * copy that provides `volatile()` is DSH's, and a stale copy left next to this + * plugin must not be able to shadow it. See `host/schemastery.ts` — including + * why a missing `volatile()` degrades the form instead of throwing while the + * plugin entry is being imported. */ import type {} from '@deepseek-ai/dsh-settings' import type { Context } from '@deepseek-ai/cordis' -import z from '@deepseek-ai/schemastery' +import type z from '@deepseek-ai/schemastery' import type { ReleaseChannel } from '../shared/types.ts' import { DEFAULT_CACHE_TTL_MINUTES } from '../shared/types.ts' +import { schemaRuntime } from './schemastery.ts' import { DEFAULT_TIMEOUT_MS } from './update-status.ts' /** Deployment config (ordinary) plus the volatile user preferences the form edits. */ @@ -49,22 +56,51 @@ export interface UpdateStatusConfig { cacheTtlMinutes?: number } +/** + * Build the Config schema from a resolved schemastery factory. + * + * `volatileAvailable` is the capability the resolver *verified*, not an + * assumption. On a copy without `volatile()` the three preferences stay ordinary + * fields — the plugin keeps loading and reports why through + * `schemaRuntime().warning` instead of throwing while its own entry is imported. + * + * @param factory - A schemastery factory, structurally the package's default export. + * @param volatileAvailable - Whether `factory` exposes the Loader's volatile projection. + */ +export function buildConfigSchema( + factory: typeof z, + volatileAvailable: boolean, +): z> { + const volatile = (field: T): T => { + if (!volatileAvailable) return field + const method = (field as { volatile?: () => T }).volatile + return typeof method === 'function' ? method.call(field) : field + } + return factory.object({ + cacheTtlHours: factory.number().step(1).min(1).max(24).default(6), + timeoutMs: factory.number().step(1).min(1_000).max(30_000).default(DEFAULT_TIMEOUT_MS), + autoCheckOnMount: factory.boolean().default(true), + sidebarEnabled: volatile(factory.boolean().default(true)), + channel: volatile(factory.union(['latest', 'next', 'alpha']).default('latest').loose()), + cacheTtlMinutes: volatile(factory.number().min(30).max(1_440).default(DEFAULT_CACHE_TTL_MINUTES)), + }) as unknown as z> +} + +const runtime = schemaRuntime() + /** * The plugin's Config schema; its volatile fields ARE the settings form. * * The explicit two-argument annotation is load-bearing: a `.volatile()` field's * output is a stable reference (`Volatile`) rather than the bare value the * input side takes, so the inferred schema type cannot be named by the emitted - * `.d.ts` (TS2883) without stating the input side here. + * `.d.ts` (TS2883) without stating the input side here. The builder preserves + * that contract even on the degraded path, where no field is volatile. */ -export const Config: z> = z.object({ - cacheTtlHours: z.number().step(1).min(1).max(24).default(6), - timeoutMs: z.number().step(1).min(1_000).max(30_000).default(DEFAULT_TIMEOUT_MS), - autoCheckOnMount: z.boolean().default(true), - sidebarEnabled: z.boolean().default(true).volatile(), - channel: z.union(['latest', 'next', 'alpha']).default('latest').loose().volatile(), - cacheTtlMinutes: z.number().min(30).max(1_440).default(DEFAULT_CACHE_TTL_MINUTES).volatile(), -}) +export const Config: z> = buildConfigSchema( + runtime.z as typeof z, + runtime.volatile, +) /** * Suppress the auto-generated settings page for this entry. diff --git a/src/host/update-status.ts b/src/host/update-status.ts index b48a97e..39c2bea 100644 --- a/src/host/update-status.ts +++ b/src/host/update-status.ts @@ -6,6 +6,7 @@ import type { InstallationInfo } from './installation.ts' import { upgradeCommandFor } from './installation.ts' +import { SCHEMASTERY_NAME } from './schemastery.ts' import { compareSemver } from '../shared/semver.ts' import { PACKAGE_NAME, @@ -38,6 +39,11 @@ export interface UpdateStatusServiceOptions { now?: () => number ttlMs?: number releaseUrl?: string + /** + * Facts about this process's own runtime, appended to every status. The schema + * resolution is the only producer today; the registry read never sets these. + */ + runtimeWarnings?: readonly UpdateWarning[] } interface CachedRelease { @@ -51,22 +57,41 @@ function boundedMessage(error: unknown): string { return trimmed === '' ? 'unknown error' : trimmed.slice(0, 220) } -function warningText(warning: UpdateWarning): string { +/** English, single-line rendering of one warning; the Host log uses it directly. */ +export function describeWarning(warning: UpdateWarning): string { switch (warning.code) { case 'registry-unavailable': return `Unable to check the npm registry: ${warning.detail}` case 'channel-unavailable': return `The npm registry does not publish a ${warning.channel} channel.` case 'version-incomparable': return `Unable to compare the current version ${warning.currentVersion} with ${warning.channel} channel version ${warning.selectedVersion} using SemVer.` case 'preview-unverified': return `${warning.channel} is a preview channel; version ${warning.version} has not been verified as compatible with this plugin.` + case 'stale-schemastery': { + const version = warning.version === null ? '' : ` ${warning.version}` + // Only name a removal command when the directory is known; `rm -rf` on the + // module path would delete one file out of the stale package. + const remedy = warning.nodeModulesDir === null + ? `Remove the stale ${SCHEMASTERY_NAME} directory there and restart DSH.` + : `Remove the stale copy and restart DSH: rm -rf ${warning.nodeModulesDir}` + return `This plugin resolved ${SCHEMASTERY_NAME}${version} from ${warning.path} instead of the copy DSH provides, so preference fields cannot be marked volatile. ${remedy}` + } } } function warningFallback(warnings: UpdateWarning[]): string | null { - return warnings.length === 0 ? null : warnings.map(warningText).join(' ') + return warnings.length === 0 ? null : warnings.map(describeWarning).join(' ') } +/** + * Advisory codes leave the chip alone; everything else is a failed read. + * + * `stale-schemastery` belongs here: the version answer is complete and usable, + * and only the settings form is degraded, so repainting the brand row would + * misreport a plugin-runtime fact as a failed update check. + */ +const ADVISORY_CODES: readonly UpdateWarning['code'][] = ['preview-unverified', 'stale-schemastery'] + function warningKindOf(warnings: UpdateWarning[]): UpdateWarningKind | null { if (warnings.length === 0) return null - return warnings.every(warning => warning.code === 'preview-unverified') ? 'notice' : 'failure' + return warnings.every(warning => ADVISORY_CODES.includes(warning.code)) ? 'notice' : 'failure' } function dateOrNull(value: unknown): string | null { @@ -145,6 +170,7 @@ export class UpdateStatusService { private readonly now: () => number private readonly ttlMs: number private readonly releaseUrl: string + private readonly runtimeWarnings: readonly UpdateWarning[] private cache: CachedRelease | undefined private inFlight: Promise | undefined @@ -154,6 +180,7 @@ export class UpdateStatusService { this.now = options.now ?? Date.now this.ttlMs = Math.max(1, Math.floor(options.ttlMs ?? DEFAULT_TTL_MS)) this.releaseUrl = options.releaseUrl ?? RELEASES_URL + this.runtimeWarnings = options.runtimeWarnings ?? [] } getStatus(channel: ReleaseChannel = 'latest', cacheTtlMinutes?: number): Promise { @@ -216,6 +243,7 @@ export class UpdateStatusService { if (channel !== 'latest' && selected.version !== null && selected.compatibility !== 'verified') { warnings.push({ code: 'preview-unverified', channel, version: selected.version }) } + warnings.push(...this.runtimeWarnings) return { currentVersion: this.installation.currentVersion, latestVersion: selected.version, @@ -238,15 +266,16 @@ export class UpdateStatusService { } private statusWithoutRemoteRelease(channel: ReleaseChannel, warnings: UpdateWarning[]): UpdateStatus { + const combined = [...warnings, ...this.runtimeWarnings] return { currentVersion: this.installation.currentVersion, latestVersion: null, hasUpdate: false, cached: false, checkedAt: null, - warning: warningFallback(warnings), - warningKind: warningKindOf(warnings), - warnings, + warning: warningFallback(combined), + warningKind: warningKindOf(combined), + warnings: combined, installKind: this.installation.installKind, upgradeCommand: upgradeCommandFor(this.installation.installKind, this.installation.packageName || PACKAGE_NAME, channel), releaseUrl: this.releaseUrl, diff --git a/src/index.ts b/src/index.ts index b5e8369..5103ad5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,8 +3,9 @@ import type { Context } from '@deepseek-ai/cordis' import { detectInstallation } from './host/installation.ts' import { installUpdateStatusRpc } from './host/rpc.ts' +import { describeSchemaRuntime, schemaRuntime } from './host/schemastery.ts' import { Config, installSettings, type UpdateStatusConfig } from './host/settings.ts' -import { createRegistryFetcher, DEFAULT_TIMEOUT_MS, UpdateStatusService } from './host/update-status.ts' +import { createRegistryFetcher, DEFAULT_TIMEOUT_MS, describeWarning, UpdateStatusService } from './host/update-status.ts' export const name = 'dsh-update-status' /** Connection supplies the authenticated transport; settings remains optional. */ @@ -28,10 +29,19 @@ export function apply(ctx: Context, config: Config = {}): void { // safe read-only behavior if an older loader calls apply directly. const ttlHours = boundedNumber(config.cacheTtlHours, 6, 1, 24) const timeoutMs = boundedNumber(config.timeoutMs, DEFAULT_TIMEOUT_MS, 1_000, 30_000) + // The schema resolution already ran at module load (the Loader reads Config + // before apply). Re-reading it here is free, and it is the one place that can + // report a degraded resolution: the plugin keeps working, but its preference + // fields cannot be volatile, and the panel says exactly which copy to remove. + const schema = schemaRuntime() + if (schema.warning !== null) { + console.warn(`[dsh-update-status] ${describeSchemaRuntime(schema)} — ${describeWarning(schema.warning)}`) + } const service = new UpdateStatusService({ installation: detectInstallation(ctx), fetchLatest: createRegistryFetcher(timeoutMs), ttlMs: ttlHours * 60 * 60 * 1_000, + runtimeWarnings: schema.warning === null ? [] : [schema.warning], }) installSettings(ctx) @@ -44,3 +54,8 @@ export function apply(ctx: Context, config: Config = {}): void { export { UpdateStatusService } from './host/update-status.ts' export type { UpdateStatus, InstallKind } from './shared/types.ts' +// Exported for the shadowed-peer guard (`tests/guards/shadowed-peer.mjs`), which +// loads the built entry with a stale schemastery planted beside it and asserts +// the platform copy still wins. +export { resolveSchemaRuntime, schemaRuntime } from './host/schemastery.ts' +export type { SchemaRuntime } from './host/schemastery.ts' diff --git a/src/shared/types.ts b/src/shared/types.ts index a8d136f..9012c9f 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -62,6 +62,13 @@ export type UpdateWarning = | { code: 'channel-unavailable'; channel: ReleaseChannel } | { code: 'version-incomparable'; currentVersion: string; channel: ReleaseChannel; selectedVersion: string } | { code: 'preview-unverified'; channel: ReleaseChannel; version: string } + /** + * The Host resolved a `@deepseek-ai/schemastery` that DSH does not ship, so the + * Loader's volatile projection is unavailable and the preference fields cannot + * be marked volatile. Advisory: the version answer itself is complete, and the + * text carries the exact directory to remove. See `host/schemastery.ts`. + */ + | { code: 'stale-schemastery'; version: string | null; path: string; nodeModulesDir: string | null } export function isReleaseChannel(value: unknown): value is ReleaseChannel { return value === 'latest' || value === 'next' || value === 'alpha' diff --git a/tests/client/contract.spec.ts b/tests/client/contract.spec.ts new file mode 100644 index 0000000..342318b --- /dev/null +++ b/tests/client/contract.spec.ts @@ -0,0 +1,56 @@ +/** + * dsh-update-status — RPC status boundary specs. + * + * `updateStatusOf` is the one place a Host payload becomes a client value, so a + * warning code added on the Host side is only usable once this guard knows it. + * The `stale-schemastery` case is the one added with the shadow-proof peer + * resolution; the rejection cases keep a malformed payload from rendering as a + * half-built panel. + */ +import { describe, expect, it } from 'vitest' +import { updateStatusOf } from '../../src/client/contract.ts' + +function payload(warnings: unknown): Record { + return { + currentVersion: '0.1.7-rc.2', + latestVersion: null, + hasUpdate: false, + cached: false, + checkedAt: null, + warning: null, + warnings, + installKind: 'npm-global', + upgradeCommand: 'npm install -g @deepseek-ai/dsh@latest', + releaseUrl: 'https://example.test/releases', + changelogUrl: 'https://example.test/releases', + publishedAt: null, + packageName: '@deepseek-ai/dsh', + channel: 'latest', + channels: [{ channel: 'latest', version: null, publishedAt: null, compatibility: 'unverified' }], + canApplyInPlace: false, + } +} + +const stale = { + code: 'stale-schemastery', + version: '3.18.2', + path: '/p/node_modules/@deepseek-ai/schemastery/lib/index.cjs', + nodeModulesDir: '/p/node_modules', +} + +describe('RPC status boundary', () => { + it('accepts the stale-schemastery warning', () => { + expect(updateStatusOf(payload([stale]))?.warnings).toEqual([stale]) + }) + + it('accepts it with an unreadable version and an unknown directory', () => { + const unknown = { ...stale, version: null, nodeModulesDir: null } + expect(updateStatusOf(payload([unknown]))?.warnings).toEqual([unknown]) + }) + + it('drops a payload whose warning is malformed or unknown', () => { + expect(updateStatusOf(payload([{ code: 'stale-schemastery', version: '3.18.2' }]))).toBeUndefined() + expect(updateStatusOf(payload([{ ...stale, path: 7 }]))).toBeUndefined() + expect(updateStatusOf(payload([{ code: 'something-else' }]))).toBeUndefined() + }) +}) diff --git a/tests/client/i18n.spec.ts b/tests/client/i18n.spec.ts index 927594b..9cd059a 100644 --- a/tests/client/i18n.spec.ts +++ b/tests/client/i18n.spec.ts @@ -32,6 +32,26 @@ describe('runtime message localization', () => { expect(localizedWarning({ warning: 'legacy fallback' }, 'zh')).toBe('legacy fallback') }) + it('renders the stale-schemastery warning with the directory to remove', () => { + const stale: UpdateWarning = { + code: 'stale-schemastery', + version: '3.18.2', + path: '/p/node_modules/@deepseek-ai/schemastery/lib/index.cjs', + nodeModulesDir: '/p/node_modules', + } + expect(warningText(stale, 'en')).toContain('3.18.2') + expect(warningText(stale, 'en')).toContain('rm -rf /p/node_modules') + expect(warningText(stale, 'zh')).toContain('偏好字段无法标记为 volatile') + expect(warningText(stale, 'zh')).toContain('rm -rf /p/node_modules') + // An unreadable version and an unknown directory still produce a usable + // sentence, and never a removal command aimed at a single module file. + const unknown: UpdateWarning = { ...stale, version: null, nodeModulesDir: null } + expect(warningText(unknown, 'en')).toContain(stale.path) + expect(warningText(unknown, 'en')).not.toContain('rm -rf') + expect(warningText(unknown, 'zh')).toContain('—') + expect(warningText(unknown, 'zh')).not.toContain('rm -rf') + }) + it('localizes non-command installation guidance without changing real commands', () => { expect(localizedUpgradeGuidance({ installKind: 'source-checkout', upgradeCommand: 'fallback' }, 'zh')).toContain('DSH 源码 checkout') expect(localizedUpgradeGuidance({ installKind: 'unknown', upgradeCommand: 'fallback' }, 'en')).toBe( diff --git a/tests/guards/shadowed-peer.mjs b/tests/guards/shadowed-peer.mjs new file mode 100644 index 0000000..225d0d2 --- /dev/null +++ b/tests/guards/shadowed-peer.mjs @@ -0,0 +1,191 @@ +#!/usr/bin/env node +/** + * Shadowed-peer guard — runs against the *built* host half, after `pnpm run build`. + * + * The failure this locks down is not a logic bug: `lib/index.js` used to take a + * plain `import z from '@deepseek-ai/schemastery'`, so Node resolved the peer + * from the plugin's own directory. A stale copy left next to the plugin (an + * unmanaged dev tree copied in by a local-directory install, which pnpm never + * removes) then shadowed the platform copy, and the missing `volatile()` method + * threw while the entry was being imported — the whole host half vanished before + * any plugin code ran. + * + * Two scenarios, both deterministic on a laptop and on a bare CI runner: + * + * A. only a stale copy is reachable — the entry must still load and must report + * the stale copy instead of throwing; + * B. a platform copy is reachable — it must win over the stale copy beside the + * plugin, and `volatile()` must be available. + * + * Usage: node tests/guards/shadowed-peer.mjs + */ + +import { execFileSync } from 'node:child_process' +import { cpSync, existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join, relative, resolve } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' + +const SCHEMASTERY = '@deepseek-ai/schemastery' +const STALE_VERSION = '3.18.2' +const script = fileURLToPath(import.meta.url) +const root = resolve(dirname(script), '..', '..') + +/** Child mode: load one built entry and report what it resolved. */ +if (process.argv[2] === '--scenario') { + const entry = process.argv[3] + try { + const loaded = await import(pathToFileURL(entry).href) + const runtime = loaded.schemaRuntime() + const warning = runtime.warning + console.log(JSON.stringify({ + ok: true, + exports: Object.keys(loaded).sort(), + volatile: runtime.volatile, + source: runtime.source, + version: runtime.version, + path: runtime.path, + candidates: runtime.candidates.map(candidate => ({ + source: candidate.source, + path: candidate.path, + version: candidate.version, + volatile: candidate.volatile, + error: candidate.error, + })), + warning: warning === null ? null : { code: warning.code, version: warning.version, nodeModulesDir: warning.nodeModulesDir }, + })) + } catch (error) { + console.error(`loading ${entry} failed: ${error instanceof Error ? error.message : String(error)}`) + process.exit(1) + } + process.exit(0) +} + +/** A stale copy: package-shaped, `volatile()` absent, exactly like 3.18.2. */ +function plantStaleCopy(work) { + const directory = join(work, 'node_modules', '@deepseek-ai', 'schemastery') + mkdirSync(directory, { recursive: true }) + writeFileSync(join(directory, 'package.json'), JSON.stringify({ + name: SCHEMASTERY, + version: STALE_VERSION, + main: 'index.cjs', + })) + writeFileSync(join(directory, 'index.cjs'), [ + 'function Schema() {}', + 'Schema.prototype.meta = {}', + 'Schema.prototype.default = function (value) { this.meta.default = value; return this }', + 'Schema.prototype.step = function () { return this }', + 'Schema.prototype.min = function () { return this }', + 'Schema.prototype.max = function () { return this }', + 'Schema.prototype.loose = function () { return this }', + 'const factory = function () { return new Schema() }', + 'factory.object = function (shape) { const schema = new Schema(); schema.dict = shape; return schema }', + 'factory.number = function () { return new Schema() }', + 'factory.boolean = function () { return new Schema() }', + 'factory.union = function () { return new Schema() }', + 'module.exports = factory', + '', + ].join('\n')) + return directory +} + +/** A throwaway install tree: the built entry, its manifest, and the planted copy. */ +function makeWork() { + const work = mkdtempSync(join(tmpdir(), 'dus-shadowed-peer-')) + mkdirSync(join(work, 'lib'), { recursive: true }) + cpSync(join(root, 'lib', 'index.js'), join(work, 'lib', 'index.js')) + cpSync(join(root, 'package.json'), join(work, 'package.json')) + plantStaleCopy(work) + return work +} + +function run(entry, env) { + const output = execFileSync(process.execPath, [script, '--scenario', entry], { + encoding: 'utf8', + env: { ...process.env, ...env }, + stdio: ['ignore', 'pipe', 'pipe'], + }) + return JSON.parse(output.trim().split('\n').at(-1)) +} + +const failures = [] +function check(condition, message) { + if (!condition) failures.push(message) +} + +function report(title, result) { + console.log(`\n${title}`) + console.log(` resolved ${result.version ?? 'unknown version'} via ${result.source}`) + console.log(` path ${result.path ?? '(none)'}`) + console.log(` volatile ${result.volatile ? 'available' : 'UNAVAILABLE'}`) + console.log(` warning ${result.warning === null ? '(none)' : result.warning.code}`) + for (const candidate of result.candidates) { + const state = candidate.error === null ? `volatile=${candidate.volatile}` : `skipped: ${candidate.error}` + console.log(` candidate ${candidate.source}: ${candidate.path ?? '(not loaded)'} — ${state}`) + } +} + +if (!existsSync(join(root, 'lib', 'index.js'))) { + console.error(`shadowed-peer guard needs a build first: ${relative(root, join(root, 'lib', 'index.js'))} is missing`) + process.exit(1) +} + +const workA = makeWork() +// A sibling directory, never a parent of `workA`: otherwise the profile anchors +// would reach the planted copy by walking up and the scenario would prove nothing. +const emptyHome = mkdtempSync(join(tmpdir(), 'dus-shadowed-peer-home-')) +try { + const entry = join(workA, 'lib', 'index.js') + const result = run(entry, { + DSH_HOME: emptyHome, + DSH_PROFILE: 'web', + DSH_PROFILE_DIR: join(emptyHome, 'profiles', 'web'), + }) + report('A. stale copy only — the built entry must still load', result) + check(result.ok === true, 'A: the built entry did not report a resolution') + check(result.exports.includes('Config'), 'A: the built entry did not export Config') + check(result.volatile === false, 'A: the stale copy should not report volatile support') + check(result.source === 'plugin-local', `A: the shadowing copy beside the plugin should be the one loaded, got ${result.source}`) + check(result.warning !== null && result.warning.code === 'stale-schemastery', 'A: the stale copy was not reported as a stale-schemastery warning') + check(result.warning?.nodeModulesDir === realpathSync(join(workA, 'node_modules')), `A: the warning should name ${join(workA, 'node_modules')}`) +} catch (error) { + failures.push(`A: the built entry failed to load beside a stale copy — ${error.stderr ?? error.message}`) +} finally { + rmSync(workA, { recursive: true, force: true }) + rmSync(emptyHome, { recursive: true, force: true }) +} + +const realSchemastery = join(root, 'node_modules', '@deepseek-ai', 'schemastery') +if (!existsSync(realSchemastery)) { + failures.push(`the guard needs an installed ${SCHEMASTERY} at ${relative(root, realSchemastery)}; run pnpm install first`) +} else { + const workB = makeWork() + try { + const entry = join(workB, 'lib', 'index.js') + const home = join(workB, '.dsh') + const peers = join(home, 'profiles', 'node_modules', '@deepseek-ai') + mkdirSync(peers, { recursive: true }) + symlinkSync(realSchemastery, join(peers, 'schemastery'), 'dir') + const result = run(entry, { + DSH_HOME: home, + DSH_PROFILE: 'web', + DSH_PROFILE_DIR: join(home, 'profiles', 'web'), + }) + report('B. platform copy present — it must win over the stale copy', result) + check(result.ok === true, 'B: the built entry did not report a resolution') + check(result.volatile === true, 'B: the platform copy should report volatile support') + check(result.source === 'profile-peers', `B: expected the profile peer farm to win, got ${result.source}`) + check(result.path !== null && !result.path.startsWith(realpathSync(join(workB, 'node_modules'))), 'B: the stale copy beside the plugin won instead of the platform copy') + } catch (error) { + failures.push(`B: the built entry failed to load — ${error.stderr ?? error.message}`) + } finally { + rmSync(workB, { recursive: true, force: true }) + } +} + +if (failures.length > 0) { + console.error('\nshadowed-peer guard FAILED:') + for (const failure of failures) console.error(` - ${failure}`) + process.exit(1) +} +console.log('\nshadowed-peer guard passed') diff --git a/tests/host/schemastery.spec.ts b/tests/host/schemastery.spec.ts new file mode 100644 index 0000000..eb95627 --- /dev/null +++ b/tests/host/schemastery.spec.ts @@ -0,0 +1,281 @@ +/** + * dsh-update-status — peer resolution specs. + * + * `@deepseek-ai/schemastery` is the one peer this plugin cannot take by bare + * specifier: Node resolves it from the importing file, so a stale copy left next + * to the plugin shadows the platform's copy and its missing `volatile()` throws + * while the host half is being imported — before any plugin code runs. These + * specs lock the two properties that keep that state survivable: + * + * - the resolution tries platform copies first and only accepts a copy that + * actually exposes `volatile()`, and + * - a resolution that cannot find it still loads and reports what it used, + * instead of throwing during import. + * + * The last block is the source guard for the root cause: no file in `src/` may + * take a *value* import of the peer again. + */ +import { mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { afterEach, describe, expect, it } from 'vitest' +import realSchema from '@deepseek-ai/schemastery' +import { defaultSchemaCandidates, describeSchemaRuntime, resolveSchemaRuntime, SCHEMASTERY_NAME } from '../../src/host/schemastery.ts' +import { buildConfigSchema } from '../../src/host/settings.ts' + +const temporaryDirectories: string[] = [] + +function temporaryRoot(): string { + const directory = mkdtempSync(join(tmpdir(), 'dus-peer-')) + temporaryDirectories.push(directory) + return directory +} + +afterEach(() => { + for (const directory of temporaryDirectories.splice(0)) rmSync(directory, { recursive: true, force: true }) +}) + +/** A package-shaped schemastery whose `volatile()` can be present or absent. */ +const FAKE_FACTORY = [ + 'function Schema() {}', + 'Schema.prototype.meta = {}', + 'Schema.prototype.default = function (value) { this.meta.default = value; return this }', + 'Schema.prototype.step = function () { return this }', + 'Schema.prototype.min = function () { return this }', + 'Schema.prototype.max = function () { return this }', + 'Schema.prototype.loose = function () { return this }', + '/*VOLATILE*/', + 'const factory = function () { return new Schema() }', + 'factory.object = function (shape) { const schema = new Schema(); schema.dict = shape; return schema }', + 'factory.number = function () { return new Schema() }', + 'factory.boolean = function () { return new Schema() }', + 'factory.union = function () { return new Schema() }', + '/*EXPORT*/', + '', +].join('\n') + +const EXPORTS = { + direct: 'module.exports = factory', + wrapped: 'module.exports = { default: factory }', + empty: 'module.exports = {}', +} as const + +function writeSchemaPackage( + root: string, + options: { version: string; volatile: boolean; exportShape?: keyof typeof EXPORTS }, +): string { + const directory = join(root, 'node_modules', '@deepseek-ai', 'schemastery') + mkdirSync(directory, { recursive: true }) + writeFileSync(join(directory, 'package.json'), JSON.stringify({ + name: SCHEMASTERY_NAME, + version: options.version, + main: 'index.cjs', + })) + const volatile = options.volatile + ? 'Schema.prototype.volatile = function () { this.meta.volatile = true; return this }' + : '' + writeFileSync(join(directory, 'index.cjs'), FAKE_FACTORY + .replace('/*VOLATILE*/', volatile) + .replace('/*EXPORT*/', EXPORTS[options.exportShape ?? 'direct'])) + return directory +} + +describe('schema resolution', () => { + it('prefers the platform copy over a stale copy beside the plugin', () => { + const platform = temporaryRoot() + const shadow = temporaryRoot() + writeSchemaPackage(platform, { version: '3.18.4', volatile: true }) + writeSchemaPackage(shadow, { version: '3.18.2', volatile: false }) + + const runtime = resolveSchemaRuntime({ + candidates: [ + { source: 'dsh-install', anchor: join(platform, 'probe.cjs') }, + { source: 'plugin-local', anchor: join(shadow, 'probe.cjs') }, + ], + }) + + expect(runtime.volatile).toBe(true) + expect(runtime.source).toBe('dsh-install') + expect(runtime.version).toBe('3.18.4') + expect(runtime.warning).toBeNull() + // The shadowing copy was never even loaded: the first usable candidate wins. + expect(runtime.candidates).toHaveLength(1) + }) + + it('keeps loading on a stale copy and reports exactly what to remove', () => { + const shadow = temporaryRoot() + writeSchemaPackage(shadow, { version: '3.18.2', volatile: false }) + + const runtime = resolveSchemaRuntime({ + candidates: [{ source: 'plugin-local', anchor: join(shadow, 'probe.cjs') }], + }) + + expect(runtime.volatile).toBe(false) + expect(runtime.version).toBe('3.18.2') + const warning = runtime.warning + if (warning?.code !== 'stale-schemastery') throw new Error('expected a stale-schemastery warning') + expect(warning.version).toBe('3.18.2') + // The resolver reports real paths, so /var and /private/var agree on macOS. + expect(warning.nodeModulesDir).toBe(realpathSync(join(shadow, 'node_modules'))) + expect(warning.path).toContain('schemastery') + }) + + it('skips an unresolvable candidate and keeps searching', () => { + const empty = temporaryRoot() + const shadow = temporaryRoot() + writeSchemaPackage(shadow, { version: '3.18.2', volatile: false }) + + const runtime = resolveSchemaRuntime({ + candidates: [ + { source: 'dsh-install', anchor: join(empty, 'probe.cjs') }, + { source: 'profile-peers', anchor: join(shadow, 'probe.cjs') }, + ], + }) + + expect(runtime.source).toBe('profile-peers') + expect(runtime.candidates[0]?.error).toBeTruthy() + expect(runtime.candidates[1]?.error).toBeNull() + }) + + it('unwraps a default-wrapped factory instead of taking the wrapper', () => { + const wrapped = temporaryRoot() + writeSchemaPackage(wrapped, { version: '3.18.2', volatile: false, exportShape: 'wrapped' }) + + const runtime = resolveSchemaRuntime({ + candidates: [{ source: 'plugin-local', anchor: join(wrapped, 'probe.cjs') }], + }) + + // The wrapper cannot build a schema, so choosing it would throw while the + // entry loads — the exact failure this resolution exists to avoid. + expect(runtime.volatile).toBe(false) + expect(Object.keys(buildConfigSchema(runtime.z as typeof realSchema, false).dict ?? {})).toHaveLength(6) + }) + + it('skips a module that exports no factory at all', () => { + const broken = temporaryRoot() + const good = temporaryRoot() + writeSchemaPackage(broken, { version: '3.18.2', volatile: false, exportShape: 'empty' }) + writeSchemaPackage(good, { version: '3.18.2', volatile: false }) + + const runtime = resolveSchemaRuntime({ + candidates: [ + { source: 'plugin-local', anchor: join(broken, 'probe.cjs') }, + { source: 'profile-peers', anchor: join(good, 'probe.cjs') }, + ], + }) + expect(runtime.candidates[0]?.error).toContain('schemastery factory') + expect(runtime.source).toBe('profile-peers') + }) + + it('throws only when nothing resolves, naming every candidate', () => { + const empty = temporaryRoot() + expect(() => resolveSchemaRuntime({ + candidates: [{ source: 'dsh-install', anchor: join(empty, 'probe.cjs') }], + })).toThrow(/could not resolve .*dsh-install/) + }) + + it('orders the default candidates platform-first and the shadowable walk last', () => { + const candidates = defaultSchemaCandidates() + expect(candidates.at(-1)?.source).toBe('plugin-local') + expect(new Set(candidates.map(candidate => candidate.source)).size).toBe(candidates.length) + for (const candidate of candidates) expect(candidate.anchor.startsWith('/')).toBe(true) + }) + + it('describes a resolution in one line for the Host log', () => { + const shadow = temporaryRoot() + writeSchemaPackage(shadow, { version: '3.18.2', volatile: false }) + const runtime = resolveSchemaRuntime({ + candidates: [{ source: 'plugin-local', anchor: join(shadow, 'probe.cjs') }], + }) + expect(describeSchemaRuntime(runtime)).toContain('3.18.2') + expect(describeSchemaRuntime(runtime)).toContain('volatile preferences unavailable') + }) +}) + +describe('degraded Config form', () => { + function volatileFields(schema: ReturnType): string[] { + return Object.entries(schema.dict ?? {}) + .filter(([, field]) => field.meta.volatile === true) + .map(([name]) => name) + .sort() + } + + it('marks exactly the preferences volatile when the capability is present', () => { + expect(volatileFields(buildConfigSchema(realSchema, true))) + .toEqual(['cacheTtlMinutes', 'channel', 'sidebarEnabled']) + }) + + it('builds the same fields as ordinary ones instead of throwing without it', () => { + const degraded = buildConfigSchema(realSchema, false) + expect(volatileFields(degraded)).toEqual([]) + expect(Object.keys(degraded.dict ?? {}).sort()).toEqual([ + 'autoCheckOnMount', 'cacheTtlHours', 'cacheTtlMinutes', 'channel', 'sidebarEnabled', 'timeoutMs', + ]) + const field = degraded.dict?.channel?.meta as { default?: unknown } + expect(field.default).toBe('latest') + expect(degraded.dict?.cacheTtlMinutes?.meta.default).toBe(360) + }) + + it('accepts a factory whose schemas are structurally the same', () => { + const shadow = temporaryRoot() + writeSchemaPackage(shadow, { version: '3.18.2', volatile: false }) + const runtime = resolveSchemaRuntime({ + candidates: [{ source: 'plugin-local', anchor: join(shadow, 'probe.cjs') }], + }) + // A degraded resolution must still produce a Config the Loader can read. + const degraded = buildConfigSchema(runtime.z as typeof realSchema, runtime.volatile) + expect(Object.keys(degraded.dict ?? {}).sort()).toEqual([ + 'autoCheckOnMount', 'cacheTtlHours', 'cacheTtlMinutes', 'channel', 'sidebarEnabled', 'timeoutMs', + ]) + expect(volatileFields(degraded)).toEqual([]) + }) +}) + +describe('source guard: the peer is never imported by value', () => { + const sourceRoot = fileURLToPath(new URL('../../src', import.meta.url)) + const VALUE_IMPORT = /(?:^|\n)[ \t]*(?:import|export)[ \t]+(?!type\b)[^\n]*?from[ \t]*['"]@deepseek-ai\/schemastery['"]/g + const SIDE_EFFECT_IMPORT = /(?:^|\n)[ \t]*import[ \t]*['"]@deepseek-ai\/schemastery['"]/g + const LITERAL_REQUIRE = /require\([ \t]*['"]@deepseek-ai\/schemastery['"][ \t]*\)/g + + function sourceFiles(directory: string): string[] { + const found: string[] = [] + for (const entry of readdirSync(directory)) { + const path = join(directory, entry) + if (statSync(path).isDirectory()) found.push(...sourceFiles(path)) + else if (/\.tsx?$/.test(entry)) found.push(path) + } + return found + } + + /** Comments may quote the bare import; only real statements count. */ + function withoutComments(text: string): string { + return text.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/(^|[^:])\/\/[^\n]*/g, '$1') + } + + function valueImportsOf(text: string): string[] { + const stripped = withoutComments(text) + return [ + ...stripped.match(VALUE_IMPORT) ?? [], + ...stripped.match(SIDE_EFFECT_IMPORT) ?? [], + ...stripped.match(LITERAL_REQUIRE) ?? [], + ] + } + + it('recognises the regression it exists to catch', () => { + expect(valueImportsOf("import z from '@deepseek-ai/schemastery'")).toHaveLength(1) + expect(valueImportsOf("const z = require('@deepseek-ai/schemastery')")).toHaveLength(1) + expect(valueImportsOf("import type z from '@deepseek-ai/schemastery'")).toHaveLength(0) + expect(valueImportsOf('// import z from \'@deepseek-ai/schemastery\'')).toHaveLength(0) + }) + + it('holds for every file in src', () => { + const offenders: string[] = [] + for (const file of sourceFiles(sourceRoot)) { + for (const statement of valueImportsOf(readFileSync(file, 'utf8'))) { + offenders.push(`${file}: ${statement.trim()}`) + } + } + expect(offenders).toEqual([]) + }) +}) diff --git a/tests/host/update-status.spec.ts b/tests/host/update-status.spec.ts index 3abf7c4..b00b480 100644 --- a/tests/host/update-status.spec.ts +++ b/tests/host/update-status.spec.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest' import { upgradeCommandFor, type InstallationInfo } from '../../src/host/installation.ts' import { assertApprovedRegistryUrl, registryReleaseOf, UpdateStatusService, type RegistryRelease } from '../../src/host/update-status.ts' +import type { UpdateWarning } from '../../src/shared/types.ts' const installation: InstallationInfo = { currentVersion: '0.1.2-rc.1', @@ -133,6 +134,36 @@ describe('UpdateStatusService', () => { ]) }) + it('appends the process runtime warnings to every status as advisory', async () => { + const stale: UpdateWarning = { + code: 'stale-schemastery', + version: '3.18.2', + path: '/tmp/stale/node_modules/@deepseek-ai/schemastery/lib/index.cjs', + nodeModulesDir: '/tmp/stale/node_modules', + } + const service = new UpdateStatusService({ + installation, + fetchLatest: async () => releases(), + runtimeWarnings: [stale], + }) + const status = await service.getStatus() + expect(status.warnings).toEqual([stale]) + // A degraded settings form is advisory: the version answer is complete and + // usable, so the chip must not repaint for it. + expect(status.warningKind).toBe('notice') + expect(status.warning).toContain('rm -rf /tmp/stale/node_modules') + + const offline = new UpdateStatusService({ + installation, + fetchLatest: async () => { throw new Error('offline') }, + runtimeWarnings: [stale], + }) + const cold = await offline.getStatus() + // The registry failure still leads, and still decides the severity. + expect(cold.warnings).toEqual([{ code: 'registry-unavailable', detail: 'offline' }, stale]) + expect(cold.warningKind).toBe('failure') + }) + it('parses supported npm dist-tags from one registry document', () => { const release = registryReleaseOf({ 'dist-tags': { latest: '0.1.7-rc.1', next: '0.1.8-rc.1', alpha: '0.1.7-alpha.2', beta: '9.9.9' },