dsh-any-background 0.2.9 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -1,5 +1,6 @@
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";
3
4
  import { Readable } from "node:stream";
4
5
  import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
5
6
  //#region src/index.ts
@@ -100,7 +101,8 @@ const DEFAULT_CONFIG = {
100
101
  trajectory: 30,
101
102
  input: 30,
102
103
  panel: 30,
103
- produced: 30
104
+ produced: 30,
105
+ header: 30
104
106
  },
105
107
  strokes: {
106
108
  bg: {
@@ -147,6 +149,11 @@ const DEFAULT_CONFIG = {
147
149
  width: 0,
148
150
  color: "auto",
149
151
  customColor: "#808080"
152
+ },
153
+ header: {
154
+ width: 0,
155
+ color: "auto",
156
+ customColor: "#808080"
150
157
  }
151
158
  },
152
159
  settingsOpacity: .5,
@@ -177,6 +184,7 @@ const DEFAULT_CONFIG = {
177
184
  trajectoryOpacity: .5,
178
185
  panelOpacity: .5,
179
186
  producedOpacity: .5,
187
+ headerOpacity: .5,
180
188
  profiles: [],
181
189
  rotation: {
182
190
  enabled: false,
@@ -197,6 +205,55 @@ const DEFAULT_CONFIG = {
197
205
  schemeOverride: "auto",
198
206
  activeProfile: null
199
207
  };
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
+ }
200
257
  const dataDir = () => dshHomePath(DATA_DIR);
201
258
  const configPath = () => dshHomePath(DATA_DIR, CONFIG_FILE);
202
259
  const wallpaperPath = () => dshHomePath(DATA_DIR, WALLPAPER_FILE);
@@ -297,7 +354,8 @@ const STROKE_GROUPS = [
297
354
  "trajectory",
298
355
  "input",
299
356
  "panel",
300
- "produced"
357
+ "produced",
358
+ "header"
301
359
  ];
302
360
  const STROKE_COLOR_KEYS = [
303
361
  "auto",
@@ -359,7 +417,8 @@ function normalizeConfig(raw) {
359
417
  "trajectory",
360
418
  "input",
361
419
  "panel",
362
- "produced"
420
+ "produced",
421
+ "header"
363
422
  ]) blurs[k] = clamp(bl[k], 0, 60, DEFAULT_CONFIG.blurs[k]);
364
423
  return {
365
424
  color,
@@ -387,6 +446,7 @@ function normalizeConfig(raw) {
387
446
  trajectoryOpacity: clamp(r.trajectoryOpacity, 0, 1, DEFAULT_CONFIG.trajectoryOpacity),
388
447
  panelOpacity: clamp(r.panelOpacity, 0, 1, DEFAULT_CONFIG.panelOpacity),
389
448
  producedOpacity: clamp(r.producedOpacity, 0, 1, DEFAULT_CONFIG.producedOpacity),
449
+ headerOpacity: clamp(r.headerOpacity, 0, 1, DEFAULT_CONFIG.headerOpacity),
390
450
  profiles: normalizeProfiles(r.profiles),
391
451
  rotation: normalizeRotation(r.rotation),
392
452
  schedule: normalizeSchedule(r.schedule),
@@ -471,7 +531,8 @@ function normalizeProfileAppearance(raw) {
471
531
  chatTextOpacity: clamp(a.chatTextOpacity, 0, 1, DEFAULT_CONFIG.chatTextOpacity),
472
532
  trajectoryOpacity: clamp(a.trajectoryOpacity, 0, 1, DEFAULT_CONFIG.trajectoryOpacity),
473
533
  panelOpacity: clamp(a.panelOpacity, 0, 1, DEFAULT_CONFIG.panelOpacity),
474
- producedOpacity: clamp(a.producedOpacity, 0, 1, DEFAULT_CONFIG.producedOpacity)
534
+ producedOpacity: clamp(a.producedOpacity, 0, 1, DEFAULT_CONFIG.producedOpacity),
535
+ headerOpacity: clamp(a.headerOpacity, 0, 1, DEFAULT_CONFIG.headerOpacity)
475
536
  };
476
537
  }
477
538
  function normalizeProfiles(raw) {
@@ -536,12 +597,41 @@ async function ensureDir() {
536
597
  console.warn(`dsh-any-background: cannot create data dir "${dataDir()}"`, e);
537
598
  }
538
599
  }
600
+ let configCacheKey = null;
601
+ let configCacheValue = null;
602
+ function dropConfigCache() {
603
+ configCacheKey = null;
604
+ configCacheValue = null;
605
+ }
539
606
  async function readConfig() {
540
607
  await ensureDir();
608
+ const file = configPath();
609
+ let text;
610
+ let key;
541
611
  try {
542
- const raw = await readFile(configPath(), "utf8");
543
- return normalizeConfig(JSON.parse(raw));
612
+ const st = await stat(file);
613
+ key = {
614
+ mtimeMs: st.mtimeMs,
615
+ size: st.size
616
+ };
617
+ if (configCacheKey !== null && configCacheValue !== null && configCacheKey.mtimeMs === key.mtimeMs && configCacheKey.size === key.size) return configCacheValue;
618
+ text = await readFile(file, "utf8");
544
619
  } catch {
620
+ dropConfigCache();
621
+ return { ...DEFAULT_CONFIG };
622
+ }
623
+ try {
624
+ const parsed = normalizeConfig(JSON.parse(text));
625
+ configCacheKey = key;
626
+ configCacheValue = parsed;
627
+ return parsed;
628
+ } catch (e) {
629
+ try {
630
+ const into = `${file}.corrupt-${Date.now()}`;
631
+ await rename(file, into);
632
+ console.warn(`dsh-any-background: theme-config.json was corrupt, archived to "${into}" and reset to defaults`, e);
633
+ } catch {}
634
+ dropConfigCache();
545
635
  return { ...DEFAULT_CONFIG };
546
636
  }
547
637
  }
@@ -573,13 +663,19 @@ function warnUnknownConfigKeys(raw, normalized) {
573
663
  }
574
664
  async function writeConfig(config) {
575
665
  await ensureDir();
666
+ const tmp = `${configPath()}.tmp`;
576
667
  try {
577
668
  const normalized = normalizeConfig(config);
578
669
  warnUnknownConfigKeys(config, normalized);
579
- await writeFile(configPath(), JSON.stringify(normalized, null, 2), "utf8");
670
+ await writeFile(tmp, JSON.stringify(normalized, null, 2), "utf8");
671
+ await rename(tmp, configPath());
672
+ dropConfigCache();
580
673
  return true;
581
674
  } catch (e) {
582
675
  console.error(`dsh-any-background: failed to write "${CONFIG_FILE}"`, e);
676
+ try {
677
+ await rm(tmp, { force: true });
678
+ } catch {}
583
679
  return false;
584
680
  }
585
681
  }
@@ -618,6 +714,17 @@ function sniffImageMime(buf) {
618
714
  if (buf.length >= 12 && buf[0] === 82 && buf[1] === 73 && buf[2] === 70 && buf[3] === 70 && buf[8] === 87 && buf[9] === 69 && buf[10] === 66 && buf[11] === 80) return "image/webp";
619
715
  return "image/jpeg";
620
716
  }
717
+ /** True when the leading bytes are a container the serve route knows how to
718
+ * sniff. The Content-Type only reflects what the server claims; validating the
719
+ * bytes themselves rejects a mislabeled or hostile payload with a clear error
720
+ * instead of persisting a file that renders as a broken image. */
721
+ function isImageBytes(buf) {
722
+ if (buf.length >= 4 && buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) return true;
723
+ if (buf.length >= 3 && buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return true;
724
+ if (buf.length >= 6 && buf[0] === 71 && buf[1] === 73 && buf[2] === 70) return true;
725
+ if (buf.length >= 12 && buf[0] === 82 && buf[1] === 73 && buf[2] === 70 && buf[3] === 70 && buf[8] === 87 && buf[9] === 69 && buf[10] === 66 && buf[11] === 80) return true;
726
+ return false;
727
+ }
621
728
  /** Download a wallpaper from a network URL and persist it into the local
622
729
  * wallpaper.jpg slot (replacing whatever was stored), so type switches and
623
730
  * rotation keep working through the single active slot. The response carries
@@ -690,6 +797,10 @@ async function writeWallpaperFromUrl(url) {
690
797
  error: "read failed"
691
798
  };
692
799
  }
800
+ if (!isImageBytes(buf)) return {
801
+ ok: false,
802
+ error: "not an image"
803
+ };
693
804
  await ensureDir();
694
805
  try {
695
806
  await writeFile(wallpaperPath(), buf);
@@ -911,21 +1022,28 @@ async function writeVideoFromUrl(url) {
911
1022
  const out = createWriteStream(tmp);
912
1023
  let received = 0;
913
1024
  let failed = false;
914
- const fail = () => {
915
- if (failed) return;
916
- failed = true;
917
- out.destroy();
918
- rm(tmp, { force: true });
919
- };
920
1025
  const nodeStream = Readable.fromWeb(res.body);
921
1026
  let idle = null;
1027
+ const clearIdle = () => {
1028
+ if (idle !== null) {
1029
+ clearTimeout(idle);
1030
+ idle = null;
1031
+ }
1032
+ };
922
1033
  const pokeIdle = () => {
923
- if (idle) clearTimeout(idle);
1034
+ clearIdle();
924
1035
  idle = setTimeout(() => {
925
1036
  nodeStream.destroy();
926
1037
  fail();
927
1038
  }, VIDEO_FETCH_IDLE_TIMEOUT);
928
1039
  };
1040
+ const fail = () => {
1041
+ if (failed) return;
1042
+ failed = true;
1043
+ clearIdle();
1044
+ out.destroy();
1045
+ rmWhenClosed(out, tmp);
1046
+ };
929
1047
  nodeStream.on("data", (chunk) => {
930
1048
  pokeIdle();
931
1049
  received += chunk.byteLength;
@@ -934,10 +1052,7 @@ async function writeVideoFromUrl(url) {
934
1052
  fail();
935
1053
  }
936
1054
  });
937
- nodeStream.on("end", () => {
938
- if (idle) clearTimeout(idle);
939
- idle = null;
940
- });
1055
+ nodeStream.on("end", clearIdle);
941
1056
  nodeStream.on("aborted", fail);
942
1057
  nodeStream.on("error", fail);
943
1058
  out.on("error", fail);
@@ -1001,6 +1116,37 @@ async function writeVideo(dataUrl) {
1001
1116
  return false;
1002
1117
  }
1003
1118
  }
1119
+ /** Drop a partial upload's temp file once its sink is really closed. Windows
1120
+ * refuses to unlink a file that still has an open handle, so removing it in the
1121
+ * same tick as `out.destroy()` silently fails and leaves the aborted transfer
1122
+ * on disk (up to the full limit) until some later upload overwrites it. */
1123
+ function rmWhenClosed(out, tmp) {
1124
+ const drop = () => {
1125
+ rm(tmp, { force: true });
1126
+ };
1127
+ if (out.closed === true) drop();
1128
+ else out.once("close", drop);
1129
+ }
1130
+ /** Reject an oversized upload with a real status and tear the transfer down.
1131
+ * The bare req.destroy()+fail() path left the client holding a network error
1132
+ * with no way to tell "too large" from "connection died". The body is machine
1133
+ * readable (`error` + the enforced `limit`) so the panel can phrase the
1134
+ * refusal in the user's own language, with `message` kept for logs. */
1135
+ function rejectOversizedUpload(req, res, fail, limit) {
1136
+ try {
1137
+ res.writeHead(413, { "Content-Type": "application/json" });
1138
+ res.end(JSON.stringify({
1139
+ ok: false,
1140
+ error: "too large",
1141
+ limit,
1142
+ message: `too large (limit ${limit})`
1143
+ }));
1144
+ } catch {}
1145
+ fail();
1146
+ try {
1147
+ req.destroy();
1148
+ } catch {}
1149
+ }
1004
1150
  /** Stream the stored video: correct MIME, no caching, Range answers so the
1005
1151
  * browser can seek (snapshot capture does). */
1006
1152
  async function serveVideo(req, res) {
@@ -1104,16 +1250,14 @@ async function handleVideoUpload(req, res) {
1104
1250
  if (failed) return;
1105
1251
  failed = true;
1106
1252
  out.destroy();
1107
- rm(tmp, { force: true });
1253
+ rmWhenClosed(out, tmp);
1108
1254
  };
1109
1255
  req.on("aborted", fail);
1110
1256
  req.on("error", fail);
1111
1257
  req.on("data", (chunk) => {
1258
+ if (failed) return;
1112
1259
  received += chunk.byteLength;
1113
- if (received > VIDEO_UPLOAD_MAX) {
1114
- req.destroy();
1115
- fail();
1116
- }
1260
+ if (received > VIDEO_UPLOAD_MAX) rejectOversizedUpload(req, res, fail, "2 GB");
1117
1261
  });
1118
1262
  out.on("error", () => {
1119
1263
  fail();
@@ -1226,7 +1370,7 @@ async function handleWallpaperUpload(req, res) {
1226
1370
  if (failed) return;
1227
1371
  failed = true;
1228
1372
  out.destroy();
1229
- rm(tmp, { force: true });
1373
+ rmWhenClosed(out, tmp);
1230
1374
  };
1231
1375
  req.on("aborted", fail);
1232
1376
  req.on("error", fail);
@@ -1238,11 +1382,9 @@ async function handleWallpaperUpload(req, res) {
1238
1382
  } catch {}
1239
1383
  });
1240
1384
  req.on("data", (chunk) => {
1385
+ if (failed) return;
1241
1386
  received += chunk.byteLength;
1242
- if (received > WALLPAPER_UPLOAD_MAX) {
1243
- req.destroy();
1244
- fail();
1245
- }
1387
+ if (received > WALLPAPER_UPLOAD_MAX) rejectOversizedUpload(req, res, fail, "100 MB");
1246
1388
  });
1247
1389
  req.pipe(out);
1248
1390
  out.on("finish", async () => {
@@ -1338,7 +1480,7 @@ async function handleFontUpload(req, res) {
1338
1480
  if (failed) return;
1339
1481
  failed = true;
1340
1482
  out.destroy();
1341
- rm(tmp, { force: true });
1483
+ rmWhenClosed(out, tmp);
1342
1484
  };
1343
1485
  req.on("aborted", fail);
1344
1486
  req.on("error", fail);
@@ -1350,11 +1492,9 @@ async function handleFontUpload(req, res) {
1350
1492
  } catch {}
1351
1493
  });
1352
1494
  req.on("data", (chunk) => {
1495
+ if (failed) return;
1353
1496
  received += chunk.byteLength;
1354
- if (received > FONT_UPLOAD_MAX) {
1355
- req.destroy();
1356
- fail();
1357
- }
1497
+ if (received > FONT_UPLOAD_MAX) rejectOversizedUpload(req, res, fail, "100 MB");
1358
1498
  });
1359
1499
  req.pipe(out);
1360
1500
  out.on("finish", async () => {
@@ -1458,8 +1598,7 @@ function isoWeekKey(d) {
1458
1598
  * old one never reaches the screen. The client's maybeRotate stays as a
1459
1599
  * fallback and skips when this already advanced (the `rotated` flag). */
1460
1600
  async function advanceRotationIfDue() {
1461
- const cfg = await readConfig();
1462
- const rot = cfg.rotation;
1601
+ const rot = (await readConfig()).rotation;
1463
1602
  if (!rot.enabled || rot.items.length === 0) return false;
1464
1603
  const now = /* @__PURE__ */ new Date();
1465
1604
  const last = rot.lastRotate !== null ? new Date(rot.lastRotate) : null;
@@ -1477,12 +1616,14 @@ async function advanceRotationIfDue() {
1477
1616
  console.error("dsh-any-background: failed to advance the rotation pool", e);
1478
1617
  return false;
1479
1618
  }
1480
- cfg.rotation = {
1481
- ...rot,
1482
- current: idx,
1483
- lastRotate: now.toISOString()
1484
- };
1485
- if (!await writeConfig(cfg)) return false;
1619
+ if (!await writeConfig({
1620
+ ...await readConfig(),
1621
+ rotation: {
1622
+ ...rot,
1623
+ current: idx,
1624
+ lastRotate: now.toISOString()
1625
+ }
1626
+ })) return false;
1486
1627
  return true;
1487
1628
  }
1488
1629
  /** Dispatch one decoded RPC method to the matching persistence routine and
@@ -1504,7 +1645,8 @@ async function handleRpcMethod(endpoint, payload) {
1504
1645
  videoUrl: await videoUrl(),
1505
1646
  fontUrl: await fontUrl(),
1506
1647
  rotated,
1507
- firstRun
1648
+ firstRun,
1649
+ host: await resolveHostInfo()
1508
1650
  }
1509
1651
  };
1510
1652
  }
@@ -1588,6 +1730,22 @@ function apply(ctx) {
1588
1730
  }));
1589
1731
  return;
1590
1732
  }
1733
+ const declaredLen = Number(req.headers["content-length"] ?? "");
1734
+ if (Number.isFinite(declaredLen) && declaredLen > RPC_BODY_MAX) {
1735
+ res.writeHead(413, {
1736
+ "Content-Type": "application/json",
1737
+ connection: "close"
1738
+ });
1739
+ res.end(JSON.stringify({
1740
+ ok: false,
1741
+ error: {
1742
+ code: "dsh-any-background/too-large",
1743
+ message: `body exceeds ${RPC_BODY_MAX} bytes; uploads must use the binary routes`,
1744
+ details: {}
1745
+ }
1746
+ }));
1747
+ return;
1748
+ }
1591
1749
  const pathname = new URL(req.url ?? "/", "http://dsh.internal").pathname;
1592
1750
  const endpoint = pathname.startsWith(`${RPC_CHANNEL}/`) ? pathname.slice(20) : void 0;
1593
1751
  if (endpoint === void 0 || endpoint.length === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-any-background",
3
- "version": "0.2.9",
3
+ "version": "0.3.0",
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"
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"
27
27
  },
28
28
  "exports": {
29
29
  ".": {
@@ -67,15 +67,23 @@
67
67
  },
68
68
  "peerDependencies": {
69
69
  "@deepseek-ai/cordis": "^4.0.2",
70
- "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.2",
71
- "@deepseek-ai/dsh-client-store": "^0.1.2-alpha.2",
72
- "@deepseek-ai/dsh-client-connection": "^0.1.2-alpha.2",
73
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.2-alpha.2",
74
- "@deepseek-ai/dsh-client-ui-theme": "^0.1.2-alpha.2",
75
- "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
76
- "react": "^18.2.0"
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",
77
+ "react": "^18.2.0",
78
+ "dsh-better-sidebar": ">=0.4.0"
77
79
  },
78
80
  "peerDependenciesMeta": {
81
+ "dsh-better-sidebar": {
82
+ "optional": true
83
+ },
84
+ "@deepseek-ai/dsh-client-ui-sidebar-right": {
85
+ "optional": true
86
+ },
79
87
  "react": {
80
88
  "optional": true
81
89
  },
@@ -103,9 +111,13 @@
103
111
  },
104
112
  "dsh": {
105
113
  "compatibility": {
106
- "dsh": "0.1.5-rc.2",
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",
107
115
  "dshReleases": {
108
- "0.1.5-rc.2": "compatible"
116
+ "0.1.5-rc.2": "compatible",
117
+ "0.1.5-rc.3": "compatible",
118
+ "0.1.6-alpha.1": "compatible",
119
+ "0.1.6-alpha.2": "compatible",
120
+ "0.1.7-alpha.1": "compatible"
109
121
  }
110
122
  },
111
123
  "bundle": {