lg-dev-manager-bridge 1.0.0 → 1.0.2

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 (65) hide show
  1. package/README.md +176 -18
  2. package/dist/cli.js +118 -16
  3. package/package.json +6 -4
  4. package/web/assets/{index-DmYG3adw.js → index-CWU_4o5k.js} +23 -23
  5. package/web/assets/{index-CTz5i-Zo.css → index-Cc--HRgL.css} +1 -1
  6. package/web/index.html +2 -2
  7. package/web/assets/inter-cyrillic-400-normal-HOLc17fK.woff +0 -0
  8. package/web/assets/inter-cyrillic-500-normal-CxZf_p3X.woff +0 -0
  9. package/web/assets/inter-cyrillic-600-normal-4D_pXhcN.woff +0 -0
  10. package/web/assets/inter-cyrillic-700-normal-DrXBdSj3.woff +0 -0
  11. package/web/assets/inter-cyrillic-ext-400-normal-DQukG94-.woff +0 -0
  12. package/web/assets/inter-cyrillic-ext-500-normal-BmqWE9Dz.woff +0 -0
  13. package/web/assets/inter-cyrillic-ext-600-normal-Bcila6Z-.woff +0 -0
  14. package/web/assets/inter-cyrillic-ext-700-normal-LO58E6JB.woff +0 -0
  15. package/web/assets/inter-greek-400-normal-q2sYcFCs.woff +0 -0
  16. package/web/assets/inter-greek-500-normal-Xzm54t5V.woff +0 -0
  17. package/web/assets/inter-greek-600-normal-BZpKdvQh.woff +0 -0
  18. package/web/assets/inter-greek-700-normal-BUv2fZ6O.woff +0 -0
  19. package/web/assets/inter-greek-ext-400-normal-KugGGMne.woff +0 -0
  20. package/web/assets/inter-greek-ext-500-normal-2j5mBUwD.woff +0 -0
  21. package/web/assets/inter-greek-ext-600-normal-B8X0CLgF.woff +0 -0
  22. package/web/assets/inter-greek-ext-700-normal-BoQ6DsYi.woff +0 -0
  23. package/web/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
  24. package/web/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
  25. package/web/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
  26. package/web/assets/inter-latin-700-normal-BLAVimhd.woff +0 -0
  27. package/web/assets/inter-latin-ext-400-normal-77YHD8bZ.woff +0 -0
  28. package/web/assets/inter-latin-ext-500-normal-BxGbmqWO.woff +0 -0
  29. package/web/assets/inter-latin-ext-600-normal-CIVaiw4L.woff +0 -0
  30. package/web/assets/inter-latin-ext-700-normal-TidjK2hL.woff +0 -0
  31. package/web/assets/inter-tight-cyrillic-500-normal-CRXauZSq.woff +0 -0
  32. package/web/assets/inter-tight-cyrillic-600-normal-BeTasGgk.woff +0 -0
  33. package/web/assets/inter-tight-cyrillic-700-normal-DDZiLR3d.woff +0 -0
  34. package/web/assets/inter-tight-cyrillic-ext-500-normal-Dy1ZB9JY.woff +0 -0
  35. package/web/assets/inter-tight-cyrillic-ext-600-normal-_3ga77Eo.woff +0 -0
  36. package/web/assets/inter-tight-cyrillic-ext-700-normal-BFcoPt71.woff +0 -0
  37. package/web/assets/inter-tight-greek-500-normal-Ca-cFdRH.woff +0 -0
  38. package/web/assets/inter-tight-greek-600-normal-67HAlfW-.woff +0 -0
  39. package/web/assets/inter-tight-greek-700-normal-DU8XoIeX.woff +0 -0
  40. package/web/assets/inter-tight-greek-ext-500-normal-BEGGcu3x.woff +0 -0
  41. package/web/assets/inter-tight-greek-ext-600-normal-5gKW7MDU.woff +0 -0
  42. package/web/assets/inter-tight-greek-ext-700-normal-mW3dmpoD.woff +0 -0
  43. package/web/assets/inter-tight-latin-500-normal-pobXraBK.woff +0 -0
  44. package/web/assets/inter-tight-latin-600-normal-D7bG6gX1.woff +0 -0
  45. package/web/assets/inter-tight-latin-700-normal-DvYAVZQd.woff +0 -0
  46. package/web/assets/inter-tight-latin-ext-500-normal-_hLDZz_3.woff +0 -0
  47. package/web/assets/inter-tight-latin-ext-600-normal-BvYdOF2O.woff +0 -0
  48. package/web/assets/inter-tight-latin-ext-700-normal-CrlzIQ10.woff +0 -0
  49. package/web/assets/inter-tight-vietnamese-500-normal-CO4EMNTO.woff +0 -0
  50. package/web/assets/inter-tight-vietnamese-600-normal-BCXZ1BbA.woff +0 -0
  51. package/web/assets/inter-tight-vietnamese-700-normal-DwHBAXeT.woff +0 -0
  52. package/web/assets/inter-vietnamese-400-normal-Bbgyi5SW.woff +0 -0
  53. package/web/assets/inter-vietnamese-500-normal-mJboJaSs.woff +0 -0
  54. package/web/assets/inter-vietnamese-600-normal-BuLX-rYi.woff +0 -0
  55. package/web/assets/inter-vietnamese-700-normal-BZaoP0fm.woff +0 -0
  56. package/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  57. package/web/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
  58. package/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  59. package/web/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
  60. package/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  61. package/web/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
  62. package/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  63. package/web/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
  64. package/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  65. package/web/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
