lg-dev-manager-bridge 1.0.1 → 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.
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,6 +182,7 @@ 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.
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.1";
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.1",
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)",
@@ -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.2",
55
+ "@lgdm/protocol": "1.0.2"
54
56
  },
55
57
  "scripts": {
56
58
  "dev": "tsx watch src/cli.ts --dev",