portholejs 0.1.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.
Files changed (116) hide show
  1. package/client-dist/assets/index-C_qSS_Gy.js +49 -0
  2. package/client-dist/index.html +12 -0
  3. package/dist/adb-actions.d.ts +4 -0
  4. package/dist/adb-actions.d.ts.map +1 -0
  5. package/dist/adb-actions.js +40 -0
  6. package/dist/adb-actions.js.map +1 -0
  7. package/dist/cli-errors.d.ts +5 -0
  8. package/dist/cli-errors.d.ts.map +1 -0
  9. package/dist/cli-errors.js +19 -0
  10. package/dist/cli-errors.js.map +1 -0
  11. package/dist/cli.d.ts +3 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +542 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/control-client.d.ts +22 -0
  16. package/dist/control-client.d.ts.map +1 -0
  17. package/dist/control-client.js +100 -0
  18. package/dist/control-client.js.map +1 -0
  19. package/dist/crashes.d.ts +8 -0
  20. package/dist/crashes.d.ts.map +1 -0
  21. package/dist/crashes.js +38 -0
  22. package/dist/crashes.js.map +1 -0
  23. package/dist/device-manager.d.ts +29 -0
  24. package/dist/device-manager.d.ts.map +1 -0
  25. package/dist/device-manager.js +213 -0
  26. package/dist/device-manager.js.map +1 -0
  27. package/dist/doctor.d.ts +8 -0
  28. package/dist/doctor.d.ts.map +1 -0
  29. package/dist/doctor.js +115 -0
  30. package/dist/doctor.js.map +1 -0
  31. package/dist/engine/scrcpy-engine.d.ts +38 -0
  32. package/dist/engine/scrcpy-engine.d.ts.map +1 -0
  33. package/dist/engine/scrcpy-engine.js +216 -0
  34. package/dist/engine/scrcpy-engine.js.map +1 -0
  35. package/dist/engine/types.d.ts +26 -0
  36. package/dist/engine/types.d.ts.map +1 -0
  37. package/dist/engine/types.js +2 -0
  38. package/dist/engine/types.js.map +1 -0
  39. package/dist/index.d.ts +14 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +9 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/input-validation.d.ts +5 -0
  44. package/dist/input-validation.d.ts.map +1 -0
  45. package/dist/input-validation.js +62 -0
  46. package/dist/input-validation.js.map +1 -0
  47. package/dist/input.d.ts +22 -0
  48. package/dist/input.d.ts.map +1 -0
  49. package/dist/input.js +2 -0
  50. package/dist/input.js.map +1 -0
  51. package/dist/keycodes.d.ts +19 -0
  52. package/dist/keycodes.d.ts.map +1 -0
  53. package/dist/keycodes.js +32 -0
  54. package/dist/keycodes.js.map +1 -0
  55. package/dist/log.d.ts +3 -0
  56. package/dist/log.d.ts.map +1 -0
  57. package/dist/log.js +10 -0
  58. package/dist/log.js.map +1 -0
  59. package/dist/mcp/server.d.ts +2 -0
  60. package/dist/mcp/server.d.ts.map +1 -0
  61. package/dist/mcp/server.js +401 -0
  62. package/dist/mcp/server.js.map +1 -0
  63. package/dist/middleware.d.ts +13 -0
  64. package/dist/middleware.d.ts.map +1 -0
  65. package/dist/middleware.js +31 -0
  66. package/dist/middleware.js.map +1 -0
  67. package/dist/paths.d.ts +5 -0
  68. package/dist/paths.d.ts.map +1 -0
  69. package/dist/paths.js +27 -0
  70. package/dist/paths.js.map +1 -0
  71. package/dist/port-check.d.ts +5 -0
  72. package/dist/port-check.d.ts.map +1 -0
  73. package/dist/port-check.js +27 -0
  74. package/dist/port-check.js.map +1 -0
  75. package/dist/profiles.d.ts +4 -0
  76. package/dist/profiles.d.ts.map +1 -0
  77. package/dist/profiles.js +25 -0
  78. package/dist/profiles.js.map +1 -0
  79. package/dist/protocol.d.ts +10 -0
  80. package/dist/protocol.d.ts.map +1 -0
  81. package/dist/protocol.js +27 -0
  82. package/dist/protocol.js.map +1 -0
  83. package/dist/server/frame-convert.d.ts +6 -0
  84. package/dist/server/frame-convert.d.ts.map +1 -0
  85. package/dist/server/frame-convert.js +33 -0
  86. package/dist/server/frame-convert.js.map +1 -0
  87. package/dist/server/http.d.ts +20 -0
  88. package/dist/server/http.d.ts.map +1 -0
  89. package/dist/server/http.js +509 -0
  90. package/dist/server/http.js.map +1 -0
  91. package/dist/server/mjpeg.d.ts +21 -0
  92. package/dist/server/mjpeg.d.ts.map +1 -0
  93. package/dist/server/mjpeg.js +92 -0
  94. package/dist/server/mjpeg.js.map +1 -0
  95. package/dist/server/ws.d.ts +15 -0
  96. package/dist/server/ws.d.ts.map +1 -0
  97. package/dist/server/ws.js +102 -0
  98. package/dist/server/ws.js.map +1 -0
  99. package/dist/session.d.ts +45 -0
  100. package/dist/session.d.ts.map +1 -0
  101. package/dist/session.js +188 -0
  102. package/dist/session.js.map +1 -0
  103. package/dist/state.d.ts +27 -0
  104. package/dist/state.d.ts.map +1 -0
  105. package/dist/state.js +70 -0
  106. package/dist/state.js.map +1 -0
  107. package/dist/target-resolution.d.ts +14 -0
  108. package/dist/target-resolution.d.ts.map +1 -0
  109. package/dist/target-resolution.js +37 -0
  110. package/dist/target-resolution.js.map +1 -0
  111. package/dist/ui-tree.d.ts +47 -0
  112. package/dist/ui-tree.d.ts.map +1 -0
  113. package/dist/ui-tree.js +179 -0
  114. package/dist/ui-tree.js.map +1 -0
  115. package/package.json +63 -0
  116. package/scripts/download-scrcpy-server.mjs +67 -0
