@ninkch/dsh-artcraft 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -280,6 +280,11 @@ The three places that must agree on the package name are the profile `package.js
280
280
  GitHub: this repository is self-contained; `README.md` is English-first with a
281
281
  [简体中文](README.zh-CN.md) companion.
282
282
 
283
+ **Releases run through npm Trusted Publishing** (GitHub Actions OIDC — no tokens, no OTP):
284
+ `.github/workflows/publish.yml` fires on `v*` tags; the trusted publisher on the npm package
285
+ points at `NINKCH/dsh-artcraft` / `publish.yml`. To cut a release: bump `version`, commit,
286
+ `git tag v0.x.y && git push origin v0.x.y`.
287
+
283
288
  ## License
284
289
 
285
290
  **This plugin (`@ninkch/dsh-artcraft`): Apache-2.0** — see [LICENSE](LICENSE). It ships none of the
package/README.zh-CN.md CHANGED
@@ -263,6 +263,10 @@ HTTP 面无鉴权,挂在 Harness Web 服务之下(仅回环监听)——
263
263
  GitHub 上传本仓库即可:`README.md` 英文优先,附
264
264
  [简体中文](README.zh-CN.md) 对照。
265
265
 
266
+ **发版走 npm Trusted Publishing**(GitHub Actions OIDC —— 无令牌、无 OTP):
267
+ `.github/workflows/publish.yml` 在 `v*` 标签推送时触发;npm 包侧的可信发布指向
268
+ `NINKCH/dsh-artcraft` / `publish.yml`。发版流程:改 `version` → 提交 → `git tag v0.x.y && git push origin v0.x.y`。
269
+
266
270
  ## 许可
267
271
 
268
272
  **本插件(`@ninkch/dsh-artcraft`):Apache-2.0**(见 [LICENSE](LICENSE))。不包含 ArtCraft 应用的任何代码 —— 只调用其公开自动化面
package/client.js ADDED
@@ -0,0 +1,181 @@
1
+ // @ninkch/dsh-artcraft — client half.
2
+ // 侧栏 "ArtCraft" 按钮 + 面板:工具链状态 / 自更新(检查 + 一键应用)。
3
+ window.__ModuleLoader__.load({
4
+ id: '@ninkch/dsh-artcraft',
5
+ factory(require) {
6
+ const React = require('react');
7
+ const h = React.createElement;
8
+ const { useState, useEffect, useCallback, useRef } = React;
9
+
10
+ const NS = 'artcraft';
11
+ const PANEL_ID = 'artcraft';
12
+ const API = '/artcraft/api';
13
+
14
+ const CSS = `
15
+ .dshac-panel { padding: 14px; font-size: 13px; line-height: 1.5; overflow: auto; height: 100%; box-sizing: border-box; }
16
+ .dshac-h { font-size: 14px; font-weight: 600; margin: 0 0 10px; display: flex; align-items: center; gap: 8px; }
17
+ .dshac-card { border: 1px solid var(--dsw-border-primary, rgba(128,128,128,.35)); border-radius: 10px; padding: 12px; margin-bottom: 12px; }
18
+ .dshac-title { font-weight: 600; margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center; }
19
+ .dshac-row { display: flex; align-items: center; gap: 8px; margin: 4px 0; }
20
+ .dshac-dot { width: 9px; height: 9px; border-radius: 50%; flex: none; }
21
+ .dshac-dot.ok { background: #34c759; } .dshac-dot.bad { background: #ff5f57; } .dshac-dot.unknown { background: #8e8e93; }
22
+ .dshac-mono { font-family: ui-monospace, Consolas, monospace; font-size: 12px; word-break: break-all; }
23
+ .dshac-muted { opacity: .65; font-size: 12px; }
24
+ .dshac-btn { border: 1px solid var(--dsw-border-primary, rgba(128,128,128,.4)); background: transparent; color: inherit;
25
+ border-radius: 7px; padding: 4px 12px; font-size: 12px; cursor: pointer; }
26
+ .dshac-btn:hover:not(:disabled) { background: rgba(128,128,128,.15); }
27
+ .dshac-btn:disabled { opacity: .5; cursor: default; }
28
+ .dshac-btn.primary { background: rgba(80,120,255,.25); }
29
+ .dshac-out { white-space: pre-wrap; font-family: ui-monospace, Consolas, monospace; font-size: 11px;
30
+ background: rgba(128,128,128,.12); border-radius: 7px; padding: 8px; margin-top: 8px; max-height: 180px; overflow: auto; }
31
+ .dshac-note { margin-top: 8px; color: var(--dsw-alias-state-warning-primary, #d69e2e); font-size: 12px; }
32
+ .dshac-ok { color: var(--dsw-alias-state-success-primary, #34c759); }
33
+ `;
34
+
35
+ async function api(path, opts) {
36
+ const ctrl = typeof AbortController !== 'undefined' ? new AbortController() : null;
37
+ const timer = ctrl ? setTimeout(() => ctrl.abort(), 30000) : null;
38
+ try {
39
+ const res = await fetch(API + path, { ...(opts || {}), signal: ctrl ? ctrl.signal : undefined });
40
+ const j = await res.json().catch(() => ({}));
41
+ if (!res.ok) throw new Error(j.error || ('HTTP ' + res.status));
42
+ return j;
43
+ } finally { if (timer) clearTimeout(timer); }
44
+ }
45
+
46
+ function Dot({ ok }) {
47
+ return h('span', { className: 'dshac-dot ' + (ok === true ? 'ok' : ok === false ? 'bad' : 'unknown') });
48
+ }
49
+
50
+ function PanelIcon() {
51
+ return h('svg', { width: 18, height: 18, viewBox: '0 0 20 20', fill: 'none' },
52
+ h('rect', { x: 3, y: 3, width: 14, height: 14, rx: 3, stroke: 'currentColor', 'stroke-width': 1.5 }),
53
+ h('path', { d: 'M6.5 10h4M8.5 8v4', stroke: 'currentColor', 'stroke-width': 1.5, 'stroke-linecap': 'round' }),
54
+ h('circle', { cx: 13.5, cy: 13, r: 1.6, fill: 'currentColor' }),
55
+ );
56
+ }
57
+
58
+ function ArtCraftPage() {
59
+ const [status, setStatus] = useState(null);
60
+ const [statusErr, setStatusErr] = useState(null);
61
+ const [upd, setUpd] = useState(null);
62
+ const [busy, setBusy] = useState('');
63
+ const [out, setOut] = useState('');
64
+ const [needRestart, setNeedRestart] = useState(false);
65
+ const alive = useRef(true);
66
+ useEffect(() => () => { alive.current = false; }, []);
67
+
68
+ const loadStatus = useCallback(async () => {
69
+ try {
70
+ const s = await api('/status');
71
+ if (!alive.current) return;
72
+ setStatus(s); setStatusErr(null);
73
+ } catch (e) {
74
+ if (!alive.current) return;
75
+ setStatusErr(String(e.message || e));
76
+ }
77
+ }, []);
78
+ const checkUpdate = useCallback(async () => {
79
+ setBusy('check'); setOut('');
80
+ try {
81
+ const u = await api('/update/check');
82
+ if (!alive.current) return;
83
+ setUpd(u);
84
+ setOut(u.updateAvailable
85
+ ? `有新版本:${u.latest}(当前 ${u.current}),安装方式:${u.mode === 'git' ? 'git 仓库' : 'npm 安装'}`
86
+ : `已是最新${u.latest ? '(npm latest ' + u.latest + ')' : ''}${u.note ? ' · ' + u.note : ''}`);
87
+ } catch (e) { if (alive.current) setOut('检查失败:' + String(e.message || e)); }
88
+ finally { if (alive.current) setBusy(''); }
89
+ }, []);
90
+ const applyUpdate = useCallback(async () => {
91
+ setBusy('apply'); setOut('');
92
+ try {
93
+ const r = await api('/update/apply', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' });
94
+ if (!alive.current) return;
95
+ setOut((r.output || String(r.message || '')) + (r.needRestart ? '\n\n✅ 更新完成 —— 请重启 DeepSeek Harness 后生效。' : ''));
96
+ if (r.needRestart) setNeedRestart(true);
97
+ } catch (e) { if (alive.current) setOut('更新失败:' + String(e.message || e)); }
98
+ finally { if (alive.current) setBusy(''); }
99
+ }, []);
100
+
101
+ useEffect(() => { loadStatus(); checkUpdate(); }, [loadStatus, checkUpdate]);
102
+
103
+ const bins = status?.bins ?? {};
104
+ const binRow = (key, label) => {
105
+ const b = bins[key];
106
+ return h('div', { className: 'dshac-row' },
107
+ h(Dot, { ok: b ? b.found : null }),
108
+ h('span', null, label),
109
+ b?.found ? null : h('span', { className: 'dshac-muted' }, b?.note || '未配置'),
110
+ );
111
+ };
112
+
113
+ return h('div', { className: 'dshac-panel' },
114
+ h('div', { className: 'dshac-h' }, 'ArtCraft Bridge',
115
+ status ? h('span', { className: 'dshac-muted' }, 'v' + status.version) : null),
116
+
117
+ statusErr
118
+ ? h('div', { className: 'dshac-card' },
119
+ h('div', { className: 'dshac-title' }, '未连接'),
120
+ h('div', { className: 'dshac-muted' }, statusErr + ' —— 插件可能未注册或 Harness 需要重启。'))
121
+ : h('div', { className: 'dshac-card' },
122
+ h('div', { className: 'dshac-title' }, '工具链',
123
+ h('button', { className: 'dshac-btn', onClick: loadStatus, disabled: busy !== '' }, '刷新')),
124
+ binRow('photocraft', 'PhotoCraft (CLI)'),
125
+ binRow('filmcraft', 'FilmCraft (CLI)'),
126
+ binRow('effectcraft', 'EffectCraft (CLI)'),
127
+ status?.dataRoot ? h('div', { className: 'dshac-row dshac-muted' },
128
+ '数据目录:', h('span', { className: 'dshac-mono' }, status.dataRoot.path)) : null),
129
+
130
+ h('div', { className: 'dshac-card' },
131
+ h('div', { className: 'dshac-title' }, '插件更新',
132
+ h('button', { className: 'dshac-btn primary', onClick: checkUpdate, disabled: busy !== '' },
133
+ busy === 'check' ? '检查中…' : '检查更新')),
134
+ upd
135
+ ? h('div', { className: 'dshac-row' },
136
+ h('span', null, '当前 v' + (upd.current ?? '?')),
137
+ h('span', { className: 'dshac-muted' }, '→'),
138
+ h('span', { className: upd.updateAvailable ? 'dshac-ok' : 'dshac-muted' },
139
+ 'npm latest v' + (upd.latest ?? '—')),
140
+ upd.mode ? h('span', { className: 'dshac-muted' },
141
+ '(' + (upd.mode === 'git' ? 'git 仓库 · ' + (upd.branch || '?') + (upd.gitClean === false ? ' · 有本地改动' : ' · 工作区干净') : 'npm 安装') + ')') : null)
142
+ : h('div', { className: 'dshac-muted' }, '尚未检查'),
143
+ h('div', { style: { marginTop: '8px' } },
144
+ h('button', {
145
+ className: 'dshac-btn primary',
146
+ onClick: applyUpdate,
147
+ disabled: busy !== '' || !upd?.updateAvailable,
148
+ title: upd?.updateAvailable ? '' : '先检查更新,有新版本才可应用',
149
+ }, busy === 'apply' ? '更新中…' : '一键更新')),
150
+ needRestart ? h('div', { className: 'dshac-note' }, '更新已写入 —— 重启 DeepSeek Harness 后生效。') : null,
151
+ out ? h('div', { className: 'dshac-out' }, out) : null),
152
+ );
153
+ }
154
+
155
+ return {
156
+ inject: ['slots', 'locale'],
157
+ apply(ctx) {
158
+ ctx.effect(() => ctx.locale.register(NS, { zh: { panel: 'ArtCraft' }, en: { panel: 'ArtCraft' } }), 'artcraft: dictionaries');
159
+ ctx.effect(() => {
160
+ const el = document.createElement('style');
161
+ el.id = 'dshac-styles';
162
+ el.textContent = CSS;
163
+ document.head.appendChild(el);
164
+ return () => el.remove();
165
+ }, 'artcraft: styles');
166
+ ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({
167
+ name: 'sidebar.panellist',
168
+ id: PANEL_ID,
169
+ order: 101,
170
+ label: () => 'ArtCraft',
171
+ locale: NS,
172
+ }, PanelIcon));
173
+ ctx.slots.inject('main', () => ctx.slots.register({
174
+ name: 'main',
175
+ key: PANEL_ID,
176
+ locale: NS,
177
+ }, ArtCraftPage));
178
+ },
179
+ };
180
+ },
181
+ });
package/index.js CHANGED
@@ -39,10 +39,11 @@ import crypto from 'node:crypto';
39
39
  import { fileURLToPath } from 'node:url';
40
40
 
41
41
  const NAME = 'artcraft';
42
- const VERSION = '0.1.1';
42
+ const VERSION = '0.2.0';
43
43
  const HERE = path.dirname(fileURLToPath(import.meta.url));
44
44
 
45
45
  const BINS = ['photocraft', 'filmcraft', 'effectcraft'];
46
+ const PKG_NAME = '@ninkch/dsh-artcraft';
46
47
  const DEFAULT_PORTS = { filmcraft: 9876, effectcraft: 9877 };
47
48
 
48
49
  const BODY_CAP = 2 * 1024 * 1024; // 普通 JSON 请求体上限
@@ -152,14 +153,14 @@ function pushTail(arr, chunk) {
152
153
  }
153
154
 
154
155
  /** 运行子进程到结束;返回 {ok, code, signal, stdout, stderr, ms} */
155
- function runProcess(exe, args, { cwd, timeoutMs = 15 * 60_000 } = {}) {
156
+ function runProcess(exe, args, { cwd, timeoutMs = 15 * 60_000, shell = false } = {}) {
156
157
  return new Promise((resolve) => {
157
158
  const started = Date.now();
158
159
  const stdout = [];
159
160
  const stderr = [];
160
161
  let child;
161
162
  try {
162
- child = spawn(String(exe), (args ?? []).map(String), { cwd, windowsHide: true });
163
+ child = spawn(String(exe), (args ?? []).map(String), { cwd, windowsHide: true, shell });
163
164
  } catch (e) {
164
165
  resolve({ ok: false, code: -1, signal: null, stdout: '', stderr: String(e?.message ?? e), ms: 0 });
165
166
  return;
@@ -442,6 +443,131 @@ function makeRoute(path, handler) {
442
443
  };
443
444
  }
444
445
 
446
+ /* ============================ 自更新(检查 / 应用) ============================ */
447
+
448
+ function semverGt(a, b) {
449
+ const p = (s) => String(s ?? '').trim().replace(/^v/, '').split('.').map((n) => parseInt(n, 10) || 0);
450
+ const [a1, a2, a3] = p(a), [b1, b2, b3] = p(b);
451
+ return a1 !== b1 ? a1 > b1 : a2 !== b2 ? a2 > b2 : a3 > b3;
452
+ }
453
+
454
+ /** 探测本插件的安装形态:git 仓库(link 开发)还是 npm 安装 */
455
+ async function detectInstall() {
456
+ const here = path.dirname(fileURLToPath(import.meta.url));
457
+ const real = await fs.realpath(here);
458
+ let dir = real;
459
+ let repoDir = null;
460
+ while (true) {
461
+ try {
462
+ const st = await fs.stat(path.join(dir, '.git'));
463
+ if (st.isDirectory() || st.isFile()) { repoDir = dir; break; }
464
+ } catch { /* 继续 */ }
465
+ const parent = path.dirname(dir);
466
+ if (parent === dir) break;
467
+ dir = parent;
468
+ }
469
+ let profileDir = null;
470
+ if (!repoDir) {
471
+ dir = path.dirname(here);
472
+ while (true) {
473
+ try {
474
+ const txt = await fs.readFile(path.join(dir, 'package.json'), 'utf8');
475
+ if (txt.includes('"dsh"')) { profileDir = dir; break; }
476
+ } catch { /* 继续 */ }
477
+ const parent = path.dirname(dir);
478
+ if (parent === dir) break;
479
+ dir = parent;
480
+ }
481
+ }
482
+ let gitClean = null;
483
+ let branch = null;
484
+ if (repoDir) {
485
+ const st = await runProcess('git', ['status', '--porcelain'], { cwd: repoDir, timeoutMs: 20000 });
486
+ gitClean = st.ok && st.stdout.trim() === '';
487
+ const b = await runProcess('git', ['branch', '--show-current'], { cwd: repoDir, timeoutMs: 20000 });
488
+ branch = (b.stdout || '').trim() || null;
489
+ }
490
+ return { mode: repoDir ? 'git' : 'npm', here, real, repoDir, profileDir, gitClean, branch };
491
+ }
492
+
493
+ async function fetchLatestVersion() {
494
+ const url = `https://registry.npmjs.org/${PKG_NAME.replace('/', '%2f')}/latest`;
495
+ try {
496
+ const res = await fetch(url, { signal: AbortSignal.timeout(12000) });
497
+ if (res.status === 404) return { latest: null, note: 'npm 上还没有这个包' };
498
+ if (!res.ok) throw new Error('registry 返回 ' + res.status);
499
+ const j = await res.json();
500
+ return { latest: j.version ?? null };
501
+ } catch (e) {
502
+ return { latest: null, note: '查询 npm 失败: ' + String(e?.cause?.message || e?.message || e) };
503
+ }
504
+ }
505
+
506
+ async function updateCheck() {
507
+ const current = VERSION;
508
+ const { latest, note } = await fetchLatestVersion();
509
+ const info = await detectInstall();
510
+ return {
511
+ current,
512
+ latest,
513
+ updateAvailable: !!(latest && semverGt(latest, current)),
514
+ mode: info.mode,
515
+ repoDir: info.repoDir,
516
+ gitClean: info.gitClean,
517
+ branch: info.branch,
518
+ note,
519
+ };
520
+ }
521
+
522
+ async function findRuntimeTools() {
523
+ const isWin = process.platform === 'win32';
524
+ const pnCmd = isWin ? 'pnpm.cmd' : 'pnpm';
525
+ const probe = await runProcess(pnCmd, ['--version'], { timeoutMs: 20000 });
526
+ if (probe.ok) return { pnpmCmd: pnCmd };
527
+ try {
528
+ const rtRoot = path.join(os.homedir(), '.dsh', 'dsh-runtimes');
529
+ const entries = await fs.readdir(rtRoot, { withFileTypes: true });
530
+ for (const e of entries) {
531
+ if (!e.isDirectory()) continue;
532
+ const pnpmMjs = path.join(rtRoot, e.name, 'dependencies', 'pnpm', 'bin', 'pnpm.mjs');
533
+ const nodeExe = path.join(rtRoot, e.name, 'dependencies', 'node', 'bin', isWin ? 'node.exe' : 'node');
534
+ if ((await fileExists(pnpmMjs)) && (await fileExists(nodeExe))) {
535
+ return { nodeExe, pnpmMjs };
536
+ }
537
+ }
538
+ } catch { /* ignore */ }
539
+ return {};
540
+ }
541
+
542
+ async function updateApply() {
543
+ const info = await detectInstall();
544
+ const { latest } = await fetchLatestVersion();
545
+ if (!latest) throw new Error('npm 上没有可用的发布版本');
546
+ if (!semverGt(latest, VERSION)) return { ok: true, message: `已是最新(${VERSION})`, needRestart: false };
547
+
548
+ if (info.mode === 'git') {
549
+ if (info.gitClean === false) {
550
+ throw new Error('插件目录有未提交的本地改动,为避免覆盖请先 git commit / stash,再执行更新');
551
+ }
552
+ const r = await runProcess('git', ['pull', '--ff-only'], { cwd: info.repoDir, timeoutMs: 120000 });
553
+ return { ok: r.ok, output: (r.stdout + '\n' + r.stderr).trim(), needRestart: r.ok, latest };
554
+ }
555
+
556
+ // npm 安装模式
557
+ if (!info.profileDir) throw new Error('未定位到 Harness profile 目录,无法自动更新');
558
+ const tools = await findRuntimeTools();
559
+ let cmd, args;
560
+ if (tools.pnpmCmd) {
561
+ cmd = tools.pnpmCmd; args = ['add', `${PKG_NAME}@latest`, '--dir', info.profileDir];
562
+ } else if (tools.pnpmMjs) {
563
+ cmd = tools.nodeExe; args = [tools.pnpmMjs, 'add', `${PKG_NAME}@latest`, '--dir', info.profileDir];
564
+ } else {
565
+ throw new Error('未找到 pnpm(PATH 与 ~/.dsh/dsh-runtimes 均未命中),请手动在 profile 目录执行 pnpm add');
566
+ }
567
+ const r = await runProcess(cmd, args, { cwd: info.profileDir, timeoutMs: 300000, shell: !!tools.pnpmCmd });
568
+ return { ok: r.ok, output: (r.stdout + '\n' + r.stderr).trim(), needRestart: r.ok, latest };
569
+ }
570
+
445
571
  /* ============================ 插件入口 ============================ */
446
572
 
447
573
  const toolNames = [];
@@ -584,6 +710,23 @@ export function apply(ctx) {
584
710
  },
585
711
  });
586
712
 
713
+ attempt({
714
+ name: 'artcraft_update',
715
+ description: '检查/应用 @ninkch/dsh-artcraft 插件自身的更新:action=check 对比本地与 npm latest;action=apply 执行更新(git pull 或 pnpm add,按安装形态自动选择),完成后需重启 Harness 生效。',
716
+ schema: {
717
+ type: 'object',
718
+ properties: {
719
+ action: { type: 'string', enum: ['check', 'apply'] },
720
+ },
721
+ required: ['action'],
722
+ additionalProperties: false,
723
+ },
724
+ async execute(args) {
725
+ if (String(args.action) === 'apply') return updateApply();
726
+ return updateCheck();
727
+ },
728
+ });
729
+
587
730
  return () => {
588
731
  for (const d of disposers) { try { d(); } catch { /* ignore */ } }
589
732
  };
@@ -739,6 +882,15 @@ export function apply(ctx) {
739
882
  }
740
883
  }),
