dsh-mobilecode 0.11.3 → 0.11.5
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 +3 -2
- package/lib/client.js +116 -53
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -156,8 +156,9 @@ card is **Device 1**; co-op docks a second, **Device 2**. It is produced
|
|
|
156
156
|
independently. Each pane grants its own HMAC capability; closing one pane
|
|
157
157
|
never stalls or disturbs the other stream. A presence watcher polls device
|
|
158
158
|
liveness while streaming: if a device is powered off (⏻ off, crash, unplug)
|
|
159
|
-
its pane drops the frozen frame and the Live badge within ~5 s, and
|
|
160
|
-
|
|
159
|
+
its pane drops the frozen frame and the Live badge within ~5 s, and stays
|
|
160
|
+
idle instead of silently grabbing another device — both panes guard against
|
|
161
|
+
the re-grab (0.11.3–4). Both stream
|
|
161
162
|
captions carry the running client version (`· v0.11.x`) so a page refresh is
|
|
162
163
|
visibly proven.
|
|
163
164
|
- **Security**: every stream route sits behind a loopback + trusted-browser
|
package/lib/client.js
CHANGED
|
@@ -121,7 +121,7 @@ window.__ModuleLoader__.load({
|
|
|
121
121
|
.mc-live-stage .mc-live-off { display: flex; align-items: center; justify-content: center; width: 260px; max-width: 100%; height: 460px; color: #9aa0a8; font-size: 13px; line-height: 1.5; text-align: center; }
|
|
122
122
|
/* 0.9.1 co-op: two forced columns — side-by-side MUST survive a narrow drawer. */
|
|
123
123
|
.mc-live-section { display: flex; flex-direction: column; gap: 10px; }
|
|
124
|
-
.mc-live-section.coop { display: grid; grid-template-columns: repeat(
|
|
124
|
+
.mc-live-section.coop { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(250px, 100%), 1fr)); gap: 10px; align-items: start; }
|
|
125
125
|
.mc-live-section.coop > .mc-card { min-width: 0; }
|
|
126
126
|
.mc-live-section.coop .mc-card-head { flex-wrap: wrap; }
|
|
127
127
|
/* No coop-specific height cap: both panes must share Device 1's 60vh rule so
|
|
@@ -171,8 +171,8 @@ window.__ModuleLoader__.load({
|
|
|
171
171
|
.mc-seg { display: inline-flex; border: 1px solid light-dark(rgba(0,0,0,.18), rgba(255,255,255,.2)); border-radius: 8px; overflow: hidden; }
|
|
172
172
|
.mc-seg button { border: 0; background: transparent; color: inherit; font: inherit; font-size: 11px; padding: 4px 9px; cursor: pointer; opacity: .6; }
|
|
173
173
|
.mc-seg button[data-on] { background: light-dark(rgba(66,133,244,.12), rgba(66,133,244,.22)); color: #4285f4; opacity: 1; }
|
|
174
|
-
.mc-live-stage.fit {
|
|
175
|
-
.mc-live-stage.fit img { max-height: 100%; }
|
|
174
|
+
.mc-live-stage.fit { width: min(100%, calc(60vh * var(--mc-ar-n, 0.45))); aspect-ratio: var(--mc-ar-n, 0.45); display: flex; align-items: center; justify-content: center; margin-inline: auto; }
|
|
175
|
+
.mc-live-stage.fit img { width: auto; height: auto; max-width: 100%; max-height: 100%; object-fit: contain; }
|
|
176
176
|
.mc-live-stage.bezel { padding: 8px; background: #111; border-radius: 16px; }
|
|
177
177
|
.mc-live-stage.device { padding: 12px; background: linear-gradient(145deg, #2c2f36, #17191d); border-radius: 26px; box-shadow: 0 10px 30px rgba(0,0,0,.45); }
|
|
178
178
|
/* ── 0.7.0: compact conversation cards ── */
|
|
@@ -190,7 +190,7 @@ window.__ModuleLoader__.load({
|
|
|
190
190
|
// Shown on both stream captions so a refresh visibly proves which client
|
|
191
191
|
// bundle the browser is actually running. Kept in lockstep with
|
|
192
192
|
// package.json by test/client.mjs.
|
|
193
|
-
const MC_VERSION = "0.11.
|
|
193
|
+
const MC_VERSION = "0.11.5";
|
|
194
194
|
// The panel shows cards for platforms the host can actually drive; the
|
|
195
195
|
// server reports its OS in info.os (iOS tooling is darwin-only).
|
|
196
196
|
const fallbackPlatforms = (os) => (os === "darwin" ? ["ios", "android"] : ["android"]);
|
|
@@ -242,6 +242,58 @@ window.__ModuleLoader__.load({
|
|
|
242
242
|
);
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
// Shared SVG icon paths for the stream toolbars (0.11.5 — both panes
|
|
246
|
+
// carry the same navigation controls; Device 2 used to lack them).
|
|
247
|
+
const NAV_ICONS = {
|
|
248
|
+
back: "M15 18l-6-6 6-6",
|
|
249
|
+
home: "M3 10.5L12 3l9 7.5M5 9.5V21h14V9.5",
|
|
250
|
+
recents: "M12 3l9 5-9 5-9-5 9-5zM3 13l9 5 9-5",
|
|
251
|
+
screenshot: "M4 7h4l2-3h4l2 3h4v13H4zM12 17a3 3 0 1 0 0-6 3 3 0 0 0 0 6z",
|
|
252
|
+
rotate: "M21 12a9 9 0 1 1-2.6-6.3M21 3v6h-6",
|
|
253
|
+
};
|
|
254
|
+
/** One icon toolbar row; items: {title, icon, run}. Used by both panes. */
|
|
255
|
+
function ToolbarRow({ items }) {
|
|
256
|
+
return h("div", { className: "mc-toolbar" },
|
|
257
|
+
items.map((item) => h("button", { key: item.title, title: item.title, onClick: item.run },
|
|
258
|
+
h("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" },
|
|
259
|
+
h("path", { d: item.icon }),
|
|
260
|
+
),
|
|
261
|
+
)),
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
/** Back/Home/Recents/(Screen)/Rotate built from nav icons (0.11.5). */
|
|
265
|
+
const navToolbar = (control, capture, refresh) => [
|
|
266
|
+
{ title: "Back", icon: NAV_ICONS.back, run: () => control({ kind: "button", name: "back" }) },
|
|
267
|
+
{ title: "Home", icon: NAV_ICONS.home, run: () => control({ kind: "button", name: "home" }) },
|
|
268
|
+
{ title: "Recents", icon: NAV_ICONS.recents, run: () => control({ kind: "button", name: "recents" }) },
|
|
269
|
+
...(capture ? [{ title: "Screenshot", icon: NAV_ICONS.screenshot, run: () => capture() }] : []),
|
|
270
|
+
{ title: "Rotate", icon: NAV_ICONS.rotate, run: () => control({ kind: "rotate" }) },
|
|
271
|
+
{ title: "Refresh", icon: NAV_ICONS.rotate, run: refresh },
|
|
272
|
+
];
|
|
273
|
+
/** ☰ device-actions popover; serial-bound, shared by both panes. */
|
|
274
|
+
function DeviceMenu({ serial, onError }) {
|
|
275
|
+
const [open, setOpen] = useState(false);
|
|
276
|
+
const ACTION_ITEMS = [
|
|
277
|
+
{ action: "notifications", label: "Notifications" },
|
|
278
|
+
{ action: "quick_settings", label: "Quick settings" },
|
|
279
|
+
{ action: "collapse", label: "Collapse" },
|
|
280
|
+
{ action: "lock", label: "Lock" },
|
|
281
|
+
{ action: "wake", label: "Wake" },
|
|
282
|
+
{ action: "assistant", label: "Assistant" },
|
|
283
|
+
];
|
|
284
|
+
const run = async (action) => {
|
|
285
|
+
if (serial === "") return;
|
|
286
|
+
try { await streamPost("/stream/device-action", { device: serial, action }); setOpen(false); }
|
|
287
|
+
catch (err) { onError(err instanceof Error ? err.message : String(err)); }
|
|
288
|
+
};
|
|
289
|
+
return h("div", { className: "mc-menu" },
|
|
290
|
+
h("button", { className: "mc-btn", disabled: serial === "", title: "Device actions", onClick: () => setOpen((v) => !v) }, "☰"),
|
|
291
|
+
open && h("div", { className: "mc-menu-pop" },
|
|
292
|
+
ACTION_ITEMS.map((item) => h("button", { key: item.action, onClick: () => run(item.action) }, item.label)),
|
|
293
|
+
),
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
245
297
|
function PanelController() {
|
|
246
298
|
let panelOpen = false;
|
|
247
299
|
const listeners = new Set();
|
|
@@ -566,6 +618,10 @@ window.__ModuleLoader__.load({
|
|
|
566
618
|
* the full card and by the second co-op pane. */
|
|
567
619
|
function useLiveStream({ serial, onError }) {
|
|
568
620
|
const [streamUrl, setStreamUrl] = useState("");
|
|
621
|
+
// Native aspect ratio (w/h) of the streamed frames, learned from the
|
|
622
|
+
// first decoded MJPEG frame so the Fit stage has the device's real
|
|
623
|
+
// shape — zero letterboxing regardless of pane/device mix (0.11.5).
|
|
624
|
+
const [ar, setAr] = useState(null);
|
|
569
625
|
const imgRef = useRef(null);
|
|
570
626
|
const dragRef = useRef(null);
|
|
571
627
|
const grantTimer = useRef(null);
|
|
@@ -643,7 +699,22 @@ window.__ModuleLoader__.load({
|
|
|
643
699
|
control({ kind: "tap", x: end.x, y: end.y });
|
|
644
700
|
}
|
|
645
701
|
};
|
|
646
|
-
|
|
702
|
+
// Aspect-ratio learner: MJPEG frames decode into the same <img>, so
|
|
703
|
+
// naturalWidth/naturalHeight is live. Poll once a second while the
|
|
704
|
+
// stream is up; stage boxes read --mc-ar-n from here (fit mode).
|
|
705
|
+
useEffect(() => {
|
|
706
|
+
if (streamUrl === "") return;
|
|
707
|
+
const tick = setInterval(() => {
|
|
708
|
+
const el = imgRef.current;
|
|
709
|
+
if (el && el.naturalWidth > 0 && el.naturalHeight > 0) {
|
|
710
|
+
const a = +(el.naturalWidth / el.naturalHeight).toFixed(4);
|
|
711
|
+
setAr((prev) => (prev === a ? prev : a));
|
|
712
|
+
}
|
|
713
|
+
}, 1000);
|
|
714
|
+
return () => clearInterval(tick);
|
|
715
|
+
}, [streamUrl]);
|
|
716
|
+
|
|
717
|
+
return { streamUrl, grant, reset, control, ar, imgProps: { ref: imgRef, draggable: false, onPointerDown: onDown, onPointerUp: onUp } };
|
|
647
718
|
}
|
|
648
719
|
|
|
649
720
|
/** Device 1 stream: picker-popover header, SVG toolbar pill, device
|
|
@@ -656,7 +727,6 @@ window.__ModuleLoader__.load({
|
|
|
656
727
|
const [serial, setSerial] = useState("");
|
|
657
728
|
const [error, setError] = useState("");
|
|
658
729
|
const [pickerOpen, setPickerOpen] = useState(false);
|
|
659
|
-
const [menuOpen, setMenuOpen] = useState(false);
|
|
660
730
|
const [still, setStill] = useState(null);
|
|
661
731
|
const [view, setView] = useState("live"); // 'live' | 'still'
|
|
662
732
|
// sizeMode/frame are per-pane (0.11.2); StageControls renders the
|
|
@@ -676,7 +746,12 @@ window.__ModuleLoader__.load({
|
|
|
676
746
|
const r = await streamPost("/stream/devices", {});
|
|
677
747
|
setDevices(r.devices);
|
|
678
748
|
setAvds(r.avds ?? []);
|
|
679
|
-
|
|
749
|
+
// Prefer emulators for the welcome auto-pick: a physical
|
|
750
|
+
// serial sorts first in adb order but has no ⏻ off control.
|
|
751
|
+
// (Serial prefix, not the avd field — avdName() can fail.)
|
|
752
|
+
const isEmu = (d) => d.serial.startsWith("emulator-");
|
|
753
|
+
const first = r.devices.find((d) => d.streaming && isEmu(d))?.serial ?? r.devices.find(isEmu)?.serial
|
|
754
|
+
?? r.devices.find((d) => d.streaming)?.serial ?? r.devices[0]?.serial ?? "";
|
|
680
755
|
const cur = serialRef.current;
|
|
681
756
|
if (cur === "") {
|
|
682
757
|
// Auto-stream once per page load (the welcome flow). After
|
|
@@ -719,12 +794,6 @@ window.__ModuleLoader__.load({
|
|
|
719
794
|
}
|
|
720
795
|
};
|
|
721
796
|
|
|
722
|
-
const menuAction = async (action) => {
|
|
723
|
-
if (serial === "") return;
|
|
724
|
-
try { await streamPost("/stream/device-action", { device: serial, action }); setMenuOpen(false); }
|
|
725
|
-
catch (err) { setError(err instanceof Error ? err.message : String(err)); }
|
|
726
|
-
};
|
|
727
|
-
|
|
728
797
|
const pick = (device) => { autoPicked.current = true; setSerial(device); live.reset(); setStill(null); setView("live"); setError(""); setPickerOpen(false); };
|
|
729
798
|
// 0.10.0 merged "Start server": boot a configured AVD from the picker.
|
|
730
799
|
// The route waits for boot completion; then we re-list and stream it.
|
|
@@ -761,22 +830,7 @@ window.__ModuleLoader__.load({
|
|
|
761
830
|
const sizeStyle = stageSizeStyle(sizeMode);
|
|
762
831
|
const stageClass = stageClassFor(frame, sizeMode.mode === "fit");
|
|
763
832
|
const runningAvds = new Set(devices.map((d) => d.avd).filter(Boolean));
|
|
764
|
-
const toolbar =
|
|
765
|
-
{ title: "Back", icon: "M15 18l-6-6 6-6", run: () => control({ kind: "button", name: "back" }) },
|
|
766
|
-
{ title: "Home", icon: "M3 10.5L12 3l9 7.5M5 9.5V21h14V9.5", run: () => control({ kind: "button", name: "home" }) },
|
|
767
|
-
{ title: "Recents", icon: "M12 3l9 5-9 5-9-5 9-5zM3 13l9 5 9-5", run: () => control({ kind: "button", name: "recents" }) },
|
|
768
|
-
{ title: "Screenshot", icon: "M4 7h4l2-3h4l2 3h4v13H4zM12 17a3 3 0 1 0 0-6 3 3 0 0 0 0 6z", run: () => capture() },
|
|
769
|
-
{ title: "Rotate", icon: "M21 12a9 9 0 1 1-2.6-6.3M21 3v6h-6", run: () => control({ kind: "rotate" }) },
|
|
770
|
-
{ title: "Refresh", icon: "M21 12a9 9 0 1 1-2.6-6.3M21 3v6h-6", run: () => { if (serial !== "") grant(serial); } },
|
|
771
|
-
];
|
|
772
|
-
const menuItems = [
|
|
773
|
-
{ action: "notifications", label: "Notifications" },
|
|
774
|
-
{ action: "quick_settings", label: "Quick settings" },
|
|
775
|
-
{ action: "collapse", label: "Collapse" },
|
|
776
|
-
{ action: "lock", label: "Lock" },
|
|
777
|
-
{ action: "wake", label: "Wake" },
|
|
778
|
-
{ action: "assistant", label: "Assistant" },
|
|
779
|
-
];
|
|
833
|
+
const toolbar = navToolbar(control, capture, () => { if (serial !== "") grant(serial); });
|
|
780
834
|
|
|
781
835
|
return h("div", { className: "mc-card mc-live" },
|
|
782
836
|
h("div", { className: "mc-card-head" },
|
|
@@ -818,12 +872,7 @@ window.__ModuleLoader__.load({
|
|
|
818
872
|
),
|
|
819
873
|
),
|
|
820
874
|
),
|
|
821
|
-
h(
|
|
822
|
-
h("button", { className: "mc-btn", disabled: serial === "", title: "Device actions", onClick: () => setMenuOpen((v) => !v) }, "☰"),
|
|
823
|
-
menuOpen && h("div", { className: "mc-menu-pop" },
|
|
824
|
-
menuItems.map((item) => h("button", { key: item.action, onClick: () => menuAction(item.action) }, item.label)),
|
|
825
|
-
),
|
|
826
|
-
),
|
|
875
|
+
h(DeviceMenu, { serial, onError: setError }),
|
|
827
876
|
// 0.9.0 co-op: side-by-side second pane (needs two online devices).
|
|
828
877
|
devices.length >= 2 && h("button", { className: "mc-btn", "data-on": coopOn || undefined, title: "Co-op: watch Device 1 + Device 2 side by side", onClick: onToggleCoop }, "⧉"),
|
|
829
878
|
),
|
|
@@ -832,14 +881,8 @@ window.__ModuleLoader__.load({
|
|
|
832
881
|
h("span", null, h("span", { className: "mc-dot warn" }), " Booting " + booting + " — it appears here once online (about a minute).")),
|
|
833
882
|
// 0.11.0 audit: no ghost affordances — the toolbar appears once a
|
|
834
883
|
// device is selected (disabled-grey read as broken).
|
|
835
|
-
serial !== "" && h(
|
|
836
|
-
|
|
837
|
-
h("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" },
|
|
838
|
-
h("path", { d: item.icon }),
|
|
839
|
-
),
|
|
840
|
-
)),
|
|
841
|
-
),
|
|
842
|
-
h("div", { className: stageClass },
|
|
884
|
+
serial !== "" && h(ToolbarRow, { items: toolbar }),
|
|
885
|
+
h("div", { className: stageClass, style: sizeMode.mode === "fit" && live.ar != null ? { "--mc-ar-n": live.ar } : undefined },
|
|
843
886
|
view === "still" && still?.dataUrl
|
|
844
887
|
? h("img", { src: still.dataUrl, alt: "still capture", draggable: false })
|
|
845
888
|
: live.streamUrl !== ""
|
|
@@ -870,22 +913,35 @@ window.__ModuleLoader__.load({
|
|
|
870
913
|
const [error, setError] = useState("");
|
|
871
914
|
const live = useLiveStream({ serial, onError: setError });
|
|
872
915
|
const grant = (device) => live.grant(device).then((ok) => { if (ok) setError(""); });
|
|
873
|
-
// Serial mirror for the
|
|
916
|
+
// Serial mirror for the 5s poller + one-shot auto-pick guard: this
|
|
917
|
+
// pane may steal a partner while nothing is picked, but it must NOT
|
|
918
|
+
// silently switch to a third device after its own goes offline.
|
|
874
919
|
const serialRef = useRef(serial);
|
|
875
920
|
serialRef.current = serial;
|
|
921
|
+
const autoPicked = useRef(false);
|
|
876
922
|
|
|
877
923
|
const refresh = async () => {
|
|
878
924
|
try {
|
|
879
925
|
const r = await streamPost("/stream/devices", {});
|
|
880
926
|
setDevices(r.devices);
|
|
881
|
-
|
|
882
|
-
|
|
927
|
+
// Partner candidates: emulators only (serial prefix, not the
|
|
928
|
+
// flaky avd field) — the co-op partner must be killable from
|
|
929
|
+
// the picker, and never Device 1's own serial.
|
|
930
|
+
const others = r.devices.filter((d) => d.serial !== excludeSerial && d.serial.startsWith("emulator-"));
|
|
883
931
|
const cur = serialRef.current;
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
932
|
+
if (cur !== "") {
|
|
933
|
+
// Keep any valid pick — including a manually chosen physical
|
|
934
|
+
// device; only emulators are eligible for auto-pick. A dead
|
|
935
|
+
// or stolen pick clears to the placeholder (never re-grabs).
|
|
936
|
+
if (cur !== excludeSerial && r.devices.some((d) => d.serial === cur)) return;
|
|
937
|
+
autoPicked.current = true;
|
|
938
|
+
setSerial(""); live.reset();
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
if (!autoPicked.current) {
|
|
942
|
+
const next = (others.find((d) => d.streaming) ?? others[0])?.serial ?? "";
|
|
943
|
+
if (next !== "") { autoPicked.current = true; setSerial(next); }
|
|
944
|
+
}
|
|
889
945
|
} catch {
|
|
890
946
|
/* the header keeps the last device list */
|
|
891
947
|
}
|
|
@@ -905,12 +961,19 @@ window.__ModuleLoader__.load({
|
|
|
905
961
|
live.streamUrl !== "" && h("span", { className: "mc-live-badge" }, h("span", { className: "mc-dot on" }), "Live"),
|
|
906
962
|
h("span", { className: "sp" }),
|
|
907
963
|
h("select", { className: "mc-input mc-coop-select", value: serial, onChange: (e) => { setSerial(e.target.value); live.reset(); } },
|
|
908
|
-
|
|
964
|
+
// An honest empty option: a select with no matching option
|
|
965
|
+
// silently DISPLAYS devices[0] while state is still "" —
|
|
966
|
+
// that made the probe read a phantom pick (0.11.4).
|
|
967
|
+
!devices.some((d) => d.serial === serial) && h("option", { value: "" }, devices.length === 0 ? "no device" : "— pick device —"),
|
|
909
968
|
devices.map((d) => h("option", { key: d.serial, value: d.serial }, d.serial + (d.serial === excludeSerial ? " (Device 1)" : d.streaming ? " ●" : ""))),
|
|
910
969
|
),
|
|
970
|
+
h(DeviceMenu, { serial, onError: setError }),
|
|
911
971
|
),
|
|
912
972
|
error !== "" && h("div", { className: "mc-error" }, error),
|
|
913
|
-
|
|
973
|
+
// 0.11.5: Device 2 gets the same nav toolbar as Device 1
|
|
974
|
+
// (back/home/recents/rotate/refresh) — full control of both players.
|
|
975
|
+
serial !== "" && h(ToolbarRow, { items: navToolbar(live.control, undefined, () => { if (serial !== "") grant(serial); }) }),
|
|
976
|
+
h("div", { className: stageClassFor(frame, sizeMode.mode === "fit"), style: sizeMode.mode === "fit" && live.ar != null ? { "--mc-ar-n": live.ar } : undefined },
|
|
914
977
|
live.streamUrl !== ""
|
|
915
978
|
? h("img", { ...live.imgProps, src: live.streamUrl, alt: "co-op device screen", style: stageSizeStyle(sizeMode), onError: () => grant(serial) })
|
|
916
979
|
: h("div", { className: "mc-live-off" }, devices.filter((d) => d.serial !== excludeSerial).length === 0
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mobilecode",
|
|
3
3
|
"description": "MobileCode for the dsh web GUI: detect iOS/Android projects, run preview servers, and drive the simulator/emulator from the session — 37 agent tools (device_run, device_screen, device_ui_tree, device_ui_rows, device_tap_row, device_tap_element, device_wait_for, device_scroll_to, device_input, device_batch, device_intent, device_connect, device_pair_qr, device_perf, device_meminfo, device_backtrace, device_display, device_avd_create, device_pair_capture, device_app_info, device_install, device_uninstall, device_reboot, device_log, live screen stream, multimodal screenshots) plus a host-mediated co-op mesh hub (random callsigns, JSON pub/sub, tunable latency/jitter/drop/dup/throttle) so two virtual devices — and the AI watching them — can talk in real time. One classified adb boundary and a Wi-Fi connect/pair QR flow in the Connection settings tab. Hot-pluggable — mounted via the profile bundle list + cordis.patch.yml, no dsh source changes.",
|
|
4
|
-
"version": "0.11.
|
|
4
|
+
"version": "0.11.5",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.22.0",
|
|
7
7
|
"engines": {
|