@@ -0,0 +1,47 @@
1
+ export interface UiBounds {
2
+ l: number;
3
+ t: number;
4
+ r: number;
5
+ b: number;
6
+ }
7
+ export interface UiNode {
8
+ class: string;
9
+ text: string;
10
+ resourceId: string;
11
+ contentDesc: string;
12
+ bounds: UiBounds;
13
+ focused: boolean;
14
+ focusable: boolean;
15
+ clickable: boolean;
16
+ enabled: boolean;
17
+ children: UiNode[];
18
+ }
19
+ export interface UiMatch extends UiNode {
20
+ center: {
21
+ x: number;
22
+ y: number;
23
+ };
24
+ normalizedCenter: {
25
+ x: number;
26
+ y: number;
27
+ } | null;
28
+ }
29
+ export declare function dumpUi(serial: string, filter?: string): Promise<UiNode[]>;
30
+ export declare function getFocusedNode(serial: string): Promise<UiNode | null>;
31
+ export declare function findElement(serial: string, query: {
32
+ text?: string;
33
+ resourceId?: string;
34
+ }): Promise<UiMatch | null>;
35
+ export declare function findInTree(tree: UiNode[], query: {
36
+ text?: string;
37
+ resourceId?: string;
38
+ }): UiMatch | null;
39
+ export declare function displaySize(tree: UiNode[]): {
40
+ width: number;
41
+ height: number;
42
+ };
43
+ export declare function waitForUiText(serial: string, text: string, timeoutMs: number): Promise<UiNode>;
44
+ export declare function parseUiAutomatorXml(xml: string): UiNode[];
45
+ export declare function findFocused(nodes: UiNode[]): UiNode | null;
46
+ export declare function filterTree(nodes: UiNode[], query: string): UiNode[];
47
+ //# sourceMappingURL=ui-tree.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui-tree.d.ts","sourceRoot":"","sources":["../src/ui-tree.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,QAAQ;IACvB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,MAAM;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,QAAQ,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,OAAQ,SAAQ,MAAM;IACrC,MAAM,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAIjC,gBAAgB,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CACnD;AAED,wBAAsB,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAI/E;AAED,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAE3E;AAED,wBAAsB,WAAW,CAC/B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5C,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAEzB;AAED,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EAAE,EACd,KAAK,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5C,OAAO,GAAG,IAAI,CAShB;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAQ7E;AAED,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC,CASjB;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAoCzD;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAO1D;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAUnE"}
@@ -0,0 +1,179 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { adbBin, findAndroidSdk } from "./device-manager.js";
4
+ const execFileAsync = promisify(execFile);
5
+ export async function dumpUi(serial, filter) {
6
+ const xml = await dumpUiXml(serial);
7
+ const tree = parseUiAutomatorXml(xml);
8
+ return filter ? filterTree(tree, filter) : tree;
9
+ }
10
+ export async function getFocusedNode(serial) {
11
+ return findFocused(parseUiAutomatorXml(await dumpUiXml(serial)));
12
+ }
13
+ export async function findElement(serial, query) {
14
+ return findInTree(parseUiAutomatorXml(await dumpUiXml(serial)), query);
15
+ }
16
+ export function findInTree(tree, query) {
17
+ const node = flatten(tree).find((candidate) => {
18
+ if (query.text && !candidate.text.includes(query.text))
19
+ return false;
20
+ if (query.resourceId && !candidate.resourceId.includes(query.resourceId)) {
21
+ return false;
22
+ }
23
+ return true;
24
+ });
25
+ return node ? withCenter(node, displaySize(tree)) : null;
26
+ }
27
+ export function displaySize(tree) {
28
+ let width = 0;
29
+ let height = 0;
30
+ for (const node of flatten(tree)) {
31
+ if (node.bounds.r > width)
32
+ width = node.bounds.r;
33
+ if (node.bounds.b > height)
34
+ height = node.bounds.b;
35
+ }
36
+ return { width, height };
37
+ }
38
+ export async function waitForUiText(serial, text, timeoutMs) {
39
+ const start = Date.now();
40
+ while (Date.now() - start < timeoutMs) {
41
+ const tree = parseUiAutomatorXml(await dumpUiXml(serial));
42
+ const found = flatten(tree).find((node) => node.text.includes(text));
43
+ if (found)
44
+ return found;
45
+ await new Promise((resolve) => setTimeout(resolve, 500));
46
+ }
47
+ throw new Error(`Timed out waiting for text: ${text}`);
48
+ }
49
+ export function parseUiAutomatorXml(xml) {
50
+ const nodes = [];
51
+ const stack = [];
52
+ const tagRe = /<\/node>|<node\b([^>]*?)(\/?)>/g;
53
+ let match;
54
+ while ((match = tagRe.exec(xml)) !== null) {
55
+ if (match[0] === "</node>") {
56
+ stack.pop();
57
+ continue;
58
+ }
59
+ const attrs = parseAttributes(match[1] ?? "");
60
+ const node = {
61
+ class: attrs.get("class") ?? "",
62
+ text: attrs.get("text") ?? "",
63
+ resourceId: attrs.get("resource-id") ?? "",
64
+ contentDesc: attrs.get("content-desc") ?? "",
65
+ bounds: parseBounds(attrs.get("bounds") ?? ""),
66
+ focused: attrs.get("focused") === "true",
67
+ focusable: attrs.get("focusable") === "true",
68
+ clickable: attrs.get("clickable") === "true",
69
+ enabled: attrs.get("enabled") !== "false",
70
+ children: [],
71
+ };
72
+ const parent = stack.at(-1);
73
+ if (parent)
74
+ parent.children.push(node);
75
+ else
76
+ nodes.push(node);
77
+ if (match[2] !== "/") {
78
+ stack.push(node);
79
+ }
80
+ }
81
+ return nodes;
82
+ }
83
+ export function findFocused(nodes) {
84
+ for (const node of nodes) {
85
+ if (node.focused)
86
+ return node;
87
+ const child = findFocused(node.children);
88
+ if (child)
89
+ return child;
90
+ }
91
+ return null;
92
+ }
93
+ export function filterTree(nodes, query) {
94
+ const needle = query.toLowerCase();
95
+ return nodes.flatMap((node) => {
96
+ const children = filterTree(node.children, query);
97
+ const selfMatches = node.text.toLowerCase().includes(needle) ||
98
+ node.resourceId.toLowerCase().includes(needle) ||
99
+ node.contentDesc.toLowerCase().includes(needle);
100
+ return selfMatches || children.length > 0 ? [{ ...node, children }] : [];
101
+ });
102
+ }
103
+ function flatten(nodes) {
104
+ return nodes.flatMap((node) => [node, ...flatten(node.children)]);
105
+ }
106
+ function withCenter(node, display) {
107
+ const center = {
108
+ x: (node.bounds.l + node.bounds.r) / 2,
109
+ y: (node.bounds.t + node.bounds.b) / 2,
110
+ };
111
+ const normalizedCenter = display.width > 0 && display.height > 0
112
+ ? { x: center.x / display.width, y: center.y / display.height }
113
+ : null;
114
+ return { ...node, center, normalizedCenter };
115
+ }
116
+ async function dumpUiXml(serial) {
117
+ for (let attempt = 0; attempt < 2; attempt++) {
118
+ const direct = await tryDumpDirect(serial);
119
+ if (direct.trim())
120
+ return direct;
121
+ await new Promise((resolve) => setTimeout(resolve, 300));
122
+ }
123
+ const adb = adbBin(findAndroidSdk());
124
+ await execFileAsync(adb, [
125
+ "-s",
126
+ serial,
127
+ "shell",
128
+ "uiautomator",
129
+ "dump",
130
+ "/sdcard/window_dump.xml",
131
+ ]);
132
+ const { stdout } = await execFileAsync(adb, [
133
+ "-s",
134
+ serial,
135
+ "exec-out",
136
+ "cat",
137
+ "/sdcard/window_dump.xml",
138
+ ]);
139
+ return stdout;
140
+ }
141
+ async function tryDumpDirect(serial) {
142
+ try {
143
+ const { stdout } = await execFileAsync(adbBin(findAndroidSdk()), ["-s", serial, "exec-out", "uiautomator", "dump", "/dev/tty"], { maxBuffer: 10 * 1024 * 1024 });
144
+ const start = stdout.indexOf("<?xml");
145
+ return start === -1 ? stdout : stdout.slice(start);
146
+ }
147
+ catch {
148
+ return "";
149
+ }
150
+ }
151
+ function parseAttributes(input) {
152
+ const attrs = new Map();
153
+ const attrRe = /([\w:-]+)="([^"]*)"/g;
154
+ let match;
155
+ while ((match = attrRe.exec(input)) !== null) {
156
+ attrs.set(match[1] ?? "", decodeXml(match[2] ?? ""));
157
+ }
158
+ return attrs;
159
+ }
160
+ function parseBounds(value) {
161
+ const match = /^\[(\d+),(\d+)\]\[(\d+),(\d+)\]$/.exec(value);
162
+ if (!match)
163
+ return { l: 0, t: 0, r: 0, b: 0 };
164
+ return {
165
+ l: Number(match[1]),
166
+ t: Number(match[2]),
167
+ r: Number(match[3]),
168
+ b: Number(match[4]),
169
+ };
170
+ }
171
+ function decodeXml(value) {
172
+ return value
173
+ .replaceAll("&quot;", '"')
174
+ .replaceAll("&apos;", "'")
175
+ .replaceAll("&lt;", "<")
176
+ .replaceAll("&gt;", ">")
177
+ .replaceAll("&amp;", "&");
178
+ }
179
+ //# sourceMappingURL=ui-tree.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui-tree.js","sourceRoot":"","sources":["../src/ui-tree.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "portholejs",
3
+ "version": "0.1.0",
4
+ "description": "A window into your Android emulator — stream and control AVDs from the browser",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "porthole": "dist/cli.js"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc && node scripts/copy-package-assets.mjs",
13
+ "typecheck": "tsc --noEmit",
14
+ "postinstall": "node scripts/download-scrcpy-server.mjs",
15
+ "prepublishOnly": "npm run build --prefix ../.."
16
+ },
17
+ "keywords": [
18
+ "android",
19
+ "emulator",
20
+ "android-tv",
21
+ "scrcpy",
22
+ "adb",
23
+ "avd",
24
+ "mcp",
25
+ "claude"
26
+ ],
27
+ "license": "Apache-2.0",
28
+ "author": "Giovanni Laquidara (https://github.com/giolaq)",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/giolaq/porthole.git"
32
+ },
33
+ "homepage": "https://github.com/giolaq/porthole#readme",
34
+ "bugs": {
35
+ "url": "https://github.com/giolaq/porthole/issues"
36
+ },
37
+ "files": [
38
+ "dist/",
39
+ "!dist/__tests__/",
40
+ "scripts/download-scrcpy-server.mjs",
41
+ "client-dist/"
42
+ ],
43
+ "engines": {
44
+ "node": ">=20"
45
+ },
46
+ "dependencies": {
47
+ "@modelcontextprotocol/sdk": "^1.29.0",
48
+ "@yume-chan/adb": "^2.6.0",
49
+ "@yume-chan/adb-scrcpy": "^2.3.2",
50
+ "@yume-chan/adb-server-node-tcp": "^2.5.2",
51
+ "@yume-chan/scrcpy": "^2.3.0",
52
+ "@yume-chan/stream-extra": "^2.5.3",
53
+ "commander": "^14.0.3",
54
+ "jpeg-js": "^0.4.4",
55
+ "pngjs": "^7.0.0",
56
+ "ws": "^8.21.0"
57
+ },
58
+ "devDependencies": {
59
+ "@types/node": "^25.9.1",
60
+ "@types/pngjs": "^6.0.5",
61
+ "@types/ws": "^8.18.1"
62
+ }
63
+ }
@@ -0,0 +1,67 @@
1
+ // Downloads the pinned scrcpy-server jar from the official Genymobile/scrcpy
2
+ // GitHub release at install time. The jar is NOT committed to the repository
3
+ // or shipped in the npm tarball; this script is the single source of truth
4
+ // for the pinned version and its checksum.
5
+ //
6
+ // Bump process: update SCRCPY_VERSION and SCRCPY_SHA256 together, then verify
7
+ // the engine's AdbScrcpyOptions class in scrcpy-engine.ts matches the new
8
+ // server's protocol version.
9
+ import { createHash } from "node:crypto";
10
+ import { existsSync } from "node:fs";
11
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
12
+ import { dirname, join, resolve } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+
15
+ export const SCRCPY_VERSION = "3.1";
16
+ export const SCRCPY_SHA256 =
17
+ "958f0944a62f23b1f33a16e9eb14844c1a04b882ca175a738c16d23cb22b86c0";
18
+ export const SCRCPY_URL = `https://github.com/Genymobile/scrcpy/releases/download/v${SCRCPY_VERSION}/scrcpy-server-v${SCRCPY_VERSION}`;
19
+
20
+ const here = dirname(fileURLToPath(import.meta.url));
21
+ const packageRoot = resolve(here, "..");
22
+ const targetPath = join(packageRoot, "assets", "scrcpy-server");
23
+
24
+ function sha256(buffer) {
25
+ return createHash("sha256").update(buffer).digest("hex");
26
+ }
27
+
28
+ async function alreadyPresent() {
29
+ if (!existsSync(targetPath)) return false;
30
+ return sha256(await readFile(targetPath)) === SCRCPY_SHA256;
31
+ }
32
+
33
+ async function download() {
34
+ const response = await fetch(SCRCPY_URL, { redirect: "follow" });
35
+ if (!response.ok) {
36
+ throw new Error(`HTTP ${response.status} fetching ${SCRCPY_URL}`);
37
+ }
38
+ const bytes = Buffer.from(await response.arrayBuffer());
39
+ const digest = sha256(bytes);
40
+ if (digest !== SCRCPY_SHA256) {
41
+ throw new Error(
42
+ `Checksum mismatch for scrcpy-server v${SCRCPY_VERSION}: expected ${SCRCPY_SHA256}, got ${digest}`,
43
+ );
44
+ }
45
+ await mkdir(dirname(targetPath), { recursive: true });
46
+ const tmpPath = `${targetPath}.download-${process.pid}`;
47
+ await writeFile(tmpPath, bytes);
48
+ await rename(tmpPath, targetPath);
49
+ }
50
+
51
+ try {
52
+ if (await alreadyPresent()) {
53
+ process.exit(0);
54
+ }
55
+ await download();
56
+ console.log(`porthole: downloaded scrcpy-server v${SCRCPY_VERSION} (sha256 verified)`);
57
+ } catch (error) {
58
+ const message = error instanceof Error ? error.message : String(error);
59
+ console.warn(
60
+ `porthole: could not download scrcpy-server v${SCRCPY_VERSION}: ${message}\n` +
61
+ `porthole: streaming will not work until it is fetched. Re-run:\n` +
62
+ `porthole: node ${join("scripts", "download-scrcpy-server.mjs")} (from the portholejs package directory)`,
63
+ );
64
+ // Do not fail `npm install` on offline machines; the CLI and doctor
65
+ // report the missing asset with the same fix instructions at runtime.
66
+ process.exit(0);
67
+ }