dsh-mobilecode 0.10.1 → 0.11.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 +12 -8
- package/lib/client.js +69 -33
- package/lib/index.js +35 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,9 +8,10 @@ with embedded iOS Simulator / Android Emulator support) into
|
|
|
8
8
|
It detects the mobile project in a directory (iOS / Android, Expo / React
|
|
9
9
|
Native / native), builds, installs and launches the app on the booted
|
|
10
10
|
simulator or emulator — and exposes the same to the agent through tools. The
|
|
11
|
-
device screen has exactly **one** home in the panel: the **
|
|
12
|
-
stream. (0.10.0 merged the old Android
|
|
13
|
-
`serve-avd` is retired for the panel;
|
|
11
|
+
device screen has exactly **one** home in the panel: the **Device 1** live
|
|
12
|
+
stream (co-op adds a second pane, **Device 2**). (0.10.0 merged the old Android
|
|
13
|
+
"Start server" preview into it — `serve-avd` is retired for the panel;
|
|
14
|
+
`serve-sim` remains the iOS-only view.)
|
|
14
15
|
|
|
15
16
|
## Installation
|
|
16
17
|
|
|
@@ -32,12 +33,14 @@ If you find this useful, you can support development at [ko-fi.com/spix18](https
|
|
|
32
33
|
drawer:
|
|
33
34
|
|
|
34
35
|
- project directory input (persisted in localStorage) + Detect
|
|
35
|
-
- platform pills — Android: attached-device status (its view is the
|
|
36
|
-
|
|
36
|
+
- platform pills — Android: attached-device status (its view is the Device 1
|
|
37
|
+
card); iOS: preview-server status. The iOS pill and card only render when the
|
|
38
|
+
host OS is macOS — on Windows/Linux there is no simulator to drive, so 0.11.0
|
|
39
|
+
stopped showing that dead affordance.
|
|
37
40
|
- per platform: **Run app / Stop app**, build status/step/error and an
|
|
38
41
|
expandable log tail. iOS additionally **Start/Stop server** with the
|
|
39
42
|
embedded serve-sim iframe — Android's duplicated serve-avd preview was
|
|
40
|
-
merged into the
|
|
43
|
+
merged into the Device 1 card in 0.10.0
|
|
41
44
|
- a Metro (bundler) status card
|
|
42
45
|
- polls `GET /api/dsh-mobilecode` every 2 s while open
|
|
43
46
|
|
|
@@ -105,9 +108,10 @@ drawer:
|
|
|
105
108
|
that just need to see the screen should still prefer `device_screen` /
|
|
106
109
|
`device_ui_tree`.
|
|
107
110
|
|
|
108
|
-
**Live device stream (the panel)**
|
|
111
|
+
**Live device stream (the panel — Device 1 / Device 2)**
|
|
109
112
|
|
|
110
|
-
The Devices pane shows a real-time mirror of the attached device.
|
|
113
|
+
The Devices pane shows a real-time mirror of the attached device. The primary
|
|
114
|
+
card is **Device 1**; co-op docks a second, **Device 2**. It is produced
|
|
111
115
|
**in-process** — no inner loopback port, no external helper:
|
|
112
116
|
|
|
113
117
|
- ONE persistent `adb exec-out "while :; do screencap -p; done"` child **per
|
package/lib/client.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - directory input (remembers the last one) + platform pills from detect,
|
|
8
8
|
* - per platform: run/stop the app + build status (iOS only: start/stop
|
|
9
9
|
* the serve-sim preview server with its embedded iframe),
|
|
10
|
-
* -
|
|
10
|
+
* - Device 1 card (the live stream): 0.10.0 merged the Android
|
|
11
11
|
* "Start server" into it (serve-avd retired; picker ⏻ boot → POST /boot),
|
|
12
12
|
* - build status, error and expandable log tail,
|
|
13
13
|
* - polls GET /api/dsh-mobilecode every ~2s while open.
|
|
@@ -185,8 +185,17 @@ window.__ModuleLoader__.load({
|
|
|
185
185
|
//#endregion
|
|
186
186
|
|
|
187
187
|
const API_BASE = "/api/dsh-mobilecode";
|
|
188
|
-
|
|
188
|
+
// The panel shows cards for platforms the host can actually drive; the
|
|
189
|
+
// server reports its OS in info.os (iOS tooling is darwin-only).
|
|
190
|
+
const fallbackPlatforms = (os) => (os === "darwin" ? ["ios", "android"] : ["android"]);
|
|
189
191
|
const label = (p) => (p === "ios" ? "iOS" : "Android");
|
|
192
|
+
// Shared by both panes so Device 1 and Device 2 scale identically (0.11.0).
|
|
193
|
+
const stageSizeStyle = (m) => (m.mode === "pct"
|
|
194
|
+
? { width: m.value + "%", maxWidth: "100%", maxHeight: "60vh" }
|
|
195
|
+
: m.mode === "px"
|
|
196
|
+
? { width: m.value + "px", maxWidth: "100%", maxHeight: "60vh" }
|
|
197
|
+
: {});
|
|
198
|
+
const stageClassFor = (f) => "mc-live-stage" + (f !== "none" ? " " + f : "");
|
|
190
199
|
|
|
191
200
|
function PanelController() {
|
|
192
201
|
let panelOpen = false;
|
|
@@ -297,7 +306,7 @@ window.__ModuleLoader__.load({
|
|
|
297
306
|
|
|
298
307
|
const server = (platform) => info?.servers?.find((s) => s.platform === platform);
|
|
299
308
|
const build = (platform) => info?.builds?.find((b) => b.platform === platform);
|
|
300
|
-
const platforms = info?.platforms?.length > 0 ? info.platforms :
|
|
309
|
+
const platforms = info?.platforms?.length > 0 ? info.platforms : fallbackPlatforms(info?.os);
|
|
301
310
|
|
|
302
311
|
const statusClass = (status) =>
|
|
303
312
|
status === "running" ? "on" : status === "starting" ? "warn" : status === "exited" ? "off" : "err";
|
|
@@ -337,13 +346,13 @@ window.__ModuleLoader__.load({
|
|
|
337
346
|
error !== "" && h("div", { className: "mc-error" }, error),
|
|
338
347
|
h("div", { className: "mc-pills" },
|
|
339
348
|
platforms.map((platform) => {
|
|
340
|
-
// 0.10.0: Android's "server" IS the
|
|
349
|
+
// 0.10.0: Android's "server" IS the Device 1 stream — the pill
|
|
341
350
|
// tracks attached devices, not a serve-avd process.
|
|
342
351
|
const running = platform === "android"
|
|
343
352
|
? (info?.deviceCount ?? 0) > 0
|
|
344
353
|
: server(platform)?.status === "running";
|
|
345
354
|
return h("span", { key: platform, className: "mc-pill", "data-on": running ? "true" : undefined,
|
|
346
|
-
title: platform === "android" ? (running ? "Android device attached — mirrored in
|
|
355
|
+
title: platform === "android" ? (running ? "Android device attached — mirrored in Device 1" : "No Android device attached") : "Preview server running" },
|
|
347
356
|
h("span", { className: "mc-dot " + (running ? "on" : "off") }),
|
|
348
357
|
label(platform),
|
|
349
358
|
);
|
|
@@ -353,7 +362,7 @@ window.__ModuleLoader__.load({
|
|
|
353
362
|
platforms.map((platform) => {
|
|
354
363
|
const srv = server(platform);
|
|
355
364
|
const bld = build(platform);
|
|
356
|
-
// 0.10.0 merge: "Start server" (serve-avd iframe) and "
|
|
365
|
+
// 0.10.0 merge: "Start server" (serve-avd iframe) and "Device 1" were
|
|
357
366
|
// two views of the same screen. The in-process stream won; the Android
|
|
358
367
|
// card keeps project controls only. iOS keeps serve-sim (its only view).
|
|
359
368
|
const preview = platform === "ios";
|
|
@@ -365,7 +374,7 @@ window.__ModuleLoader__.load({
|
|
|
365
374
|
? h("button", { className: "mc-btn", disabled: busy !== "", onClick: () => act(platform + "-stop-server", "/stop", { platform }) },
|
|
366
375
|
srv.status === "running" ? "Stop server" : "Server " + srv.status)
|
|
367
376
|
: h("button", { className: "mc-btn primary", disabled: busy !== "", onClick: () => act(platform + "-start-server", "/start", { platform }) }, "Start server")),
|
|
368
|
-
!preview && h("span", { className: "mc-live-cap" }, "screen:
|
|
377
|
+
!preview && h("span", { className: "mc-live-cap" }, "screen: Device 1 ↑"),
|
|
369
378
|
),
|
|
370
379
|
preview && (srv?.url
|
|
371
380
|
? h("iframe", {
|
|
@@ -418,7 +427,7 @@ window.__ModuleLoader__.load({
|
|
|
418
427
|
openLogs["bundler"] && h("pre", { className: "mc-log" }, info.bundler.log.join("\n")),
|
|
419
428
|
),
|
|
420
429
|
info?.servers?.length === 0 && info?.builds?.length === 0 && (info?.deviceCount ?? 0) === 0 &&
|
|
421
|
-
h("div", { className: "mc-empty" }, "Nothing running yet — pick a project directory and press Run app, or boot an AVD in
|
|
430
|
+
h("div", { className: "mc-empty" }, "Nothing running yet — pick a project directory and press Run app, or boot an AVD in Device 1 above."),
|
|
422
431
|
),
|
|
423
432
|
h("div", { className: "mc-footer" },
|
|
424
433
|
h("span", null, "dsh-mobilecode · polls every 2s"),
|
|
@@ -574,9 +583,9 @@ window.__ModuleLoader__.load({
|
|
|
574
583
|
return { streamUrl, grant, reset, control, imgProps: { ref: imgRef, draggable: false, onPointerDown: onDown, onPointerUp: onUp } };
|
|
575
584
|
}
|
|
576
585
|
|
|
577
|
-
/**
|
|
586
|
+
/** Device 1 stream: picker-popover header, SVG toolbar pill, device
|
|
578
587
|
* menu, quick sizes + frame styles, and tap/drag with duration. */
|
|
579
|
-
function LiveDeviceCard({ coopOn, onToggleCoop, onSerial }) {
|
|
588
|
+
function LiveDeviceCard({ coopOn, onToggleCoop, onSerial, sizeMode, setSizeMode, frame, setFrame }) {
|
|
580
589
|
const [devices, setDevices] = useState([]);
|
|
581
590
|
const [avds, setAvds] = useState([]);
|
|
582
591
|
const [serial, setSerial] = useState("");
|
|
@@ -585,9 +594,10 @@ window.__ModuleLoader__.load({
|
|
|
585
594
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
586
595
|
const [still, setStill] = useState(null);
|
|
587
596
|
const [view, setView] = useState("live"); // 'live' | 'still'
|
|
588
|
-
|
|
589
|
-
|
|
597
|
+
// sizeMode/frame live in LiveStreamSection (0.11.0) so both co-op
|
|
598
|
+
// panes scale identically — Device 2 must never be smaller.
|
|
590
599
|
const [booting, setBooting] = useState(""); // AVD name while POST /boot is in flight
|
|
600
|
+
const [powering, setPowering] = useState(""); // serial while POST /off is in flight
|
|
591
601
|
const live = useLiveStream({ serial, onError: setError });
|
|
592
602
|
const grant = (device) => live.grant(device).then((ok) => { if (ok) setError(""); });
|
|
593
603
|
const control = live.control;
|
|
@@ -653,14 +663,26 @@ window.__ModuleLoader__.load({
|
|
|
653
663
|
setBooting("");
|
|
654
664
|
}
|
|
655
665
|
};
|
|
666
|
+
// 0.11.0 device power-off: graceful `adb emu kill` via POST /off.
|
|
667
|
+
// If we just killed the streamed device, drop back to the picker.
|
|
668
|
+
const powerOff = async (device) => {
|
|
669
|
+
setPowering(device); setError("");
|
|
670
|
+
try {
|
|
671
|
+
await streamPost("/off", { serial: device });
|
|
672
|
+
if (device === serial) { setSerial(""); live.reset(); }
|
|
673
|
+
await refreshDevices();
|
|
674
|
+
} catch (err) {
|
|
675
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
676
|
+
await refreshDevices();
|
|
677
|
+
} finally {
|
|
678
|
+
setPowering("");
|
|
679
|
+
}
|
|
680
|
+
};
|
|
656
681
|
const showLive = () => { setView("live"); if (serial !== "" && live.streamUrl === "") grant(serial); };
|
|
657
682
|
|
|
658
|
-
const sizeStyle = sizeMode
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
? { width: sizeMode.value + "px", maxWidth: "100%", maxHeight: "60vh" }
|
|
662
|
-
: {};
|
|
663
|
-
const stageClass = "mc-live-stage" + (frame !== "none" ? " " + frame : "");
|
|
683
|
+
const sizeStyle = stageSizeStyle(sizeMode);
|
|
684
|
+
const stageClass = stageClassFor(frame);
|
|
685
|
+
const runningAvds = new Set(devices.map((d) => d.avd).filter(Boolean));
|
|
664
686
|
const toolbar = [
|
|
665
687
|
{ title: "Back", icon: "M15 18l-6-6 6-6", run: () => control({ kind: "button", name: "back" }) },
|
|
666
688
|
{ title: "Home", icon: "M3 10.5L12 3l9 7.5M5 9.5V21h14V9.5", run: () => control({ kind: "button", name: "home" }) },
|
|
@@ -687,7 +709,7 @@ window.__ModuleLoader__.load({
|
|
|
687
709
|
|
|
688
710
|
return h("div", { className: "mc-card mc-live" },
|
|
689
711
|
h("div", { className: "mc-card-head" },
|
|
690
|
-
h("span", null, "
|
|
712
|
+
h("span", null, "Device 1"),
|
|
691
713
|
live.streamUrl !== "" && h("span", { className: "mc-live-badge" }, h("span", { className: "mc-dot on" }), "Live"),
|
|
692
714
|
h("span", { className: "sp" }),
|
|
693
715
|
h("div", { className: "mc-picker" },
|
|
@@ -696,20 +718,28 @@ window.__ModuleLoader__.load({
|
|
|
696
718
|
devices.length > 0
|
|
697
719
|
? h("div", { className: "mc-picker-group" },
|
|
698
720
|
h("div", { className: "mc-picker-label" }, "Online devices"),
|
|
699
|
-
devices.map((d) => h("
|
|
721
|
+
devices.map((d) => h("div", { key: d.serial, className: "mc-picker-row" + (d.serial === serial ? " on" : ""), onClick: () => pick(d.serial) },
|
|
700
722
|
h("span", null, d.kind === "emulator" ? "🖥" : "📱"),
|
|
701
|
-
h("span", { className: "mc-serial" }, d.serial),
|
|
723
|
+
h("span", { className: "mc-serial" }, d.avd ? d.avd + " · " + d.serial : d.serial),
|
|
702
724
|
d.streaming && h("span", { className: "mc-badge" }, "streaming"),
|
|
725
|
+
d.kind === "emulator" && h("span", { className: "sp" }),
|
|
726
|
+
d.kind === "emulator" && h("button", {
|
|
727
|
+
className: "mc-btn", disabled: booting !== "" || powering !== "",
|
|
728
|
+
title: "Shut down this emulator (adb emu kill)",
|
|
729
|
+
onClick: (e) => { e.stopPropagation(); powerOff(d.serial); },
|
|
730
|
+
}, powering === d.serial ? "stopping…" : "⏻ off"),
|
|
703
731
|
)),
|
|
704
732
|
)
|
|
705
733
|
: h("div", { className: "mc-picker-hint" }, "No device attached"),
|
|
706
|
-
|
|
734
|
+
// Running AVDs already have their row (with ⏻ off) above;
|
|
735
|
+
// the boot list shows only the stopped ones.
|
|
736
|
+
avds.filter((avd) => !runningAvds.has(avd)).length > 0 && h("div", { className: "mc-picker-group" },
|
|
707
737
|
h("div", { className: "mc-picker-label" }, "AVDs"),
|
|
708
|
-
avds.map((avd) => h("div", { key: avd, className: "mc-picker-row" },
|
|
738
|
+
avds.filter((avd) => !runningAvds.has(avd)).map((avd) => h("div", { key: avd, className: "mc-picker-row" },
|
|
709
739
|
h("span", null, "▫ " + avd),
|
|
710
740
|
h("span", { className: "sp" }),
|
|
711
741
|
h("button", {
|
|
712
|
-
className: "mc-btn", disabled: booting !== "",
|
|
742
|
+
className: "mc-btn", disabled: booting !== "" || powering !== "",
|
|
713
743
|
title: "Boot this AVD (runs detached; it appears online when ready)",
|
|
714
744
|
onClick: () => boot(avd),
|
|
715
745
|
}, booting === avd ? "booting…" : "⏻ boot"),
|
|
@@ -724,12 +754,14 @@ window.__ModuleLoader__.load({
|
|
|
724
754
|
),
|
|
725
755
|
),
|
|
726
756
|
// 0.9.0 co-op: side-by-side second pane (needs two online devices).
|
|
727
|
-
devices.length >= 2 && h("button", { className: "mc-btn", "data-on": coopOn || undefined, title: "Co-op: watch
|
|
757
|
+
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 }, "⧉"),
|
|
728
758
|
),
|
|
729
759
|
error !== "" && h("div", { className: "mc-error" }, error),
|
|
730
760
|
booting !== "" && h("div", { className: "mc-status" },
|
|
731
761
|
h("span", null, h("span", { className: "mc-dot warn" }), " Booting " + booting + " — it appears here once online (about a minute).")),
|
|
732
|
-
|
|
762
|
+
// 0.11.0 audit: no ghost affordances — the toolbar appears once a
|
|
763
|
+
// device is selected (disabled-grey read as broken).
|
|
764
|
+
serial !== "" && h("div", { className: "mc-toolbar" },
|
|
733
765
|
toolbar.map((item) => h("button", { key: item.title, title: item.title, disabled: serial === "", onClick: item.run },
|
|
734
766
|
h("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" },
|
|
735
767
|
h("path", { d: item.icon }),
|
|
@@ -779,7 +811,7 @@ window.__ModuleLoader__.load({
|
|
|
779
811
|
/** Co-op second pane (0.9.0): a compact stream view for a second device —
|
|
780
812
|
* device select, live image, tap/drag. No toolbar/menu/size knobs; the
|
|
781
813
|
* full card next to it carries those for whichever device it shows. */
|
|
782
|
-
function CoopPane({ excludeSerial }) {
|
|
814
|
+
function CoopPane({ excludeSerial, sizeMode, frame }) {
|
|
783
815
|
const [devices, setDevices] = useState([]);
|
|
784
816
|
const [serial, setSerial] = useState("");
|
|
785
817
|
const [error, setError] = useState("");
|
|
@@ -811,18 +843,18 @@ window.__ModuleLoader__.load({
|
|
|
811
843
|
|
|
812
844
|
return h("div", { className: "mc-card mc-live mc-coop-pane" },
|
|
813
845
|
h("div", { className: "mc-card-head" },
|
|
814
|
-
h("span", null, "
|
|
846
|
+
h("span", null, "Device 2"),
|
|
815
847
|
live.streamUrl !== "" && h("span", { className: "mc-live-badge" }, h("span", { className: "mc-dot on" }), "Live"),
|
|
816
848
|
h("span", { className: "sp" }),
|
|
817
849
|
h("select", { className: "mc-input mc-coop-select", value: serial, onChange: (e) => { setSerial(e.target.value); live.reset(); } },
|
|
818
850
|
devices.length === 0 && h("option", { value: "" }, "no device"),
|
|
819
|
-
devices.map((d) => h("option", { key: d.serial, value: d.serial }, d.serial + (d.serial === excludeSerial ? " (
|
|
851
|
+
devices.map((d) => h("option", { key: d.serial, value: d.serial }, d.serial + (d.serial === excludeSerial ? " (Device 1)" : d.streaming ? " ●" : ""))),
|
|
820
852
|
),
|
|
821
853
|
),
|
|
822
854
|
error !== "" && h("div", { className: "mc-error" }, error),
|
|
823
|
-
h("div", { className:
|
|
855
|
+
h("div", { className: stageClassFor(frame) },
|
|
824
856
|
live.streamUrl !== ""
|
|
825
|
-
? h("img", { ...live.imgProps, src: live.streamUrl, alt: "co-op device screen", onError: () => grant(serial) })
|
|
857
|
+
? h("img", { ...live.imgProps, src: live.streamUrl, alt: "co-op device screen", style: stageSizeStyle(sizeMode), onError: () => grant(serial) })
|
|
826
858
|
: h("div", { className: "mc-live-off" }, devices.length === 0 ? "No second device — boot another emulator." : "Connecting to " + serial + "..."),
|
|
827
859
|
),
|
|
828
860
|
h("div", { className: "mc-live-cap" }, "tap or drag on the screen to drive this device"),
|
|
@@ -834,9 +866,13 @@ window.__ModuleLoader__.load({
|
|
|
834
866
|
function LiveStreamSection() {
|
|
835
867
|
const [coop, setCoop] = useState(false);
|
|
836
868
|
const [serialA, setSerialA] = useState("");
|
|
869
|
+
// Shared knobs (0.11.0): Device 1's Size/Frame controls drive both
|
|
870
|
+
// panes, so the pair always renders at the same height.
|
|
871
|
+
const [sizeMode, setSizeMode] = useState({ mode: "fit" }); // {mode:'fit'|'pct'|'px', value?}
|
|
872
|
+
const [frame, setFrame] = useState("none"); // 'none' | 'bezel' | 'device'
|
|
837
873
|
return h("div", { className: "mc-live-section" + (coop ? " coop" : "") },
|
|
838
|
-
h(LiveDeviceCard, { coopOn: coop, onToggleCoop: () => setCoop((v) => !v), onSerial: setSerialA }),
|
|
839
|
-
coop && h(CoopPane, { excludeSerial: serialA }),
|
|
874
|
+
h(LiveDeviceCard, { coopOn: coop, onToggleCoop: () => setCoop((v) => !v), onSerial: setSerialA, sizeMode, setSizeMode, frame, setFrame }),
|
|
875
|
+
coop && h(CoopPane, { excludeSerial: serialA, sizeMode, frame }),
|
|
840
876
|
);
|
|
841
877
|
}
|
|
842
878
|
|
package/lib/index.js
CHANGED
|
@@ -140,6 +140,8 @@ function makeRoutes(engine, config, stream) {
|
|
|
140
140
|
const info = await engine.info({ directory })
|
|
141
141
|
// 0.10.0: Android's device view is the Live device stream, not serve-avd.
|
|
142
142
|
// The panel's pill shows attached-device count instead of a server state.
|
|
143
|
+
// 0.11.0: os lets the client hide the iOS card on non-mac hosts.
|
|
144
|
+
info.os = process.platform
|
|
143
145
|
try { info.deviceCount = (await DeviceBuild.devices()).length } catch { /* no adb: pill stays off */ }
|
|
144
146
|
writeJson(res, 200, info)
|
|
145
147
|
} catch (error) {
|
|
@@ -502,16 +504,19 @@ function makeRoutes(engine, config, stream) {
|
|
|
502
504
|
if (!fence(req, res, true) || !isPost(req, res)) return
|
|
503
505
|
await readBody(req, res)
|
|
504
506
|
try {
|
|
505
|
-
const [
|
|
507
|
+
const [rows, avds] = await Promise.all([
|
|
506
508
|
stream.host.listDevices(),
|
|
507
509
|
DeviceBuild.androidAvds().catch(() => []),
|
|
508
510
|
])
|
|
509
511
|
const streaming = new Set(stream.host.runningSerials())
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
512
|
+
// Attach the AVD name to online emulators so the picker can hide them
|
|
513
|
+
// from the boot list and label its rows (0.11.0).
|
|
514
|
+
const devices = await Promise.all(rows.map(async (device) => ({
|
|
515
|
+
...device,
|
|
516
|
+
...(streaming.has(device.serial) ? { streaming: true } : {}),
|
|
517
|
+
...(device.serial.startsWith('emulator-') ? { avd: await DeviceBuild.avdName(device.serial).catch(() => undefined) } : {}),
|
|
518
|
+
})))
|
|
519
|
+
writeJson(res, 200, { ok: true, avds, devices })
|
|
515
520
|
} catch (error) {
|
|
516
521
|
writeJson(res, 503, { code: 'devices_unavailable', error: error instanceof Error ? error.message : String(error) })
|
|
517
522
|
}
|
|
@@ -570,6 +575,30 @@ function makeRoutes(engine, config, stream) {
|
|
|
570
575
|
}
|
|
571
576
|
},
|
|
572
577
|
},
|
|
578
|
+
// POST /api/dsh-mobilecode/off {serial} — the picker's ⏻ off button (0.11.0):
|
|
579
|
+
// graceful `adb emu kill` for a RUNNING emulator. Physical devices are
|
|
580
|
+
// refused (adb has no power-off verb for phones); starting is /boot's job.
|
|
581
|
+
{
|
|
582
|
+
kind: 'exact',
|
|
583
|
+
path: API_BASE + '/off',
|
|
584
|
+
handler: async (req, res) => {
|
|
585
|
+
if (!fence(req, res, true) || !isPost(req, res)) return
|
|
586
|
+
const body = await readBody(req, res)
|
|
587
|
+
if (body === undefined) return
|
|
588
|
+
const serial = String(body.serial ?? '').trim()
|
|
589
|
+
if (serial === '' || !/^[\w][\w.:-]{0,63}$/.test(serial)) { writeJson(res, 400, { error: 'serial must be an attached device serial' }); return }
|
|
590
|
+
try {
|
|
591
|
+
const rows = await DeviceBuild.devices()
|
|
592
|
+
const row = rows.find((item) => item.serial === serial && item.state === 'device')
|
|
593
|
+
if (!row) { writeJson(res, 404, { error: `${serial} is not online` }); return }
|
|
594
|
+
if (!serial.startsWith('emulator-')) { writeJson(res, 400, { error: 'only emulators can be powered off here — a physical device keeps its own power button' }); return }
|
|
595
|
+
await DeviceBuild.adbRun(serial, ['emu', 'kill'])
|
|
596
|
+
writeJson(res, 200, { ok: true, serial, killed: true })
|
|
597
|
+
} catch (error) {
|
|
598
|
+
writeJson(res, 500, { error: error instanceof Error ? error.message : String(error) })
|
|
599
|
+
}
|
|
600
|
+
},
|
|
601
|
+
},
|
|
573
602
|
// POST /api/dsh-mobilecode/stream/still {device?} — capture ONE fresh
|
|
574
603
|
// screencap PNG and serve it; returns {path, width, height, dataUrl}. The
|
|
575
604
|
// panel's Screenshot button uses this (a still is the current pixels, unlike
|
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.
|
|
4
|
+
"version": "0.11.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.22.0",
|
|
7
7
|
"engines": {
|