dsh-any-background 0.2.7 → 0.2.9

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
@@ -27,10 +27,14 @@ const VIDEO_ROUTE = "/dsh-any-background/video";
27
27
  const UPLOAD_ROUTE = "/dsh-any-background/video/upload";
28
28
  const WALLPAPER_ROUTE = "/dsh-any-background/wallpaper";
29
29
  const WALLPAPER_UPLOAD_ROUTE = "/dsh-any-background/wallpaper/upload";
30
+ const FONT_ROUTE = "/dsh-any-background/font";
31
+ const FONT_UPLOAD_ROUTE = "/dsh-any-background/font/upload";
30
32
  const UPLOAD_TMP = "wallpaper.upload.tmp";
31
33
  const VIDEO_UPLOAD_TMP = "video.upload.tmp";
34
+ const FONT_UPLOAD_TMP = "font.upload.tmp";
32
35
  const WALLPAPER_UPLOAD_MAX = 104857600;
33
36
  const VIDEO_UPLOAD_MAX = 2147483648;
37
+ const FONT_UPLOAD_MAX = 104857600;
34
38
  const WALLPAPER_FETCH_MAX = 26214400;
35
39
  const WALLPAPER_FETCH_TIMEOUT = 2e4;
36
40
  const VIDEO_FETCH_MAX = 2147483648;
@@ -54,27 +58,100 @@ const VIDEO_CANDIDATES = [
54
58
  "wallpaper.mkv",
55
59
  "wallpaper.video"
56
60
  ];
