dsh-any-background 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -1,8 +1,226 @@
1
1
  import { access, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
2
2
  import { createReadStream, createWriteStream } from "node:fs";
3
- import { basename, dirname, join } from "node:path";
4
3
  import { Readable } from "node:stream";
5
4
  import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
5
+ import { basename, dirname, join } from "node:path";
6
+ //#region src/host-compat/channel.ts
7
+ /** What this plugin supports, in upgrade order. One row = one build whose facts
8
+ * were checked; several rows may share a `channel` when a newer build was
9
+ * verified as needing no change to that folder's adapter (as `0.1.7-rc.1` was) —
10
+ * a row then reports `exact` instead of `line`, and no new folder appears.
11
+ * Adding an ADAPTER means adding a channel plus a folder in
12
+ * `client/host-compat/versions/`. */
13
+ const SUPPORTED_RELEASES = [
14
+ {
15
+ channel: "0.1.5-rc",
16
+ release: "0.1.5-rc.2"
17
+ },
18
+ {
19
+ channel: "0.1.6-alpha",
20
+ release: "0.1.6-alpha.2"
21
+ },
22
+ {
23
+ channel: "0.1.7-alpha",
24
+ release: "0.1.7-alpha.2"
25
+ },
26
+ {
27
+ channel: "0.1.7-alpha",
28
+ release: "0.1.7-rc.1"
29
+ }
30
+ ];
31
+ const UNKNOWN_HOST_INFO = {
32
+ version: null,
33
+ channel: "unknown"
34
+ };
35
+ /** True when a string looks like a release version. A bare `~/.dsh` directory
36
+ * basename does not, which is why the disk probe sanity-checks before trusting
37
+ * a path segment. */
38
+ const VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
39
+ function looksLikeVersion(value) {
40
+ return VERSION_RE.test(value);
41
+ }
42
+ const PARSE_RE = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
43
+ function parse(version) {
44
+ const m = PARSE_RE.exec(version);
45
+ if (m === null) return null;
46
+ const parts = m[4] === void 0 ? [] : m[4].split(".");
47
+ const build = parts.length > 1 ? Number(parts[1]) : NaN;
48
+ return {
49
+ core: [
50
+ Number(m[1]),
51
+ Number(m[2]),
52
+ Number(m[3])
53
+ ],
54
+ channel: parts.length > 0 ? parts[0] : null,
55
+ build: Number.isFinite(build) ? build : null
56
+ };
57
+ }
58
+ /** The table, parsed once: every row is a literal this file controls. */
59
+ const TABLE = SUPPORTED_RELEASES.map((row) => ({
60
+ ...row,
61
+ parsed: parse(row.release)
62
+ }));
63
+ function cmpCore(a, b) {
64
+ for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1;
65
+ return 0;
66
+ }
67
+ /** Total order over releases, following the semver rule that a prerelease sorts
68
+ * BELOW the release it precedes (`0.1.6-alpha.2 < 0.1.6`). */
69
+ function cmp(a, b) {
70
+ const core = cmpCore(a.core, b.core);
71
+ if (core !== 0) return core;
72
+ if (a.channel === null || b.channel === null) {
73
+ if (a.channel === b.channel) return 0;
74
+ return a.channel === null ? 1 : -1;
75
+ }
76
+ if (a.channel !== b.channel) return a.channel < b.channel ? -1 : 1;
77
+ const ab = a.build ?? 0;
78
+ const bb = b.build ?? 0;
79
+ return ab === bb ? 0 : ab < bb ? -1 : 1;
80
+ }
81
+ /** Pick the adapter for a release, clamping to the nearest verified line when the
82
+ * release is outside everything this plugin has seen.
83
+ *
84
+ * A release is matched by PATCH LINE before anything else: `0.1.6-alpha.4` and a
85
+ * future `0.1.6` stable both belong to the line verified at `0.1.6-alpha.2`, and
86
+ * handing them that adapter beats giving up — the DOM probes only cover panel
87
+ * mechanics, not the surface decisions the line is known for.
88
+ *
89
+ * Outside every verified line the answer is a guess, so it is the closest one and
90
+ * it says so (`nearest`): newer than the newest line → that newest adapter, older
91
+ * than the oldest → the oldest, strictly between two → the LOWER one, because an
92
+ * adapter may only claim what it was verified for. A release that will not parse
93
+ * at all is `unknown`, and the DOM arbitrates there. */
94
+ function classifyRelease(version) {
95
+ const host = version === null ? null : parse(version);
96
+ if (host === null) return {
97
+ channel: "unknown",
98
+ match: "unresolved"
99
+ };
100
+ const sameLine = TABLE.filter((row) => cmpCore(row.parsed.core, host.core) === 0);
101
+ if (sameLine.length > 0) {
102
+ const exact = sameLine.find((row) => row.release === version);
103
+ if (exact !== void 0) return {
104
+ channel: exact.channel,
105
+ match: "exact"
106
+ };
107
+ const atOrBelow = sameLine.filter((row) => cmp(row.parsed, host) <= 0);
108
+ return {
109
+ channel: (atOrBelow.length > 0 ? atOrBelow[atOrBelow.length - 1] : sameLine[0]).channel,
110
+ match: "line"
111
+ };
112
+ }
113
+ const older = TABLE.filter((row) => cmp(row.parsed, host) < 0);
114
+ if (older.length === 0) return {
115
+ channel: TABLE[0].channel,
116
+ match: "nearest"
117
+ };
118
+ if (TABLE.filter((row) => cmp(row.parsed, host) > 0).length === 0) return {
119
+ channel: TABLE[TABLE.length - 1].channel,
120
+ match: "nearest"
121
+ };
122
+ return {
123
+ channel: older[older.length - 1].channel,
124
+ match: "nearest"
125
+ };
126
+ }
127
+ //#endregion
128
+ //#region src/host-compat/detect.ts
129
+ /**
130
+ * The front door: resolve which DSH release this plugin is running under.
131
+ *
132
+ * WHY IT RUNS HERE (node) AND NOT IN THE BROWSER
133
+ * The client context exposes no host version — verified against the harness: no
134
+ * `hostVersion`/`dshVersion` field on `ctx`, and `window.__DSH_BOOT__.version` is
135
+ * the module-table format tag `'client'`, not a release. Capability probing alone
136
+ * is also not enough, because the obvious probes do not separate the release
137
+ * lines:
138
+ * `ctx.sidebarRightTabs` and the `[data-sidebar-right-panel]` marker both exist on
139
+ * 0.1.5-rc.2 already, so either one used as a "0.1.6+" test mis-classifies 0.1.5
140
+ * as newer. The install the process was composed FROM states the release directly,
141
+ * and only the node half can read it.
142
+ *
143
+ * Three hops, most authoritative first:
144
+ * 1. `ctx.profileContext.installAnchor` — the absolute path of the running
145
+ * app's own manifest (`<…>/node_modules/@deepseek-ai/dsh/package.json`; the
146
+ * host passes exactly that file, `new URL('../package.json', import.meta.url)`
147
+ * of its `dsh` package). Whatever process is serving this plugin is what that
148
+ * manifest versioned, so it needs no path guessing. Provided by
149
+ * `dsh-app-boot` only in a dsh-launched profile, hence optional.
150
+ * 2. The installed manifest next to the launcher home:
151
+ * `<root>/versions/<ver>/node_modules/@deepseek-ai/dsh/package.json`, reached
152
+ * from `$DSH_HOME`'s `homes/<ver>` layout. For a host that gives no
153
+ * `profileContext`.
154
+ * 3. `$DSH_HOME`'s basename, because launcher homes live in a folder named after
155
+ * the release (`.../homes/0.1.6-alpha.2`). A user may point DSH_HOME at a
156
+ * plain `~/.dsh` (no launcher layout at all), whose basename is not a
157
+ * version — hence `looksLikeVersion` gates it.
158
+ *
159
+ * A release that resolves but matches no verified patch line is clamped to the
160
+ * nearest adapter by `classifyRelease()`; only a release that will not parse at
161
+ * all degrades to `version: null` / `channel: 'unknown'`, where the client half
162
+ * falls back to DOM-shape probing. That fallback is not hypothetical: a plain
163
+ * `~/.dsh` install lands here, and a wrong guess would silently restyle the
164
+ * user's window.
165
+ *
166
+ * @module
167
+ */
168
+ /** What "the verified lines" are, spelled out for the clamp log below. */
169
+ const SUPPORTED_HINT = SUPPORTED_RELEASES.map((row) => row.release).join(" ~ ");
170
+ /** Read the `version` field of a package manifest, or null when the file is
171
+ * missing, unreadable, not JSON or does not declare a release-looking version —
172
+ * every one of which just means "this hop has no answer". */
173
+ async function manifestVersion(file) {
174
+ try {
175
+ const raw = JSON.parse(await readFile(file, "utf8"));
176
+ if (typeof raw.version === "string" && looksLikeVersion(raw.version.trim())) return raw.version.trim();
177
+ } catch {}
178
+ return null;
179
+ }
180
+ /** Hop 1: the anchor the host itself hands us. A custom composition may point it
181
+ * at its own manifest, whose `version` is whatever that project calls itself —
182
+ * `looksLikeVersion` inside `manifestVersion` is what keeps a non-release out. */
183
+ async function versionFromAnchor(anchor) {
184
+ if (typeof anchor !== "string" || anchor === "") return null;
185
+ if (!/[/\\]package\.json$/.test(anchor)) return null;
186
+ return manifestVersion(anchor);
187
+ }
188
+ /** Hop 2: the installed manifest two levels up from a launcher home
189
+ * (`<root>/homes/<ver>` → `<root>/versions/<ver>`), or null when the layout does
190
+ * not apply (plain `~/.dsh`, non-launcher hosts). */
191
+ async function versionFromLauncher(homeDir) {
192
+ const root = dirname(dirname(homeDir));
193
+ const versionSeg = basename(homeDir);
194
+ if (!looksLikeVersion(versionSeg)) return null;
195
+ return manifestVersion(join(root, "versions", versionSeg, "node_modules", "@deepseek-ai", "dsh", "package.json"));
196
+ }
197
+ /** Cached verdict: the probe touches the filesystem, and the host release never
198
+ * changes within a process lifetime. */
199
+ let cache;
200
+ /** Resolve (and cache) the host release + channel. `installAnchor` is
201
+ * `ctx.profileContext?.installAnchor`, read at plugin mount time.
202
+ *
203
+ * A clamped verdict (`nearest`) is logged once: it means the plugin is styling a
204
+ * release it was never verified against, and that is the first thing to know when
205
+ * a new host build reports a broken surface. */
206
+ async function resolveHostInfo(installAnchor) {
207
+ if (cache !== void 0) return cache;
208
+ const homeDir = dshHomePath();
209
+ let version = await versionFromAnchor(installAnchor);
210
+ if (version === null) version = await versionFromLauncher(homeDir);
211
+ if (version === null) {
212
+ const seg = basename(homeDir);
213
+ if (looksLikeVersion(seg)) version = seg;
214
+ }
215
+ const verdict = classifyRelease(version);
216
+ if (verdict.match === "nearest") console.log(`dsh-any-background: host release ${version} is outside the verified lines, styling it as ${verdict.channel} (supports ${SUPPORTED_HINT})`);
217
+ cache = {
218
+ version,
219
+ channel: verdict.channel
220
+ };
221
+ return cache;
222
+ }
223
+ //#endregion
6
224
  //#region src/index.ts
7
225
  /**
8
226
  * Node half of dsh-any-background: file-backed theme persistence.
@@ -87,13 +305,13 @@ function sniffFontMime(buf) {
87
305
  const DEFAULT_CONFIG = {
88
306
  color: null,
89
307
  opacities: {
90
- bg: .5,
308
+ bg: 0,
91
309
  sidebar: .5,
92
310
  card: .5,
93
311
  input: .5
94
312
  },
95
313
  blurs: {
96
- bg: 30,
314
+ bg: 0,
97
315
  sidebar: 30,
98
316
  card: 30,
99
317
  settings: 30,
@@ -158,7 +376,7 @@ const DEFAULT_CONFIG = {
158
376
  },
159
377
  settingsOpacity: .5,
160
378
  wallpaperOpacity: 1,
161
- blur: 30,
379
+ blur: 0,
162
380
  bgState: {
163
381
  zoom: 1,
164
382
  x: 0,
@@ -205,55 +423,6 @@ const DEFAULT_CONFIG = {
205
423
  schemeOverride: "auto",
206
424
  activeProfile: null
207
425
  };
208
- const VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
209
- /** True when a string looks like a release version (a bare `~/.dsh` basename does not). */
210
- function looksLikeVersion(value) {
211
- return VERSION_RE.test(value);
212
- }
213
- /** Read the installed `@deepseek-ai/dsh` manifest version for a launcher dir,
214
- * or null when the layout does not apply (plain `~/.dsh`, non-launcher hosts). */
215
- async function readInstalledVersion(homeDir) {
216
- const root = dirname(dirname(homeDir));
217
- const versionSeg = basename(homeDir);
218
- const manifest = join(root, "versions", versionSeg, "node_modules", "@deepseek-ai", "dsh", "package.json");
219
- try {
220
- const raw = JSON.parse(await readFile(manifest, "utf8"));
221
- if (typeof raw.version === "string" && raw.version.trim() !== "") return raw.version.trim();
222
- } catch {}
223
- return null;
224
- }
225
- /** Cached verdict: the probe touches the filesystem, and the host version never
226
- * changes within a process lifetime. */
227
- let hostInfoCache;
228
- /** Map a release string onto the generation bucket the plugin gates features by.
229
- * The published lines are `0.1.5`, `0.1.6` and `0.1.7`, so the bucket is the
230
- * `major.minor.patch` prefix — every rc/alpha within a line shares it. */
231
- function generationOf(version) {
232
- if (version === null) return "unknown";
233
- const m = /^(\d+\.\d+\.\d+)/.exec(version);
234
- if (m === null) return "unknown";
235
- const key = m[1];
236
- return key === "0.1.5" || key === "0.1.6" || key === "0.1.7" ? key : "unknown";
237
- }
238
- /** Resolve (and cache) the host release + generation. */
239
- async function resolveHostInfo() {
240
- if (hostInfoCache !== void 0) return hostInfoCache;
241
- const homeDir = dshHomePath();
242
- let version = null;
243
- try {
244
- version = await readInstalledVersion(homeDir);
245
- } catch {}
246
- if (version === null) {
247
- const seg = basename(homeDir);
248
- if (looksLikeVersion(seg)) version = seg;
249
- }
250
- if (version !== null && !looksLikeVersion(version)) version = null;
251
- hostInfoCache = {
252
- version,
253
- generation: generationOf(version)
254
- };
255
- return hostInfoCache;
256
- }
257
426
  const dataDir = () => dshHomePath(DATA_DIR);
258
427
  const configPath = () => dshHomePath(DATA_DIR, CONFIG_FILE);
259
428
  const wallpaperPath = () => dshHomePath(DATA_DIR, WALLPAPER_FILE);
@@ -1628,7 +1797,7 @@ async function advanceRotationIfDue() {
1628
1797
  }
1629
1798
  /** Dispatch one decoded RPC method to the matching persistence routine and
1630
1799
  * return the wire `result` half of the server-response envelope. */
1631
- async function handleRpcMethod(endpoint, payload) {
1800
+ async function handleRpcMethod(endpoint, payload, hostInfo) {
1632
1801
  const method = endpoint.slice(`${NS}/`.length);
1633
1802
  try {
1634
1803
  switch (method) {
@@ -1646,7 +1815,7 @@ async function handleRpcMethod(endpoint, payload) {
1646
1815
  fontUrl: await fontUrl(),
1647
1816
  rotated,
1648
1817
  firstRun,
1649
- host: await resolveHostInfo()
1818
+ host: await hostInfo
1650
1819
  }
1651
1820
  };
1652
1821
  }
@@ -1707,6 +1876,10 @@ async function handleRpcMethod(endpoint, payload) {
1707
1876
  }
1708
1877
  }
1709
1878
  function apply(ctx) {
1879
+ const hostInfo = resolveHostInfo(ctx.profileContext?.installAnchor).catch((e) => {
1880
+ console.warn("dsh-any-background: host release detection failed, falling back to DOM probing", e);
1881
+ return UNKNOWN_HOST_INFO;
1882
+ });
1710
1883
  ctx.inject(["connection", "webServer"], (webCtx) => {
1711
1884
  webCtx.effect(() => webCtx.webServer.register({
1712
1885
  kind: "prefix",
@@ -1809,7 +1982,7 @@ function apply(ctx) {
1809
1982
  }));
1810
1983
  return;
1811
1984
  }
1812
- const result = await handleRpcMethod(endpoint, env.payload);
1985
+ const result = await handleRpcMethod(endpoint, env.payload, hostInfo);
1813
1986
  res.writeHead(200, { "Content-Type": "application/json" });
1814
1987
  res.end(JSON.stringify({
1815
1988
  type: "server-response",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-any-background",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Appearance plugin for DeepSeek Harness: custom theme color (PS-style color wheel), background wallpaper with opacity/blur controls, and separate main/settings interface opacity.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -23,7 +23,7 @@
23
23
  "author": "Tkingxiao",
24
24
  "type": "module",
25
25
  "engines": {
26
- "dsh": "0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1"
26
+ "dsh": "0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1"
27
27
  },
28
28
  "exports": {
29
29
  ".": {
@@ -67,13 +67,13 @@
67
67
  },
68
68
  "peerDependencies": {
69
69
  "@deepseek-ai/cordis": "^4.0.2",
70
- "@deepseek-ai/dsh-client-locale": "0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1",
71
- "@deepseek-ai/dsh-client-store": "0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1",
72
- "@deepseek-ai/dsh-client-connection": "0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1",
73
- "@deepseek-ai/dsh-client-ui-settings": "0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1",
74
- "@deepseek-ai/dsh-client-ui-sidebar-right": "0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1",
75
- "@deepseek-ai/dsh-client-ui-theme": "0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1",
76
- "@deepseek-ai/dsh-invariants": "0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1",
70
+ "@deepseek-ai/dsh-client-locale": "0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1",
71
+ "@deepseek-ai/dsh-client-store": "0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1",
72
+ "@deepseek-ai/dsh-client-connection": "0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1",
73
+ "@deepseek-ai/dsh-client-ui-settings": "0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1",
74
+ "@deepseek-ai/dsh-client-ui-sidebar-right": "0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1",
75
+ "@deepseek-ai/dsh-client-ui-theme": "0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1",
76
+ "@deepseek-ai/dsh-invariants": "0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1",
77
77
  "react": "^18.2.0",
78
78
  "dsh-better-sidebar": ">=0.4.0"
79
79
  },
@@ -111,13 +111,15 @@
111
111
  },
112
112
  "dsh": {
113
113
  "compatibility": {
114
- "dsh": "0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1",
114
+ "dsh": "0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1",
115
115
  "dshReleases": {
116
116
  "0.1.5-rc.2": "compatible",
117
117
  "0.1.5-rc.3": "compatible",
118
118
  "0.1.6-alpha.1": "compatible",
119
119
  "0.1.6-alpha.2": "compatible",
120
- "0.1.7-alpha.1": "compatible"
120
+ "0.1.7-alpha.1": "compatible",
121
+ "0.1.7-alpha.2": "compatible",
122
+ "0.1.7-rc.1": "compatible"
121
123
  }
122
124
  },
123
125
  "bundle": {