741
884
 
885
+ makeRoute(`/${NAME}/api/update/check`, async (req, res) => {
886
+ sendJson(res, 200, await updateCheck());
887
+ }),
888
+
889
+ makeRoute(`/${NAME}/api/update/apply`, async (req, res) => {
890
+ if (req.method !== 'POST') throw new Error('仅支持 POST');
891
+ sendJson(res, 200, await updateApply());
892
+ }),
893
+
742
894
  makeRoute(`/${NAME}/api/control`, async (req, res) => {
743
895
  if (req.method !== 'POST') throw new Error('仅支持 POST');
744
896
  const cfg = await loadConfig();
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@ninkch/dsh-artcraft",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
- "description": "Drive the open-source ArtCraft studio apps (PhotoCraft / FilmCraft / EffectCraft) from DeepSeek Harness — agent tools + a same-origin HTTP API for headless CLI runs and the JSON control channel.",
5
+ "description": "Drive the open-source ArtCraft studio apps (PhotoCraft / FilmCraft / EffectCraft) from DeepSeek Harness — agent tools, a same-origin HTTP API, and a sidebar panel with toolchain status and self-update.",
6
6
  "keywords": [
7
7
  "deepseek-harness",
8
8
  "dsh-plugin",
@@ -14,15 +14,25 @@
14
14
  "ai-agents"
15
15
  ],
16
16
  "license": "Apache-2.0",
17
+ "author": "NINKCH",
18
+ "publishConfig": {
19
+ "registry": "https://registry.npmjs.org/"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/NINKCH/dsh-artcraft.git"
24
+ },
17
25
  "engines": {
18
26
  "node": ">=18"
19
27
  },
20
28
  "exports": {
21
29
  ".": "./index.js",
30
+ "./client": "./client.js",
22
31
  "./package.json": "./package.json"
23
32
  },
24
33
  "files": [
25
34
  "index.js",
35
+ "client.js",
26
36
  "cordis.patch.yml",
27
37
  "artcraft.config.json.example",
28
38
  "README.md",
@@ -32,6 +42,10 @@
32
42
  "dsh": {
33
43
  "bundle": {
34
44
  "patch": "./cordis.patch.yml"
45
+ },
46
+ "client": {
47
+ "platform": "web",
48
+ "immediately": true
35
49
  }
36
50
  }
37
51
  }