dsh-composer-plus 0.9.0 → 1.0.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/README.md +7 -3
- package/client.js +135 -28
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ dsh plugin --profile web add dsh-composer-plus
|
|
|
21
21
|
|
|
22
22
|
Then restart dsh. (The CLI can't install into the desktop profile; the app manages it.)
|
|
23
23
|
|
|
24
|
-
> dsh's pnpm skips versions younger than 24 hours. Right after a release, add the version: `dsh-composer-plus@0.
|
|
24
|
+
> dsh's pnpm skips versions younger than 24 hours. Right after a release, add the version: `dsh-composer-plus@1.0.1`.
|
|
25
25
|
|
|
26
26
|
## Features
|
|
27
27
|
|
|
@@ -35,11 +35,15 @@ Then restart dsh. (The CLI can't install into the desktop profile; the app manag
|
|
|
35
35
|
| **Copy deeplink** | Right-click a chat in the sidebar → **Copy deeplink**. Paste the `dsh-session:…` link into another chat to attach that chat as read-only context. |
|
|
36
36
|
| **Allow for this chat** | When Auto Review asks you, the approval card gets **Allow for this chat**: the same tool in the same chat is allowed from then on, with any arguments. A header chip lists the grants and revokes them. Grants survive a restart. |
|
|
37
37
|
| **Project status** | A project row in the sidebar shows the same spinner / waiting / done marker as its chats, so a collapsed project still tells you a chat inside is working or needs you. Hover it for details. |
|
|
38
|
-
| **Active projects first** | When a chat starts running or needs you, its project moves to the top of the sidebar with dsh's own row animation (the same move as dragging it there). Finished projects stay where they are; drag them anywhere as usual. |
|
|
38
|
+
| **Active projects first** | When a chat starts running or needs you, its project moves to the top of the sidebar with dsh's own row animation (the same move as dragging it there). Finished projects stay where they are; drag them anywhere as usual. Moves are saved to dsh's project order, so turning this off keeps the current order. |
|
|
39
39
|
| **Background notifications** | While dsh is in the background you get a notification when a turn finishes (first line of the reply), or when a chat needs an approval (tool name), an answer (the question) or a plan review. The Dock/taskbar badge counts waiting chats. Clicking opens that chat. |
|
|
40
40
|
|
|
41
41
|
All text — buttons, tooltips, notices, notifications and the Continue message — follows the language selected in dsh (English or Chinese, the two languages dsh ships). Switching the language in dsh settings updates it live.
|
|
42
42
|
|
|
43
|
+
## Settings
|
|
44
|
+
|
|
45
|
+
Every feature can be switched off on its own: dsh **Settings → Composer Plus**. Each row has a switch and a short description; changes apply immediately, without a restart, and are remembered on that device. Everything is on by default.
|
|
46
|
+
|
|
43
47
|
## Keyboard shortcuts
|
|
44
48
|
|
|
45
49
|
| Key | Where | Action |
|
|
@@ -65,7 +69,7 @@ Tested on dsh `0.2.0-rc.2` (desktop and web) and `0.2.1-alpha.2` (web). CI runs
|
|
|
65
69
|
|
|
66
70
|
- **No notifications on macOS:** System Settings → Notifications → DeepSeek Harness → allow. Choose **Alerts** to keep them on screen.
|
|
67
71
|
- **▶, queue grips, project status, Allow for this chat or right-click missing after a dsh update:** these attach to dsh's current HTML because dsh has no slot for them yet. If dsh changes that markup they hide themselves; nothing else breaks. Please open an [issue](https://github.com/furkancak1r/dsh-composer-plus/issues).
|
|
68
|
-
- **Install fails right after a release:** add the version (`dsh-composer-plus@0.
|
|
72
|
+
- **Install fails right after a release:** add the version (`dsh-composer-plus@1.0.1`), see the install note above.
|
|
69
73
|
|
|
70
74
|
## Limits
|
|
71
75
|
|
package/client.js
CHANGED
|
@@ -799,10 +799,49 @@ window.__ModuleLoader__.load({
|
|
|
799
799
|
return React.createElement('section', { 'data-composer-plus-changes': '', style: { height: '100%', overflowY: 'auto', fontSize: 13 } }, header, body);
|
|
800
800
|
}
|
|
801
801
|
|
|
802
|
+
/** Feature switches, in Settings order. Every feature is on until switched off. */
|
|
803
|
+
const FEATURES = ['changesPanel', 'projectStatus', 'activeProjectsFirst', 'notifications', 'allowForChat', 'continueButton',
|
|
804
|
+
'inputHistory', 'planToggle', 'queueReorder', 'copyDeeplink'];
|
|
805
|
+
const TOGGLES_KEY = 'dsh-composer-plus.features';
|
|
806
|
+
/** `{ [feature]: false }` for switched-off features, kept in the app's own browser storage (per device). */
|
|
807
|
+
function createToggles() {
|
|
808
|
+
let state = {};
|
|
809
|
+
try { const raw = localStorage.getItem(TOGGLES_KEY); if (raw !== null) state = JSON.parse(raw) ?? {}; } catch { state = {}; }
|
|
810
|
+
const listeners = new Set();
|
|
811
|
+
return {
|
|
812
|
+
getSnapshot: () => state,
|
|
813
|
+
subscribe: (fn) => { listeners.add(fn); return () => { listeners.delete(fn); }; },
|
|
814
|
+
update: (mutate) => {
|
|
815
|
+
const next = { ...state }; mutate(next);
|
|
816
|
+
for (const key of Object.keys(next)) if (next[key] !== false) delete next[key];
|
|
817
|
+
state = next;
|
|
818
|
+
try { localStorage.setItem(TOGGLES_KEY, JSON.stringify(state)); } catch { /* storage full or blocked: keep in memory */ }
|
|
819
|
+
for (const fn of [...listeners]) fn();
|
|
820
|
+
},
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
/** Settings → Composer Plus: one switch per feature, styled like dsh's own preference rows. */
|
|
824
|
+
function FeatureSettings({ useComposerPlusToggles, setFeature, t }) {
|
|
825
|
+
const enabled = useComposerPlusToggles((state) => state);
|
|
826
|
+
const row = { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 24, padding: '16px 0',
|
|
827
|
+
borderBottom: '.5px solid var(--dsw-alias-border-l2)' };
|
|
828
|
+
return React.createElement('div', { 'data-composer-plus-settings': '' },
|
|
829
|
+
React.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: 12, lineHeight: '18px', paddingBottom: 4 } }, t('settings.intro')),
|
|
830
|
+
...FEATURES.map((id) => React.createElement('div', { key: id, style: row },
|
|
831
|
+
React.createElement('div', null,
|
|
832
|
+
React.createElement('div', { style: { fontSize: 14, lineHeight: '20px' } }, t(`feature.${id}`)),
|
|
833
|
+
React.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary)', marginTop: 4, fontSize: 12, lineHeight: '18px' } }, t(`feature.${id}.description`))),
|
|
834
|
+
React.createElement(primitives.Switch, { checked: enabled[id] !== false, label: t(`feature.${id}`), onChange: (on) => setFeature(id, on) }))));
|
|
835
|
+
}
|
|
836
|
+
|
|
802
837
|
return {
|
|
803
838
|
inject: ['slots', 'sessions', 'locale', 'remote', 'remote.commands'],
|
|
804
839
|
apply(ctx) {
|
|
805
|
-
|
|
840
|
+
// Each feature installs through `feature(id, setup)`: setup returns its disposers, and a switch in
|
|
841
|
+
// Settings → Composer Plus removes or reinstalls just that feature, live.
|
|
842
|
+
const features = [];
|
|
843
|
+
const feature = (id, setup) => { features.push({ id, setup, off: null }); };
|
|
844
|
+
feature('queueReorder', () => [ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
|
|
806
845
|
name: 'conversation.input.dock',
|
|
807
846
|
id: 'queue-reorder',
|
|
808
847
|
order: 21,
|
|
@@ -815,8 +854,8 @@ window.__ModuleLoader__.load({
|
|
|
815
854
|
notify: (level, text) => { try { conversation.input.for(actx).notify(level, text); } catch { /* notice is best-effort */ } },
|
|
816
855
|
};
|
|
817
856
|
},
|
|
818
|
-
}, QueueReorder));
|
|
819
|
-
ctx.slots.inject('conversation.input.right', () => ctx.slots.register({
|
|
857
|
+
}, QueueReorder))]);
|
|
858
|
+
feature('planToggle', () => [ctx.slots.inject('conversation.input.right', () => ctx.slots.register({
|
|
820
859
|
name: 'conversation.input.right',
|
|
821
860
|
id: 'composer-plan-toggle',
|
|
822
861
|
order: 999,
|
|
@@ -832,15 +871,37 @@ window.__ModuleLoader__.load({
|
|
|
832
871
|
notify: (level, text) => { try { actx.get('conversation').input.for(actx).notify(level, text); } catch { /* notice is best-effort */ } },
|
|
833
872
|
};
|
|
834
873
|
},
|
|
835
|
-
}, PlanToggle));
|
|
836
|
-
ctx.slots.inject('conversation.input.right', () => ctx.slots.register({
|
|
874
|
+
}, PlanToggle))]);
|
|
875
|
+
feature('continueButton', () => [ctx.slots.inject('conversation.input.right', () => ctx.slots.register({
|
|
837
876
|
name: 'conversation.input.right',
|
|
838
877
|
id: 'composer-continue',
|
|
839
878
|
order: 1000,
|
|
840
879
|
locale: LOCALE_NS,
|
|
841
|
-
}, ContinueButton));
|
|
880
|
+
}, ContinueButton))]);
|
|
842
881
|
if (ctx.locale !== undefined) ctx.effect(() => ctx.locale.register(LOCALE_NS, {
|
|
843
882
|
en: {
|
|
883
|
+
'settings.nav': 'Composer Plus',
|
|
884
|
+
'settings.intro': 'Turn individual dsh-composer-plus features on or off. Changes apply immediately on this device.',
|
|
885
|
+
'feature.changesPanel': 'Changes panel',
|
|
886
|
+
'feature.changesPanel.description': 'Right sidebar page with uncommitted, staged and branch changes and their diffs.',
|
|
887
|
+
'feature.projectStatus': 'Project status',
|
|
888
|
+
'feature.projectStatus.description': 'Show the running / waiting / done marker of a project\'s chats on the project row.',
|
|
889
|
+
'feature.activeProjectsFirst': 'Active projects first',
|
|
890
|
+
'feature.activeProjectsFirst.description': 'Move a project to the top of the sidebar when one of its chats runs or needs you. Moves are saved; turning this off keeps the current order.',
|
|
891
|
+
'feature.notifications': 'Background notifications',
|
|
892
|
+
'feature.notifications.description': 'Notify when a chat finishes or needs you while dsh is in the background, with a Dock badge.',
|
|
893
|
+
'feature.allowForChat': 'Allow for this chat',
|
|
894
|
+
'feature.allowForChat.description': 'Add “Allow for this chat” to Auto Review approvals and auto-allow granted tools.',
|
|
895
|
+
'feature.continueButton': 'One-click Continue',
|
|
896
|
+
'feature.continueButton.description': 'Turn Send into ▶ after a stopped turn; one click sends “continue”.',
|
|
897
|
+
'feature.inputHistory': 'Input history',
|
|
898
|
+
'feature.inputHistory.description': 'Use ↑ / ↓ in an empty composer to reuse messages you sent.',
|
|
899
|
+
'feature.planToggle': 'Shift+Tab plan mode',
|
|
900
|
+
'feature.planToggle.description': 'Shift+Tab adds or removes /plan, or leaves plan mode.',
|
|
901
|
+
'feature.queueReorder': 'Queue drag-and-drop',
|
|
902
|
+
'feature.queueReorder.description': 'Reorder queued messages by dragging their grip.',
|
|
903
|
+
'feature.copyDeeplink': 'Copy deeplink',
|
|
904
|
+
'feature.copyDeeplink.description': 'Right-click a chat in the sidebar to copy a link to it.',
|
|
844
905
|
'project.running': '{count} chat(s) running',
|
|
845
906
|
'project.approval': 'A chat is waiting for approval',
|
|
846
907
|
'project.plan-review': 'A plan is waiting for review',
|
|
@@ -886,6 +947,28 @@ window.__ModuleLoader__.load({
|
|
|
886
947
|
'changes.refresh': 'Refresh',
|
|
887
948
|
},
|
|
888
949
|
zh: {
|
|
950
|
+
'settings.nav': 'Composer Plus',
|
|
951
|
+
'settings.intro': '单独开启或关闭 dsh-composer-plus 的各项功能。更改会立即在本设备生效。',
|
|
952
|
+
'feature.changesPanel': '改动面板',
|
|
953
|
+
'feature.changesPanel.description': '右侧边栏页面,显示未提交、已暂存和分支改动及其差异。',
|
|
954
|
+
'feature.projectStatus': '项目状态',
|
|
955
|
+
'feature.projectStatus.description': '在项目行上显示其对话的进行中 / 等待 / 完成标记。',
|
|
956
|
+
'feature.activeProjectsFirst': '活跃项目置顶',
|
|
957
|
+
'feature.activeProjectsFirst.description': '当项目中的对话正在运行或需要你处理时,将该项目移到侧边栏顶部。移动会被保存;关闭此项后保留当前顺序。',
|
|
958
|
+
'feature.notifications': '后台通知',
|
|
959
|
+
'feature.notifications.description': 'dsh 在后台时,对话完成或需要你处理时发出通知,并显示程序坞角标。',
|
|
960
|
+
'feature.allowForChat': '本对话内允许',
|
|
961
|
+
'feature.allowForChat.description': '在 Auto Review 审批中添加“本对话内允许”,并自动允许已授权的工具。',
|
|
962
|
+
'feature.continueButton': '一键继续',
|
|
963
|
+
'feature.continueButton.description': '回合停止后,发送按钮变为 ▶,点击即发送“继续”。',
|
|
964
|
+
'feature.inputHistory': '输入历史',
|
|
965
|
+
'feature.inputHistory.description': '在空输入框中按 ↑ / ↓ 复用已发送的消息。',
|
|
966
|
+
'feature.planToggle': 'Shift+Tab 计划模式',
|
|
967
|
+
'feature.planToggle.description': 'Shift+Tab 添加或移除 /plan,或退出计划模式。',
|
|
968
|
+
'feature.queueReorder': '队列拖拽排序',
|
|
969
|
+
'feature.queueReorder.description': '拖动排队消息的把手以调整顺序。',
|
|
970
|
+
'feature.copyDeeplink': '复制深层链接',
|
|
971
|
+
'feature.copyDeeplink.description': '在侧边栏右键点击对话以复制其链接。',
|
|
889
972
|
'project.running': '{count} 个对话进行中',
|
|
890
973
|
'project.approval': '有对话等待审批',
|
|
891
974
|
'project.plan-review': '有计划等待审阅',
|
|
@@ -931,29 +1014,29 @@ window.__ModuleLoader__.load({
|
|
|
931
1014
|
'changes.refresh': '刷新',
|
|
932
1015
|
},
|
|
933
1016
|
}), 'dsh-composer-plus: dictionaries');
|
|
934
|
-
ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
1017
|
+
feature('projectStatus', () => [ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
935
1018
|
name: 'shell.overlay',
|
|
936
1019
|
id: 'project-status',
|
|
937
1020
|
locale: LOCALE_NS,
|
|
938
|
-
}, ProjectStatusMarkers));
|
|
939
|
-
ctx.
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
}
|
|
945
|
-
|
|
1021
|
+
}, ProjectStatusMarkers))]);
|
|
1022
|
+
// ctx.inject returns a Cordis fiber, not a disposer.
|
|
1023
|
+
const injectScope = (deps, setup) => {
|
|
1024
|
+
if (ctx.inject === undefined) return [];
|
|
1025
|
+
const fiber = ctx.inject(deps, setup);
|
|
1026
|
+
return [() => fiber.dispose()];
|
|
1027
|
+
};
|
|
1028
|
+
feature('notifications', () => injectScope(['uiSession', 'uiWorkspace', 'sessions'], (scope) => {
|
|
946
1029
|
const t = ctx.locale.bind(LOCALE_NS);
|
|
947
1030
|
scope.effect(() => watchAttention(scope.uiSession.sessionStatus, scope.sessions.list, t,
|
|
948
1031
|
(sessionId) => scope.uiWorkspace.openSession(sessionId)), 'dsh-composer-plus: attention notifications');
|
|
949
1032
|
// First use asks the OS once; a denied permission stays denied (re-enable it in System Settings).
|
|
950
1033
|
if (typeof Notification !== 'undefined' && Notification.permission === 'default') Notification.requestPermission().catch(() => {});
|
|
951
|
-
});
|
|
952
|
-
|
|
1034
|
+
}));
|
|
1035
|
+
feature('activeProjectsFirst', () => injectScope(['workspaces', 'uiSession', 'sessions'], (scope) => {
|
|
953
1036
|
scope.effect(() => watchActiveProjects(scope.workspaces, scope.sessions.list, scope.uiSession.sessionStatus),
|
|
954
1037
|
'dsh-composer-plus: active projects first');
|
|
955
|
-
});
|
|
956
|
-
|
|
1038
|
+
}));
|
|
1039
|
+
feature('changesPanel', () => injectScope(['sidebarRight', 'sidebarRightTabs'], (scope) => {
|
|
957
1040
|
const t = ctx.locale.bind(LOCALE_NS);
|
|
958
1041
|
scope.effect(() => scope.sidebarRightTabs.register({
|
|
959
1042
|
id: CHANGES_ID,
|
|
@@ -972,8 +1055,13 @@ window.__ModuleLoader__.load({
|
|
|
972
1055
|
openLastTurn: (sessionId, { seq, turn }) => scope.sidebarRight.openResource(`dsh-resource://changes-review/session/${encodeURIComponent(sessionId)}/${seq}/${turn}`),
|
|
973
1056
|
}),
|
|
974
1057
|
}, ChangesTab)), 'dsh-composer-plus: changes tab body');
|
|
975
|
-
});
|
|
976
|
-
|
|
1058
|
+
}));
|
|
1059
|
+
feature('allowForChat', () => [ctx.slots.inject('conversation.session.header.utilities', () => ctx.slots.register({
|
|
1060
|
+
name: 'conversation.session.header.utilities',
|
|
1061
|
+
id: 'chat-tool-grants',
|
|
1062
|
+
order: 0,
|
|
1063
|
+
locale: LOCALE_NS,
|
|
1064
|
+
}, ChatToolGrants)), ...injectScope(['uiSession'], (scope) => {
|
|
977
1065
|
const status = scope.uiSession.sessionStatus;
|
|
978
1066
|
// Deferred so answering never re-enters the status notification that triggered it.
|
|
979
1067
|
const scan = () => { queueMicrotask(() => answerGranted(status.getSnapshot())); };
|
|
@@ -983,25 +1071,44 @@ window.__ModuleLoader__.load({
|
|
|
983
1071
|
scan();
|
|
984
1072
|
return () => { offStatus(); offGrants(); };
|
|
985
1073
|
}, 'dsh-composer-plus: chat tool grants');
|
|
986
|
-
});
|
|
1074
|
+
})]);
|
|
987
1075
|
if (ctx.effect !== undefined) ctx.effect(() => () => { grants.clear(); grantsChanged(); }, 'dsh-composer-plus: forget grants on unload');
|
|
988
1076
|
if (typeof fetch === 'function') loadGrants();
|
|
989
|
-
ctx.slots.inject('sidebar.workspaces.session.menu.item', () => ctx.slots.register({
|
|
1077
|
+
feature('copyDeeplink', () => [ctx.slots.inject('sidebar.workspaces.session.menu.item', () => ctx.slots.register({
|
|
990
1078
|
name: 'sidebar.workspaces.session.menu.item',
|
|
991
1079
|
id: 'copy-deeplink',
|
|
992
1080
|
order: 350,
|
|
993
1081
|
locale: LOCALE_NS,
|
|
994
|
-
}, CopyDeeplinkItem))
|
|
995
|
-
ctx.slots.inject('sidebar.workspaces.session.row.action', () => ctx.slots.register({
|
|
1082
|
+
}, CopyDeeplinkItem)), ctx.slots.inject('sidebar.workspaces.session.row.action', () => ctx.slots.register({
|
|
996
1083
|
name: 'sidebar.workspaces.session.row.action',
|
|
997
1084
|
id: 'session-context-menu',
|
|
998
1085
|
order: 0,
|
|
999
|
-
}, RowContextMenu));
|
|
1000
|
-
ctx.slots.inject('conversation.composer.dock', () => ctx.slots.register({
|
|
1086
|
+
}, RowContextMenu))]);
|
|
1087
|
+
feature('inputHistory', () => [ctx.slots.inject('conversation.composer.dock', () => ctx.slots.register({
|
|
1001
1088
|
name: 'conversation.composer.dock',
|
|
1002
1089
|
id: 'composer-history',
|
|
1003
1090
|
order: 20,
|
|
1004
|
-
}, ComposerHistory));
|
|
1091
|
+
}, ComposerHistory))]);
|
|
1092
|
+
const toggles = createToggles();
|
|
1093
|
+
const apply = () => {
|
|
1094
|
+
const enabled = toggles.getSnapshot();
|
|
1095
|
+
for (const f of features) {
|
|
1096
|
+
const on = enabled[f.id] !== false;
|
|
1097
|
+
if (on && f.off === null) { const offs = f.setup(); f.off = () => { for (const off of offs.reverse()) off?.(); }; }
|
|
1098
|
+
else if (!on && f.off !== null) { f.off(); f.off = null; }
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
apply();
|
|
1102
|
+
const offToggles = toggles.subscribe(apply);
|
|
1103
|
+
if (ctx.effect !== undefined) ctx.effect(() => () => { offToggles(); for (const f of features) if (f.off !== null) { f.off(); f.off = null; } }, 'dsh-composer-plus: feature toggles');
|
|
1104
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
1105
|
+
name: 'settings.section',
|
|
1106
|
+
id: 'composer-plus',
|
|
1107
|
+
order: 90,
|
|
1108
|
+
label: () => (ctx.locale === undefined ? 'Composer Plus' : ctx.locale.bind(LOCALE_NS)('settings.nav')),
|
|
1109
|
+
locale: LOCALE_NS,
|
|
1110
|
+
inject: () => ({ hooks: { composerPlusToggles: toggles }, setFeature: (id, on) => toggles.update((draft) => { draft[id] = on; }) }),
|
|
1111
|
+
}, FeatureSettings));
|
|
1005
1112
|
},
|
|
1006
1113
|
};
|
|
1007
1114
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-composer-plus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "DeepSeek Harness helpers: Codex-style Changes panel, input history, Shift+Tab /plan, one-click Continue, queue drag-and-drop, Copy deeplink, Auto Review allow-for-chat, background notifications",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"LICENSE"
|
|
26
26
|
],
|
|
27
27
|
"scripts": {
|
|
28
|
-
"test": "node test/continue.test.mjs && node test/queue-drag.test.mjs && node test/plan-toggle.test.mjs && node test/deeplink.test.mjs && node test/chat-grant.test.mjs && node test/notify.test.mjs && node test/changes.test.mjs && node test/project-status.test.mjs"
|
|
28
|
+
"test": "node test/continue.test.mjs && node test/queue-drag.test.mjs && node test/plan-toggle.test.mjs && node test/deeplink.test.mjs && node test/chat-grant.test.mjs && node test/notify.test.mjs && node test/changes.test.mjs && node test/project-status.test.mjs && node test/toggles.test.mjs"
|
|
29
29
|
},
|
|
30
30
|
"dsh": {
|
|
31
31
|
"bundle": {
|