lg-dev-manager-bridge 1.0.2 → 1.0.4

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
@@ -188,6 +188,12 @@ pnpm app # the standalone app for this computer → apps/bridge/relea
188
188
  `tools/mock-tv` fakes webOS TVs (SSH, luna-send, key server, logs) so everything can be tested without a TV.
189
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.
190
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
+
191
197
  ## 📄 License
192
198
 
193
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 as existsSync2, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
6
- import { fileURLToPath as fileURLToPath2 } 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";
@@ -124,7 +124,9 @@ var AppsErrorCodes = {
124
124
  FileTooLarge: "file_too_large",
125
125
  TransferFailed: "transfer_failed",
126
126
  /** Another app with this id is installed outside the developer partition (e.g. from the LG Content Store). */
127
- Conflict: "app_conflict"
127
+ Conflict: "app_conflict",
128
+ /** The TV is in standby: apps can be installed, updated and removed, but a launch would show nothing. */
129
+ TvOff: "tv_off"
128
130
  };
129
131
 
130
132
  // ../../packages/protocol/src/console.ts
@@ -335,6 +337,7 @@ var LitefinErrorCodes = {
335
337
  var PROTOCOL_VERSION = 8;
336
338
  var base64 = z9.string().regex(/^[A-Za-z0-9+/]*={0,2}$/, "Not base64");
337
339
  var DEFAULT_BRIDGE_PORT = 5199;
340
+ var TvPowerState = z9.enum(["on", "screenOff", "standby", "unknown"]);
338
341
  var Methods = {
339
342
  /** First message on every connection. Authenticates with the pairing token. */
340
343
  "system.hello": {
@@ -404,6 +407,15 @@ var Methods = {
404
407
  socName: z9.string().optional()
405
408
  })
406
409
  },
410
+ /**
411
+ * Is the TV on? `com.webos.service.tvpower/power/getPowerState`: `on` (also with the screen saver), `screenOff` (on,
412
+ * picture turned off), `standby` (looks off — the system still runs, so installs work, but nothing can be shown),
413
+ * `unknown` (the TV didn't say; treat as on). `raw` is the TV's own state text.
414
+ */
415
+ "device.power": {
416
+ params: z9.object({ device: DeviceTarget }),
417
+ result: z9.object({ state: TvPowerState, raw: z9.string().optional() })
418
+ },
407
419
  /** `df` of a folder (KiB; default the developer partition). Null if the TV gave no usable answer. */
408
420
  "device.storage": {
409
421
  params: z9.object({ device: DeviceTarget, path: RemotePath.optional() }),
@@ -743,9 +755,9 @@ var REPO_URL = "https://github.com/Scoty/lg-dev-manager";
743
755
  var RELEASES_URL = `${REPO_URL}/releases/latest`;
744
756
 
745
757
  // src/standalone.ts
746
- import { existsSync } from "fs";
747
- import { fileURLToPath } from "url";
748
- import * as sea from "sea";
758
+ import { existsSync } from "node:fs";
759
+ import { fileURLToPath } from "node:url";
760
+ import * as sea from "node:sea";
749
761
  function isStandalone() {
750
762
  try {
751
763
  return sea.isSea();
@@ -868,7 +880,7 @@ function parseArgs(argv) {
868
880
  }
869
881
 
870
882
  // src/server.ts
871
- import { createServer } from "http";
883
+ import { createServer } from "node:http";
872
884
  import { WebSocketServer } from "ws";
873
885
 
874
886
  // src/rpc/errors.ts
@@ -886,16 +898,16 @@ var RpcError = class extends Error {
886
898
  };
887
899
 
888
900
  // src/rpc/handlers.ts
889
- import { StringDecoder as StringDecoder3 } from "string_decoder";
890
- import { timingSafeEqual } from "crypto";
891
- import { platform } from "os";
892
- import { randomUUID as randomUUID2 } from "crypto";
901
+ import { StringDecoder as StringDecoder3 } from "node:string_decoder";
902
+ import { timingSafeEqual } from "node:crypto";
903
+ import { platform } from "node:os";
904
+ import { randomUUID as randomUUID2 } from "node:crypto";
893
905
 
894
906
  // src/version.ts
895
- var BRIDGE_VERSION = "1.0.2";
907
+ var BRIDGE_VERSION = "1.0.4";
896
908
 
897
909
  // src/ssh/logged.ts
898
- import { randomUUID } from "crypto";
910
+ import { randomUUID } from "node:crypto";
899
911
  var MAX_LOG_OUTPUT = 8 * 1024;
900
912
  var targetOf = (t) => `${t.username}@${t.host}:${t.port}`;
901
913
  function summarize(stdout, stderr) {
@@ -1000,16 +1012,16 @@ var LoggedSsh = class {
1000
1012
  };
1001
1013
 
1002
1014
  // src/devices/scan.ts
1003
- import { createSocket } from "dgram";
1004
- import { request } from "http";
1005
- import { networkInterfaces } from "os";
1015
+ import { createSocket } from "node:dgram";
1016
+ import { request } from "node:http";
1017
+ import { networkInterfaces } from "node:os";
1006
1018
 
1007
1019
  // src/devices/ports.ts
1008
- import { connect as connect2 } from "net";
1020
+ import { connect as connect2 } from "node:net";
1009
1021
 
1010
1022
  // src/devices/webos.ts
1011
1023
  import WebSocket from "ws";
1012
- import { connect } from "net";
1024
+ import { connect } from "node:net";
1013
1025
  var PROBE_TIMEOUT_MS = 1500;
1014
1026
  function probeSsap(host, port, secure, timeoutMs = PROBE_TIMEOUT_MS) {
1015
1027
  return new Promise((resolve2) => {
@@ -1273,7 +1285,7 @@ async function scanNetwork(opts = {}) {
1273
1285
  }
1274
1286
 
1275
1287
  // src/ssh/luna.ts
1276
- import { StringDecoder } from "string_decoder";
1288
+ import { StringDecoder } from "node:string_decoder";
1277
1289
  function shellQuote(value) {
1278
1290
  return value.split("'").map((s) => `'${s}'`).join("\\'");
1279
1291
  }
@@ -1394,7 +1406,7 @@ async function lunaSubscribe(pool, device, uri, params, onMessage, opts = {}) {
1394
1406
  }
1395
1407
 
1396
1408
  // src/devices/keys.ts
1397
- import { createHash } from "crypto";
1409
+ import { createHash } from "node:crypto";
1398
1410
  import ssh2 from "ssh2";
1399
1411
  var { utils } = ssh2;
1400
1412
  function verifyKey(privateKey, passphrase) {
@@ -1415,7 +1427,7 @@ function verifyKey(privateKey, passphrase) {
1415
1427
  }
1416
1428
 
1417
1429
  // src/devices/keyserver.ts
1418
- import { request as request2 } from "http";
1430
+ import { request as request2 } from "node:http";
1419
1431
  var MAX_KEY_RESPONSE = 65536;
1420
1432
  var TIMEOUT_MS = 1e4;
1421
1433
  var MAX_REDIRECTS = 5;
@@ -1495,7 +1507,7 @@ async function fetchKey(host, port = KEY_SERVER_PORT) {
1495
1507
  }
1496
1508
 
1497
1509
  // src/devices/info.ts
1498
- import { createHash as createHash2 } from "crypto";
1510
+ import { createHash as createHash2 } from "node:crypto";
1499
1511
  import ssh22 from "ssh2";
1500
1512
 
1501
1513
  // src/ssh/transfer.ts
@@ -1777,9 +1789,42 @@ async function renewDevMode(pool, device) {
1777
1789
  await lunaCall(pool, device, "luna://com.webos.applicationManager/launch", { id: "com.palmdts.devmode", subscribe: false, params: { extend: true } }, true);
1778
1790
  }
1779
1791
 
1792
+ // src/devices/power.ts
1793
+ var GET_POWER_STATE = "luna://com.webos.service.tvpower/power/getPowerState";
1794
+ var TURN_ON_SCREEN = "luna://com.webos.service.tvpower/power/turnOnScreen";
1795
+ function powerStateOf(msg) {
1796
+ const state = typeof msg.state === "string" ? msg.state : "";
1797
+ const processing = typeof msg.processing === "string" ? msg.processing : "";
1798
+ if (/power ?off|suspend|standby/i.test(processing)) return "standby";
1799
+ if (state === "Active" || state === "Screen Saver") return "on";
1800
+ if (state === "Screen Off") return "screenOff";
1801
+ if (/standby|suspend|power ?off/i.test(state)) return "standby";
1802
+ return "unknown";
1803
+ }
1804
+ async function tvPower(pool, device) {
1805
+ for (const pub of device.username === "root" ? [true, false] : [true]) {
1806
+ try {
1807
+ const msg = await lunaCall(pool, device, GET_POWER_STATE, {}, pub);
1808
+ return { state: powerStateOf(msg), raw: [msg.state, msg.processing].filter((v) => typeof v === "string").join(" \xB7 ") || void 0 };
1809
+ } catch {
1810
+ }
1811
+ }
1812
+ return { state: "unknown" };
1813
+ }
1814
+ async function turnOnScreen(pool, device) {
1815
+ for (const pub of device.username === "root" ? [true, false] : [true]) {
1816
+ try {
1817
+ await lunaCall(pool, device, TURN_ON_SCREEN, {}, pub);
1818
+ return true;
1819
+ } catch {
1820
+ }
1821
+ }
1822
+ return false;
1823
+ }
1824
+
1780
1825
  // src/apps/apps.ts
1781
- import { createHash as createHash3 } from "crypto";
1782
- import { posix } from "path";
1826
+ import { createHash as createHash3 } from "node:crypto";
1827
+ import { posix } from "node:path";
1783
1828
 
1784
1829
  // src/ssh/serve.ts
1785
1830
  var HEADER_LIMIT = 16 * 1024;
@@ -1868,7 +1913,7 @@ Connection: close\r
1868
1913
  }
1869
1914
 
1870
1915
  // src/apps/ipk.ts
1871
- import { gunzipSync } from "zlib";
1916
+ import { gunzipSync } from "node:zlib";
1872
1917
  function readIpkControl(ipk) {
1873
1918
  try {
1874
1919
  const control = arMember(ipk, /^control\.tar\.gz\/?$/);
@@ -1942,7 +1987,16 @@ async function listApps(pool, device) {
1942
1987
  });
1943
1988
  }
1944
1989
  async function launchApp(pool, device, id, params) {
1990
+ const power = await tvPower(pool, device);
1991
+ if (power.state === "standby") {
1992
+ throw new RpcError(
1993
+ AppsErrorCodes.TvOff,
1994
+ "The TV is off (in standby). Turn it on with the remote, then launch the app. Installing, updating and removing apps work while it\u2019s off.",
1995
+ power.raw ? `getPowerState: ${power.raw}` : void 0
1996
+ );
1997
+ }
1945
1998
  await lunaCall(pool, device, "luna://com.webos.applicationManager/launch", { id, subscribe: false, params }, true);
1999
+ if (power.state === "screenOff") await turnOnScreen(pool, device);
1946
2000
  }
1947
2001
  async function appIcon(pool, device, path) {
1948
2002
  const mime = ICON_TYPES[posix.extname(path).toLowerCase()];
@@ -2174,7 +2228,7 @@ async function installDownloadable(pool, device, pkg, download, progress) {
2174
2228
  }
2175
2229
 
2176
2230
  // src/files/files.ts
2177
- import { posix as posix2 } from "path";
2231
+ import { posix as posix2 } from "node:path";
2178
2232
  var S_IFMT = 61440;
2179
2233
  var TYPES = {
2180
2234
  16384: "d",
@@ -2413,9 +2467,9 @@ async function removePath(pool, device, path) {
2413
2467
  }
2414
2468
 
2415
2469
  // src/debug/debug.ts
2416
- import { posix as posix3 } from "path";
2417
- import { StringDecoder as StringDecoder2 } from "string_decoder";
2418
- import { createGunzip } from "zlib";
2470
+ import { posix as posix3 } from "node:path";
2471
+ import { StringDecoder as StringDecoder2 } from "node:string_decoder";
2472
+ import { createGunzip } from "node:zlib";
2419
2473
  var requireRoot = (device, what) => {
2420
2474
  if (device.username !== "root") {
2421
2475
  throw new RpcError(DeviceErrorCodes.WrongLogin, `${what} needs a rooted TV (logged in as root).`);
@@ -2681,6 +2735,8 @@ var handlers = {
2681
2735
  },
2682
2736
  "device.info": ({ device, quiet }, session, ctx) => deviceInfo(sshFor(session, ctx, quiet), device),
2683
2737
  "device.storage": ({ device, path }, session, ctx) => storageInfo(sshFor(session, ctx), device, path),
2738
+ // Polled by the apps pages: a background read in the console.
2739
+ "device.power": ({ device }, session, ctx) => tvPower(sshFor(session, ctx, true), device),
2684
2740
  "device.generateKey": ({ comment }) => generateKey(comment),
2685
2741
  // The token is read through the plain pool so it never appears in the console; the LG request is traced
2686
2742
  // with the token blanked out.
@@ -2924,8 +2980,8 @@ async function dispatch(raw, session, ctx) {
2924
2980
  }
2925
2981
 
2926
2982
  // src/http/static.ts
2927
- import { createReadStream, statSync } from "fs";
2928
- import { extname, join as join2, normalize, posix as posix4, resolve, sep } from "path";
2983
+ import { createReadStream, statSync } from "node:fs";
2984
+ import { extname, join as join2, normalize, posix as posix4, resolve, sep } from "node:path";
2929
2985
  var MIME = {
2930
2986
  ".html": "text/html; charset=utf-8",
2931
2987
  ".js": "text/javascript; charset=utf-8",
@@ -3019,7 +3075,7 @@ function serveStatic(root, req, res) {
3019
3075
  }
3020
3076
 
3021
3077
  // src/ssh/pool.ts
3022
- import { createHash as createHash4 } from "crypto";
3078
+ import { createHash as createHash4 } from "node:crypto";
3023
3079
  import ssh23 from "ssh2";
3024
3080
  var { Client } = ssh23;
3025
3081
  var IDLE_MS = 12e4;
@@ -3347,7 +3403,7 @@ var SshPool = class {
3347
3403
  };
3348
3404
 
3349
3405
  // src/rpc/uploads.ts
3350
- import { randomUUID as randomUUID3 } from "crypto";
3406
+ import { randomUUID as randomUUID3 } from "node:crypto";
3351
3407
  var UploadBudget = class {
3352
3408
  constructor(limit = MAX_UPLOAD_BYTES) {
3353
3409
  this.limit = limit;
@@ -3450,13 +3506,13 @@ var UploadStore = class {
3450
3506
  };
3451
3507
 
3452
3508
  // src/repo/repo.ts
3453
- import { createHash as createHash5 } from "crypto";
3454
- import { lookup } from "dns/promises";
3455
- import { lookup as dnsLookup } from "dns";
3456
- import { request as httpRequest } from "http";
3457
- import { request as httpsRequest } from "https";
3458
- import { BlockList, isIP } from "net";
3459
- import { Readable } from "stream";
3509
+ import { createHash as createHash5 } from "node:crypto";
3510
+ import { lookup } from "node:dns/promises";
3511
+ import { lookup as dnsLookup } from "node:dns";
3512
+ import { request as httpRequest } from "node:http";
3513
+ import { request as httpsRequest } from "node:https";
3514
+ import { BlockList, isIP } from "node:net";
3515
+ import { Readable } from "node:stream";
3460
3516
  var untraced = (_t, _c, fn) => fn();
3461
3517
  var CACHE_MS = 5 * 6e4;
3462
3518
  var JSON_MAX_BYTES = 16 * 1024 * 1024;
@@ -4015,7 +4071,7 @@ function lookup2(entries, tag, variant) {
4015
4071
  }
4016
4072
 
4017
4073
  // src/shell/shells.ts
4018
- import { randomUUID as randomUUID4 } from "crypto";
4074
+ import { randomUUID as randomUUID4 } from "node:crypto";
4019
4075
  var MAX_SHELLS = 8;
4020
4076
  var FLUSH_MS2 = 8;
4021
4077
  var FLUSH_BYTES = 64 * 1024;
@@ -4296,7 +4352,7 @@ function startServer(config, pool = new SshPool(), repo = new RepoClient(config.
4296
4352
  }
4297
4353
 
4298
4354
  // src/open.ts
4299
- import { spawn } from "child_process";
4355
+ import { spawn } from "node:child_process";
4300
4356
  function openInBrowser(url2) {
4301
4357
  if (!/^https:\/\/[a-z0-9.-]+\/?$/i.test(url2)) return;
4302
4358
  const [cmd, args] = process.platform === "darwin" ? ["open", [url2]] : process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url2]] : ["xdg-open", [url2]];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lg-dev-manager-bridge",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
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)",
@@ -51,8 +51,8 @@
51
51
  "tsx": "^4.20.0",
52
52
  "typescript": "^5.9.0",
53
53
  "vitest": "^4.1.11",
54
- "@lgdm/mock-tv": "1.0.2",
55
- "@lgdm/protocol": "1.0.2"
54
+ "@lgdm/mock-tv": "1.0.4",
55
+ "@lgdm/protocol": "1.0.4"
56
56
  },
57
57
  "scripts": {
58
58
  "dev": "tsx watch src/cli.ts --dev",