package/README.md CHANGED
@@ -1,39 +1,197 @@
1
- # lg-dev-manager-bridge
1
+ # 📺 LG Dev Manager
2
2
 
3
- The local bridge for **[LG Dev Manager](https://lg.scoty.uk)** — manage LG webOS TVs (rooted or in Developer
4
- Mode) from your browser: install and update apps, the webOS Homebrew repository, files, a shell, logs and more.
3
+ Manage LG webOS TVs from your browser — rooted (Homebrew Channel) or in Developer Mode. Install apps from the
4
+ webOS Homebrew repository or from an IPK file, update them, install any build of [Litefin](#litefin-in-one-click)
5
+ in one click, browse the TV's files, open a shell, take screenshots, renew the Dev Mode session and dig into logs and
6
+ the luna bus.
5
7
 
6
- Browsers can't speak SSH, so this small program does it for the web app, on your own computer.
8
+ 🌐 **Open it:** https://lg.scoty.uk — then run the bridge on your computer (one command, below).
9
+
10
+ A web rebuild of [webosbrew/dev-manager-desktop](https://github.com/webosbrew/dev-manager-desktop), styled after
11
+ [Adminator](https://github.com/puikinsh/adminator-admin-dashboard).
12
+
13
+ > ⚠️ **Note:** LG Dev Manager is an independent, community project. It is not affiliated with, endorsed by or supported
14
+ > by LG Electronics. LG and webOS are trademarks of LG Electronics. Rooting a TV or using Developer Mode is at your own
15
+ > risk.
16
+
17
+ ### 🎬 Add a TV, install an app and update it
18
+
19
+ ![Adding the Living Room TV with the wizard, installing Moonfin 2.8.2 from an IPK file and updating it to 2.9.0 from the Homebrew repository](https://raw.githubusercontent.com/Scoty/lg-dev-manager/main/docs/demo-basic.webp)
20
+
21
+ ### 🐞 Debug tools on a rooted TV
22
+
23
+ ![Switching to the Bedroom TV and going through the system log, log levels, kernel log, crash reports and the luna monitor](https://raw.githubusercontent.com/Scoty/lg-dev-manager/main/docs/demo-advanced.webp)
24
+
25
+ ## 🚀 Get started
26
+
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
+
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) |
35
+
36
+ ### 🖥️ Option 1: the app — no Node.js needed
37
+
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:
7
61
 
8
62
  ```bash
9
63
  npx lg-dev-manager-bridge@latest
10
64
  ```
11
65
 
12
- Then open **https://lg.scoty.uk** (or the local copy at **http://localhost:5199**), go to **Bridge** and paste the
13
- pairing token it printed. Needs Node.js 22 or newer.
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
14
69
 
15
- ## Safety
70
+ 1. Open **https://lg.scoty.uk**, or the local page the bridge serves at **http://localhost:5199** — they are the
71
+ same app. On the **Bridge** page, paste the token.
72
+ 2. On **Devices → Add a TV**, pick your TV from the scan. Rooted TVs log in as root (Homebrew Channel's SSH server);
73
+ other TVs use the Developer Mode app and its passphrase.
16
74
 
17
- - Listens on `127.0.0.1` only; other devices on your network can't reach it.
18
- - Accepts connections only from the app's own pages, and only with the pairing token (kept in
19
- `~/.lg-dev-manager/bridge.json`, readable only by you).
20
- - Stores nothing else: your TVs' addresses, keys and passwords stay in your browser and are sent with each request.
21
- - Offers fixed operations (list apps, install, read a log…), never a general network tunnel.
75
+ 🍎 On macOS, the first time the bridge looks for your TV, the system asks whether your terminal may find devices on the
76
+ local network — choose **Allow**. 📱 Phones and tablets aren't supported: the bridge has to run on the same computer
77
+ as the browser.
22
78
 
23
- ## Options
79
+ Which page to use is up to you. 🌐 **lg.scoty.uk** always has the latest version and needs nothing installed besides the
80
+ bridge. 🏠 **http://localhost:5199** is served from your computer by the bridge itself, so it works even when the
81
+ website can't be reached. Each keeps its own list of TVs (browsers store them per site); use **Export backup** / **Import backup** on
82
+ the Devices page to move them.
83
+
84
+ <a id="how-it-works"></a>
85
+
86
+ ## 🔧 How it works
87
+
88
+ Browsers can't open SSH connections, so the app comes in two parts:
89
+
90
+ ```
91
+ Browser (lg.scoty.uk or localhost:5199) Your computer Your TV
92
+ ┌──────────────────────────────────┐ WebSocket ┌────────────────────┐ SSH / SFTP ┌──────────────┐
93
+ │ Web app — your TVs, keys and │ ────────────▶ │ Bridge │ ─────────────▶ │ webOS │
94
+ │ passwords are stored only here │ 127.0.0.1 │ (app or npx) │ │ (port 22 or │
95
+ └──────────────────────────────────┘ + token └────────────────────┘ │ 9922) │
96
+ └──────────────┘
97
+ ```
98
+
99
+ - 🖥️ **The web app** is a static site. Saved TVs — names, addresses, keys and passwords — live only in your browser's
100
+ storage. They are never sent to the website; each request sends them to your own bridge, and only to it.
101
+ - 🌉 **The bridge** is a small Node.js program that does the SSH, SFTP and HTTP work the browser can't. It listens on
102
+ `127.0.0.1` only (nothing else on your network can reach it), accepts only the app's own pages, needs the pairing
103
+ token, and keeps nothing on disk except that token. It offers fixed operations — list apps, install, read a log —
104
+ never a general "connect anywhere" tunnel.
105
+ - 📦 **Homebrew repository and Litefin releases** are fetched by the bridge, so the site itself talks to no one but your
106
+ bridge. Downloads are checked against their published SHA-256 whenever one is published.
107
+
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.
111
+
112
+ ## ✨ What it does
113
+
114
+ - 📡 **Devices** — network scan, add rooted or Developer Mode TVs (key fetched from the TV's key server, or a new key
115
+ made for it), edit, test, export / import.
116
+ - 📦 **Apps** — installed apps with update badges; install IPK files (drag and drop); the webOS **Homebrew repository**
117
+ with search, details, install and update; the **Litefin repo** with every webOS build of the latest releases
118
+ (see [below](#litefin-in-one-click)).
119
+ - 📁 **Files** — browse, upload, download, preview, rename, delete over SFTP.
120
+ - 💻 **Terminal** — full shell in tabs, plus a simple mode for TVs without a terminal.
121
+ - ℹ️ **Device info** — model, webOS and firmware, Homebrew Channel status and update, screenshots, Dev Mode session
122
+ countdown and renewal.
123
+ - 🐞 **Debug** — live system log and kernel log, PmLog levels, crash reports, luna bus monitor (rooted TVs).
124
+ - 🧾 **Console** — every command the bridge runs on the TV, live, with the option to type your own.
125
+
126
+ 🌗 Light and dark themes, a command palette (⌘K / Ctrl K), and no analytics or tracking.
127
+
128
+ <a id="litefin-in-one-click"></a>
129
+
130
+ ## 🍿 Litefin in one click
131
+
132
+ This app started with one itch. [Litefin](https://github.com/MoazSalem/litefin), a lightweight Jellyfin client for
133
+ LG TVs, publishes a separate IPK for each generation of webOS (Modern, Normal, Legacy, Ultra Legacy) on every
134
+ release, and the Homebrew repository carries only one of them. Updating used to mean opening the GitHub releases,
135
+ downloading the right IPK by hand and installing it through the desktop webOS Dev Manager.
136
+
137
+ 👉 **Apps → Litefin repo** does all of that in one click:
138
+
139
+ - It lists the last five Litefin releases with every webOS build side by side, read straight from GitHub by the
140
+ bridge (pre-releases are skipped).
141
+ - It suggests the right build for the active TV from its webOS version, and shows which version is installed and
142
+ whether a newer one is out.
143
+ - **Install** downloads that build, checks it against GitHub's SHA-256 when one is published, and installs it on the TV
144
+ — through Homebrew Channel on a rooted TV, or the Developer Mode installer otherwise. All builds are the same app,
145
+ so switching builds or versions simply replaces it.
146
+
147
+ <a id="run-from-source"></a>
148
+
149
+ ## 🛠️ Run the bridge from the source code
150
+
151
+ The third way: run it from a clone. It's the same bridge, and it serves the local page too:
152
+
153
+ ```bash
154
+ git clone https://github.com/Scoty/lg-dev-manager.git
155
+ cd lg-dev-manager
156
+ corepack enable # once, to get pnpm
157
+ pnpm install
158
+ pnpm build
159
+ pnpm bridge # same as: node apps/bridge/dist/cli.js
160
+ ```
161
+
162
+ This needs Node.js 22.12 or newer, the same as npx (the project itself is developed on Node 24, see `.nvmrc`).
163
+ To update: `git pull`, `pnpm install`, `pnpm build`, then `pnpm bridge`.
164
+
165
+ Bridge options (`--help`; with the app, run it from a terminal: `lg-dev-manager-bridge --help`):
24
166
 
25
167
  | Option | |
26
168
  |---|---|
27
169
  | `--port <n>` | Port to listen on (default 5199) |
28
170
  | `--no-ui` | Don't serve the local page |
29
- | `--web-root <dir>` | Serve the web app from another folder |
30
171
  | `--allow-origin <url>` | Also accept another origin, e.g. a self-hosted copy of the web app |
31
172
  | `--reset-token` | Make a new pairing token (un-pairs every browser) |
32
- | `--version`, `--help` | |
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 |
175
+
176
+ ## 🧪 Development
177
+
178
+ ```bash
179
+ pnpm install
180
+ pnpm dev # web app (Vite, http://localhost:5173) + bridge with --dev
181
+ pnpm test # unit and integration tests (against a mock TV)
182
+ pnpm e2e # Playwright: built app + real bridge + mock TVs + fake Homebrew repo
183
+ pnpm dev:rig # the same rig to try by hand: http://127.0.0.1:5299, token e2e-token
184
+ pnpm lint && pnpm typecheck
185
+ pnpm app # the standalone app for this computer → apps/bridge/release (after pnpm build)
186
+ ```
187
+
188
+ `tools/mock-tv` fakes webOS TVs (SSH, luna-send, key server, logs) so everything can be tested without a TV.
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.
33
190
 
34
- Source, documentation and the from-source instructions: https://github.com/Scoty/lg-dev-manager
191
+ ## 📄 License
35
192
 
36
- Apache-2.0 — see LICENSE and NOTICE (credits the original
37
- [dev-manager-desktop](https://github.com/webosbrew/dev-manager-desktop) and Adminator).
193
+ [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
194
+ as you keep the license and the [NOTICE](https://github.com/Scoty/lg-dev-manager/blob/main/NOTICE) file that credits its authors — this project, the original
195
+ [dev-manager-desktop](https://github.com/webosbrew/dev-manager-desktop) by the webOS Homebrew team, and Adminator.
38
196
 
39
197
  Not affiliated with or endorsed by LG Electronics. LG and webOS are trademarks of LG Electronics.
package/dist/cli.js CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  // src/config.ts
4
4
  import { randomBytes } from "crypto";
5
- import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
6
- import { fileURLToPath } from "url";
5
+ import { chmodSync, existsSync as existsSync2, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
6
+ import { fileURLToPath as fileURLToPath2 } from "url";
7
7
  import { homedir } from "os";
8
8
  import { join } from "path";
9
9
 
@@ -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 "fs";
747
+ import { fileURLToPath } from "url";
748
+ import * as sea from "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
  }
@@ -837,7 +892,7 @@ import { platform } from "os";
837
892
  import { randomUUID as randomUUID2 } from "crypto";
838
893
 
839
894
  // src/version.ts
840
- var BRIDGE_VERSION = "1.0.0";
895
+ var BRIDGE_VERSION = "1.0.2";
841
896
 
842
897
  // src/ssh/logged.ts
843
898
  import { randomUUID } from "crypto";
@@ -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),
@@ -2870,7 +2925,7 @@ async function dispatch(raw, session, ctx) {
2870
2925
 
2871
2926
  // src/http/static.ts
2872
2927
  import { createReadStream, statSync } from "fs";
2873
- import { extname, join as join2, normalize, resolve, sep } from "path";
2928
+ import { extname, join as join2, normalize, posix as posix4, resolve, sep } from "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;
@@ -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 "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.0",
3
+ "version": "1.0.2",
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)",
@@ -34,7 +34,7 @@
34
34
  "README.md"
35
35
  ],
36
36
  "engines": {
37
- "node": ">=22"
37
+ "node": ">=22.12"
38
38
  },
39
39
  "dependencies": {
40
40
  "ssh2": "^1.17.0",
@@ -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.0",
53
- "@lgdm/protocol": "1.0.0"
54
+ "@lgdm/mock-tv": "1.0.2",
55
+ "@lgdm/protocol": "1.0.2"
54
56
  },
55
57
  "scripts": {
56
58
  "dev": "tsx watch src/cli.ts --dev",