lg-dev-manager-bridge 1.0.1 → 1.0.3

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
@@ -24,20 +24,52 @@ A web rebuild of [webosbrew/dev-manager-desktop](https://github.com/webosbrew/de
24
24
 
25
25
  ## 🚀 Get started
26
26
 
27
- **You need [Node.js](https://nodejs.org) 22.12 or newer** (any current LTS — 22 or 24). The same version works
28
- for npx and for running from the source code.
27
+ The app needs a small helper on your computer, **the bridge** (why: [How it works](#how-it-works)). There are three
28
+ ways to run it. It's the same bridge every time, so pick whichever suits you:
29
29
 
30
- 1. Install Node.js if you don't have it (`node --version` shows yours).
31
- 2. In a terminal, start the bridge:
30
+ | | Needs | How |
31
+ |---|---|---|
32
+ | 🖥️ **The app** | nothing | Download it from the [latest release](https://github.com/Scoty/lg-dev-manager/releases/latest) and open it |
33
+ | 📦 **npx** | Node.js 22.12+ | `npx lg-dev-manager-bridge@latest` in a terminal |
34
+ | 🛠️ **From the source code** | Node.js 22.12+ | [Clone, build and run](#run-from-source) |
32
35
 
33
- ```bash
34
- npx lg-dev-manager-bridge@latest
35
- ```
36
+ ### 🖥️ Option 1: the app — no Node.js needed
36
37
 
37
- It prints a **pairing token**. Keep the terminal open while you use the app (Ctrl+C stops it).
38
- 3. Open **https://lg.scoty.uk**, or the local page the bridge serves at **http://localhost:5199** — they are the
38
+ One file with everything inside. Download the one for your computer from the
39
+ [latest release](https://github.com/Scoty/lg-dev-manager/releases/latest):
40
+
41
+ | Computer | File |
42
+ |---|---|
43
+ | 🪟 Windows | `lg-dev-manager-bridge-<version>-windows-x64.exe` — just run it (also works on Windows on ARM) |
44
+ | 🍎 Mac with Apple Silicon (M1 and newer) | `…-macos-arm64.dmg` — open it, then double-click `lg-dev-manager-bridge` |
45
+ | 🍎 Mac with Intel | `…-macos-x64.dmg` |
46
+ | 🐧 Linux (x64 / ARM64) | `…-linux-x64.tar.gz` / `…-linux-arm64.tar.gz` — `tar -xzf …`, then `./lg-dev-manager-bridge` |
47
+
48
+ A window opens with the **pairing token**, and https://lg.scoty.uk opens in your browser. Keep the window open while you
49
+ use the app; closing it stops the bridge.
50
+
51
+ > 🔐 **The first time:** the app isn't signed by Apple or Microsoft (that costs a yearly fee), so your computer asks
52
+ > before running it. **Windows**: "Windows protected your PC" → **More info** → **Run anyway**. **macOS**: "Apple
53
+ > could not verify…" → **Done**, then **System Settings → Privacy & Security** → **Open Anyway**. You only do this
54
+ > once. It's the same code as the npm package, built and tested by GitHub Actions in public. Each release lists
55
+ > SHA-256 checksums.
56
+
57
+ ### 📦 Option 2: npx
58
+
59
+ **You need [Node.js](https://nodejs.org) 22.12 or newer** (any current LTS — 22 or 24). `node --version` shows
60
+ yours. In a terminal:
61
+
62
+ ```bash
63
+ npx lg-dev-manager-bridge@latest
64
+ ```
65
+
66
+ It prints a **pairing token**. Keep the terminal open while you use the app (Ctrl+C stops it).
67
+
68
+ ### 🔗 Then, whichever way you chose
69
+
70
+ 1. Open **https://lg.scoty.uk**, or the local page the bridge serves at **http://localhost:5199** — they are the
39
71
  same app. On the **Bridge** page, paste the token.
40
- 4. On **Devices → Add a TV**, pick your TV from the scan. Rooted TVs log in as root (Homebrew Channel's SSH server);
72
+ 2. On **Devices → Add a TV**, pick your TV from the scan. Rooted TVs log in as root (Homebrew Channel's SSH server);
41
73
  other TVs use the Developer Mode app and its passphrase.
42
74
 
43
75
  🍎 On macOS, the first time the bridge looks for your TV, the system asks whether your terminal may find devices on the
@@ -49,6 +81,8 @@ bridge. 🏠 **http://localhost:5199** is served from your computer by the bridg
49
81
  website can't be reached. Each keeps its own list of TVs (browsers store them per site); use **Export backup** / **Import backup** on
50
82
  the Devices page to move them.
51
83
 
84
+ <a id="how-it-works"></a>
85
+
52
86
  ## 🔧 How it works
53
87
 
54
88
  Browsers can't open SSH connections, so the app comes in two parts:
@@ -57,7 +91,7 @@ Browsers can't open SSH connections, so the app comes in two parts:
57
91
  Browser (lg.scoty.uk or localhost:5199) Your computer Your TV
58
92
  ┌──────────────────────────────────┐ WebSocket ┌────────────────────┐ SSH / SFTP ┌──────────────┐
59
93
  │ Web app — your TVs, keys and │ ────────────▶ │ Bridge │ ─────────────▶ │ webOS │
60
- │ passwords are stored only here │ 127.0.0.1 │ (npx …, Node.js) │ │ (port 22 or │
94
+ │ passwords are stored only here │ 127.0.0.1 │ (app or npx) │ │ (port 22 or │
61
95
  └──────────────────────────────────┘ + token └────────────────────┘ │ 9922) │
62
96
  └──────────────┘
63
97
  ```
@@ -71,8 +105,9 @@ Browsers can't open SSH connections, so the app comes in two parts:
71
105
  - 📦 **Homebrew repository and Litefin releases** are fetched by the bridge, so the site itself talks to no one but your
72
106
  bridge. Downloads are checked against their published SHA-256 whenever one is published.
73
107
 
74
- 🔄 The bridge tells the site its version; when a newer one is out, the app says so. Update by running
75
- `npx lg-dev-manager-bridge@latest` again.
108
+ 🔄 The bridge tells the site its version; when a newer one is out, the app says so and how to update: download the
109
+ new app from the [releases](https://github.com/Scoty/lg-dev-manager/releases/latest), or run
110
+ `npx lg-dev-manager-bridge@latest` again. Your pairing and TVs stay as they are.
76
111
 
77
112
  ## ✨ What it does
78
113
 
@@ -109,9 +144,11 @@ downloading the right IPK by hand and installing it through the desktop webOS De
109
144
  — through Homebrew Channel on a rooted TV, or the Developer Mode installer otherwise. All builds are the same app,
110
145
  so switching builds or versions simply replaces it.
111
146
 
147
+ <a id="run-from-source"></a>
148
+
112
149
  ## 🛠️ Run the bridge from the source code
113
150
 
114
- Prefer not to use npx? Run it from a clone — it's the same bridge, and it serves the local page too:
151
+ The third way: run it from a clone. It's the same bridge, and it serves the local page too:
115
152
 
116
153
  ```bash
117
154
  git clone https://github.com/Scoty/lg-dev-manager.git
@@ -125,7 +162,7 @@ pnpm bridge # same as: node apps/bridge/dist/cli.js
125
162
  This needs Node.js 22.12 or newer, the same as npx (the project itself is developed on Node 24, see `.nvmrc`).
126
163
  To update: `git pull`, `pnpm install`, `pnpm build`, then `pnpm bridge`.
127
164
 
128
- Bridge options (`npx lg-dev-manager-bridge --help`):
165
+ Bridge options (`--help`; with the app, run it from a terminal: `lg-dev-manager-bridge --help`):
129
166
 
130
167
  | Option | |
131
168
  |---|---|
@@ -133,6 +170,8 @@ Bridge options (`npx lg-dev-manager-bridge --help`):
133
170
  | `--no-ui` | Don't serve the local page |
134
171
  | `--allow-origin <url>` | Also accept another origin, e.g. a self-hosted copy of the web app |
135
172
  | `--reset-token` | Make a new pairing token (un-pairs every browser) |
173
+ | `--no-open` | The app only: don't open lg.scoty.uk in the browser when it starts |
174
+ | `--license` | Show the licenses of the bridge and everything it includes |
136
175
 
137
176
  ## 🧪 Development
138
177
 
@@ -143,11 +182,18 @@ pnpm test # unit and integration tests (against a mock TV)
143
182
  pnpm e2e # Playwright: built app + real bridge + mock TVs + fake Homebrew repo
144
183
  pnpm dev:rig # the same rig to try by hand: http://127.0.0.1:5299, token e2e-token
145
184
  pnpm lint && pnpm typecheck
185
+ pnpm app # the standalone app for this computer → apps/bridge/release (after pnpm build)
146
186
  ```
147
187
 
148
188
  `tools/mock-tv` fakes webOS TVs (SSH, luna-send, key server, logs) so everything can be tested without a TV.
149
189
  Contributors, human or AI: read [AGENTS.md](https://github.com/Scoty/lg-dev-manager/blob/main/AGENTS.md) and [PLAN.md](https://github.com/Scoty/lg-dev-manager/blob/main/PLAN.md) first.
150
190
 
191
+ ## 🙋 Problems and ideas
192
+
193
+ [Open an issue](https://github.com/Scoty/lg-dev-manager/issues/new/choose): there are forms for bugs, connection
194
+ problems and feature requests that ask for what helps track a problem down. Security problems: please report them
195
+ privately (see the [security policy](https://github.com/Scoty/lg-dev-manager/security/policy)).
196
+
151
197
  ## 📄 License
152
198
 
153
199
  [Apache-2.0](https://github.com/Scoty/lg-dev-manager/blob/main/LICENSE). You may use, change and share the app and its code freely, including commercially, as long
package/dist/cli.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/config.ts
4
- import { randomBytes } from "crypto";
5
- import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
6
- import { fileURLToPath } from "url";
7
- import { homedir } from "os";
8
- import { join } from "path";
4
+ import { randomBytes } from "node:crypto";
5
+ import { chmodSync, existsSync as existsSync2, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
6
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
7
+ import { homedir } from "node:os";
8
+ import { join } from "node:path";
9
9
 
10
10
  // ../../packages/protocol/src/envelope.ts
11
11
  import { z } from "zod";
@@ -346,7 +346,9 @@ var Methods = {
346
346
  result: z9.object({
347
347
  protocolVersion: z9.number().int(),
348
348
  bridgeVersion: z9.string(),
349
- platform: z9.string()
349
+ platform: z9.string(),
350
+ /** How the bridge was installed, so the site can say how to update it (bridges before 1.0.2 don't say). */
351
+ distribution: z9.enum(["app", "npm", "source"]).optional()
350
352
  })
351
353
  },
352
354
  "system.ping": {
@@ -737,6 +739,54 @@ var UNAUTHENTICATED_METHODS = ["system.hello"];
737
739
  var PUBLIC_WEB_ORIGIN = "https://lg.scoty.uk";
738
740
  var DEFAULT_ALLOWED_ORIGINS = [PUBLIC_WEB_ORIGIN];
739
741
  var DEV_ALLOWED_ORIGINS = ["http://localhost:5173", "http://127.0.0.1:5173"];
742
+ var REPO_URL = "https://github.com/Scoty/lg-dev-manager";
743
+ var RELEASES_URL = `${REPO_URL}/releases/latest`;
744
+
745
+ // src/standalone.ts
746
+ import { existsSync } from "node:fs";
747
+ import { fileURLToPath } from "node:url";
748
+ import * as sea from "node:sea";
749
+ function isStandalone() {
750
+ try {
751
+ return sea.isSea();
752
+ } catch {
753
+ return false;
754
+ }
755
+ }
756
+ function distribution() {
757
+ if (isStandalone()) return "app";
758
+ try {
759
+ return existsSync(fileURLToPath(new URL("../src/", import.meta.url))) ? "source" : "npm";
760
+ } catch {
761
+ return "npm";
762
+ }
763
+ }
764
+ var WEB_MANIFEST_ASSET = "web-manifest.json";
765
+ function standaloneUi() {
766
+ if (!isStandalone()) return void 0;
767
+ try {
768
+ const names = JSON.parse(sea.getAsset(WEB_MANIFEST_ASSET, "utf8"));
769
+ if (!Array.isArray(names)) return void 0;
770
+ const files = /* @__PURE__ */ new Map();
771
+ for (const name of names) {
772
+ if (typeof name === "string") files.set(name, new Uint8Array(sea.getAsset(`web/${name}`)));
773
+ }
774
+ return files.has("index.html") ? files : void 0;
775
+ } catch {
776
+ return void 0;
777
+ }
778
+ }
779
+ function licenseText() {
780
+ if (!isStandalone()) {
781
+ return `Apache-2.0. See the LICENSE and NOTICE files next to this program, or https://github.com/Scoty/lg-dev-manager/blob/main/LICENSE
782
+ `;
783
+ }
784
+ const rule = `
785
+ ${"=".repeat(78)}
786
+
787
+ `;
788
+ return ["LICENSE", "NOTICE", "THIRD-PARTY-LICENSES.txt"].map((f) => sea.getAsset(f, "utf8").trim()).join(rule) + "\n";
789
+ }
740
790
 
741
791
  // src/config.ts
742
792
  var stateDir = () => process.env.LGDM_STATE_DIR ?? join(homedir(), ".lg-dev-manager");
@@ -769,9 +819,14 @@ function loadOrCreateToken(reset = false) {
769
819
  return token;
770
820
  }
771
821
  function bundledUi() {
822
+ const packed = standaloneUi();
823
+ if (packed) return packed;
772
824
  for (const rel of ["../web/", "../../web/dist/"]) {
773
- const dir = fileURLToPath(new URL(rel, import.meta.url));
774
- if (existsSync(join(dir, "index.html"))) return dir;
825
+ try {
826
+ const dir = fileURLToPath2(new URL(rel, import.meta.url));
827
+ if (existsSync2(join(dir, "index.html"))) return dir;
828
+ } catch {
829
+ }
775
830
  }
776
831
  return void 0;
777
832
  }
@@ -813,7 +868,7 @@ function parseArgs(argv) {
813
868
  }
814
869
 
815
870
  // src/server.ts
816
- import { createServer } from "http";
871
+ import { createServer } from "node:http";
817
872
  import { WebSocketServer } from "ws";
818
873
 
819
874
  // src/rpc/errors.ts
@@ -831,16 +886,16 @@ var RpcError = class extends Error {
831
886
  };
832
887
 
833
888
  // src/rpc/handlers.ts
834
- import { StringDecoder as StringDecoder3 } from "string_decoder";
835
- import { timingSafeEqual } from "crypto";
836
- import { platform } from "os";
837
- import { randomUUID as randomUUID2 } from "crypto";
889
+ import { StringDecoder as StringDecoder3 } from "node:string_decoder";
890
+ import { timingSafeEqual } from "node:crypto";
891
+ import { platform } from "node:os";
892
+ import { randomUUID as randomUUID2 } from "node:crypto";
838
893
 
839
894
  // src/version.ts
840
- var BRIDGE_VERSION = "1.0.1";
895
+ var BRIDGE_VERSION = "1.0.3";
841
896
 
842
897
  // src/ssh/logged.ts
843
- import { randomUUID } from "crypto";
898
+ import { randomUUID } from "node:crypto";
844
899
  var MAX_LOG_OUTPUT = 8 * 1024;
845
900
  var targetOf = (t) => `${t.username}@${t.host}:${t.port}`;
846
901
  function summarize(stdout, stderr) {
@@ -945,16 +1000,16 @@ var LoggedSsh = class {
945
1000
  };
946
1001
 
947
1002
  // src/devices/scan.ts
948
- import { createSocket } from "dgram";
949
- import { request } from "http";
950
- import { networkInterfaces } from "os";
1003
+ import { createSocket } from "node:dgram";
1004
+ import { request } from "node:http";
1005
+ import { networkInterfaces } from "node:os";
951
1006
 
952
1007
  // src/devices/ports.ts
953
- import { connect as connect2 } from "net";
1008
+ import { connect as connect2 } from "node:net";
954
1009
 
955
1010
  // src/devices/webos.ts
956
1011
  import WebSocket from "ws";
957
- import { connect } from "net";
1012
+ import { connect } from "node:net";
958
1013
  var PROBE_TIMEOUT_MS = 1500;
959
1014
  function probeSsap(host, port, secure, timeoutMs = PROBE_TIMEOUT_MS) {
960
1015
  return new Promise((resolve2) => {
@@ -1218,7 +1273,7 @@ async function scanNetwork(opts = {}) {
1218
1273
  }
1219
1274
 
1220
1275
  // src/ssh/luna.ts
1221
- import { StringDecoder } from "string_decoder";
1276
+ import { StringDecoder } from "node:string_decoder";
1222
1277
  function shellQuote(value) {
1223
1278
  return value.split("'").map((s) => `'${s}'`).join("\\'");
1224
1279
  }
@@ -1339,7 +1394,7 @@ async function lunaSubscribe(pool, device, uri, params, onMessage, opts = {}) {
1339
1394
  }
1340
1395
 
1341
1396
  // src/devices/keys.ts
1342
- import { createHash } from "crypto";
1397
+ import { createHash } from "node:crypto";
1343
1398
  import ssh2 from "ssh2";
1344
1399
  var { utils } = ssh2;
1345
1400
  function verifyKey(privateKey, passphrase) {
@@ -1360,7 +1415,7 @@ function verifyKey(privateKey, passphrase) {
1360
1415
  }
1361
1416
 
1362
1417
  // src/devices/keyserver.ts
1363
- import { request as request2 } from "http";
1418
+ import { request as request2 } from "node:http";
1364
1419
  var MAX_KEY_RESPONSE = 65536;
1365
1420
  var TIMEOUT_MS = 1e4;
1366
1421
  var MAX_REDIRECTS = 5;
@@ -1440,7 +1495,7 @@ async function fetchKey(host, port = KEY_SERVER_PORT) {
1440
1495
  }
1441
1496
 
1442
1497
  // src/devices/info.ts
1443
- import { createHash as createHash2 } from "crypto";
1498
+ import { createHash as createHash2 } from "node:crypto";
1444
1499
  import ssh22 from "ssh2";
1445
1500
 
1446
1501
  // src/ssh/transfer.ts
@@ -1723,8 +1778,8 @@ async function renewDevMode(pool, device) {
1723
1778
  }
1724
1779
 
1725
1780
  // src/apps/apps.ts
1726
- import { createHash as createHash3 } from "crypto";
1727
- import { posix } from "path";
1781
+ import { createHash as createHash3 } from "node:crypto";
1782
+ import { posix } from "node:path";
1728
1783
 
1729
1784
  // src/ssh/serve.ts
1730
1785
  var HEADER_LIMIT = 16 * 1024;
@@ -1813,7 +1868,7 @@ Connection: close\r
1813
1868
  }
1814
1869
 
1815
1870
  // src/apps/ipk.ts
1816
- import { gunzipSync } from "zlib";
1871
+ import { gunzipSync } from "node:zlib";
1817
1872
  function readIpkControl(ipk) {
1818
1873
  try {
1819
1874
  const control = arMember(ipk, /^control\.tar\.gz\/?$/);
@@ -2119,7 +2174,7 @@ async function installDownloadable(pool, device, pkg, download, progress) {
2119
2174
  }
2120
2175
 
2121
2176
  // src/files/files.ts
2122
- import { posix as posix2 } from "path";
2177
+ import { posix as posix2 } from "node:path";
2123
2178
  var S_IFMT = 61440;
2124
2179
  var TYPES = {
2125
2180
  16384: "d",
@@ -2358,9 +2413,9 @@ async function removePath(pool, device, path) {
2358
2413
  }
2359
2414
 
2360
2415
  // src/debug/debug.ts
2361
- import { posix as posix3 } from "path";
2362
- import { StringDecoder as StringDecoder2 } from "string_decoder";
2363
- import { createGunzip } from "zlib";
2416
+ import { posix as posix3 } from "node:path";
2417
+ import { StringDecoder as StringDecoder2 } from "node:string_decoder";
2418
+ import { createGunzip } from "node:zlib";
2364
2419
  var requireRoot = (device, what) => {
2365
2420
  if (device.username !== "root") {
2366
2421
  throw new RpcError(DeviceErrorCodes.WrongLogin, `${what} needs a rooted TV (logged in as root).`);
@@ -2603,7 +2658,7 @@ var handlers = {
2603
2658
  );
2604
2659
  }
2605
2660
  session.authed = true;
2606
- return { protocolVersion: PROTOCOL_VERSION, bridgeVersion: BRIDGE_VERSION, platform: platform() };
2661
+ return { protocolVersion: PROTOCOL_VERSION, bridgeVersion: BRIDGE_VERSION, platform: platform(), distribution: distribution() };
2607
2662
  },
2608
2663
  "system.ping": () => ({ now: Date.now() }),
2609
2664
  "device.checkConnection": ({ host }) => checkConnection(host),
@@ -2869,8 +2924,8 @@ async function dispatch(raw, session, ctx) {
2869
2924
  }
2870
2925
 
2871
2926
  // src/http/static.ts
2872
- import { createReadStream, statSync } from "fs";
2873
- import { extname, join as join2, normalize, resolve, sep } from "path";
2927
+ import { createReadStream, statSync } from "node:fs";
2928
+ import { extname, join as join2, normalize, posix as posix4, resolve, sep } from "node:path";
2874
2929
  var MIME = {
2875
2930
  ".html": "text/html; charset=utf-8",
2876
2931
  ".js": "text/javascript; charset=utf-8",
@@ -2898,13 +2953,29 @@ var statOf = (file) => {
2898
2953
  return null;
2899
2954
  }
2900
2955
  };
2956
+ var headersFor = (name, size) => ({
2957
+ "content-type": MIME[extname(name)] ?? "application/octet-stream",
2958
+ "content-length": size,
2959
+ "cache-control": name.endsWith("index.html") ? "no-cache" : "public, max-age=31536000, immutable",
2960
+ ...SECURITY_HEADERS
2961
+ });
2962
+ function serveFiles(files, urlPath, req, res) {
2963
+ let name = posix4.normalize(urlPath).replace(/^\/+/, "");
2964
+ if (!files.has(name)) name = "index.html";
2965
+ const body = files.get(name);
2966
+ if (!body) {
2967
+ res.writeHead(404, { "content-type": "text/plain", ...SECURITY_HEADERS }).end("Web UI missing from this build.");
2968
+ return;
2969
+ }
2970
+ res.writeHead(200, headersFor(name, body.byteLength));
2971
+ res.end(req.method === "HEAD" ? void 0 : body);
2972
+ }
2901
2973
  function serveStatic(root, req, res) {
2902
2974
  try {
2903
2975
  if (req.method !== "GET" && req.method !== "HEAD") {
2904
2976
  res.writeHead(405, { allow: "GET, HEAD", ...SECURITY_HEADERS }).end();
2905
2977
  return;
2906
2978
  }
2907
- const base = resolve(root);
2908
2979
  let urlPath;
2909
2980
  try {
2910
2981
  urlPath = decodeURIComponent((req.url ?? "/").split("?")[0] ?? "/");
@@ -2916,6 +2987,11 @@ function serveStatic(root, req, res) {
2916
2987
  res.writeHead(400, SECURITY_HEADERS).end();
2917
2988
  return;
2918
2989
  }
2990
+ if (typeof root !== "string") {
2991
+ serveFiles(root, urlPath, req, res);
2992
+ return;
2993
+ }
2994
+ const base = resolve(root);
2919
2995
  let file = normalize(join2(base, urlPath));
2920
2996
  if (!file.startsWith(base + sep) && file !== base) {
2921
2997
  res.writeHead(403, SECURITY_HEADERS).end();
@@ -2930,12 +3006,7 @@ function serveStatic(root, req, res) {
2930
3006
  res.writeHead(404, { "content-type": "text/plain", ...SECURITY_HEADERS }).end("Web UI not built. Run `pnpm build`.");
2931
3007
  return;
2932
3008
  }
2933
- res.writeHead(200, {
2934
- "content-type": MIME[extname(file)] ?? "application/octet-stream",
2935
- "content-length": st.size,
2936
- "cache-control": file.endsWith("index.html") ? "no-cache" : "public, max-age=31536000, immutable",
2937
- ...SECURITY_HEADERS
2938
- });
3009
+ res.writeHead(200, headersFor(file, st.size));
2939
3010
  if (req.method === "HEAD") {
2940
3011
  res.end();
2941
3012
  return;
@@ -2948,7 +3019,7 @@ function serveStatic(root, req, res) {
2948
3019
  }
2949
3020
 
2950
3021
  // src/ssh/pool.ts
2951
- import { createHash as createHash4 } from "crypto";
3022
+ import { createHash as createHash4 } from "node:crypto";
2952
3023
  import ssh23 from "ssh2";
2953
3024
  var { Client } = ssh23;
2954
3025
  var IDLE_MS = 12e4;
@@ -3276,7 +3347,7 @@ var SshPool = class {
3276
3347
  };
3277
3348
 
3278
3349
  // src/rpc/uploads.ts
3279
- import { randomUUID as randomUUID3 } from "crypto";
3350
+ import { randomUUID as randomUUID3 } from "node:crypto";
3280
3351
  var UploadBudget = class {
3281
3352
  constructor(limit = MAX_UPLOAD_BYTES) {
3282
3353
  this.limit = limit;
@@ -3379,13 +3450,13 @@ var UploadStore = class {
3379
3450
  };
3380
3451
 
3381
3452
  // src/repo/repo.ts
3382
- import { createHash as createHash5 } from "crypto";
3383
- import { lookup } from "dns/promises";
3384
- import { lookup as dnsLookup } from "dns";
3385
- import { request as httpRequest } from "http";
3386
- import { request as httpsRequest } from "https";
3387
- import { BlockList, isIP } from "net";
3388
- import { Readable } from "stream";
3453
+ import { createHash as createHash5 } from "node:crypto";
3454
+ import { lookup } from "node:dns/promises";
3455
+ import { lookup as dnsLookup } from "node:dns";
3456
+ import { request as httpRequest } from "node:http";
3457
+ import { request as httpsRequest } from "node:https";
3458
+ import { BlockList, isIP } from "node:net";
3459
+ import { Readable } from "node:stream";
3389
3460
  var untraced = (_t, _c, fn) => fn();
3390
3461
  var CACHE_MS = 5 * 6e4;
3391
3462
  var JSON_MAX_BYTES = 16 * 1024 * 1024;
@@ -3944,7 +4015,7 @@ function lookup2(entries, tag, variant) {
3944
4015
  }
3945
4016
 
3946
4017
  // src/shell/shells.ts
3947
- import { randomUUID as randomUUID4 } from "crypto";
4018
+ import { randomUUID as randomUUID4 } from "node:crypto";
3948
4019
  var MAX_SHELLS = 8;
3949
4020
  var FLUSH_MS2 = 8;
3950
4021
  var FLUSH_BYTES = 64 * 1024;
@@ -4224,6 +4295,20 @@ function startServer(config, pool = new SshPool(), repo = new RepoClient(config.
4224
4295
  });
4225
4296
  }
4226
4297
 
4298
+ // src/open.ts
4299
+ import { spawn } from "node:child_process";
4300
+ function openInBrowser(url2) {
4301
+ if (!/^https:\/\/[a-z0-9.-]+\/?$/i.test(url2)) return;
4302
+ const [cmd, args] = process.platform === "darwin" ? ["open", [url2]] : process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url2]] : ["xdg-open", [url2]];
4303
+ try {
4304
+ const child = spawn(cmd, args, { detached: true, stdio: "ignore", windowsHide: true });
4305
+ child.on("error", () => {
4306
+ });
4307
+ child.unref();
4308
+ } catch {
4309
+ }
4310
+ }
4311
+
4227
4312
  // src/cli.ts
4228
4313
  var HELP = `lg-dev-manager-bridge ${BRIDGE_VERSION}
4229
4314
 
@@ -4234,7 +4319,9 @@ Usage: lg-dev-manager-bridge [options]
4234
4319
  --web-root <dir> Serve the web UI from this folder instead of the bundled one (env LGDM_WEB_ROOT)
4235
4320
  --no-ui Don't serve the local page; use https://lg.scoty.uk only
4236
4321
  --reset-token Generate a new pairing token (un-pairs existing browsers)
4322
+ --no-open The app only: don't open ${PUBLIC_WEB_ORIGIN} in the browser on start
4237
4323
  --version Print the version
4324
+ --license Print the licenses of this program and what it includes
4238
4325
  --help Show this help
4239
4326
  `;
4240
4327
  process.on("uncaughtException", (e) => console.error(`[bridge] unexpected error: ${e instanceof Error ? e.message : String(e)}`));
@@ -4250,6 +4337,10 @@ async function main() {
4250
4337
  `);
4251
4338
  return;
4252
4339
  }
4340
+ if (argv.includes("--license")) {
4341
+ process.stdout.write(licenseText());
4342
+ return;
4343
+ }
4253
4344
  const config = parseArgs(argv);
4254
4345
  await startServer(config);
4255
4346
  const shown = config.host;
@@ -4260,8 +4351,19 @@ async function main() {
4260
4351
  console.log(` Pairing token ${config.token}`);
4261
4352
  console.log(` Allowed origins ${config.allowedOrigins.join(", ")}
4262
4353
  `);
4354
+ if (isStandalone()) {
4355
+ console.log(" Keep this window open while you use the app. Close it (or press Ctrl+C) to stop the bridge.\n");
4356
+ if (!argv.includes("--no-open")) openInBrowser(PUBLIC_WEB_ORIGIN);
4357
+ }
4263
4358
  }
4264
4359
  main().catch((e) => {
4265
- console.error(`[bridge] failed to start: ${e.message}`);
4360
+ console.error(
4361
+ e.code === "EADDRINUSE" ? `[bridge] failed to start: port ${/:(\d+)$/.exec(e.message)?.[1] ?? ""} is already in use. Is the bridge already running in another window? Close that one, or start this one with --port <another port>.` : `[bridge] failed to start: ${e.message}`
4362
+ );
4363
+ if (isStandalone() && process.stdin.isTTY) {
4364
+ console.error("\nPress Enter to close this window.");
4365
+ process.stdin.once("data", () => process.exit(1));
4366
+ return;
4367
+ }
4266
4368
  process.exit(1);
4267
4369
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lg-dev-manager-bridge",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Local bridge for LG Dev Manager: lets the web app at lg.scoty.uk (or its local copy) manage LG webOS TVs over SSH",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Scoty (https://github.com/Scoty)",
@@ -45,12 +45,14 @@
45
45
  "@types/node": "^22.10.0",
46
46
  "@types/ssh2": "^1.15.6",
47
47
  "@types/ws": "^8.5.0",
48
+ "esbuild": "^0.27.7",
49
+ "postject": "1.0.0-alpha.6",
48
50
  "tsup": "^8.5.0",
49
51
  "tsx": "^4.20.0",
50
52
  "typescript": "^5.9.0",
51
53
  "vitest": "^4.1.11",
52
- "@lgdm/mock-tv": "1.0.1",
53
- "@lgdm/protocol": "1.0.1"
54
+ "@lgdm/mock-tv": "1.0.3",
55
+ "@lgdm/protocol": "1.0.3"
54
56
  },
55
57
  "scripts": {
56
58
  "dev": "tsx watch src/cli.ts --dev",