61
+ /** Font slot: one font owns the slot, named by format like the video slot. */
62
+ function fontFileName(mime) {
63
+ switch (mime) {
64
+ case "font/woff2": return "font.woff2";
65
+ case "font/woff": return "font.woff";
66
+ case "font/otf": return "font.otf";
67
+ case "font/ttf": return "font.ttf";
68
+ default: return "font.ttf";
69
+ }
70
+ }
71
+ const FONT_CANDIDATES = [
72
+ "font.woff2",
73
+ "font.woff",
74
+ "font.otf",
75
+ "font.ttf"
76
+ ];
77
+ /** Sniff a font's container format from its leading magic bytes (null when the
78
+ * bytes are not a recognized font — uploads are rejected rather than stored). */
79
+ function sniffFontMime(buf) {
80
+ if (buf.length >= 4 && buf[0] === 119 && buf[1] === 79 && buf[2] === 70 && buf[3] === 50) return "font/woff2";
81
+ if (buf.length >= 4 && buf[0] === 119 && buf[1] === 79 && buf[2] === 70 && buf[3] === 70) return "font/woff";
82
+ if (buf.length >= 4 && buf[0] === 79 && buf[1] === 84 && buf[2] === 84 && buf[3] === 79) return "font/otf";
83
+ if (buf.length >= 4 && buf[0] === 0 && buf[1] === 1 && buf[2] === 0 && buf[3] === 0) return "font/ttf";
84
+ return null;
85
+ }
57
86
  const DEFAULT_CONFIG = {
58
87
  color: null,
59
88
  opacities: {
60
- bg: .85,
61
- sidebar: .93,
62
- card: 1,
63
- input: 1
89
+ bg: .5,
90
+ sidebar: .5,
91
+ card: .5,
92
+ input: .5
64
93
  },
65
94
  blurs: {
66
- bg: 0,
67
- sidebar: 0,
68
- card: 0,
69
- settings: 0,
70
- chat: 0,
71
- trajectory: 0,
72
- input: 0,
73
- panel: 0
95
+ bg: 30,
96
+ sidebar: 30,
97
+ card: 30,
98
+ settings: 30,
99
+ chat: 30,
100
+ trajectory: 30,
101
+ input: 30,
102
+ panel: 30,
103
+ produced: 30
74
104
  },
75
- settingsOpacity: 1,
105
+ strokes: {
106
+ bg: {
107
+ width: 0,
108
+ color: "auto",
109
+ customColor: "#808080"
110
+ },
111
+ sidebar: {
112
+ width: 0,
113
+ color: "auto",
114
+ customColor: "#808080"
115
+ },
116
+ card: {
117
+ width: 0,
118
+ color: "auto",
119
+ customColor: "#808080"
120
+ },
121
+ settings: {
122
+ width: 0,
123
+ color: "auto",
124
+ customColor: "#808080"
125
+ },
126
+ chat: {
127
+ width: 0,
128
+ color: "auto",
129
+ customColor: "#808080"
130
+ },
131
+ trajectory: {
132
+ width: 0,
133
+ color: "auto",
134
+ customColor: "#808080"
135
+ },
136
+ input: {
137
+ width: 0,
138
+ color: "auto",
139
+ customColor: "#808080"
140
+ },
141
+ panel: {
142
+ width: 0,
143
+ color: "auto",
144
+ customColor: "#808080"
145
+ },
146
+ produced: {
147
+ width: 0,
148
+ color: "auto",
149
+ customColor: "#808080"
150
+ }
151
+ },
152
+ settingsOpacity: .5,
76
153
  wallpaperOpacity: 1,
77
- blur: 0,
154
+ blur: 30,
78
155
  bgState: {
79
156
  zoom: 1,
80
157
  x: 0,
@@ -92,11 +169,14 @@ const DEFAULT_CONFIG = {
92
169
  backgroundType: "image",
93
170
  bgMode: "fit",
94
171
  videoMime: null,
172
+ fontMime: null,
173
+ fontEnabled: true,
95
174
  generatedBg: null,
96
175
  regenerateOnReload: false,
97
- chatTextOpacity: 0,
98
- trajectoryOpacity: 1,
99
- panelOpacity: 1,
176
+ chatTextOpacity: .5,
177
+ trajectoryOpacity: .5,
178
+ panelOpacity: .5,
179
+ producedOpacity: .5,
100
180
  profiles: [],
101
181
  rotation: {
102
182
  enabled: false,
@@ -121,6 +201,7 @@ const dataDir = () => dshHomePath(DATA_DIR);
121
201
  const configPath = () => dshHomePath(DATA_DIR, CONFIG_FILE);
122
202
  const wallpaperPath = () => dshHomePath(DATA_DIR, WALLPAPER_FILE);
123
203
  const videoPathFor = (mime) => dshHomePath(DATA_DIR, videoFileName(mime));
204
+ const fontPathFor = (mime) => dshHomePath(DATA_DIR, fontFileName(mime));
124
205
  const exists = async (p) => {
125
206
  try {
126
207
  await access(p);
@@ -160,6 +241,43 @@ async function findVideoFile() {
160
241
  function clamp(n, lo, hi, def) {
161
242
  return typeof n === "number" && isFinite(n) ? Math.min(hi, Math.max(lo, n)) : def;
162
243
  }
244
+ /** Locate the stored font: the recorded MIME decides the expected name; stray
245
+ * files from a lost config write are adopted via rename (mirrors the video
246
+ * slot's recovery). */
247
+ async function findFontFile() {
248
+ const mime = (await readConfig()).fontMime ?? "font/ttf";
249
+ const expected = fontPathFor(mime);
250
+ if (await exists(expected)) return {
251
+ path: expected,
252
+ mime
253
+ };
254
+ for (const name of FONT_CANDIDATES) {
255
+ const p = dshHomePath(DATA_DIR, name);
256
+ if (!await exists(p)) continue;
257
+ const foundMime = mimeForFontFile(name);
258
+ try {
259
+ await rename(p, expected);
260
+ return {
261
+ path: expected,
262
+ mime: foundMime
263
+ };
264
+ } catch {
265
+ return null;
266
+ }
267
+ }
268
+ return null;
269
+ }
270
+ function mimeForFontFile(name) {
271
+ switch (name) {
272
+ case "font.woff2": return "font/woff2";
273
+ case "font.woff": return "font/woff";
274
+ case "font.otf": return "font/otf";
275
+ default: return "font/ttf";
276
+ }
277
+ }
278
+ async function fontUrl() {
279
+ return await findFontFile() ? FONT_ROUTE : null;
280
+ }
163
281
  function normalizeBgState(s) {
164
282
  return {
165
283
  zoom: clamp(s.zoom, .1, 10, 1),
@@ -170,6 +288,40 @@ function normalizeBgState(s) {
170
288
  };
171
289
  }
172
290
  /** Coerce an unknown persisted value into a valid ThemeConfig, falling back per-field. */
291
+ const STROKE_GROUPS = [
292
+ "bg",
293
+ "sidebar",
294
+ "card",
295
+ "settings",
296
+ "chat",
297
+ "trajectory",
298
+ "input",
299
+ "panel",
300
+ "produced"
301
+ ];
302
+ const STROKE_COLOR_KEYS = [
303
+ "auto",
304
+ "gray",
305
+ "black",
306
+ "white",
307
+ "theme",
308
+ "custom"
309
+ ];
310
+ const HEX_RE = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
311
+ function normalizeStroke(raw) {
312
+ const s = raw ?? {};
313
+ return {
314
+ width: clamp(s.width, 0, 4, 0),
315
+ color: STROKE_COLOR_KEYS.includes(s.color) ? s.color : "auto",
316
+ customColor: typeof s.customColor === "string" && HEX_RE.test(s.customColor) ? s.customColor : "#808080"
317
+ };
318
+ }
319
+ function normalizeStrokes(raw) {
320
+ const s = raw ?? {};
321
+ const out = {};
322
+ for (const k of STROKE_GROUPS) out[k] = normalizeStroke(s[k]);
323
+ return out;
324
+ }
173
325
  function normalizeConfig(raw) {
174
326
  const r = raw ?? {};
175
327
  const c = r.color;
@@ -206,7 +358,8 @@ function normalizeConfig(raw) {
206
358
  "chat",
207
359
  "trajectory",
208
360
  "input",
209
- "panel"
361
+ "panel",
362
+ "produced"
210
363
  ]) blurs[k] = clamp(bl[k], 0, 60, DEFAULT_CONFIG.blurs[k]);
211
364
  return {
212
365
  color,
@@ -217,6 +370,7 @@ function normalizeConfig(raw) {
217
370
  input: clamp(ops.input, 0, 1, DEFAULT_CONFIG.opacities.input)
218
371
  },
219
372
  blurs,
373
+ strokes: normalizeStrokes(r.strokes),
220
374
  settingsOpacity: clamp(r.settingsOpacity, 0, 1, DEFAULT_CONFIG.settingsOpacity),
221
375
  wallpaperOpacity: clamp(r.wallpaperOpacity, 0, 1, DEFAULT_CONFIG.wallpaperOpacity),
222
376
  blur: clamp(r.blur, 0, 60, DEFAULT_CONFIG.blur),
@@ -225,11 +379,14 @@ function normalizeConfig(raw) {
225
379
  backgroundType: bgType,
226
380
  bgMode,
227
381
  videoMime: typeof r.videoMime === "string" ? r.videoMime : null,
382
+ fontMime: typeof r.fontMime === "string" ? r.fontMime : null,
383
+ fontEnabled: typeof r.fontEnabled === "boolean" ? r.fontEnabled : DEFAULT_CONFIG.fontEnabled,
228
384
  generatedBg,
229
385
  regenerateOnReload: typeof r.regenerateOnReload === "boolean" ? r.regenerateOnReload : DEFAULT_CONFIG.regenerateOnReload,
230
386
  chatTextOpacity: clamp(r.chatTextOpacity, 0, 1, DEFAULT_CONFIG.chatTextOpacity),
231
387
  trajectoryOpacity: clamp(r.trajectoryOpacity, 0, 1, DEFAULT_CONFIG.trajectoryOpacity),
232
388
  panelOpacity: clamp(r.panelOpacity, 0, 1, DEFAULT_CONFIG.panelOpacity),
389
+ producedOpacity: clamp(r.producedOpacity, 0, 1, DEFAULT_CONFIG.producedOpacity),
233
390
  profiles: normalizeProfiles(r.profiles),
234
391
  rotation: normalizeRotation(r.rotation),
235
392
  schedule: normalizeSchedule(r.schedule),
@@ -291,7 +448,8 @@ function normalizeProfileAppearance(raw) {
291
448
  "chat",
292
449
  "trajectory",
293
450
  "input",
294
- "panel"
451
+ "panel",
452
+ "produced"
295
453
  ]) blurs[k] = clamp(bl[k], 0, 60, DEFAULT_CONFIG.blurs[k]);
296
454
  return {
297
455
  color: Array.isArray(c) && c.length === 3 && c.every((x) => typeof x === "number" && isFinite(x)) ? [
@@ -306,12 +464,14 @@ function normalizeProfileAppearance(raw) {
306
464
  input: clamp(ops.input, 0, 1, DEFAULT_CONFIG.opacities.input)
307
465
  },
308
466
  blurs,
467
+ strokes: normalizeStrokes(a.strokes),
309
468
  settingsOpacity: clamp(a.settingsOpacity, 0, 1, DEFAULT_CONFIG.settingsOpacity),
310
469
  wallpaperOpacity: clamp(a.wallpaperOpacity, 0, 1, DEFAULT_CONFIG.wallpaperOpacity),
311
470
  blur: clamp(a.blur, 0, 60, DEFAULT_CONFIG.blur),
312
471
  chatTextOpacity: clamp(a.chatTextOpacity, 0, 1, DEFAULT_CONFIG.chatTextOpacity),
313
472
  trajectoryOpacity: clamp(a.trajectoryOpacity, 0, 1, DEFAULT_CONFIG.trajectoryOpacity),
314
- panelOpacity: clamp(a.panelOpacity, 0, 1, DEFAULT_CONFIG.panelOpacity)
473
+ panelOpacity: clamp(a.panelOpacity, 0, 1, DEFAULT_CONFIG.panelOpacity),
474
+ producedOpacity: clamp(a.producedOpacity, 0, 1, DEFAULT_CONFIG.producedOpacity)
315
475
  };
316
476
  }
317
477
  function normalizeProfiles(raw) {
@@ -385,10 +545,38 @@ async function readConfig() {
385
545
  return { ...DEFAULT_CONFIG };
386
546
  }
387
547
  }
548
+ const LEGACY_CONFIG_KEYS = /* @__PURE__ */ new Set(["opacity"]);
549
+ const warnedConfigKeys = /* @__PURE__ */ new Set();
550
+ function warnUnknownConfigKeys(raw, normalized) {
551
+ if (raw === null || typeof raw !== "object") return;
552
+ const r = raw;
553
+ const warn = (id) => {
554
+ if (warnedConfigKeys.has(id)) return;
555
+ warnedConfigKeys.add(id);
556
+ console.warn(`dsh-any-background: ignoring unknown config field "${id}" (declared in one half only?)`);
557
+ };
558
+ const known = new Set(Object.keys(normalized));
559
+ for (const key of Object.keys(r)) {
560
+ if (known.has(key) || LEGACY_CONFIG_KEYS.has(key)) continue;
561
+ warn(key);
562
+ }
563
+ for (const group of [
564
+ "blurs",
565
+ "opacities",
566
+ "strokes"
567
+ ]) {
568
+ const got = r[group];
569
+ if (got === null || typeof got !== "object") continue;
570
+ const have = new Set(Object.keys(normalized[group]));
571
+ for (const key of Object.keys(got)) if (!have.has(key)) warn(`${group}.${key}`);
572
+ }
573
+ }
388
574
  async function writeConfig(config) {
389
575
  await ensureDir();
390
576
  try {
391
- await writeFile(configPath(), JSON.stringify(normalizeConfig(config), null, 2), "utf8");
577
+ const normalized = normalizeConfig(config);
578
+ warnUnknownConfigKeys(config, normalized);
579
+ await writeFile(configPath(), JSON.stringify(normalized, null, 2), "utf8");
392
580
  return true;
393
581
  } catch (e) {
394
582
  console.error(`dsh-any-background: failed to write "${CONFIG_FILE}"`, e);
@@ -1093,6 +1281,164 @@ async function handleWallpaperUpload(req, res) {
1093
1281
  } catch {}
1094
1282
  }
1095
1283
  }
1284
+ /** Stream the stored custom font: sniffed MIME, no caching (uploads replace
1285
+ * the file in place). Fonts need no Range support — the browser fetches once. */
1286
+ async function serveFont(req, res) {
1287
+ if (req.method !== "GET" && req.method !== "HEAD") {
1288
+ res.writeHead(405, { "Content-Type": "application/json" });
1289
+ res.end(JSON.stringify({
1290
+ ok: false,
1291
+ error: "font route only serves GET/HEAD"
1292
+ }));
1293
+ return;
1294
+ }
1295
+ try {
1296
+ const found = await findFontFile();
1297
+ if (found === null) {
1298
+ res.writeHead(404);
1299
+ res.end("no custom font stored");
1300
+ return;
1301
+ }
1302
+ const st = await stat(found.path);
1303
+ res.writeHead(200, {
1304
+ "Content-Type": found.mime,
1305
+ "Content-Length": st.size,
1306
+ "Cache-Control": "no-store"
1307
+ });
1308
+ if (req.method === "HEAD") {
1309
+ res.end();
1310
+ return;
1311
+ }
1312
+ createReadStream(found.path).pipe(res);
1313
+ } catch (e) {
1314
+ console.error("dsh-any-background: failed to serve the custom font", e);
1315
+ try {
1316
+ res.writeHead(500);
1317
+ res.end();
1318
+ } catch {}
1319
+ }
1320
+ }
1321
+ /** Accept a raw font upload (POST): pipe the body into a temp file, sniff the
1322
+ * container format from its magic bytes (rejecting anything that is not a
1323
+ * recognizable font), then rename it into the format-derived slot and record
1324
+ * the MIME in the config so serve/find resolve immediately. */
1325
+ async function handleFontUpload(req, res) {
1326
+ if (req.method !== "POST") {
1327
+ res.writeHead(405);
1328
+ res.end();
1329
+ return;
1330
+ }
1331
+ try {
1332
+ await ensureDir();
1333
+ const tmp = dshHomePath(DATA_DIR, FONT_UPLOAD_TMP);
1334
+ const out = createWriteStream(tmp);
1335
+ let received = 0;
1336
+ let failed = false;
1337
+ const fail = () => {
1338
+ if (failed) return;
1339
+ failed = true;
1340
+ out.destroy();
1341
+ rm(tmp, { force: true });
1342
+ };
1343
+ req.on("aborted", fail);
1344
+ req.on("error", fail);
1345
+ out.on("error", () => {
1346
+ fail();
1347
+ try {
1348
+ res.writeHead(500);
1349
+ res.end();
1350
+ } catch {}
1351
+ });
1352
+ req.on("data", (chunk) => {
1353
+ received += chunk.byteLength;
1354
+ if (received > FONT_UPLOAD_MAX) {
1355
+ req.destroy();
1356
+ fail();
1357
+ }
1358
+ });
1359
+ req.pipe(out);
1360
+ out.on("finish", async () => {
1361
+ if (failed) return;
1362
+ try {
1363
+ if (received === 0) {
1364
+ fail();
1365
+ res.writeHead(400, { "Content-Type": "application/json" });
1366
+ res.end(JSON.stringify({
1367
+ ok: false,
1368
+ error: "empty upload"
1369
+ }));
1370
+ return;
1371
+ }
1372
+ const mime = sniffFontMime(await new Promise((resolve, reject) => {
1373
+ const chunks = [];
1374
+ const s = createReadStream(tmp, {
1375
+ start: 0,
1376
+ end: 15
1377
+ });
1378
+ s.on("data", (c) => chunks.push(c));
1379
+ s.on("end", () => resolve(Buffer.concat(chunks)));
1380
+ s.on("error", reject);
1381
+ }));
1382
+ if (mime === null) {
1383
+ fail();
1384
+ res.writeHead(415, { "Content-Type": "application/json" });
1385
+ res.end(JSON.stringify({
1386
+ ok: false,
1387
+ error: "not a font (expected ttf/otf/woff/woff2)"
1388
+ }));
1389
+ return;
1390
+ }
1391
+ const target = fontPathFor(mime);
1392
+ for (const name of FONT_CANDIDATES) {
1393
+ const p = dshHomePath(DATA_DIR, name);
1394
+ if (p !== target) await rm(p, { force: true });
1395
+ }
1396
+ await rm(target, { force: true });
1397
+ await rename(tmp, target);
1398
+ const cfg = await readConfig();
1399
+ if (cfg.fontMime !== mime) {
1400
+ cfg.fontMime = mime;
1401
+ await writeConfig(cfg);
1402
+ }
1403
+ res.writeHead(200, { "Content-Type": "application/json" });
1404
+ res.end(JSON.stringify({
1405
+ ok: true,
1406
+ fontUrl: FONT_ROUTE,
1407
+ mime
1408
+ }));
1409
+ } catch (e) {
1410
+ console.error("dsh-any-background: failed to finalize the font upload", e);
1411
+ rm(tmp, { force: true });
1412
+ try {
1413
+ res.writeHead(500);
1414
+ res.end();
1415
+ } catch {}
1416
+ }
1417
+ });
1418
+ } catch (e) {
1419
+ console.error("dsh-any-background: failed to accept the font upload", e);
1420
+ try {
1421
+ res.writeHead(500);
1422
+ res.end();
1423
+ } catch {}
1424
+ }
1425
+ }
1426
+ /** Remove every stored font variant and clear the recorded MIME. */
1427
+ async function removeFontFile() {
1428
+ try {
1429
+ for (const name of FONT_CANDIDATES) await rm(dshHomePath(DATA_DIR, name), { force: true });
1430
+ await rm(dshHomePath(DATA_DIR, FONT_UPLOAD_TMP), { force: true });
1431
+ const cfg = await readConfig();
1432
+ if (cfg.fontMime !== null) {
1433
+ cfg.fontMime = null;
1434
+ await writeConfig(cfg);
1435
+ }
1436
+ return true;
1437
+ } catch (e) {
1438
+ console.error("dsh-any-background: failed to remove the custom font", e);
1439
+ return false;
1440
+ }
1441
+ }
1096
1442
  const NS = "dshAnyBackground";
1097
1443
  const RPC_CHANNEL = "/dsh-any-background";
1098
1444
  const RPC_BODY_MAX = 314572800;
@@ -1147,13 +1493,18 @@ async function handleRpcMethod(endpoint, payload) {
1147
1493
  switch (method) {
1148
1494
  case "read": {
1149
1495
  const rotated = await advanceRotationIfDue();
1496
+ const config = await readConfig();
1497
+ const firstRun = !await exists(configPath());
1498
+ if (firstRun) await writeConfig(config);
1150
1499
  return {
1151
1500
  ok: true,
1152
1501
  value: {
1153
- config: await readConfig(),
1502
+ config,
1154
1503
  wallpaperUrl: await wallpaperServeUrl(),
1155
1504
  videoUrl: await videoUrl(),
1156
- rotated
1505
+ fontUrl: await fontUrl(),
1506
+ rotated,
1507
+ firstRun
1157
1508
  }
1158
1509
  };
1159
1510
  }
@@ -1189,6 +1540,10 @@ async function handleRpcMethod(endpoint, payload) {
1189
1540
  ok: true,
1190
1541
  value: await handleRotationSet(payload)
1191
1542
  };
1543
+ case "removeFont": return {
1544
+ ok: true,
1545
+ value: await removeFontFile()
1546
+ };
1192
1547
  default: return {
1193
1548
  ok: false,
1194
1549
  error: {
@@ -1334,7 +1689,17 @@ function apply(ctx) {
1334
1689
  path: WALLPAPER_UPLOAD_ROUTE,
1335
1690
  handler: fenceUpload(handleWallpaperUpload)
1336
1691
  }), "dsh-any-background: wallpaper upload route");
1692
+ webCtx.effect(() => webCtx.webServer.register({
1693
+ kind: "prefix",
1694
+ path: FONT_ROUTE,
1695
+ handler: serveFont
1696
+ }), "dsh-any-background: font route");
1697
+ webCtx.effect(() => webCtx.webServer.register({
1698
+ kind: "exact",
1699
+ path: FONT_UPLOAD_ROUTE,
1700
+ handler: fenceUpload(handleFontUpload)
1701
+ }), "dsh-any-background: font upload route");
1337
1702
  });
1338
1703
  }
1339
1704
  //#endregion
1340
- export { apply, inject, name };
1705
+ export { DEFAULT_CONFIG, apply, inject, name };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-any-background",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
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",
@@ -103,6 +103,7 @@
103
103
  },
104
104
  "dsh": {
105
105
  "compatibility": {
106
+ "dsh": "0.1.5-rc.2",
106
107
  "dshReleases": {
107
108
  "0.1.5-rc.2": "compatible"
108
109
  }